From 05fe7bb89e51f3d8b643726f3283ba8adee740e4 Mon Sep 17 00:00:00 2001
From: Melissa Zhang <mzhang19096@gmail.com>
Date: Thu, 4 Jun 2020 16:22:27 -0700
Subject: hover to show autoscroll button

---
 src/client/views/DocumentDecorations.tsx | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

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

diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 04f02c683..7cdf955a1 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -1,5 +1,5 @@
 import { IconProp, library } from '@fortawesome/fontawesome-svg-core';
-import { faCaretUp, faFilePdf, faFilm, faImage, faObjectGroup, faStickyNote, faTextHeight, faArrowAltCircleDown, faArrowAltCircleUp, faCheckCircle, faCloudUploadAlt, faLink, faShare, faStopCircle, faSyncAlt, faTag, faTimes } from '@fortawesome/free-solid-svg-icons';
+import { faCaretUp, faFilePdf, faFilm, faImage, faObjectGroup, faStickyNote, faTextHeight, faArrowAltCircleDown, faArrowAltCircleUp, faCheckCircle, faCloudUploadAlt, faLink, faShare, faStopCircle, faSyncAlt, faTag, faTimes, faAngleLeft, faAngleRight, faAngleDoubleLeft, faAngleDoubleRight, faPause } from '@fortawesome/free-solid-svg-icons';
 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
 import { action, computed, observable, reaction, runInAction } from "mobx";
 import { observer } from "mobx-react";
@@ -38,6 +38,11 @@ library.add(faCheckCircle);
 library.add(faCloudUploadAlt);
 library.add(faSyncAlt);
 library.add(faShare);
+library.add(faAngleDoubleLeft);
+library.add(faAngleDoubleRight);
+library.add(faAngleLeft);
+library.add(faAngleRight);
+library.add(faPause);
 
 @observer
 export class DocumentDecorations extends React.Component<{}, { value: string }> {
-- 
cgit v1.2.3-70-g09d2


From 639be55eaff422e1e982cf41d09edd9e96f014a4 Mon Sep 17 00:00:00 2001
From: yunahi <60233430+yunahi@users.noreply.github.com>
Date: Thu, 18 Jun 2020 14:10:34 +0900
Subject: updated menu

---
 package-lock.json                                  |  44 +++--
 src/client/documents/Documents.ts                  |  10 +-
 src/client/util/CurrentUserUtils.ts                |   4 +
 src/client/util/InteractionUtils.tsx               |  64 +++++--
 src/client/views/DocumentDecorations.scss          |  50 +++--
 src/client/views/DocumentDecorations.tsx           |  81 +++++++-
 src/client/views/GestureOverlay.tsx                |  21 ++-
 src/client/views/InkingStroke.tsx                  |  41 ++++-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |   4 +-
 .../collectionFreeForm/InkOptionsMenu.tsx          | 203 +++++++++++++++++++--
 src/fields/InkField.ts                             |   4 +-
 11 files changed, 459 insertions(+), 67 deletions(-)

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

diff --git a/package-lock.json b/package-lock.json
index c956fd3e9..cd80ecc5e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -2832,7 +2832,8 @@
                         },
                         "ansi-regex": {
                             "version": "2.1.1",
-                            "bundled": true
+                            "bundled": true,
+                            "optional": true
                         },
                         "aproba": {
                             "version": "1.2.0",
@@ -2850,11 +2851,13 @@
                         },
                         "balanced-match": {
                             "version": "1.0.0",
-                            "bundled": true
+                            "bundled": true,
+                            "optional": true
                         },
                         "brace-expansion": {
                             "version": "1.1.11",
                             "bundled": true,
+                            "optional": true,
                             "requires": {
                                 "balanced-match": "^1.0.0",
                                 "concat-map": "0.0.1"
@@ -2867,15 +2870,18 @@
                         },
                         "code-point-at": {
                             "version": "1.1.0",
-                            "bundled": true
+                            "bundled": true,
+                            "optional": true
                         },
                         "concat-map": {
                             "version": "0.0.1",
-                            "bundled": true
+                            "bundled": true,
+                            "optional": true
                         },
                         "console-control-strings": {
                             "version": "1.1.0",
-                            "bundled": true
+                            "bundled": true,
+                            "optional": true
                         },
                         "core-util-is": {
                             "version": "1.0.2",
@@ -2978,7 +2984,8 @@
                         },
                         "inherits": {
                             "version": "2.0.4",
-                            "bundled": true
+                            "bundled": true,
+                            "optional": true
                         },
                         "ini": {
                             "version": "1.3.5",
@@ -2988,6 +2995,7 @@
                         "is-fullwidth-code-point": {
                             "version": "1.0.0",
                             "bundled": true,
+                            "optional": true,
                             "requires": {
                                 "number-is-nan": "^1.0.0"
                             }
@@ -3000,17 +3008,20 @@
                         "minimatch": {
                             "version": "3.0.4",
                             "bundled": true,
+                            "optional": true,
                             "requires": {
                                 "brace-expansion": "^1.1.7"
                             }
                         },
                         "minimist": {
                             "version": "1.2.5",
-                            "bundled": true
+                            "bundled": true,
+                            "optional": true
                         },
                         "minipass": {
                             "version": "2.9.0",
                             "bundled": true,
+                            "optional": true,
                             "requires": {
                                 "safe-buffer": "^5.1.2",
                                 "yallist": "^3.0.0"
@@ -3027,6 +3038,7 @@
                         "mkdirp": {
                             "version": "0.5.3",
                             "bundled": true,
+                            "optional": true,
                             "requires": {
                                 "minimist": "^1.2.5"
                             }
@@ -3082,7 +3094,8 @@
                         },
                         "npm-normalize-package-bin": {
                             "version": "1.0.1",
-                            "bundled": true
+                            "bundled": true,
+                            "optional": true
                         },
                         "npm-packlist": {
                             "version": "1.4.8",
@@ -3107,7 +3120,8 @@
                         },
                         "number-is-nan": {
                             "version": "1.0.1",
-                            "bundled": true
+                            "bundled": true,
+                            "optional": true
                         },
                         "object-assign": {
                             "version": "4.1.1",
@@ -3117,6 +3131,7 @@
                         "once": {
                             "version": "1.4.0",
                             "bundled": true,
+                            "optional": true,
                             "requires": {
                                 "wrappy": "1"
                             }
@@ -3185,7 +3200,8 @@
                         },
                         "safe-buffer": {
                             "version": "5.1.2",
-                            "bundled": true
+                            "bundled": true,
+                            "optional": true
                         },
                         "safer-buffer": {
                             "version": "2.1.2",
@@ -3215,6 +3231,7 @@
                         "string-width": {
                             "version": "1.0.2",
                             "bundled": true,
+                            "optional": true,
                             "requires": {
                                 "code-point-at": "^1.0.0",
                                 "is-fullwidth-code-point": "^1.0.0",
@@ -3232,6 +3249,7 @@
                         "strip-ansi": {
                             "version": "3.0.1",
                             "bundled": true,
+                            "optional": true,
                             "requires": {
                                 "ansi-regex": "^2.0.0"
                             }
@@ -3270,11 +3288,13 @@
                         },
                         "wrappy": {
                             "version": "1.0.2",
-                            "bundled": true
+                            "bundled": true,
+                            "optional": true
                         },
                         "yallist": {
                             "version": "3.1.1",
-                            "bundled": true
+                            "bundled": true,
+                            "optional": true
                         }
                     }
                 }
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 8d867348f..cbf97e8e8 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -37,7 +37,7 @@ import { DashWebRTCVideo } from "../views/webcam/DashWebRTCVideo";
 import { QueryBox } from "../views/nodes/QueryBox";
 import { ColorBox } from "../views/nodes/ColorBox";
 import { DocHolderBox } from "../views/nodes/DocHolderBox";
-import { InkingStroke, ActiveInkColor, ActiveInkWidth, ActiveInkBezierApprox } from "../views/InkingStroke";
+import { InkingStroke, ActiveInkColor, ActiveInkWidth, ActiveInkBezierApprox, ActiveFillColor, ActiveArrowStart, ActiveArrowEnd, ActiveDash } from "../views/InkingStroke";
 import { InkField } from "../../fields/InkField";
 import { RichTextField } from "../../fields/RichTextField";
 import { extname } from "path";
@@ -631,13 +631,17 @@ export namespace Docs {
             return doc;
         }
 
-        export function InkDocument(color: string, tool: string, strokeWidth: string, strokeBezier: string, points: { X: number, Y: number }[], options: DocumentOptions = {}) {
+        export function InkDocument(color: string, tool: string, strokeWidth: string, strokeBezier: string, fillColor: string, arrowStart: string, arrowEnd: string, dash: string, points: { X: number, Y: number }[], options: DocumentOptions = {}) {
             const I = new Doc();
             I.type = DocumentType.INK;
             I.layout = InkingStroke.LayoutString("data");
             I.color = color;
             I.strokeWidth = strokeWidth;
             I.strokeBezier = strokeBezier;
+            I.fillColor = fillColor;
+            I.arrowStart = arrowStart;
+            I.arrowEnd = arrowEnd;
+            I.dash = dash;
             I.tool = tool;
             I.title = "ink";
             I.x = options.x;
@@ -852,7 +856,7 @@ export namespace DocUtils {
             created = Docs.Create.AudioDocument((field).url.href, resolved);
             layout = AudioBox.LayoutString;
         } else if (field instanceof InkField) {
-            created = Docs.Create.InkDocument(ActiveInkColor(), Doc.GetSelectedTool(), ActiveInkWidth(), ActiveInkBezierApprox(), (field).inkData, resolved);
+            created = Docs.Create.InkDocument(ActiveInkColor(), Doc.GetSelectedTool(), ActiveInkWidth(), ActiveInkBezierApprox(), ActiveFillColor(), ActiveArrowStart(), ActiveArrowEnd(), ActiveDash(), (field).inkData, resolved);
             layout = InkingStroke.LayoutString;
         } else if (field instanceof List && field[0] instanceof Doc) {
             created = Docs.Create.StackingDocument(DocListCast(field), resolved);
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index b0cea9947..3d0c0ef4a 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -691,6 +691,10 @@ export class CurrentUserUtils {
         doc.activeInkColor = StrCast(doc.activeInkColor, "rgb(0, 0, 0)");
         doc.activeInkWidth = StrCast(doc.activeInkWidth, "1");
         doc.activeInkBezier = StrCast(doc.activeInkBezier, "");
+        doc.activeFillColor = StrCast(doc.activeFillColor, "none");
+        doc.activeArrowStart = StrCast(doc.activeArrowStart, "none");
+        doc.activeArrowEnd = StrCast(doc.activeArrowEnd, "none");
+        doc.activeDash = StrCast(doc.activeDash, "0");
         doc.fontSize = NumCast(doc.fontSize, 12);
         doc["constants-snapThreshold"] = NumCast(doc["constants-snapThreshold"], 10); // 
         doc["constants-dragThreshold"] = NumCast(doc["constants-dragThreshold"], 4); // 
diff --git a/src/client/util/InteractionUtils.tsx b/src/client/util/InteractionUtils.tsx
index 81f9b9362..c6b96df45 100644
--- a/src/client/util/InteractionUtils.tsx
+++ b/src/client/util/InteractionUtils.tsx
@@ -1,6 +1,7 @@
 import React = require("react");
 import * as beziercurve from 'bezier-curve';
 import * as fitCurve from 'fit-curve';
+import InkOptionsMenu from "../views/collections/collectionFreeForm/InkOptionsMenu";
 
 export namespace InteractionUtils {
     export const MOUSETYPE = "mouse";
@@ -89,7 +90,8 @@ export namespace InteractionUtils {
         return myTouches;
     }
 
-    export function CreatePolyline(points: { X: number, Y: number }[], left: number, top: number, color: string, width: string, bezier: string, scalex: number, scaley: number, shape: string, pevents: string, drawHalo: boolean) {
+
+    export function CreatePolyline(points: { X: number, Y: number }[], left: number, top: number, color: string, width: string, bezier: string, fill: string, arrowStart: string, arrowEnd: string, dash: string, scalex: number, scaley: number, shape: string, pevents: string, drawHalo: boolean) {
         var pts = "";
         if (shape) {
             //if any of the shape are true
@@ -116,22 +118,58 @@ export namespace InteractionUtils {
             //in the middle of drawing
             pts = points.reduce((acc: string, pt: { X: number, Y: number }) => acc + `${pt.X * scalex - left * scalex},${pt.Y * scaley - top * scaley} `, "");
         }
+        const dashArray = String(Number(width) * Number(dash));
+
         return (
-            <polyline
-                points={pts}
-                style={{
-                    filter: drawHalo ? "url(#dangerShine)" : undefined,
-                    fill: "none",
-                    pointerEvents: pevents as any,
-                    stroke: color ?? "rgb(0, 0, 0)",
-                    strokeWidth: parseInt(width),
-                    strokeLinejoin: "round",
-                    strokeLinecap: "round"
-                }}
-            />
+            <svg>
+                <defs>
+
+                    {/* {makeArrow()} */}
+                    <marker id="dot" orient="auto" overflow="visible">
+                        <circle r={1} fill="red" />
+                    </marker>
+                    <marker id="arrowHead" orient="auto" overflow="visible" refX="3" refY="1" markerWidth="10" markerHeight="7">
+                        {/* <rect width={strokeWidth} height={strokeWidth} transform='rotate(45)' fill="dodgerblue" /> */}
+                        <polygon points="3 0, 3 2, 0 1" fill="dodgerblue" />
+                    </marker>
+                    <marker id="arrowEnd" orient="auto" overflow="visible" refX="0" refY="1" markerWidth="10" markerHeight="7">
+                        {/* <rect width={strokeWidth} height={strokeWidth} transform='rotate(45)' fill="dodgerblue" /> */}
+                        <polygon points="0 0, 3 1, 0 2" fill={"#" + color} />
+                    </marker>
+                </defs>
+
+                <polyline
+                    points={pts}
+                    style={{
+                        filter: drawHalo ? "url(#dangerShine)" : undefined,
+                        fill: fill,
+                        pointerEvents: pevents as any,
+                        stroke: color ?? "rgb(0, 0, 0)",
+                        strokeWidth: parseInt(width),
+                        strokeLinejoin: "round",
+                        strokeLinecap: "round",
+                        strokeDasharray: dashArray
+                    }}
+                    markerStart={arrowStart}
+                    markerEnd={arrowEnd}
+                />
+            </svg>
+
         );
     }
 
+    // export function makeArrow() {
+    //     return (
+    //         InkOptionsMenu.Instance.getColors().map(color => {
+    //             const id1 = "arrowHeadTest" + color;
+    //             console.log(color);
+    //             <marker id={id1} orient="auto" overflow="visible" refX="0" refY="1" markerWidth="10" markerHeight="7">
+    //                 <polygon points="0 0, 3 1, 0 2" fill={"#" + color} />
+    //             </marker>;
+    //         })
+    //     );
+    // }
+
     export function makePolygon(shape: string, points: { X: number, Y: number }[]) {
         if (points.length > 1 && points[points.length - 1].X === points[0].X && points[points.length - 1].Y + 1 === points[0].Y) {
             //pointer is up (first and last points are the same)
diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss
index 0a96b058b..cf5871bda 100644
--- a/src/client/views/DocumentDecorations.scss
+++ b/src/client/views/DocumentDecorations.scss
@@ -22,11 +22,20 @@ $linkGap : 3px;
     }
 
 
+    .documentDecorations-rotation {
+        pointer-events: auto;
+        // cursor: grabbing;
+        cursor: ns-resize;
+        width: 10px;
+        height: 10px;
+    }
+
     .documentDecorations-resizer {
         pointer-events: auto;
         background: $alt-accent;
         opacity: 0.1;
     }
+
     .documentDecorations-resizer:hover {
         opacity: 1;
     }
@@ -87,14 +96,17 @@ $linkGap : 3px;
         background: unset;
         opacity: 1;
     }
+
     #documentDecorations-topLeftResizer {
-        border-left:  2px solid;
-        border-top:  solid 2px;
+        border-left: 2px solid;
+        border-top: solid 2px;
     }
+
     #documentDecorations-bottomRightResizer {
-        border-right:  2px solid;
-        border-bottom:  solid 2px;
+        border-right: 2px solid;
+        border-bottom: solid 2px;
     }
+
     #documentDecorations-topLeftResizer:hover,
     #documentDecorations-bottomRightResizer:hover {
         opacity: 1;
@@ -110,14 +122,17 @@ $linkGap : 3px;
         background: unset;
         opacity: 1;
     }
+
     #documentDecorations-topRightResizer {
-        border-right:  2px solid;
-        border-top:  2px solid;
+        border-right: 2px solid;
+        border-top: 2px solid;
     }
+
     #documentDecorations-bottomLeftResizer {
-        border-left:  2px solid;
-        border-bottom:  2px solid;
+        border-left: 2px solid;
+        border-bottom: 2px solid;
     }
+
     #documentDecorations-topRightResizer:hover,
     #documentDecorations-bottomLeftResizer:hover {
         cursor: nesw-resize;
@@ -139,10 +154,11 @@ $linkGap : 3px;
         width: 25px;
         height: calc(100% + 8px); // 8px for the height of the top resizer bar
         grid-column-start: 2;
-        grid-column-end : 2;
+        grid-column-end: 2;
         pointer-events: all;
         padding-left: 5px;
     }
+
     .documentDecorations-title {
         opacity: 1;
         grid-column-start: 3;
@@ -150,14 +166,16 @@ $linkGap : 3px;
         pointer-events: auto;
         overflow: hidden;
         text-align: center;
-        display: flex;    
+        display: flex;
         border-bottom: solid 1px;
-        margin-left:10px;
+        margin-left: 10px;
         width: calc(100% - 10px);
     }
+
     .focus-visible {
-        margin-left:0px;
+        margin-left: 0px;
     }
+
     .publishBox {
         width: 20px;
         height: 22px;
@@ -194,8 +212,9 @@ $linkGap : 3px;
     width: 8px;
     height: $MINIMIZED_ICON_SIZE;
     max-height: 20px;
-    > svg {
-        margin:0;
+
+    >svg {
+        margin: 0;
     }
 }
 
@@ -332,7 +351,8 @@ $linkGap : 3px;
     padding: 2px 12px;
     list-style: none;
 
-    .templateToggle, .chromeToggle {
+    .templateToggle,
+    .chromeToggle {
         text-align: left;
     }
 
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 6ca7331d6..8cb0c83ed 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -17,11 +17,13 @@ import { DocumentButtonBar } from './DocumentButtonBar';
 import './DocumentDecorations.scss';
 import { DocumentView } from "./nodes/DocumentView";
 import React = require("react");
-import { Id } from '../../fields/FieldSymbols';
+import { Id, Copy, Update } from '../../fields/FieldSymbols';
 import e = require('express');
 import { CollectionDockingView } from './collections/CollectionDockingView';
 import { SnappingManager } from '../util/SnappingManager';
 import { HtmlField } from '../../fields/HtmlField';
+import { InkData, InkField, InkTool } from "../../fields/InkField";
+import { update } from 'serializr';
 
 library.add(faCaretUp);
 library.add(faObjectGroup);
@@ -50,6 +52,10 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
     private _resizeUndo?: UndoManager.Batch;
     private _offX = 0; _offY = 0;  // offset from click pt to inner edge of resize border
     private _snapX = 0; _snapY = 0; // last snapped location of resize border
+    private _prevX = 0;
+    private _prevY = 0;
+    private _centerPoints: { X: number, Y: number }[] = [];
+
     @observable private _accumulatedTitle = "";
     @observable private _titleControlString: string = "#title";
     @observable private _edtingTitle = false;
@@ -237,6 +243,77 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
         return false;
     }
 
+    @action
+    onRotateDown = (e: React.PointerEvent): void => {
+        setupMoveUpEvents(this, e, this.onRotateMove, this.onRotateUp, (e) => { });
+        this._prevX = e.clientX;
+        this._prevY = e.clientY;
+        SelectionManager.SelectedDocuments().forEach(action((element: DocumentView) => {
+            const doc = Document(element.rootDoc);
+            if (doc.type === DocumentType.INK && doc.x && doc.y && doc._width && doc._height && doc.data) {
+                const ink = Cast(doc.data, InkField)?.inkData;
+                if (ink) {
+                    const xs = ink.map(p => p.X);
+                    const ys = ink.map(p => p.Y);
+                    const left = Math.min(...xs);
+                    const top = Math.min(...ys);
+                    const right = Math.max(...xs);
+                    const bottom = Math.max(...ys);
+                    this._centerPoints.push({ X: ((right - left) / 2) + left, Y: ((bottom - top) / 2) + bottom });
+                }
+            }
+        }));
+
+    }
+    @action
+    onRotateMove = (e: PointerEvent, down: number[]): boolean => {
+        // const distance = Math.sqrt((this._prevY - e.clientY) * (this._prevY - e.clientY) + (this._prevX - e.clientX) * (this._prevX - e.clientX));
+        const distance = Math.abs(this._prevY - e.clientY);
+        var angle = 0;
+        //think of a better condition later...
+        // if ((down[0] < e.clientX && this._prevY < e.clientY) || (down[0] > e.clientX && this._prevY > e.clientY)) {
+        if (e.clientY > this._prevY) {
+            angle = distance * (Math.PI / 180);
+            // } else if ((down[0] < e.clientX && this._prevY > e.clientY) || (down[0] > e.clientX && this._prevY <= e.clientY)) {
+        } else if (e.clientY < this._prevY) {
+            angle = - distance * (Math.PI / 180);
+        }
+        this._prevX = e.clientX;
+        this._prevY = e.clientY;
+        var index = 0;
+        SelectionManager.SelectedDocuments().forEach(action((element: DocumentView) => {
+            const doc = Document(element.rootDoc);
+            if (doc.type === DocumentType.INK && doc.x && doc.y && doc._width && doc._height && doc.data) {
+                const ink = Cast(doc.data, InkField)?.inkData;
+                if (ink) {
+                    const newPoints: { X: number, Y: number }[] = [];
+                    for (var i = 0; i < ink.length; i++) {
+                        const newX = Math.cos(angle) * (ink[i].X - this._centerPoints[index].X) - Math.sin(angle) * (ink[i].Y - this._centerPoints[index].Y) + this._centerPoints[index].X;
+                        const newY = Math.sin(angle) * (ink[i].X - this._centerPoints[index].X) + Math.cos(angle) * (ink[i].Y - this._centerPoints[index].Y) + this._centerPoints[index].Y;
+                        newPoints.push({ X: newX, Y: newY });
+                    }
+                    doc.data = new InkField(newPoints);
+                    const xs = newPoints.map(p => p.X);
+                    const ys = newPoints.map(p => p.Y);
+                    const left = Math.min(...xs);
+                    const top = Math.min(...ys);
+                    const right = Math.max(...xs);
+                    const bottom = Math.max(...ys);
+                    doc._height = bottom - top;
+                    doc._width = right - left;
+                }
+                index++;
+            }
+        }));
+        return false;
+    }
+
+    onRotateUp = (e: PointerEvent) => {
+        this._centerPoints = [];
+    }
+
+
+
     _initialAutoHeight = false;
     _dragHeights = new Map<Doc, number>();
     @action
@@ -521,6 +598,8 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
                 <div className="documentDecorations-closeButton" title="Open Document in Tab" onPointerDown={this.onMaximizeDown}>
                     {SelectionManager.SelectedDocuments().length === 1 ? DocumentDecorations.DocumentIcon(StrCast(seldoc.props.Document.layout, "...")) : "..."}
                 </div>
+                <div id="documentDecorations-rotation" className="documentDecorations-rotation"
+                    onPointerDown={this.onRotateDown}> ⟲ </div>
                 <div id="documentDecorations-topLeftResizer" className="documentDecorations-resizer"
                     onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
                 <div id="documentDecorations-topResizer" className="documentDecorations-resizer"
diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx
index d239a1d6f..5b6cbc372 100644
--- a/src/client/views/GestureOverlay.tsx
+++ b/src/client/views/GestureOverlay.tsx
@@ -18,7 +18,7 @@ import { LinkManager } from "../util/LinkManager";
 import { Scripting } from "../util/Scripting";
 import { Transform } from "../util/Transform";
 import "./GestureOverlay.scss";
-import { ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, InkingStroke, SetActiveInkColor, SetActiveInkWidth } from "./InkingStroke";
+import { ActiveInkBezierApprox, ActiveArrowStart, ActiveArrowEnd, ActiveFillColor, ActiveInkColor, ActiveInkWidth, InkingStroke, SetActiveInkColor, SetActiveInkWidth, SetActiveFillColor, SetActiveArrowStart, SetActiveArrowEnd, ActiveDash, SetActiveDash } from "./InkingStroke";
 import { DocumentView } from "./nodes/DocumentView";
 import { RadialMenu } from "./nodes/RadialMenu";
 import HorizontalPalette from "./Palette";
@@ -32,6 +32,10 @@ export default class GestureOverlay extends Touchable {
     @observable public InkShape: string = "";
     @observable public SavedColor?: string;
     @observable public SavedWidth?: string;
+    @observable public SavedFill?: string;
+    @observable public SavedArrowStart: string = "none";
+    @observable public SavedArrowEnd: string = "none";
+    @observable public SavedDash: String = "0";
     @observable public Tool: ToolglassTools = ToolglassTools.None;
 
     @observable private _thumbX?: number;
@@ -802,6 +806,7 @@ export default class GestureOverlay extends Touchable {
     }
 
     @computed get elements() {
+
         const B = this.svgBounds;
         return [
             this.props.children,
@@ -809,11 +814,11 @@ export default class GestureOverlay extends Touchable {
             [this._strokes.map(l => {
                 const b = this.getBounds(l);
                 return <svg key={b.left} width={b.width} height={b.height} style={{ transform: `translate(${b.left}px, ${b.top}px)`, pointerEvents: "none", position: "absolute", zIndex: 30000, overflow: "visible" }}>
-                    {InteractionUtils.CreatePolyline(l, b.left, b.top, ActiveInkColor(), ActiveInkWidth(), ActiveInkBezierApprox(), 1, 1, this.InkShape)}
+                    {InteractionUtils.CreatePolyline(l, b.left, b.top, ActiveInkColor(), ActiveInkWidth(), ActiveInkBezierApprox(), ActiveFillColor(), ActiveArrowStart(), ActiveArrowEnd(), ActiveDash(), 1, 1, this.InkShape, "none", false)}
                 </svg>;
             }),
             this._points.length <= 1 ? (null) : <svg width={B.width} height={B.height} style={{ transform: `translate(${B.left}px, ${B.top}px)`, pointerEvents: "none", position: "absolute", zIndex: 30000, overflow: "visible" }}>
-                {InteractionUtils.CreatePolyline(this._points, B.left, B.top, ActiveInkColor(), ActiveInkWidth(), ActiveInkBezierApprox(), 1, 1, this.InkShape)}
+                {InteractionUtils.CreatePolyline(this._points, B.left, B.top, ActiveInkColor(), ActiveInkWidth(), ActiveInkBezierApprox(), ActiveFillColor(), ActiveArrowStart(), ActiveArrowEnd(), ActiveDash(), 1, 1, this.InkShape, "none", false)}
             </svg>]
         ];
     }
@@ -901,12 +906,20 @@ Scripting.addGlobal("GestureOverlay", GestureOverlay);
 Scripting.addGlobal(function setToolglass(tool: any) {
     runInAction(() => GestureOverlay.Instance.Tool = tool);
 });
-Scripting.addGlobal(function setPen(width: any, color: any) {
+Scripting.addGlobal(function setPen(width: any, color: any, fill: any, arrowStart: any, arrowEnd: any, dash: any) {
     runInAction(() => {
         GestureOverlay.Instance.SavedColor = ActiveInkColor();
         SetActiveInkColor(color);
         GestureOverlay.Instance.SavedWidth = ActiveInkWidth();
         SetActiveInkWidth(width);
+        GestureOverlay.Instance.SavedFill = ActiveFillColor();
+        SetActiveFillColor(fill);
+        GestureOverlay.Instance.SavedArrowStart = ActiveArrowStart();
+        SetActiveArrowStart(arrowStart);
+        GestureOverlay.Instance.SavedArrowEnd = ActiveArrowEnd();
+        SetActiveArrowStart(arrowEnd);
+        GestureOverlay.Instance.SavedDash = ActiveDash();
+        SetActiveDash(dash);
     });
 });
 Scripting.addGlobal(function resetPen() {
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index a7650163f..324665aea 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -58,11 +58,13 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
         const points = InteractionUtils.CreatePolyline(data, left, top,
             strokeColor,
             strokeWidth.toString(),
-            StrCast(this.layoutDoc.strokeBezier, ActiveInkBezierApprox()), scaleX, scaleY, "", "none", this.props.isSelected() && strokeWidth <= 5);
+            StrCast(this.layoutDoc.strokeBezier, ActiveInkBezierApprox()), StrCast(this.layoutDoc.fillColor, ActiveFillColor()), StrCast(this.layoutDoc.arrowStart, ActiveArrowStart()), StrCast(this.layoutDoc.arrowEnd, ActiveArrowEnd()), StrCast(this.layoutDoc.dash, ActiveDash()), scaleX, scaleY, "", "none", this.props.isSelected() && strokeWidth <= 5);
         const hpoints = InteractionUtils.CreatePolyline(data, left, top,
             this.props.isSelected() && strokeWidth > 5 ? strokeColor : "transparent",
+            // strokeColor,
             (strokeWidth + 15).toString(),
-            StrCast(this.layoutDoc.strokeBezier, ActiveInkBezierApprox()), scaleX, scaleY, "", this.props.active() ? "visiblestroke" : "none", false);
+            StrCast(this.layoutDoc.strokeBezier, ActiveInkBezierApprox()), StrCast(this.layoutDoc.fillColor, ActiveFillColor()), StrCast(this.layoutDoc.arrowStart, ActiveArrowStart()), StrCast(this.layoutDoc.arrowEnd, ActiveArrowEnd()), StrCast(this.layoutDoc.dash, ActiveDash()), scaleX, scaleY, "", this.props.active() ? "visiblestroke" : "none", false);
+        console.log("#" + strokeColor);
         return (
             <svg className="inkingStroke"
                 width={width}
@@ -70,7 +72,8 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
                 style={{
                     pointerEvents: this.props.Document.isInkMask ? "all" : "none",
                     transform: this.props.Document.isInkMask ? "translate(2500px, 2500px)" : undefined,
-                    mixBlendMode: this.layoutDoc.tool === InkTool.Highlighter ? "multiply" : "unset"
+                    mixBlendMode: this.layoutDoc.tool === InkTool.Highlighter ? "multiply" : "unset",
+                    overflow: "visible",
                 }}
                 onContextMenu={() => {
                     ContextMenu.Instance.addItem({ description: "Analyze Stroke", event: this.analyzeStrokes, icon: "paint-brush" });
@@ -93,6 +96,18 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
                             <feMergeNode in="SourceGraphic"></feMergeNode>
                         </feMerge>
                     </filter>
+                    {/* <marker id="arrow" markerWidth="10" markerHeight="10" refX="0" refY="3" orient="auto" markerUnits="strokeWidth">
+                        <path d="M0,0 L0,6 L9,3 z" fill="black" />
+                    </marker> */}
+                    {/* <marker id="arrowHead" orient="auto" overflow="visible" refX="10" refY="3.5" markerWidth="10" markerHeight="7">
+                        <polygon points="10 0, 10 7, 0 3.5" fill="dodgerblue" />
+                    </marker>
+                    <marker id="arrowEnd" orient="auto" overflow="visible" refX="0" refY="3.5" markerWidth="10" markerHeight="7">
+                        <polygon points="0 0, 10 3.5, 0 7" fill="dodgerblue" />
+                    </marker> */}
+                    {/* <marker id="dot" orient="auto" overflow="visible">
+                        <circle r={strokeWidth} fill={"#" + strokeColor} />
+                    </marker> */}
                 </defs>
                 {hpoints}
                 {points}
@@ -105,17 +120,33 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
 export function SetActiveInkWidth(width: string): void { !isNaN(parseInt(width)) && ActiveInkPen() && (ActiveInkPen().activeInkWidth = width); }
 export function SetActiveBezierApprox(bezier: string): void { ActiveInkPen() && (ActiveInkPen().activeInkBezier = isNaN(parseInt(bezier)) ? "" : bezier); }
 export function SetActiveInkColor(value: string) { ActiveInkPen() && (ActiveInkPen().activeInkColor = value); }
+export function SetActiveFillColor(value: string) { ActiveInkPen() && (ActiveInkPen().activeFillColor = value); }
+export function SetActiveArrowStart(value: string) { ActiveInkPen() && (ActiveInkPen().activeArrowStart = value); }
+export function SetActiveArrowEnd(value: string) { ActiveInkPen() && (ActiveInkPen().activeArrowEnd = value); }
+export function SetActiveDash(dash: string): void { !isNaN(parseInt(dash)) && ActiveInkPen() && (ActiveInkPen().activeDash = dash); }
 export function ActiveInkPen(): Doc { return Cast(Doc.UserDoc().activeInkPen, Doc, null); }
 export function ActiveInkColor(): string { return StrCast(ActiveInkPen()?.activeInkColor, "black"); }
+export function ActiveFillColor(): string { return StrCast(ActiveInkPen()?.activeFillColor, "none"); }
+export function ActiveArrowStart(): string { return StrCast(ActiveInkPen()?.activeArrowStart, "none"); }
+export function ActiveArrowEnd(): string { return StrCast(ActiveInkPen()?.activeArrowEnd, "none"); }
+export function ActiveDash(): string { return StrCast(ActiveInkPen()?.activeDash, "0"); }
 export function ActiveInkWidth(): string { return StrCast(ActiveInkPen()?.activeInkWidth, "1"); }
 export function ActiveInkBezierApprox(): string { return StrCast(ActiveInkPen()?.activeInkBezier); }
-Scripting.addGlobal(function activateBrush(pen: any, width: any, color: any) {
+Scripting.addGlobal(function activateBrush(pen: any, width: any, color: any, fill: any, arrowStart: any, arrowEnd: any, dash: any) {
     Doc.SetSelectedTool(pen ? InkTool.Highlighter : InkTool.None);
     SetActiveInkWidth(width);
     SetActiveInkColor(color);
+    SetActiveFillColor(fill);
+    SetActiveArrowStart(arrowStart);
+    SetActiveArrowEnd(arrowEnd);
+    SetActiveDash(dash);
 });
 Scripting.addGlobal(function activateEraser(pen: any) { return Doc.SetSelectedTool(pen ? InkTool.Eraser : InkTool.None); });
 Scripting.addGlobal(function activateStamp(pen: any) { return Doc.SetSelectedTool(pen ? InkTool.Stamp : InkTool.None); });
 Scripting.addGlobal(function deactivateInk() { return Doc.SetSelectedTool(InkTool.None); });
 Scripting.addGlobal(function setInkWidth(width: any) { return SetActiveInkWidth(width); });
-Scripting.addGlobal(function setInkColor(color: any) { return SetActiveInkColor(color); });
\ No newline at end of file
+Scripting.addGlobal(function setInkColor(color: any) { return SetActiveInkColor(color); });
+Scripting.addGlobal(function setFillColor(fill: any) { return SetActiveFillColor(fill); });
+Scripting.addGlobal(function setActiveArrowStart(arrowStart: any) { return SetActiveArrowStart(arrowStart); });
+Scripting.addGlobal(function setActiveArrowEnd(arrowEnd: any) { return SetActiveArrowStart(arrowEnd); });
+Scripting.addGlobal(function setActiveDash(dash: any) { return SetActiveDash(dash); });
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 678ad2a53..b97ac286c 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -45,7 +45,7 @@ import React = require("react");
 import { CollectionViewType } from "../CollectionView";
 import { Timeline } from "../../animationtimeline/Timeline";
 import { SnappingManager } from "../../../util/SnappingManager";
-import { InkingStroke, ActiveInkColor, ActiveInkWidth, ActiveInkBezierApprox } from "../../InkingStroke";
+import { InkingStroke, ActiveArrowStart, ActiveArrowEnd, ActiveInkColor, ActiveFillColor, ActiveInkWidth, ActiveInkBezierApprox, ActiveDash } from "../../InkingStroke";
 
 library.add(faEye as any, faTable, faPaintBrush, faExpandArrowsAlt, faCompressArrowsAlt, faCompass, faUpload, faBraille, faChalkboard, faFileUpload);
 
@@ -456,7 +456,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
             case GestureUtils.Gestures.Stroke:
                 const points = ge.points;
                 const B = this.getTransform().transformBounds(ge.bounds.left, ge.bounds.top, ge.bounds.width, ge.bounds.height);
-                const inkDoc = Docs.Create.InkDocument(ActiveInkColor(), Doc.GetSelectedTool(), ActiveInkWidth(), ActiveInkBezierApprox(), points, { title: "ink stroke", x: B.x, y: B.y, _width: B.width, _height: B.height });
+                const inkDoc = Docs.Create.InkDocument(ActiveInkColor(), Doc.GetSelectedTool(), ActiveInkWidth(), ActiveInkBezierApprox(), ActiveFillColor(), ActiveArrowStart(), ActiveArrowEnd(), ActiveDash(), points, { title: "ink stroke", x: B.x, y: B.y, _width: B.width, _height: B.height });
                 this.addDocument(inkDoc);
                 e.stopPropagation();
                 break;
diff --git a/src/client/views/collections/collectionFreeForm/InkOptionsMenu.tsx b/src/client/views/collections/collectionFreeForm/InkOptionsMenu.tsx
index 5a27f74e5..96a9cbbdb 100644
--- a/src/client/views/collections/collectionFreeForm/InkOptionsMenu.tsx
+++ b/src/client/views/collections/collectionFreeForm/InkOptionsMenu.tsx
@@ -3,25 +3,39 @@ import AntimodeMenu from "../../AntimodeMenu";
 import { observer } from "mobx-react";
 import { observable, action, computed } from "mobx";
 import "./InkOptionsMenu.scss";
-import { ActiveInkColor, ActiveInkBezierApprox, SetActiveInkWidth, SetActiveInkColor, SetActiveBezierApprox } from "../../InkingStroke";
+import { ActiveInkColor, ActiveInkBezierApprox, ActiveFillColor, ActiveArrowStart, ActiveArrowEnd, SetActiveInkWidth, SetActiveInkColor, SetActiveBezierApprox, SetActiveFillColor, SetActiveArrowStart, SetActiveArrowEnd, ActiveDash, SetActiveDash } from "../../InkingStroke";
 import { Scripting } from "../../../util/Scripting";
 import { InkTool } from "../../../../fields/InkField";
 import { ColorState } from "react-color";
 import { Utils } from "../../../../Utils";
 import GestureOverlay from "../../GestureOverlay";
 import { Doc } from "../../../../fields/Doc";
+import { SelectionManager } from "../../../util/SelectionManager";
+import { DocumentView } from "../../../views/nodes/DocumentView";
+import { Document } from "../../../../fields/documentSchemas";
+import { DocumentType } from "../../../documents/DocumentTypes";
 
 @observer
 export default class InkOptionsMenu extends AntimodeMenu {
     static Instance: InkOptionsMenu;
 
-    private _palette = ["D0021B", "F5A623", "F8E71C", "8B572A", "7ED321", "417505", "9013FE", "4A90E2", "50E3C2", "B8E986", "000000", "4A4A4A", "9B9B9B", "FFFFFF"];
+    private _palette = ["D0021B", "F5A623", "F8E71C", "8B572A", "7ED321", "417505", "9013FE", "4A90E2", "50E3C2", "B8E986", "000000", "4A4A4A", "9B9B9B", "FFFFFF", "none"];
     private _width = ["1", "5", "10", "100", "200", "300"];
-    private _buttons = ["circle", "triangle", "rectangle", "arrow", "line"];
-    private _icons = ["O", "∆", "ロ", "➜", "-"];
+    // private _buttons = ["circle", "triangle", "rectangle", "arrow", "line"];
+    // private _icons = ["O", "∆", "ロ", "➜", "-"];
+    private _buttons = ["circle", "triangle", "rectangle", "line", ""];
+    private _icons = ["O", "∆", "ロ", "-", " "];
+    //arrowStart and arrowEnd must match and defs must exist in Inking Stroke
+    private _arrowStart = ["url(#arrowHead)", "url(#arrowHead)", "url(#dot)", "url(#dot)", "none"];
+    private _arrowEnd = ["none", "url(#arrowEnd)", "none", "url(#dot)", "none"];
+    private _arrowIcons = ["➡︎", "↔︎", "o-", "o-o", " "];
 
     @observable _colorBtn = false;
     @observable _widthBtn = false;
+    @observable _fillBtn = false;
+    @observable _arrowBtn = false;
+    @observable _dashBtn = false;
+    @observable _shapeBtn = false;
 
     constructor(props: Readonly<{}>) {
         super(props);
@@ -29,18 +43,103 @@ export default class InkOptionsMenu extends AntimodeMenu {
         this._canFade = false; // don't let the inking menu fade away
     }
 
+    getColors = () => {
+        return this._palette;
+    }
+
+    @action
+    changeArrow = (arrowStart: string, arrowEnd: string) => {
+        SetActiveArrowStart(arrowStart);
+        SetActiveArrowEnd(arrowEnd);
+    }
+
     @action
-    changeColor = (color: string) => {
+    changeColor = (color: string, type: string) => {
         const col: ColorState = {
             hex: color, hsl: { a: 0, h: 0, s: 0, l: 0, source: "" }, hsv: { a: 0, h: 0, s: 0, v: 0, source: "" },
             rgb: { a: 0, r: 0, b: 0, g: 0, source: "" }, oldHue: 0, source: "",
         };
-        SetActiveInkColor(Utils.colorString(col));
+        if (type === "color") {
+            SetActiveInkColor(Utils.colorString(col));
+        } else if (type === "fill") {
+            SetActiveFillColor(Utils.colorString(col));
+        }
     }
 
+    @action
+    editProperties = (value: any, field: string) => {
+        SelectionManager.SelectedDocuments().forEach(action((element: DocumentView) => {
+            const doc = Document(element.rootDoc);
+            if (doc.type === DocumentType.INK) {
+                switch (field) {
+                    case "width":
+                        doc.strokeWidth = Number(value);
+                        break;
+                    case "color":
+                        doc.color = String(value);
+                        break;
+                    case "fill":
+                        doc.fillColor = String(value);
+                        break;
+                    case "bezier":
+                        // doc.strokeBezier === 300 ? doc.strokeBezier = 0 : doc.strokeBezier = 300;
+                        break;
+                    case "arrowStart":
+                        doc.arrowStart = String(value);
+                        break;
+                    case "arrowEnd":
+                        doc.arrowEnd = String(value);
+                        break;
+                    case "dash":
+                        doc.dash = Number(value);
+                    default:
+                        break;
+                }
+            }
+        }));
+    }
+
+
     @action
     changeBezier = (e: React.PointerEvent): void => {
         SetActiveBezierApprox(!ActiveInkBezierApprox() ? "300" : "");
+        this.editProperties(0, "bezier");
+    }
+    @action
+    changeDash = (e: React.PointerEvent): void => {
+        SetActiveDash(ActiveDash() === "0" ? "2" : "0");
+        this.editProperties(ActiveDash(), "dash");
+    }
+
+    @computed get arrowPicker() {
+        var currIcon;
+        for (var i = 0; i < this._arrowStart.length; i++) {
+            if (this._arrowStart[i] === ActiveArrowStart() && this._arrowEnd[i] === ActiveArrowEnd()) {
+                currIcon = this._arrowIcons[i];
+            }
+        }
+        var arrowPicker = <button
+            className="antimodeMenu-button"
+            key="arrow"
+            onPointerDown={action(e => this._arrowBtn = !this._arrowBtn)}
+            style={{ backgroundColor: this._arrowBtn ? "121212" : "" }}>
+            {currIcon}
+        </button>;
+        if (this._arrowBtn) {
+            arrowPicker = <div className="btn2-group" key="arrows">
+                {arrowPicker}
+                {this._arrowStart.map((arrowStart, i) => {
+                    return <button
+                        className="antimodeMenu-button"
+                        key={arrowStart}
+                        onPointerDown={action(() => { SetActiveArrowStart(arrowStart); SetActiveArrowEnd(this._arrowEnd[i]); this.editProperties(arrowStart, "arrowStart"), this.editProperties(this._arrowEnd[i], "arrowEnd"); this._arrowBtn = false; })}
+                        style={{ backgroundColor: this._arrowBtn ? "121212" : "" }}>
+                        {this._arrowIcons[i]}
+                    </button>;
+                })}
+            </div>;
+        }
+        return arrowPicker;
     }
 
     @computed get widthPicker() {
@@ -58,7 +157,7 @@ export default class InkOptionsMenu extends AntimodeMenu {
                     return <button
                         className="antimodeMenu-button"
                         key={wid}
-                        onPointerDown={action(() => { SetActiveInkWidth(wid); this._widthBtn = false; })}
+                        onPointerDown={action(() => { SetActiveInkWidth(wid); this._widthBtn = false; this.editProperties(wid, "width"); })}
                         style={{ backgroundColor: this._widthBtn ? "121212" : "" }}>
                         {wid}
                     </button>;
@@ -68,6 +167,8 @@ export default class InkOptionsMenu extends AntimodeMenu {
         return widthPicker;
     }
 
+
+
     @computed get colorPicker() {
         var colorPicker = <button
             className="antimodeMenu-button"
@@ -84,7 +185,7 @@ export default class InkOptionsMenu extends AntimodeMenu {
                     return <button
                         className="antimodeMenu-button"
                         key={color}
-                        onPointerDown={action(() => { this.changeColor(color); this._colorBtn = false; })}
+                        onPointerDown={action(() => { this.changeColor(color, "color"); this._colorBtn = false; this.editProperties(color, "color"); })}
                         style={{ backgroundColor: this._colorBtn ? "121212" : "" }}>
                         <div className="color-preview" style={{ backgroundColor: color }}></div>
                     </button>;
@@ -94,6 +195,35 @@ export default class InkOptionsMenu extends AntimodeMenu {
         return colorPicker;
     }
 
+    @computed get fillPicker() {
+        var fillPicker = <button
+            className="antimodeMenu-button"
+            key="fill"
+            title="fillChanger"
+            onPointerDown={action(e => this._fillBtn = !this._fillBtn)}
+            style={{ backgroundColor: this._fillBtn ? "121212" : "" }}>
+            <div className="color-preview" style={{ backgroundColor: ActiveFillColor() ?? "121212" }}></div>
+        </button>;
+        if (this._fillBtn) {
+            fillPicker = <div className="btn-group" key="fill">
+                {fillPicker}
+                {this._palette.map(color => {
+                    return <button
+                        className="antimodeMenu-button"
+                        key={color}
+                        onPointerDown={action(() => { this.changeColor(color, "fill"); this._fillBtn = false; this.editProperties(color, "fill"); })}
+                        style={{ backgroundColor: this._fillBtn ? "121212" : "" }}>
+                        <div className="color-preview" style={{ backgroundColor: color }}></div>
+                    </button>;
+                })}
+
+            </div>;
+        }
+        return fillPicker;
+    }
+
+
+
     @computed get shapeButtons() {
         return <>
             {this._buttons.map((btn, i) => <button
@@ -103,28 +233,79 @@ export default class InkOptionsMenu extends AntimodeMenu {
                 onPointerDown={action(e => GestureOverlay.Instance.InkShape = btn)}
                 style={{ backgroundColor: btn === GestureOverlay.Instance.InkShape ? "121212" : "" }}>
                 {this._icons[i]}
-            </button>)},
+            </button>)}
         </>;
     }
 
+    @computed get shapePicker() {
+        var currIcon;
+        if (GestureOverlay.Instance.InkShape === "") {
+            currIcon = "S";
+        } else {
+            for (var i = 0; i < this._icons.length; i++) {
+                if (GestureOverlay.Instance.InkShape === this._buttons[i]) {
+                    currIcon = this._icons[i];
+                }
+            }
+        }
+        var shapePicker = <button
+            className="antimodeMenu-button"
+            key="shape"
+            onPointerDown={action(e => this._shapeBtn = !this._shapeBtn)}
+            style={{ backgroundColor: this._widthBtn ? "121212" : "" }}>
+            {currIcon}
+        </button>;
+        if (this._shapeBtn) {
+            shapePicker = <div className="btn2-group" key="shape">
+                {shapePicker}
+                {this._buttons.map((btn, i) => {
+                    return <button
+                        className="antimodeMenu-button"
+                        title={`Draw ${btn}`}
+                        key={btn}
+                        onPointerDown={action((e) => { GestureOverlay.Instance.InkShape = btn; this._shapeBtn = false; })}
+                        style={{ backgroundColor: this._shapeBtn ? "121212" : "" }}>
+                        {this._icons[i]}
+                    </button>;
+                })}
+            </div>;
+        }
+        return shapePicker;
+    }
+
     @computed get bezierButton() {
         return <button
             className="antimodeMenu-button"
             title="Bezier changer"
             key="bezier"
             onPointerDown={e => this.changeBezier(e)}
-            style={ { backgroundColor:ActiveInkBezierApprox() ? "121212":"" } }>
+            style={{ backgroundColor: ActiveInkBezierApprox() ? "121212" : "" }}>
             B
         </button>;
     }
 
+    @computed get dashButton() {
+        return <button
+            className="antimodeMenu-button"
+            title="dash changer"
+            key="dash"
+            onPointerDown={e => this.changeDash(e)}
+            style={{ backgroundColor: ActiveDash() !== "0" ? "121212" : "" }}>
+            ...
+        </button>;
+    }
+
     render() {
         const buttons = [
             <button className="antimodeMenu-button" title="Drag" key="drag" onPointerDown={e => this.dragStart(e)}>  ✜  </button>,
-            this.shapeButtons,
+            this.shapePicker,
+            // this.shapeButtons,
             this.bezierButton,
             this.widthPicker,
             this.colorPicker,
+            this.fillPicker,
+            this.arrowPicker,
+            this.dashButton
         ];
         return this.getElement(buttons);
     }
diff --git a/src/fields/InkField.ts b/src/fields/InkField.ts
index 51a5768bf..7cfd74cc4 100644
--- a/src/fields/InkField.ts
+++ b/src/fields/InkField.ts
@@ -1,7 +1,7 @@
 import { Deserializable } from "../client/util/SerializationHelper";
 import { serializable, custom, createSimpleSchema, list, object, map } from "serializr";
 import { ObjectField } from "./ObjectField";
-import { Copy, ToScriptString, ToString } from "./FieldSymbols";
+import { Copy, ToScriptString, ToString, Update } from "./FieldSymbols";
 
 export enum InkTool {
     None = "none",
@@ -31,6 +31,8 @@ const strokeDataSchema = createSimpleSchema({
 export class InkField extends ObjectField {
     @serializable(list(object(strokeDataSchema)))
     readonly inkData: InkData;
+    // inkData: InkData;
+
 
     constructor(data: InkData) {
         super();
-- 
cgit v1.2.3-70-g09d2


From 780ef6fec5add6a0b6b05f5fa64f47b1a2b71df4 Mon Sep 17 00:00:00 2001
From: yunahi <60233430+yunahi@users.noreply.github.com>
Date: Sat, 27 Jun 2020 02:44:31 +0900
Subject: fixed rotation resize

---
 src/client/views/DocumentDecorations.tsx | 12 +++++++++---
 1 file changed, 9 insertions(+), 3 deletions(-)

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

diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index c091b2315..6e8eed287 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -24,6 +24,7 @@ import { SnappingManager } from '../util/SnappingManager';
 import { HtmlField } from '../../fields/HtmlField';
 import { InkData, InkField, InkTool } from "../../fields/InkField";
 import { update } from 'serializr';
+import { Transform } from "../util/Transform";
 
 library.add(faCaretUp);
 library.add(faObjectGroup);
@@ -260,7 +261,8 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
                     const top = Math.min(...ys);
                     const right = Math.max(...xs);
                     const bottom = Math.max(...ys);
-                    this._centerPoints.push({ X: ((right - left) / 2) + left, Y: ((bottom - top) / 2) + bottom });
+                    // this._centerPoints.push({ X: ((right - left) / 2) + left, Y: ((bottom - top) / 2) + bottom });
+                    this._centerPoints.push({ X: left, Y: top });
                 }
             }
         }));
@@ -268,6 +270,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
     }
     @action
     onRotateMove = (e: PointerEvent, down: number[]): boolean => {
+
         // const distance = Math.sqrt((this._prevY - e.clientY) * (this._prevY - e.clientY) + (this._prevX - e.clientX) * (this._prevX - e.clientX));
         const distance = Math.abs(this._prevY - e.clientY);
         var angle = 0;
@@ -287,6 +290,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
             if (doc.type === DocumentType.INK && doc.x && doc.y && doc._width && doc._height && doc.data) {
                 const ink = Cast(doc.data, InkField)?.inkData;
                 if (ink) {
+
                     const newPoints: { X: number, Y: number }[] = [];
                     for (var i = 0; i < ink.length; i++) {
                         const newX = Math.cos(angle) * (ink[i].X - this._centerPoints[index].X) - Math.sin(angle) * (ink[i].Y - this._centerPoints[index].Y) + this._centerPoints[index].X;
@@ -300,8 +304,10 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
                     const top = Math.min(...ys);
                     const right = Math.max(...xs);
                     const bottom = Math.max(...ys);
-                    doc._height = bottom - top;
-                    doc._width = right - left;
+
+                    doc._height = (bottom - top) * element.props.ScreenToLocalTransform().Scale;
+                    doc._width = (right - left) * element.props.ScreenToLocalTransform().Scale;
+
                 }
                 index++;
             }
-- 
cgit v1.2.3-70-g09d2


From 99d2b19dba130353ac3b021bc7eab079bb125408 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Sat, 27 Jun 2020 02:21:24 -0400
Subject: fixed icon button of document deoratins. cleaned up several compile
 errors and warnings

---
 package-lock.json                                  | 57 ++++++++++------------
 package.json                                       |  1 +
 src/client/documents/Documents.ts                  | 17 ++-----
 src/client/util/LinkManager.ts                     |  2 +-
 src/client/views/DocumentButtonBar.tsx             |  2 +-
 src/client/views/DocumentDecorations.scss          |  9 ++--
 src/client/views/DocumentDecorations.tsx           |  7 +--
 src/client/views/nodes/DocumentView.tsx            |  2 +-
 src/client/views/nodes/LinkDocPreview.tsx          |  4 +-
 .../formattedText/ProsemirrorExampleTransfer.ts    |  2 +-
 src/fields/List.ts                                 |  7 +--
 src/fields/ObjectField.ts                          |  4 +-
 src/fields/Schema.ts                               |  5 +-
 13 files changed, 55 insertions(+), 64 deletions(-)

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

diff --git a/package-lock.json b/package-lock.json
index 67d572ee2..928c59df4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -637,6 +637,14 @@
                 "@types/node": "*"
             }
         },
+        "@types/google-maps": {
+            "version": "3.2.2",
+            "resolved": "https://registry.npmjs.org/@types/google-maps/-/google-maps-3.2.2.tgz",
+            "integrity": "sha512-/XPVfS28VzUdE/HlmBRoe5ii1nNMyWujyRfRY08bD/JgmPlWSiY8enB2dqTe9mlc3kULq7LfFa1wcupM+lQfqA==",
+            "requires": {
+                "@types/googlemaps": "*"
+            }
+        },
         "@types/google-maps-react": {
             "version": "2.0.5",
             "resolved": "https://registry.npmjs.org/@types/google-maps-react/-/google-maps-react-2.0.5.tgz",
@@ -646,6 +654,11 @@
                 "google-maps-react": "*"
             }
         },
+        "@types/googlemaps": {
+            "version": "3.39.8",
+            "resolved": "https://registry.npmjs.org/@types/googlemaps/-/googlemaps-3.39.8.tgz",
+            "integrity": "sha512-z03u79t1v8QIktoUXypWD06Fzl499/hA162hurA+eCDlWXxFynuU+hMZIaferILF5Gzr4PMX1ShHszT666sUHQ=="
+        },
         "@types/jquery": {
             "version": "3.5.0",
             "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.0.tgz",
@@ -2922,8 +2935,7 @@
                         },
                         "ansi-regex": {
                             "version": "2.1.1",
-                            "bundled": true,
-                            "optional": true
+                            "bundled": true
                         },
                         "aproba": {
                             "version": "1.2.0",
@@ -2941,13 +2953,11 @@
                         },
                         "balanced-match": {
                             "version": "1.0.0",
-                            "bundled": true,
-                            "optional": true
+                            "bundled": true
                         },
                         "brace-expansion": {
                             "version": "1.1.11",
                             "bundled": true,
-                            "optional": true,
                             "requires": {
                                 "balanced-match": "^1.0.0",
                                 "concat-map": "0.0.1"
@@ -2960,18 +2970,15 @@
                         },
                         "code-point-at": {
                             "version": "1.1.0",
-                            "bundled": true,
-                            "optional": true
+                            "bundled": true
                         },
                         "concat-map": {
                             "version": "0.0.1",
-                            "bundled": true,
-                            "optional": true
+                            "bundled": true
                         },
                         "console-control-strings": {
                             "version": "1.1.0",
-                            "bundled": true,
-                            "optional": true
+                            "bundled": true
                         },
                         "core-util-is": {
                             "version": "1.0.2",
@@ -3074,8 +3081,7 @@
                         },
                         "inherits": {
                             "version": "2.0.4",
-                            "bundled": true,
-                            "optional": true
+                            "bundled": true
                         },
                         "ini": {
                             "version": "1.3.5",
@@ -3085,7 +3091,6 @@
                         "is-fullwidth-code-point": {
                             "version": "1.0.0",
                             "bundled": true,
-                            "optional": true,
                             "requires": {
                                 "number-is-nan": "^1.0.0"
                             }
@@ -3098,20 +3103,17 @@
                         "minimatch": {
                             "version": "3.0.4",
                             "bundled": true,
-                            "optional": true,
                             "requires": {
                                 "brace-expansion": "^1.1.7"
                             }
                         },
                         "minimist": {
                             "version": "1.2.5",
-                            "bundled": true,
-                            "optional": true
+                            "bundled": true
                         },
                         "minipass": {
                             "version": "2.9.0",
                             "bundled": true,
-                            "optional": true,
                             "requires": {
                                 "safe-buffer": "^5.1.2",
                                 "yallist": "^3.0.0"
@@ -3128,7 +3130,6 @@
                         "mkdirp": {
                             "version": "0.5.3",
                             "bundled": true,
-                            "optional": true,
                             "requires": {
                                 "minimist": "^1.2.5"
                             }
@@ -3184,8 +3185,7 @@
                         },
                         "npm-normalize-package-bin": {
                             "version": "1.0.1",
-                            "bundled": true,
-                            "optional": true
+                            "bundled": true
                         },
                         "npm-packlist": {
                             "version": "1.4.8",
@@ -3210,8 +3210,7 @@
                         },
                         "number-is-nan": {
                             "version": "1.0.1",
-                            "bundled": true,
-                            "optional": true
+                            "bundled": true
                         },
                         "object-assign": {
                             "version": "4.1.1",
@@ -3221,7 +3220,6 @@
                         "once": {
                             "version": "1.4.0",
                             "bundled": true,
-                            "optional": true,
                             "requires": {
                                 "wrappy": "1"
                             }
@@ -3290,8 +3288,7 @@
                         },
                         "safe-buffer": {
                             "version": "5.1.2",
-                            "bundled": true,
-                            "optional": true
+                            "bundled": true
                         },
                         "safer-buffer": {
                             "version": "2.1.2",
@@ -3321,7 +3318,6 @@
                         "string-width": {
                             "version": "1.0.2",
                             "bundled": true,
-                            "optional": true,
                             "requires": {
                                 "code-point-at": "^1.0.0",
                                 "is-fullwidth-code-point": "^1.0.0",
@@ -3339,7 +3335,6 @@
                         "strip-ansi": {
                             "version": "3.0.1",
                             "bundled": true,
-                            "optional": true,
                             "requires": {
                                 "ansi-regex": "^2.0.0"
                             }
@@ -3378,13 +3373,11 @@
                         },
                         "wrappy": {
                             "version": "1.0.2",
-                            "bundled": true,
-                            "optional": true
+                            "bundled": true
                         },
                         "yallist": {
                             "version": "3.1.1",
-                            "bundled": true,
-                            "optional": true
+                            "bundled": true
                         }
                     }
                 }
diff --git a/package.json b/package.json
index 79c33bd99..683f181c3 100644
--- a/package.json
+++ b/package.json
@@ -118,6 +118,7 @@
         "@hig/flyout": "^1.2.1",
         "@hig/theme-context": "^2.1.3",
         "@hig/theme-data": "^2.16.1",
+        "@types/google-maps": "^3.2.2",
         "@types/webscopeio__react-textarea-autocomplete": "^4.6.1",
         "@webscopeio/react-textarea-autocomplete": "^4.7.0",
         "adm-zip": "^0.4.16",
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 0fc58c39d..d5b7f07b5 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -13,28 +13,23 @@ import { Cast, NumCast, StrCast } from "../../fields/Types";
 import { AudioField, ImageField, PdfField, VideoField, WebField, YoutubeField } from "../../fields/URLField";
 import { MessageStore } from "../../server/Message";
 import { OmitKeys, Utils } from "../../Utils";
+import { YoutubeBox } from "../apis/youtube/YoutubeBox";
 import { DocServer } from "../DocServer";
+import { DocumentManager } from "../util/DocumentManager";
 import { dropActionType } from "../util/DragManager";
+import { DirectoryImportBox } from "../util/Import & Export/DirectoryImportBox";
 import { LinkManager } from "../util/LinkManager";
 import { Scripting } from "../util/Scripting";
 import { UndoManager } from "../util/UndoManager";
-import { DocumentType } from "./DocumentTypes";
 import { CollectionDockingView } from "../views/collections/CollectionDockingView";
 import { CollectionView, CollectionViewType } from "../views/collections/CollectionView";
 import { ContextMenu } from "../views/ContextMenu";
 import { ContextMenuProps } from "../views/ContextMenuItem";
-import { ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, InkingStroke } from "../views/InkingStroke";
+import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, InkingStroke } from "../views/InkingStroke";
 import { AudioBox } from "../views/nodes/AudioBox";
 import { ColorBox } from "../views/nodes/ColorBox";
 import { ComparisonBox } from "../views/nodes/ComparisonBox";
 import { DocHolderBox } from "../views/nodes/DocHolderBox";
-import { InkingStroke, ActiveInkColor, ActiveInkWidth, ActiveInkBezierApprox, ActiveFillColor, ActiveArrowStart, ActiveArrowEnd, ActiveDash } from "../views/InkingStroke";
-import { InkField } from "../../fields/InkField";
-import { RichTextField } from "../../fields/RichTextField";
-import { extname } from "path";
-import { MessageStore } from "../../server/Message";
-import { ContextMenuProps } from "../views/ContextMenuItem";
-import { ContextMenu } from "../views/ContextMenu";
 import { FontIconBox } from "../views/nodes/FontIconBox";
 import { FormattedTextBox } from "../views/nodes/formattedText/FormattedTextBox";
 import { ImageBox } from "../views/nodes/ImageBox";
@@ -52,9 +47,7 @@ import { WebBox } from "../views/nodes/WebBox";
 import { PresElementBox } from "../views/presentationview/PresElementBox";
 import { RecommendationsBox } from "../views/RecommendationsBox";
 import { DashWebRTCVideo } from "../views/webcam/DashWebRTCVideo";
-import { YoutubeBox } from "../apis/youtube/YoutubeBox";
-import { DocumentManager } from "../util/DocumentManager";
-import { DirectoryImportBox } from "../util/Import & Export/DirectoryImportBox";
+import { DocumentType } from "./DocumentTypes";
 const path = require('path');
 
 export interface DocumentOptions {
diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts
index 438f3eb9b..0aec81ab0 100644
--- a/src/client/util/LinkManager.ts
+++ b/src/client/util/LinkManager.ts
@@ -72,7 +72,7 @@ export class LinkManager {
         });
         DocListCast(anchor[Doc.LayoutFieldKey(anchor) + "-annotations"]).map(anno => {
             related.push(...LinkManager.Instance.getAllRelatedLinks(anno));
-        })
+        });
         return related;
     }
 
diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index fcaea0f0b..c05ca33fb 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -271,7 +271,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
         const considerPush = isText && this.considerGoogleDocsPush;
         return <div className="documentButtonBar">
             <div className="documentButtonBar-button">
-                <DocumentLinksButton View={this.view0!} AlwaysOn={true} />
+                <DocumentLinksButton View={this.view0} AlwaysOn={true} />
             </div>
             <div className="documentButtonBar-button">
                 {this.templateButton}
diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss
index c8347165d..5948ada88 100644
--- a/src/client/views/DocumentDecorations.scss
+++ b/src/client/views/DocumentDecorations.scss
@@ -193,10 +193,10 @@ $linkGap : 3px;
 .documentDecorations-iconifyButton {
     opacity: 1;
     grid-column-start: 4;
-    grid-column-end: 6;
+    grid-column-end: 5;
     pointer-events: all;
     text-align: center;
-    left: -20px;
+    left: -25px;
     top: -2px;
     cursor: pointer;
     position: absolute;
@@ -207,10 +207,13 @@ $linkGap : 3px;
 .documentDecorations-closeButton {
     opacity: 1;
     grid-column-start: 4;
-    grid-column-end: 6;
+    grid-column-end: 5;
     pointer-events: all;
     text-align: center;
     cursor: pointer;
+    width: 15px;
+    margin-left: -8px;
+    margin-top: auto;
 }
 
 .documentDecorations-minimizeButton {
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 6e8eed287..f5454216a 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -603,9 +603,10 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
             }}>
                 {maximizeIcon}
                 {titleArea}
-                <div className="documentDecorations-iconifyButton" title={`${seldoc.finalLayoutKey.includes("icon") ? "De" : ""}Iconify Document`} onPointerDown={this.onIconifyDown}>
-                    {"_"}
-                </div>
+                {SelectionManager.SelectedDocuments().length !== 1 || seldoc.Document.type === DocumentType.INK ? (null) :
+                    <div className="documentDecorations-iconifyButton" title={`${seldoc.finalLayoutKey.includes("icon") ? "De" : ""}Iconify Document`} onPointerDown={this.onIconifyDown}>
+                        {"_"}
+                    </div>}
                 <div className="documentDecorations-closeButton" title="Open Document in Tab" onPointerDown={this.onMaximizeDown}>
                     {SelectionManager.SelectedDocuments().length === 1 ? DocumentDecorations.DocumentIcon(StrCast(seldoc.props.Document.layout, "...")) : "..."}
                 </div>
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 8f139e39e..a2bb9700e 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -742,7 +742,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         this.props.contextMenuItems?.().forEach(item =>
             cm.addItem({ description: item.label, event: () => item.script.script.run({ this: this.layoutDoc, self: this.rootDoc }), icon: "sticky-note" }));
 
-        let options = cm.findByDescription("Options...");
+        const options = cm.findByDescription("Options...");
         const optionItems: ContextMenuProps[] = options && "subitems" in options ? options.subitems : [];
         const templateDoc = Cast(this.props.Document[StrCast(this.props.Document.layoutKey)], Doc, null);
         templateDoc && optionItems.push({ description: "Open Template   ", event: () => this.props.addDocTab(templateDoc, "onRight"), icon: "eye" });
diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx
index 126dc240a..92b443d3b 100644
--- a/src/client/views/nodes/LinkDocPreview.tsx
+++ b/src/client/views/nodes/LinkDocPreview.tsx
@@ -25,8 +25,8 @@ export class LinkDocPreview extends React.Component<Props> {
     @observable _targetDoc: Opt<Doc>;
     @observable _toolTipText = "";
 
-    componentDidUpdate() { this.updatePreview() }
-    componentDidMount() { this.updatePreview() }
+    componentDidUpdate() { this.updatePreview(); }
+    componentDidMount() { this.updatePreview(); }
     async updatePreview() {
         const linkDoc = this.props.linkDoc;
         const linkSrc = this.props.linkSrc;
diff --git a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
index 0abaf3df6..1bbcb9fa8 100644
--- a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
+++ b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
@@ -175,7 +175,7 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, props: any
         }
     });
 
-    var backspace = chainCommands(deleteSelection, joinBackward, selectNodeBackward);
+    // backspace = chainCommands(deleteSelection, joinBackward, selectNodeBackward);
     bind("Backspace", (state: EditorState<S>, dispatch: (tx: Transaction<Schema<any, any>>) => void) => {
         if (!deleteSelection(state, (tx: Transaction<Schema<any, any>>) => {
             dispatch(updateBullets(tx, schema));
diff --git a/src/fields/List.ts b/src/fields/List.ts
index fdabea365..a9da75abb 100644
--- a/src/fields/List.ts
+++ b/src/fields/List.ts
@@ -291,9 +291,10 @@ class ListImpl<T extends Field> extends ObjectField {
         this.___fields = value;
         for (const key in value) {
             const field = value[key];
-            if (!(field instanceof ObjectField)) continue;
-            (field as ObjectField)[Parent] = this[Self];
-            (field as ObjectField)[OnUpdate] = updateFunction(this[Self], key, field, this[SelfProxy]);
+            if (field instanceof ObjectField) {
+                field[Parent] = this[Self];
+                field[OnUpdate] = updateFunction(this[Self], key, field, this[SelfProxy]);
+            }
         }
     }
 
diff --git a/src/fields/ObjectField.ts b/src/fields/ObjectField.ts
index 9aa1c9b04..92b2cfa60 100644
--- a/src/fields/ObjectField.ts
+++ b/src/fields/ObjectField.ts
@@ -3,8 +3,8 @@ import { OnUpdate, Parent, Copy, ToScriptString, ToString } from "./FieldSymbols
 import { Scripting } from "../client/util/Scripting";
 
 export abstract class ObjectField {
-    protected [OnUpdate](diff?: any) { }
-    private [Parent]?: RefField | ObjectField;
+    public [OnUpdate](diff?: any) { }
+    public [Parent]?: RefField | ObjectField;
     abstract [Copy](): ObjectField;
 
     abstract [ToScriptString](): string;
diff --git a/src/fields/Schema.ts b/src/fields/Schema.ts
index 72bce283d..98ef3e087 100644
--- a/src/fields/Schema.ts
+++ b/src/fields/Schema.ts
@@ -65,9 +65,8 @@ export function makeInterface<T extends Interface[]>(...schemas: T): InterfaceFu
         return obj;
     };
     return function (doc?: Doc | Doc[]) {
-        doc = doc || new Doc;
-        if (doc instanceof Doc) {
-            return fn(doc);
+        if (doc instanceof Doc || doc === undefined) {
+            return fn(doc || new Doc);
         } else {
             return doc.map(fn);
         }
-- 
cgit v1.2.3-70-g09d2


From f2bbf54077432852b2a078e70d675bada8b54961 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Sat, 27 Jun 2020 19:47:32 -0400
Subject: cleaned up  a bunch of stuff related to TreeViews and link dots. 
 added option for link dots to show up in tree views.  changed titles in tree
 views to be truncatable.  fixed edting of treeview titles when an alias is in
 the tree view as well.

---
 src/client/documents/Documents.ts                  |   1 +
 src/client/util/CurrentUserUtils.ts                |  19 +-
 src/client/util/DragManager.ts                     |   6 +-
 src/client/views/DocumentDecorations.tsx           |   2 +-
 src/client/views/EditableView.tsx                  |  60 ++--
 .../views/collections/CollectionTreeView.scss      |   3 +-
 .../views/collections/CollectionTreeView.tsx       | 391 +++++++++++----------
 .../CollectionFreeFormLinksView.tsx                |   5 +-
 src/client/views/nodes/DocumentView.scss           |  11 +
 src/client/views/nodes/DocumentView.tsx            |  44 +--
 src/client/views/nodes/LinkAnchorBox.tsx           |  10 +-
 src/client/views/search/SearchItem.tsx             |   2 +-
 12 files changed, 287 insertions(+), 267 deletions(-)

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index d5b7f07b5..b175c018e 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -168,6 +168,7 @@ export interface DocumentOptions {
     treeViewOpen?: boolean; // whether this document is expanded in a tree view
     treeViewExpandedView?: string; // which field/thing is displayed when this item is opened in tree view
     treeViewChecked?: ScriptField; // script to call when a tree view checkbox is checked
+    treeViewTruncateTitleWidth?: number;
     limitHeight?: number; // maximum height for newly created (eg, from pasting) text documents
     // [key: string]: Opt<Field>;
     pointerHack?: boolean; // for buttons, allows onClick handler to fire onPointerDown
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 9a4c95571..2b0b2c738 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -306,21 +306,17 @@ export class CurrentUserUtils {
     // setup templates for different document types when they are iconified from Document Decorations
     static setupDefaultIconTemplates(doc: Doc) {
         if (doc["template-icon-view"] === undefined) {
-            const iconView = Docs.Create.TextDocument("", {
-                title: "icon", _width: 150, _height: 30, isTemplateDoc: true, onDoubleClick: ScriptField.MakeScript("deiconifyView(self)")
+            const iconView = Docs.Create.LabelDocument({
+                title: "icon", textTransform: "unset", letterSpacing: "unset", layout: LabelBox.LayoutString("title"), _backgroundColor: "dimGray",
+                _width: 150, _height: 70, _xPadding: 10, _yPadding: 10, isTemplateDoc: true, onDoubleClick: ScriptField.MakeScript("deiconifyView(self)")
             });
-            Doc.GetProto(iconView).icon = new RichTextField('{"doc":{"type":"doc","content":[{"type":"paragraph","attrs":{"align":null,"color":null,"id":null,"indent":null,"inset":null,"lineSpacing":null,"paddingBottom":null,"paddingTop":null},"content":[{"type":"dashField","attrs":{"fieldKey":"title","docid":""}}]}]},"selection":{"type":"text","anchor":2,"head":2},"storedMarks":[]}', "");
+            //  Docs.Create.TextDocument("", {
+            //     title: "icon", _width: 150, _height: 30, isTemplateDoc: true, onDoubleClick: ScriptField.MakeScript("deiconifyView(self)")
+            // });
+            // Doc.GetProto(iconView).icon = new RichTextField('{"doc":{"type":"doc","content":[{"type":"paragraph","attrs":{"align":null,"color":null,"id":null,"indent":null,"inset":null,"lineSpacing":null,"paddingBottom":null,"paddingTop":null},"content":[{"type":"dashField","attrs":{"fieldKey":"title","docid":""}}]}]},"selection":{"type":"text","anchor":2,"head":2},"storedMarks":[]}', "");
             iconView.isTemplateDoc = makeTemplate(iconView);
             doc["template-icon-view"] = new PrefetchProxy(iconView);
         }
-        if (doc["template-icon-view-pdf"] === undefined) {
-            const iconPdfView = Docs.Create.LabelDocument({
-                title: "icon_" + DocumentType.PDF, textTransform: "unset", letterSpacing: "unset", layout: LabelBox.LayoutString("title"), _backgroundColor: "dimGray",
-                _width: 150, _height: 70, _xPadding: 10, _yPadding: 10, isTemplateDoc: true, onDoubleClick: ScriptField.MakeScript("deiconifyView(self)")
-            });
-            iconPdfView.isTemplateDoc = makeTemplate(iconPdfView, true, "icon_" + DocumentType.PDF);
-            doc["template-icon-view-pdf"] = new PrefetchProxy(iconPdfView);
-        }
         if (doc["template-icon-view-rtf"] === undefined) {
             const iconRtfView = Docs.Create.LabelDocument({
                 title: "icon_" + DocumentType.RTF, textTransform: "unset", letterSpacing: "unset", layout: LabelBox.LayoutString("text"),
@@ -605,6 +601,7 @@ export class CurrentUserUtils {
         if (doc["tabs-button-library"] === undefined) {
             const libraryStack = new PrefetchProxy(Docs.Create.TreeDocument([workspaces, documents, recentlyClosed, doc], {
                 title: "Library", _xMargin: 5, _yMargin: 5, _gridGap: 5, forceActive: true, childDropAction: "alias",
+                treeViewTruncateTitleWidth: 150,
                 lockedPosition: true, boxShadow: "0 0", dontRegisterChildViews: true, targetDropAction: "same"
             })) as any as Doc;
             doc["tabs-button-library"] = new PrefetchProxy(Docs.Create.ButtonDocument({
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index 417ddf989..0db3963b2 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -19,8 +19,6 @@ export function SetupDrag(
     docFunc: () => Doc | Promise<Doc> | undefined,
     moveFunc?: DragManager.MoveFunction,
     dropAction?: dropActionType,
-    treeViewId?: string,
-    dontHideOnDrop?: boolean,
     dragStarted?: () => void
 ) {
     const onRowMove = async (e: PointerEvent) => {
@@ -34,8 +32,6 @@ export function SetupDrag(
             const dragData = new DragManager.DocumentDragData([doc]);
             dragData.dropAction = dropAction;
             dragData.moveDocument = moveFunc;
-            dragData.treeViewId = treeViewId;
-            dragData.dontHideOnDrop = dontHideOnDrop;
             DragManager.StartDocumentDrag([_reference.current!], dragData, e.x, e.y);
             dragStarted?.();
         }
@@ -128,7 +124,7 @@ export namespace DragManager {
         draggedDocuments: Doc[];
         droppedDocuments: Doc[];
         dragDivName?: string;
-        treeViewId?: string;
+        treeViewDoc?: Doc;
         dontHideOnDrop?: boolean;
         offset: number[];
         dropAction: dropActionType;
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index f5454216a..6e28ad654 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -77,7 +77,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
     get Bounds(): { x: number, y: number, b: number, r: number } {
         return SelectionManager.SelectedDocuments().reduce((bounds, documentView) => {
             if (documentView.props.renderDepth === 0 ||
-                documentView.props.treeViewId ||
+                documentView.props.treeViewDoc ||
                 Doc.AreProtosEqual(documentView.props.Document, Doc.UserDoc())) {
                 return bounds;
             }
diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx
index ee3ce1cf3..bab3a1634 100644
--- a/src/client/views/EditableView.tsx
+++ b/src/client/views/EditableView.tsx
@@ -31,6 +31,7 @@ export interface EditableProps {
     fontStyle?: string;
     fontSize?: number;
     height?: number | "auto";
+    sizeToContent?: boolean;
     maxHeight?: number;
     display?: string;
     autosuggestProps?: {
@@ -60,13 +61,11 @@ export interface EditableProps {
  */
 @observer
 export class EditableView extends React.Component<EditableProps> {
-    public static loadId = "";
     @observable _editing: boolean = false;
 
     constructor(props: EditableProps) {
         super(props);
         this._editing = this.props.editing ? true : false;
-        EditableView.loadId = "";
     }
 
     // @action
@@ -150,37 +149,44 @@ export class EditableView extends React.Component<EditableProps> {
     @action
     setIsFocused = (value: boolean) => {
         const wasFocused = this._editing;
-        this._editing = value;
+        //this._editing = value;
         return wasFocused !== this._editing;
     }
 
     _ref = React.createRef<HTMLDivElement>();
+    renderEditor() {
+        return this.props.autosuggestProps
+            ? <Autosuggest
+                {...this.props.autosuggestProps.autosuggestProps}
+                inputProps={{
+                    className: "editableView-input",
+                    onKeyDown: this.onKeyDown,
+                    autoFocus: true,
+                    onBlur: e => this.finalizeEdit(e.currentTarget.value, false, true),
+                    onPointerDown: this.stopPropagation,
+                    onClick: this.stopPropagation,
+                    onPointerUp: this.stopPropagation,
+                    value: this.props.autosuggestProps.value,
+                    onChange: this.props.autosuggestProps.onChange
+                }}
+            />
+            : <input className="editableView-input"
+                defaultValue={this.props.GetValue()}
+                onKeyDown={this.onKeyDown}
+                autoFocus={true}
+                onBlur={e => this.finalizeEdit(e.currentTarget.value, false, true)}
+                onPointerDown={this.stopPropagation} onClick={this.stopPropagation} onPointerUp={this.stopPropagation}
+                style={{ display: this.props.display, fontSize: this.props.fontSize, minWidth: 20 }}
+                placeholder={this.props.placeholder}
+            />;
+    }
     render() {
         if (this._editing && this.props.GetValue() !== undefined) {
-            return this.props.autosuggestProps
-                ? <Autosuggest
-                    {...this.props.autosuggestProps.autosuggestProps}
-                    inputProps={{
-                        className: "editableView-input",
-                        onKeyDown: this.onKeyDown,
-                        autoFocus: true,
-                        onBlur: e => this.finalizeEdit(e.currentTarget.value, false, true),
-                        onPointerDown: this.stopPropagation,
-                        onClick: this.stopPropagation,
-                        onPointerUp: this.stopPropagation,
-                        value: this.props.autosuggestProps.value,
-                        onChange: this.props.autosuggestProps.onChange
-                    }}
-                />
-                : <input className="editableView-input"
-                    defaultValue={this.props.GetValue()}
-                    onKeyDown={this.onKeyDown}
-                    autoFocus={true}
-                    onBlur={e => this.finalizeEdit(e.currentTarget.value, false, true)}
-                    onPointerDown={this.stopPropagation} onClick={this.stopPropagation} onPointerUp={this.stopPropagation}
-                    style={{ display: this.props.display, fontSize: this.props.fontSize }}
-                    placeholder={this.props.placeholder}
-                />;
+            return this.props.sizeToContent ?
+                <div style={{ display: "grid", minWidth: 100 }}>
+                    <div style={{ display: "inline-block", position: "relative", height: 0, width: "100%", overflow: "hidden" }}>{this.props.GetValue()}</div>
+                    {this.renderEditor()}
+                </div> : this.renderEditor();
         } else {
             this.props.autosuggestProps?.resetValue();
             return (this.props.contents instanceof ObjectField ? (null) :
diff --git a/src/client/views/collections/CollectionTreeView.scss b/src/client/views/collections/CollectionTreeView.scss
index 2aac81146..50f0534bd 100644
--- a/src/client/views/collections/CollectionTreeView.scss
+++ b/src/client/views/collections/CollectionTreeView.scss
@@ -82,8 +82,6 @@
     text-overflow: ellipsis;
     white-space: pre-wrap;
     min-width: 10px;
-    // width:100%;//width: max-content;
-
 }
 
 .treeViewItem-openRight {
@@ -100,6 +98,7 @@
     border-left: dashed 1px #00000042;
 }
 
+.treeViewItem-header-editing,
 .treeViewItem-header {
     border: transparent 1px solid;
     display: flex;
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index 68dc0ced2..7ffecdc26 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -13,7 +13,7 @@ import { Docs, DocUtils } from '../../documents/Documents';
 import { DocumentType } from "../../documents/DocumentTypes";
 import { DocumentManager } from '../../util/DocumentManager';
 import { SnappingManager } from '../../util/SnappingManager';
-import { DragManager, dropActionType, SetupDrag } from "../../util/DragManager";
+import { DragManager, dropActionType } from "../../util/DragManager";
 import { Scripting } from '../../util/Scripting';
 import { SelectionManager } from '../../util/SelectionManager';
 import { Transform } from '../../util/Transform';
@@ -55,7 +55,7 @@ export interface TreeViewProps {
     ScreenToLocalTransform: () => Transform;
     backgroundColor?: (doc: Doc) => string | undefined;
     outerXf: () => { translateX: number, translateY: number };
-    treeViewId: Doc;
+    treeViewDoc: Doc;
     parentKey: string;
     active: (outsideReaction?: boolean) => boolean;
     treeViewHideHeaderFields: () => boolean;
@@ -76,31 +76,32 @@ export interface TreeViewProps {
  * treeViewExpandedView : name of field whose contents are being displayed as the document's subtree
  */
 class TreeView extends React.Component<TreeViewProps> {
-    static _editTitleScript: ScriptField | undefined;
+    private _editTitleScript: ScriptField | undefined;
     private _header?: React.RefObject<HTMLDivElement> = React.createRef();
     private _treedropDisposer?: DragManager.DragDropDisposer;
     private _dref = React.createRef<HTMLDivElement>();
     private _tref = React.createRef<HTMLDivElement>();
     private _docRef = React.createRef<DocumentView>();
+    private _uniqueId = Utils.GenerateGuid();
+    private _editMaxWidth: number | string = 0;
 
+    get doc() { return this.props.document; }
     get noviceMode() { return BoolCast(Doc.UserDoc().noviceMode, false); }
-    get displayName() { return "TreeView(" + this.props.document.title + ")"; }  // this makes mobx trace() statements more descriptive
-    get defaultExpandedView() { return this.childDocs ? this.fieldKey : StrCast(this.props.document.defaultExpandedView, this.noviceMode ? "layout" : "fields"); }
+    get displayName() { return "TreeView(" + this.doc.title + ")"; }  // this makes mobx trace() statements more descriptive
+    get defaultExpandedView() { return this.childDocs ? this.fieldKey : StrCast(this.doc.defaultExpandedView, this.noviceMode ? "layout" : "fields"); }
     @observable _overrideTreeViewOpen = false; // override of the treeViewOpen field allowing the display state to be independent of the document's state
-    set treeViewOpen(c: boolean) { if (this.props.treeViewPreventOpen) this._overrideTreeViewOpen = c; else this.props.document.treeViewOpen = this._overrideTreeViewOpen = c; }
-    @computed get treeViewOpen() { return (!this.props.treeViewPreventOpen && !this.props.document.treeViewPreventOpen && BoolCast(this.props.document.treeViewOpen)) || this._overrideTreeViewOpen; }
-    @computed get treeViewExpandedView() { return StrCast(this.props.document.treeViewExpandedView, this.defaultExpandedView); }
+    set treeViewOpen(c: boolean) { if (this.props.treeViewPreventOpen) this._overrideTreeViewOpen = c; else this.doc.treeViewOpen = this._overrideTreeViewOpen = c; }
+    @computed get treeViewOpen() { return (!this.props.treeViewPreventOpen && !this.doc.treeViewPreventOpen && BoolCast(this.doc.treeViewOpen)) || this._overrideTreeViewOpen; }
+    @computed get treeViewExpandedView() { return StrCast(this.doc.treeViewExpandedView, this.defaultExpandedView); }
     @computed get MAX_EMBED_HEIGHT() { return NumCast(this.props.containingCollection.maxEmbedHeight, 200); }
-    @computed get dataDoc() { return this.props.document[DataSym]; }
-    @computed get fieldKey() {
-        const splits = StrCast(Doc.LayoutField(this.props.document)).split("fieldKey={\'");
-        return splits.length > 1 ? splits[1].split("\'")[0] : "data";
-    }
+    @computed get dataDoc() { return this.doc[DataSym]; }
+    @computed get layoutDoc() { return Doc.Layout(this.doc); }
+    @computed get fieldKey() { const splits = StrCast(Doc.LayoutField(this.doc)).split("fieldKey={\'"); return splits.length > 1 ? splits[1].split("\'")[0] : "data"; }
     childDocList(field: string) {
-        const layout = Doc.LayoutField(this.props.document) instanceof Doc ? Doc.LayoutField(this.props.document) as Doc : undefined;
+        const layout = Doc.LayoutField(this.doc) instanceof Doc ? Doc.LayoutField(this.doc) as Doc : undefined;
         return ((this.props.dataDoc ? DocListCast(this.props.dataDoc[field]) : undefined) || // if there's a data doc for an expanded template, use it's data field
             (layout ? Cast(layout[field], listSpec(Doc)) : undefined) || // else if there's a layout doc, display it's fields
-            Cast(this.props.document[field], listSpec(Doc))) as Doc[]; // otherwise use the document's data field
+            Cast(this.doc[field], listSpec(Doc))) as Doc[]; // otherwise use the document's data field
     }
     @computed get childDocs() { return this.childDocList(this.fieldKey); }
     @computed get childLinks() { return this.childDocList("links"); }
@@ -110,21 +111,27 @@ class TreeView extends React.Component<TreeViewProps> {
     }
 
     @undoBatch openRight = () => this.props.addDocTab(this.props.document, "onRight", this.props.libraryPath);
+    @undoBatch openRight = () => this.props.addDocTab(this.doc, "onRight", this.props.libraryPath);
     @undoBatch move = (doc: Doc | Doc[], target: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => {
-        return this.props.document !== target && this.props.deleteDoc(doc) && addDoc(doc);
+        return this.doc !== target && this.props.deleteDoc(doc) && addDoc(doc);
     }
     @undoBatch @action remove = (doc: Doc | Doc[], key: string) => {
-        return (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) =>
-            flg && Doc.RemoveDocFromList(this.dataDoc, key, doc), true);
+        return (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && Doc.RemoveDocFromList(this.dataDoc, key, doc), true);
     }
     @undoBatch @action removeDoc = (doc: Doc | Doc[]) => {
         return (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) =>
             flg && Doc.RemoveDocFromList(this.props.containingCollection, Doc.LayoutFieldKey(this.props.containingCollection), doc), true);
     }
 
+    constructor(props: any) {
+        super(props);
+        this._editTitleScript = ScriptField.MakeFunction(`setInPlace(self, 'editTitle', '${this._uniqueId}')`);
+        if (Doc.GetT(this.doc, "editTitle", "string", true) === "*") Doc.SetInPlace(this.doc, "editTitle", this._uniqueId, false);
+    }
+
     protected createTreeDropTarget = (ele: HTMLDivElement) => {
         this._treedropDisposer?.();
-        ele && (this._treedropDisposer = DragManager.MakeDropTarget(ele, this.treeDrop.bind(this), undefined, this.preTreeDrop.bind(this)), this.props.document);
+        ele && (this._treedropDisposer = DragManager.MakeDropTarget(ele, this.treeDrop.bind(this), undefined, this.preTreeDrop.bind(this)), this.doc);
     }
 
     onPointerEnter = (e: React.PointerEvent): void => {
@@ -136,7 +143,9 @@ class TreeView extends React.Component<TreeViewProps> {
     }
     onPointerLeave = (e: React.PointerEvent): void => {
         Doc.UnBrushDoc(this.dataDoc);
-        this._header!.current!.className = "treeViewItem-header";
+        if (this._header?.current?.className !== "treeViewItem-header-editing") {
+            this._header!.current!.className = "treeViewItem-header";
+        }
         document.removeEventListener("pointermove", this.onDragMove, true);
     }
     onDragMove = (e: PointerEvent): void => {
@@ -144,7 +153,7 @@ class TreeView extends React.Component<TreeViewProps> {
         const pt = [e.clientX, e.clientY];
         const rect = this._header!.current!.getBoundingClientRect();
         const before = pt[1] < rect.top + rect.height / 2;
-        const inside = pt[0] > Math.min(rect.left + 75, rect.left + rect.width * .75) || (!before && this.treeViewOpen && DocListCast(this.dataDoc[this.fieldKey]).length);
+        const inside = pt[0] > Math.min(rect.left + 75, rect.left + rect.width * .75) || (!before && this.treeViewOpen && this.childDocList.length);
         this._header!.current!.className = "treeViewItem-header";
         if (inside) this._header!.current!.className += " treeViewItem-header-inside";
         else if (before) this._header!.current!.className += " treeViewItem-header-above";
@@ -155,42 +164,38 @@ class TreeView extends React.Component<TreeViewProps> {
     editableView = (key: string, style?: string) => (<EditableView
         oneLine={true}
         display={"inline-block"}
-        editing={true /*this.dataDoc[Id] === EditableView.loadId*/}
-        contents={StrCast(this.props.document[key])}
+        editing={true}
+        contents={StrCast(this.doc[key])}
         height={12}
+        sizeToContent={true}
         fontStyle={style}
         fontSize={12}
-        GetValue={() => StrCast(this.props.document[key])}
+        GetValue={() => StrCast(this.doc[key])}
         SetValue={undoBatch((value: string) => {
-            Doc.SetInPlace(this.props.document, key, value, false) || true;
-            Doc.SetInPlace(this.props.document, "editTitle", undefined, false);
+            Doc.SetInPlace(this.doc, key, value, false) || true;
+            Doc.SetInPlace(this.doc, "editTitle", undefined, false);
         })}
         OnFillDown={undoBatch((value: string) => {
-            Doc.SetInPlace(this.props.document, key, value, false);
+            Doc.SetInPlace(this.doc, key, value, false);
             const doc = Docs.Create.FreeformDocument([], { title: "-", x: 0, y: 0, _width: 100, _height: 25, _LODdisable: true, templates: new List<string>([Templates.Title.Layout]) });
-            Doc.SetInPlace(this.props.document, "editTitle", undefined, false);
-            Doc.SetInPlace(doc, "editTitle", true, false);
+            Doc.SetInPlace(this.doc, "editTitle", undefined, false);
+            Doc.SetInPlace(doc, "editTitle", "*", false);
             return this.props.addDocument(doc);
         })}
         onClick={() => {
             SelectionManager.DeselectAll();
-            Doc.UserDoc().activeSelection = new List([this.props.document]);
+            Doc.UserDoc().activeSelection = new List([this.doc]);
             return false;
         }}
         OnTab={undoBatch((shift?: boolean) => {
-            EditableView.loadId = this.dataDoc[Id];
             shift ? this.props.outdentDocument?.() : this.props.indentDocument?.();
-            setTimeout(() => {  // unsetting/setting brushing for this doc will recreate & refocus this editableView after all other treeview changes have been made to the Dom (which may remove focus from this document).
-                Doc.UnBrushDoc(this.props.document);
-                Doc.BrushDoc(this.props.document);
-                EditableView.loadId = "";
-            }, 0);
+            setTimeout(() => Doc.SetInPlace(this.doc, "editTitle", "*", false), 0);
         })}
     />)
 
     preTreeDrop = (e: Event, de: DragManager.DropEvent, targetAction: dropActionType) => {
         const dragData = de.complete.docDragData;
-        dragData && (dragData.dropAction = this.props.treeViewId[Id] === dragData.treeViewId ? "same" : dragData.dropAction);
+        dragData && (dragData.dropAction = this.props.treeViewDoc === dragData.treeViewDoc ? "same" : dragData.dropAction);
     }
 
     @undoBatch
@@ -198,18 +203,18 @@ class TreeView extends React.Component<TreeViewProps> {
         const pt = [de.x, de.y];
         const rect = this._header!.current!.getBoundingClientRect();
         const before = pt[1] < rect.top + rect.height / 2;
-        const inside = pt[0] > Math.min(rect.left + 75, rect.left + rect.width * .75) || (!before && this.treeViewOpen && DocListCast(this.dataDoc[this.fieldKey]).length);
+        const inside = pt[0] > Math.min(rect.left + 75, rect.left + rect.width * .75) || (!before && this.treeViewOpen && this.childDocList.length);
         const complete = de.complete;
         if (complete.linkDragData) {
             const sourceDoc = complete.linkDragData.linkSourceDocument;
-            const destDoc = this.props.document;
+            const destDoc = this.doc;
             DocUtils.MakeLink({ doc: sourceDoc }, { doc: destDoc }, "tree link");
             e.stopPropagation();
         }
         const docDragData = complete.docDragData;
         if (docDragData) {
             e.stopPropagation();
-            if (docDragData.draggedDocuments[0] === this.props.document) return true;
+            if (docDragData.draggedDocuments[0] === this.doc) return true;
             const parentAddDoc = (doc: Doc | Doc[]) => this.props.addDocument(doc, undefined, before);
             let addDoc = parentAddDoc;
             if (inside) {
@@ -222,35 +227,28 @@ class TreeView extends React.Component<TreeViewProps> {
         return false;
     }
 
-    docTransform = () => {
-        const { scale, translateX, translateY } = Utils.GetScreenTransform(this._dref.current!);
-        const outerXf = this.props.outerXf();
-        const offset = this.props.ScreenToLocalTransform().transformDirection((outerXf.translateX - translateX), outerXf.translateY - translateY);
-        const finalXf = this.props.ScreenToLocalTransform().translate(offset[0], offset[1]);
-
-        return finalXf;
-    }
-    getTransform = () => {
-        const { scale, translateX, translateY } = Utils.GetScreenTransform(this._tref.current!);
+    refTransform = (ref: HTMLDivElement) => {
+        const { scale, translateX, translateY } = Utils.GetScreenTransform(ref);
         const outerXf = this.props.outerXf();
         const offset = this.props.ScreenToLocalTransform().transformDirection(outerXf.translateX - translateX, outerXf.translateY - translateY);
-        const finalXf = this.props.ScreenToLocalTransform().translate(offset[0], offset[1]);
-        return finalXf;
+        return this.props.ScreenToLocalTransform().translate(offset[0], offset[1]);
     }
+    docTransform = () => this.refTransform(this._dref.current!)
+    getTransform = () => this.refTransform(this._tref.current!);
     docWidth = () => {
-        const layoutDoc = Doc.Layout(this.props.document);
+        const layoutDoc = this.layoutDoc;
         const aspect = NumCast(layoutDoc._nativeHeight, layoutDoc._fitWidth ? 0 : layoutDoc[HeightSym]()) / NumCast(layoutDoc._nativeWidth, layoutDoc._fitWidth ? 1 : layoutDoc[WidthSym]());
         if (aspect) return Math.min(layoutDoc[WidthSym](), Math.min(this.MAX_EMBED_HEIGHT / aspect, this.props.panelWidth() - 20));
         return NumCast(layoutDoc._nativeWidth) ? Math.min(layoutDoc[WidthSym](), this.props.panelWidth() - 20) : this.props.panelWidth() - 20;
     }
     docHeight = () => {
-        const layoutDoc = Doc.Layout(this.props.document);
+        const layoutDoc = this.layoutDoc;
         const bounds = this.boundsOfCollectionDocument;
         return Math.max(70, Math.min(this.MAX_EMBED_HEIGHT, (() => {
             const aspect = NumCast(layoutDoc._nativeHeight, layoutDoc._fitWidth ? 0 : layoutDoc[HeightSym]()) / NumCast(layoutDoc._nativeWidth, layoutDoc._fitWidth ? 1 : layoutDoc[WidthSym]());
             if (aspect) return this.docWidth() * aspect;
             if (bounds) return this.docWidth() * (bounds.b - bounds.y) / (bounds.r - bounds.x);
-            return layoutDoc._fitWidth ? (!this.props.document._nativeHeight ? NumCast(this.props.containingCollection._height) :
+            return layoutDoc._fitWidth ? (!this.doc._nativeHeight ? NumCast(this.props.containingCollection._height) :
                 Math.min(this.docWidth() * NumCast(layoutDoc.scrollHeight, NumCast(layoutDoc._nativeHeight)) / NumCast(layoutDoc._nativeWidth,
                     NumCast(this.props.containingCollection._height)))) :
                 NumCast(layoutDoc._height) ? NumCast(layoutDoc._height) : 50;
@@ -259,7 +257,7 @@ class TreeView extends React.Component<TreeViewProps> {
 
     @computed get expandedField() {
         const ids: { [key: string]: string } = {};
-        const doc = this.props.document;
+        const doc = this.doc;
         doc && Object.keys(doc).forEach(key => !(key in ids) && doc[key] !== ComputedField.undefined && (ids[key] = key));
 
         const rows: JSX.Element[] = [];
@@ -273,13 +271,12 @@ class TreeView extends React.Component<TreeViewProps> {
                 const addDoc = (doc: Doc | Doc[], addBefore?: Doc, before?: boolean) => (doc instanceof Doc ? [doc] : doc).reduce(
                     (flg, doc) => flg && Doc.AddDocToList(this.dataDoc, key, doc, addBefore, before, false, true), true);
                 contentElement = TreeView.GetChildElements(contents instanceof Doc ? [contents] :
-                    DocListCast(contents), this.props.treeViewId, doc, undefined, key, this.props.containingCollection, this.props.prevSibling, addDoc, remDoc, this.move,
+                    DocListCast(contents), this.props.treeViewDoc, doc, undefined, key, this.props.containingCollection, this.props.prevSibling, addDoc, remDoc, this.move,
                     this.props.dropAction, this.props.addDocTab, this.props.pinToPres, this.props.backgroundColor, this.props.ScreenToLocalTransform, this.props.outerXf, this.props.active,
                     this.props.panelWidth, this.props.ChromeHeight, this.props.renderDepth, this.props.treeViewHideHeaderFields, this.props.treeViewPreventOpen,
                     [...this.props.renderedIds, doc[Id]], this.props.libraryPath, this.props.onCheckedClick, this.props.onChildClick, this.props.ignoreFields);
             } else {
-                contentElement = <EditableView
-                    key="editableView"
+                contentElement = <EditableView key="editableView"
                     contents={contents !== undefined ? Field.toString(contents as Field) : "null"}
                     height={13}
                     fontSize={12}
@@ -307,8 +304,8 @@ class TreeView extends React.Component<TreeViewProps> {
         return rows;
     }
 
-    rtfWidth = () => Math.min(Doc.Layout(this.props.document)?.[WidthSym](), this.props.panelWidth() - 20);
-    rtfHeight = () => this.rtfWidth() < Doc.Layout(this.props.document)?.[WidthSym]() ? Math.min(Doc.Layout(this.props.document)?.[HeightSym](), this.MAX_EMBED_HEIGHT) : this.MAX_EMBED_HEIGHT;
+    rtfWidth = () => Math.min(this.layoutDoc?.[WidthSym](), this.props.panelWidth() - 20);
+    rtfHeight = () => this.rtfWidth() < this.layoutDoc?.[WidthSym]() ? Math.min(this.layoutDoc?.[HeightSym](), this.MAX_EMBED_HEIGHT) : this.MAX_EMBED_HEIGHT;
 
     @computed get renderContent() {
         TraceMobx();
@@ -320,32 +317,32 @@ class TreeView extends React.Component<TreeViewProps> {
             const docs = expandKey === "links" ? this.childLinks : this.childDocs;
             const sortKey = `${this.fieldKey}-sortAscending`;
             return <ul key={expandKey + "more"} onClick={(e) => {
-                this.props.document[sortKey] = (this.props.document[sortKey] ? false : (this.props.document[sortKey] === false ? undefined : true));
+                this.doc[sortKey] = (this.doc[sortKey] ? false : (this.doc[sortKey] === false ? undefined : true));
                 e.stopPropagation();
             }}>
                 {!docs ? (null) :
-                    TreeView.GetChildElements(docs, this.props.treeViewId, Doc.Layout(this.props.document),
+                    TreeView.GetChildElements(docs, this.props.treeViewDoc, this.layoutDoc,
                         this.dataDoc, expandKey, this.props.containingCollection, this.props.prevSibling, addDoc, remDoc, this.move,
-                        StrCast(this.props.document.childDropAction, this.props.dropAction) as dropActionType, this.props.addDocTab, this.props.pinToPres, this.props.backgroundColor, this.props.ScreenToLocalTransform,
+                        StrCast(this.doc.childDropAction, this.props.dropAction) as dropActionType, this.props.addDocTab, this.props.pinToPres, this.props.backgroundColor, this.props.ScreenToLocalTransform,
                         this.props.outerXf, this.props.active, this.props.panelWidth, this.props.ChromeHeight, this.props.renderDepth, this.props.treeViewHideHeaderFields, this.props.treeViewPreventOpen,
-                        [...this.props.renderedIds, this.props.document[Id]], this.props.libraryPath, this.props.onCheckedClick, this.props.onChildClick, this.props.ignoreFields)}
+                        [...this.props.renderedIds, this.doc[Id]], this.props.libraryPath, this.props.onCheckedClick, this.props.onChildClick, this.props.ignoreFields)}
             </ul >;
         } else if (this.treeViewExpandedView === "fields") {
-            return <ul><div ref={this._dref} style={{ display: "inline-block" }} key={this.props.document[Id] + this.props.document.title}>
+            return <ul><div ref={this._dref} style={{ display: "inline-block" }} key={this.doc[Id] + this.doc.title}>
                 {this.expandedField}
             </div></ul>;
         } else {
-            const layoutDoc = Doc.Layout(this.props.document);
+            const layoutDoc = this.layoutDoc;
             const panelHeight = layoutDoc.type === DocumentType.RTF ? this.rtfHeight : this.docHeight;
             const panelWidth = layoutDoc.type === DocumentType.RTF ? this.rtfWidth : this.docWidth;
-            return <div ref={this._dref} style={{ display: "inline-block", height: panelHeight() }} key={this.props.document[Id]}>
+            return <div ref={this._dref} style={{ display: "inline-block", height: panelHeight() }} key={this.doc[Id]}>
                 <ContentFittingDocumentView
                     Document={layoutDoc}
                     DataDoc={this.dataDoc}
                     LibraryPath={emptyPath}
                     renderDepth={this.props.renderDepth + 1}
                     rootSelected={returnTrue}
-                    treeViewId={this.props.treeViewId[Id]}
+                    treeViewDoc={undefined}
                     backgroundColor={this.props.backgroundColor}
                     fitToBox={this.boundsOfCollectionDocument !== undefined}
                     FreezeDimensions={true}
@@ -372,17 +369,17 @@ class TreeView extends React.Component<TreeViewProps> {
         }
     }
 
-    get onCheckedClick() { return this.props.onCheckedClick || ScriptCast(this.props.document.onCheckedClick); }
+    get onCheckedClick() { return this.props.onCheckedClick || ScriptCast(this.doc.onCheckedClick); }
 
     @action
     bulletClick = (e: React.MouseEvent) => {
-        if (this.onCheckedClick && this.props.document.type !== DocumentType.COL) {
+        if (this.onCheckedClick && this.doc.type !== DocumentType.COL) {
             // this.props.document.treeViewChecked = this.props.document.treeViewChecked === "check" ? "x" : this.props.document.treeViewChecked === "x" ? undefined : "check";
             this.onCheckedClick.script.run({
-                this: this.props.document.isTemplateForField && this.props.dataDoc ? this.props.dataDoc : this.props.document,
+                this: this.doc.isTemplateForField && this.props.dataDoc ? this.props.dataDoc : this.doc,
                 heading: this.props.containingCollection.title,
-                checked: this.props.document.treeViewChecked === "check" ? "x" : this.props.document.treeViewChecked === "x" ? undefined : "check",
-                containingTreeView: this.props.treeViewId,
+                checked: this.doc.treeViewChecked === "check" ? "x" : this.doc.treeViewChecked === "x" ? undefined : "check",
+                containingTreeView: this.props.treeViewDoc,
             }, console.log);
         } else {
             this.treeViewOpen = !this.treeViewOpen;
@@ -392,104 +389,110 @@ class TreeView extends React.Component<TreeViewProps> {
 
     @computed
     get renderBullet() {
-        const checked = this.props.document.type === DocumentType.COL ? undefined : this.onCheckedClick ? (this.props.document.treeViewChecked ? this.props.document.treeViewChecked : "unchecked") : undefined;
+        const checked = this.doc.type === DocumentType.COL ? undefined : this.onCheckedClick ? (this.doc.treeViewChecked ?? "unchecked") : undefined;
         return <div className="bullet"
             title={this.childDocs?.length ? `click to see ${this.childDocs?.length} items` : "view fields"}
             onClick={this.bulletClick}
-            style={{ color: StrCast(this.props.document.color, checked === "unchecked" ? "white" : "inherit"), opacity: checked === "unchecked" ? undefined : 0.4 }}>
+            style={{ color: StrCast(this.doc.color, checked === "unchecked" ? "white" : "inherit"), opacity: checked === "unchecked" ? undefined : 0.4 }}>
             {<FontAwesomeIcon icon={checked === "check" ? "check" : (checked === "x" ? "times" : checked === "unchecked" ? "square" : !this.treeViewOpen ? (this.childDocs ? "caret-square-right" : "caret-right") : (this.childDocs ? "caret-square-down" : "caret-down"))} />}
         </div>;
     }
 
     showContextMenu = (e: React.MouseEvent) => {
         this._docRef.current?.ContentDiv && simulateMouseClick(this._docRef.current.ContentDiv, e.clientX, e.clientY + 30, e.screenX, e.screenY + 30);
-        e.stopPropagation();
     }
     focusOnDoc = (doc: Doc) => DocumentManager.Instance.getFirstDocumentView(doc)?.props.focus(doc, true);
-    contextMenuItems = () => {
-        const focusScript = ScriptField.MakeFunction(`DocFocus(self)`);
-        return [{ script: focusScript!, label: "Focus" }];
-    }
+    contextMenuItems = () => [{ script: ScriptField.MakeFunction(`DocFocus(self)`)!, label: "Focus" }]
+    truncateTitleWidth = () => NumCast(this.props.treeViewDoc.treeViewTruncateTitleWidth, 0);
+    showTitleEdit = () => ["*", this._uniqueId].includes(Doc.GetT(this.doc, "editTitle", "string", true) || "")
     /**
      * Renders the EditableView title element for placement into the tree.
      */
     @computed
     get renderTitle() {
         TraceMobx();
-        (!TreeView._editTitleScript) && (TreeView._editTitleScript = ScriptField.MakeFunction("setInPlace(self, 'editTitle', true)"));
-        const headerElements = (
+        const headerElements = this.props.treeViewHideHeaderFields() ? (null) :
             <>
-                <FontAwesomeIcon icon="cog" size="sm" onClick={e => this.showContextMenu(e)}></FontAwesomeIcon>
+                <FontAwesomeIcon icon="cog" size="sm" onClick={e => { this.showContextMenu(e); e.stopPropagation(); }} />
                 <span className="collectionTreeView-keyHeader" key={this.treeViewExpandedView}
                     onPointerDown={action(() => {
                         if (this.treeViewOpen) {
-                            this.props.document.treeViewExpandedView = this.treeViewExpandedView === this.fieldKey ? (Doc.UserDoc().noviceMode ? "layout" : "fields") :
-                                this.treeViewExpandedView === "fields" && Doc.Layout(this.props.document) ? "layout" :
-                                    this.treeViewExpandedView === "layout" && this.props.document.links ? "links" :
+                            this.doc.treeViewExpandedView = this.treeViewExpandedView === this.fieldKey ? (Doc.UserDoc().noviceMode ? "layout" : "fields") :
+                                this.treeViewExpandedView === "fields" && this.layoutDoc ? "layout" :
+                                    this.treeViewExpandedView === "layout" && DocListCast(this.doc.links).length ? "links" :
                                         this.childDocs ? this.fieldKey : (Doc.UserDoc().noviceMode ? "layout" : "fields");
                         }
                         this.treeViewOpen = true;
                     })}>
                     {this.treeViewExpandedView}
                 </span>
-            </>);
-        const openRight = (<div className="treeViewItem-openRight" onClick={this.openRight}>
-            <FontAwesomeIcon title="open in pane on right" icon="angle-right" size="lg" />
-        </div>);
+            </>;
+        const view = this.showTitleEdit() ? this.editableView("title") :
+            <DocumentView
+                ref={this._docRef}
+                Document={this.doc}
+                DataDoc={undefined}
+                treeViewDoc={this.props.treeViewDoc}
+                LibraryPath={this.props.libraryPath || emptyPath}
+                addDocument={undefined}
+                addDocTab={this.props.addDocTab}
+                rootSelected={returnTrue}
+                pinToPres={emptyFunction}
+                onClick={this.props.onChildClick || this._editTitleScript}
+                dropAction={this.props.dropAction}
+                moveDocument={this.move}
+                removeDocument={this.removeDoc}
+                ScreenToLocalTransform={this.getTransform}
+                ContentScaling={returnOne}
+                PanelWidth={this.truncateTitleWidth}
+                PanelHeight={returnZero}
+                NativeHeight={returnZero}
+                NativeWidth={returnZero}
+                contextMenuItems={this.contextMenuItems}
+                renderDepth={1}
+                focus={returnTrue}
+                parentActive={returnTrue}
+                whenActiveChanged={emptyFunction}
+                bringToFront={emptyFunction}
+                dontRegisterView={BoolCast(this.props.treeViewDoc.dontRegisterChildViews)}
+                docFilters={returnEmptyFilter}
+                ContainingCollectionView={undefined}
+                ContainingCollectionDoc={this.props.containingCollection}
+            />;
         return <>
             <div className="docContainer" ref={this._tref} title="click to edit title" id={`docContainer-${this.props.parentKey}`}
                 style={{
-                    fontWeight: this.props.document.searchMatch ? "bold" : undefined,
-                    textDecoration: Doc.GetT(this.props.document, "title", "string", true) ? "underline" : undefined,
-                    outline: BoolCast(this.props.document.workspaceBrush) ? "dashed 1px #06123232" : undefined,
+                    fontWeight: this.doc.searchMatch ? "bold" : undefined,
+                    textDecoration: Doc.GetT(this.doc, "title", "string", true) ? "underline" : undefined,
+                    outline: BoolCast(this.doc.workspaceBrush) ? "dashed 1px #06123232" : undefined,
                     pointerEvents: this.props.active() || SnappingManager.GetIsDragging() ? undefined : "none"
                 }} >
-                {Doc.GetT(this.props.document, "editTitle", "boolean", true) ?
-                    this.editableView("title") :
-                    <DocumentView
-                        ref={this._docRef}
-                        Document={this.props.document}
-                        DataDoc={undefined}
-                        treeViewId={this.props.treeViewId[Id]}
-                        LibraryPath={this.props.libraryPath || emptyPath}
-                        addDocument={undefined}
-                        addDocTab={this.props.addDocTab}
-                        rootSelected={returnTrue}
-                        pinToPres={emptyFunction}
-                        onClick={this.props.onChildClick || TreeView._editTitleScript}
-                        dropAction={this.props.dropAction}
-                        moveDocument={this.move}
-                        removeDocument={this.removeDoc}
-                        ScreenToLocalTransform={this.getTransform}
-                        ContentScaling={returnOne}
-                        PanelWidth={returnZero}
-                        PanelHeight={returnZero}
-                        NativeHeight={returnZero}
-                        NativeWidth={returnZero}
-                        contextMenuItems={this.contextMenuItems}
-                        renderDepth={1}
-                        focus={returnTrue}
-                        parentActive={returnTrue}
-                        whenActiveChanged={emptyFunction}
-                        bringToFront={emptyFunction}
-                        dontRegisterView={BoolCast(this.props.treeViewId.dontRegisterChildViews)}
-                        docFilters={returnEmptyFilter}
-                        ContainingCollectionView={undefined}
-                        ContainingCollectionDoc={this.props.containingCollection}
-                    />}
+                {view}
             </div >
-            {this.props.treeViewHideHeaderFields() ? (null) : headerElements}
-            {openRight}
+            {headerElements}
+            <div className="treeViewItem-openRight" onClick={this.openRight}>
+                <FontAwesomeIcon title="open in pane on right" icon="external-link-alt" size="sm" />
+            </div>
         </>;
     }
 
     render() {
         TraceMobx();
-        const sorting = this.props.document[`${this.fieldKey}-sortAscending`];
-        //setTimeout(() => runInAction(() => untracked(() => this._overrideTreeViewOpen = this.treeViewOpen)), 0);
+        const sorting = this.doc[`${this.fieldKey}-sortAscending`];
+        if (this.showTitleEdit()) { // find  containing CollectionTreeView and set our maximum width so  the containing tree view won't have to scroll
+            let par: any = this._header?.current;
+            if (par) {
+                while (par && par.className !== "collectionTreeView-dropTarget") par = par.parentNode;
+                if (par) {
+                    const par_rect = (par as HTMLElement).getBoundingClientRect();
+                    const my_recct = this._docRef.current?.ContentDiv?.getBoundingClientRect();
+                    this._editMaxWidth = Math.max(100, par_rect.right - (my_recct?.left || 0));
+                }
+            }
+        } else this._editMaxWidth = "";
         return <div className="treeViewItem-container" ref={this.createTreeDropTarget} onPointerDown={e => this.props.active() && SelectionManager.DeselectAll()}>
             <li className="collection-child">
-                <div className="treeViewItem-header" ref={this._header} onClick={e => {
+                <div className={`treeViewItem-header` + (this._editMaxWidth ? "-editing" : "")} ref={this._header} style={{ maxWidth: this._editMaxWidth }} onClick={e => {
                     if (this.props.active(true)) {
                         e.stopPropagation();
                         e.preventDefault();
@@ -507,14 +510,14 @@ class TreeView extends React.Component<TreeViewProps> {
                     {this.renderTitle}
                 </div>
                 <div className="treeViewItem-border" style={{ borderColor: sorting === undefined ? undefined : sorting ? "crimson" : "blue" }}>
-                    {!this.treeViewOpen || this.props.renderedIds.indexOf(this.props.document[Id]) !== -1 ? (null) : this.renderContent}
+                    {!this.treeViewOpen || this.props.renderedIds.indexOf(this.doc[Id]) !== -1 ? (null) : this.renderContent}
                 </div>
             </li>
         </div>;
     }
     public static GetChildElements(
         childDocs: Doc[],
-        treeViewId: Doc,
+        treeViewDoc: Doc,
         containingCollection: Doc,
         dataDoc: Doc | undefined,
         key: string,
@@ -624,7 +627,7 @@ class TreeView extends React.Component<TreeViewProps> {
                 libraryPath={libraryPath ? [...libraryPath, containingCollection] : undefined}
                 containingCollection={containingCollection}
                 prevSibling={docs[i]}
-                treeViewId={treeViewId}
+                treeViewDoc={treeViewDoc}
                 key={child[Id]}
                 indentDocument={indent}
                 outdentDocument={outdent}
@@ -665,21 +668,22 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll
     private treedropDisposer?: DragManager.DragDropDisposer;
     private _mainEle?: HTMLDivElement;
 
-    @computed get dataDoc() { return this.props.DataDoc || this.props.Document; }
+    @computed get doc() { return this.props.Document; }
+    @computed get dataDoc() { return this.props.DataDoc || this.doc; }
 
     protected createTreeDropTarget = (ele: HTMLDivElement) => {
         this.treedropDisposer?.();
         if (this._mainEle = ele) {
-            this.treedropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.props.Document, this.onInternalPreDrop.bind(this));
+            this.treedropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.doc, this.onInternalPreDrop.bind(this));
         }
     }
 
     protected onInternalPreDrop = (e: Event, de: DragManager.DropEvent, targetAction: dropActionType) => {
         const dragData = de.complete.docDragData;
         if (dragData) {
-            if (targetAction && !dragData.draggedDocuments.some(d => d.context === this.props.Document && this.childDocs.includes(d))) {
+            if (targetAction && !dragData.draggedDocuments.some(d => d.context === this.doc && this.childDocs.includes(d))) {
                 dragData.dropAction = targetAction;
-            } else dragData.dropAction = this.props.Document[Id] === dragData?.treeViewId ? "same" : dragData.dropAction;
+            } else dragData.dropAction = this.doc === dragData?.treeViewDoc ? "same" : dragData.dropAction;
         }
     }
 
@@ -691,7 +695,7 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll
     @action
     remove = (doc: Doc | Doc[]): boolean => {
         const docs = doc instanceof Doc ? [doc] : doc;
-        const targetDataDoc = this.props.Document[DataSym];
+        const targetDataDoc = this.doc[DataSym];
         const value = DocListCast(targetDataDoc[this.props.fieldKey]);
         const result = value.filter(v => !docs.includes(v));
         if (result.length !== value.length) {
@@ -704,9 +708,9 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll
     addDoc = (doc: Doc | Doc[], relativeTo: Opt<Doc>, before?: boolean): boolean => {
         const doAddDoc = (doc: Doc | Doc[]) =>
             (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) =>
-                flg && Doc.AddDocToList(this.props.Document[DataSym], this.props.fieldKey, doc, relativeTo, before, false, false, false), true);
-        if (this.props.Document.resolvedDataDoc instanceof Promise) {
-            this.props.Document.resolvedDataDoc.then((resolved: any) => doAddDoc(doc));
+                flg && Doc.AddDocToList(this.doc[DataSym], this.props.fieldKey, doc, relativeTo, before, false, false, false), true);
+        if (this.doc.resolvedDataDoc instanceof Promise) {
+            this.doc.resolvedDataDoc.then((resolved: any) => doAddDoc(doc));
         } else {
             doAddDoc(doc);
         }
@@ -714,22 +718,23 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll
     }
     onContextMenu = (e: React.MouseEvent): void => {
         // need to test if propagation has stopped because GoldenLayout forces a parallel react hierarchy to be created for its top-level layout
-        if (!e.isPropagationStopped() && this.props.Document === Doc.UserDoc().myWorkspaces) {
+        if (!e.isPropagationStopped() && this.doc === Doc.UserDoc().myWorkspaces) {
             ContextMenu.Instance.addItem({ description: "Create Workspace", event: () => MainView.Instance.createNewWorkspace(), icon: "plus" });
-            ContextMenu.Instance.addItem({ description: "Delete Workspace", event: () => this.remove(this.props.Document), icon: "minus" });
+            ContextMenu.Instance.addItem({ description: "Delete Workspace", event: () => this.remove(this.doc), icon: "minus" });
             e.stopPropagation();
             e.preventDefault();
             ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15);
-        } else if (!e.isPropagationStopped() && this.props.Document === Doc.UserDoc().myRecentlyClosed) {
+        } else if (!e.isPropagationStopped() && this.doc === Doc.UserDoc().myRecentlyClosed) {
             ContextMenu.Instance.addItem({ description: "Clear All", event: () => Doc.UserDoc().myRecentlyClosed = new List<Doc>(), icon: "plus" });
             e.stopPropagation();
             e.preventDefault();
             ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15);
         } else {
             const layoutItems: ContextMenuProps[] = [];
-            layoutItems.push({ description: (this.props.Document.treeViewPreventOpen ? "Persist" : "Abandon") + "Treeview State", event: () => this.props.Document.treeViewPreventOpen = !this.props.Document.treeViewPreventOpen, icon: "paint-brush" });
-            layoutItems.push({ description: (this.props.Document.treeViewHideHeaderFields ? "Show" : "Hide") + " Header Fields", event: () => this.props.Document.treeViewHideHeaderFields = !this.props.Document.treeViewHideHeaderFields, icon: "paint-brush" });
-            layoutItems.push({ description: (this.props.Document.treeViewHideTitle ? "Show" : "Hide") + " Title", event: () => this.props.Document.treeViewHideTitle = !this.props.Document.treeViewHideTitle, icon: "paint-brush" });
+            layoutItems.push({ description: (this.doc.treeViewPreventOpen ? "Persist" : "Abandon") + "Treeview State", event: () => this.doc.treeViewPreventOpen = !this.doc.treeViewPreventOpen, icon: "paint-brush" });
+            layoutItems.push({ description: (this.doc.treeViewHideHeaderFields ? "Show" : "Hide") + " Header Fields", event: () => this.doc.treeViewHideHeaderFields = !this.doc.treeViewHideHeaderFields, icon: "paint-brush" });
+            layoutItems.push({ description: (this.doc.treeViewHideTitle ? "Show" : "Hide") + " Title", event: () => this.doc.treeViewHideTitle = !this.doc.treeViewHideTitle, icon: "paint-brush" });
+            layoutItems.push({ description: (this.doc.treeViewHideLinkLines ? "Show" : "Hide") + " Link Lines", event: () => this.doc.treeViewHideLinkLines = !this.doc.treeViewHideLinkLines, icon: "paint-brush" });
             ContextMenu.Instance.addItem({ description: "Options...", subitems: layoutItems, icon: "eye" });
         }
         !Doc.UserDoc().noviceMode && ContextMenu.Instance.addItem({
@@ -745,7 +750,7 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll
 
                 const fallback = ImageDocument("http://cs.brown.edu/~bcz/face.gif", { _width: 400 });  // replace with desired double click target
                 let pdfContent: string;
-                DocListCast(this.dataDoc[this.props.fieldKey]).map(d => {
+                this.childDocs?.map(d => {
                     DocListCast(d.data).map((img, i) => {
                         const caption = (d.captions as any)[i];
                         if (caption) {
@@ -774,7 +779,7 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll
         const existingOnClick = ContextMenu.Instance.findByDescription("OnClick...");
         const onClicks: ContextMenuProps[] = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : [];
         onClicks.push({
-            description: "Edit onChecked Script", event: () => UndoManager.RunInBatch(() => DocUtils.makeCustomViewClicked(this.props.Document, undefined, "onCheckedClick"), "edit onCheckedClick"), icon: "edit"
+            description: "Edit onChecked Script", event: () => UndoManager.RunInBatch(() => DocUtils.makeCustomViewClicked(this.doc, undefined, "onCheckedClick"), "edit onCheckedClick"), icon: "edit"
         });
         !existingOnClick && ContextMenu.Instance.addItem({ description: "OnClick...", subitems: onClicks, icon: "hand-point-right" });
     }
@@ -784,7 +789,7 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll
     @computed get renderClearButton() {
         return <div id="toolbar" key="toolbar">
             <button className="toolbar-button round-button" title="Empty"
-                onClick={undoBatch(action(() => Doc.GetProto(this.props.Document)[this.props.fieldKey] = undefined))}>
+                onClick={undoBatch(action(() => Doc.GetProto(this.doc)[this.props.fieldKey] = undefined))}>
                 <FontAwesomeIcon icon={"trash"} size="sm" />
             </button>
         </div >;
@@ -795,51 +800,51 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll
     }
     render() {
         TraceMobx();
-        if (!(this.props.Document instanceof Doc)) return (null);
-        const dropAction = StrCast(this.props.Document.childDropAction) as dropActionType;
+        if (!(this.doc instanceof Doc)) return (null);
+        const dropAction = StrCast(this.doc.childDropAction) as dropActionType;
         const addDoc = (doc: Doc | Doc[], relativeTo?: Doc, before?: boolean) => this.addDoc(doc, relativeTo, before);
         const moveDoc = (d: Doc | Doc[], target: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => this.props.moveDocument(d, target, addDoc);
         const childDocs = this.props.overrideDocuments ? this.props.overrideDocuments : this.childDocs;
         return !childDocs ? (null) : (
-            <div className="collectionTreeView-dropTarget" id="body"
-                style={{
-                    background: this.props.backgroundColor?.(this.props.Document),
-                    paddingLeft: `${NumCast(this.props.Document._xPadding, 10)}px`,
-                    paddingRight: `${NumCast(this.props.Document._xPadding, 10)}px`,
-                    paddingTop: `${NumCast(this.props.Document._yPadding, 20)}px`,
-                    pointerEvents: !this.props.active() && !SnappingManager.GetIsDragging() ? "none" : undefined
-                }}
-                onKeyPress={this.onKeyPress}
-                onContextMenu={this.onContextMenu}
-                onWheel={(e: React.WheelEvent) => this._mainEle && this._mainEle.scrollHeight > this._mainEle.clientHeight && e.stopPropagation()}
-                onDrop={this.onTreeDrop}
-                ref={this.createTreeDropTarget}>
-                {(this.props.treeViewHideTitle || this.props.Document.treeViewHideTitle ? (null) : <EditableView
-                    contents={this.dataDoc.title}
-                    editing={false}
-                    display={"block"}
-                    maxHeight={72}
-                    height={"auto"}
-                    GetValue={() => StrCast(this.dataDoc.title)}
-                    SetValue={undoBatch((value: string) => Doc.SetInPlace(this.dataDoc, "title", value, false) || true)}
-                    OnFillDown={undoBatch((value: string) => {
-                        Doc.SetInPlace(this.dataDoc, "title", value, false);
-                        const doc = Docs.Create.FreeformDocument([], { title: "", x: 0, y: 0, _width: 100, _height: 25, _LODdisable: true, templates: new List<string>([Templates.Title.Layout]) });
-                        EditableView.loadId = doc[Id];
-                        Doc.SetInPlace(doc, "editTitle", true, false);
-                        this.addDoc(doc, childDocs.length ? childDocs[0] : undefined, true);
-                    })} />)}
-                {this.props.Document.allowClear ? this.renderClearButton : (null)}
-                <ul className="no-indent" style={{ width: "max-content" }} >
-                    {
-                        TreeView.GetChildElements(childDocs, this.props.Document, this.props.Document, this.props.DataDoc, this.props.fieldKey, this.props.ContainingCollectionDoc, undefined, addDoc, this.remove,
-                            moveDoc, dropAction, this.props.addDocTab, this.props.pinToPres, this.props.backgroundColor, this.props.ScreenToLocalTransform,
-                            this.outerXf, this.props.active, this.props.PanelWidth, this.props.ChromeHeight, this.props.renderDepth, () => this.props.treeViewHideHeaderFields || BoolCast(this.props.Document.treeViewHideHeaderFields),
-                            BoolCast(this.props.Document.treeViewPreventOpen), [], this.props.LibraryPath, this.props.onCheckedClick,
-                            this.props.onChildClick || ScriptCast(this.props.Document.onChildClick), this.props.ignoreFields)
-                    }
-                </ul>
-            </div >
+            <div className="collectionTreeView-container" onContextMenu={this.onContextMenu}>
+                <div className="collectionTreeView-dropTarget" id="body"
+                    style={{
+                        background: this.props.backgroundColor?.(this.doc),
+                        paddingLeft: `${NumCast(this.doc._xPadding, 10)}px`,
+                        paddingRight: `${NumCast(this.doc._xPadding, 10)}px`,
+                        paddingTop: `${NumCast(this.doc._yPadding, 20)}px`,
+                        pointerEvents: !this.props.active() && !SnappingManager.GetIsDragging() ? "none" : undefined
+                    }}
+                    onKeyPress={this.onKeyPress}
+                    onWheel={(e) => this._mainEle && this._mainEle.scrollHeight > this._mainEle.clientHeight && e.stopPropagation()}
+                    onDrop={this.onTreeDrop}
+                    ref={this.createTreeDropTarget}>
+                    {this.props.treeViewHideTitle || this.doc.treeViewHideTitle ? (null) : <EditableView
+                        contents={this.dataDoc.title}
+                        editing={false}
+                        display={"block"}
+                        maxHeight={72}
+                        height={"auto"}
+                        GetValue={() => StrCast(this.dataDoc.title)}
+                        SetValue={undoBatch((value: string) => Doc.SetInPlace(this.dataDoc, "title", value, false) || true)}
+                        OnFillDown={undoBatch((value: string) => {
+                            Doc.SetInPlace(this.dataDoc, "title", value, false);
+                            const doc = Docs.Create.FreeformDocument([], { title: "", x: 0, y: 0, _width: 100, _height: 25, _LODdisable: true, templates: new List<string>([Templates.Title.Layout]) });
+                            Doc.SetInPlace(doc, "editTitle", "*", false);
+                            this.addDoc(doc, childDocs.length ? childDocs[0] : undefined, true);
+                        })} />}
+                    {this.doc.allowClear ? this.renderClearButton : (null)}
+                    <ul className="no-indent" style={{ width: "max-content" }} >
+                        {
+                            TreeView.GetChildElements(childDocs, this.doc, this.doc, this.props.DataDoc, this.props.fieldKey, this.props.ContainingCollectionDoc, undefined, addDoc, this.remove,
+                                moveDoc, dropAction, this.props.addDocTab, this.props.pinToPres, this.props.backgroundColor, this.props.ScreenToLocalTransform,
+                                this.outerXf, this.props.active, this.props.PanelWidth, this.props.ChromeHeight, this.props.renderDepth, () => this.props.treeViewHideHeaderFields || BoolCast(this.doc.treeViewHideHeaderFields),
+                                BoolCast(this.doc.treeViewPreventOpen), [], this.props.LibraryPath, this.props.onCheckedClick,
+                                this.props.onChildClick || ScriptCast(this.doc.onChildClick), this.props.ignoreFields)
+                        }
+                    </ul>
+                </div >
+            </div>
         );
     }
 }
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
index ae81b4b36..1a2421bfd 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
@@ -10,6 +10,7 @@ import React = require("react");
 import { Utils, emptyFunction } from "../../../../Utils";
 import { DocumentType } from "../../../documents/DocumentTypes";
 import { SnappingManager } from "../../../util/SnappingManager";
+import { Cast } from "../../../../fields/Types";
 
 @observer
 export class CollectionFreeFormLinksView extends React.Component {
@@ -30,8 +31,8 @@ export class CollectionFreeFormLinksView extends React.Component {
             return drawnPairs;
         }, [] as { a: DocumentView, b: DocumentView, l: Doc[] }[]);
         return connections.filter(c =>
-            c.a.props.Document.type === DocumentType.LINK &&
-            c.a.props.pinToPres !== emptyFunction && c.b.props.pinToPres !== emptyFunction // bcz: this prevents links to be drawn to anchors in CollectionTree views -- this is a hack that should be fixed
+            c.a.props.Document.type === DocumentType.LINK
+            && !c.a.props.treeViewDoc?.treeViewHideLinkLines && !c.b.props.treeViewDoc?.treeViewHideLinkLines
         ).map(c => <CollectionFreeFormLinkView key={Utils.GenerateGuid()} A={c.a} B={c.b} LinkDocs={c.l} />);
     }
 
diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss
index b7726f7ba..b978f6245 100644
--- a/src/client/views/nodes/DocumentView.scss
+++ b/src/client/views/nodes/DocumentView.scss
@@ -42,6 +42,17 @@
             width:10px !important;
         }
     }
+    .documentView-treeView {
+        max-height: 1.5em;
+        text-overflow: ellipsis;
+        display: inline-block;
+        white-space: pre;
+        width: 100%;
+        overflow: hidden;
+        > .documentView-node {
+            position: absolute;
+        }
+    }
 
     .documentView-lock {
         width: 20; 
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index a2bb9700e..22917f18b 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -69,7 +69,7 @@ export interface DocumentViewProps {
     onDoubleClick?: ScriptField;
     onPointerDown?: ScriptField;
     onPointerUp?: ScriptField;
-    treeViewId?: string;
+    treeViewDoc?: Doc;
     dropAction?: dropActionType;
     dragDivName?: string;
     nudge?: (x: number, y: number) => void;
@@ -237,7 +237,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
             dragData.removeDocument = this.props.removeDocument;
             dragData.moveDocument = this.props.moveDocument;//  this.layoutDoc.onDragStart ? undefined : this.props.moveDocument;
             dragData.dragDivName = this.props.dragDivName;
-            dragData.treeViewId = this.props.treeViewId;
+            dragData.treeViewDoc = this.props.treeViewDoc;
             DragManager.StartDocumentDrag([this._mainCont.current], dragData, x, y, { hideSource: !dropAction && !this.layoutDoc.onDragStart });
         }
     }
@@ -1071,30 +1071,34 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     anchorPanelHeight = () => this.props.PanelHeight() || 1;
     @computed get anchors() {
         TraceMobx();
-        return this.props.forcedBackgroundColor?.(this.Document) === "transparent" || this.layoutDoc.presBox || this.props.dontRegisterView ? (null) : DocListCast(this.Document.links).filter(d => !d.hidden && this.isNonTemporalLink).map((d, i) =>
-            <DocumentView {...this.props} key={i + 1}
-                Document={d}
-                ContainingCollectionView={this.props.ContainingCollectionView}
-                ContainingCollectionDoc={this.props.Document} // bcz: hack this.props.Document is not a collection  Need a better prop for passing the containing document to the LinkAnchorBox
-                PanelWidth={this.anchorPanelWidth}
-                PanelHeight={this.anchorPanelHeight}
-                ContentScaling={returnOne}
-                dontRegisterView={false}
-                forcedBackgroundColor={returnTransparent}
-                removeDocument={this.hideLinkAnchor}
-                pointerEvents={false}
-                LayoutTemplate={undefined}
-                LayoutTemplateString={LinkAnchorBox.LayoutString(`anchor${Doc.LinkEndpoint(d, this.props.Document)}`)}
-            />);
+        return (this.props.treeViewDoc && this.props.LayoutTemplateString) || // render nothing for: tree view anchor dots
+            this.layoutDoc.presBox ||  // presentationbox nodes
+            this.props.dontRegisterView ? (null) : // view that are not registered
+            DocListCast(this.Document.links).filter(d => !d.hidden && this.isNonTemporalLink).map((d, i) =>
+                <DocumentView {...this.props} key={i + 1}
+                    Document={d}
+                    ContainingCollectionView={this.props.ContainingCollectionView}
+                    ContainingCollectionDoc={this.props.Document} // bcz: hack this.props.Document is not a collection  Need a better prop for passing the containing document to the LinkAnchorBox
+                    PanelWidth={this.anchorPanelWidth}
+                    PanelHeight={this.anchorPanelHeight}
+                    ContentScaling={returnOne}
+                    dontRegisterView={false}
+                    forcedBackgroundColor={returnTransparent}
+                    removeDocument={this.hideLinkAnchor}
+                    pointerEvents={false}
+                    LayoutTemplate={undefined}
+                    LayoutTemplateString={LinkAnchorBox.LayoutString(`anchor${Doc.LinkEndpoint(d, this.props.Document)}`)}
+                />);
     }
     @computed get innards() {
         TraceMobx();
-        if (!this.props.PanelWidth()) {  // this happens when the document is a tree view label
-            return <div className="documentView-linkAnchorBoxAnchor" >
+        if (this.props.treeViewDoc && !this.props.LayoutTemplateString) {  // this happens when the document is a tree view label (but not an anchor dot)
+            return <div className="documentView-treeView" style={{ maxWidth: this.props.PanelWidth() || undefined }}>
                 {StrCast(this.props.Document.title)}
                 {this.anchors}
             </div>;
         }
+
         const showTitle = StrCast(this.layoutDoc._showTitle);
         const showTitleHover = StrCast(this.layoutDoc._showTitleHover);
         const showCaption = StrCast(this.layoutDoc._showCaption);
@@ -1156,7 +1160,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     renderLock() {
         return (this.Document.isBackground !== undefined || this.isSelected(false)) &&
             ((this.Document.type === DocumentType.COL && this.Document._viewType !== CollectionViewType.Pile) || this.Document.type === DocumentType.IMG) &&
-            this.props.renderDepth > 0 && this.props.PanelWidth() > 0 ?
+            this.props.renderDepth > 0 && !this.props.treeViewDoc ?
             <div className="documentView-lock" onClick={() => this.toggleBackground(true)}>
                 <FontAwesomeIcon icon={this.Document.isBackground ? "unlock" : "lock"} style={{ color: this.Document.isBackground ? "red" : undefined }} size="lg" />
             </div>
diff --git a/src/client/views/nodes/LinkAnchorBox.tsx b/src/client/views/nodes/LinkAnchorBox.tsx
index 2bcc6168b..d4ab70200 100644
--- a/src/client/views/nodes/LinkAnchorBox.tsx
+++ b/src/client/views/nodes/LinkAnchorBox.tsx
@@ -115,8 +115,9 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps, LinkAnch
 
     render() {
         TraceMobx();
-        const x = this.props.PanelWidth() > 1 ? NumCast(this.rootDoc[this.fieldKey + "_x"], 100) : 0;
-        const y = this.props.PanelWidth() > 1 ? NumCast(this.rootDoc[this.fieldKey + "_y"], 100) : 0;
+        const small = this.props.PanelWidth() <= 1; // this happens when rendered in a treeView
+        const x = NumCast(this.rootDoc[this.fieldKey + "_x"], 100);
+        const y = NumCast(this.rootDoc[this.fieldKey + "_y"], 100);
         const c = StrCast(this.layoutDoc._backgroundColor, StrCast(this.layoutDoc.backgroundColor, StrCast(this.dataDoc.backgroundColor, "lightBlue"))); // note this is not where the typical lightBlue default color comes from.  See Documents.Create.LinkDocument()
         const anchor = this.fieldKey === "anchor1" ? "anchor2" : "anchor1";
         const anchorScale = (x === 0 || x === 100 || y === 0 || y === 100) ? 1 : .25;
@@ -131,7 +132,6 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps, LinkAnch
                 </div>}
             </div>
         );
-        const small = this.props.PanelWidth() <= 1;
         return <div className={`linkAnchorBox-cont${small ? "-small" : ""} ${this.rootDoc[Id]}`}
             onPointerLeave={action(() => LinkDocPreview.LinkInfo = undefined)}
             onPointerEnter={action(e => LinkDocPreview.LinkInfo = {
@@ -143,8 +143,8 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps, LinkAnch
             onPointerDown={this.onPointerDown} onClick={this.onClick} title={targetTitle} onContextMenu={this.specificContextMenu}
             ref={this._ref} style={{
                 background: c,
-                left: !small ? `calc(${x}% - 7.5px)` : undefined,
-                top: !small ? `calc(${y}% - 7.5px)` : undefined,
+                left: `calc(${x}% - ${small ? 2.5 : 7.5}px)`,
+                top: `calc(${y}% - ${small ? 2.5 : 7.5}px)`,
                 transform: `scale(${anchorScale / this.props.ContentScaling()})`
             }} >
             {!this._editing && !this._forceOpen ? (null) :
diff --git a/src/client/views/search/SearchItem.tsx b/src/client/views/search/SearchItem.tsx
index 74262d81a..2436bf418 100644
--- a/src/client/views/search/SearchItem.tsx
+++ b/src/client/views/search/SearchItem.tsx
@@ -85,7 +85,7 @@ export class SelectorContextMenu extends React.Component<SearchItemProps> {
                     const item = React.createRef<HTMLDivElement>();
                     return <div className="collection" key={doc.col[Id] + doc.target[Id]} ref={item}>
                         <div className="collection-item" onPointerDown={
-                            SetupDrag(item, () => doc.col, undefined, undefined, undefined, undefined, () => SearchBox.Instance.closeSearch())}>
+                            SetupDrag(item, () => doc.col, undefined, undefined, () => SearchBox.Instance.closeSearch())}>
                             <FontAwesomeIcon icon={faStickyNote} />
                         </div>
                         <a onClick={this.getOnClick(doc)}>{doc.col.title}</a>
-- 
cgit v1.2.3-70-g09d2


From 973e49dc36f208cc7f9a261cbe03e8d65d001b92 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Wed, 1 Jul 2020 01:53:42 -0400
Subject: added isPushpin.  added 'hidden' for DocumentViews to stop displaying
 them.  made pushpin's FontIconBox's.  made Esc close context menu.

---
 src/client/util/DocumentManager.ts                 |  8 +-
 src/client/views/ContextMenu.tsx                   |  2 +
 src/client/views/DocumentDecorations.tsx           | 92 +++++++++++-----------
 src/client/views/GlobalKeyHandler.ts               |  6 +-
 src/client/views/collections/CollectionSubView.tsx |  2 +-
 src/client/views/collections/CollectionView.tsx    | 12 +--
 src/client/views/nodes/DocumentView.tsx            |  3 +-
 src/client/views/nodes/FontIconBox.tsx             |  9 ++-
 8 files changed, 74 insertions(+), 60 deletions(-)

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

diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index fb5d1717e..1fa5faeb3 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -146,7 +146,7 @@ export class DocumentManager {
         };
         const docView = getFirstDocView(targetDoc, originatingDoc);
         let annotatedDoc = await Cast(targetDoc.annotationOn, Doc);
-        if (annotatedDoc) {
+        if (annotatedDoc && !linkDoc?.isPushpin) {
             const first = getFirstDocView(annotatedDoc);
             if (first) {
                 annotatedDoc = first.props.Document;
@@ -156,7 +156,11 @@ export class DocumentManager {
             }
         }
         if (docView) {  // we have a docView already and aren't forced to create a new one ... just focus on the document.  TODO move into view if necessary otherwise just highlight?
-            docView.props.focus(docView.props.Document, willZoom, undefined, focusAndFinish);
+            if (linkDoc?.isPushpin) docView.props.Document.hidden = !docView.props.Document.hidden;
+            else {
+                docView.props.Document.hidden && (docView.props.Document.hidden = undefined);
+                docView.props.focus(docView.props.Document, willZoom, undefined, focusAndFinish);
+            }
             highlight();
         } else {
             const contextDocs = docContext ? await DocListCastAsync(docContext.data) : undefined;
diff --git a/src/client/views/ContextMenu.tsx b/src/client/views/ContextMenu.tsx
index 941d7b44a..07f7b8e6d 100644
--- a/src/client/views/ContextMenu.tsx
+++ b/src/client/views/ContextMenu.tsx
@@ -155,9 +155,11 @@ export class ContextMenu extends React.Component {
 
     @action
     closeMenu = () => {
+        const wasOpen = this._display;
         this.clearItems();
         this._display = false;
         this._shouldDisplay = false;
+        return wasOpen;
     }
 
     @computed get filteredItems(): (OriginalMenuProps | string[])[] {
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 4fda10926..a45ef8862 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -600,52 +600,54 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
                 zIndex: SelectionManager.SelectedDocuments().length > 1 ? 900 : 0,
             }} onPointerDown={this.onBackgroundDown} onContextMenu={e => { e.preventDefault(); e.stopPropagation(); }} >
             </div>
-            <div className="documentDecorations-container" ref={this.setTextBar} style={{
-                width: (bounds.r - bounds.x + this._resizeBorderWidth) + "px",
-                height: (bounds.b - bounds.y + this._resizeBorderWidth + this._titleHeight) + "px",
-                left: bounds.x - this._resizeBorderWidth / 2,
-                top: bounds.y - this._resizeBorderWidth / 2 - this._titleHeight,
-            }}>
-                {maximizeIcon}
-                {titleArea}
-                {SelectionManager.SelectedDocuments().length !== 1 || seldoc.Document.type === DocumentType.INK ? (null) :
-                    <div className="documentDecorations-iconifyButton" title={`${seldoc.finalLayoutKey.includes("icon") ? "De" : ""}Iconify Document`} onPointerDown={this.onIconifyDown}>
-                        {"_"}
-                    </div>}
-                <div className="documentDecorations-closeButton" title="Open Document in Tab" onPointerDown={this.onMaximizeDown}>
-                    {SelectionManager.SelectedDocuments().length === 1 ? DocumentDecorations.DocumentIcon(StrCast(seldoc.props.Document.layout, "...")) : "..."}
+            {bounds.r - bounds.x < 15 && bounds.b - bounds.y < 15 ? (null) : <>
+                <div className="documentDecorations-container" key="container" ref={this.setTextBar} style={{
+                    width: (bounds.r - bounds.x + this._resizeBorderWidth) + "px",
+                    height: (bounds.b - bounds.y + this._resizeBorderWidth + this._titleHeight) + "px",
+                    left: bounds.x - this._resizeBorderWidth / 2,
+                    top: bounds.y - this._resizeBorderWidth / 2 - this._titleHeight,
+                }}>
+                    {maximizeIcon}
+                    {titleArea}
+                    {SelectionManager.SelectedDocuments().length !== 1 || seldoc.Document.type === DocumentType.INK ? (null) :
+                        <div className="documentDecorations-iconifyButton" title={`${seldoc.finalLayoutKey.includes("icon") ? "De" : ""}Iconify Document`} onPointerDown={this.onIconifyDown}>
+                            {"_"}
+                        </div>}
+                    <div className="documentDecorations-closeButton" title="Open Document in Tab" onPointerDown={this.onMaximizeDown}>
+                        {SelectionManager.SelectedDocuments().length === 1 ? DocumentDecorations.DocumentIcon(StrCast(seldoc.props.Document.layout, "...")) : "..."}
+                    </div>
+                    <div id="documentDecorations-rotation" className="documentDecorations-rotation"
+                        onPointerDown={this.onRotateDown}> ⟲ </div>
+                    <div id="documentDecorations-topLeftResizer" className="documentDecorations-resizer"
+                        onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
+                    <div id="documentDecorations-topResizer" className="documentDecorations-resizer"
+                        onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
+                    <div id="documentDecorations-topRightResizer" className="documentDecorations-resizer"
+                        onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
+                    <div id="documentDecorations-leftResizer" className="documentDecorations-resizer"
+                        onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
+                    <div id="documentDecorations-centerCont"></div>
+                    <div id="documentDecorations-rightResizer" className="documentDecorations-resizer"
+                        onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
+                    <div id="documentDecorations-bottomLeftResizer" className="documentDecorations-resizer"
+                        onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
+                    <div id="documentDecorations-bottomResizer" className="documentDecorations-resizer"
+                        onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
+                    <div id="documentDecorations-bottomRightResizer" className="documentDecorations-resizer"
+                        onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
+                    {seldoc.props.renderDepth <= 1 || !seldoc.props.ContainingCollectionView ? (null) :
+                        <div id="documentDecorations-levelSelector" className="documentDecorations-selector"
+                            title="tap to select containing document" onPointerDown={this.onSelectorUp} onContextMenu={e => e.preventDefault()}>
+                            <FontAwesomeIcon className="documentdecorations-times" icon={faArrowAltCircleUp} size="lg" />
+                        </div>}
+                    <div id="documentDecorations-borderRadius" className="documentDecorations-radius"
+                        onPointerDown={this.onRadiusDown} onContextMenu={(e) => e.preventDefault()}></div>
+
+                </div >
+                <div className="link-button-container" key="links" style={{ left: bounds.x - this._resizeBorderWidth / 2 + 10, top: bounds.b + this._resizeBorderWidth / 2 }}>
+                    <DocumentButtonBar views={SelectionManager.SelectedDocuments} />
                 </div>
-                <div id="documentDecorations-rotation" className="documentDecorations-rotation"
-                    onPointerDown={this.onRotateDown}> ⟲ </div>
-                <div id="documentDecorations-topLeftResizer" className="documentDecorations-resizer"
-                    onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
-                <div id="documentDecorations-topResizer" className="documentDecorations-resizer"
-                    onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
-                <div id="documentDecorations-topRightResizer" className="documentDecorations-resizer"
-                    onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
-                <div id="documentDecorations-leftResizer" className="documentDecorations-resizer"
-                    onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
-                <div id="documentDecorations-centerCont"></div>
-                <div id="documentDecorations-rightResizer" className="documentDecorations-resizer"
-                    onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
-                <div id="documentDecorations-bottomLeftResizer" className="documentDecorations-resizer"
-                    onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
-                <div id="documentDecorations-bottomResizer" className="documentDecorations-resizer"
-                    onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
-                <div id="documentDecorations-bottomRightResizer" className="documentDecorations-resizer"
-                    onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
-                {seldoc.props.renderDepth <= 1 || !seldoc.props.ContainingCollectionView ? (null) :
-                    <div id="documentDecorations-levelSelector" className="documentDecorations-selector"
-                        title="tap to select containing document" onPointerDown={this.onSelectorUp} onContextMenu={e => e.preventDefault()}>
-                        <FontAwesomeIcon className="documentdecorations-times" icon={faArrowAltCircleUp} size="lg" />
-                    </div>}
-                <div id="documentDecorations-borderRadius" className="documentDecorations-radius"
-                    onPointerDown={this.onRadiusDown} onContextMenu={(e) => e.preventDefault()}></div>
-
-            </div >
-            <div className="link-button-container" style={{ left: bounds.x - this._resizeBorderWidth / 2 + 10, top: bounds.b + this._resizeBorderWidth / 2 }}>
-                <DocumentButtonBar views={SelectionManager.SelectedDocuments} />
-            </div>
+            </>}
         </div >
         );
     }
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index e3546dece..c85849adb 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -20,6 +20,7 @@ import { MainView } from "./MainView";
 import { DocumentView } from "./nodes/DocumentView";
 import { DocumentLinksButton } from "./nodes/DocumentLinksButton";
 import PDFMenu from "./pdf/PDFMenu";
+import { ContextMenu } from "./ContextMenu";
 
 const modifiers = ["control", "meta", "shift", "alt"];
 type KeyHandler = (keycode: string, e: KeyboardEvent) => KeyControlInfo | Promise<KeyControlInfo>;
@@ -81,16 +82,17 @@ export default class KeyManager {
                 DocumentLinksButton.StartLink = undefined;
                 const main = MainView.Instance;
                 Doc.SetSelectedTool(InkTool.None);
+                var doDeselect = true;
                 if (main.isPointerDown) {
                     DragManager.AbortDrag();
                 } else {
                     if (CollectionDockingView.Instance.HasFullScreen()) {
                         CollectionDockingView.Instance.CloseFullScreen();
                     } else {
-                        SelectionManager.DeselectAll();
+                        doDeselect = !ContextMenu.Instance.closeMenu();
                     }
                 }
-                SelectionManager.DeselectAll();
+                doDeselect && SelectionManager.DeselectAll();
                 DictationManager.Controls.stop();
                 // RecommendationsBox.Instance.closeMenu();
                 GoogleAuthenticationManager.Instance.cancel();
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index d107db86b..95c4898cc 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -225,7 +225,7 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
                     const movedDocs = docDragData.droppedDocuments.filter((d, i) => docDragData.draggedDocuments[i] === d);
                     const addedDocs = docDragData.droppedDocuments.filter((d, i) => docDragData.draggedDocuments[i] !== d);
                     const res = addedDocs.length ? this.addDocument(addedDocs) : true;
-                    added = movedDocs.length ? docDragData.moveDocument(movedDocs, this.props.Document, de.embedKey || !this.props.isAnnotationOverlay ? this.addDocument : returnFalse) : res;
+                    added = movedDocs.length ? docDragData.moveDocument(movedDocs, this.props.Document, Doc.AreProtosEqual(Cast(movedDocs[0].annotationOn, Doc, null), this.props.Document) || de.embedKey || !this.props.isAnnotationOverlay ? this.addDocument : returnFalse) : res;
                 } else {
                     added = this.addDocument(docDragData.droppedDocuments);
                 }
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index dab82185a..705fbdd34 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -141,15 +141,17 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus
             } else {
                 added.map(doc => {
                     const context = Cast(doc.context, Doc, null);
-                    if (context && (context?.type === DocumentType.VID || context.type === DocumentType.WEB || context.type === DocumentType.PDF || context.type === DocumentType.IMG)) {
-                        const pushpin = Docs.Create.FreeformDocument([], {
-                            title: "pushpin", x: Cast(doc.x, "number", null), y: Cast(doc.y, "number", null),
-                            _width: 10, _height: 10, _backgroundColor: "red", isLinkButton: true, displayTimecode: Cast(doc.displayTimecode, "number", null)
+                    if (context && (context.type === DocumentType.VID || context.type === DocumentType.WEB || context.type === DocumentType.PDF || context.type === DocumentType.IMG) &&
+                        !DocListCast(doc.links).some(d => d.isPushpin)) {
+                        const pushpin = Docs.Create.FontIconDocument({
+                            icon: "map-pin", x: Cast(doc.x, "number", null), y: Cast(doc.y, "number", null), _backgroundColor: "#0000003d", color: "#ACCEF7",
+                            _width: 15, _height: 15, _xPadding: 0, isLinkButton: true, displayTimecode: Cast(doc.displayTimecode, "number", null)
                         });
                         Doc.AddDocToList(context, Doc.LayoutFieldKey(context) + "-annotations", pushpin);
-                        DocUtils.MakeLink({ doc: pushpin }, { doc: doc }, "pushpin");
+                        const pushpinLink = DocUtils.MakeLink({ doc: pushpin }, { doc: doc }, "pushpin");
                         const first = DocListCast(pushpin.links).find(d => d instanceof Doc);
                         first && (first.hidden = true);
+                        pushpinLink && (Doc.GetProto(pushpinLink).isPushpin = true);
                         doc.displayTimecode = undefined;
                     }
                     doc.context = this.props.Document;
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 21b6d8310..09eeaee36 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1169,8 +1169,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     }
 
     render() {
-        if (this.props.Document[AclSym] && this.props.Document[AclSym] === AclPrivate) return (null);
         if (!(this.props.Document instanceof Doc)) return (null);
+        if (this.props.Document[AclSym] && this.props.Document[AclSym] === AclPrivate) return (null);
+        if (this.props.Document.hidden) return (null);
         const backgroundColor = Doc.UserDoc().renderStyle === "comic" ? undefined : this.props.forcedBackgroundColor?.(this.Document) || StrCast(this.layoutDoc._backgroundColor) || StrCast(this.layoutDoc.backgroundColor) || StrCast(this.Document.backgroundColor) || this.props.backgroundColor?.(this.Document);
         const opacity = Cast(this.layoutDoc._opacity, "number", Cast(this.layoutDoc.opacity, "number", Cast(this.Document.opacity, "number", null)));
         const finalOpacity = this.props.opacity ? this.props.opacity() : opacity;
diff --git a/src/client/views/nodes/FontIconBox.tsx b/src/client/views/nodes/FontIconBox.tsx
index cf0b16c7c..5e8dd2497 100644
--- a/src/client/views/nodes/FontIconBox.tsx
+++ b/src/client/views/nodes/FontIconBox.tsx
@@ -5,7 +5,7 @@ import { createSchema, makeInterface } from '../../../fields/Schema';
 import { DocComponent } from '../DocComponent';
 import './FontIconBox.scss';
 import { FieldView, FieldViewProps } from './FieldView';
-import { StrCast, Cast } from '../../../fields/Types';
+import { StrCast, Cast, NumCast } from '../../../fields/Types';
 import { Utils } from "../../../Utils";
 import { runInAction, observable, reaction, IReactionDisposer } from 'mobx';
 import { Doc } from '../../../fields/Doc';
@@ -59,13 +59,14 @@ export class FontIconBox extends DocComponent<FieldViewProps, FontIconDocument>(
 
     render() {
         const referenceDoc = (this.layoutDoc.dragFactory instanceof Doc ? this.layoutDoc.dragFactory : this.layoutDoc);
-        const referenceLayout = Doc.Layout(referenceDoc);
+        const refLayout = Doc.Layout(referenceDoc);
         return <button className="fontIconBox-outerDiv" title={StrCast(this.layoutDoc.title)} ref={this._ref} onContextMenu={this.specificContextMenu}
             style={{
-                background: StrCast(referenceLayout.backgroundColor),
+                padding: Cast(this.layoutDoc._xPadding, "number", null),
+                background: StrCast(refLayout._backgroundColor, StrCast(refLayout.backgroundColor)),
                 boxShadow: this.layoutDoc.ischecked ? `4px 4px 12px black` : undefined
             }}>
-            <FontAwesomeIcon className="fontIconBox-icon" icon={this.dataDoc.icon as any} color={this._foregroundColor} size="sm" />
+            <FontAwesomeIcon className="fontIconBox-icon" icon={this.dataDoc.icon as any} color={StrCast(this.layoutDoc.color, this._foregroundColor)} size="sm" />
             {!this.rootDoc.label ? (null) : <div className="fontIconBox-label"> {StrCast(this.rootDoc.label).substring(0, 5)} </div>}
         </button>;
     }
-- 
cgit v1.2.3-70-g09d2

