From 5ed2968c5d3e62f06b6355c33d3cb17e9828db26 Mon Sep 17 00:00:00 2001
From: Stanley Yip <stanley_yip@brown.edu>
Date: Tue, 8 Oct 2019 16:50:14 -0400
Subject: basic interactions, not very robust

---
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 218 ++++++++++++++++-----
 1 file changed, 169 insertions(+), 49 deletions(-)

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

diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 4bdede48a..314ce5cdb 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -38,6 +38,7 @@ import { CollectionFreeFormRemoteCursors } from "./CollectionFreeFormRemoteCurso
 import "./CollectionFreeFormView.scss";
 import { MarqueeView } from "./MarqueeView";
 import React = require("react");
+import { InteractionUtils } from "../../../util/InteractionUtils";
 
 library.add(faEye as any, faTable, faPaintBrush, faExpandArrowsAlt, faCompressArrowsAlt, faCompass, faUpload, faBraille, faChalkboard, faFileUpload);
 
@@ -279,12 +280,63 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
     }
 
     onPointerUp = (e: PointerEvent): void => {
+        if (InteractionUtils.IsType(e, InteractionUtils.TOUCH)) return;
         document.removeEventListener("pointermove", this.onPointerMove);
         document.removeEventListener("pointerup", this.onPointerUp);
+        document.removeEventListener("touchmove", this.onTouch);
+        document.removeEventListener("touchend", this.onTouchEnd);
+    }
+
+    @action
+    pan = (e: PointerEvent | React.Touch): void => {
+        let x = this.Document.panX || 0;
+        let y = this.Document.panY || 0;
+        let docs = this.childLayoutPairs.map(pair => pair.layout);
+        let [dx, dy] = this.getTransform().transformDirection(e.clientX - this._lastX, e.clientY - this._lastY);
+        if (!this.isAnnotationOverlay) {
+            PDFMenu.Instance.fadeOut(true);
+            let minx = docs.length ? NumCast(docs[0].x) : 0;
+            let maxx = docs.length ? NumCast(docs[0].width) + minx : minx;
+            let miny = docs.length ? NumCast(docs[0].y) : 0;
+            let maxy = docs.length ? NumCast(docs[0].height) + miny : miny;
+            let ranges = docs.filter(doc => doc).reduce((range, doc) => {
+                let x = NumCast(doc.x);
+                let xe = x + NumCast(doc.width);
+                let y = NumCast(doc.y);
+                let ye = y + NumCast(doc.height);
+                return [[range[0][0] > x ? x : range[0][0], range[0][1] < xe ? xe : range[0][1]],
+                [range[1][0] > y ? y : range[1][0], range[1][1] < ye ? ye : range[1][1]]];
+            }, [[minx, maxx], [miny, maxy]]);
+            let ink = Cast(this.fieldExtensionDoc.ink, InkField);
+            if (ink && ink.inkData) {
+                ink.inkData.forEach((value: StrokeData, key: string) => {
+                    let bounds = InkingCanvas.StrokeRect(value);
+                    ranges[0] = [Math.min(ranges[0][0], bounds.left), Math.max(ranges[0][1], bounds.right)];
+                    ranges[1] = [Math.min(ranges[1][0], bounds.top), Math.max(ranges[1][1], bounds.bottom)];
+                });
+            }
+
+            let cscale = this.props.ContainingCollectionDoc ? NumCast(this.props.ContainingCollectionDoc.scale) : 1;
+            let panelDim = this.props.ScreenToLocalTransform().transformDirection(this.props.PanelWidth() / this.zoomScaling() * cscale,
+                this.props.PanelHeight() / this.zoomScaling() * cscale);
+            if (ranges[0][0] - dx > (this.panX() + panelDim[0] / 2)) x = ranges[0][1] + panelDim[0] / 2;
+            if (ranges[0][1] - dx < (this.panX() - panelDim[0] / 2)) x = ranges[0][0] - panelDim[0] / 2;
+            if (ranges[1][0] - dy > (this.panY() + panelDim[1] / 2)) y = ranges[1][1] + panelDim[1] / 2;
+            if (ranges[1][1] - dy < (this.panY() - panelDim[1] / 2)) y = ranges[1][0] - panelDim[1] / 2;
+        }
+        this.setPan(x - dx, y - dy);
+        this._lastX = e.pageX;
+        this._lastY = e.pageY;
     }
 
     @action
     onPointerMove = (e: PointerEvent): void => {
+        if (InteractionUtils.IsType(e, InteractionUtils.TOUCH)) {
+            if (this.props.active()) {
+                e.stopPropagation();
+            }
+            return;
+        }
         if (!e.cancelBubble && !this.isAnnotationOverlay) {
             if (this._hitCluster && this.tryDragCluster(e)) {
                 e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document'  however it does mark the event as cancelBubble=true which we test for in the move event handlers
@@ -293,47 +345,125 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
                 document.removeEventListener("pointerup", this.onPointerUp);
                 return;
             }
-            let x = this.Document.panX || 0;
-            let y = this.Document.panY || 0;
-            let docs = this.childLayoutPairs.map(pair => pair.layout);
-            let [dx, dy] = this.getTransform().transformDirection(e.clientX - this._lastX, e.clientY - this._lastY);
-            if (!this.isAnnotationOverlay) {
-                PDFMenu.Instance.fadeOut(true);
-                let minx = docs.length ? NumCast(docs[0].x) : 0;
-                let maxx = docs.length ? NumCast(docs[0].width) + minx : minx;
-                let miny = docs.length ? NumCast(docs[0].y) : 0;
-                let maxy = docs.length ? NumCast(docs[0].height) + miny : miny;
-                let ranges = docs.filter(doc => doc).reduce((range, doc) => {
-                    let x = NumCast(doc.x);
-                    let xe = x + NumCast(doc.width);
-                    let y = NumCast(doc.y);
-                    let ye = y + NumCast(doc.height);
-                    return [[range[0][0] > x ? x : range[0][0], range[0][1] < xe ? xe : range[0][1]],
-                    [range[1][0] > y ? y : range[1][0], range[1][1] < ye ? ye : range[1][1]]];
-                }, [[minx, maxx], [miny, maxy]]);
-                let ink = Cast(this.fieldExtensionDoc.ink, InkField);
-                if (ink && ink.inkData) {
-                    ink.inkData.forEach((value: StrokeData, key: string) => {
-                        let bounds = InkingCanvas.StrokeRect(value);
-                        ranges[0] = [Math.min(ranges[0][0], bounds.left), Math.max(ranges[0][1], bounds.right)];
-                        ranges[1] = [Math.min(ranges[1][0], bounds.top), Math.max(ranges[1][1], bounds.bottom)];
-                    });
+            this.pan(e);
+            e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document'  however it does mark the event as cancelBubble=true which we test for in the move event handlers
+            e.preventDefault();
+        }
+    }
+
+    private prevPoints: Map<number, React.Touch> = new Map<number, React.Touch>();
+    public FirstX: number = 0;
+    public FirstY: number = 0;
+    public SecondX: number = 0;
+    public SecondY: number = 0;
+
+    private _touchDrag: boolean = false;
+
+    /**
+     * When a touch even starts, we keep track of each touch that is associated with that event
+     */
+    @action
+    onTouchStart = (e: React.TouchEvent): void => {
+        for (let i = 0; i < e.targetTouches.length; i++) {
+            let pt = e.targetTouches.item(i);
+            this.prevPoints.set(pt.identifier, pt);
+        }
+        document.removeEventListener("touchmove", this.onTouch);
+        document.addEventListener("touchmove", this.onTouch);
+        document.removeEventListener("touchend", this.onTouchEnd);
+        document.addEventListener("touchend", this.onTouchEnd);
+    }
+
+    /**
+    * Handle touch move event
+    */
+    @action
+    onTouch = (e: TouchEvent): void => {
+        // if we're not actually moving a lot, don't consider it as dragging yet
+        if (!InteractionUtils.IsDragging(this.prevPoints, e.targetTouches, 5) && !this._touchDrag) return;
+        this._touchDrag = true;
+        switch (e.targetTouches.length) {
+            case 1:
+                // panning a workspace
+                if (!e.cancelBubble && this.props.active()) {
+                    let pt = e.targetTouches.item(0);
+                    if (pt) {
+                        this.pan(pt);
+                    }
+                    e.stopPropagation();
+                    e.preventDefault();
+                }
+                break;
+            case 2:
+                // pinch zooming
+                if (!e.cancelBubble) {
+                    let pt1: Touch | null = e.targetTouches.item(0);
+                    let pt2: Touch | null = e.targetTouches.item(1);
+                    if (!pt1 || !pt2) return;
+
+                    if (this.prevPoints.size === 2) {
+                        let oldPoint1 = this.prevPoints.get(pt1.identifier);
+                        let oldPoint2 = this.prevPoints.get(pt2.identifier);
+                        if (oldPoint1 && oldPoint2) {
+                            let dir = InteractionUtils.Pinching(pt1, pt2, oldPoint1, oldPoint2);
+
+                            // if zooming, zoom
+                            if (dir !== 0) {
+                                let d1 = Math.sqrt(Math.pow(pt1.clientX - oldPoint1.clientX, 2) + Math.pow(pt1.clientY - oldPoint1.clientY, 2));
+                                let d2 = Math.sqrt(Math.pow(pt2.clientX - oldPoint2.clientX, 2) + Math.pow(pt2.clientY - oldPoint2.clientY, 2));
+                                let centerX = Math.min(pt1.clientX, pt2.clientX) + Math.abs(pt2.clientX - pt1.clientX) / 2;
+                                let centerY = Math.min(pt1.clientY, pt2.clientY) + Math.abs(pt2.clientY - pt1.clientY) / 2;
+                                let delta = dir * (d1 + d2);
+                                this.zoom(centerX, centerY, delta, 250);
+                                this.prevPoints.set(pt1.identifier, pt1);
+                                this.prevPoints.set(pt2.identifier, pt2);
+                            }
+                        }
+                    }
                 }
+                e.stopPropagation();
+                e.preventDefault();
+                break;
+        }
+    }
 
-                let cscale = this.props.ContainingCollectionDoc ? NumCast(this.props.ContainingCollectionDoc.scale) : 1;
-                let panelDim = this.props.ScreenToLocalTransform().transformDirection(this.props.PanelWidth() / this.zoomScaling() * cscale,
-                    this.props.PanelHeight() / this.zoomScaling() * cscale);
-                if (ranges[0][0] - dx > (this.panX() + panelDim[0] / 2)) x = ranges[0][1] + panelDim[0] / 2;
-                if (ranges[0][1] - dx < (this.panX() - panelDim[0] / 2)) x = ranges[0][0] - panelDim[0] / 2;
-                if (ranges[1][0] - dy > (this.panY() + panelDim[1] / 2)) y = ranges[1][1] + panelDim[1] / 2;
-                if (ranges[1][1] - dy < (this.panY() - panelDim[1] / 2)) y = ranges[1][0] - panelDim[1] / 2;
+    @action
+    onTouchEnd = (e: TouchEvent): void => {
+        this._touchDrag = false;
+        e.stopPropagation();
+
+        // remove all the touches associated with the event
+        for (let i = 0; i < e.targetTouches.length; i++) {
+            let pt = e.targetTouches.item(i);
+            if (pt) {
+                if (this.prevPoints.has(pt.identifier)) {
+                    this.prevPoints.delete(pt.identifier);
+                }
             }
-            this.setPan(x - dx, y - dy);
-            this._lastX = e.pageX;
-            this._lastY = e.pageY;
-            e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document'  however it does mark the event as cancelBubble=true which we test for in the move event handlers
-            e.preventDefault();
         }
+
+        if (e.targetTouches.length === 0) {
+            this.prevPoints.clear();
+        }
+        document.removeEventListener("pointermove", this.onPointerMove);
+        document.removeEventListener("pointerup", this.onPointerUp);
+        document.removeEventListener("touchmove", this.onTouch);
+        document.removeEventListener("touchend", this.onTouchEnd);
+    }
+
+    @action
+    zoom = (pointX: number, pointY: number, deltaY: number, coefficient: number): void => {
+        let deltaScale = deltaY > 0 ? (1 / 1.1) : 1.1;
+        if (deltaScale * this.zoomScaling() < 1 && this.isAnnotationOverlay) {
+            deltaScale = 1 / this.zoomScaling();
+        }
+        if (deltaScale < 0) deltaScale = -deltaScale;
+        let [x, y] = this.getTransform().transformPoint(pointX, pointY);
+        let localTransform = this.getLocalTransform().inverse().scaleAbout(deltaScale, x, y);
+
+        let safeScale = Math.min(Math.max(0.15, localTransform.Scale), 40);
+        this.props.Document.scale = Math.abs(safeScale);
+        this.setPan(-localTransform.TranslateX / safeScale, -localTransform.TranslateY / safeScale);
     }
 
     @action
@@ -344,17 +474,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         }
         else if (this.props.active()) {
             e.stopPropagation();
-            let deltaScale = e.deltaY > 0 ? (1 / 1.1) : 1.1;
-            if (deltaScale * this.zoomScaling() < 1 && this.isAnnotationOverlay) {
-                deltaScale = 1 / this.zoomScaling();
-            }
-            if (deltaScale < 0) deltaScale = -deltaScale;
-            let [x, y] = this.getTransform().transformPoint(e.clientX, e.clientY);
-            let localTransform = this.getLocalTransform().inverse().scaleAbout(deltaScale, x, y);
-
-            let safeScale = Math.min(Math.max(0.15, localTransform.Scale), 40);
-            this.props.Document.scale = Math.abs(safeScale);
-            this.setPan(-localTransform.TranslateX / safeScale, -localTransform.TranslateY / safeScale);
+            this.zoom(e.clientX, e.clientY, e.deltaY, 1)
         }
     }
 
@@ -696,7 +816,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         return (
             <div className={"collectionfreeformview-container"} ref={this.createDropTarget} onWheel={this.onPointerWheel}
                 style={{ pointerEvents: SelectionManager.GetIsDragging() ? "all" : undefined, height: this.isAnnotationOverlay ? (NumCast(this.props.Document.scrollHeight) ? NumCast(this.props.Document.scrollHeight) : "100%") : this.props.PanelHeight() }}
-                onPointerDown={this.onPointerDown} onPointerMove={this.onCursorMove} onDrop={this.onDrop.bind(this)} onContextMenu={this.onContextMenu}>
+                onPointerDown={this.onPointerDown} onPointerMove={this.onCursorMove} onDrop={this.onDrop.bind(this)} onContextMenu={this.onContextMenu} onTouchStart={this.onTouchStart}>
                 <MarqueeView container={this} activeDocuments={this.getActiveDocuments} selectDocuments={this.selectDocuments} isSelected={this.props.isSelected}
                     addDocument={this.addDocument} removeDocument={this.props.removeDocument} addLiveTextDocument={this.addLiveTextBox} setPreviewCursor={this.props.setPreviewCursor}
                     getContainerTransform={this.getContainerTransform} getTransform={this.getTransform} isAnnotationOverlay={this.isAnnotationOverlay}>
-- 
cgit v1.2.3-70-g09d2


From b29832a0b75e91f7d53e3820b12d517e6bf3ee94 Mon Sep 17 00:00:00 2001
From: Stanley Yip <stanley_yip@brown.edu>
Date: Tue, 8 Oct 2019 18:14:09 -0400
Subject: touchable added baseline

---
 src/client/util/InteractionUtils.ts                |   2 +-
 src/client/views/DocComponent.tsx                  |   3 +-
 src/client/views/Touchable.tsx                     |  82 ++++++++++++++
 .../views/collections/CollectionBaseView.tsx       |   3 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 122 ++++++---------------
 src/client/views/nodes/DocumentView.tsx            |   3 +-
 6 files changed, 125 insertions(+), 90 deletions(-)
 create mode 100644 src/client/views/Touchable.tsx

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

diff --git a/src/client/util/InteractionUtils.ts b/src/client/util/InteractionUtils.ts
index f8088825a..63cba4982 100644
--- a/src/client/util/InteractionUtils.ts
+++ b/src/client/util/InteractionUtils.ts
@@ -2,7 +2,7 @@ export namespace InteractionUtils {
     export const MOUSE = "mouse";
     export const TOUCH = "touch";
 
-    export function IsType(e: PointerEvent, type: PointerTypes): boolean {
+    export function IsType(e: PointerEvent | React.PointerEvent, type: string): boolean {
         return e.pointerType === type;
     }
 
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index d6562492f..6d51122d4 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -1,9 +1,10 @@
 import * as React from 'react';
 import { Doc } from '../../new_fields/Doc';
 import { computed } from 'mobx';
+import { Touchable } from './Touchable';
 
 export function DocComponent<P extends { Document: Doc }, T>(schemaCtor: (doc: Doc) => T) {
-    class Component extends React.Component<P> {
+    class Component extends Touchable<P> {
         //TODO This might be pretty inefficient if doc isn't observed, because computed doesn't cache then
         @computed
         get Document(): T {
diff --git a/src/client/views/Touchable.tsx b/src/client/views/Touchable.tsx
new file mode 100644
index 000000000..e9671ab8b
--- /dev/null
+++ b/src/client/views/Touchable.tsx
@@ -0,0 +1,82 @@
+import * as React from 'react';
+import { action } from 'mobx';
+import { InteractionUtils } from '../util/InteractionUtils';
+
+export abstract class Touchable<T> extends React.Component<T> {
+    protected _touchDrag: boolean = false;
+    protected prevPoints: Map<number, React.Touch> = new Map<number, React.Touch>();
+
+    public FirstX: number = 0;
+    public FirstY: number = 0;
+    public SecondX: number = 0;
+    public SecondY: number = 0;
+
+    /**
+     * When a touch even starts, we keep track of each touch that is associated with that event
+     */
+    @action
+    protected onTouchStart = (e: React.TouchEvent): void => {
+        for (let i = 0; i < e.targetTouches.length; i++) {
+            let pt = e.targetTouches.item(i);
+            this.prevPoints.set(pt.identifier, pt);
+        }
+        document.removeEventListener("touchmove", this.onTouch);
+        document.addEventListener("touchmove", this.onTouch);
+        document.removeEventListener("touchend", this.onTouchEnd);
+        document.addEventListener("touchend", this.onTouchEnd);
+    }
+
+    /**
+    * Handle touch move event
+    */
+    @action
+    protected onTouch = (e: TouchEvent): void => {
+        // if we're not actually moving a lot, don't consider it as dragging yet
+        if (!InteractionUtils.IsDragging(this.prevPoints, e.targetTouches, 5) && !this._touchDrag) return;
+        this._touchDrag = true;
+        switch (e.targetTouches.length) {
+            case 1:
+                this.handle1Pointer(e)
+                break;
+            case 2:
+                this.handle2Pointers(e);
+                break;
+        }
+    }
+
+    @action
+    protected onTouchEnd = (e: TouchEvent): void => {
+        this._touchDrag = false;
+        e.stopPropagation();
+
+        // remove all the touches associated with the event
+        for (let i = 0; i < e.targetTouches.length; i++) {
+            let pt = e.targetTouches.item(i);
+            if (pt) {
+                if (this.prevPoints.has(pt.identifier)) {
+                    this.prevPoints.delete(pt.identifier);
+                }
+            }
+        }
+
+        if (e.targetTouches.length === 0) {
+            this.prevPoints.clear();
+        }
+        this.cleanUpInteractions();
+    }
+
+    cleanUpInteractions = (): void => {
+        document.removeEventListener("touchmove", this.onTouch);
+        document.removeEventListener("touchend", this.onTouchEnd);
+    }
+
+    handle1Pointer = (e: TouchEvent): any => {
+        e.stopPropagation();
+        e.preventDefault();
+    }
+
+    handle2Pointers = (e: TouchEvent): any => {
+        e.stopPropagation();
+        e.preventDefault();
+    }
+}
\ No newline at end of file
diff --git a/src/client/views/collections/CollectionBaseView.tsx b/src/client/views/collections/CollectionBaseView.tsx
index 38d050e5c..f8988338f 100644
--- a/src/client/views/collections/CollectionBaseView.tsx
+++ b/src/client/views/collections/CollectionBaseView.tsx
@@ -13,6 +13,7 @@ import { FieldViewProps } from '../nodes/FieldView';
 import './CollectionBaseView.scss';
 import { DateField } from '../../../new_fields/DateField';
 import { ImageField } from '../../../new_fields/URLField';
+import { Touchable } from '../Touchable';
 
 export enum CollectionViewType {
     Invalid,
@@ -60,7 +61,7 @@ export interface CollectionViewProps extends FieldViewProps {
 }
 
 @observer
-export class CollectionBaseView extends React.Component<CollectionViewProps> {
+export class CollectionBaseView extends Touchable<CollectionViewProps> {
     @observable private static _safeMode = false;
     static InSafeMode() { return this._safeMode; }
     static SetSafeMode(safeMode: boolean) { this._safeMode = safeMode; }
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 314ce5cdb..2f9d2606f 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -351,100 +351,50 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         }
     }
 
-    private prevPoints: Map<number, React.Touch> = new Map<number, React.Touch>();
-    public FirstX: number = 0;
-    public FirstY: number = 0;
-    public SecondX: number = 0;
-    public SecondY: number = 0;
-
-    private _touchDrag: boolean = false;
-
-    /**
-     * When a touch even starts, we keep track of each touch that is associated with that event
-     */
-    @action
-    onTouchStart = (e: React.TouchEvent): void => {
-        for (let i = 0; i < e.targetTouches.length; i++) {
-            let pt = e.targetTouches.item(i);
-            this.prevPoints.set(pt.identifier, pt);
+    handle1Pointer = (e: TouchEvent) => {
+        // panning a workspace
+        if (!e.cancelBubble && this.props.active()) {
+            let pt = e.targetTouches.item(0);
+            if (pt) {
+                this.pan(pt);
+            }
+            e.stopPropagation();
+            e.preventDefault();
         }
-        document.removeEventListener("touchmove", this.onTouch);
-        document.addEventListener("touchmove", this.onTouch);
-        document.removeEventListener("touchend", this.onTouchEnd);
-        document.addEventListener("touchend", this.onTouchEnd);
     }
 
-    /**
-    * Handle touch move event
-    */
-    @action
-    onTouch = (e: TouchEvent): void => {
-        // if we're not actually moving a lot, don't consider it as dragging yet
-        if (!InteractionUtils.IsDragging(this.prevPoints, e.targetTouches, 5) && !this._touchDrag) return;
-        this._touchDrag = true;
-        switch (e.targetTouches.length) {
-            case 1:
-                // panning a workspace
-                if (!e.cancelBubble && this.props.active()) {
-                    let pt = e.targetTouches.item(0);
-                    if (pt) {
-                        this.pan(pt);
-                    }
-                    e.stopPropagation();
-                    e.preventDefault();
-                }
-                break;
-            case 2:
-                // pinch zooming
-                if (!e.cancelBubble) {
-                    let pt1: Touch | null = e.targetTouches.item(0);
-                    let pt2: Touch | null = e.targetTouches.item(1);
-                    if (!pt1 || !pt2) return;
-
-                    if (this.prevPoints.size === 2) {
-                        let oldPoint1 = this.prevPoints.get(pt1.identifier);
-                        let oldPoint2 = this.prevPoints.get(pt2.identifier);
-                        if (oldPoint1 && oldPoint2) {
-                            let dir = InteractionUtils.Pinching(pt1, pt2, oldPoint1, oldPoint2);
-
-                            // if zooming, zoom
-                            if (dir !== 0) {
-                                let d1 = Math.sqrt(Math.pow(pt1.clientX - oldPoint1.clientX, 2) + Math.pow(pt1.clientY - oldPoint1.clientY, 2));
-                                let d2 = Math.sqrt(Math.pow(pt2.clientX - oldPoint2.clientX, 2) + Math.pow(pt2.clientY - oldPoint2.clientY, 2));
-                                let centerX = Math.min(pt1.clientX, pt2.clientX) + Math.abs(pt2.clientX - pt1.clientX) / 2;
-                                let centerY = Math.min(pt1.clientY, pt2.clientY) + Math.abs(pt2.clientY - pt1.clientY) / 2;
-                                let delta = dir * (d1 + d2);
-                                this.zoom(centerX, centerY, delta, 250);
-                                this.prevPoints.set(pt1.identifier, pt1);
-                                this.prevPoints.set(pt2.identifier, pt2);
-                            }
-                        }
+    handle2Pointers = (e: TouchEvent) => {
+        // pinch zooming
+        if (!e.cancelBubble) {
+            let pt1: Touch | null = e.targetTouches.item(0);
+            let pt2: Touch | null = e.targetTouches.item(1);
+            if (!pt1 || !pt2) return;
+
+            if (this.prevPoints.size === 2) {
+                let oldPoint1 = this.prevPoints.get(pt1.identifier);
+                let oldPoint2 = this.prevPoints.get(pt2.identifier);
+                if (oldPoint1 && oldPoint2) {
+                    let dir = InteractionUtils.Pinching(pt1, pt2, oldPoint1, oldPoint2);
+
+                    // if zooming, zoom
+                    if (dir !== 0) {
+                        let d1 = Math.sqrt(Math.pow(pt1.clientX - oldPoint1.clientX, 2) + Math.pow(pt1.clientY - oldPoint1.clientY, 2));
+                        let d2 = Math.sqrt(Math.pow(pt2.clientX - oldPoint2.clientX, 2) + Math.pow(pt2.clientY - oldPoint2.clientY, 2));
+                        let centerX = Math.min(pt1.clientX, pt2.clientX) + Math.abs(pt2.clientX - pt1.clientX) / 2;
+                        let centerY = Math.min(pt1.clientY, pt2.clientY) + Math.abs(pt2.clientY - pt1.clientY) / 2;
+                        let delta = dir * (d1 + d2);
+                        this.zoom(centerX, centerY, delta, 250);
+                        this.prevPoints.set(pt1.identifier, pt1);
+                        this.prevPoints.set(pt2.identifier, pt2);
                     }
                 }
-                e.stopPropagation();
-                e.preventDefault();
-                break;
-        }
-    }
-
-    @action
-    onTouchEnd = (e: TouchEvent): void => {
-        this._touchDrag = false;
-        e.stopPropagation();
-
-        // remove all the touches associated with the event
-        for (let i = 0; i < e.targetTouches.length; i++) {
-            let pt = e.targetTouches.item(i);
-            if (pt) {
-                if (this.prevPoints.has(pt.identifier)) {
-                    this.prevPoints.delete(pt.identifier);
-                }
             }
         }
+        e.stopPropagation();
+        e.preventDefault();
+    }
 
-        if (e.targetTouches.length === 0) {
-            this.prevPoints.clear();
-        }
+    cleanUpInteractions = () => {
         document.removeEventListener("pointermove", this.onPointerMove);
         document.removeEventListener("pointerup", this.onPointerUp);
         document.removeEventListener("touchmove", this.onTouch);
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 6627b8792..bf90e2d08 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -611,8 +611,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
             DataDoc={this.props.DataDoc} />);
     }
 
-    onTouchStart = (e: React.TouchEvent) => {
+    handle1Pointer = (e: TouchEvent) => {
         e.stopPropagation();
+        e.preventDefault();
     }
 
     render() {
-- 
cgit v1.2.3-70-g09d2


From d58195a0470b3882d6b43b18f1f4ab7a373a671f Mon Sep 17 00:00:00 2001
From: Stanley Yip <stanley_yip@brown.edu>
Date: Sat, 12 Oct 2019 17:02:31 -0400
Subject: marquee now relies on pdf-style menu. pdf-style menu is now
 componentized so that other things can use it

---
 src/Utils.ts                                       |   2 +
 src/client/views/AntimodeMenu.scss                 |  29 +++
 src/client/views/AntimodeMenu.tsx                  | 114 ++++++++++++
 src/client/views/MainView.tsx                      |   6 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |   4 +
 .../collectionFreeForm/MarqueeOptionsMenu.tsx      |  52 ++++++
 .../collections/collectionFreeForm/MarqueeView.tsx | 206 +++++++++++++--------
 src/client/views/nodes/DocumentView.tsx            |   8 +-
 src/client/views/pdf/PDFMenu.scss                  |  40 +---
 src/client/views/pdf/PDFMenu.tsx                   | 128 ++-----------
 10 files changed, 358 insertions(+), 231 deletions(-)
 create mode 100644 src/client/views/AntimodeMenu.scss
 create mode 100644 src/client/views/AntimodeMenu.tsx
 create mode 100644 src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx

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

diff --git a/src/Utils.ts b/src/Utils.ts
index 4b892aa70..ca134e165 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -278,6 +278,8 @@ export function returnEmptyString() { return ""; }
 
 export function emptyFunction() { }
 
+export function unimplementedFunction() { throw new Error("This function is not implemented, but should be."); }
+
 export type Without<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
 
 export type Predicate<K, V> = (entry: [K, V]) => boolean;
diff --git a/src/client/views/AntimodeMenu.scss b/src/client/views/AntimodeMenu.scss
new file mode 100644
index 000000000..f3da5f284
--- /dev/null
+++ b/src/client/views/AntimodeMenu.scss
@@ -0,0 +1,29 @@
+.antimodeMenu-cont {
+    position: absolute;
+    z-index: 10000;
+    height: 35px;
+    background: #323232;
+    box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.25);
+    border-radius: 0px 6px 6px 6px;
+    overflow: hidden;
+    display: flex;
+
+    .antimodeMenu-button {
+        background-color: transparent;
+        width: 35px;
+        height: 35px;
+    }
+
+    .antimodeMenu-button:hover {
+        background-color: #121212;
+    }
+
+    .antimodeMenu-dragger {
+        height: 100%;
+        transition: width .2s;
+        background-image: url("https://logodix.com/logo/1020374.png");
+        background-size: 90% 100%;
+        background-repeat: no-repeat;
+        background-position: left center;
+    }
+}
\ No newline at end of file
diff --git a/src/client/views/AntimodeMenu.tsx b/src/client/views/AntimodeMenu.tsx
new file mode 100644
index 000000000..9b0ba6312
--- /dev/null
+++ b/src/client/views/AntimodeMenu.tsx
@@ -0,0 +1,114 @@
+import React = require("react");
+import { observer } from "mobx-react";
+import { observable, action } from "mobx";
+import "./AntimodeMenu.scss";
+
+export default abstract class AntimodeMenu extends React.Component {
+    protected _offsetY: number = 0;
+    protected _offsetX: number = 0;
+    protected _mainCont: React.RefObject<HTMLDivElement> = React.createRef();
+    protected _dragging: boolean = false;
+
+    @observable protected _top: number = -300;
+    @observable protected _left: number = -300;
+    @observable protected _opacity: number = 1;
+    @observable protected _transition: string = "opacity 0.5s";
+    @observable protected _transitionDelay: string = "";
+
+    @observable public Pinned: boolean = false;
+
+    @action
+    public jumpTo = (x: number, y: number, forceJump: boolean = false) => {
+        if (!this.Pinned || forceJump) {
+            this._transition = this._transitionDelay = "";
+            this._opacity = 1;
+            this._left = x;
+            this._top = y;
+        }
+    }
+
+    @action
+    public fadeOut = (forceOut: boolean) => {
+        if (!this.Pinned) {
+            if (this._opacity === 0.2) {
+                this._transition = "opacity 0.1s";
+                this._transitionDelay = "";
+                this._opacity = 0;
+                this._left = this._top = -300;
+            }
+
+            if (forceOut) {
+                this._transition = "";
+                this._transitionDelay = "";
+                this._opacity = 0;
+                this._left = this._top = -300;
+            }
+        }
+    }
+
+    @action
+    protected pointerLeave = (e: React.PointerEvent) => {
+        if (!this.Pinned) {
+            this._transition = "opacity 0.5s";
+            this._transitionDelay = "1s";
+            this._opacity = 0.2;
+            setTimeout(() => this.fadeOut(false), 3000);
+        }
+    }
+
+    @action
+    protected pointerEntered = (e: React.PointerEvent) => {
+        this._transition = "opacity 0.1s";
+        this._transitionDelay = "";
+        this._opacity = 1;
+    }
+
+    @action
+    protected togglePin = (e: React.MouseEvent) => {
+        this.Pinned = !this.Pinned;
+    }
+
+    protected dragStart = (e: React.PointerEvent) => {
+        document.removeEventListener("pointermove", this.dragging);
+        document.addEventListener("pointermove", this.dragging);
+        document.removeEventListener("pointerup", this.dragEnd);
+        document.addEventListener("pointerup", this.dragEnd);
+
+        this._offsetX = this._mainCont.current!.getBoundingClientRect().width - e.nativeEvent.offsetX;
+        this._offsetY = e.nativeEvent.offsetY;
+
+        e.stopPropagation();
+        e.preventDefault();
+    }
+
+    @action
+    protected dragging = (e: PointerEvent) => {
+        this._left = e.pageX - this._offsetX;
+        this._top = e.pageY - this._offsetY;
+
+        e.stopPropagation();
+        e.preventDefault();
+    }
+
+    protected dragEnd = (e: PointerEvent) => {
+        document.removeEventListener("pointermove", this.dragging);
+        document.removeEventListener("pointerup", this.dragEnd);
+        e.stopPropagation();
+        e.preventDefault();
+    }
+
+    protected handleContextMenu = (e: React.MouseEvent) => {
+        e.stopPropagation();
+        e.preventDefault();
+    }
+
+    protected getElement(buttons: JSX.Element[]) {
+        return (
+            <div className="antimodeMenu-cont" onPointerLeave={this.pointerLeave} onPointerEnter={this.pointerEntered} ref={this._mainCont} onContextMenu={this.handleContextMenu}
+                style={{ left: this._left, top: this._top, opacity: this._opacity, transition: this._transition, transitionDelay: this._transitionDelay }}>
+                {buttons}
+                <div className="antimodeMenu-dragger" onPointerDown={this.dragStart} style={{ width: this.Pinned ? "20px" : "0px" }} />
+            </div>
+        );
+    }
+}
\ No newline at end of file
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 660b42290..53951224c 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -1,6 +1,5 @@
 import { IconName, library } from '@fortawesome/fontawesome-svg-core';
-import { faLink, faArrowDown, faArrowUp, faBolt, faCaretUp, faCat, faCheck, faClone, faCloudUploadAlt, faCommentAlt, faCut, faExclamation, faFilePdf, faFilm, faFont, faGlobeAsia, faLongArrowAltRight, faMusic, faObjectGroup, faPause, faPenNib, faPlay, faPortrait, faRedoAlt, faThumbtack, faTree, faUndoAlt, faTv, faChevronRight, faEllipsisV } from '@fortawesome/free-solid-svg-icons';
-import { faArrowDown, faArrowUp, faBolt, faCaretUp, faCat, faCheck, faClone, faCloudUploadAlt, faCommentAlt, faCut, faExclamation, faFilePdf, faFilm, faFont, faGlobeAsia, faLongArrowAltRight, faMusic, faObjectGroup, faPause, faPenNib, faPlay, faPortrait, faRedoAlt, faThumbtack, faTree, faTv, faUndoAlt } from '@fortawesome/free-solid-svg-icons';
+import { faLink, faArrowDown, faArrowUp, faBolt, faCaretUp, faCat, faCheck, faClone, faCloudUploadAlt, faCommentAlt, faCut, faExclamation, faFilePdf, faFilm, faFont, faGlobeAsia, faLongArrowAltRight, faMusic, faObjectGroup, faPause, faPenNib, faPlay, faPortrait, faRedoAlt, faThumbtack, faTree, faUndoAlt, faTv, faChevronRight, faEllipsisV, faCompressArrowsAlt } from '@fortawesome/free-solid-svg-icons';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { action, computed, configure, observable, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
@@ -42,6 +41,7 @@ import { OverlayView } from './OverlayView';
 import PDFMenu from './pdf/PDFMenu';
 import { PreviewCursor } from './PreviewCursor';
 import { FilterBox } from './search/FilterBox';
+import MarqueeOptionsMenu from './collections/collectionFreeForm/MarqueeOptionsMenu';
 
 @observer
 export class MainView extends React.Component {
@@ -202,6 +202,7 @@ export class MainView extends React.Component {
         library.add(faMusic);
         library.add(faTree);
         library.add(faPlay);
+        library.add(faCompressArrowsAlt);
         library.add(faPause);
         library.add(faClone);
         library.add(faCut);
@@ -687,6 +688,7 @@ export class MainView extends React.Component {
                 {this.nodesMenu()}
                 {this.miscButtons}
                 <PDFMenu />
+                <MarqueeOptionsMenu />
                 <MainOverlayTextBox firstinstance={true} />
                 <OverlayView />
             </div >
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 2f9d2606f..54d5b95b6 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -39,6 +39,7 @@ import "./CollectionFreeFormView.scss";
 import { MarqueeView } from "./MarqueeView";
 import React = require("react");
 import { InteractionUtils } from "../../../util/InteractionUtils";
+import MarqueeOptionsMenu from "./MarqueeOptionsMenu";
 
 library.add(faEye as any, faTable, faPaintBrush, faExpandArrowsAlt, faCompressArrowsAlt, faCompass, faUpload, faBraille, faChalkboard, faFileUpload);
 
@@ -289,6 +290,9 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
 
     @action
     pan = (e: PointerEvent | React.Touch): void => {
+        // I think it makes sense for the marquee menu to go away when panned. -syip2
+        MarqueeOptionsMenu.Instance.fadeOut(true);
+
         let x = this.Document.panX || 0;
         let y = this.Document.panY || 0;
         let docs = this.childLayoutPairs.map(pair => pair.layout);
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx
new file mode 100644
index 000000000..7c5b87283
--- /dev/null
+++ b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx
@@ -0,0 +1,52 @@
+import React = require("react")
+import AntimodeMenu from "../../AntimodeMenu";
+import { observer } from "mobx-react";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { unimplementedFunction } from "../../../../Utils";
+
+@observer
+export default class MarqueeOptionsMenu extends AntimodeMenu {
+    static Instance: MarqueeOptionsMenu;
+
+    public createCollection: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction;
+    public delete: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction;
+    public summarize: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction;
+    public showMarquee: () => void = unimplementedFunction;
+    public hideMarquee: () => void = unimplementedFunction;
+
+    constructor(props: Readonly<{}>) {
+        super(props);
+
+        MarqueeOptionsMenu.Instance = this;
+    }
+
+    render() {
+        let buttons = [
+            <button
+                className="antimodeMenu-button"
+                title="Create a Collection"
+                onPointerDown={this.createCollection}
+                onPointerLeave={this.hideMarquee}
+                onPointerEnter={this.showMarquee}>
+                <FontAwesomeIcon icon="object-group" size="lg" />
+            </button>,
+            <button
+                className="antimodeMenu-button"
+                title="Summarize Documents"
+                onPointerDown={this.summarize}
+                onPointerLeave={this.hideMarquee}
+                onPointerEnter={this.showMarquee}>
+                <FontAwesomeIcon icon="compress-arrows-alt" size="lg" />
+            </button>,
+            <button
+                className="antimodeMenu-button"
+                title="Delete Documents"
+                onPointerDown={this.delete}
+                onPointerLeave={this.hideMarquee}
+                onPointerEnter={this.showMarquee}>
+                <FontAwesomeIcon icon="trash-alt" size="lg" />
+            </button>,
+        ]
+        return this.getElement(buttons);
+    }
+}
\ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 82193aefa..ad4da7733 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -19,6 +19,7 @@ import { CollectionViewType } from "../CollectionBaseView";
 import { CollectionFreeFormView } from "./CollectionFreeFormView";
 import "./MarqueeView.scss";
 import React = require("react");
+import MarqueeOptionsMenu from "./MarqueeOptionsMenu";
 
 interface MarqueeViewProps {
     getContainerTransform: () => Transform;
@@ -195,6 +196,14 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
             }
             this.props.selectDocuments(mselect.length ? mselect : [this.props.container.props.Document]);
         }
+        if (!this._commandExecuted) {
+            MarqueeOptionsMenu.Instance.createCollection = this.collection;
+            MarqueeOptionsMenu.Instance.delete = this.delete;
+            MarqueeOptionsMenu.Instance.summarize = this.summary;
+            MarqueeOptionsMenu.Instance.showMarquee = this.showMarquee;
+            MarqueeOptionsMenu.Instance.hideMarquee = this.hideMarquee;
+            MarqueeOptionsMenu.Instance.jumpTo(e.clientX, e.clientY);
+        }
         this.cleanupInteractions(true);
 
         if (e.altKey) {
@@ -255,6 +264,117 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
         Doc.fieldExtensionDoc(cprops.Document, cprops.fieldKey).ink = value;
     }
 
+    @action
+    showMarquee = () => {
+        this._visible = true;
+    }
+
+    @action
+    hideMarquee = () => {
+        this._visible = false;
+    }
+
+    @action
+    delete = () => {
+        this.marqueeSelect(false).map(d => this.props.removeDocument(d));
+        if (this.ink) {
+            this.marqueeInkDelete(this.ink.inkData);
+        }
+        SelectionManager.DeselectAll();
+        this.cleanupInteractions(false);
+        MarqueeOptionsMenu.Instance.fadeOut(true);
+    }
+
+    getCollection = (selected: Doc[]) => {
+        let bounds = this.Bounds;
+        let defaultPalette = ["rgb(114,229,239)", "rgb(255,246,209)", "rgb(255,188,156)", "rgb(247,220,96)", "rgb(122,176,238)",
+            "rgb(209,150,226)", "rgb(127,235,144)", "rgb(252,188,189)", "rgb(247,175,81)",];
+        let colorPalette = Cast(this.props.container.props.Document.colorPalette, listSpec("string"));
+        if (!colorPalette) this.props.container.props.Document.colorPalette = new List<string>(defaultPalette);
+        let palette = Array.from(Cast(this.props.container.props.Document.colorPalette, listSpec("string")) as string[]);
+        let usedPaletted = new Map<string, number>();
+        [...this.props.activeDocuments(), this.props.container.props.Document].map(child => {
+            let bg = StrCast(child.layout instanceof Doc ? child.layout.backgroundColor : child.backgroundColor);
+            if (palette.indexOf(bg) !== -1) {
+                palette.splice(palette.indexOf(bg), 1);
+                if (usedPaletted.get(bg)) usedPaletted.set(bg, usedPaletted.get(bg)! + 1);
+                else usedPaletted.set(bg, 1);
+            }
+        });
+        usedPaletted.delete("#f1efeb");
+        usedPaletted.delete("white");
+        usedPaletted.delete("rgba(255,255,255,1)");
+        let usedSequnce = Array.from(usedPaletted.keys()).sort((a, b) => usedPaletted.get(a)! < usedPaletted.get(b)! ? -1 : usedPaletted.get(a)! > usedPaletted.get(b)! ? 1 : 0);
+        let chosenColor = (usedPaletted.size === 0) ? "white" : palette.length ? palette[0] : usedSequnce[0];
+        let inkData = this.ink ? this.ink.inkData : undefined;
+        let newCollection = Docs.Create.FreeformDocument(selected, {
+            x: bounds.left,
+            y: bounds.top,
+            panX: 0,
+            panY: 0,
+            backgroundColor: this.props.isAnnotationOverlay ? undefined : chosenColor,
+            defaultBackgroundColor: this.props.isAnnotationOverlay ? undefined : chosenColor,
+            width: bounds.width,
+            height: bounds.height,
+            title: "a nested collection",
+        });
+        let dataExtensionField = Doc.CreateDocumentExtensionForField(newCollection, "data");
+        dataExtensionField.ink = inkData ? new InkField(this.marqueeInkSelect(inkData)) : undefined;
+        this.marqueeInkDelete(inkData);
+        return newCollection;
+    }
+
+    @action
+    collection = (e: KeyboardEvent | React.PointerEvent | undefined) => {
+        let bounds = this.Bounds;
+        let selected = this.marqueeSelect(false);
+        if (e instanceof KeyboardEvent ? e.key === "c" : true) {
+            selected.map(d => {
+                this.props.removeDocument(d);
+                d.x = NumCast(d.x) - bounds.left - bounds.width / 2;
+                d.y = NumCast(d.y) - bounds.top - bounds.height / 2;
+                d.page = -1;
+                return d;
+            });
+        }
+        let newCollection = this.getCollection(selected);
+        this.props.addDocument(newCollection, false);
+        this.props.selectDocuments([newCollection]);
+        MarqueeOptionsMenu.Instance.fadeOut(true);
+    }
+
+    @action
+    summary = (e: KeyboardEvent | React.PointerEvent | undefined) => {
+        let bounds = this.Bounds;
+        let selected = this.marqueeSelect(false);
+        let newCollection = this.getCollection(selected);
+
+        selected.map(d => {
+            this.props.removeDocument(d);
+            d.x = NumCast(d.x) - bounds.left - bounds.width / 2;
+            d.y = NumCast(d.y) - bounds.top - bounds.height / 2;
+            d.page = -1;
+            return d;
+        });
+        newCollection.chromeStatus = "disabled";
+        let summary = Docs.Create.TextDocument({ x: bounds.left, y: bounds.top, width: 300, height: 100, autoHeight: true, backgroundColor: "#e2ad32" /* yellow */, title: "-summary-" });
+        Doc.GetProto(summary).summarizedDocs = new List<Doc>([newCollection]);
+        newCollection.x = bounds.left + bounds.width;
+        Doc.GetProto(newCollection).summaryDoc = summary;
+        Doc.GetProto(newCollection).title = ComputedField.MakeFunction(`summaryTitle(this);`);
+        if (e instanceof KeyboardEvent ? e.key === "s" : true) { // summary is wrapped in an expand/collapse container that also contains the summarized documents in a free form view.
+            let container = Docs.Create.FreeformDocument([summary, newCollection], { x: bounds.left, y: bounds.top, width: 300, height: 200, chromeStatus: "disabled", title: "-summary-" });
+            container.viewType = CollectionViewType.Stacking;
+            container.autoHeight = true;
+            Doc.GetProto(summary).maximizeLocation = "inPlace";  // or "onRight"
+            this.props.addLiveTextDocument(container);
+        } else if (e instanceof KeyboardEvent ? e.key === "S" : false) { // the summary stands alone, but is linked to a collection of the summarized documents - set the OnCLick behavior to link follow to access them
+            Doc.GetProto(summary).maximizeLocation = "inTab";  // or "inPlace", or "onRight"
+            this.props.addLiveTextDocument(summary);
+        }
+        MarqueeOptionsMenu.Instance.fadeOut(true);
+    }
+
     @undoBatch
     @action
     marqueeCommand = async (e: KeyboardEvent) => {
@@ -265,12 +385,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
             this._commandExecuted = true;
             e.stopPropagation();
             (e as any).propagationIsStopped = true;
-            this.marqueeSelect(false).map(d => this.props.removeDocument(d));
-            if (this.ink) {
-                this.marqueeInkDelete(this.ink.inkData);
-            }
-            SelectionManager.DeselectAll();
-            this.cleanupInteractions(false);
+            this.delete();
             e.stopPropagation();
         }
         if (e.key === "c" || e.key === "s" || e.key === "S") {
@@ -278,80 +393,12 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
             e.stopPropagation();
             e.preventDefault();
             (e as any).propagationIsStopped = true;
-            let bounds = this.Bounds;
-            let selected = this.marqueeSelect(false);
             if (e.key === "c") {
-                selected.map(d => {
-                    this.props.removeDocument(d);
-                    d.x = NumCast(d.x) - bounds.left - bounds.width / 2;
-                    d.y = NumCast(d.y) - bounds.top - bounds.height / 2;
-                    d.page = -1;
-                    return d;
-                });
+                this.collection(e);
             }
-            let defaultPalette = ["rgb(114,229,239)", "rgb(255,246,209)", "rgb(255,188,156)", "rgb(247,220,96)", "rgb(122,176,238)",
-                "rgb(209,150,226)", "rgb(127,235,144)", "rgb(252,188,189)", "rgb(247,175,81)",];
-            let colorPalette = Cast(this.props.container.props.Document.colorPalette, listSpec("string"));
-            if (!colorPalette) this.props.container.props.Document.colorPalette = new List<string>(defaultPalette);
-            let palette = Array.from(Cast(this.props.container.props.Document.colorPalette, listSpec("string")) as string[]);
-            let usedPaletted = new Map<string, number>();
-            [...this.props.activeDocuments(), this.props.container.props.Document].map(child => {
-                let bg = StrCast(child.layout instanceof Doc ? child.layout.backgroundColor : child.backgroundColor);
-                if (palette.indexOf(bg) !== -1) {
-                    palette.splice(palette.indexOf(bg), 1);
-                    if (usedPaletted.get(bg)) usedPaletted.set(bg, usedPaletted.get(bg)! + 1);
-                    else usedPaletted.set(bg, 1);
-                }
-            });
-            usedPaletted.delete("#f1efeb");
-            usedPaletted.delete("white");
-            usedPaletted.delete("rgba(255,255,255,1)");
-            let usedSequnce = Array.from(usedPaletted.keys()).sort((a, b) => usedPaletted.get(a)! < usedPaletted.get(b)! ? -1 : usedPaletted.get(a)! > usedPaletted.get(b)! ? 1 : 0);
-            let chosenColor = (usedPaletted.size === 0) ? "white" : palette.length ? palette[0] : usedSequnce[0];
-            let inkData = this.ink ? this.ink.inkData : undefined;
-            let newCollection = Docs.Create.FreeformDocument(selected, {
-                x: bounds.left,
-                y: bounds.top,
-                panX: 0,
-                panY: 0,
-                backgroundColor: this.props.isAnnotationOverlay ? undefined : chosenColor,
-                defaultBackgroundColor: this.props.isAnnotationOverlay ? undefined : chosenColor,
-                width: bounds.width,
-                height: bounds.height,
-                title: "a nested collection",
-            });
-            let dataExtensionField = Doc.CreateDocumentExtensionForField(newCollection, "data");
-            dataExtensionField.ink = inkData ? new InkField(this.marqueeInkSelect(inkData)) : undefined;
-            this.marqueeInkDelete(inkData);
 
             if (e.key === "s" || e.key === "S") {
-                selected.map(d => {
-                    this.props.removeDocument(d);
-                    d.x = NumCast(d.x) - bounds.left - bounds.width / 2;
-                    d.y = NumCast(d.y) - bounds.top - bounds.height / 2;
-                    d.page = -1;
-                    return d;
-                });
-                newCollection.chromeStatus = "disabled";
-                let summary = Docs.Create.TextDocument({ x: bounds.left, y: bounds.top, width: 300, height: 100, autoHeight: true, backgroundColor: "#e2ad32" /* yellow */, title: "-summary-" });
-                Doc.GetProto(summary).summarizedDocs = new List<Doc>([newCollection]);
-                newCollection.x = bounds.left + bounds.width;
-                Doc.GetProto(newCollection).summaryDoc = summary;
-                Doc.GetProto(newCollection).title = ComputedField.MakeFunction(`summaryTitle(this);`);
-                if (e.key === "s") { // summary is wrapped in an expand/collapse container that also contains the summarized documents in a free form view.
-                    let container = Docs.Create.FreeformDocument([summary, newCollection], { x: bounds.left, y: bounds.top, width: 300, height: 200, chromeStatus: "disabled", title: "-summary-" });
-                    container.viewType = CollectionViewType.Stacking;
-                    container.autoHeight = true;
-                    Doc.GetProto(summary).maximizeLocation = "inPlace";  // or "onRight"
-                    this.props.addLiveTextDocument(container);
-                } else if (e.key === "S") { // the summary stands alone, but is linked to a collection of the summarized documents - set the OnCLick behavior to link follow to access them
-                    Doc.GetProto(summary).maximizeLocation = "inTab";  // or "inPlace", or "onRight"
-                    this.props.addLiveTextDocument(summary);
-                }
-            }
-            else {
-                this.props.addDocument(newCollection, false);
-                this.props.selectDocuments([newCollection]);
+                this.summary(e);
             }
             this.cleanupInteractions(false);
         }
@@ -435,8 +482,13 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
     @computed
     get marqueeDiv() {
         let v = this.props.getContainerTransform().transformDirection(this._lastX - this._downX, this._lastY - this._downY);
+        /**
+         * @RE - The commented out span below
+         * This contains the "C for collection, ..." text on marquees.
+         * Commented out by syip2 when the marquee menu was added.
+         */
         return <div className="marquee" style={{ width: `${Math.abs(v[0])}`, height: `${Math.abs(v[1])}`, zIndex: 2000 }} >
-            <span className="marquee-legend" />
+            {/* <span className="marquee-legend" /> */}
         </div>;
     }
 
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index bf90e2d08..f74023f42 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -41,6 +41,7 @@ import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils';
 import { ImageField } from '../../../new_fields/URLField';
 import SharingManager from '../../util/SharingManager';
 import { Scripting } from '../../util/Scripting';
+import { InteractionUtils } from '../../util/InteractionUtils';
 
 library.add(fa.faTrash);
 library.add(fa.faShare);
@@ -248,6 +249,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         document.addEventListener("pointermove", this.onPointerMove);
         document.addEventListener("pointerup", this.onPointerUp);
     }
+
     onPointerMove = (e: PointerEvent): void => {
         if (e.cancelBubble && this.active) {
             document.removeEventListener("pointermove", this.onPointerMove); // stop listening to pointerMove if something else has stopPropagated it (e.g., the MarqueeView)
@@ -264,6 +266,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
             e.preventDefault();
         }
     }
+
     onPointerUp = (e: PointerEvent): void => {
         document.removeEventListener("pointermove", this.onPointerMove);
         document.removeEventListener("pointerup", this.onPointerUp);
@@ -611,11 +614,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
             DataDoc={this.props.DataDoc} />);
     }
 
-    handle1Pointer = (e: TouchEvent) => {
-        e.stopPropagation();
-        e.preventDefault();
-    }
-
     render() {
         let animDims = this.props.Document.animateToDimensions ? Array.from(Cast(this.props.Document.animateToDimensions, listSpec("number"))!) : undefined;
         const ruleColor = this.props.ruleProvider ? StrCast(this.props.ruleProvider["ruleColor_" + this.Document.heading]) : undefined;
diff --git a/src/client/views/pdf/PDFMenu.scss b/src/client/views/pdf/PDFMenu.scss
index b06d19c53..3c08ba80d 100644
--- a/src/client/views/pdf/PDFMenu.scss
+++ b/src/client/views/pdf/PDFMenu.scss
@@ -1,36 +1,6 @@
-.pdfMenu-cont {
-    position: absolute;
-    z-index: 10000;
-    height: 35px;
-    background: #323232;
-    box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.25);
-    border-radius: 0px 6px 6px 6px;
-    overflow: hidden;
-    display: flex;
-
-    .pdfMenu-button {
-        background-color: transparent;
-        width: 35px;
-        height: 35px;
-    }
-
-    .pdfMenu-button:hover {
-        background-color: #121212;
-    }
-
-    .pdfMenu-dragger {
-        height: 100%;
-        transition: width .2s;
-        background-image: url("https://logodix.com/logo/1020374.png");
-        background-size: 90% 100%;
-        background-repeat: no-repeat;
-        background-position: left center;
-    }
-
-    .pdfMenu-addTag {
-        display: grid;
-        width: 200px;
-        padding: 5px;
-        grid-template-columns: 90px 20px 90px;
-    }
+.pdfMenu-addTag {
+    display: grid;
+    width: 200px;
+    padding: 5px;
+    grid-template-columns: 90px 20px 90px;
 }
\ No newline at end of file
diff --git a/src/client/views/pdf/PDFMenu.tsx b/src/client/views/pdf/PDFMenu.tsx
index e62542014..1997ee0f5 100644
--- a/src/client/views/pdf/PDFMenu.tsx
+++ b/src/client/views/pdf/PDFMenu.tsx
@@ -3,39 +3,29 @@ import "./PDFMenu.scss";
 import { observable, action, } from "mobx";
 import { observer } from "mobx-react";
 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { emptyFunction, returnFalse } from "../../../Utils";
-import { Doc } from "../../../new_fields/Doc";
+import { unimplementedFunction, returnFalse } from "../../../Utils";
+import AntimodeMenu from "../AntimodeMenu";
 
 @observer
-export default class PDFMenu extends React.Component {
+export default class PDFMenu extends AntimodeMenu {
     static Instance: PDFMenu;
 
-    private _offsetY: number = 0;
-    private _offsetX: number = 0;
-    private _mainCont: React.RefObject<HTMLDivElement> = React.createRef();
     private _commentCont = React.createRef<HTMLButtonElement>();
     private _snippetButton: React.RefObject<HTMLButtonElement> = React.createRef();
-    private _dragging: boolean = false;
 
-    @observable private _top: number = -300;
-    @observable private _left: number = -300;
-    @observable private _opacity: number = 1;
-    @observable private _transition: string = "opacity 0.5s";
-    @observable private _transitionDelay: string = "";
     @observable private _keyValue: string = "";
     @observable private _valueValue: string = "";
     @observable private _added: boolean = false;
 
     @observable public Highlighting: boolean = false;
     @observable public Status: "pdf" | "annotation" | "snippet" | "" = "";
-    @observable public Pinned: boolean = false;
 
-    public StartDrag: (e: PointerEvent, ele: HTMLElement) => void = emptyFunction;
-    public Highlight: (color: string) => void = emptyFunction;
-    public Delete: () => void = emptyFunction;
-    public Snippet: (marquee: { left: number, top: number, width: number, height: number }) => void = emptyFunction;
+    public StartDrag: (e: PointerEvent, ele: HTMLElement) => void = unimplementedFunction;
+    public Highlight: (color: string) => void = unimplementedFunction;
+    public Delete: () => void = unimplementedFunction;
+    public Snippet: (marquee: { left: number, top: number, width: number, height: number }) => void = unimplementedFunction;
     public AddTag: (key: string, value: string) => boolean = returnFalse;
-    public PinToPres: () => void = emptyFunction;
+    public PinToPres: () => void = unimplementedFunction;
     public Marquee: { left: number; top: number; width: number; height: number; } | undefined;
 
     constructor(props: Readonly<{}>) {
@@ -72,87 +62,12 @@ export default class PDFMenu extends React.Component {
         e.preventDefault();
     }
 
-    @action
-    jumpTo = (x: number, y: number, forceJump: boolean = false) => {
-        if (!this.Pinned || forceJump) {
-            this._transition = this._transitionDelay = "";
-            this._opacity = 1;
-            this._left = x;
-            this._top = y;
-        }
-    }
-
-    @action
-    fadeOut = (forceOut: boolean) => {
-        if (!this.Pinned) {
-            if (this._opacity === 0.2) {
-                this._transition = "opacity 0.1s";
-                this._transitionDelay = "";
-                this._opacity = 0;
-                this._left = this._top = -300;
-            }
-
-            if (forceOut) {
-                this._transition = "";
-                this._transitionDelay = "";
-                this._opacity = 0;
-                this._left = this._top = -300;
-            }
-        }
-    }
-
-    @action
-    pointerLeave = (e: React.PointerEvent) => {
-        if (!this.Pinned) {
-            this._transition = "opacity 0.5s";
-            this._transitionDelay = "1s";
-            this._opacity = 0.2;
-            setTimeout(() => this.fadeOut(false), 3000);
-        }
-    }
-
-    @action
-    pointerEntered = (e: React.PointerEvent) => {
-        this._transition = "opacity 0.1s";
-        this._transitionDelay = "";
-        this._opacity = 1;
-    }
-
     @action
     togglePin = (e: React.MouseEvent) => {
         this.Pinned = !this.Pinned;
         !this.Pinned && (this.Highlighting = false);
     }
 
-    dragStart = (e: React.PointerEvent) => {
-        document.removeEventListener("pointermove", this.dragging);
-        document.addEventListener("pointermove", this.dragging);
-        document.removeEventListener("pointerup", this.dragEnd);
-        document.addEventListener("pointerup", this.dragEnd);
-
-        this._offsetX = this._mainCont.current!.getBoundingClientRect().width - e.nativeEvent.offsetX;
-        this._offsetY = e.nativeEvent.offsetY;
-
-        e.stopPropagation();
-        e.preventDefault();
-    }
-
-    @action
-    dragging = (e: PointerEvent) => {
-        this._left = e.pageX - this._offsetX;
-        this._top = e.pageY - this._offsetY;
-
-        e.stopPropagation();
-        e.preventDefault();
-    }
-
-    dragEnd = (e: PointerEvent) => {
-        document.removeEventListener("pointermove", this.dragging);
-        document.removeEventListener("pointerup", this.dragEnd);
-        e.stopPropagation();
-        e.preventDefault();
-    }
-
     @action
     highlightClicked = (e: React.MouseEvent) => {
         if (!this.Pinned) {
@@ -168,11 +83,6 @@ export default class PDFMenu extends React.Component {
         this.Delete();
     }
 
-    handleContextMenu = (e: React.MouseEvent) => {
-        e.stopPropagation();
-        e.preventDefault();
-    }
-
     snippetStart = (e: React.PointerEvent) => {
         document.removeEventListener("pointermove", this.snippetDrag);
         document.addEventListener("pointermove", this.snippetDrag);
@@ -223,33 +133,27 @@ export default class PDFMenu extends React.Component {
     render() {
         let buttons = this.Status === "pdf" || this.Status === "snippet" ?
             [
-                <button key="1" className="pdfMenu-button" title="Click to Highlight" onClick={this.highlightClicked} style={this.Highlighting ? { backgroundColor: "#121212" } : {}}>
+                <button key="1" className="antimodeMenu-button" title="Click to Highlight" onClick={this.highlightClicked} style={this.Highlighting ? { backgroundColor: "#121212" } : {}}>
                     <FontAwesomeIcon icon="highlighter" size="lg" style={{ transition: "transform 0.1s", transform: this.Highlighting ? "" : "rotate(-45deg)" }} /></button>,
-                <button key="2" className="pdfMenu-button" title="Drag to Annotate" ref={this._commentCont} onPointerDown={this.pointerDown}>
+                <button key="2" className="antimodeMenu-button" title="Drag to Annotate" ref={this._commentCont} onPointerDown={this.pointerDown}>
                     <FontAwesomeIcon icon="comment-alt" size="lg" /></button>,
-                <button key="3" className="pdfMenu-button" title="Drag to Snippetize Selection" style={{ display: this.Status === "snippet" ? "" : "none" }} onPointerDown={this.snippetStart} ref={this._snippetButton}>
+                <button key="3" className="antimodeMenu-button" title="Drag to Snippetize Selection" style={{ display: this.Status === "snippet" ? "" : "none" }} onPointerDown={this.snippetStart} ref={this._snippetButton}>
                     <FontAwesomeIcon icon="cut" size="lg" /></button>,
-                <button key="4" className="pdfMenu-button" title="Pin Menu" onClick={this.togglePin} style={this.Pinned ? { backgroundColor: "#121212" } : {}}>
+                <button key="4" className="antimodeMenu-button" title="Pin Menu" onClick={this.togglePin} style={this.Pinned ? { backgroundColor: "#121212" } : {}}>
                     <FontAwesomeIcon icon="thumbtack" size="lg" style={{ transition: "transform 0.1s", transform: this.Pinned ? "rotate(45deg)" : "" }} /> </button>
             ] : [
-                <button key="5" className="pdfMenu-button" title="Delete Anchor" onPointerDown={this.deleteClicked}>
+                <button key="5" className="antimodeMenu-button" title="Delete Anchor" onPointerDown={this.deleteClicked}>
                     <FontAwesomeIcon icon="trash-alt" size="lg" /></button>,
-                <button key="6" className="pdfMenu-button" title="Pin to Presentation" onPointerDown={this.PinToPres}>
+                <button key="6" className="antimodeMenu-button" title="Pin to Presentation" onPointerDown={this.PinToPres}>
                     <FontAwesomeIcon icon="map-pin" size="lg" /></button>,
                 <div key="7" className="pdfMenu-addTag" >
                     <input onChange={this.keyChanged} placeholder="Key" style={{ gridColumn: 1 }} />
                     <input onChange={this.valueChanged} placeholder="Value" style={{ gridColumn: 3 }} />
                 </div>,
-                <button key="8" className="pdfMenu-button" title={`Add tag: ${this._keyValue} with value: ${this._valueValue}`} onPointerDown={this.addTag}>
+                <button key="8" className="antimodeMenu-button" title={`Add tag: ${this._keyValue} with value: ${this._valueValue}`} onPointerDown={this.addTag}>
                     <FontAwesomeIcon style={{ transition: "all .2s" }} color={this._added ? "#42f560" : "white"} icon="check" size="lg" /></button>,
             ];
 
-        return (
-            <div className="pdfMenu-cont" onPointerLeave={this.pointerLeave} onPointerEnter={this.pointerEntered} ref={this._mainCont} onContextMenu={this.handleContextMenu}
-                style={{ left: this._left, top: this._top, opacity: this._opacity, transition: this._transition, transitionDelay: this._transitionDelay }}>
-                {buttons}
-                <div className="pdfMenu-dragger" onPointerDown={this.dragStart} style={{ width: this.Pinned ? "20px" : "0px" }} />
-            </div >
-        );
+        return this.getElement(buttons);
     }
 }
\ No newline at end of file
-- 
cgit v1.2.3-70-g09d2


From bd796f24677b2aacd32a371da6d2b7c7eb354018 Mon Sep 17 00:00:00 2001
From: Stanley Yip <stanley_yip@brown.edu>
Date: Sun, 13 Oct 2019 15:48:38 -0400
Subject: pinching code improved

---
 src/client/util/InteractionUtils.ts                | 14 ++++++-----
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 29 ++++++++++++++++------
 2 files changed, 30 insertions(+), 13 deletions(-)

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

diff --git a/src/client/util/InteractionUtils.ts b/src/client/util/InteractionUtils.ts
index 63cba4982..e58635a6f 100644
--- a/src/client/util/InteractionUtils.ts
+++ b/src/client/util/InteractionUtils.ts
@@ -28,13 +28,15 @@ export namespace InteractionUtils {
      * @param oldPoint2 - previous point 2
      */
     export function Pinching(pt1: React.Touch, pt2: React.Touch, oldPoint1: React.Touch, oldPoint2: React.Touch): number {
-        const leniency = 10;
-        let dist1 = TwoPointEuclidist(oldPoint1, pt1) + leniency;
-        let dist2 = TwoPointEuclidist(oldPoint2, pt2) + leniency;
+        let threshold = 4;
+        let oldDist = TwoPointEuclidist(oldPoint1, oldPoint2);
+        let newDist = TwoPointEuclidist(pt1, pt2);
 
-        if (Math.sign(dist1) === Math.sign(dist2)) {
-            let oldDist = TwoPointEuclidist(oldPoint1, oldPoint2);
-            let newDist = TwoPointEuclidist(pt1, pt2);
+        /** if they have the same sign, then we are either pinching in or out.
+          * threshold it by 10 (it has to be pinching by at least threshold to be a valid pinch)
+          * so that it can still pan without freaking out
+          */
+        if (Math.sign(oldDist) === Math.sign(newDist) && Math.abs(oldDist - newDist) > threshold) {
             return Math.sign(oldDist - newDist);
         }
         return 0;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 54d5b95b6..915126742 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -289,7 +289,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
     }
 
     @action
-    pan = (e: PointerEvent | React.Touch): void => {
+    pan = (e: PointerEvent | React.Touch | { clientX: number, clientY: number }): void => {
         // I think it makes sense for the marquee menu to go away when panned. -syip2
         MarqueeOptionsMenu.Instance.fadeOut(true);
 
@@ -329,8 +329,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
             if (ranges[1][1] - dy < (this.panY() - panelDim[1] / 2)) y = ranges[1][0] - panelDim[1] / 2;
         }
         this.setPan(x - dx, y - dy);
-        this._lastX = e.pageX;
-        this._lastY = e.pageY;
+        this._lastX = e.clientX;
+        this._lastY = e.clientY;
     }
 
     @action
@@ -386,11 +386,25 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
                         let d2 = Math.sqrt(Math.pow(pt2.clientX - oldPoint2.clientX, 2) + Math.pow(pt2.clientY - oldPoint2.clientY, 2));
                         let centerX = Math.min(pt1.clientX, pt2.clientX) + Math.abs(pt2.clientX - pt1.clientX) / 2;
                         let centerY = Math.min(pt1.clientY, pt2.clientY) + Math.abs(pt2.clientY - pt1.clientY) / 2;
-                        let delta = dir * (d1 + d2);
-                        this.zoom(centerX, centerY, delta, 250);
+
+                        // calculate the raw delta value
+                        let rawDelta = (dir * (d1 + d2));
+
+                        // this floors and ceils the delta value to prevent jitteriness
+                        let delta = Math.sign(rawDelta) * Math.min(Math.abs(rawDelta), 16);
+                        this.zoom(centerX, centerY, delta);
                         this.prevPoints.set(pt1.identifier, pt1);
                         this.prevPoints.set(pt2.identifier, pt2);
                     }
+                    // this is not zooming. derive some form of panning from it.
+                    else {
+                        // use the centerx and centery as the "new mouse position"
+                        let centerX = Math.min(pt1.clientX, pt2.clientX) + Math.abs(pt2.clientX - pt1.clientX) / 2;
+                        let centerY = Math.min(pt1.clientY, pt2.clientY) + Math.abs(pt2.clientY - pt1.clientY) / 2;
+                        this.pan({ clientX: centerX, clientY: centerY });
+                        this._lastX = centerX;
+                        this._lastY = centerY;
+                    }
                 }
             }
         }
@@ -406,7 +420,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
     }
 
     @action
-    zoom = (pointX: number, pointY: number, deltaY: number, coefficient: number): void => {
+    zoom = (pointX: number, pointY: number, deltaY: number): void => {
+        console.log(deltaY);
         let deltaScale = deltaY > 0 ? (1 / 1.1) : 1.1;
         if (deltaScale * this.zoomScaling() < 1 && this.isAnnotationOverlay) {
             deltaScale = 1 / this.zoomScaling();
@@ -428,7 +443,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         }
         else if (this.props.active()) {
             e.stopPropagation();
-            this.zoom(e.clientX, e.clientY, e.deltaY, 1)
+            this.zoom(e.clientX, e.clientY, e.deltaY)
         }
     }
 
-- 
cgit v1.2.3-70-g09d2


From 3cff8e7d101a528e392d885420de118cccca6ae5 Mon Sep 17 00:00:00 2001
From: Stanley Yip <stanley_yip@brown.edu>
Date: Tue, 22 Oct 2019 16:39:52 -0400
Subject: touch + inking can now pan with two fingers

---
 src/client/views/InkingCanvas.tsx                  | 35 +++++++++++++++++-----
 src/client/views/Touchable.tsx                     | 20 ++++++++++---
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 15 ++++++++--
 3 files changed, 57 insertions(+), 13 deletions(-)

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

diff --git a/src/client/views/InkingCanvas.tsx b/src/client/views/InkingCanvas.tsx
index 7651060af..2f3083cfe 100644
--- a/src/client/views/InkingCanvas.tsx
+++ b/src/client/views/InkingCanvas.tsx
@@ -10,6 +10,8 @@ import { UndoManager } from "../util/UndoManager";
 import { StrokeData, InkField, InkTool } from "../../new_fields/InkField";
 import { Doc } from "../../new_fields/Doc";
 import { Cast, PromiseValue, NumCast } from "../../new_fields/Types";
+import { Touchable } from "./Touchable";
+import { InteractionUtils } from "../util/InteractionUtils";
 
 interface InkCanvasProps {
     getScreenTransform: () => Transform;
@@ -20,7 +22,7 @@ interface InkCanvasProps {
 }
 
 @observer
-export class InkingCanvas extends React.Component<InkCanvasProps> {
+export class InkingCanvas extends Touchable<InkCanvasProps> {
     maxCanvasDim = 8192 / 2; // 1/2 of the maximum canvas dimension for Chrome
     @observable inkMidX: number = 0;
     @observable inkMidY: number = 0;
@@ -93,6 +95,18 @@ export class InkingCanvas extends React.Component<InkCanvasProps> {
         }
     }
 
+    @action
+    handle1PointerMove = (e: TouchEvent) => {
+        e.stopPropagation();
+        e.preventDefault();
+        let pointer = e.targetTouches.item(0);
+        if (pointer) {
+            this.handleMove(pointer.clientX, pointer.clientY);
+        }
+    }
+
+    handle2PointersMove = () => { }
+
     @action
     onPointerUp = (e: PointerEvent): void => {
         document.removeEventListener("pointermove", this.onPointerMove, true);
@@ -116,21 +130,28 @@ export class InkingCanvas extends React.Component<InkCanvasProps> {
         batch.end();
     }
 
-    @action
-    onPointerMove = (e: PointerEvent): void => {
-        e.stopPropagation();
-        e.preventDefault();
+    handleMove = (x: number, y: number) => {
         if (InkingControl.Instance.selectedTool !== InkTool.Eraser) {
             let data = this.inkData;  // add points to new line as it is being drawn
             let strokeData = data.get(this._currentStrokeId);
             if (strokeData) {
-                strokeData.pathData.push(this.relativeCoordinatesForEvent(e.clientX, e.clientY));
+                strokeData.pathData.push(this.relativeCoordinatesForEvent(x, y));
                 data.set(this._currentStrokeId, strokeData);
             }
             this.inkData = data;
         }
     }
 
+    @action
+    onPointerMove = (e: PointerEvent): void => {
+        if (InteractionUtils.IsType(e, InteractionUtils.TOUCH)) {
+            return;
+        }
+        e.stopPropagation();
+        e.preventDefault();
+        this.handleMove(e.clientX, e.clientY);
+    }
+
     relativeCoordinatesForEvent = (ex: number, ey: number): { x: number, y: number } => {
         let [x, y] = this.props.getScreenTransform().transformPoint(ex, ey);
         return { x, y };
@@ -183,7 +204,7 @@ export class InkingCanvas extends React.Component<InkCanvasProps> {
         let svgCanvasStyle = InkingControl.Instance.selectedTool !== InkTool.None && !this.props.Document.isBackground ? "canSelect" : "noSelect";
         return (
             <div className="inkingCanvas">
-                <div className={`inkingCanvas-${svgCanvasStyle}`} onPointerDown={this.onPointerDown} onTouchStart={(e) => e.stopPropagation()} />
+                <div className={`inkingCanvas-${svgCanvasStyle}`} onPointerDown={this.onPointerDown} onTouchStart={this.onTouchStart} />
                 {this.props.children()}
                 {this.drawnPaths}
             </div >
diff --git a/src/client/views/Touchable.tsx b/src/client/views/Touchable.tsx
index e9671ab8b..4955129ba 100644
--- a/src/client/views/Touchable.tsx
+++ b/src/client/views/Touchable.tsx
@@ -20,6 +20,15 @@ export abstract class Touchable<T> extends React.Component<T> {
             let pt = e.targetTouches.item(i);
             this.prevPoints.set(pt.identifier, pt);
         }
+
+        switch (e.targetTouches.length) {
+            case 1:
+                this.handle1PointerDown();
+                break;
+            case 2:
+                this.handle2PointersDown(e);
+        }
+
         document.removeEventListener("touchmove", this.onTouch);
         document.addEventListener("touchmove", this.onTouch);
         document.removeEventListener("touchend", this.onTouchEnd);
@@ -36,10 +45,10 @@ export abstract class Touchable<T> extends React.Component<T> {
         this._touchDrag = true;
         switch (e.targetTouches.length) {
             case 1:
-                this.handle1Pointer(e)
+                this.handle1PointerMove(e)
                 break;
             case 2:
-                this.handle2Pointers(e);
+                this.handle2PointersMove(e);
                 break;
         }
     }
@@ -70,13 +79,16 @@ export abstract class Touchable<T> extends React.Component<T> {
         document.removeEventListener("touchend", this.onTouchEnd);
     }
 
-    handle1Pointer = (e: TouchEvent): any => {
+    handle1PointerMove = (e: TouchEvent): any => {
         e.stopPropagation();
         e.preventDefault();
     }
 
-    handle2Pointers = (e: TouchEvent): any => {
+    handle2PointersMove = (e: TouchEvent): any => {
         e.stopPropagation();
         e.preventDefault();
     }
+
+    handle1PointerDown = (): any => { };
+    handle2PointersDown = (e: React.TouchEvent): any => { };
 }
\ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 440a0a8e5..123941b03 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -356,7 +356,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         }
     }
 
-    handle1Pointer = (e: TouchEvent) => {
+    handle1PointerMove = (e: TouchEvent) => {
         // panning a workspace
         if (!e.cancelBubble && this.props.active()) {
             let pt = e.targetTouches.item(0);
@@ -368,7 +368,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         }
     }
 
-    handle2Pointers = (e: TouchEvent) => {
+    handle2PointersMove = (e: TouchEvent) => {
         // pinch zooming
         if (!e.cancelBubble) {
             let pt1: Touch | null = e.targetTouches.item(0);
@@ -413,6 +413,17 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         e.preventDefault();
     }
 
+    handle2PointersDown = (e: React.TouchEvent) => {
+        let pt1: React.Touch | null = e.targetTouches.item(0);
+        let pt2: React.Touch | null = e.targetTouches.item(1);
+        if (!pt1 || !pt2) return;
+
+        let centerX = Math.min(pt1.clientX, pt2.clientX) + Math.abs(pt2.clientX - pt1.clientX) / 2;
+        let centerY = Math.min(pt1.clientY, pt2.clientY) + Math.abs(pt2.clientY - pt1.clientY) / 2;
+        this._lastX = centerX;
+        this._lastY = centerY;
+    }
+
     cleanUpInteractions = () => {
         document.removeEventListener("pointermove", this.onPointerMove);
         document.removeEventListener("pointerup", this.onPointerUp);
-- 
cgit v1.2.3-70-g09d2


From b7353705ee06292e570c9847d72287190f3f42ed Mon Sep 17 00:00:00 2001
From: Stanley Yip <stanley_yip@brown.edu>
Date: Tue, 29 Oct 2019 19:09:04 -0400
Subject: started implementing ink select with document decorations

---
 src/client/util/SelectionManager.ts                | 30 +++++++++++
 src/client/views/DocumentDecorations.tsx           | 57 ++++++++++++++-------
 src/client/views/InkSelectDecorations.scss         |  5 ++
 src/client/views/InkSelectDecorations.tsx          | 59 ++++++++++++++++++++++
 src/client/views/MainView.tsx                      |  2 +
 src/client/views/Touchable.tsx                     |  2 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  5 +-
 .../collections/collectionFreeForm/MarqueeView.tsx | 18 +++++--
 8 files changed, 152 insertions(+), 26 deletions(-)
 create mode 100644 src/client/views/InkSelectDecorations.scss
 create mode 100644 src/client/views/InkSelectDecorations.tsx

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

diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts
index 2d717ca57..3ae43e029 100644
--- a/src/client/util/SelectionManager.ts
+++ b/src/client/util/SelectionManager.ts
@@ -11,6 +11,7 @@ export namespace SelectionManager {
 
         @observable IsDragging: boolean = false;
         @observable SelectedDocuments: Array<DocumentView> = [];
+        @observable SelectedInk: Array<Map<any, any>> = [];
 
 
         @action
@@ -41,6 +42,20 @@ export namespace SelectionManager {
         DeselectAll(): void {
             manager.SelectedDocuments.map(dv => dv.props.whenActiveChanged(false));
             manager.SelectedDocuments = [];
+            manager.SelectedInk = [];
+        }
+
+        @action
+        SelectInk(ink: Map<any, any>, ctrlPressed: boolean): void {
+            if (manager.SelectedInk.indexOf(ink) === -1) {
+                if (!ctrlPressed) {
+                    this.DeselectAll();
+                }
+
+                manager.SelectedInk.push(ink);
+            } else if (!ctrlPressed && manager.SelectedDocuments.length > 1) {
+                manager.SelectedInk = [ink];
+            }
         }
     }
 
@@ -53,6 +68,10 @@ export namespace SelectionManager {
         manager.SelectDoc(docView, ctrlPressed);
     }
 
+    export function SelectInk(ink: Map<any, any>, ctrlPressed: boolean): void {
+        manager.SelectInk(ink, ctrlPressed);
+    }
+
     export function IsSelected(doc: DocumentView): boolean {
         return manager.SelectedDocuments.indexOf(doc) !== -1;
     }
@@ -75,4 +94,15 @@ export namespace SelectionManager {
     export function SelectedDocuments(): Array<DocumentView> {
         return manager.SelectedDocuments.slice();
     }
+
+    export function SelectedInk(): Array<Map<any, any>> {
+        return manager.SelectedInk.slice();
+    }
+
+    export function AllSelected(): Array<DocumentView | Map<any, any>> {
+        let arr: Array<DocumentView | Map<any, any>> = [];
+        arr = SelectionManager.SelectedDocuments();
+        arr.push(...SelectionManager.SelectedInk());
+        return arr;
+    }
 }
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 3d73f048d..90d6e1e8d 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -25,6 +25,8 @@ import { FormattedTextBox } from "./nodes/FormattedTextBox";
 import { IconBox } from "./nodes/IconBox";
 import React = require("react");
 import { TooltipTextMenu } from '../util/TooltipTextMenu';
+import { InkingCanvas } from './InkingCanvas';
+import { StrokeData } from '../../new_fields/InkField';
 const higflyout = require("@hig/flyout");
 export const { anchorPoints } = higflyout;
 export const Flyout = higflyout.default;
@@ -166,23 +168,42 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
     @computed
     get Bounds(): { x: number, y: number, b: number, r: number } {
         let x = this._forceUpdate;
-        this._lastBox = SelectionManager.SelectedDocuments().reduce((bounds, documentView) => {
-            if (documentView.props.renderDepth === 0 ||
-                Doc.AreProtosEqual(documentView.props.Document, CurrentUserUtils.UserDocument)) {
-                return bounds;
+        this._lastBox = SelectionManager.AllSelected().reduce((bounds, docViewOrInk) => {
+            if (docViewOrInk instanceof DocumentView) {
+                if (docViewOrInk.props.renderDepth === 0 ||
+                    Doc.AreProtosEqual(docViewOrInk.props.Document, CurrentUserUtils.UserDocument)) {
+                    return bounds;
+                }
+                let transform = (docViewOrInk.props.ScreenToLocalTransform().scale(docViewOrInk.props.ContentScaling())).inverse();
+                if (transform.TranslateX === 0 && transform.TranslateY === 0) {
+                    setTimeout(action(() => this._forceUpdate++), 0); // bcz: fix CollectionStackingView's getTransform() somehow...without this, resizing things in the library view, for instance, show the wrong bounds
+                    return this._lastBox;
+                }
+
+                var [sptX, sptY] = transform.transformPoint(0, 0);
+                let [bptX, bptY] = transform.transformPoint(docViewOrInk.props.PanelWidth(), docViewOrInk.props.PanelHeight());
+                return {
+                    x: Math.min(sptX, bounds.x), y: Math.min(sptY, bounds.y),
+                    r: Math.max(bptX, bounds.r), b: Math.max(bptY, bounds.b)
+                };
             }
-            let transform = (documentView.props.ScreenToLocalTransform().scale(documentView.props.ContentScaling())).inverse();
-            if (transform.TranslateX === 0 && transform.TranslateY === 0) {
-                setTimeout(action(() => this._forceUpdate++), 0); // bcz: fix CollectionStackingView's getTransform() somehow...without this, resizing things in the library view, for instance, show the wrong bounds
-                return this._lastBox;
+            else {
+                let left = bounds.x;
+                let top = bounds.y;
+                let right = bounds.r;
+                let bottom = bounds.b;
+                docViewOrInk.forEach((value: StrokeData, key: string) => {
+                    value.pathData.map(val => {
+                        left = Math.min(val.x, left);
+                        top = Math.min(val.y, top);
+                        right = Math.max(val.x, right);
+                        bottom = Math.max(val.y, bottom);
+                    });
+                });
+                return {
+                    x: left, y: top, r: right, b: bottom
+                };
             }
-
-            var [sptX, sptY] = transform.transformPoint(0, 0);
-            let [bptX, bptY] = transform.transformPoint(documentView.props.PanelWidth(), documentView.props.PanelHeight());
-            return {
-                x: Math.min(sptX, bounds.x), y: Math.min(sptY, bounds.y),
-                r: Math.max(bptX, bounds.r), b: Math.max(bptY, bounds.b)
-            };
         }, { x: Number.MAX_VALUE, y: Number.MAX_VALUE, r: Number.MIN_VALUE, b: Number.MIN_VALUE });
         return this._lastBox;
     }
@@ -559,7 +580,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
     }
     render() {
         var bounds = this.Bounds;
-        let seldoc = SelectionManager.SelectedDocuments().length ? SelectionManager.SelectedDocuments()[0] : undefined;
+        let seldoc = SelectionManager.AllSelected().length ? SelectionManager.AllSelected()[0] : undefined;
         if (bounds.x === Number.MAX_VALUE || !seldoc || this._hidden || isNaN(bounds.r) || isNaN(bounds.b) || isNaN(bounds.x) || isNaN(bounds.y)) {
             return (null);
         }
@@ -586,7 +607,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
                 left: bounds.x - this._resizeBorderWidth / 2,
                 top: bounds.y - this._resizeBorderWidth / 2,
                 pointerEvents: this.Interacting ? "none" : "all",
-                zIndex: SelectionManager.SelectedDocuments().length > 1 ? 900 : 0,
+                zIndex: SelectionManager.AllSelected().length > 1 ? 900 : 0,
             }} onPointerDown={this.onBackgroundDown} onContextMenu={(e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); }} >
             </div>
             <div className="documentDecorations-container" ref={this.setTextBar} style={{
@@ -615,7 +636,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
                 <div id="documentDecorations-bottomRightResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
                 <div id="documentDecorations-borderRadius" className="documentDecorations-radius" onPointerDown={this.onRadiusDown} onContextMenu={(e) => e.preventDefault()}><span className="borderRadiusTooltip" title="Drag Corner Radius"></span></div>
                 <div className="link-button-container">
-                    <DocumentButtonBar views={SelectionManager.SelectedDocuments()} />
+                    {(SelectionManager.SelectedDocuments.length && SelectionManager.SelectedDocuments()[0]) ? <DocumentButtonBar views={SelectionManager.SelectedDocuments()} /> : (null)}
                 </div>
             </div >
         </div>
diff --git a/src/client/views/InkSelectDecorations.scss b/src/client/views/InkSelectDecorations.scss
new file mode 100644
index 000000000..daff58fd6
--- /dev/null
+++ b/src/client/views/InkSelectDecorations.scss
@@ -0,0 +1,5 @@
+.inkSelectDecorations {
+    position: absolute;
+    border: black 1px solid;
+    z-index: 9001;
+}
\ No newline at end of file
diff --git a/src/client/views/InkSelectDecorations.tsx b/src/client/views/InkSelectDecorations.tsx
new file mode 100644
index 000000000..a8eef3305
--- /dev/null
+++ b/src/client/views/InkSelectDecorations.tsx
@@ -0,0 +1,59 @@
+import React = require("react");
+import { Touchable } from "./Touchable";
+import { StrokeData } from "../../new_fields/InkField";
+import { observer } from "mobx-react";
+import { computed, observable, action, runInAction } from "mobx";
+import "./InkSelectDecorations.scss"
+
+@observer
+export default class InkSelectDecorations extends Touchable {
+    static Instance: InkSelectDecorations;
+
+    @observable private _selectedInkNodes: Map<any, any> = new Map();
+
+    constructor(props: Readonly<{}>) {
+        super(props);
+
+        InkSelectDecorations.Instance = this;
+    }
+
+    @action
+    public SetSelected = (inkNodes: Map<any, any>, keepOld: boolean = false) => {
+        if (!keepOld) {
+            this._selectedInkNodes = new Map();
+        }
+        inkNodes.forEach((value: any, key: any) => {
+            runInAction(() => this._selectedInkNodes.set(key, value));
+        });
+    }
+
+    @computed
+    get Bounds(): { x: number, y: number, b: number, r: number } {
+        let left = Number.MAX_VALUE;
+        let top = Number.MAX_VALUE;
+        let right = -Number.MAX_VALUE;
+        let bottom = -Number.MAX_VALUE;
+        this._selectedInkNodes.forEach((value: StrokeData, key: string) => {
+            value.pathData.map(val => {
+                left = Math.min(val.x, left);
+                top = Math.min(val.y, top);
+                right = Math.max(val.x, right);
+                bottom = Math.max(val.y, bottom);
+            });
+        });
+        return { x: left, y: top, b: bottom, r: right };
+    }
+
+    render() {
+        let bounds = this.Bounds;
+        return (
+            <div style={{
+                top: bounds.y, left: bounds.x,
+                height: bounds.b - bounds.y,
+                width: bounds.r - bounds.x
+            }}>
+
+            </div>
+        )
+    }
+}
\ No newline at end of file
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 0ede0b770..41de32f1f 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -37,6 +37,7 @@ import { OverlayView } from './OverlayView';
 import PDFMenu from './pdf/PDFMenu';
 import { PreviewCursor } from './PreviewCursor';
 import MarqueeOptionsMenu from './collections/collectionFreeForm/MarqueeOptionsMenu';
+import InkSelectDecorations from './InkSelectDecorations';
 
 @observer
 export class MainView extends React.Component {
@@ -512,6 +513,7 @@ export class MainView extends React.Component {
             <SharingManager />
             <GoogleAuthenticationManager />
             <DocumentDecorations />
+            <InkSelectDecorations />
             {this.mainContent}
             <PreviewCursor />
             <ContextMenu />
diff --git a/src/client/views/Touchable.tsx b/src/client/views/Touchable.tsx
index 4955129ba..26779b168 100644
--- a/src/client/views/Touchable.tsx
+++ b/src/client/views/Touchable.tsx
@@ -2,7 +2,7 @@ import * as React from 'react';
 import { action } from 'mobx';
 import { InteractionUtils } from '../util/InteractionUtils';
 
-export abstract class Touchable<T> extends React.Component<T> {
+export abstract class Touchable<T = {}> extends React.Component<T> {
     protected _touchDrag: boolean = false;
     protected prevPoints: Map<number, React.Touch> = new Map<number, React.Touch>();
 
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 123941b03..c24e52aba 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -103,9 +103,10 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         added && this.updateCluster(newBox);
         return added;
     }
-    private selectDocuments = (docs: Doc[]) => {
+    private selectDocuments = (docs: 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));
     }
     public isCurrent(doc: Doc) { return !doc.isMinimized && (Math.abs(NumCast(doc.displayTimecode, -1) - NumCast(this.Document.currentTimecode, -1)) < 1.5 || NumCast(doc.displayTimecode, -1) === -1); }
 
@@ -190,7 +191,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
 
             // hacky way to get a list of DocumentViews in the current view given a list of Documents in the current view
             let prevSelected = SelectionManager.SelectedDocuments();
-            this.selectDocuments(eles);
+            this.selectDocuments(eles, []);
             let clusterDocs = SelectionManager.SelectedDocuments();
             SelectionManager.DeselectAll();
             prevSelected.map(dv => SelectionManager.SelectDoc(dv, true));
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 07db6354f..cd9ac7ecc 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -20,6 +20,7 @@ import { CollectionFreeFormView } from "./CollectionFreeFormView";
 import "./MarqueeView.scss";
 import React = require("react");
 import MarqueeOptionsMenu from "./MarqueeOptionsMenu";
+import InkSelectDecorations from "../../InkSelectDecorations";
 
 interface MarqueeViewProps {
     getContainerTransform: () => Transform;
@@ -27,7 +28,7 @@ interface MarqueeViewProps {
     container: CollectionFreeFormView;
     addDocument: (doc: Doc) => boolean;
     activeDocuments: () => Doc[];
-    selectDocuments: (docs: Doc[]) => void;
+    selectDocuments: (docs: Doc[], ink: Map<any, any>[]) => void;
     removeDocument: (doc: Doc) => boolean;
     addLiveTextDocument: (doc: Doc) => void;
     isSelected: () => boolean;
@@ -190,13 +191,14 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
 
     @action
     onPointerUp = (e: PointerEvent): void => {
-        if (!this.props.container.props.active()) this.props.selectDocuments([this.props.container.props.Document]);
+        if (!this.props.container.props.active()) this.props.selectDocuments([this.props.container.props.Document], []);
         if (this._visible) {
             let mselect = this.marqueeSelect();
             if (!e.shiftKey) {
                 SelectionManager.DeselectAll(mselect.length ? undefined : this.props.container.props.Document);
             }
-            this.props.selectDocuments(mselect.length ? mselect : [this.props.container.props.Document]);
+            this.props.selectDocuments(mselect.length ? mselect : [this.props.container.props.Document],
+                this.ink ? [this.marqueeInkSelect(this.ink.inkData)] : []);
         }
         if (!this._commandExecuted && (Math.abs(this.Bounds.height * this.Bounds.width) > 100)) {
             MarqueeOptionsMenu.Instance.createCollection = this.collection;
@@ -350,7 +352,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
         }
         let newCollection = this.getCollection(selected);
         this.props.addDocument(newCollection);
-        this.props.selectDocuments([newCollection]);
+        this.props.selectDocuments([newCollection], []);
         MarqueeOptionsMenu.Instance.fadeOut(true);
         this.hideMarquee();
     }
@@ -422,9 +424,14 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
         let centerShiftY = 0 - (this.Bounds.top + this.Bounds.height / 2);
         ink.forEach((value: StrokeData, key: string, map: any) => {
             if (InkingCanvas.IntersectStrokeRect(value, this.Bounds)) {
+                // let transform = this.props.container.props.ScreenToLocalTransform().scale(this.props.container.props.ContentScaling());
                 idata.set(key,
                     {
-                        pathData: value.pathData.map(val => ({ x: val.x + centerShiftX, y: val.y + centerShiftY })),
+                        pathData: value.pathData.map(val => {
+                            let tVal = this.props.getTransform().inverse().transformPoint(val.x, val.y);
+                            return { x: tVal[0], y: tVal[1] };
+                            // return { x: val.x + centerShiftX, y: val.y + centerShiftY }
+                        }),
                         color: value.color,
                         width: value.width,
                         tool: value.tool,
@@ -432,6 +439,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
                     });
             }
         });
+        // InkSelectDecorations.Instance.SetSelected(idata);
         return idata;
     }
 
-- 
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/collectionFreeForm/CollectionFreeFormView.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 563a8926c0646e9907c8a4eec2e648ab5ae79e02 Mon Sep 17 00:00:00 2001
From: yipstanley <stanley_yip@brown.edu>
Date: Sat, 9 Nov 2019 16:27:07 -0500
Subject: hey, Sam's pushing these changes

---
 src/client/documents/DocumentTypes.ts              |  3 +-
 src/client/documents/Documents.ts                  |  4 ++
 src/client/views/InkingCanvas.tsx                  | 64 +++++++++++-----------
 src/client/views/InkingStroke.tsx                  | 13 +++--
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 45 ++++++++++-----
 src/client/views/nodes/DocumentContentsView.tsx    |  2 +-
 6 files changed, 79 insertions(+), 52 deletions(-)

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

diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts
index 12501065a..f6dd0c346 100644
--- a/src/client/documents/DocumentTypes.ts
+++ b/src/client/documents/DocumentTypes.ts
@@ -24,5 +24,6 @@ export enum DocumentType {
     QUERY = "search",
     COLOR = "color",
     DOCULINK = "doculink",
-    PDFANNO = "pdfanno"
+    PDFANNO = "pdfanno",
+    INK = "ink"
 }
\ No newline at end of file
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index d1fcabc4a..2c6b40cb9 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -411,6 +411,10 @@ export namespace Docs {
             return InstanceFromProto(Prototypes.get(DocumentType.TEXT), "", options);
         }
 
+        export function InkDocument(options: DocumentOptions = {}) {
+            return InstanceFromProto(Prototypes.get(DocumentType.INK), "", options);
+        }
+
         export function IconDocument(icon: string, options: DocumentOptions = {}) {
             return InstanceFromProto(Prototypes.get(DocumentType.ICON), new IconField(icon), options);
         }
diff --git a/src/client/views/InkingCanvas.tsx b/src/client/views/InkingCanvas.tsx
index 5c17696c8..a0ea37300 100644
--- a/src/client/views/InkingCanvas.tsx
+++ b/src/client/views/InkingCanvas.tsx
@@ -170,37 +170,37 @@ export class InkingCanvas extends Touchable<InkCanvasProps> {
         this.inkData = data;
     }
 
-    @computed
-    get drawnPaths() {
-        let curTimecode = NumCast(this.props.Document.currentTimecode, -1);
-        let paths = Array.from(this.inkData).reduce((paths, [id, strokeData]) => {
-            if (strokeData.displayTimecode === -1 || (Math.abs(Math.round(strokeData.displayTimecode) - Math.round(curTimecode)) < 3)) {
-                paths.push(<InkingStroke key={id} id={id}
-                    line={strokeData.pathData}
-                    count={strokeData.pathData.length}
-                    offsetX={this.maxCanvasDim - this.inkMidX}
-                    offsetY={this.maxCanvasDim - this.inkMidY}
-                    color={strokeData.color}
-                    width={strokeData.width}
-                    tool={strokeData.tool}
-                    creationTime={strokeData.creationTime}
-                    deleteCallback={this.removeLine} />);
-            }
-            return paths;
-        }, [] as JSX.Element[]);
-        let markerPaths = paths.filter(path => path.props.tool === InkTool.Highlighter);
-        let penPaths = paths.filter(path => path.props.tool !== InkTool.Highlighter);
-        return [!penPaths.length ? (null) :
-            <svg className={`inkingCanvas-paths-ink`} key="Pens"
-                style={{ left: `${this.inkMidX - this.maxCanvasDim}px`, top: `${this.inkMidY - this.maxCanvasDim}px` }} >
-                {penPaths}
-            </svg>,
-        !markerPaths.length ? (null) :
-            <svg className={`inkingCanvas-paths-markers`} key="Markers"
-                style={{ left: `${this.inkMidX - this.maxCanvasDim}px`, top: `${this.inkMidY - this.maxCanvasDim}px` }}>
-                {markerPaths}
-            </svg>];
-    }
+    // @computed
+    // get drawnPaths() {
+    //     let curTimecode = NumCast(this.props.Document.currentTimecode, -1);
+    //     let paths = Array.from(this.inkData).reduce((paths, [id, strokeData]) => {
+    //         if (strokeData.displayTimecode === -1 || (Math.abs(Math.round(strokeData.displayTimecode) - Math.round(curTimecode)) < 3)) {
+    //             paths.push(<InkingStroke key={id} id={id}
+    //                 line={strokeData.pathData}
+    //                 count={strokeData.pathData.length}
+    //                 offsetX={this.maxCanvasDim - this.inkMidX}
+    //                 offsetY={this.maxCanvasDim - this.inkMidY}
+    //                 color={strokeData.color}
+    //                 width={strokeData.width}
+    //                 tool={strokeData.tool}
+    //                 creationTime={strokeData.creationTime}
+    //                 deleteCallback={this.removeLine} />);
+    //         }
+    //         return paths;
+    //     }, [] as JSX.Element[]);
+    //     let markerPaths = paths.filter(path => path.props.tool === InkTool.Highlighter);
+    //     let penPaths = paths.filter(path => path.props.tool !== InkTool.Highlighter);
+    //     return [!penPaths.length ? (null) :
+    //         <svg className={`inkingCanvas-paths-ink`} key="Pens"
+    //             style={{ left: `${this.inkMidX - this.maxCanvasDim}px`, top: `${this.inkMidY - this.maxCanvasDim}px` }} >
+    //             {penPaths}
+    //         </svg>,
+    //     !markerPaths.length ? (null) :
+    //         <svg className={`inkingCanvas-paths-markers`} key="Markers"
+    //             style={{ left: `${this.inkMidX - this.maxCanvasDim}px`, top: `${this.inkMidY - this.maxCanvasDim}px` }}>
+    //             {markerPaths}
+    //         </svg>];
+    // }
 
     render() {
         let svgCanvasStyle = InkingControl.Instance.selectedTool !== InkTool.None && !this.props.Document.isBackground ? "canSelect" : "noSelect";
@@ -210,7 +210,7 @@ export class InkingCanvas extends Touchable<InkCanvasProps> {
             <div className="inkingCanvas">
                 <div className={`inkingCanvas-${svgCanvasStyle}`} onPointerDown={this.onPointerDown} onTouchStart={this.onTouchStart} style={{ cursor: cursor }} />
                 {this.props.children()}
-                {this.drawnPaths}
+                {/* {this.drawnPaths} */}
             </div >
         );
     }
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index a097a7991..824f40b1f 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -6,6 +6,10 @@ import { InkTool } from "../../new_fields/InkField";
 import "./InkingStroke.scss";
 import { AudioBox } from "./nodes/AudioBox";
 import { Doc } from "../../new_fields/Doc";
+import { createSchema, makeInterface } from "../../new_fields/Schema";
+import { documentSchema } from "../../new_fields/documentSchemas";
+import { DocExtendableComponent } from "./DocComponent";
+import { FieldViewProps, FieldView } from "./nodes/FieldView";
 
 
 interface StrokeProps {
@@ -21,13 +25,12 @@ interface StrokeProps {
     deleteCallback: (index: string) => void;
 }
 
-export type InkDocAndStroke = {
-    Document: Doc;
-    Ink: Map<any, any>;
-};
+type InkDocument = makeInterface<[typeof documentSchema]>;
+const InkDocument = makeInterface(documentSchema);
 
 @observer
-export class InkingStroke extends React.Component<StrokeProps> {
+export class InkingStroke extends DocExtendableComponent<FieldViewProps & StrokeProps, InkDocument>(InkDocument) {
+    public static LayoutString(fieldStr: string) { return FieldView.LayoutString(InkingStroke, fieldStr); }
 
     @observable private _strokeTool: InkTool = this.props.tool;
     @observable private _strokeColor: string = this.props.color;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 9acffc952..8294eaaec 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -5,7 +5,7 @@ import { action, computed, observable } from "mobx";
 import { observer } from "mobx-react";
 import { Doc, DocListCast, HeightSym, Opt, WidthSym } from "../../../../new_fields/Doc";
 import { Id } from "../../../../new_fields/FieldSymbols";
-import { InkField, StrokeData } from "../../../../new_fields/InkField";
+import { InkField, StrokeData, InkTool } from "../../../../new_fields/InkField";
 import { createSchema, makeInterface } from "../../../../new_fields/Schema";
 import { ScriptField } from "../../../../new_fields/ScriptField";
 import { BoolCast, Cast, DateCast, NumCast, StrCast } from "../../../../new_fields/Types";
@@ -39,6 +39,7 @@ import { InteractionUtils } from "../../../util/InteractionUtils";
 import MarqueeOptionsMenu from "./MarqueeOptionsMenu";
 import PDFMenu from "../../pdf/PDFMenu";
 import { documentSchema, positionSchema } from "../../../../new_fields/documentSchemas";
+import { InkingControl } from "../../InkingControl";
 
 library.add(faEye as any, faTable, faPaintBrush, faExpandArrowsAlt, faCompressArrowsAlt, faCompass, faUpload, faBraille, faChalkboard, faFileUpload);
 
@@ -263,6 +264,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         return clusterColor;
     }
 
+    @observable private _points: { x: number, y: number }[] = [];
+
     @action
     onPointerDown = (e: React.PointerEvent): void => {
         if (e.nativeEvent.cancelBubble) return;
@@ -272,8 +275,18 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
             document.removeEventListener("pointerup", this.onPointerUp);
             document.addEventListener("pointermove", this.onPointerMove);
             document.addEventListener("pointerup", this.onPointerUp);
-            this._lastX = e.pageX;
-            this._lastY = e.pageY;
+            if (InkingControl.Instance.selectedTool === InkTool.None) {
+                this._lastX = e.pageX;
+                this._lastY = e.pageY;
+            }
+            else {
+                e.stopPropagation();
+                e.preventDefault();
+
+                if (InkingControl.Instance.selectedTool !== InkTool.Eraser && InkingControl.Instance.selectedTool !== InkTool.Scrubber) {
+                    this._points.push({ x: e.pageX, y: e.pageY });
+                }
+            }
         }
     }
 
@@ -340,14 +353,19 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
             return;
         }
         if (!e.cancelBubble) {
-            if (this._hitCluster && this.tryDragCluster(e)) {
-                e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document'  however it does mark the event as cancelBubble=true which we test for in the move event handlers
-                e.preventDefault();
-                document.removeEventListener("pointermove", this.onPointerMove);
-                document.removeEventListener("pointerup", this.onPointerUp);
-                return;
+            if (InkingControl.Instance.selectedTool === InkTool.None) {
+                if (this._hitCluster && this.tryDragCluster(e)) {
+                    e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document'  however it does mark the event as cancelBubble=true which we test for in the move event handlers
+                    e.preventDefault();
+                    document.removeEventListener("pointermove", this.onPointerMove);
+                    document.removeEventListener("pointerup", this.onPointerUp);
+                    return;
+                }
+                this.pan(e);
+            }
+            if (InkingControl.Instance.selectedTool !== InkTool.Eraser && InkingControl.Instance.selectedTool !== InkTool.Scrubber) {
+                this._points.push({ x: e.clientX, y: e.clientY });
             }
-            this.pan(e);
             e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document'  however it does mark the event as cancelBubble=true which we test for in the move event handlers
             e.preventDefault();
         }
@@ -789,9 +807,10 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
                     <CollectionFreeFormViewPannableContents centeringShiftX={this.centeringShiftX} centeringShiftY={this.centeringShiftY}
                         easing={this.easing} zoomScaling={this.zoomScaling} panX={this.panX} panY={this.panY}>
                         {!this.extensionDoc ? (null) :
-                            <InkingCanvas getScreenTransform={this.getTransform} Document={this.props.Document} AnnotationDocument={this.extensionDoc} inkFieldKey={"ink"} >
-                                {this.childViews}
-                            </InkingCanvas>}
+                            // <InkingCanvas getScreenTransform={this.getTransform} Document={this.props.Document} AnnotationDocument={this.extensionDoc} inkFieldKey={"ink"} >
+                            this.childViews
+                            // </InkingCanvas>
+                        }
                         <CollectionFreeFormRemoteCursors {...this.props} key="remoteCursors" />
                     </CollectionFreeFormViewPannableContents>
                 </MarqueeView>
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index 96271cfe1..12ae5b6e5 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -99,7 +99,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
                     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
+                    ColorBox, DocuLinkBox, InkingStroke
                 }}
                 bindings={this.CreateBindings()}
                 jsx={this.layout}
-- 
cgit v1.2.3-70-g09d2


From c7c18eeea36b35ee9172a120352af84fe21f267b Mon Sep 17 00:00:00 2001
From: yipstanley <stanley_yip@brown.edu>
Date: Sun, 10 Nov 2019 16:27:56 -0500
Subject: inks are now dox

---
 src/client/documents/Documents.ts                  |  14 ++-
 src/client/util/SelectionManager.ts                |  32 ------
 src/client/views/DocumentDecorations.tsx           |  80 ++++++---------
 src/client/views/InkSelectDecorations.tsx          |  16 +--
 src/client/views/InkingCanvas.tsx                  |  12 +--
 src/client/views/InkingStroke.tsx                  | 107 +++++++++------------
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  64 +++++++++---
 .../collections/collectionFreeForm/MarqueeView.tsx |  92 +++++++++---------
 src/client/views/nodes/DocumentView.tsx            |   2 +-
 src/new_fields/InkField.ts                         |  20 ++--
 src/new_fields/documentSchemas.ts                  |   1 +
 11 files changed, 207 insertions(+), 233 deletions(-)

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 2c6b40cb9..a074d267e 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -48,6 +48,8 @@ import { PresElementBox } from "../views/presentationview/PresElementBox";
 import { QueryBox } from "../views/nodes/QueryBox";
 import { ColorBox } from "../views/nodes/ColorBox";
 import { DocuLinkBox } from "../views/nodes/DocuLinkBox";
+import { InkingStroke } from "../views/InkingStroke";
+import { InkField } from "../../new_fields/InkField";
 var requestImageSize = require('../util/request-image-size');
 var path = require('path');
 
@@ -107,6 +109,7 @@ export interface DocumentOptions {
     sourcePanel?: Doc; // panel to display in 'targetContainer' as the result of a button onClick script
     targetContainer?: Doc; // document whose proto will be set to 'panel' as the result of a onClick click script
     dropConverter?: ScriptField; // script to run when documents are dropped on this Document.
+    strokeWidth?: number;
     // [key: string]: Opt<Field>;
 }
 
@@ -209,6 +212,9 @@ export namespace Docs {
             [DocumentType.PRESELEMENT, {
                 layout: { view: PresElementBox, dataField: data }
             }],
+            [DocumentType.INK, {
+                layout: { view: InkingStroke, dataField: data }
+            }]
         ]);
 
         // All document prototypes are initialized with at least these values
@@ -411,8 +417,12 @@ export namespace Docs {
             return InstanceFromProto(Prototypes.get(DocumentType.TEXT), "", options);
         }
 
-        export function InkDocument(options: DocumentOptions = {}) {
-            return InstanceFromProto(Prototypes.get(DocumentType.INK), "", options);
+        export function InkDocument(color: string, tool: number, strokeWidth: number, points: { x: number, y: number }[], options: DocumentOptions = {}) {
+            let doc = InstanceFromProto(Prototypes.get(DocumentType.INK), new InkField(points), options);
+            doc.color = color;
+            doc.strokeWidth = strokeWidth;
+            doc.tool = tool;
+            return doc;
         }
 
         export function IconDocument(icon: string, options: DocumentOptions = {}) {
diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts
index 2a57c67bd..ca61f9014 100644
--- a/src/client/util/SelectionManager.ts
+++ b/src/client/util/SelectionManager.ts
@@ -3,8 +3,6 @@ import { Doc, Opt } from "../../new_fields/Doc";
 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 {
 
@@ -12,7 +10,6 @@ export namespace SelectionManager {
 
         @observable IsDragging: boolean = false;
         @observable SelectedDocuments: Array<DocumentView> = [];
-        @observable SelectedInk: Array<{ Document: Doc, Ink: Map<any, any> }> = [];
 
 
         @action
@@ -43,20 +40,6 @@ export namespace SelectionManager {
         DeselectAll(): void {
             manager.SelectedDocuments.map(dv => dv.props.whenActiveChanged(false));
             manager.SelectedDocuments = [];
-            manager.SelectedInk = [];
-        }
-
-        @action
-        SelectInk(ink: { Document: Doc, Ink: Map<any, any> }, ctrlPressed: boolean): void {
-            if (manager.SelectedInk.indexOf(ink) === -1) {
-                if (!ctrlPressed) {
-                    this.DeselectAll();
-                }
-
-                manager.SelectedInk.push(ink);
-            } else if (!ctrlPressed && manager.SelectedDocuments.length > 1) {
-                manager.SelectedInk = [ink];
-            }
         }
     }
 
@@ -69,10 +52,6 @@ export namespace SelectionManager {
         manager.SelectDoc(docView, ctrlPressed);
     }
 
-    export function SelectInk(ink: { Document: Doc, Ink: Map<any, any> }, ctrlPressed: boolean): void {
-        manager.SelectInk(ink, ctrlPressed);
-    }
-
     export function IsSelected(doc: DocumentView): boolean {
         return manager.SelectedDocuments.indexOf(doc) !== -1;
     }
@@ -95,15 +74,4 @@ export namespace SelectionManager {
     export function SelectedDocuments(): Array<DocumentView> {
         return manager.SelectedDocuments.slice();
     }
-
-    export function SelectedInk(): Array<{ Document: Doc, Ink: Map<any, any> }> {
-        return manager.SelectedInk.slice();
-    }
-
-    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 e208e5f3b..10764a9ce 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -24,7 +24,7 @@ import { DocumentView } from "./nodes/DocumentView";
 import { FieldView } from "./nodes/FieldView";
 import { IconBox } from "./nodes/IconBox";
 import React = require("react");
-import { StrokeData } from '../../new_fields/InkField';
+import { PointData } from '../../new_fields/InkField';
 const higflyout = require("@hig/flyout");
 export const { anchorPoints } = higflyout;
 export const Flyout = higflyout.default;
@@ -162,44 +162,23 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
     @computed
     get Bounds(): { x: number, y: number, b: number, r: number } {
         let x = this._forceUpdate;
-        this._lastBox = SelectionManager.AllSelected().reduce((bounds, docViewOrInk) => {
-            if (docViewOrInk instanceof DocumentView) {
-                if (docViewOrInk.props.renderDepth === 0 ||
-                    Doc.AreProtosEqual(docViewOrInk.props.Document, CurrentUserUtils.UserDocument)) {
-                    return bounds;
-                }
-                let transform = (docViewOrInk.props.ScreenToLocalTransform().scale(docViewOrInk.props.ContentScaling())).inverse();
-                if (transform.TranslateX === 0 && transform.TranslateY === 0) {
-                    setTimeout(action(() => this._forceUpdate++), 0); // bcz: fix CollectionStackingView's getTransform() somehow...without this, resizing things in the library view, for instance, show the wrong bounds
-                    return this._lastBox;
-                }
-
-                var [sptX, sptY] = transform.transformPoint(0, 0);
-                let [bptX, bptY] = transform.transformPoint(docViewOrInk.props.PanelWidth(), docViewOrInk.props.PanelHeight());
-                return {
-                    x: Math.min(sptX, bounds.x), y: Math.min(sptY, bounds.y),
-                    r: Math.max(bptX, bounds.r), b: Math.max(bptY, bounds.b)
-                };
+        this._lastBox = SelectionManager.SelectedDocuments().reduce((bounds, docView) => {
+            if (docView.props.renderDepth === 0 ||
+                Doc.AreProtosEqual(docView.props.Document, CurrentUserUtils.UserDocument)) {
+                return bounds;
             }
-            else {
-                let left = bounds.x;
-                let top = bounds.y;
-                let right = bounds.r;
-                let bottom = bounds.b;
-                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);
-                        bottom = Math.max(val.y, bottom);
-                    });
-                });
-                return {
-                    x: left, y: top, r: right, b: bottom
-                };
+            let transform = (docView.props.ScreenToLocalTransform().scale(docView.props.ContentScaling())).inverse();
+            if (transform.TranslateX === 0 && transform.TranslateY === 0) {
+                setTimeout(action(() => this._forceUpdate++), 0); // bcz: fix CollectionStackingView's getTransform() somehow...without this, resizing things in the library view, for instance, show the wrong bounds
+                return this._lastBox;
             }
+
+            var [sptX, sptY] = transform.transformPoint(0, 0);
+            let [bptX, bptY] = transform.transformPoint(docView.props.PanelWidth(), docView.props.PanelHeight());
+            return {
+                x: Math.min(sptX, bounds.x), y: Math.min(sptY, bounds.y),
+                r: Math.max(bptX, bounds.r), b: Math.max(bptY, bounds.b)
+            };
         }, { x: Number.MAX_VALUE, y: Number.MAX_VALUE, r: Number.MIN_VALUE, b: Number.MIN_VALUE });
         return this._lastBox;
     }
@@ -226,7 +205,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.AllSelected().map(docOrInk => docOrInk instanceof DocumentView ? docOrInk.ContentDiv! : (document.createElement("div"))), dragData, e.x, e.y, {
+        DragManager.StartDocumentDrag(SelectionManager.SelectedDocuments().map(docView => docView.ContentDiv!), dragData, e.x, e.y, {
             handlers: { dragComplete: action(() => this._hidden = this.Interacting = false) },
             hideSource: true
         });
@@ -550,21 +529,16 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
 
     @computed
     get selectionTitle(): string {
-        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();
-                }
+        if (SelectionManager.SelectedDocuments().length === 1) {
+            let selected = SelectionManager.SelectedDocuments()[0];
+            let field = selected.props.Document[this._fieldKey];
+            if (typeof field === "string") {
+                return field;
             }
-            else {
-                return "-ink strokes-";
+            else if (typeof field === "number") {
+                return field.toString();
             }
-        } else if (SelectionManager.AllSelected().length > 1) {
+        } else if (SelectionManager.SelectedDocuments().length > 1) {
             return "-multiple-";
         }
         return "-unset-";
@@ -590,7 +564,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
         let minimizeIcon = (
             <div className="documentDecorations-minimizeButton" onPointerDown={this.onMinimizeDown}>
                 {/* 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, "...")) : "..."}
+                {(SelectionManager.SelectedDocuments().length === 1) ? IconBox.DocumentIcon(StrCast(SelectionManager.SelectedDocuments()[0].props.Document.layout, "...")) : "..."}
             </div>);
 
         bounds.x = Math.max(0, bounds.x - this._resizeBorderWidth / 2) + this._resizeBorderWidth / 2;
@@ -611,7 +585,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
                 left: bounds.x - this._resizeBorderWidth / 2,
                 top: bounds.y - this._resizeBorderWidth / 2,
                 pointerEvents: this.Interacting ? "none" : "all",
-                zIndex: SelectionManager.AllSelected().length > 1 ? 900 : 0,
+                zIndex: SelectionManager.SelectedDocuments().length > 1 ? 900 : 0,
             }} onPointerDown={this.onBackgroundDown} onContextMenu={(e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); }} >
             </div>
             <div className="documentDecorations-container" ref={this.setTextBar} style={{
diff --git a/src/client/views/InkSelectDecorations.tsx b/src/client/views/InkSelectDecorations.tsx
index a8eef3305..95ccc1777 100644
--- a/src/client/views/InkSelectDecorations.tsx
+++ b/src/client/views/InkSelectDecorations.tsx
@@ -1,6 +1,6 @@
 import React = require("react");
 import { Touchable } from "./Touchable";
-import { StrokeData } from "../../new_fields/InkField";
+import { PointData } from "../../new_fields/InkField";
 import { observer } from "mobx-react";
 import { computed, observable, action, runInAction } from "mobx";
 import "./InkSelectDecorations.scss"
@@ -33,13 +33,13 @@ export default class InkSelectDecorations extends Touchable {
         let top = Number.MAX_VALUE;
         let right = -Number.MAX_VALUE;
         let bottom = -Number.MAX_VALUE;
-        this._selectedInkNodes.forEach((value: StrokeData, key: string) => {
-            value.pathData.map(val => {
-                left = Math.min(val.x, left);
-                top = Math.min(val.y, top);
-                right = Math.max(val.x, right);
-                bottom = Math.max(val.y, bottom);
-            });
+        this._selectedInkNodes.forEach((value: PointData, key: string) => {
+            // value.pathData.map(val => {
+            //     left = Math.min(val.x, left);
+            //     top = Math.min(val.y, top);
+            //     right = Math.max(val.x, right);
+            //     bottom = Math.max(val.y, bottom);
+            // });
         });
         return { x: left, y: top, b: bottom, r: right };
     }
diff --git a/src/client/views/InkingCanvas.tsx b/src/client/views/InkingCanvas.tsx
index a0ea37300..e5253c377 100644
--- a/src/client/views/InkingCanvas.tsx
+++ b/src/client/views/InkingCanvas.tsx
@@ -7,7 +7,7 @@ import { InkingControl } from "./InkingControl";
 import { InkingStroke } from "./InkingStroke";
 import React = require("react");
 import { UndoManager } from "../util/UndoManager";
-import { StrokeData, InkField, InkTool } from "../../new_fields/InkField";
+import { PointData, InkField, InkTool } from "../../new_fields/InkField";
 import { Doc } from "../../new_fields/Doc";
 import { Cast, PromiseValue, NumCast } from "../../new_fields/Types";
 import { Touchable } from "./Touchable";
@@ -26,15 +26,15 @@ export class InkingCanvas extends Touchable<InkCanvasProps> {
     maxCanvasDim = 8192 / 2; // 1/2 of the maximum canvas dimension for Chrome
     @observable inkMidX: number = 0;
     @observable inkMidY: number = 0;
-    private previousState?: Map<string, StrokeData>;
+    private previousState?: Map<string, PointData>;
     private _currentStrokeId: string = "";
-    public static IntersectStrokeRect(stroke: StrokeData, selRect: { left: number, top: number, width: number, height: number }): boolean {
+    public static IntersectStrokeRect(stroke: PointData, selRect: { left: number, top: number, width: number, height: number }): boolean {
         return stroke.pathData.reduce((inside: boolean, val) => inside ||
             (selRect.left < val.x && selRect.left + selRect.width > val.x &&
                 selRect.top < val.y && selRect.top + selRect.height > val.y)
             , false);
     }
-    public static StrokeRect(stroke: StrokeData): { left: number, top: number, right: number, bottom: number } {
+    public static StrokeRect(stroke: PointData): { left: number, top: number, right: number, bottom: number } {
         return stroke.pathData.reduce((bounds: { left: number, top: number, right: number, bottom: number }, val) =>
             ({
                 left: Math.min(bounds.left, val.x), top: Math.min(bounds.top, val.y),
@@ -58,12 +58,12 @@ export class InkingCanvas extends Touchable<InkCanvasProps> {
     }
 
     @computed
-    get inkData(): Map<string, StrokeData> {
+    get inkData(): Map<string, PointData> {
         let map = Cast(this.props.AnnotationDocument[this.props.inkFieldKey], InkField);
         return !map ? new Map : new Map(map.inkData);
     }
 
-    set inkData(value: Map<string, StrokeData>) {
+    set inkData(value: Map<string, PointData>) {
         this.props.AnnotationDocument[this.props.inkFieldKey] = new InkField(value);
     }
 
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index 824f40b1f..411b0d3a0 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -1,79 +1,66 @@
 import { observer } from "mobx-react";
-import { observable, trace, runInAction } from "mobx";
+import { observable, trace, runInAction, computed } from "mobx";
 import { InkingControl } from "./InkingControl";
 import React = require("react");
-import { InkTool } from "../../new_fields/InkField";
+import { InkTool, InkField, InkData } from "../../new_fields/InkField";
 import "./InkingStroke.scss";
 import { AudioBox } from "./nodes/AudioBox";
-import { Doc } from "../../new_fields/Doc";
-import { createSchema, makeInterface } from "../../new_fields/Schema";
+import { Doc, FieldResult } from "../../new_fields/Doc";
+import { createSchema, makeInterface, listSpec } from "../../new_fields/Schema";
 import { documentSchema } from "../../new_fields/documentSchemas";
 import { DocExtendableComponent } from "./DocComponent";
 import { FieldViewProps, FieldView } from "./nodes/FieldView";
-
-
-interface StrokeProps {
-    offsetX: number;
-    offsetY: number;
-    id: string;
-    count: number;
-    line: Array<{ x: number, y: number }>;
-    color: string;
-    width: string;
-    tool: InkTool;
-    creationTime: number;
-    deleteCallback: (index: string) => void;
-}
+import { Transform } from "../util/Transform";
+import { Cast, FieldValue } from "../../new_fields/Types";
+import { List } from "../../new_fields/List";
 
 type InkDocument = makeInterface<[typeof documentSchema]>;
 const InkDocument = makeInterface(documentSchema);
 
+export function CreatePolyline(points: { x: number, y: number }[], left: number, top: number, color?: string, width?: number) {
+    let pts = points.reduce((acc: string, pt: { x: number, y: number }) => acc + `${pt.x - left},${pt.y - top} `, "");
+    return (
+        <polyline
+            points={pts}
+            style={{
+                fill: "none",
+                stroke: color ?? InkingControl.Instance.selectedColor,
+                strokeWidth: width ?? InkingControl.Instance.selectedWidth
+            }}
+        />
+    );
+}
+
 @observer
-export class InkingStroke extends DocExtendableComponent<FieldViewProps & StrokeProps, InkDocument>(InkDocument) {
+export class InkingStroke extends DocExtendableComponent<FieldViewProps, InkDocument>(InkDocument) {
     public static LayoutString(fieldStr: string) { return FieldView.LayoutString(InkingStroke, fieldStr); }
 
-    @observable private _strokeTool: InkTool = this.props.tool;
-    @observable private _strokeColor: string = this.props.color;
-    @observable private _strokeWidth: string = this.props.width;
-
-    deleteStroke = (e: React.PointerEvent): void => {
-        if (InkingControl.Instance.selectedTool === InkTool.Eraser && e.buttons === 1) {
-            this.props.deleteCallback(this.props.id);
-            e.stopPropagation();
-            e.preventDefault();
-        }
-        if (InkingControl.Instance.selectedTool === InkTool.Scrubber && e.buttons === 1) {
-            AudioBox.SetScrubTime(this.props.creationTime);
-            e.stopPropagation();
-            e.preventDefault();
-        }
-    }
-
-    parseData = (line: Array<{ x: number, y: number }>): string => {
-        return !line.length ? "" : "M " + line.map(p => (p.x + this.props.offsetX) + " " + (p.y + this.props.offsetY)).join(" L ");
-    }
-
-    createStyle() {
-        switch (this._strokeTool) {
-            // add more tool styles here
-            default:
-                return {
-                    fill: "none",
-                    stroke: this._strokeColor,
-                    strokeWidth: this._strokeWidth + "px",
-                };
-        }
-    }
+    @computed get PanelWidth() { return this.props.PanelWidth(); }
+    @computed get PanelHeight() { return this.props.PanelHeight(); }
 
     render() {
-        let pathStyle = this.createStyle();
-        let pathData = this.parseData(this.props.line);
-        let pathlength = this.props.count; // bcz: this is needed to force reactions to the line's data changes
-        let marker = this.props.tool === InkTool.Highlighter ? "-marker" : "";
-
-        let pointerEvents: any = InkingControl.Instance.selectedTool === InkTool.Eraser ||
-            InkingControl.Instance.selectedTool === InkTool.Scrubber ? "all" : "none";
-        return (<path className={`inkingStroke${marker}`} d={pathData} style={{ ...pathStyle, pointerEvents: pointerEvents }}
-            strokeLinejoin="round" strokeLinecap="round" onPointerOver={this.deleteStroke} onPointerDown={this.deleteStroke} />);
+        // let pathData = this.parseData(this.props.line);
+        let data: InkData = Cast(this.Document.data, InkField) ?.inkData ?? [];
+        let xs = data.map(p => p.x);
+        let ys = data.map(p => p.y);
+        let left = Math.min(...xs);
+        let top = Math.min(...ys);
+        let right = Math.max(...xs);
+        let bottom = Math.max(...ys);
+        let points = CreatePolyline(data, 0, 0, this.Document.color, this.Document.strokeWidth);
+        let width = right - left;
+        let height = bottom - top;
+        let scaleX = this.PanelWidth / width;
+        let scaleY = this.PanelHeight / height;
+        // let pathlength = this.props.count; // bcz: this is needed to force reactions to the line's data changes
+        return (
+            <svg width={width} height={height} style={{
+                transformOrigin: "top left",
+                transform: `translate(${left}px, ${top}px) scale(${scaleX}, ${scaleY})`,
+                mixBlendMode: this.Document.tool === InkTool.Highlighter ? "multiply" : "unset"
+            }}>
+                {points}
+            </svg>
+        );
     }
 }
\ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 8294eaaec..21981c25e 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -5,7 +5,7 @@ import { action, computed, observable } from "mobx";
 import { observer } from "mobx-react";
 import { Doc, DocListCast, HeightSym, Opt, WidthSym } from "../../../../new_fields/Doc";
 import { Id } from "../../../../new_fields/FieldSymbols";
-import { InkField, StrokeData, InkTool } from "../../../../new_fields/InkField";
+import { InkField, PointData, InkTool } from "../../../../new_fields/InkField";
 import { createSchema, makeInterface } from "../../../../new_fields/Schema";
 import { ScriptField } from "../../../../new_fields/ScriptField";
 import { BoolCast, Cast, DateCast, NumCast, StrCast } from "../../../../new_fields/Types";
@@ -40,6 +40,7 @@ import MarqueeOptionsMenu from "./MarqueeOptionsMenu";
 import PDFMenu from "../../pdf/PDFMenu";
 import { documentSchema, positionSchema } from "../../../../new_fields/documentSchemas";
 import { InkingControl } from "../../InkingControl";
+import { InkingStroke, CreatePolyline } from "../../InkingStroke";
 
 library.add(faEye as any, faTable, faPaintBrush, faExpandArrowsAlt, faCompressArrowsAlt, faCompass, faUpload, faBraille, faChalkboard, faFileUpload);
 
@@ -108,10 +109,9 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         added && this.updateCluster(newBox);
         return added;
     }
-    private selectDocuments = (docs: Doc[], ink: { Document: Doc, Ink: Map<any, any> }[]) => {
+    private selectDocuments = (docs: Doc[]) => {
         SelectionManager.DeselectAll();
         docs.map(doc => DocumentManager.Instance.getDocumentView(doc)).map(dv => dv && SelectionManager.SelectDoc(dv, true));
-        ink.forEach(i => SelectionManager.SelectInk(i, true));
     }
     public isCurrent(doc: Doc) { return !doc.isMinimized && (Math.abs(NumCast(doc.displayTimecode, -1) - NumCast(this.Document.currentTimecode, -1)) < 1.5 || NumCast(doc.displayTimecode, -1) === -1); }
 
@@ -284,14 +284,25 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
                 e.preventDefault();
 
                 if (InkingControl.Instance.selectedTool !== InkTool.Eraser && InkingControl.Instance.selectedTool !== InkTool.Scrubber) {
-                    this._points.push({ x: e.pageX, y: e.pageY });
+                    let point = this.getTransform().transformPoint(e.pageX, e.pageY);
+                    this._points.push({ x: point[0], y: point[1] });
                 }
             }
         }
     }
 
+    @action
     onPointerUp = (e: PointerEvent): void => {
         if (InteractionUtils.IsType(e, InteractionUtils.TOUCH)) return;
+
+        if (this._points.length > 1) {
+            let B = this.svgBounds;
+            let points = this._points.map(p => ({ x: p.x - B.left, y: p.y - B.top }));
+            let inkDoc = Docs.Create.InkDocument(InkingControl.Instance.selectedColor, InkingControl.Instance.selectedTool, parseInt(InkingControl.Instance.selectedWidth), points, { width: B.width, height: B.height, x: B.left, y: B.top });
+            this.addDocument(inkDoc);
+            this._points = [];
+        }
+
         document.removeEventListener("pointermove", this.onPointerMove);
         document.removeEventListener("pointerup", this.onPointerUp);
         document.removeEventListener("touchmove", this.onTouch);
@@ -324,11 +335,11 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
             }, [[minx, maxx], [miny, maxy]]);
             let ink = this.extensionDoc && Cast(this.extensionDoc.ink, InkField);
             if (ink && ink.inkData) {
-                ink.inkData.forEach((value: StrokeData, key: string) => {
-                    let bounds = InkingCanvas.StrokeRect(value);
-                    ranges[0] = [Math.min(ranges[0][0], bounds.left), Math.max(ranges[0][1], bounds.right)];
-                    ranges[1] = [Math.min(ranges[1][0], bounds.top), Math.max(ranges[1][1], bounds.bottom)];
-                });
+                // ink.inkData.forEach((value: PointData, key: string) => {
+                //     let bounds = InkingCanvas.StrokeRect(value);
+                //     ranges[0] = [Math.min(ranges[0][0], bounds.left), Math.max(ranges[0][1], bounds.right)];
+                //     ranges[1] = [Math.min(ranges[1][0], bounds.top), Math.max(ranges[1][1], bounds.bottom)];
+                // });
             }
 
             let cscale = this.props.ContainingCollectionDoc ? NumCast(this.props.ContainingCollectionDoc.scale) : 1;
@@ -363,8 +374,9 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
                 }
                 this.pan(e);
             }
-            if (InkingControl.Instance.selectedTool !== InkTool.Eraser && InkingControl.Instance.selectedTool !== InkTool.Scrubber) {
-                this._points.push({ x: e.clientX, y: e.clientY });
+            else if (InkingControl.Instance.selectedTool !== InkTool.Eraser && InkingControl.Instance.selectedTool !== InkTool.Scrubber) {
+                let point = this.getTransform().transformPoint(e.clientX, e.clientY);
+                this._points.push({ x: point[0], y: point[1] });
             }
             e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document'  however it does mark the event as cancelBubble=true which we test for in the move event handlers
             e.preventDefault();
@@ -470,7 +482,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         }
         else if (this.props.active()) {
             e.stopPropagation();
-            this.zoom(e.clientX, e.clientY, e.deltaY)
+            this.zoom(e.clientX, e.clientY, e.deltaY);
         }
     }
 
@@ -790,6 +802,31 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
             ...this.views,
         ];
     }
+
+    @computed get svgBounds() {
+        let xs = this._points.map(p => p.x);
+        let ys = this._points.map(p => p.y);
+        let right = Math.max(...xs);
+        let left = Math.min(...xs);
+        let bottom = Math.max(...ys);
+        let top = Math.min(...ys);
+        return { right: right, left: left, bottom: bottom, top: top, width: right - left, height: bottom - top };
+    }
+
+    @computed get currentStroke() {
+        if (this._points.length <= 1) {
+            return (null);
+        }
+
+        let B = this.svgBounds;
+
+        return (
+            <svg width={B.width} height={B.height} style={{ transform: `translate(${B.left}px, ${B.top}px)` }}>
+                {CreatePolyline(this._points, B.left, B.top)}
+            </svg>
+        );
+    }
+
     render() {
         // 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;
@@ -808,9 +845,10 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
                         easing={this.easing} zoomScaling={this.zoomScaling} panX={this.panX} panY={this.panY}>
                         {!this.extensionDoc ? (null) :
                             // <InkingCanvas getScreenTransform={this.getTransform} Document={this.props.Document} AnnotationDocument={this.extensionDoc} inkFieldKey={"ink"} >
-                            this.childViews
+                            this.childViews()
                             // </InkingCanvas>
                         }
+                        {this.currentStroke}
                         <CollectionFreeFormRemoteCursors {...this.props} key="remoteCursors" />
                     </CollectionFreeFormViewPannableContents>
                 </MarqueeView>
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 138168fed..b5f6f095e 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -1,7 +1,7 @@
 import { action, computed, observable } from "mobx";
 import { observer } from "mobx-react";
 import { Doc, DocListCast } from "../../../../new_fields/Doc";
-import { InkField, StrokeData } from "../../../../new_fields/InkField";
+import { InkField, PointData } from "../../../../new_fields/InkField";
 import { List } from "../../../../new_fields/List";
 import { listSpec } from "../../../../new_fields/Schema";
 import { SchemaHeaderField } from "../../../../new_fields/SchemaHeaderField";
@@ -198,10 +198,10 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
             if (!e.shiftKey) {
                 SelectionManager.DeselectAll(mselect.length ? undefined : this.props.Document);
             }
-            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);
+            // 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 : [this.props.Document];
+            this.props.selectDocuments(docs, []);
         }
         if (!this._commandExecuted && (Math.abs(this.Bounds.height * this.Bounds.width) > 100)) {
             MarqueeOptionsMenu.Instance.createCollection = this.collection;
@@ -217,7 +217,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
             this.hideMarquee();
             MarqueeOptionsMenu.Instance.fadeOut(true);
             document.removeEventListener("pointerdown", hideMarquee);
-        }
+        };
         document.addEventListener("pointerdown", hideMarquee);
 
         if (e.altKey) {
@@ -294,7 +294,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
     delete = () => {
         this.marqueeSelect(false).map(d => this.props.removeDocument(d));
         if (this.ink) {
-            this.marqueeInkDelete(this.ink.inkData);
+            // this.marqueeInkDelete(this.ink.inkData);
         }
         SelectionManager.DeselectAll();
         this.cleanupInteractions(false);
@@ -336,8 +336,8 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
             title: "a nested collection",
         });
         let dataExtensionField = Doc.CreateDocumentExtensionForField(newCollection, "data");
-        dataExtensionField.ink = inkData ? new InkField(this.marqueeInkSelect(inkData)) : undefined;
-        this.marqueeInkDelete(inkData);
+        // dataExtensionField.ink = inkData ? new InkField(this.marqueeInkSelect(inkData)) : undefined;
+        // this.marqueeInkDelete(inkData);
         this.hideMarquee();
         return newCollection;
     }
@@ -422,45 +422,45 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
             this.cleanupInteractions(false);
         }
     }
-    @action
-    marqueeInkSelect(ink: Map<any, any>) {
-        let idata = new Map();
-        let centerShiftX = 0 - (this.Bounds.left + this.Bounds.width / 2); // moves each point by the offset that shifts the selection's center to the origin.
-        let centerShiftY = 0 - (this.Bounds.top + this.Bounds.height / 2);
-        ink.forEach((value: StrokeData, key: string, map: any) => {
-            if (InkingCanvas.IntersectStrokeRect(value, this.Bounds)) {
-                // let transform = this.props.container.props.ScreenToLocalTransform().scale(this.props.container.props.ContentScaling());
-                idata.set(key,
-                    {
-                        pathData: value.pathData.map(val => {
-                            let tVal = this.props.getTransform().inverse().transformPoint(val.x, val.y);
-                            return { x: tVal[0], y: tVal[1] };
-                            // return { x: val.x + centerShiftX, y: val.y + centerShiftY }
-                        }),
-                        color: value.color,
-                        width: value.width,
-                        tool: value.tool,
-                        page: -1
-                    });
-            }
-        });
-        // InkSelectDecorations.Instance.SetSelected(idata);
-        return idata;
-    }
+    // @action
+    // marqueeInkSelect(ink: Map<any, any>) {
+    //     let idata = new Map();
+    //     let centerShiftX = 0 - (this.Bounds.left + this.Bounds.width / 2); // moves each point by the offset that shifts the selection's center to the origin.
+    //     let centerShiftY = 0 - (this.Bounds.top + this.Bounds.height / 2);
+    //     ink.forEach((value: PointData, key: string, map: any) => {
+    //         if (InkingCanvas.IntersectStrokeRect(value, this.Bounds)) {
+    //             // let transform = this.props.container.props.ScreenToLocalTransform().scale(this.props.container.props.ContentScaling());
+    //             idata.set(key,
+    //                 {
+    //                     pathData: value.pathData.map(val => {
+    //                         let tVal = this.props.getTransform().inverse().transformPoint(val.x, val.y);
+    //                         return { x: tVal[0], y: tVal[1] };
+    //                         // return { x: val.x + centerShiftX, y: val.y + centerShiftY }
+    //                     }),
+    //                     color: value.color,
+    //                     width: value.width,
+    //                     tool: value.tool,
+    //                     page: -1
+    //                 });
+    //         }
+    //     });
+    //     // InkSelectDecorations.Instance.SetSelected(idata);
+    //     return idata;
+    // }
 
-    @action
-    marqueeInkDelete(ink?: Map<any, any>) {
-        // bcz: this appears to work but when you restart all the deleted strokes come back -- InkField isn't observing its changes so they aren't written to the DB.
-        // ink.forEach((value: StrokeData, key: string, map: any) =>
-        //     InkingCanvas.IntersectStrokeRect(value, this.Bounds) && ink.delete(key));
+    // @action
+    // marqueeInkDelete(ink?: Map<any, any>) {
+    //     // bcz: this appears to work but when you restart all the deleted strokes come back -- InkField isn't observing its changes so they aren't written to the DB.
+    //     // ink.forEach((value: StrokeData, key: string, map: any) =>
+    //     //     InkingCanvas.IntersectStrokeRect(value, this.Bounds) && ink.delete(key));
 
-        if (ink) {
-            let idata = new Map();
-            ink.forEach((value: StrokeData, key: string, map: any) =>
-                !InkingCanvas.IntersectStrokeRect(value, this.Bounds) && idata.set(key, value));
-            this.ink = new InkField(idata);
-        }
-    }
+    //     if (ink) {
+    //         let idata = new Map();
+    //         ink.forEach((value: PointData, key: string, map: any) =>
+    //             !InkingCanvas.IntersectStrokeRect(value, this.Bounds) && idata.set(key, value));
+    //         this.ink = new InkField(idata);
+    //     }
+    // }
 
     marqueeSelect(selectBackgrounds: boolean = true) {
         let selRect = this.Bounds;
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 62529a5fb..5c89472ce 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -632,7 +632,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
                     {searchHighlight}
                 </div>
             }
-        </>
+        </>;
     }
     render() {
         if (!this.props.Document) return (null);
diff --git a/src/new_fields/InkField.ts b/src/new_fields/InkField.ts
index d94834e91..2d8bb582a 100644
--- a/src/new_fields/InkField.ts
+++ b/src/new_fields/InkField.ts
@@ -12,16 +12,12 @@ export enum InkTool {
     Scrubber
 }
 
-export interface StrokeData {
-    pathData: Array<{ x: number, y: number }>;
-    color: string;
-    width: string;
-    tool: InkTool;
-    displayTimecode: number;
-    creationTime: number;
+export interface PointData {
+    x: number;
+    y: number;
 }
 
-export type InkData = Map<string, StrokeData>;
+export type InkData = Array<PointData>;
 
 const pointSchema = createSimpleSchema({
     x: true, y: true
@@ -34,16 +30,16 @@ const strokeDataSchema = createSimpleSchema({
 
 @Deserializable("ink")
 export class InkField extends ObjectField {
-    @serializable(map(object(strokeDataSchema)))
+    @serializable(list(object(strokeDataSchema)))
     readonly inkData: InkData;
 
-    constructor(data?: InkData) {
+    constructor(data: InkData) {
         super();
-        this.inkData = data || new Map;
+        this.inkData = data;
     }
 
     [Copy]() {
-        return new InkField(DeepCopy(this.inkData));
+        return new InkField(this.inkData);
     }
 
     [ToScriptString]() {
diff --git a/src/new_fields/documentSchemas.ts b/src/new_fields/documentSchemas.ts
index e2730914f..0b28561bf 100644
--- a/src/new_fields/documentSchemas.ts
+++ b/src/new_fields/documentSchemas.ts
@@ -43,6 +43,7 @@ export const documentSchema = createSchema({
     isAnimating: "boolean",     // whether the document is in the midst of animating between two layouts (used by icons to de/iconify documents).
     animateToDimensions: listSpec("number"), // layout information about the target rectangle a document is animating towards 
     scrollToLinkID: "string",   // id of link being traversed. allows this doc to scroll/highlight/etc its link anchor. scrollToLinkID should be set to undefined by this doc after it sets up its scroll,etc.
+    strokeWidth: "number"
 });
 
 export const positionSchema = createSchema({
-- 
cgit v1.2.3-70-g09d2


From e86c44757770c097392fb17726041a4410c16e74 Mon Sep 17 00:00:00 2001
From: yipstanley <stanley_yip@brown.edu>
Date: Sat, 16 Nov 2019 16:22:42 -0500
Subject: fixed some bugs with touch, text document are a little buggy?

---
 src/client/views/InkingCanvas.scss                 |  51 -----
 src/client/views/InkingCanvas.tsx                  | 217 ---------------------
 .../views/collections/CollectionStaffView.tsx      |   4 -
 .../collectionFreeForm/CollectionFreeFormView.tsx  |   9 +-
 .../collections/collectionFreeForm/MarqueeView.tsx |   1 -
 src/client/views/nodes/DocumentView.tsx            |   7 +-
 6 files changed, 9 insertions(+), 280 deletions(-)
 delete mode 100644 src/client/views/InkingCanvas.scss
 delete mode 100644 src/client/views/InkingCanvas.tsx

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

diff --git a/src/client/views/InkingCanvas.scss b/src/client/views/InkingCanvas.scss
deleted file mode 100644
index 8f32652ed..000000000
--- a/src/client/views/InkingCanvas.scss
+++ /dev/null
@@ -1,51 +0,0 @@
-@import "globalCssVariables";
-
-.inkingCanvas {
-   // opacity: 0.99;
-    touch-action: none;
-
-    .jsx-parser {
-        position: absolute;
-        width: 100%;
-        height: 100%;
-        background: inherit;
-        //z-index: -1; // allows annotations to appear on videos when screen is full-size & ... 
-    }
-}
-
-.inkingCanvas-paths-ink,
-.inkingCanvas-paths-markers,
-.inkingCanvas-noSelect,
-.inkingCanvas-canSelect {
-    position: absolute;
-    top: 0;
-    left: 0;
-    width: 8192px;
-    height: 8192px;
-    cursor: "crosshair";
-    pointer-events: all;
-}
-
-.inkingCanvas-canSelect,
-.inkingCanvas-noSelect {
-    top: -50000px;
-    left: -50000px;
-    width: 100000px;
-    height: 100000px;
-}
-
-.inkingCanvas-noSelect {
-    pointer-events: none;
-    cursor: "crosshair";
-}
-
-.inkingCanvas-paths-ink,
-.inkingCanvas-paths-markers {
-    pointer-events: none;
-    z-index: 10000; // overlays ink on top of everything 
-    cursor: "arrow";
-}
-
-.inkingCanvas-paths-markers {
-    mix-blend-mode: multiply;
-}
\ No newline at end of file
diff --git a/src/client/views/InkingCanvas.tsx b/src/client/views/InkingCanvas.tsx
deleted file mode 100644
index e5253c377..000000000
--- a/src/client/views/InkingCanvas.tsx
+++ /dev/null
@@ -1,217 +0,0 @@
-import { action, computed, trace, observable, runInAction } from "mobx";
-import { observer } from "mobx-react";
-import { Utils } from "../../Utils";
-import { Transform } from "../util/Transform";
-import "./InkingCanvas.scss";
-import { InkingControl } from "./InkingControl";
-import { InkingStroke } from "./InkingStroke";
-import React = require("react");
-import { UndoManager } from "../util/UndoManager";
-import { PointData, InkField, InkTool } from "../../new_fields/InkField";
-import { Doc } from "../../new_fields/Doc";
-import { Cast, PromiseValue, NumCast } from "../../new_fields/Types";
-import { Touchable } from "./Touchable";
-import { InteractionUtils } from "../util/InteractionUtils";
-
-interface InkCanvasProps {
-    getScreenTransform: () => Transform;
-    AnnotationDocument: Doc;
-    Document: Doc;
-    inkFieldKey: string;
-    children: () => JSX.Element[];
-}
-
-@observer
-export class InkingCanvas extends Touchable<InkCanvasProps> {
-    maxCanvasDim = 8192 / 2; // 1/2 of the maximum canvas dimension for Chrome
-    @observable inkMidX: number = 0;
-    @observable inkMidY: number = 0;
-    private previousState?: Map<string, PointData>;
-    private _currentStrokeId: string = "";
-    public static IntersectStrokeRect(stroke: PointData, selRect: { left: number, top: number, width: number, height: number }): boolean {
-        return stroke.pathData.reduce((inside: boolean, val) => inside ||
-            (selRect.left < val.x && selRect.left + selRect.width > val.x &&
-                selRect.top < val.y && selRect.top + selRect.height > val.y)
-            , false);
-    }
-    public static StrokeRect(stroke: PointData): { left: number, top: number, right: number, bottom: number } {
-        return stroke.pathData.reduce((bounds: { left: number, top: number, right: number, bottom: number }, val) =>
-            ({
-                left: Math.min(bounds.left, val.x), top: Math.min(bounds.top, val.y),
-                right: Math.max(bounds.right, val.x), bottom: Math.max(bounds.bottom, val.y)
-            })
-            , { left: Number.MAX_VALUE, top: Number.MAX_VALUE, right: -Number.MAX_VALUE, bottom: -Number.MAX_VALUE });
-    }
-
-    componentDidMount() {
-        PromiseValue(Cast(this.props.AnnotationDocument[this.props.inkFieldKey], InkField)).then(ink => runInAction(() => {
-            if (ink) {
-                let bounds = Array.from(ink.inkData).reduce(([mix, max, miy, may], [id, strokeData]) =>
-                    strokeData.pathData.reduce(([mix, max, miy, may], p) =>
-                        [Math.min(mix, p.x), Math.max(max, p.x), Math.min(miy, p.y), Math.max(may, p.y)],
-                        [mix, max, miy, may]),
-                    [Number.MAX_VALUE, Number.MIN_VALUE, Number.MAX_VALUE, Number.MIN_VALUE]);
-                this.inkMidX = (bounds[0] + bounds[1]) / 2;
-                this.inkMidY = (bounds[2] + bounds[3]) / 2;
-            }
-        }));
-    }
-
-    @computed
-    get inkData(): Map<string, PointData> {
-        let map = Cast(this.props.AnnotationDocument[this.props.inkFieldKey], InkField);
-        return !map ? new Map : new Map(map.inkData);
-    }
-
-    set inkData(value: Map<string, PointData>) {
-        this.props.AnnotationDocument[this.props.inkFieldKey] = new InkField(value);
-    }
-
-    @action
-    onPointerDown = (e: React.PointerEvent): void => {
-        if (e.button !== 0 || e.altKey || e.ctrlKey || InkingControl.Instance.selectedTool === InkTool.None) {
-            return;
-        }
-
-        document.addEventListener("pointermove", this.onPointerMove, true);
-        document.addEventListener("pointerup", this.onPointerUp, true);
-        e.stopPropagation();
-        e.preventDefault();
-
-        this.previousState = new Map(this.inkData);
-
-        if (InkingControl.Instance.selectedTool !== InkTool.Eraser && InkingControl.Instance.selectedTool !== InkTool.Scrubber) {
-            // start the new line, saves a uuid to represent the field of the stroke
-            this._currentStrokeId = Utils.GenerateGuid();
-            const data = this.inkData;
-            data.set(this._currentStrokeId, {
-                pathData: [this.relativeCoordinatesForEvent(e.clientX, e.clientY)],
-                color: InkingControl.Instance.selectedColor,
-                width: InkingControl.Instance.selectedWidth,
-                tool: InkingControl.Instance.selectedTool,
-                displayTimecode: NumCast(this.props.Document.currentTimecode, -1),
-                creationTime: new Date().getTime()
-            });
-            this.inkData = data;
-        }
-    }
-
-    @action
-    handle1PointerMove = (e: TouchEvent) => {
-        e.stopPropagation();
-        e.preventDefault();
-        let pointer = e.targetTouches.item(0);
-        if (pointer) {
-            this.handleMove(pointer.clientX, pointer.clientY);
-        }
-    }
-
-    handle2PointersMove = () => { }
-
-    @action
-    onPointerUp = (e: PointerEvent): void => {
-        document.removeEventListener("pointermove", this.onPointerMove, true);
-        document.removeEventListener("pointerup", this.onPointerUp, true);
-        let coord = this.relativeCoordinatesForEvent(e.clientX, e.clientY);
-        if (Math.abs(coord.x - this.inkMidX) > 500 || Math.abs(coord.y - this.inkMidY) > 500) {
-            this.inkMidX = coord.x;
-            this.inkMidY = coord.y;
-        }
-        e.stopPropagation();
-        e.preventDefault();
-
-        const batch = UndoManager.StartBatch("One ink stroke");
-        const oldState = this.previousState || new Map;
-        this.previousState = undefined;
-        const newState = new Map(this.inkData);
-        UndoManager.AddEvent({
-            undo: () => this.inkData = oldState,
-            redo: () => this.inkData = newState
-        });
-        batch.end();
-    }
-
-    handleMove = (x: number, y: number) => {
-        if (InkingControl.Instance.selectedTool !== InkTool.Eraser && InkingControl.Instance.selectedTool !== InkTool.Scrubber) {
-            let data = this.inkData;  // add points to new line as it is being drawn
-            let strokeData = data.get(this._currentStrokeId);
-            if (strokeData) {
-                strokeData.pathData.push(this.relativeCoordinatesForEvent(x, y));
-                data.set(this._currentStrokeId, strokeData);
-            }
-            this.inkData = data;
-        }
-    }
-
-    @action
-    onPointerMove = (e: PointerEvent): void => {
-        if (InteractionUtils.IsType(e, InteractionUtils.TOUCH)) {
-            return;
-        }
-        e.stopPropagation();
-        e.preventDefault();
-        this.handleMove(e.clientX, e.clientY);
-    }
-
-    relativeCoordinatesForEvent = (ex: number, ey: number): { x: number, y: number } => {
-        let [x, y] = this.props.getScreenTransform().transformPoint(ex, ey);
-        return { x, y };
-    }
-
-    @action
-    removeLine = (id: string): void => {
-        if (!this.previousState) {
-            this.previousState = new Map(this.inkData);
-            document.addEventListener("pointermove", this.onPointerMove, true);
-            document.addEventListener("pointerup", this.onPointerUp, true);
-        }
-        let data = this.inkData;
-        data.delete(id);
-        this.inkData = data;
-    }
-
-    // @computed
-    // get drawnPaths() {
-    //     let curTimecode = NumCast(this.props.Document.currentTimecode, -1);
-    //     let paths = Array.from(this.inkData).reduce((paths, [id, strokeData]) => {
-    //         if (strokeData.displayTimecode === -1 || (Math.abs(Math.round(strokeData.displayTimecode) - Math.round(curTimecode)) < 3)) {
-    //             paths.push(<InkingStroke key={id} id={id}
-    //                 line={strokeData.pathData}
-    //                 count={strokeData.pathData.length}
-    //                 offsetX={this.maxCanvasDim - this.inkMidX}
-    //                 offsetY={this.maxCanvasDim - this.inkMidY}
-    //                 color={strokeData.color}
-    //                 width={strokeData.width}
-    //                 tool={strokeData.tool}
-    //                 creationTime={strokeData.creationTime}
-    //                 deleteCallback={this.removeLine} />);
-    //         }
-    //         return paths;
-    //     }, [] as JSX.Element[]);
-    //     let markerPaths = paths.filter(path => path.props.tool === InkTool.Highlighter);
-    //     let penPaths = paths.filter(path => path.props.tool !== InkTool.Highlighter);
-    //     return [!penPaths.length ? (null) :
-    //         <svg className={`inkingCanvas-paths-ink`} key="Pens"
-    //             style={{ left: `${this.inkMidX - this.maxCanvasDim}px`, top: `${this.inkMidY - this.maxCanvasDim}px` }} >
-    //             {penPaths}
-    //         </svg>,
-    //     !markerPaths.length ? (null) :
-    //         <svg className={`inkingCanvas-paths-markers`} key="Markers"
-    //             style={{ left: `${this.inkMidX - this.maxCanvasDim}px`, top: `${this.inkMidY - this.maxCanvasDim}px` }}>
-    //             {markerPaths}
-    //         </svg>];
-    // }
-
-    render() {
-        let svgCanvasStyle = InkingControl.Instance.selectedTool !== InkTool.None && !this.props.Document.isBackground ? "canSelect" : "noSelect";
-        let cursor = svgCanvasStyle === "canSelect" ? (InkingControl.Instance.selectedTool === InkTool.Eraser ||
-            InkingControl.Instance.selectedTool === InkTool.Scrubber ? "pointer" : "default") : undefined;
-        return (
-            <div className="inkingCanvas">
-                <div className={`inkingCanvas-${svgCanvasStyle}`} onPointerDown={this.onPointerDown} onTouchStart={this.onTouchStart} style={{ cursor: cursor }} />
-                {this.props.children()}
-                {/* {this.drawnPaths} */}
-            </div >
-        );
-    }
-}
\ No newline at end of file
diff --git a/src/client/views/collections/CollectionStaffView.tsx b/src/client/views/collections/CollectionStaffView.tsx
index 5b1224b96..eea05ea61 100644
--- a/src/client/views/collections/CollectionStaffView.tsx
+++ b/src/client/views/collections/CollectionStaffView.tsx
@@ -1,5 +1,4 @@
 import { CollectionSubView } from "./CollectionSubView";
-import { InkingCanvas } from "../InkingCanvas";
 import { Transform } from "../../util/Transform";
 import React = require("react")
 import { computed, action, IReactionDisposer, reaction, runInAction, observable } from "mobx";
@@ -54,9 +53,6 @@ export class CollectionStaffView extends CollectionSubView(doc => doc) {
     render() {
         return (
             <div className="collectionStaffView" ref={this._mainCont}>
-                <InkingCanvas getScreenTransform={this.getTransform} Document={this.props.Document} AnnotationDocument={this.fieldExtensionDoc} inkFieldKey={"ink"} >
-                    {() => []}
-                </InkingCanvas>
                 {this.staves}
                 {this.addStaffButton}
             </div>
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 61d097c62..c7806a097 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -24,7 +24,6 @@ import { undoBatch, UndoManager } from "../../../util/UndoManager";
 import { COLLECTION_BORDER_WIDTH } from "../../../views/globalCssVariables.scss";
 import { ContextMenu } from "../../ContextMenu";
 import { ContextMenuProps } from "../../ContextMenuItem";
-import { InkingCanvas } from "../../InkingCanvas";
 import { CollectionFreeFormDocumentView } from "../../nodes/CollectionFreeFormDocumentView";
 import { DocumentViewProps } from "../../nodes/DocumentView";
 import { FormattedTextBox } from "../../nodes/FormattedTextBox";
@@ -298,8 +297,10 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         if (this._points.length > 1) {
             let B = this.svgBounds;
             let points = this._points.map(p => ({ x: p.x - B.left, y: p.y - B.top }));
-            let inkDoc = Docs.Create.InkDocument(InkingControl.Instance.selectedColor, InkingControl.Instance.selectedTool, parseInt(InkingControl.Instance.selectedWidth), points, { width: B.width, height: B.height, x: B.left, y: B.top });
-            this.addDocument(inkDoc);
+            UndoManager.RunInBatch(() => {
+                let inkDoc = Docs.Create.InkDocument(InkingControl.Instance.selectedColor, InkingControl.Instance.selectedTool, parseInt(InkingControl.Instance.selectedWidth), points, { width: B.width, height: B.height, x: B.left, y: B.top });
+                this.addDocument(inkDoc);
+            }, "addink");
             this._points = [];
         }
 
@@ -385,7 +386,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
 
     handle1PointerMove = (e: TouchEvent) => {
         // panning a workspace
-        if (!e.cancelBubble && this.props.active()) {
+        if (!e.cancelBubble && this.props.active() && !SelectionManager.GetIsDragging()) {
             let pt = e.targetTouches.item(0);
             if (pt) {
                 this.pan(pt);
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index b5f6f095e..1066f4f8d 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -13,7 +13,6 @@ import { Docs } from "../../../documents/Documents";
 import { SelectionManager } from "../../../util/SelectionManager";
 import { Transform } from "../../../util/Transform";
 import { undoBatch } from "../../../util/UndoManager";
-import { InkingCanvas } from "../../InkingCanvas";
 import { PreviewCursor } from "../../PreviewCursor";
 import { CollectionViewType } from "../CollectionView";
 import { CollectionFreeFormView } from "./CollectionFreeFormView";
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 5b3eea280..6ffa62fe5 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -41,6 +41,7 @@ import { DocumentContentsView } from "./DocumentContentsView";
 import "./DocumentView.scss";
 import { FormattedTextBox } from './FormattedTextBox';
 import React = require("react");
+import { InteractionUtils } from '../../util/InteractionUtils';
 
 library.add(fa.faEdit, fa.faTrash, fa.faShare, fa.faDownload, fa.faExpandArrowsAlt, fa.faCompressArrowsAlt, fa.faLayerGroup, fa.faExternalLinkAlt, fa.faAlignCenter, fa.faCaretSquareRight,
     fa.faSquare, fa.faConciergeBell, fa.faWindowRestore, fa.faFolder, fa.faMapPin, fa.faLink, fa.faFingerprint, fa.faCrosshairs, fa.faDesktop, fa.faUnlock, fa.faLock, fa.faLaptopCode, fa.faMale,
@@ -138,7 +139,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
             (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD)) {
             e.stopPropagation();
             let preventDefault = true;
-            if (this._doubleTap && this.props.renderDepth && !this.onClickHandler?.script) { // disable double-click to show full screen for things that have an on click behavior since clicking them twice can be misinterpreted as a double click
+            if (this._doubleTap && this.props.renderDepth && !this.onClickHandler ?.script) { // disable double-click to show full screen for things that have an on click behavior since clicking them twice can be misinterpreted as a double click
                 let fullScreenAlias = Doc.MakeAlias(this.props.Document);
                 if (StrCast(fullScreenAlias.layoutKey) !== "layoutCustom" && fullScreenAlias.layoutCustom !== undefined) {
                     fullScreenAlias.layoutKey = "layoutCustom";
@@ -216,7 +217,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         }
         else if (!e.cancelBubble && (SelectionManager.IsSelected(this) || this.props.parentActive() || 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) {
+                if (!e.altKey && (!this.topMost || this.Document.onDragStart || this.Document.onClick) && (e.buttons === 1 || InteractionUtils.IsType(e, InteractionUtils.TOUCH))) {
                     document.removeEventListener("pointermove", this.onPointerMove);
                     document.removeEventListener("pointerup", this.onPointerUp);
                     this.startDragging(this._downX, this._downY, this.Document.dropAction ? this.Document.dropAction as any : e.ctrlKey || e.altKey ? "alias" : undefined, this._hitTemplateDrag);
@@ -354,7 +355,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     @undoBatch
     @action
     setCustomView = (custom: boolean): void => {
-        if (this.props.ContainingCollectionView?.props.DataDoc || this.props.ContainingCollectionView?.props.Document.isTemplateDoc) {
+        if (this.props.ContainingCollectionView ?.props.DataDoc || this.props.ContainingCollectionView ?.props.Document.isTemplateDoc) {
             Doc.MakeMetadataFieldTemplate(this.props.Document, this.props.ContainingCollectionView.props.Document);
         } else {
             custom ? DocumentView.makeCustomViewClicked(this.props.Document, this.props.DataDoc) : DocumentView.makeNativeViewClicked(this.props.Document);
-- 
cgit v1.2.3-70-g09d2


From eafa4992440756085beb89fc48fa07b45252362b Mon Sep 17 00:00:00 2001
From: Stanley Yip <stanley_yip@brown.edu>
Date: Sat, 16 Nov 2019 17:08:04 -0500
Subject: some stuffs not working on the hub, idk why

---
 src/client/views/Touchable.tsx                     |  4 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 67 +++++++++++++++-------
 src/client/views/nodes/DocumentView.scss           | 45 ++++++++-------
 3 files changed, 71 insertions(+), 45 deletions(-)

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

diff --git a/src/client/views/Touchable.tsx b/src/client/views/Touchable.tsx
index 26779b168..ba87025c4 100644
--- a/src/client/views/Touchable.tsx
+++ b/src/client/views/Touchable.tsx
@@ -23,7 +23,7 @@ export abstract class Touchable<T = {}> extends React.Component<T> {
 
         switch (e.targetTouches.length) {
             case 1:
-                this.handle1PointerDown();
+                this.handle1PointerDown(e);
                 break;
             case 2:
                 this.handle2PointersDown(e);
@@ -89,6 +89,6 @@ export abstract class Touchable<T = {}> extends React.Component<T> {
         e.preventDefault();
     }
 
-    handle1PointerDown = (): any => { };
+    handle1PointerDown = (e: React.TouchEvent): any => { };
     handle2PointersDown = (e: React.TouchEvent): any => { };
 }
\ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index c7806a097..10aa93c36 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -24,6 +24,7 @@ import { undoBatch, UndoManager } from "../../../util/UndoManager";
 import { COLLECTION_BORDER_WIDTH } from "../../../views/globalCssVariables.scss";
 import { ContextMenu } from "../../ContextMenu";
 import { ContextMenuProps } from "../../ContextMenuItem";
+import { InkingCanvas } from "../../InkingCanvas";
 import { CollectionFreeFormDocumentView } from "../../nodes/CollectionFreeFormDocumentView";
 import { DocumentViewProps } from "../../nodes/DocumentView";
 import { FormattedTextBox } from "../../nodes/FormattedTextBox";
@@ -186,21 +187,24 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
                 NumCast(cd.cluster) : cluster;
         }, -1);
     }
-    tryDragCluster(e: PointerEvent) {
-        let cluster = this.pickCluster(this.getTransform().transformPoint(e.clientX, e.clientY));
-        if (cluster !== -1) {
-            let eles = this.childLayoutPairs.map(pair => pair.layout).filter(cd => NumCast(cd.cluster) === cluster);
-            let clusterDocs = eles.map(ele => DocumentManager.Instance.getDocumentView(ele, this.props.CollectionView)!);
-            let de = new DragManager.DocumentDragData(eles);
-            de.moveDocument = this.props.moveDocument;
-            const [left, top] = clusterDocs[0].props.ScreenToLocalTransform().scale(clusterDocs[0].props.ContentScaling()).inverse().transformPoint(0, 0);
-            de.offset = this.getTransform().transformDirection(e.x - left, e.y - top);
-            de.dropAction = e.ctrlKey || e.altKey ? "alias" : undefined;
-            DragManager.StartDocumentDrag(clusterDocs.map(v => v.ContentDiv!), de, e.clientX, e.clientY, {
-                handlers: { dragComplete: action(emptyFunction) },
-                hideSource: !de.dropAction
-            });
-            return true;
+    tryDragCluster(e: PointerEvent | TouchEvent) {
+        let ptsParent = e instanceof PointerEvent ? e : e.targetTouches.item(0);
+        if (ptsParent) {
+            let cluster = this.pickCluster(this.getTransform().transformPoint(ptsParent.clientX, ptsParent.clientY));
+            if (cluster !== -1) {
+                let eles = this.childLayoutPairs.map(pair => pair.layout).filter(cd => NumCast(cd.cluster) === cluster);
+                let clusterDocs = eles.map(ele => DocumentManager.Instance.getDocumentView(ele, this.props.CollectionView)!);
+                let de = new DragManager.DocumentDragData(eles);
+                de.moveDocument = this.props.moveDocument;
+                const [left, top] = clusterDocs[0].props.ScreenToLocalTransform().scale(clusterDocs[0].props.ContentScaling()).inverse().transformPoint(0, 0);
+                de.offset = this.getTransform().transformDirection(ptsParent.clientX - left, ptsParent.clientY - top);
+                de.dropAction = e.ctrlKey || e.altKey ? "alias" : undefined;
+                DragManager.StartDocumentDrag(clusterDocs.map(v => v.ContentDiv!), de, ptsParent.clientX, ptsParent.clientY, {
+                    handlers: { dragComplete: action(emptyFunction) },
+                    hideSource: !de.dropAction
+                });
+                return true;
+            }
         }
 
         return false;
@@ -290,17 +294,23 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         }
     }
 
+    @action
+    handle1PointerDown = (e: React.TouchEvent) => {
+        let pt = e.targetTouches.item(0);
+        if (pt) {
+            this._hitCluster = this.props.Document.useCluster ? this.pickCluster(this.getTransform().transformPoint(pt.clientX, pt.clientY)) !== -1 : false;
+        }
+    }
+
     @action
     onPointerUp = (e: PointerEvent): void => {
-        if (InteractionUtils.IsType(e, InteractionUtils.TOUCH)) return;
+        if (InteractionUtils.IsType(e, InteractionUtils.TOUCH) && this._points.length <= 1) return;
 
         if (this._points.length > 1) {
             let B = this.svgBounds;
             let points = this._points.map(p => ({ x: p.x - B.left, y: p.y - B.top }));
-            UndoManager.RunInBatch(() => {
-                let inkDoc = Docs.Create.InkDocument(InkingControl.Instance.selectedColor, InkingControl.Instance.selectedTool, parseInt(InkingControl.Instance.selectedWidth), points, { width: B.width, height: B.height, x: B.left, y: B.top });
-                this.addDocument(inkDoc);
-            }, "addink");
+            let inkDoc = Docs.Create.InkDocument(InkingControl.Instance.selectedColor, InkingControl.Instance.selectedTool, parseInt(InkingControl.Instance.selectedWidth), points, { width: B.width, height: B.height, x: B.left, y: B.top });
+            this.addDocument(inkDoc);
             this._points = [];
         }
 
@@ -386,10 +396,23 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
 
     handle1PointerMove = (e: TouchEvent) => {
         // panning a workspace
-        if (!e.cancelBubble && this.props.active() && !SelectionManager.GetIsDragging()) {
+        if (!e.cancelBubble) {
             let pt = e.targetTouches.item(0);
             if (pt) {
-                this.pan(pt);
+                if (InkingControl.Instance.selectedTool === InkTool.None) {
+                    if (this._hitCluster && this.tryDragCluster(e)) {
+                        e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document'  however it does mark the event as cancelBubble=true which we test for in the move event handlers
+                        e.preventDefault();
+                        document.removeEventListener("pointermove", this.onPointerMove);
+                        document.removeEventListener("pointerup", this.onPointerUp);
+                        return;
+                    }
+                    this.pan(pt);
+                }
+                else if (InkingControl.Instance.selectedTool !== InkTool.Eraser && InkingControl.Instance.selectedTool !== InkTool.Scrubber) {
+                    let point = this.getTransform().transformPoint(pt.clientX, pt.clientY);
+                    this._points.push({ x: point[0], y: point[1] });
+                }
             }
             e.stopPropagation();
             e.preventDefault();
diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss
index 65df86d27..142036354 100644
--- a/src/client/views/nodes/DocumentView.scss
+++ b/src/client/views/nodes/DocumentView.scss
@@ -1,13 +1,14 @@
 @import "../globalCssVariables";
 
-.documentView-node, .documentView-node-topmost {
+.documentView-node,
+.documentView-node-topmost {
     position: inherit;
     top: 0;
-    left:0;
+    left: 0;
     border-radius: inherit;
-    transition : outline .3s linear;
+    transition: outline .3s linear;
     cursor: grab;
-                        
+
     // background: $light-color; //overflow: hidden;
     transform-origin: left top;
 
@@ -37,44 +38,46 @@
         position: absolute;
         transform-origin: top left;
         width: 100%;
-        height: 100%; 
+        height: 100%;
     }
+
     .documentView-styleWrapper {
-        position: absolute; 
+        position: absolute;
         display: inline-block;
-        width:100%;
-        height:100%;
+        width: 100%;
+        height: 100%;
         pointer-events: none;
 
         .documentView-styleContentWrapper {
-            width:100%;
+            width: 100%;
             display: inline-block;
             position: absolute;
         }
+
         .documentView-titleWrapper {
-            overflow:hidden; 
-            color: white; 
-            transform-origin: top left; 
-            top: 0; 
+            overflow: hidden;
+            color: white;
+            transform-origin: top left;
+            top: 0;
             height: 25;
             background: rgba(0, 0, 0, .4);
-            padding: 4px; 
-            text-align: center; 
-            text-overflow: ellipsis; 
+            padding: 4px;
+            text-align: center;
+            text-overflow: ellipsis;
             white-space: pre;
         }
 
         .documentView-searchHighlight {
-            position: absolute; 
-            background: yellow; 
+            position: absolute;
+            background: yellow;
             bottom: -20px;
             border-radius: 5px;
-            transform-origin: bottom left; 
+            transform-origin: bottom left;
         }
 
         .documentView-captionWrapper {
-            position: absolute; 
-            bottom: 0; 
+            position: absolute;
+            bottom: 0;
             transform-origin: bottom left;
         }
     }
-- 
cgit v1.2.3-70-g09d2


From 6ecbfba52bad2dd64e07472b5f4ef06f95b5544b Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Mon, 18 Nov 2019 15:08:05 -0500
Subject: changing documentView to ignore transforms -- must be applied by view
 boxes

---
 src/client/documents/Documents.ts                  |  2 +-
 src/client/views/CollectionLinearView.scss         |  6 +++--
 src/client/views/CollectionLinearView.tsx          | 29 +++++++++-----------
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  9 +++----
 .../collectionFreeForm/MarqueeView.scss            |  1 +
 src/client/views/nodes/ColorBox.tsx                |  4 ++-
 src/client/views/nodes/DocumentView.tsx            | 31 ++++++++++++----------
 src/client/views/nodes/FontIconBox.scss            |  6 ++---
 src/client/views/nodes/FontIconBox.tsx             |  7 +++--
 src/client/views/nodes/ImageBox.tsx                |  7 +++--
 src/client/views/nodes/PDFBox.tsx                  |  7 +----
 src/client/views/nodes/VideoBox.tsx                |  6 +++--
 src/client/views/pdf/PDFViewer.scss                |  1 +
 src/client/views/pdf/PDFViewer.tsx                 |  8 ++++--
 src/new_fields/Doc.ts                              |  2 +-
 .../authentication/models/current_user_utils.ts    | 14 +++++-----
 16 files changed, 74 insertions(+), 66 deletions(-)

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 268a14aca..3c88173cd 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -98,7 +98,7 @@ export interface DocumentOptions {
     autoHeight?: boolean;
     removeDropProperties?: List<string>; // list of properties that should be removed from a document when it is dropped.  e.g., a creator button may be forceActive to allow it be dragged, but the forceActive property can be removed from the dropped document
     dbDoc?: Doc;
-    unchecked?: ScriptField; // returns whether a check box is unchecked
+    ischecked?: ScriptField; // returns whether a font icon box is checked
     activePen?: Doc; // which pen document is currently active (used as the radio button state for the 'unhecked' pen tool scripts)
     onClick?: ScriptField;
     dragFactory?: Doc; // document to create when dragging with a suitable onDragStart script
diff --git a/src/client/views/CollectionLinearView.scss b/src/client/views/CollectionLinearView.scss
index 4423a7020..81210d7ae 100644
--- a/src/client/views/CollectionLinearView.scss
+++ b/src/client/views/CollectionLinearView.scss
@@ -17,6 +17,7 @@
             height: 18px;
             margin-top:auto;
             margin-bottom:auto;
+            margin-right: 3px;
             cursor: pointer;
             transition: transform 0.2s;
         }
@@ -51,8 +52,9 @@
 
             .collectionLinearView-docBtn, .collectionLinearView-docBtn-scalable {
                 position:relative;
-                margin-top: auto;
-                margin-bottom: auto;
+                margin:auto;
+                margin-left: 3px;
+                transform-origin: center 80%;
             }
             .collectionLinearView-docBtn-scalable:hover {
                 transform: scale(1.15);
diff --git a/src/client/views/CollectionLinearView.tsx b/src/client/views/CollectionLinearView.tsx
index 7c6d33d36..f718735a8 100644
--- a/src/client/views/CollectionLinearView.tsx
+++ b/src/client/views/CollectionLinearView.tsx
@@ -22,18 +22,17 @@ const LinearDocument = makeInterface(documentSchema);
 export class CollectionLinearView extends CollectionSubView(LinearDocument) {
     @observable public addMenuToggle = React.createRef<HTMLInputElement>();
     private _dropDisposer?: DragManager.DragDropDisposer;
-    private _heightDisposer?: IReactionDisposer;
-    private _spacing = 20;
+    private _widthDisposer?: IReactionDisposer;
 
     componentWillUnmount() {
         this._dropDisposer && this._dropDisposer();
-        this._heightDisposer && this._heightDisposer();
+        this._widthDisposer && this._widthDisposer();
     }
 
     componentDidMount() {
         // is there any reason this needs to exist? -syip.  yes, it handles autoHeight for stacking views (masonry isn't yet supported).
-        this._heightDisposer = reaction(() => NumCast(this.props.Document.height, 0) + this.childDocs.length + (this.props.Document.isExpanded ? 1 : 0),
-            () => this.props.Document.width = 18 + (this.props.Document.isExpanded ? this.childDocs.length * (this.props.Document[HeightSym]()) : 10),
+        this._widthDisposer = reaction(() => NumCast(this.props.Document.height, 0) + this.childDocs.length + (this.props.Document.isExpanded ? 1 : 0),
+            () => this.props.Document.width = 5 + (this.props.Document.isExpanded ? this.childDocs.length * (this.props.Document[HeightSym]()) : 10),
             { fireImmediately: true }
         );
     }
@@ -52,6 +51,7 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) {
         let { scale, translateX, translateY } = Utils.GetScreenTransform(ele.current);
         return new Transform(-translateX, -translateY, 1 / scale);
     }
+
     render() {
         let guid = Utils.GenerateGuid();
         return <div className="collectionLinearView-outer">
@@ -60,19 +60,16 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) {
                     onChange={action((e: any) => this.props.Document.isExpanded = this.addMenuToggle.current!.checked)} />
                 <label htmlFor={`${guid}`} style={{ marginTop: "auto", marginBottom: "auto", background: StrCast(this.props.Document.backgroundColor, "black") === StrCast(this.props.Document.color, "white") ? "black" : StrCast(this.props.Document.backgroundColor, "black") }} title="Close Menu"><p>+</p></label>
 
-                <div className="collectionLinearView-content">
+                <div className="collectionLinearView-content" style={{ height: this.dimension(), width: NumCast(this.props.Document.width, 25) }}>
                     {this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map(pair => {
                         let nested = pair.layout.viewType === CollectionViewType.Linear;
                         let dref = React.createRef<HTMLDivElement>();
                         let nativeWidth = NumCast(pair.layout.nativeWidth, this.dimension());
-                        let scalingContent = nested ? 1 : this.dimension() / (this._spacing + nativeWidth);
-                        let scalingBox = nested ? 1 : this.dimension() / nativeWidth;
-                        let deltaSize = nativeWidth * scalingBox - nativeWidth * scalingContent;
+                        let deltaSize = nativeWidth * .15 / 2;
                         return <div className={`collectionLinearView-docBtn` + (pair.layout.onClick || pair.layout.onDragStart ? "-scalable" : "")} key={pair.layout[Id]} ref={dref}
                             style={{
-                                width: nested ? pair.layout[WidthSym]() : this.dimension(),
-                                height: nested && pair.layout.isExpanded ? pair.layout[HeightSym]() : this.dimension(),
-                                transform: nested ? undefined : `translate(${deltaSize / 2}px, ${deltaSize / 2}px)`
+                                width: nested ? pair.layout[WidthSym]() : this.dimension() - deltaSize,
+                                height: nested && pair.layout.isExpanded ? pair.layout[HeightSym]() : this.dimension() - deltaSize,
                             }}  >
                             <DocumentView
                                 Document={pair.layout}
@@ -85,9 +82,9 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) {
                                 ruleProvider={undefined}
                                 onClick={undefined}
                                 ScreenToLocalTransform={this.getTransform(dref)}
-                                ContentScaling={() => scalingContent} // ugh - need to get rid of this inline function to avoid recomputing
-                                PanelWidth={() => nested ? pair.layout[WidthSym]() : this.dimension()}
-                                PanelHeight={() => nested ? pair.layout[HeightSym]() : this.dimension()}
+                                ContentScaling={returnOne}
+                                PanelWidth={nested ? pair.layout[WidthSym] : () => this.dimension()}// ugh - need to get rid of this inline function to avoid recomputing
+                                PanelHeight={nested ? pair.layout[HeightSym] : () => this.dimension()}
                                 renderDepth={this.props.renderDepth + 1}
                                 focus={emptyFunction}
                                 backgroundColor={returnEmptyString}
@@ -101,8 +98,6 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) {
                             </DocumentView>
                         </div>;
                     })}
-                    {/* <li key="undoTest"><button className="add-button round-button" title="Click if undo isn't working" onClick={() => UndoManager.TraceOpenBatches()}><FontAwesomeIcon icon="exclamation" size="sm" /></button></li> */}
-
                 </div>
             </div>
         </div>;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 10aa93c36..a1559e049 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 } from "mobx";
+import { action, computed, observable, trace } from "mobx";
 import { observer } from "mobx-react";
 import { Doc, DocListCast, HeightSym, Opt, WidthSym } from "../../../../new_fields/Doc";
 import { Id } from "../../../../new_fields/FieldSymbols";
@@ -852,6 +852,7 @@ 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;
@@ -867,11 +868,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
                     addLiveTextDocument={this.addLiveTextBox} getContainerTransform={this.getContainerTransform} getTransform={this.getTransform} isAnnotationOverlay={this.isAnnotationOverlay}>
                     <CollectionFreeFormViewPannableContents centeringShiftX={this.centeringShiftX} centeringShiftY={this.centeringShiftY}
                         easing={this.easing} zoomScaling={this.zoomScaling} panX={this.panX} panY={this.panY}>
-                        {!this.extensionDoc ? (null) :
-                            // <InkingCanvas getScreenTransform={this.getTransform} Document={this.props.Document} AnnotationDocument={this.extensionDoc} inkFieldKey={"ink"} >
-                            this.childViews()
-                            // </InkingCanvas>
-                        }
+                        {!this.extensionDoc ? (null) : this.childViews()}
                         {this.currentStroke}
                         <CollectionFreeFormRemoteCursors {...this.props} key="remoteCursors" />
                     </CollectionFreeFormViewPannableContents>
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.scss b/src/client/views/collections/collectionFreeForm/MarqueeView.scss
index 04f6ec2ad..53b07318f 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.scss
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.scss
@@ -8,6 +8,7 @@
 }
 .marqueeView {
     overflow: hidden;
+    pointer-events: all;
 }
 
 .marqueeView:focus-within {
diff --git a/src/client/views/nodes/ColorBox.tsx b/src/client/views/nodes/ColorBox.tsx
index fda6d64f4..40674b034 100644
--- a/src/client/views/nodes/ColorBox.tsx
+++ b/src/client/views/nodes/ColorBox.tsx
@@ -38,7 +38,9 @@ export class ColorBox extends DocExtendableComponent<FieldViewProps, ColorDocume
 
     render() {
         return <div className={`colorBox-container${this.active() ? "-interactive" : ""}`}
-            onPointerDown={e => e.button === 0 && !e.ctrlKey && e.stopPropagation()}>
+            onPointerDown={e => e.button === 0 && !e.ctrlKey && e.stopPropagation()}
+            style={{ transformOrigin: "top left", transform: `scale(${this.props.ContentScaling()})`, width: `${100 / this.props.ContentScaling()}%`, height: `${100 / this.props.ContentScaling()}%` }} >
+
             <SketchPicker color={this._startupColor} onChange={InkingControl.Instance.switchColor} />
         </div>;
     }
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index fc2bc5169..d9e1f2c6b 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -95,7 +95,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     @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() { return this.props.onClick ? this.props.onClick : this.Document.onClick; }
+    @computed get onClickHandler() { trace(); console.log("this.props.doc = " + this.props.Document.title); return this.props.onClick ? this.props.onClick : this.Document.onClick; }
 
     @action
     componentDidMount() {
@@ -537,8 +537,10 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         return (showTitle ? 25 : 0) + 1;
     }
 
+    @computed get finalLayoutKey() { return this.props.layoutKey || "layout" }
     childScaling = () => (this.layoutDoc.fitWidth ? this.props.PanelWidth() / this.nativeWidth : this.props.ContentScaling());
     @computed get contents() {
+        trace();
         return (<DocumentContentsView ContainingCollectionView={this.props.ContainingCollectionView}
             ContainingCollectionDoc={this.props.ContainingCollectionDoc}
             Document={this.props.Document}
@@ -567,7 +569,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
             isSelected={this.isSelected}
             select={this.select}
             onClick={this.onClickHandler}
-            layoutKey={this.props.layoutKey || "layout"}
+            layoutKey={this.finalLayoutKey}
             DataDoc={this.props.DataDoc} />);
     }
     linkEndpoint = (linkDoc: Doc) => Doc.LinkEndpoint(linkDoc, this.props.Document);
@@ -582,16 +584,19 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     }
 
     @computed get innards() {
+        trace();
         const showOverlays = this.props.showOverlays ? this.props.showOverlays(this.Document) : undefined;
         const showTitle = showOverlays && "title" in showOverlays ? showOverlays.title : this.getLayoutPropStr("showTitle");
         const showCaption = showOverlays && "caption" in showOverlays ? showOverlays.caption : this.getLayoutPropStr("showCaption");
         const showTextTitle = showTitle && StrCast(this.Document.layout).indexOf("FormattedTextBox") !== -1 ? showTitle : undefined;
         const searchHighlight = (!this.Document.searchFields ? (null) :
-            <div className="documentView-searchHighlight" style={{ width: `${100 * this.props.ContentScaling()}%`, transform: `scale(${1 / this.props.ContentScaling()})` }}>
+            <div className="documentView-searchHighlight">
+                {/* style={{ width: `${100 * this.props.ContentScaling()}%`, transform: `scale(${1 / this.props.ContentScaling()})` }}> */}
                 {this.Document.searchFields}
             </div>);
         const captionView = (!showCaption ? (null) :
-            <div className="documentView-captionWrapper" style={{ width: `${100 * this.props.ContentScaling()}%`, transform: `scale(${1 / this.props.ContentScaling()})` }}>
+            <div className="documentView-captionWrapper">
+                {/* style={{ width: `${100 * this.props.ContentScaling()}%`, transform: `scale(${1 / this.props.ContentScaling()})` }}> */}
                 <FormattedTextBox {...this.props}
                     onClick={this.onClickHandler} DataDoc={this.props.DataDoc} active={returnTrue}
                     isSelected={this.isSelected} focus={emptyFunction} select={this.select}
@@ -600,7 +605,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
             </div>);
         const titleView = (!showTitle ? (null) :
             <div className="documentView-titleWrapper" style={{
-                width: `${100 * this.props.ContentScaling()}%`, transform: `scale(${1 / this.props.ContentScaling()})`,
+                //width: `${100 * this.props.ContentScaling()}%`, transform: `scale(${1 / this.props.ContentScaling()})`,
                 position: showTextTitle ? "relative" : "absolute",
                 pointerEvents: SelectionManager.GetIsDragging() ? "none" : "all",
             }}>
@@ -613,7 +618,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
             </div>);
         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 / this.props.ContentScaling()})` }}>
+                <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))} />
                 </div>)}
             {!showTitle && !showCaption ?
@@ -639,7 +644,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     render() {
         if (!this.props.Document) return (null);
         trace();
-        const animDims = this.Document.animateToDimensions ? Array.from(this.Document.animateToDimensions) : undefined;
         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");
@@ -648,17 +652,16 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
             this.props.backgroundColor(this.Document) || StrCast(this.layoutDoc.backgroundColor) :
             ruleColor && !colorSet ? ruleColor : StrCast(this.layoutDoc.backgroundColor) || this.props.backgroundColor(this.Document);
 
-        const nativeWidth = this.layoutDoc.fitWidth ? this.props.PanelWidth() - 2 : this.nativeWidth > 0 && !this.layoutDoc.ignoreAspect ? `${this.nativeWidth}px` : "100%";
-        const nativeHeight = this.layoutDoc.fitWidth ? this.props.PanelHeight() - 2 : this.Document.ignoreAspect ? this.props.PanelHeight() / this.props.ContentScaling() : this.nativeHeight > 0 ? `${this.nativeHeight}px` : "100%";
         const fullDegree = Doc.isBrushedHighlightedDegree(this.props.Document);
         const borderRounding = this.getLayoutPropStr("borderRounding") || ruleRounding;
-        const localScale = this.props.ScreenToLocalTransform().Scale * fullDegree;
+        const localScale = fullDegree;
 
-        let animheight = animDims ? animDims[1] : nativeHeight;
-        let animwidth = animDims ? animDims[0] : nativeWidth;
+        const animDims = this.Document.animateToDimensions ? Array.from(this.Document.animateToDimensions) : undefined;
+        let animheight = animDims ? animDims[1] : "100%";
+        let animwidth = animDims ? animDims[0] : "100%";
 
         const highlightColors = ["transparent", "maroon", "maroon", "yellow", "magenta", "cyan", "orange"];
-        const highlightStyles = ["solid", "dashed", "solid", "solid", "solid", "solid", "solid", "solid"];
+        const highlightStyles = ["solid", "dashed", "solid", "solid", "solid", "solid", "solid"];
         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}
@@ -672,7 +675,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
                 background: this.layoutDoc.type === DocumentType.FONTICON || this.layoutDoc.viewType === CollectionViewType.Linear ? undefined : backgroundColor,
                 width: animwidth,
                 height: animheight,
-                transform: `scale(${this.layoutDoc.fitWidth ? 1 : this.props.ContentScaling()})`,
+                //transform: `scale(${this.layoutDoc.fitWidth ? 1 : this.props.ContentScaling()})`,
                 opacity: this.Document.opacity
             }} >
             {this.innards}
diff --git a/src/client/views/nodes/FontIconBox.scss b/src/client/views/nodes/FontIconBox.scss
index 75d093fcb..905601ce3 100644
--- a/src/client/views/nodes/FontIconBox.scss
+++ b/src/client/views/nodes/FontIconBox.scss
@@ -5,9 +5,9 @@
     border-radius: inherit;
     background: black;
     border-radius: 100%;
+    transform-origin: top left;
     svg {
-        margin:18%;
-        width:65% !important;
-        height:65%;
+        width:95% !important;
+        height:95%;
     }
 }
diff --git a/src/client/views/nodes/FontIconBox.tsx b/src/client/views/nodes/FontIconBox.tsx
index 83ecc4657..9a5de836f 100644
--- a/src/client/views/nodes/FontIconBox.tsx
+++ b/src/client/views/nodes/FontIconBox.tsx
@@ -39,8 +39,11 @@ export class FontIconBox extends DocComponent<FieldViewProps, FontIconDocument>(
         let referenceDoc = (this.props.Document.dragFactory instanceof Doc ? this.props.Document.dragFactory : this.props.Document);
         let referenceLayout = Doc.Layout(referenceDoc);
         return <button className="fontIconBox-outerDiv" title={StrCast(this.props.Document.title)} ref={this._ref}
-            style={{ background: StrCast(referenceLayout.backgroundColor), boxShadow: this.props.Document.unchecked ? undefined : `4px 4px 12px black` }}>
-            <FontAwesomeIcon className="fontIconBox-icon" icon={this.Document.icon as any} color={this._foregroundColor} size="sm" />
+            style={{
+                background: StrCast(referenceLayout.backgroundColor),
+                boxShadow: this.props.Document.ischecked ? `4px 4px 12px black` : undefined
+            }}>
+            <FontAwesomeIcon className="fontIconBox-icon" icon={this.Document.icon as any} color={this._foregroundColor} size="md" />
         </button>;
     }
 }
\ No newline at end of file
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 212c99f9d..2a81c3577 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -268,6 +268,7 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
     }
 
     @computed get content() {
+        trace();
         const extensionDoc = this.extensionDoc;
         if (!extensionDoc) return (null);
         // let transform = this.props.ScreenToLocalTransform().inverse();
@@ -330,8 +331,10 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
             </div>);
     }
 
+    contentFunc = () => [this.content];
     render() {
-        return (<div className={"imageBox-container"} onContextMenu={this.specificContextMenu}>
+        return (<div className={"imageBox-container"} onContextMenu={this.specificContextMenu}
+            style={{ transformOrigin: "top left", transform: `scale(${this.props.ContentScaling()})`, width: `${100 / this.props.ContentScaling()}%`, height: `${100 / this.props.ContentScaling()}%` }} >
             <CollectionFreeFormView {...this.props}
                 PanelHeight={this.props.PanelHeight}
                 PanelWidth={this.props.PanelWidth}
@@ -352,7 +355,7 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
                 renderDepth={this.props.renderDepth + 1}
                 ContainingCollectionDoc={this.props.ContainingCollectionDoc}
                 chromeCollapsed={true}>
-                {() => [this.content]}
+                {this.contentFunc}
             </CollectionFreeFormView>
         </div >);
     }
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 8e0515f8a..ecbe2d309 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -205,14 +205,9 @@ export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument>
     }
 
     @computed get renderPdfView() {
-        trace();
         const pdfUrl = Cast(this.dataDoc[this.props.fieldKey], PdfField);
         let classname = "pdfBox-cont" + (this.active() ? "-interactive" : "");
-        return <div className={classname} style={{
-            width: this.props.Document.fitWidth ? `${100 / this.props.ContentScaling()}%` : undefined,
-            height: this.props.Document.fitWidth ? `${100 / this.props.ContentScaling()}%` : undefined,
-            transform: `scale(${this.props.Document.fitWidth ? this.props.ContentScaling() : 1})`
-        }} onContextMenu={this.specificContextMenu} onPointerDown={e => {
+        return <div className={classname} onContextMenu={this.specificContextMenu} onPointerDown={e => {
             let hit = document.elementFromPoint(e.clientX, e.clientY);
             if (hit && hit.localName === "span" && this.props.isSelected()) {  // drag selecting text stops propagation
                 e.button === 0 && e.stopPropagation();
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 53baea4ae..dd6e60c51 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -333,8 +333,10 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum
         return this.addDocument(doc);
     }
 
+    contentFunc = () => [this.youtubeVideoId ? this.youtubeContent : this.content];
     render() {
-        return (<div className={"videoBox-container"} onContextMenu={this.specificContextMenu}>
+        return (<div className={"videoBox-container"} onContextMenu={this.specificContextMenu}
+            style={{ transformOrigin: "top left", transform: `scale(${this.props.ContentScaling()})`, width: `${100 / this.props.ContentScaling()}%`, height: `${100 / this.props.ContentScaling()}%` }} >
             <CollectionFreeFormView {...this.props}
                 PanelHeight={this.props.PanelHeight}
                 PanelWidth={this.props.PanelWidth}
@@ -355,7 +357,7 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum
                 renderDepth={this.props.renderDepth + 1}
                 ContainingCollectionDoc={this.props.ContainingCollectionDoc}
                 chromeCollapsed={true}>
-                {() => [this.youtubeVideoId ? this.youtubeContent : this.content]}
+                {this.contentFunc}
             </CollectionFreeFormView>
             {this.uIButtons}
         </div >);
diff --git a/src/client/views/pdf/PDFViewer.scss b/src/client/views/pdf/PDFViewer.scss
index f6fedf3da..8332501f4 100644
--- a/src/client/views/pdf/PDFViewer.scss
+++ b/src/client/views/pdf/PDFViewer.scss
@@ -6,6 +6,7 @@
     position: absolute;
     overflow-y: auto;
     overflow-x: hidden;
+    transform-origin: top left;
     
     // .canvasWrapper {
     //     transform: scale(0.75);
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 0cb671156..38e29b55d 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -636,7 +636,7 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
                     ScreenToLocalTransform={this.scrollXf}
                     ruleProvider={undefined}
                     renderDepth={this.props.renderDepth + 1}
-                    ContainingCollectionDoc={this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.Document}
+                    ContainingCollectionDoc={this.props.ContainingCollectionView?.props.Document}
                     chromeCollapsed={true}>
                 </CollectionFreeFormView>
             </div>
@@ -660,7 +660,11 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
     render() {
         trace();
         return !this.extensionDoc ? (null) :
-            <div className={"pdfViewer-viewer" + (this._zoomed !== 1 ? "-zoomed" : "")} onScroll={this.onScroll} onWheel={this.onZoomWheel} onPointerDown={this.onPointerDown} onClick={this.onClick} ref={this._mainCont}>
+            <div className={"pdfViewer-viewer" + (this._zoomed !== 1 ? "-zoomed" : "")} style={{
+                width: `${100 / this.props.ContentScaling()}%`,
+                height: `${100 / this.props.ContentScaling()}%`,
+                transform: `scale(${this.props.ContentScaling()})`
+            }} onScroll={this.onScroll} onWheel={this.onZoomWheel} onPointerDown={this.onPointerDown} onClick={this.onClick} ref={this._mainCont}>
                 {this.pdfViewerDiv}
                 {this.annotationLayer}
                 {this.standinViews}
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
index 4531fd5e0..bae7f6a91 100644
--- a/src/new_fields/Doc.ts
+++ b/src/new_fields/Doc.ts
@@ -657,7 +657,7 @@ export namespace Doc {
         return brushManager.BrushedDoc.has(doc) || brushManager.BrushedDoc.has(Doc.GetDataDoc(doc));
     }
     export function IsBrushedDegree(doc: Doc) {
-        return brushManager.BrushedDoc.has(Doc.GetDataDoc(doc)) ? 2 : brushManager.BrushedDoc.has(doc) ? 1 : 0;
+        return brushManager.BrushedDoc.has(doc) ? 2 : brushManager.BrushedDoc.has(Doc.GetDataDoc(doc)) ? 1 : 0;
     }
     export function BrushDoc(doc: Doc) {
         brushManager.BrushedDoc.set(doc, true);
diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts
index a1f1294e6..5b9bba47d 100644
--- a/src/server/authentication/models/current_user_utils.ts
+++ b/src/server/authentication/models/current_user_utils.ts
@@ -46,7 +46,7 @@ export class CurrentUserUtils {
         let notes = CurrentUserUtils.setupNoteTypes(doc);
         doc.noteTypes = Docs.Create.TreeDocument(notes, { title: "Note Types", height: 75 });
         doc.activePen = doc;
-        let docProtoData: { title: string, icon: string, drag?: string, ignoreClick?: boolean, click?: string, unchecked?: string, activePen?: Doc, backgroundColor?: string, dragFactory?: Doc }[] = [
+        let docProtoData: { title: string, icon: string, drag?: string, ignoreClick?: boolean, click?: string, ischecked?: string, activePen?: Doc, backgroundColor?: string, dragFactory?: Doc }[] = [
             { title: "collection", icon: "folder", ignoreClick: true, drag: 'Docs.Create.FreeformDocument([], { nativeWidth: undefined, nativeHeight: undefined, width: 150, height: 100, title: "freeform" })' },
             { title: "todo item", icon: "check", ignoreClick: true, drag: 'getCopy(this.dragFactory, true)', dragFactory: notes[notes.length - 1] },
             { title: "web page", icon: "globe-asia", ignoreClick: true, drag: 'Docs.Create.WebDocument("https://en.wikipedia.org/wiki/Hedgehog", { width: 300, height: 300, title: "New Webpage" })' },
@@ -55,16 +55,16 @@ export class CurrentUserUtils {
             { title: "clickable button", icon: "bolt", ignoreClick: true, drag: 'Docs.Create.ButtonDocument({ width: 150, height: 50, title: "Button" })' },
             { title: "presentation", icon: "tv", ignoreClick: true, drag: 'Doc.UserDoc().curPresentation = Docs.Create.PresDocument(new List<Doc>(), { width: 200, height: 500, title: "a presentation trail" })' },
             { title: "import folder", icon: "cloud-upload-alt", ignoreClick: true, drag: 'Docs.Create.DirectoryImportDocument({ title: "Directory Import", width: 400, height: 400 })' },
-            { title: "use pen", icon: "pen-nib", click: 'activatePen(this.activePen.pen = sameDocs(this.activePen.pen, this) ? undefined : this,2, this.backgroundColor)', backgroundColor: "blue", unchecked: `!sameDocs(this.activePen.pen,  this)`, activePen: doc },
-            { title: "use highlighter", icon: "highlighter", click: 'activateBrush(this.activePen.pen = sameDocs(this.activePen.pen, this) ? undefined : this,20,this.backgroundColor)', backgroundColor: "yellow", unchecked: `!sameDocs(this.activePen.pen, this)`, activePen: doc },
-            { title: "use eraser", icon: "eraser", click: 'activateEraser(this.activePen.pen = sameDocs(this.activePen.pen, this) ? undefined : this);', unchecked: `!sameDocs(this.activePen.pen, this)`, backgroundColor: "pink", activePen: doc },
-            { title: "use scrubber", icon: "eraser", click: 'activateScrubber(this.activePen.pen = sameDocs(this.activePen.pen, this) ? undefined : this);', unchecked: `!sameDocs(this.activePen.pen, this)`, backgroundColor: "green", activePen: doc },
-            { title: "use drag", icon: "mouse-pointer", click: 'deactivateInk();this.activePen.pen = this;', unchecked: `!sameDocs(this.activePen.pen, this) && this.activePen.pen !== undefined`, backgroundColor: "white", activePen: doc },
+            { title: "use pen", icon: "pen-nib", click: 'activatePen(this.activePen.pen = sameDocs(this.activePen.pen, this) ? undefined : this,2, this.backgroundColor)', backgroundColor: "blue", ischecked: `sameDocs(this.activePen.pen,  this)`, activePen: doc },
+            { title: "use highlighter", icon: "highlighter", click: 'activateBrush(this.activePen.pen = sameDocs(this.activePen.pen, this) ? undefined : this,20,this.backgroundColor)', backgroundColor: "yellow", ischecked: `sameDocs(this.activePen.pen, this)`, activePen: doc },
+            { title: "use eraser", icon: "eraser", click: 'activateEraser(this.activePen.pen = sameDocs(this.activePen.pen, this) ? undefined : this);', ischecked: `sameDocs(this.activePen.pen, this)`, backgroundColor: "pink", activePen: doc },
+            { title: "use scrubber", icon: "eraser", click: 'activateScrubber(this.activePen.pen = sameDocs(this.activePen.pen, this) ? undefined : this);', ischecked: `sameDocs(this.activePen.pen, this)`, backgroundColor: "green", activePen: doc },
+            { title: "use drag", icon: "mouse-pointer", click: 'deactivateInk();this.activePen.pen = this;', ischecked: `sameDocs(this.activePen.pen, this)`, backgroundColor: "white", activePen: doc },
         ];
         return docProtoData.map(data => Docs.Create.FontIconDocument({
             nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, dropAction: data.click ? "copy" : undefined, title: data.title, icon: data.icon, ignoreClick: data.ignoreClick,
             onDragStart: data.drag ? ScriptField.MakeFunction(data.drag) : undefined, onClick: data.click ? ScriptField.MakeScript(data.click) : undefined,
-            unchecked: data.unchecked ? ComputedField.MakeFunction(data.unchecked) : undefined, activePen: data.activePen,
+            ischecked: data.ischecked ? ComputedField.MakeFunction(data.ischecked) : undefined, activePen: data.activePen,
             backgroundColor: data.backgroundColor, removeDropProperties: new List<string>(["dropAction"]), dragFactory: data.dragFactory,
         }));
     }
-- 
cgit v1.2.3-70-g09d2


From 6de325def7e6a187d718fb3be77581c44fbd41db Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Mon, 18 Nov 2019 21:44:22 -0500
Subject: more fixes to pdfs etc.

---
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 14 ++++++++----
 src/client/views/pdf/PDFViewer.scss                | 12 +++++-----
 src/client/views/pdf/PDFViewer.tsx                 | 26 +++++++++++++---------
 3 files changed, 31 insertions(+), 21 deletions(-)

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

diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index a1559e049..985d1d272 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -851,6 +851,13 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         );
     }
 
+    children = () => {
+        let eles: JSX.Element[] = [];
+        this.currentStroke && (eles.push(this.currentStroke));
+        this.extensionDoc && (eles.push(...this.childViews()));
+        eles.push(<CollectionFreeFormRemoteCursors {...this.props} key="remoteCursors" />);
+        return eles;
+    }
     render() {
         trace();
         // update the actual dimensions of the collection so that they can inquired (e.g., by a minimap)
@@ -868,9 +875,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
                     addLiveTextDocument={this.addLiveTextBox} getContainerTransform={this.getContainerTransform} getTransform={this.getTransform} isAnnotationOverlay={this.isAnnotationOverlay}>
                     <CollectionFreeFormViewPannableContents centeringShiftX={this.centeringShiftX} centeringShiftY={this.centeringShiftY}
                         easing={this.easing} zoomScaling={this.zoomScaling} panX={this.panX} panY={this.panY}>
-                        {!this.extensionDoc ? (null) : this.childViews()}
-                        {this.currentStroke}
-                        <CollectionFreeFormRemoteCursors {...this.props} key="remoteCursors" />
+                        {this.children}
                     </CollectionFreeFormViewPannableContents>
                 </MarqueeView>
                 {this.overlayViews}
@@ -885,6 +890,7 @@ interface CollectionFreeFormViewPannableContentsProps {
     panY: () => number;
     zoomScaling: () => number;
     easing: () => boolean;
+    children: () => JSX.Element[];
 }
 
 @observer
@@ -897,7 +903,7 @@ class CollectionFreeFormViewPannableContents extends React.Component<CollectionF
         const pany = -this.props.panY();
         const zoom = this.props.zoomScaling();
         return <div className={freeformclass} style={{ borderRadius: "inherit", transform: `translate(${cenx}px, ${ceny}px) scale(${zoom}) translate(${panx}px, ${pany}px)` }}>
-            {this.props.children}
+            {this.props.children()}
         </div>;
     }
 }
\ No newline at end of file
diff --git a/src/client/views/pdf/PDFViewer.scss b/src/client/views/pdf/PDFViewer.scss
index 38aeae04d..62c9298c6 100644
--- a/src/client/views/pdf/PDFViewer.scss
+++ b/src/client/views/pdf/PDFViewer.scss
@@ -1,6 +1,6 @@
  
 .pdfViewer-viewer, .pdfViewer-viewer-zoomed {
-    pointer-events: inherit;
+    pointer-events: all;
     width: 100%;
     height: 100%;
     position: absolute;
@@ -41,11 +41,11 @@
            user-select: text;
         }
     }
-    .pdfViewer-text {
-        .textLayer {
-            will-change: transform;
-        }
-    }
+    // .pdfViewer-text {
+    //     .textLayer {
+    //         will-change: transform;
+    //     }
+    // }
 
     .pdfViewer-dragAnnotationBox {
         position:absolute;
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 6a0c7433a..4ee548447 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -466,10 +466,10 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
                         let annoBox = document.createElement("div");
                         annoBox.className = "pdfViewer-annotationBox";
                         // transforms the positions from screen onto the pdf div
-                        annoBox.style.top = ((rect.top - boundingRect.top) * scaleY + this._mainCont.current.scrollTop).toString();
-                        annoBox.style.left = ((rect.left - boundingRect.left) * scaleX).toString();
-                        annoBox.style.width = (rect.width * this._mainCont.current.offsetWidth / boundingRect.width).toString();
-                        annoBox.style.height = (rect.height * this._mainCont.current.offsetHeight / boundingRect.height).toString();
+                        annoBox.style.top = ((rect.top - boundingRect.top) * scaleY / this._zoomed + this._mainCont.current.scrollTop).toString();
+                        annoBox.style.left = ((rect.left - boundingRect.left) * scaleX / this._zoomed).toString();
+                        annoBox.style.width = (rect.width * this._mainCont.current.offsetWidth / boundingRect.width / this._zoomed).toString();
+                        annoBox.style.height = (rect.height * this._mainCont.current.offsetHeight / boundingRect.height / this._zoomed).toString();
                         this.createAnnotation(annoBox, this.getPageFromScroll(rect.top));
                     }
                 }
@@ -610,7 +610,7 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
         e.stopPropagation();
         if (e.ctrlKey) {
             let curScale = Number(this._pdfViewer.currentScaleValue);
-            this._pdfViewer.currentScaleValue = Math.max(1, Math.min(10, curScale + curScale * e.deltaY / 1000));
+            this._pdfViewer.currentScaleValue = Math.max(1, Math.min(10, curScale - curScale * e.deltaY / 1000));
             this._zoomed = Number(this._pdfViewer.currentScaleValue);
         }
     }
@@ -622,26 +622,29 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
                 <Annotation {...this.props} focus={this.props.focus} extensionDoc={this.extensionDoc!} anno={anno} key={`${anno[Id]}-annotation`} />)}
         </div>;
     }
+    overlayTransform = () => this.scrollXf().scale(1 / this._zoomed);
+    panelWidth = () => (this.Document.scrollHeight || this.Document.nativeHeight || 0);
+    panelHeight = () => this._pageSizes.length && this._pageSizes[0] ? this._pageSizes[0].width : (this.Document.nativeWidth || 0);
     @computed get overlayLayer() {
         return <div className="pdfViewer-overlay" id="overlay" style={{ transform: `scale(${this._zoomed})` }}>
             <CollectionFreeFormView {...this.props}
                 annotationsKey={this.annotationsKey}
                 setPreviewCursor={this.setPreviewCursor}
-                PanelHeight={() => (this.Document.scrollHeight || this.Document.nativeHeight || 0)}
-                PanelWidth={() => this._pageSizes.length && this._pageSizes[0] ? this._pageSizes[0].width : (this.Document.nativeWidth || 0)}
+                PanelHeight={this.panelWidth}
+                PanelWidth={this.panelHeight}
                 VisibleHeight={this.visibleHeight}
                 focus={this.props.focus}
                 isSelected={this.props.isSelected}
                 isAnnotationOverlay={true}
                 select={emptyFunction}
                 active={this.active}
-                ContentScaling={returnOne}
+                ContentScaling={this.contentZoom}
                 whenActiveChanged={this.whenActiveChanged}
                 removeDocument={this.removeDocument}
                 moveDocument={this.moveDocument}
                 addDocument={this.addDocument}
                 CollectionView={undefined}
-                ScreenToLocalTransform={this.scrollXf}
+                ScreenToLocalTransform={this.overlayTransform}
                 ruleProvider={undefined}
                 renderDepth={this.props.renderDepth + 1}
                 ContainingCollectionDoc={this.props.ContainingCollectionView?.props.Document}
@@ -666,13 +669,14 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
     marqueeY = () => this._marqueeY;
     marqueeing = () => this._marqueeing;
     visibleHeight = () => this.props.PanelHeight() / this.props.ContentScaling() * 72 / 96;
+    contentZoom = () => this._zoomed;
     render() {
         trace();
         return !this.extensionDoc ? (null) :
             <div className={"pdfViewer-viewer" + (this._zoomed !== 1 ? "-zoomed" : "")}
                 style={{
-                    width: `${100 / this.props.ContentScaling()}%`,
-                    height: `${100 / this.props.ContentScaling()}%`,
+                    width: !this.props.Document.fitWidth ? NumCast(this.props.Document.nativeWidth) : `${100 / this.props.ContentScaling()}%`,
+                    height: !this.props.Document.fitWidth ? NumCast(this.props.Document.nativeHeight) : `${100 / this.props.ContentScaling()}%`,
                     transform: `scale(${this.props.ContentScaling()})`
                 }} onScroll={this.onScroll} onWheel={this.onZoomWheel} onPointerDown={this.onPointerDown} onClick={this.onClick} ref={this._mainCont}>
                 {this.pdfViewerDiv}
-- 
cgit v1.2.3-70-g09d2


From 617798db46a204ff089bf3400832af67872cc02f Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Tue, 19 Nov 2019 09:45:10 -0500
Subject: more fixes to pdfs

---
 src/client/documents/Documents.ts                              |  2 +-
 src/client/views/DocComponent.tsx                              |  6 +++---
 .../collections/collectionFreeForm/CollectionFreeFormView.tsx  |  1 -
 src/client/views/nodes/CollectionFreeFormDocumentView.tsx      |  5 ++++-
 src/client/views/nodes/PDFBox.scss                             |  4 ++--
 src/client/views/nodes/PDFBox.tsx                              |  4 +++-
 src/client/views/pdf/PDFViewer.scss                            | 10 +++++-----
 src/client/views/pdf/PDFViewer.tsx                             |  3 ++-
 8 files changed, 20 insertions(+), 15 deletions(-)

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 3c88173cd..c5bf109a1 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -647,7 +647,7 @@ export namespace Docs {
             }
             if (type.indexOf("pdf") !== -1) {
                 ctor = Docs.Create.PdfDocument;
-                options.nativeWidth = 1200;
+                options.nativeWidth = 927;
                 options.nativeHeight = 1200;
             }
             if (type.indexOf("excel") !== -1) {
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index b59bd4f1d..961a5a016 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -1,7 +1,7 @@
 import * as React from 'react';
 import { Doc } from '../../new_fields/Doc';
 import { Touchable } from './Touchable';
-import { computed, action } from 'mobx';
+import { computed, action, observable } from 'mobx';
 import { Cast } from '../../new_fields/Types';
 import { listSpec } from '../../new_fields/Schema';
 import { InkingControl } from './InkingControl';
@@ -54,7 +54,7 @@ interface DocAnnotatableProps {
 }
 export function DocAnnotatableComponent<P extends DocAnnotatableProps, T>(schemaCtor: (doc: Doc) => T) {
     class Component extends React.Component<P> {
-        _isChildActive = false;
+        @observable _isChildActive = false;
         //TODO This might be pretty inefficient if doc isn't observed, because computed doesn't cache then
         @computed get Document(): T { return schemaCtor(this.props.Document); }
         @computed get layoutDoc() { return Doc.Layout(this.props.Document); }
@@ -81,7 +81,7 @@ export function DocAnnotatableComponent<P extends DocAnnotatableProps, T>(schema
             return this.extensionDoc && Doc.AddDocToList(this.extensionDoc, this.annotationsKey, doc) ? true : false;
         }
 
-        whenActiveChanged = (isActive: boolean) => this.props.whenActiveChanged(this._isChildActive = isActive);
+        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)
     }
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 985d1d272..3b313c34a 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -484,7 +484,6 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
 
     @action
     zoom = (pointX: number, pointY: number, deltaY: number): void => {
-        console.log(deltaY);
         let deltaScale = deltaY > 0 ? (1 / 1.1) : 1.1;
         if (deltaScale * this.zoomScaling() < 1 && this.isAnnotationOverlay) {
             deltaScale = 1 / this.zoomScaling();
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index a035bdc3d..d0e1d1922 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -28,7 +28,10 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
     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); }
     get width() { return this.renderScriptDim ? this.renderScriptDim.width : this.props.width !== undefined ? this.props.width : this.props.dataProvider && this.dataProvider ? this.dataProvider.width : this.layoutDoc[WidthSym](); }
-    get height() { return this.renderScriptDim ? this.renderScriptDim.height : this.props.height !== undefined ? this.props.height : this.props.dataProvider && this.dataProvider ? this.dataProvider.height : this.layoutDoc[HeightSym](); }
+    get height() {
+        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 nativeWidth() { return NumCast(this.layoutDoc.nativeWidth); }
     @computed get nativeHeight() { return NumCast(this.layoutDoc.nativeHeight); }
diff --git a/src/client/views/nodes/PDFBox.scss b/src/client/views/nodes/PDFBox.scss
index 963205206..2674c0de6 100644
--- a/src/client/views/nodes/PDFBox.scss
+++ b/src/client/views/nodes/PDFBox.scss
@@ -26,10 +26,10 @@
             color:lightgray;
             margin-top: auto;
             margin-bottom: auto;
-            transform-origin: 42% -18%;
+            transform-origin: 42% 15%;
             width: 100%;
             transform: rotate(55deg);
-            font-size: 144;
+            font-size: 72;
             padding: 5%;
             overflow: hidden;
             display: inline-block;
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 8ed76f9d1..634cd67c3 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -199,11 +199,12 @@ export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument>
         let classname = "pdfBox-cont" + (this.active() ? "-interactive" : "");
         return <div className="pdfBox-title-outer" >
             <div className={classname} >
-                <strong className="pdfBox-title" >{` ${this.props.Document.title}`}</strong>
+                <strong className="pdfBox-title" >{this.props.Document.title}</strong>
             </div>
         </div>;
     }
 
+    isChildActive = () => this._isChildActive;
     @computed get renderPdfView() {
         const pdfUrl = Cast(this.dataDoc[this.props.fieldKey], PdfField);
         return <div className={"pdfBox-cont"} onContextMenu={this.specificContextMenu}>
@@ -215,6 +216,7 @@ export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument>
                 pinToPres={this.props.pinToPres} addDocument={this.addDocument}
                 ScreenToLocalTransform={this.props.ScreenToLocalTransform} select={this.props.select}
                 isSelected={this.props.isSelected} whenActiveChanged={this.whenActiveChanged}
+                isChildActive={this.isChildActive}
                 fieldKey={this.props.fieldKey} startupLive={this._initialScale < 2.5 ? true : false} />
             {this.settingsPanel()}
         </div>;
diff --git a/src/client/views/pdf/PDFViewer.scss b/src/client/views/pdf/PDFViewer.scss
index 62c9298c6..f69bdeeb6 100644
--- a/src/client/views/pdf/PDFViewer.scss
+++ b/src/client/views/pdf/PDFViewer.scss
@@ -41,11 +41,11 @@
            user-select: text;
         }
     }
-    // .pdfViewer-text {
-    //     .textLayer {
-    //         will-change: transform;
-    //     }
-    // }
+    .pdfViewer-text {
+        .textLayer {
+            will-change: transform;
+        }
+    }
 
     .pdfViewer-dragAnnotationBox {
         position:absolute;
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 4ee548447..f34c4abd3 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -59,6 +59,7 @@ interface IViewerProps {
     isSelected: () => boolean;
     loaded: (nw: number, nh: number, np: number) => void;
     active: () => boolean;
+    isChildActive: () => boolean;
     addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => boolean;
     pinToPres: (document: Doc) => void;
     addDocument?: (doc: Doc) => boolean;
@@ -653,7 +654,7 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
         </div>
     }
     @computed get pdfViewerDiv() {
-        return <div className={"pdfViewer-text" + (this.props.isSelected() ? "-selected" : "")} ref={this._viewer} style={{
+        return <div className={"pdfViewer-text" + (this.props.isSelected() || this.props.isChildActive() ? "-selected" : "")} ref={this._viewer} style={{
             transformOrigin: "left top",
         }} />;
     }
-- 
cgit v1.2.3-70-g09d2


From 185257e441cce6c0d599d95f6fe79cb3deef62d6 Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Tue, 19 Nov 2019 11:58:09 -0500
Subject: fixed drawing on images, pdfs, and videos

---
 src/client/views/DocComponent.tsx                  |  2 ++
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 34 +++++++---------------
 src/client/views/nodes/FormattedTextBox.tsx        |  2 +-
 src/client/views/nodes/ImageBox.scss               |  1 +
 src/client/views/nodes/ImageBox.tsx                |  2 +-
 src/client/views/nodes/VideoBox.tsx                |  2 +-
 src/client/views/pdf/PDFViewer.scss                |  7 ++++-
 src/client/views/pdf/PDFViewer.tsx                 |  6 ++--
 8 files changed, 26 insertions(+), 30 deletions(-)

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

diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index 961a5a016..286a77f81 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -84,6 +84,8 @@ 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)
     }
     return Component;
 }
\ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 3b313c34a..2a63a3074 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -4,43 +4,41 @@ import { faBraille, faChalkboard, faCompass, faCompressArrowsAlt, faExpandArrows
 import { action, computed, observable, trace } from "mobx";
 import { observer } from "mobx-react";
 import { Doc, DocListCast, HeightSym, Opt, WidthSym } from "../../../../new_fields/Doc";
+import { documentSchema, positionSchema } from "../../../../new_fields/documentSchemas";
 import { Id } from "../../../../new_fields/FieldSymbols";
-import { InkField, PointData, InkTool } from "../../../../new_fields/InkField";
+import { InkTool } from "../../../../new_fields/InkField";
 import { createSchema, makeInterface } from "../../../../new_fields/Schema";
 import { ScriptField } from "../../../../new_fields/ScriptField";
 import { BoolCast, Cast, DateCast, NumCast, StrCast } from "../../../../new_fields/Types";
 import { CurrentUserUtils } from "../../../../server/authentication/models/current_user_utils";
 import { aggregateBounds, emptyFunction, intersectRect, returnOne, Utils } from "../../../../Utils";
-import { CognitiveServices } from "../../../cognitive_services/CognitiveServices";
 import { DocServer } from "../../../DocServer";
 import { Docs } from "../../../documents/Documents";
 import { DocumentType } from "../../../documents/DocumentTypes";
 import { DocumentManager } from "../../../util/DocumentManager";
 import { DragManager } from "../../../util/DragManager";
 import { HistoryUtil } from "../../../util/History";
+import { InteractionUtils } from "../../../util/InteractionUtils";
 import { SelectionManager } from "../../../util/SelectionManager";
 import { Transform } from "../../../util/Transform";
 import { undoBatch, UndoManager } from "../../../util/UndoManager";
 import { COLLECTION_BORDER_WIDTH } from "../../../views/globalCssVariables.scss";
 import { ContextMenu } from "../../ContextMenu";
 import { ContextMenuProps } from "../../ContextMenuItem";
-import { InkingCanvas } from "../../InkingCanvas";
+import { InkingControl } from "../../InkingControl";
+import { CreatePolyline } from "../../InkingStroke";
 import { CollectionFreeFormDocumentView } from "../../nodes/CollectionFreeFormDocumentView";
 import { DocumentViewProps } from "../../nodes/DocumentView";
 import { FormattedTextBox } from "../../nodes/FormattedTextBox";
 import { pageSchema } from "../../nodes/ImageBox";
+import PDFMenu from "../../pdf/PDFMenu";
 import { CollectionSubView } from "../CollectionSubView";
 import { computePivotLayout, ViewDefResult } from "./CollectionFreeFormLayoutEngines";
 import { CollectionFreeFormRemoteCursors } from "./CollectionFreeFormRemoteCursors";
 import "./CollectionFreeFormView.scss";
+import MarqueeOptionsMenu from "./MarqueeOptionsMenu";
 import { MarqueeView } from "./MarqueeView";
 import React = require("react");
-import { InteractionUtils } from "../../../util/InteractionUtils";
-import MarqueeOptionsMenu from "./MarqueeOptionsMenu";
-import PDFMenu from "../../pdf/PDFMenu";
-import { documentSchema, positionSchema } from "../../../../new_fields/documentSchemas";
-import { InkingControl } from "../../InkingControl";
-import { InkingStroke, CreatePolyline } from "../../InkingStroke";
 
 library.add(faEye as any, faTable, faPaintBrush, faExpandArrowsAlt, faCompressArrowsAlt, faCompass, faUpload, faBraille, faChalkboard, faFileUpload);
 
@@ -273,7 +271,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.isAnnotationOverlay || this.zoomScaling() !== 1) && this.props.active()) {
+        if (e.button === 0 && !e.shiftKey && !e.altKey && !e.ctrlKey && this.props.active()) {
             document.removeEventListener("pointermove", this.onPointerMove);
             document.removeEventListener("pointerup", this.onPointerUp);
             document.addEventListener("pointermove", this.onPointerMove);
@@ -344,14 +342,6 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
                 return [[range[0][0] > x ? x : range[0][0], range[0][1] < xe ? xe : range[0][1]],
                 [range[1][0] > y ? y : range[1][0], range[1][1] < ye ? ye : range[1][1]]];
             }, [[minx, maxx], [miny, maxy]]);
-            let ink = this.extensionDoc && Cast(this.extensionDoc.ink, InkField);
-            if (ink && ink.inkData) {
-                // ink.inkData.forEach((value: PointData, key: string) => {
-                //     let bounds = InkingCanvas.StrokeRect(value);
-                //     ranges[0] = [Math.min(ranges[0][0], bounds.left), Math.max(ranges[0][1], bounds.right)];
-                //     ranges[1] = [Math.min(ranges[1][0], bounds.top), Math.max(ranges[1][1], bounds.bottom)];
-                // });
-            }
 
             let cscale = this.props.ContainingCollectionDoc ? NumCast(this.props.ContainingCollectionDoc.scale) : 1;
             let panelDim = this.props.ScreenToLocalTransform().transformDirection(this.props.PanelWidth() / this.zoomScaling() * cscale,
@@ -757,11 +747,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
     }
 
     analyzeStrokes = async () => {
-        const extensionDoc = this.extensionDoc;
-        let data = extensionDoc && Cast(extensionDoc.ink, InkField);
-        if (data && extensionDoc) {
-            CognitiveServices.Inking.Appliers.ConcatenateHandwriting(extensionDoc, ["inkAnalysis", "handwriting"], data.inkData);
-        }
+        //  CognitiveServices.Inking.Appliers.ConcatenateHandwriting(this.dataDoc, ["inkAnalysis", "handwriting"], data.inkData);
     }
 
     onContextMenu = (e: React.MouseEvent) => {
@@ -852,8 +838,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
 
     children = () => {
         let eles: JSX.Element[] = [];
-        this.currentStroke && (eles.push(this.currentStroke));
         this.extensionDoc && (eles.push(...this.childViews()));
+        this.currentStroke && (eles.push(this.currentStroke));
         eles.push(<CollectionFreeFormRemoteCursors {...this.props} key="remoteCursors" />);
         return eles;
     }
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 4a79a44d4..4ecdc95ac 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -1058,7 +1058,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
                         focus={this.props.focus}
                         isSelected={this.props.isSelected}
                         select={emptyFunction}
-                        active={this.active}
+                        active={this.annotationsActive}
                         ContentScaling={returnOne}
                         whenActiveChanged={this.whenActiveChanged}
                         removeDocument={this.removeDocument}
diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss
index 57c024bbf..dcecbdc6e 100644
--- a/src/client/views/nodes/ImageBox.scss
+++ b/src/client/views/nodes/ImageBox.scss
@@ -11,6 +11,7 @@
 }
 
 .imageBox-container {
+    pointer-events: all;
     border-radius: inherit;
     width:100%;
     height:100%;
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 2a81c3577..5d40e274f 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -343,7 +343,7 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
                 focus={this.props.focus}
                 isSelected={this.props.isSelected}
                 select={emptyFunction}
-                active={this.active}
+                active={this.annotationsActive}
                 ContentScaling={returnOne}
                 whenActiveChanged={this.whenActiveChanged}
                 removeDocument={this.removeDocument}
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index dd6e60c51..bbf6d8d03 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -345,7 +345,7 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum
                 isSelected={this.props.isSelected}
                 isAnnotationOverlay={true}
                 select={emptyFunction}
-                active={this.active}
+                active={this.annotationsActive}
                 ContentScaling={returnOne}
                 whenActiveChanged={this.whenActiveChanged}
                 removeDocument={this.removeDocument}
diff --git a/src/client/views/pdf/PDFViewer.scss b/src/client/views/pdf/PDFViewer.scss
index 6f9dbb78d..ac018aa0e 100644
--- a/src/client/views/pdf/PDFViewer.scss
+++ b/src/client/views/pdf/PDFViewer.scss
@@ -54,7 +54,7 @@
         opacity: 0.1;
     }
 
-    .pdfViewer-overlay {
+    .pdfViewer-overlay, .pdfViewer-overlay-inking {
         transform-origin: left top;
         position: absolute;
         top: 0px;
@@ -63,6 +63,11 @@
         width:100%;
         pointer-events: none;
     }
+    .pdfViewer-overlay-inking {
+        .collectionfreeformview-container {
+            pointer-events: all;
+        }
+    }
     .pdfViewer-annotationLayer {
         position: absolute;
         transform-origin: left top;
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 5058ce851..ec8d8be11 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -27,6 +27,8 @@ import { DocAnnotatableComponent } from "../DocComponent";
 import { DocumentType } from "../../documents/DocumentTypes";
 import { documentSchema } from "../../../new_fields/documentSchemas";
 import { DocumentDecorations } from "../DocumentDecorations";
+import { InkingControl } from "../InkingControl";
+import { InkTool } from "../../../new_fields/InkField";
 const PDFJSViewer = require("pdfjs-dist/web/pdf_viewer");
 const pdfjsLib = require("pdfjs-dist");
 
@@ -628,7 +630,7 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
     panelWidth = () => (this.Document.scrollHeight || this.Document.nativeHeight || 0);
     panelHeight = () => this._pageSizes.length && this._pageSizes[0] ? this._pageSizes[0].width : (this.Document.nativeWidth || 0);
     @computed get overlayLayer() {
-        return <div className="pdfViewer-overlay" id="overlay" style={{ transform: `scale(${this._zoomed})` }}>
+        return <div className={`pdfViewer-overlay${InkingControl.Instance.selectedTool !== InkTool.None ? "-inking" : ""}`} id="overlay" style={{ transform: `scale(${this._zoomed})` }}>
             <CollectionFreeFormView {...this.props}
                 annotationsKey={this.annotationsKey}
                 setPreviewCursor={this.setPreviewCursor}
@@ -639,7 +641,7 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
                 isSelected={this.props.isSelected}
                 isAnnotationOverlay={true}
                 select={emptyFunction}
-                active={this.active}
+                active={this.annotationsActive}
                 ContentScaling={this.contentZoom}
                 whenActiveChanged={this.whenActiveChanged}
                 removeDocument={this.removeDocument}
-- 
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/collectionFreeForm/CollectionFreeFormView.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


From b1fadd1137f35fe64b9dc8df42a3160882f4cccd Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Thu, 21 Nov 2019 11:59:07 -0500
Subject: more fixes to layout stuff

---
 src/client/views/DocumentDecorations.tsx           |  4 +--
 .../CollectionFreeFormLayoutEngines.tsx            | 34 +++++++++++++++-------
 .../collectionFreeForm/CollectionFreeFormView.scss | 11 ++++---
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 23 +++++++--------
 src/client/views/pdf/PDFViewer.tsx                 |  7 +++--
 5 files changed, 45 insertions(+), 34 deletions(-)

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

diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 210fa40dc..66f47147f 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -463,7 +463,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
                 break;
         }
 
-        SelectionManager.SelectedDocuments().forEach(element => {
+        SelectionManager.SelectedDocuments().forEach(action((element: DocumentView) => {
             if (dX !== 0 || dY !== 0 || dW !== 0 || dH !== 0) {
                 let doc = PositionDocument(element.props.Document);
                 let layoutDoc = PositionDocument(Doc.Layout(element.props.Document));
@@ -509,7 +509,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
                     dH && layoutDoc.autoHeight && (layoutDoc.autoHeight = false);
                 }
             }
-        });
+        }));
     }
 
     @action
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
index 48d330674..e1d23ddcb 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
@@ -6,6 +6,8 @@ import { ScriptField } from "../../../../new_fields/ScriptField";
 import { OverlayView, OverlayElementOptions } from "../../OverlayView";
 import { emptyFunction } from "../../../../Utils";
 import React = require("react");
+import { ObservableMap, runInAction } from "mobx";
+import { Id } from "../../../../new_fields/FieldSymbols";
 
 interface PivotData {
     type: string;
@@ -31,8 +33,7 @@ export interface ViewDefResult {
     bounds?: ViewDefBounds;
 }
 
-export function computePivotLayout(pivotDoc: Doc, childDocs: Doc[], childPairs: { layout: Doc, data?: Doc }[], viewDefsToJSX: (views: any) => ViewDefResult[]) {
-    let layoutPoolData: Map<{ layout: Doc, data?: Doc }, any> = new Map();
+export function computePivotLayout(poolData: ObservableMap<string, any>, pivotDoc: Doc, childDocs: Doc[], childPairs: { layout: Doc, data?: Doc }[], viewDefsToJSX: (views: any) => ViewDefResult[]) {
     const pivotAxisWidth = NumCast(pivotDoc.pivotWidth, 200);
     const pivotColumnGroups = new Map<FieldResult<Field>, Doc[]>();
 
@@ -49,6 +50,8 @@ export function computePivotLayout(pivotDoc: Doc, childDocs: Doc[], childPairs:
     const docMap = new Map<Doc, ViewDefBounds>();
     const groupNames: PivotData[] = [];
 
+    const expander = 1.05;
+    const gap = .15;
     let x = 0;
     pivotColumnGroups.forEach((val, key) => {
         let y = 0;
@@ -58,25 +61,31 @@ export function computePivotLayout(pivotDoc: Doc, childDocs: Doc[], childPairs:
             text: String(key),
             x,
             y: pivotAxisWidth + 50,
-            width: pivotAxisWidth * 1.25 * numCols,
+            width: pivotAxisWidth * expander * numCols,
             height: 100,
             fontSize: NumCast(pivotDoc.pivotFontSize, 10)
         });
         for (const doc of val) {
             let layoutDoc = Doc.Layout(doc);
+            let wid = pivotAxisWidth;
+            let hgt = layoutDoc.nativeWidth ? (NumCast(layoutDoc.nativeHeight) / NumCast(layoutDoc.nativeWidth)) * pivotAxisWidth : pivotAxisWidth;
+            if (hgt > pivotAxisWidth) {
+                hgt = pivotAxisWidth;
+                wid = layoutDoc.nativeHeight ? (NumCast(layoutDoc.nativeWidth) / NumCast(layoutDoc.nativeHeight)) * pivotAxisWidth : pivotAxisWidth;
+            }
             docMap.set(doc, {
-                x: x + xCount * pivotAxisWidth * 1.25,
+                x: x + xCount * pivotAxisWidth * expander + (pivotAxisWidth - wid) / 2,
                 y: -y,
-                width: pivotAxisWidth,
-                height: layoutDoc.nativeWidth ? (NumCast(layoutDoc.nativeHeight) / NumCast(layoutDoc.nativeWidth)) * pivotAxisWidth : pivotAxisWidth
+                width: wid,
+                height: hgt
             });
             xCount++;
             if (xCount >= numCols) {
-                xCount = 0;
-                y += pivotAxisWidth * 1.25;
+                xCount = (pivotAxisWidth - wid) / 2;
+                y += pivotAxisWidth * expander;
             }
         }
-        x += pivotAxisWidth * 1.25 * (numCols + 1);
+        x += pivotAxisWidth * (numCols * expander + gap);
     });
 
     childPairs.map(pair => {
@@ -88,9 +97,12 @@ export function computePivotLayout(pivotDoc: Doc, childDocs: Doc[], childPairs:
             height: NumCast(pair.layout.height)
         };
         const pos = docMap.get(pair.layout) || defaultPosition;
-        layoutPoolData.set(pair, { transition: "transform 1s", ...pos });
+        let data = poolData.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) {
+            runInAction(() => poolData.set(pair.layout[Id], { transition: "transform 1s", ...pos }));
+        }
     });
-    return { map: layoutPoolData, elements: viewDefsToJSX(groupNames) };
+    return { elements: viewDefsToJSX(groupNames) };
 }
 
 export function AddCustomFreeFormLayout(doc: Doc, dataKey: string): () => void {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
index d2731703f..070d4aa65 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
@@ -1,5 +1,6 @@
 @import "../../globalCssVariables";
 
+.collectionfreeformview-none,
 .collectionfreeformview-ease {
     position: inherit;
     top: 0;
@@ -7,16 +8,14 @@
     width: 100%;
     height: 100%;
     transform-origin: left top;
+    border-radius: inherit;
+}
+
+.collectionfreeformview-ease {
     transition: transform 1s;
 }
 
 .collectionfreeformview-none {
-    position: inherit;
-    top: 0;
-    left: 0;
-    width: 100%;
-    height: 100%;
-    transform-origin: left top;
     touch-action: none;
 }
 
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 256ceb8f8..3dd655b96 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -73,7 +73,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
     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.shallow _layoutElements: ViewDefResult[] = []; // shallow because some layout items (eg pivot labels) are just generated 'divs' and can't be frozen as observables
     @observable _clusterSets: (Doc[])[] = [];
 
     @computed get fitToContent() { return (this.props.fitToBox || this.Document.fitToBox) && !this.isAnnotationOverlay; }
@@ -653,12 +653,12 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
 
     childDataProvider = computedFn((doc: Doc) => this._layoutPoolData.get(doc[Id]));
 
-    get doPivotLayout() {
-        return computePivotLayout(this.props.Document, this.childDocs,
+    doPivotLayout(poolData: ObservableMap<string, any>) {
+        return computePivotLayout(poolData, this.props.Document, this.childDocs,
             this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)), this.viewDefsToJSX);
     }
 
-    get doFreeformLayout() {
+    doFreeformLayout(poolData: ObservableMap<string, any>) {
         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;
@@ -669,7 +669,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
             state = pos.state === undefined ? state : pos.state;
             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));
+                runInAction(() => poolData.set(pair.layout[Id], pos));
             }
         });
         return { elements: elements };
@@ -678,8 +678,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
     get doLayoutComputation() {
         let computedElementData: { elements: ViewDefResult[] };
         switch (this.Document.freeformLayoutEngine) {
-            case "pivot": computedElementData = this.doPivotLayout; break;
-            default: computedElementData = this.doFreeformLayout; break;
+            case "pivot": computedElementData = this.doPivotLayout(this._layoutPoolData); break;
+            default: computedElementData = this.doFreeformLayout(this._layoutPoolData); break;
         }
         this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).forEach(pair =>
             computedElementData.elements.push({
@@ -693,14 +693,14 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
     }
 
     componentDidMount() {
-        this._layoutComputeReaction = reaction(() => this.doLayoutComputation,
+        this._layoutComputeReaction = reaction(() => { trace(); return 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); }
+    @computed get views() { return this._layoutElements.filter(ele => ele.bounds && !ele.bounds.z).map(ele => ele.ele); }
     elementFunc = () => this._layoutElements;
 
     @action
@@ -873,9 +873,8 @@ interface CollectionFreeFormOverlayViewProps {
 
 @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;
+        return this.props.elements().filter(ele => ele.bounds && ele.bounds.z).map(ele => ele.ele);
     }
 }
 
@@ -898,7 +897,7 @@ class CollectionFreeFormViewPannableContents extends React.Component<CollectionF
         const panx = -this.props.panX();
         const pany = -this.props.panY();
         const zoom = this.props.zoomScaling();
-        return <div className={freeformclass} style={{ borderRadius: "inherit", transform: `translate(${cenx}px, ${ceny}px) scale(${zoom}) translate(${panx}px, ${pany}px)` }}>
+        return <div className={freeformclass} style={{ transform: `translate(${cenx}px, ${ceny}px) scale(${zoom}) translate(${panx}px, ${pany}px)` }}>
             {this.props.children()}
         </div>;
     }
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 7d8f39e41..22c3a29a8 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -672,14 +672,15 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
     marqueeing = () => this._marqueeing;
     visibleHeight = () => this.props.PanelHeight() / this.props.ContentScaling() * 72 / 96;
     contentZoom = () => this._zoomed;
+    @computed get contentScaling() { return this.props.ContentScaling() }
     render() {
         trace();
         return !this.extensionDoc ? (null) :
             <div className={"pdfViewer-viewer" + (this._zoomed !== 1 ? "-zoomed" : "")} ref={this._mainCont}
                 style={{
-                    width: !this.props.Document.fitWidth ? NumCast(this.props.Document.nativeWidth) : `${100 / this.props.ContentScaling()}%`,
-                    height: !this.props.Document.fitWidth ? NumCast(this.props.Document.nativeHeight) : `${100 / this.props.ContentScaling()}%`,
-                    transform: `scale(${this.props.ContentScaling()})`
+                    width: !this.props.Document.fitWidth ? NumCast(this.props.Document.nativeWidth) : `${100 / this.contentScaling}%`,
+                    height: !this.props.Document.fitWidth ? NumCast(this.props.Document.nativeHeight) : `${100 / this.contentScaling}%`,
+                    transform: `scale(${this.contentScaling})`
                 }} onScroll={this.onScroll} onWheel={this.onZoomWheel} onPointerDown={this.onPointerDown} onClick={this.onClick}>
                 {this.pdfViewerDiv}
                 {this.overlayLayer}
-- 
cgit v1.2.3-70-g09d2


From ba3f889a61715b715a230d0d24894a5951fbbb3b Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Thu, 21 Nov 2019 13:14:39 -0500
Subject: fixed inking on top of existing ink.  fixed zooming maximally out.

---
 src/client/views/InkingStroke.tsx                      | 18 +++++++-----------
 .../collectionFreeForm/CollectionFreeFormView.tsx      |  8 +++++---
 src/client/views/nodes/DocumentView.tsx                |  4 +++-
 3 files changed, 15 insertions(+), 15 deletions(-)

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

diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index 74211cc90..723b8cac4 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -35,7 +35,6 @@ export class InkingStroke extends DocExtendableComponent<FieldViewProps, InkDocu
     @computed get PanelHeight() { return this.props.PanelHeight(); }
 
     render() {
-        // let pathData = this.parseData(this.props.line);
         let data: InkData = Cast(this.Document.data, InkField)?.inkData ?? [];
         let xs = data.map(p => p.x);
         let ys = data.map(p => p.y);
@@ -48,15 +47,12 @@ export class InkingStroke extends DocExtendableComponent<FieldViewProps, InkDocu
         let height = bottom - top;
         let scaleX = this.PanelWidth / width;
         let scaleY = this.PanelHeight / height;
-        // let pathlength = this.props.count; // bcz: this is needed to force reactions to the line's data changes
-        return (
-            <svg width={width} height={height} style={{
-                transformOrigin: "top left",
-                transform: `translate(${left}px, ${top}px) scale(${scaleX}, ${scaleY})`,
-                mixBlendMode: this.Document.tool === InkTool.Highlighter ? "multiply" : "unset"
-            }}>
-                {points}
-            </svg>
-        );
+        return <svg width={width} height={height} style={{
+            transformOrigin: "top left",
+            transform: `translate(${left}px, ${top}px) scale(${scaleX}, ${scaleY})`,
+            mixBlendMode: this.Document.tool === InkTool.Highlighter ? "multiply" : "unset"
+        }}>
+            {points}
+        </svg>;
     }
 }
\ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 3dd655b96..dc98e662d 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -488,9 +488,11 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         let [x, y] = this.getTransform().transformPoint(pointX, pointY);
         let localTransform = this.getLocalTransform().inverse().scaleAbout(deltaScale, x, y);
 
-        let safeScale = Math.min(Math.max(0.15, localTransform.Scale), 40);
-        this.props.Document.scale = Math.abs(safeScale);
-        this.setPan(-localTransform.TranslateX / safeScale, -localTransform.TranslateY / safeScale);
+        if (localTransform.Scale >= 0.15) {
+            let safeScale = Math.min(Math.max(0.15, localTransform.Scale), 40);
+            this.props.Document.scale = Math.abs(safeScale);
+            this.setPan(-localTransform.TranslateX / safeScale, -localTransform.TranslateY / safeScale);
+        }
     }
 
     @action
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index cf5a94f01..a985f3069 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -41,6 +41,8 @@ import "./DocumentView.scss";
 import { FormattedTextBox } from './FormattedTextBox';
 import React = require("react");
 import { InteractionUtils } from '../../util/InteractionUtils';
+import { InkingControl } from '../InkingControl';
+import { InkTool } from '../../../new_fields/InkField';
 
 library.add(fa.faEdit, fa.faTrash, fa.faShare, fa.faDownload, fa.faExpandArrowsAlt, fa.faCompressArrowsAlt, fa.faLayerGroup, fa.faExternalLinkAlt, fa.faAlignCenter, fa.faCaretSquareRight,
     fa.faSquare, fa.faConciergeBell, fa.faWindowRestore, fa.faFolder, fa.faMapPin, fa.faLink, fa.faFingerprint, fa.faCrosshairs, fa.faDesktop, fa.faUnlock, fa.faLock, fa.faLaptopCode, fa.faMale,
@@ -639,7 +641,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         </>;
     }
     @computed get ignorePointerEvents() {
-        return this.Document.isBackground && !this.isSelected();
+        return (this.Document.isBackground && !this.isSelected()) || (this.Document.type === DocumentType.INK && InkingControl.Instance.selectedTool !== InkTool.None);
     }
 
     render() {
-- 
cgit v1.2.3-70-g09d2


From 17474b9b840c78df4dc2601e82cf63a85e0bcbf7 Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Thu, 21 Nov 2019 13:57:54 -0500
Subject: added names to make mobx tracking easier for freeform reactions

---
 .../views/collections/collectionFreeForm/CollectionFreeFormView.tsx   | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

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

diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index dc98e662d..8325ffe99 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -653,7 +653,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         }
     }
 
-    childDataProvider = computedFn((doc: Doc) => this._layoutPoolData.get(doc[Id]));
+    childDataProvider = computedFn(function childDataProvider(doc: Doc) { return (this as any)._layoutPoolData.get(doc[Id]); });
 
     doPivotLayout(poolData: ObservableMap<string, any>) {
         return computePivotLayout(poolData, this.props.Document, this.childDocs,
@@ -697,7 +697,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
     componentDidMount() {
         this._layoutComputeReaction = reaction(() => { trace(); return this.doLayoutComputation },
             action((computation: { elements: ViewDefResult[] }) => computation && (this._layoutElements = computation.elements)),
-            { fireImmediately: true });
+            { fireImmediately: true, name: "doLayout" });
     }
     componentWillUnmount() {
         this._layoutComputeReaction && this._layoutComputeReaction();
-- 
cgit v1.2.3-70-g09d2


From 7ef52a87d1731770c6e1a8cd1aef31cb384fff05 Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Thu, 21 Nov 2019 16:27:56 -0500
Subject: made textbox sidebar toggle a widget.  fixed isAnimating stuff.  made
 TraceMobx to simplify tracing

---
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 16 +++---
 .../views/nodes/CollectionFreeFormDocumentView.tsx | 13 ++---
 src/client/views/nodes/DocumentView.tsx            |  7 ++-
 src/client/views/nodes/FormattedTextBox.scss       | 24 +++++++-
 src/client/views/nodes/FormattedTextBox.tsx        | 66 +++++++++++++---------
 src/client/views/nodes/ImageBox.tsx                |  3 +-
 src/client/views/pdf/PDFViewer.tsx                 |  5 +-
 src/new_fields/documentSchemas.ts                  |  2 +-
 src/new_fields/util.ts                             |  6 +-
 9 files changed, 89 insertions(+), 53 deletions(-)

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

diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 8325ffe99..b2344771d 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -40,6 +40,7 @@ import MarqueeOptionsMenu from "./MarqueeOptionsMenu";
 import { MarqueeView } from "./MarqueeView";
 import React = require("react");
 import { computedFn, keepAlive } from "mobx-utils";
+import { TraceMobx } from "../../../../new_fields/util";
 
 library.add(faEye as any, faTable, faPaintBrush, faExpandArrowsAlt, faCompressArrowsAlt, faCompass, faUpload, faBraille, faChalkboard, faFileUpload);
 
@@ -621,12 +622,11 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
     }
 
     getCalculatedPositions(params: { doc: Doc, index: number, collection: Doc, docs: Doc[], state: any }): { x?: number, y?: number, z?: number, width?: number, height?: number, transition?: string, state?: any } {
-        const script = this.Document.arrangeScript;
-        const result = script && script.script.run(params, console.log);
-        const layoutDoc = Doc.Layout(params.doc);
-        if (result && result.success) {
+        const result = this.Document.arrangeScript?.script.run(params, console.log);
+        if (result?.success) {
             return { ...result, transition: "transform 1s" };
         }
+        const layoutDoc = Doc.Layout(params.doc);
         return { x: Cast(params.doc.x, "number"), y: Cast(params.doc.y, "number"), z: Cast(params.doc.z, "number"), width: Cast(layoutDoc.width, "number"), height: Cast(layoutDoc.height, "number") };
     }
 
@@ -653,7 +653,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         }
     }
 
-    childDataProvider = computedFn(function childDataProvider(doc: Doc) { return (this as any)._layoutPoolData.get(doc[Id]); });
+    childDataProvider = computedFn(function childDataProvider(doc: Doc) { return (this as any)._layoutPoolData.get(doc[Id]); }.bind(this));
 
     doPivotLayout(poolData: ObservableMap<string, any>) {
         return computePivotLayout(poolData, this.props.Document, this.childDocs,
@@ -667,9 +667,9 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         let elements = initResult && initResult.success ? this.viewDefsToJSX(initResult.result.views) : [];
 
         this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map((pair, i) => {
+            const data = poolData.get(pair.layout[Id]);
             const pos = this.getCalculatedPositions({ doc: pair.layout, index: i, collection: this.Document, docs: layoutDocs, state });
             state = pos.state === undefined ? state : pos.state;
-            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(() => poolData.set(pair.layout[Id], pos));
             }
@@ -695,7 +695,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
     }
 
     componentDidMount() {
-        this._layoutComputeReaction = reaction(() => { trace(); return this.doLayoutComputation },
+        this._layoutComputeReaction = reaction(() => { TraceMobx(); return this.doLayoutComputation },
             action((computation: { elements: ViewDefResult[] }) => computation && (this._layoutElements = computation.elements)),
             { fireImmediately: true, name: "doLayout" });
     }
@@ -845,7 +845,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         return eles;
     }
     render() {
-        trace();
+        TraceMobx();
         // 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;
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index 0badbd3fe..c85b59488 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -10,6 +10,7 @@ import "./CollectionFreeFormDocumentView.scss";
 import { DocumentView, DocumentViewProps } from "./DocumentView";
 import React = require("react");
 import { PositionDocument } from "../../../new_fields/documentSchemas";
+import { TraceMobx } from "../../../new_fields/util";
 
 export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps {
     dataProvider?: (doc: Doc) => { x: number, y: number, width: number, height: number, z: number, transition?: string } | undefined;
@@ -56,11 +57,9 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
         this._disposer && this._disposer();
     }
     componentDidMount() {
-        this._disposer = reaction(() => [this.props.Document.animateToPos, this.props.Document.isAnimating],
-            () => {
-                const target = this.props.Document.animateToPos ? Array.from(Cast(this.props.Document.animateToPos, listSpec("number"))!) : undefined;
-                this._animPos = !target ? undefined : target[2] ? [NumCast(this.layoutDoc.x), NumCast(this.layoutDoc.y)] : this.props.ScreenToLocalTransform().transformPoint(target[0], target[1]);
-            }, { fireImmediately: true });
+        this._disposer = reaction(() => this.props.Document.animateToPos ? Array.from(Cast(this.props.Document.animateToPos, listSpec("number"))!) : undefined,
+            target => this._animPos = !target ? undefined : target[2] ? [NumCast(this.layoutDoc.x), NumCast(this.layoutDoc.y)] : this.props.ScreenToLocalTransform().transformPoint(target[0], target[1]),
+            { fireImmediately: true });
     }
 
     contentScaling = () => this.nativeWidth > 0 && !this.props.Document.ignoreAspect ? this.width / this.nativeWidth : 1;
@@ -88,7 +87,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
     finalPanelHeight = () => this.dataProvider ? this.dataProvider.height : this.panelHeight();
 
     render() {
-        trace();
+        TraceMobx();
         return <div className="collectionFreeFormDocumentView-container"
             style={{
                 boxShadow:
@@ -99,7 +98,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
                                     StrCast(this.layoutDoc.boxShadow, ""),
                 borderRadius: this.borderRounding(),
                 transform: this.transform,
-                transition: this.Document.isAnimating !== undefined ? ".5s ease-in" : this.props.transition ? this.props.transition : this.dataProvider ? this.dataProvider.transition : StrCast(this.layoutDoc.transition),
+                transition: this.Document.isAnimating ? ".5s ease-in" : this.props.transition ? this.props.transition : this.dataProvider ? this.dataProvider.transition : StrCast(this.layoutDoc.transition),
                 width: this.width,
                 height: this.height,
                 zIndex: this.Document.zIndex || 0,
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index a985f3069..9ec339686 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -43,6 +43,7 @@ import React = require("react");
 import { InteractionUtils } from '../../util/InteractionUtils';
 import { InkingControl } from '../InkingControl';
 import { InkTool } from '../../../new_fields/InkField';
+import { TraceMobx } from '../../../new_fields/util';
 
 library.add(fa.faEdit, fa.faTrash, fa.faShare, fa.faDownload, fa.faExpandArrowsAlt, fa.faCompressArrowsAlt, fa.faLayerGroup, fa.faExternalLinkAlt, fa.faAlignCenter, fa.faCaretSquareRight,
     fa.faSquare, fa.faConciergeBell, fa.faWindowRestore, fa.faFolder, fa.faMapPin, fa.faLink, fa.faFingerprint, fa.faCrosshairs, fa.faDesktop, fa.faUnlock, fa.faLock, fa.faLaptopCode, fa.faMale,
@@ -542,7 +543,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     @computed get finalLayoutKey() { return this.props.layoutKey || "layout"; }
     childScaling = () => (this.layoutDoc.fitWidth ? this.props.PanelWidth() / this.nativeWidth : this.props.ContentScaling());
     @computed get contents() {
-        trace();
+        TraceMobx();
         return (<DocumentContentsView ContainingCollectionView={this.props.ContainingCollectionView}
             ContainingCollectionDoc={this.props.ContainingCollectionDoc}
             Document={this.props.Document}
@@ -586,7 +587,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     }
 
     @computed get innards() {
-        trace();
+        TraceMobx();
         const showOverlays = this.props.showOverlays ? this.props.showOverlays(this.Document) : undefined;
         const showTitle = showOverlays && "title" in showOverlays ? showOverlays.title : this.getLayoutPropStr("showTitle");
         const showCaption = showOverlays && "caption" in showOverlays ? showOverlays.caption : this.getLayoutPropStr("showCaption");
@@ -669,7 +670,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
             onDrop={this.onDrop} onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} onClick={this.onClick}
             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),
+                transition: this.Document.isAnimating ? ".5s linear" : StrCast(this.Document.transition),
                 pointerEvents: this.ignorePointerEvents ? "none" : "all",
                 color: StrCast(this.Document.color),
                 outline: highlighting && !borderRounding ? `${highlightColors[fullDegree]} ${highlightStyles[fullDegree]} ${localScale}px` : "solid 0px",
diff --git a/src/client/views/nodes/FormattedTextBox.scss b/src/client/views/nodes/FormattedTextBox.scss
index 269a3ca68..77cdd3d42 100644
--- a/src/client/views/nodes/FormattedTextBox.scss
+++ b/src/client/views/nodes/FormattedTextBox.scss
@@ -27,6 +27,8 @@
     pointer-events: all;
     overflow-y: auto;
     max-height: 100%;
+    display: flex;
+    flex-direction: row;
 
     .formattedTextBox-dictation {
         height: 20px;
@@ -48,10 +50,28 @@
     width: 100%;
     height: 100%;
 }
-.formattedTextBox-sidebar,.formattedTextBox-sidebar-inking {
-    border-left: solid 1px black; 
+.formattedTextBox-sidebar-handle {
+    position: absolute;
+    top: calc(50% - 17.5px);
+    width: 10px;
+    height: 35px;
+    background: lightgray;
+    border-radius: 20px;
+}
+.formattedTextBox-cont > .formattedTextBox-sidebar-handle {
+    right: 0;
+    left: unset;
+}
+.formattedTextBox-sidebar, .formattedTextBox-sidebar-inking {
+    border-left: dashed 1px black; 
     height: 100%; 
     display: inline-block;
+    position: absolute;
+    right: 0;
+    > .formattedTextBox-sidebar-handle {
+        right:unset;
+        left:-5;
+    }
 }
 
 .formattedTextBox-sidebar-inking {
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 5201f9bdc..86a32eb22 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -47,6 +47,7 @@ import { documentSchema } from '../../../new_fields/documentSchemas';
 import { AudioBox } from './AudioBox';
 import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView';
 import { InkTool } from '../../../new_fields/InkField';
+import { TraceMobx } from '../../../new_fields/util';
 
 library.add(faEdit);
 library.add(faSmile, faTextHeight, faUpload);
@@ -358,9 +359,11 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
         }
     }
 
+    toggleSidebar = () => this.props.Document.sidebarWidthPercent = StrCast(this.props.Document.sidebarWidthPercent, "0%") === "0%" ? "25%" : "0%";
+
     specificContextMenu = (e: React.MouseEvent): void => {
         let funcs: ContextMenuProps[] = [];
-        funcs.push({ description: "Toggle Sidebar", event: () => { e.stopPropagation(); this.props.Document.sidebarWidthPercent = StrCast(this.props.Document.sidebarWidthPercent, "0%") === "0%" ? "25%" : "0%"; }, icon: "expand-arrows-alt" });
+        funcs.push({ description: "Toggle Sidebar", event: () => { e.stopPropagation(); this.toggleSidebar() }, icon: "expand-arrows-alt" });
         funcs.push({ description: "Record Bullet", event: () => { e.stopPropagation(); this.recordBullet(); }, icon: "expand-arrows-alt" });
         ["My Text", "Text from Others", "Todo Items", "Important Items", "Ignore Items", "Disagree Items", "By Recent Minute", "By Recent Hour"].forEach(option =>
             funcs.push({
@@ -997,13 +1000,16 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
         if (!this._undoTyping) {
             this._undoTyping = UndoManager.StartBatch("undoTyping");
         }
-        if (this._recording) { this.stopDictation(true); setTimeout(() => this.recordDictation(), 250); }
+        if (this._recording) {
+            this.stopDictation(true);
+            setTimeout(() => this.recordDictation(), 250);
+        }
     }
 
     @action
     tryUpdateHeight() {
-        let scrollHeight = this._ref.current ? this._ref.current.scrollHeight : 0;
-        if (!this.layoutDoc.isAnimating && this.layoutDoc.autoHeight && scrollHeight !== 0 &&
+        const scrollHeight = this._ref.current?.scrollHeight;
+        if (!this.layoutDoc.animateToPos && this.layoutDoc.autoHeight && scrollHeight &&
             getComputedStyle(this._ref.current!.parentElement!).top === "0px") {  // if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation
             let nh = this.Document.isTemplateField ? 0 : NumCast(this.dataDoc.nativeHeight, 0);
             let dh = NumCast(this.layoutDoc.height, 0);
@@ -1016,7 +1022,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
     @computed get sidebarWidth() { return Number(this.sidebarWidthPercent.substring(0, this.sidebarWidthPercent.length - 1)) / 100 * this.props.PanelWidth(); }
     @computed get annotationsKey() { return "annotations"; }
     render() {
-        trace();
+        TraceMobx();
         let rounded = StrCast(this.layoutDoc.borderRounding) === "100%" ? "-rounded" : "";
         let interactive = InkingControl.Instance.selectedTool || this.layoutDoc.isBackground;
         if (this.props.isSelected()) {
@@ -1050,29 +1056,33 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
                 <div className={`formattedTextBox-outer`} style={{ width: `calc(100% - ${this.sidebarWidthPercent})`, }}>
                     <div className={`formattedTextBox-inner${rounded}`} style={{ whiteSpace: "pre-wrap", pointerEvents: ((this.Document.isButton || this.props.onClick) && !this.props.isSelected()) ? "none" : undefined }} ref={this.createDropTarget} />
                 </div>
-                {this.sidebarWidthPercent === "0%" ? (null) : <div className={"formattedTextBox-sidebar" + (InkingControl.Instance.selectedTool !== InkTool.None ? "-inking" : "")} style={{ width: `${this.sidebarWidthPercent}` }}>
-                    <CollectionFreeFormView {...this.props}
-                        PanelHeight={() => this.props.PanelHeight()}
-                        PanelWidth={() => this.sidebarWidth}
-                        annotationsKey={this.annotationsKey}
-                        isAnnotationOverlay={true}
-                        focus={this.props.focus}
-                        isSelected={this.props.isSelected}
-                        select={emptyFunction}
-                        active={this.annotationsActive}
-                        ContentScaling={returnOne}
-                        whenActiveChanged={this.whenActiveChanged}
-                        removeDocument={this.removeDocument}
-                        moveDocument={this.moveDocument}
-                        addDocument={this.addDocument}
-                        CollectionView={undefined}
-                        ScreenToLocalTransform={() => this.props.ScreenToLocalTransform().translate(-(this.props.PanelWidth() - this.sidebarWidth), 0)}
-                        ruleProvider={undefined}
-                        renderDepth={this.props.renderDepth + 1}
-                        ContainingCollectionDoc={this.props.ContainingCollectionDoc}
-                        chromeCollapsed={true}>
-                    </CollectionFreeFormView>
-                </div>}
+                {this.sidebarWidthPercent === "0%" ?
+                    <div className="formattedTextBox-sidebar-handle" onPointerDown={e => e.stopPropagation()} onClick={e => this.toggleSidebar()} /> :
+                    <div className={"formattedTextBox-sidebar" + (InkingControl.Instance.selectedTool !== InkTool.None ? "-inking" : "")}
+                        style={{ width: `${this.sidebarWidthPercent}`, backgroundColor: `${StrCast(this.extensionDoc?.backgroundColor, "transparent")}` }}>
+                        <CollectionFreeFormView {...this.props}
+                            PanelHeight={() => this.props.PanelHeight()}
+                            PanelWidth={() => this.sidebarWidth}
+                            annotationsKey={this.annotationsKey}
+                            isAnnotationOverlay={true}
+                            focus={this.props.focus}
+                            isSelected={this.props.isSelected}
+                            select={emptyFunction}
+                            active={this.annotationsActive}
+                            ContentScaling={returnOne}
+                            whenActiveChanged={this.whenActiveChanged}
+                            removeDocument={this.removeDocument}
+                            moveDocument={this.moveDocument}
+                            addDocument={this.addDocument}
+                            CollectionView={undefined}
+                            ScreenToLocalTransform={() => this.props.ScreenToLocalTransform().translate(-(this.props.PanelWidth() - this.sidebarWidth), 0)}
+                            ruleProvider={undefined}
+                            renderDepth={this.props.renderDepth + 1}
+                            ContainingCollectionDoc={this.props.ContainingCollectionDoc}
+                            chromeCollapsed={true}>
+                        </CollectionFreeFormView>
+                        <div className="formattedTextBox-sidebar-handle" onPointerDown={e => e.stopPropagation()} onClick={e => this.toggleSidebar()} />
+                    </div>}
                 <div className="formattedTextBox-dictation"
                     onClick={e => {
                         this._recording ? this.stopDictation(true) : this.recordDictation();
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 5d40e274f..d102c9600 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -27,6 +27,7 @@ import React = require("react");
 import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView';
 import { documentSchema } from '../../../new_fields/documentSchemas';
 import { Id } from '../../../new_fields/FieldSymbols';
+import { TraceMobx } from '../../../new_fields/util';
 var requestImageSize = require('../../util/request-image-size');
 var path = require('path');
 const { Howl } = require('howler');
@@ -268,7 +269,7 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
     }
 
     @computed get content() {
-        trace();
+        TraceMobx();
         const extensionDoc = this.extensionDoc;
         if (!extensionDoc) return (null);
         // let transform = this.props.ScreenToLocalTransform().inverse();
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 22c3a29a8..913aaa087 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -29,6 +29,7 @@ import { documentSchema } from "../../../new_fields/documentSchemas";
 import { DocumentDecorations } from "../DocumentDecorations";
 import { InkingControl } from "../InkingControl";
 import { InkTool } from "../../../new_fields/InkField";
+import { TraceMobx } from "../../../new_fields/util";
 const PDFJSViewer = require("pdfjs-dist/web/pdf_viewer");
 const pdfjsLib = require("pdfjs-dist");
 
@@ -620,7 +621,7 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
     }
 
     @computed get annotationLayer() {
-        trace();
+        TraceMobx();
         return <div className="pdfViewer-annotationLayer" style={{ height: (this.Document.nativeHeight || 0), transform: `scale(${this._zoomed})` }} ref={this._annotationLayer}>
             {this.nonDocAnnotations.sort((a, b) => NumCast(a.y) - NumCast(b.y)).map((anno, index) =>
                 <Annotation {...this.props} focus={this.props.focus} extensionDoc={this.extensionDoc!} anno={anno} key={`${anno[Id]}-annotation`} />)}
@@ -674,7 +675,7 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
     contentZoom = () => this._zoomed;
     @computed get contentScaling() { return this.props.ContentScaling() }
     render() {
-        trace();
+        TraceMobx();
         return !this.extensionDoc ? (null) :
             <div className={"pdfViewer-viewer" + (this._zoomed !== 1 ? "-zoomed" : "")} ref={this._mainCont}
                 style={{
diff --git a/src/new_fields/documentSchemas.ts b/src/new_fields/documentSchemas.ts
index 7592cdaa3..4c2f061a6 100644
--- a/src/new_fields/documentSchemas.ts
+++ b/src/new_fields/documentSchemas.ts
@@ -41,7 +41,7 @@ export const documentSchema = createSchema({
     showTitle: "string",        // whether an editable title banner is displayed at tht top of the document
     isButton: "boolean",        // whether document functions as a button (overiding native interactions of its content)    
     ignoreClick: "boolean",     // whether documents ignores input clicks (but does not ignore manipulation and other events) 
-    isAnimating: "boolean",     // whether the document is in the midst of animating between two layouts (used by icons to de/iconify documents).
+    isAnimating: "string",      // whether the document is in the midst of animating between two layouts (used by icons to de/iconify documents).  value is undefined|"min"|"max"
     animateToDimensions: listSpec("number"), // layout information about the target rectangle a document is animating towards 
     scrollToLinkID: "string",   // id of link being traversed. allows this doc to scroll/highlight/etc its link anchor. scrollToLinkID should be set to undefined by this doc after it sets up its scroll,etc.
     strokeWidth: "number",
diff --git a/src/new_fields/util.ts b/src/new_fields/util.ts
index 04194509c..4147be278 100644
--- a/src/new_fields/util.ts
+++ b/src/new_fields/util.ts
@@ -4,7 +4,7 @@ import { SerializationHelper } from "../client/util/SerializationHelper";
 import { ProxyField } from "./Proxy";
 import { RefField } from "./RefField";
 import { ObjectField } from "./ObjectField";
-import { action } from "mobx";
+import { action, trace } from "mobx";
 import { Parent, OnUpdate, Update, Id, SelfProxy, Self } from "./FieldSymbols";
 import { DocServer } from "../client/DocServer";
 
@@ -12,6 +12,10 @@ function _readOnlySetter(): never {
     throw new Error("Documents can't be modified in read-only mode");
 }
 
+export function TraceMobx() {
+    //trace();
+}
+
 export interface GetterResult {
     value: FieldResult;
     shouldReturn?: boolean;
-- 
cgit v1.2.3-70-g09d2


From 3b37cc31bb09b11238868c34a38a8e99f508479f Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Fri, 22 Nov 2019 13:22:21 -0500
Subject: fixed link anchors to move to be nearest alternate anchor.

---
 src/Utils.ts                                       | 57 ++++++++++++++++++++++
 src/client/views/Touchable.tsx                     |  2 +-
 .../views/collections/ParentDocumentSelector.tsx   |  1 +
 .../CollectionFreeFormLinkView.tsx                 | 55 +++++++++++++++------
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  2 +-
 src/client/views/nodes/DocumentView.tsx            | 15 ++++--
 src/client/views/nodes/FontIconBox.tsx             |  2 +-
 src/client/views/nodes/FormattedTextBox.tsx        |  2 +-
 src/client/views/nodes/ImageBox.scss               |  1 +
 src/client/views/nodes/ImageBox.tsx                |  4 +-
 src/client/views/nodes/VideoBox.scss               |  1 +
 src/client/views/nodes/VideoBox.tsx                |  2 +-
 src/client/views/pdf/PDFViewer.tsx                 |  2 +-
 13 files changed, 119 insertions(+), 27 deletions(-)

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

diff --git a/src/Utils.ts b/src/Utils.ts
index 12acda4bb..37b509370 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -153,6 +153,63 @@ export namespace Utils {
         return Math.max(lower, Math.min(upper, n));
     }
 
+    export function distanceBetweenHorizontalLines(xs: number, xe: number, y: number, xs2: number, xe2: number, y2: number): [number, number[]] {
+        if ((xs2 < xs && xe2 > xs) || (xs2 < xe && xe2 > xe) || (xs2 > xs && xe2 < xe)) return [Math.abs(y - y2), [Math.max(xs, xs2), y, Math.min(xe, xe2), y]];
+        if (xe2 < xs) return [Math.sqrt((xe2 - xs) * (xe2 - xs) + (y2 - y) * (y2 - y)), [xs, y, xs, y]];
+        //if (xs2 > xe) 
+        return [Math.sqrt((xs2 - xe) * (xs2 - xe) + (y2 - y) * (y2 - y)), [xe, y, xe, y]];
+    }
+    export function distanceBetweenVerticalLines(x: number, ys: number, ye: number, x2: number, ys2: number, ye2: number): [number, number[]] {
+        if ((ys2 < ys && ye2 > ys) || (ys2 < ye && ye2 > ye) || (ys2 > ys && ye2 < ye)) return [Math.abs(x - x2), [x, Math.max(ys, ys2), x, Math.min(ye, ye2)]];
+        if (ye2 < ys) return [Math.sqrt((ye2 - ys) * (ye2 - ys) + (x2 - x) * (x2 - x)), [x, ys, x, ys]];
+        //if (ys2 > ye) 
+        return [Math.sqrt((ys2 - ye) * (ys2 - ye) + (x2 - x) * (x2 - x)), [x, ye, x, ye]];
+    }
+
+    function project(px: number, py: number, ax: number, ay: number, bx: number, by: number) {
+
+        if (ax === bx && ay === by) return { point: { x: ax, y: ay }, left: false, dot: 0, t: 0 };
+        var atob = { x: bx - ax, y: by - ay };
+        var atop = { x: px - ax, y: py - ay };
+        var len = atob.x * atob.x + atob.y * atob.y;
+        var dot = atop.x * atob.x + atop.y * atob.y;
+        var t = Math.min(1, Math.max(0, dot / len));
+
+        dot = (bx - ax) * (py - ay) - (by - ay) * (px - ax);
+
+        return {
+            point: {
+                x: ax + atob.x * t,
+                y: ay + atob.y * t
+            },
+            left: dot < 1,
+            dot: dot,
+            t: t
+        };
+    }
+
+    export function closestPtBetweenRectangles(l: number, t: number, w: number, h: number,
+        l1: number, t1: number, w1: number, h1: number,
+        x: number, y: number) {
+        var r = l + w,
+            b = t + h;
+        var r1 = l1 + w1,
+            b1 = t1 + h1;
+        let hsegs = [[l, r, t, l1, r1, t1], [l, r, b, l1, r1, t1], [l, r, t, l1, r1, b1], [l, r, b, l1, r1, b1]];
+        let vsegs = [[l, t, b, l1, t1, b1], [r, t, b, l1, t1, b1], [l, t, b, r1, t1, b1], [r, t, b, r1, t1, b1]];
+        let res = hsegs.reduce((closest, seg) => {
+            let res = distanceBetweenHorizontalLines(seg[0], seg[1], seg[2], seg[3], seg[4], seg[5]);
+            return (res[0] < closest[0]) ? res : closest;
+        }, [Number.MAX_VALUE, []] as [number, number[]]);
+        let fres = vsegs.reduce((closest, seg) => {
+            let res = distanceBetweenVerticalLines(seg[0], seg[1], seg[2], seg[3], seg[4], seg[5]);
+            return (res[0] < closest[0]) ? res : closest;
+        }, res);
+
+        let near = project(x, y, fres[1][0], fres[1][1], fres[1][2], fres[1][3]);
+        return project(near.point.x, near.point.y, fres[1][0], fres[1][1], fres[1][2], fres[1][3]);
+    }
+
     export function getNearestPointInPerimeter(l: number, t: number, w: number, h: number, x: number, y: number) {
         var r = l + w,
             b = t + h;
diff --git a/src/client/views/Touchable.tsx b/src/client/views/Touchable.tsx
index ba87025c4..0dd4f734c 100644
--- a/src/client/views/Touchable.tsx
+++ b/src/client/views/Touchable.tsx
@@ -45,7 +45,7 @@ export abstract class Touchable<T = {}> extends React.Component<T> {
         this._touchDrag = true;
         switch (e.targetTouches.length) {
             case 1:
-                this.handle1PointerMove(e)
+                this.handle1PointerMove(e);
                 break;
             case 2:
                 this.handle2PointersMove(e);
diff --git a/src/client/views/collections/ParentDocumentSelector.tsx b/src/client/views/collections/ParentDocumentSelector.tsx
index ba83630a4..4eb9e9d1e 100644
--- a/src/client/views/collections/ParentDocumentSelector.tsx
+++ b/src/client/views/collections/ParentDocumentSelector.tsx
@@ -14,6 +14,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
 import { faEdit } from "@fortawesome/free-solid-svg-icons";
 import { library } from "@fortawesome/fontawesome-svg-core";
 import { MetadataEntryMenu } from "../MetadataEntryMenu";
+import { DocumentView } from "../nodes/DocumentView";
 const higflyout = require("@hig/flyout");
 export const { anchorPoints } = higflyout;
 export const Flyout = higflyout.default;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
index 837413842..73b45edc6 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
@@ -6,7 +6,8 @@ import "./CollectionFreeFormLinkView.scss";
 import React = require("react");
 import v5 = require("uuid/v5");
 import { DocumentType } from "../../../documents/DocumentTypes";
-import { observable, action } from "mobx";
+import { observable, action, reaction, IReactionDisposer } from "mobx";
+import { StrCast, Cast } from "../../../../new_fields/Types";
 
 export interface CollectionFreeFormLinkViewProps {
     A: DocumentView;
@@ -16,33 +17,57 @@ export interface CollectionFreeFormLinkViewProps {
 
 @observer
 export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFormLinkViewProps> {
-    @observable _alive: number = 0;
     @observable _opacity: number = 1;
+    @observable _update: number = 0;
+    _anchorDisposer: IReactionDisposer | undefined;
     @action
     componentDidMount() {
-        this._alive = 1;
-        setTimeout(this.rerender, 50);
-        setTimeout(action(() => this._opacity = 0.05), 50);
+        setTimeout(action(() => this._opacity = 0.05), 750);
+        this._anchorDisposer = reaction(() => [this.props.A.props.ScreenToLocalTransform(), this.props.B.props.ScreenToLocalTransform()],
+            () => {
+                let acont = this.props.A.props.Document.type === DocumentType.LINK ? this.props.A.ContentDiv!.getElementsByClassName("docuLinkBox-cont") : [];
+                let bcont = this.props.B.props.Document.type === DocumentType.LINK ? this.props.B.ContentDiv!.getElementsByClassName("docuLinkBox-cont") : [];
+                let adiv = (acont.length ? acont[0] : this.props.A.ContentDiv!);
+                let bdiv = (bcont.length ? bcont[0] : this.props.B.ContentDiv!);
+                let a = adiv.getBoundingClientRect();
+                let b = bdiv.getBoundingClientRect();
+                let abounds = adiv.parentElement!.getBoundingClientRect();
+                let bbounds = bdiv.parentElement!.getBoundingClientRect();
+                let apt = Utils.closestPtBetweenRectangles(abounds.left, abounds.top, abounds.width, abounds.height,
+                    bbounds.left, bbounds.top, bbounds.width, bbounds.height,
+                    a.left + a.width / 2, a.top + a.height / 2);
+                let bpt = Utils.closestPtBetweenRectangles(bbounds.left, bbounds.top, bbounds.width, bbounds.height,
+                    abounds.left, abounds.top, abounds.width, abounds.height,
+                    apt.point.x, apt.point.y);
+                let afield = StrCast(this.props.A.props.Document[StrCast(this.props.A.props.layoutKey, "layout")]).indexOf("anchor1") === -1 ? "anchor2" : "anchor1";
+                let bfield = afield === "anchor1" ? "anchor2" : "anchor1";
+                this.props.A.props.Document[afield + "_x"] = (apt.point.x - abounds.left) / abounds.width * 100;
+                this.props.A.props.Document[afield + "_y"] = (apt.point.y - abounds.top) / abounds.height * 100;
+                this.props.A.props.Document[bfield + "_x"] = (bpt.point.x - bbounds.left) / bbounds.width * 100;
+                this.props.A.props.Document[bfield + "_y"] = (bpt.point.y - bbounds.top) / bbounds.height * 100;
+                this._update++;
+            }
+            , { fireImmediately: true });
     }
     @action
     componentWillUnmount() {
-        this._alive = 0;
+        this._anchorDisposer?.();
     }
-    rerender = action(() => {
-        if (this._alive) {
-            setTimeout(this.rerender, 50);
-            this._alive++;
-        }
-    });
 
     render() {
-        let y = this._alive;
+        let y = this._update;
         let acont = this.props.A.props.Document.type === DocumentType.LINK ? this.props.A.ContentDiv!.getElementsByClassName("docuLinkBox-cont") : [];
         let bcont = this.props.B.props.Document.type === DocumentType.LINK ? this.props.B.ContentDiv!.getElementsByClassName("docuLinkBox-cont") : [];
         let a = (acont.length ? acont[0] : this.props.A.ContentDiv!).getBoundingClientRect();
         let b = (bcont.length ? bcont[0] : this.props.B.ContentDiv!).getBoundingClientRect();
-        let pt1 = Utils.getNearestPointInPerimeter(a.left, a.top, a.width, a.height, b.left + b.width / 2, b.top + b.height / 2);
-        let pt2 = Utils.getNearestPointInPerimeter(b.left, b.top, b.width, b.height, a.left + a.width / 2, a.top + a.height / 2);
+        let apt = Utils.closestPtBetweenRectangles(a.left, a.top, a.width, a.height,
+            b.left, b.top, b.width, b.height,
+            a.left + a.width / 2, a.top + a.height / 2);
+        let bpt = Utils.closestPtBetweenRectangles(b.left, b.top, b.width, b.height,
+            a.left, a.top, a.width, a.height,
+            apt.point.x, apt.point.y);
+        let pt1 = [apt.point.x, apt.point.y];
+        let pt2 = [bpt.point.x, bpt.point.y];
         return (<line key="linkLine" className="collectionfreeformlinkview-linkLine"
             style={{ opacity: this._opacity }}
             x1={`${pt1[0]}`} y1={`${pt1[1]}`}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index b2344771d..9bbaa20e7 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -695,7 +695,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
     }
 
     componentDidMount() {
-        this._layoutComputeReaction = reaction(() => { TraceMobx(); return this.doLayoutComputation },
+        this._layoutComputeReaction = reaction(() => { TraceMobx(); return this.doLayoutComputation; },
             action((computation: { elements: ViewDefResult[] }) => computation && (this._layoutElements = computation.elements)),
             { fireImmediately: true, name: "doLayout" });
     }
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 9ec339686..467fd4b32 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -617,9 +617,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
                 />
             </div>);
         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} ContentScaling={returnOne} Document={d} layoutKey={this.linkEndpoint(d)} backgroundColor={returnTransparent} removeDocument={undoBatch(doc => Doc.AddDocToList(this.layoutDoc, "hiddenLinks", doc))} />
+            {this.Document.links && DocListCast(this.Document.links).filter(d => !d.hidden).filter(this.isNonTemporalLink).map((d, i) =>
+                <div className="documentView-docuLinkWrapper" key={`${d[Id]}`}>
+                    <DocumentView {...this.props} ContentScaling={returnOne} Document={d} layoutKey={this.linkEndpoint(d)} backgroundColor={returnTransparent} removeDocument={undoBatch(doc => doc.hidden = true)} />
                 </div>)}
             {!showTitle && !showCaption ?
                 this.Document.searchFields ?
@@ -668,7 +668,14 @@ 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={e => Doc.BrushDoc(this.props.Document)} onPointerLeave={e => Doc.UnBrushDoc(this.props.Document)}
+            onPointerEnter={e => {
+                console.log("Brush" + this.props.Document.title);
+                Doc.BrushDoc(this.props.Document);
+            }} onPointerLeave={e => {
+                console.log("UnBrush" + this.props.Document.title);
+                Doc.UnBrushDoc(this.props.Document);
+
+            }}
             style={{
                 transition: this.Document.isAnimating ? ".5s linear" : StrCast(this.Document.transition),
                 pointerEvents: this.ignorePointerEvents ? "none" : "all",
diff --git a/src/client/views/nodes/FontIconBox.tsx b/src/client/views/nodes/FontIconBox.tsx
index 9a5de836f..960b55e3e 100644
--- a/src/client/views/nodes/FontIconBox.tsx
+++ b/src/client/views/nodes/FontIconBox.tsx
@@ -43,7 +43,7 @@ export class FontIconBox extends DocComponent<FieldViewProps, FontIconDocument>(
                 background: StrCast(referenceLayout.backgroundColor),
                 boxShadow: this.props.Document.ischecked ? `4px 4px 12px black` : undefined
             }}>
-            <FontAwesomeIcon className="fontIconBox-icon" icon={this.Document.icon as any} color={this._foregroundColor} size="md" />
+            <FontAwesomeIcon className="fontIconBox-icon" icon={this.Document.icon as any} color={this._foregroundColor} size="sm" />
         </button>;
     }
 }
\ No newline at end of file
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 86a32eb22..9910c9ecd 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -363,7 +363,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
 
     specificContextMenu = (e: React.MouseEvent): void => {
         let funcs: ContextMenuProps[] = [];
-        funcs.push({ description: "Toggle Sidebar", event: () => { e.stopPropagation(); this.toggleSidebar() }, icon: "expand-arrows-alt" });
+        funcs.push({ description: "Toggle Sidebar", event: () => { e.stopPropagation(); this.toggleSidebar(); }, icon: "expand-arrows-alt" });
         funcs.push({ description: "Record Bullet", event: () => { e.stopPropagation(); this.recordBullet(); }, icon: "expand-arrows-alt" });
         ["My Text", "Text from Others", "Todo Items", "Important Items", "Ignore Items", "Disagree Items", "By Recent Minute", "By Recent Hour"].forEach(option =>
             funcs.push({
diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss
index dcecbdc6e..ba4ef8879 100644
--- a/src/client/views/nodes/ImageBox.scss
+++ b/src/client/views/nodes/ImageBox.scss
@@ -16,6 +16,7 @@
     width:100%;
     height:100%;
     position: absolute;
+    transform-origin: top left;
 }
 
 .imageBox-cont-interactive {
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index d102c9600..f21ce3bf2 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -292,7 +292,7 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
         if (field instanceof ImageField) paths = [this.choosePath(field.url)];
         paths.push(...altpaths);
         // }
-        let interactive = InkingControl.Instance.selectedTool || this.Document.isBackground ? "" : "-interactive";
+        let interactive = InkingControl.Instance.selectedTool || !this.props.isSelected() ? "" : "-interactive";
         let rotation = NumCast(this.Document.rotation, 0);
         let aspect = (rotation % 180) ? this.Document[HeightSym]() / this.Document[WidthSym]() : 1;
         let shift = (rotation % 180) ? (nativeHeight - nativeWidth / aspect) / 2 : 0;
@@ -335,7 +335,7 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
     contentFunc = () => [this.content];
     render() {
         return (<div className={"imageBox-container"} onContextMenu={this.specificContextMenu}
-            style={{ transformOrigin: "top left", transform: `scale(${this.props.ContentScaling()})`, width: `${100 / this.props.ContentScaling()}%`, height: `${100 / this.props.ContentScaling()}%` }} >
+            style={{ transform: `scale(${this.props.ContentScaling()})`, width: `${100 / this.props.ContentScaling()}%`, height: `${100 / this.props.ContentScaling()}%` }} >
             <CollectionFreeFormView {...this.props}
                 PanelHeight={this.props.PanelHeight}
                 PanelWidth={this.props.PanelWidth}
diff --git a/src/client/views/nodes/VideoBox.scss b/src/client/views/nodes/VideoBox.scss
index 5829c1bd9..0a4c650a8 100644
--- a/src/client/views/nodes/VideoBox.scss
+++ b/src/client/views/nodes/VideoBox.scss
@@ -1,5 +1,6 @@
 .videoBox-container {
     pointer-events: all;
+    transform-origin: top left;
     .inkingCanvas-paths-markers {
         opacity : 0.4; // we shouldn't have to do this, but since chrome crawls to a halt with z-index unset in videoBox-content, this is a workaround
     }
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index d70f0474c..bd5bd918f 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -336,7 +336,7 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum
     contentFunc = () => [this.youtubeVideoId ? this.youtubeContent : this.content];
     render() {
         return (<div className={"videoBox-container"} onContextMenu={this.specificContextMenu}
-            style={{ transformOrigin: "top left", transform: `scale(${this.props.ContentScaling()})`, width: `${100 / this.props.ContentScaling()}%`, height: `${100 / this.props.ContentScaling()}%` }} >
+            style={{ transform: `scale(${this.props.ContentScaling()})`, width: `${100 / this.props.ContentScaling()}%`, height: `${100 / this.props.ContentScaling()}%` }} >
             <CollectionFreeFormView {...this.props}
                 PanelHeight={this.props.PanelHeight}
                 PanelWidth={this.props.PanelWidth}
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 913aaa087..f1c500391 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -673,7 +673,7 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
     marqueeing = () => this._marqueeing;
     visibleHeight = () => this.props.PanelHeight() / this.props.ContentScaling() * 72 / 96;
     contentZoom = () => this._zoomed;
-    @computed get contentScaling() { return this.props.ContentScaling() }
+    @computed get contentScaling() { return this.props.ContentScaling(); }
     render() {
         TraceMobx();
         return !this.extensionDoc ? (null) :
-- 
cgit v1.2.3-70-g09d2

