From 5bd4d4804f9feea0fd16d8aabf040ca14cf205d1 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Thu, 23 Apr 2020 02:09:10 -0400
Subject: fixed performance of panning with something selected by making
 TemplateMenu ot render unless it has explicitly been activated.

---
 src/client/views/TemplateMenu.tsx | 9 ++++-----
 1 file changed, 4 insertions(+), 5 deletions(-)

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

diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx
index b76137f06..6894500dd 100644
--- a/src/client/views/TemplateMenu.tsx
+++ b/src/client/views/TemplateMenu.tsx
@@ -124,7 +124,7 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> {
         templateMenu.push(<OtherToggle key={"chrome"} name={"Chrome"} checked={layout._chromeStatus !== "disabled"} toggle={this.toggleChrome} />);
         templateMenu.push(<OtherToggle key={"default"} name={"Default"} checked={templateName === "layout"} toggle={this.toggleDefault} />);
         if (noteTypesDoc) {
-            addedTypes.concat(noteTypes).map(template => template.treeViewChecked = ComputedField.MakeFunction(`templateIsUsed(self)`));
+            addedTypes.concat(noteTypes).map(template => template.treeViewChecked = ComputedField.MakeFunction(`templateIsUsed(self,firstDoc)`, {}, { firstDoc }));
             this._addedKeys && Array.from(this._addedKeys).filter(key => !noteTypes.some(nt => nt.title === key)).forEach(template => templateMenu.push(
                 <OtherToggle key={template} name={template} checked={templateName === template} toggle={e => this.toggleLayout(e, template)} />));
         }
@@ -175,11 +175,10 @@ Scripting.addGlobal(function switchView(doc: Doc, template: Doc | undefined) {
     return templateTitle && DocumentView.makeCustomViewClicked(doc, Docs.Create.FreeformDocument, templateTitle, template);
 });
 
-Scripting.addGlobal(function templateIsUsed(templateDoc: Doc) {
-    const firstDoc = SelectionManager.SelectedDocuments().length ? SelectionManager.SelectedDocuments()[0].props.Document : undefined;
-    if (firstDoc) {
+Scripting.addGlobal(function templateIsUsed(templateDoc: Doc, selDoc: Doc) {
+    if (selDoc) {
         const template = StrCast(templateDoc.dragFactory ? Cast(templateDoc.dragFactory, Doc, null)?.title : templateDoc.title);
-        return StrCast(firstDoc.layoutKey) === "layout_" + template ? 'check' : 'unchecked';
+        return StrCast(selDoc.layoutKey) === "layout_" + template ? 'check' : 'unchecked';
     }
     return false;
 });
\ No newline at end of file
-- 
cgit v1.2.3-70-g09d2


From a37559a417b8a19ee56ac3b2f5dd46ef70ca53a6 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Thu, 23 Apr 2020 20:52:09 -0400
Subject: fixed loading of notes with template types.  changed noteTypes to
 ["template-notes"].  Added pass through free form layout engine.

---
 src/client/documents/Documents.ts                  |  2 +-
 src/client/views/DocumentDecorations.scss          |  2 -
 src/client/views/DocumentDecorations.tsx           | 14 ++---
 src/client/views/TemplateMenu.tsx                  | 11 ++--
 .../CollectionFreeFormLayoutEngines.tsx            | 22 ++++++++
 .../collectionFreeForm/CollectionFreeFormView.scss |  2 +
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 30 ++++++-----
 .../collections/collectionFreeForm/MarqueeView.tsx | 25 ++++++++-
 src/client/views/nodes/DocumentView.tsx            |  2 +-
 src/client/views/nodes/FormattedTextBox.tsx        |  7 ++-
 src/client/views/nodes/WebBox.tsx                  |  3 +-
 src/new_fields/Doc.ts                              | 18 ++++++-
 .../authentication/models/current_user_utils.ts    | 59 ++++++++++++++--------
 13 files changed, 140 insertions(+), 57 deletions(-)

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 5ff8f29ec..578d337d6 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -973,7 +973,7 @@ export namespace DocUtils {
     export function addDocumentCreatorMenuItems(docTextAdder: (d: Doc) => void, docAdder: (d: Doc) => void, x: number, y: number): void {
         ContextMenu.Instance.addItem({
             description: "Add Note ...",
-            subitems: DocListCast((Doc.UserDoc().noteTypes as Doc).data).map((note, i) => ({
+            subitems: DocListCast((Doc.UserDoc()["template-notes"] as Doc).data).map((note, i) => ({
                 description: ":" + StrCast(note.title),
                 event: (args: { x: number, y: number }) => {
                     const textDoc = Docs.Create.TextDocument("", {
diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss
index 353520026..28cf9fd47 100644
--- a/src/client/views/DocumentDecorations.scss
+++ b/src/client/views/DocumentDecorations.scss
@@ -143,7 +143,6 @@ $linkGap : 3px;
     pointer-events: all;
     text-align: center;
     cursor: pointer;
-    padding-right: 10px;
 }
 
 .documentDecorations-minimizeButton {
@@ -157,7 +156,6 @@ $linkGap : 3px;
     position: absolute;
     left: 0px;
     top: 0px;
-    padding-top: 5px;
     width: $MINIMIZED_ICON_SIZE;
     height: $MINIMIZED_ICON_SIZE;
     max-height: 20px;
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index a4ebde3b3..e4ceb75bd 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -165,8 +165,8 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
         return true;
     }
 
-    onCloseDown = (e: React.PointerEvent): void => {
-        setupMoveUpEvents(this, e, (e, d) => false, (e) => { }, this.onMinimizeClick);
+    onIconifyDown = (e: React.PointerEvent): void => {
+        setupMoveUpEvents(this, e, (e, d) => false, (e) => { }, this.onIconifyClick);
     }
     @undoBatch
     @action
@@ -200,7 +200,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
     }
     @undoBatch
     @action
-    onMinimizeClick = (e: PointerEvent): void => {
+    onIconifyClick = (e: PointerEvent): void => {
         if (e.button === 0) {
             const selectedDocs = SelectionManager.SelectedDocuments().map(sd => sd);
             selectedDocs.map(dv => {
@@ -408,9 +408,9 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
             <div className="documentDecorations-contextMenu" title="Show context menu" onPointerDown={this.onSettingsDown}>
                 <FontAwesomeIcon size="lg" icon="cog" />
             </div>) : (
-                <div className="documentDecorations-minimizeButton" title="Iconify" onPointerDown={this.onMaximizeDown}>
+                <div className="documentDecorations-minimizeButton" title="Iconify" onPointerDown={this.onIconifyDown}>
                     {/* Currently, this is set to be enabled if there is no ink selected. It might be interesting to think about minimizing ink if it's useful? -syip2*/}
-                    {SelectionManager.SelectedDocuments().length === 1 ? DocumentDecorations.DocumentIcon(StrCast(seldoc.props.Document.layout, "...")) : "..."}
+                    <FontAwesomeIcon className="documentdecorations-times" icon={faTimes} size="lg" />
                 </div>);
 
         const titleArea = this._edtingTitle ?
@@ -465,8 +465,8 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
             }}>
                 {maximizeIcon}
                 {titleArea}
-                <div className="documentDecorations-closeButton" title="Close Document" onPointerDown={this.onCloseDown}>
-                    <FontAwesomeIcon className="documentdecorations-times" icon={faTimes} size="lg" />
+                <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-topLeftResizer" className="documentDecorations-resizer"
                     onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx
index 6894500dd..e3c749a4d 100644
--- a/src/client/views/TemplateMenu.tsx
+++ b/src/client/views/TemplateMenu.tsx
@@ -112,8 +112,7 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> {
     render() {
         const firstDoc = this.props.docViews[0].props.Document;
         const templateName = StrCast(firstDoc.layoutKey, "layout").replace("layout_", "");
-        const noteTypesDoc = Cast(Doc.UserDoc().noteTypes, Doc, null);
-        const noteTypes = DocListCast(noteTypesDoc?.data);
+        const noteTypes = DocListCast(Cast(Doc.UserDoc()["template-notes"], Doc, null));
         const addedTypes = DocListCast(Cast(Doc.UserDoc().templateButtons, Doc, null)?.data);
         const layout = Doc.Layout(firstDoc);
         const templateMenu: Array<JSX.Element> = [];
@@ -123,11 +122,9 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> {
         templateMenu.push(<OtherToggle key={"float"} name={"Float"} checked={firstDoc.z ? true : false} toggle={this.toggleFloat} />);
         templateMenu.push(<OtherToggle key={"chrome"} name={"Chrome"} checked={layout._chromeStatus !== "disabled"} toggle={this.toggleChrome} />);
         templateMenu.push(<OtherToggle key={"default"} name={"Default"} checked={templateName === "layout"} toggle={this.toggleDefault} />);
-        if (noteTypesDoc) {
-            addedTypes.concat(noteTypes).map(template => template.treeViewChecked = ComputedField.MakeFunction(`templateIsUsed(self,firstDoc)`, {}, { firstDoc }));
-            this._addedKeys && Array.from(this._addedKeys).filter(key => !noteTypes.some(nt => nt.title === key)).forEach(template => templateMenu.push(
-                <OtherToggle key={template} name={template} checked={templateName === template} toggle={e => this.toggleLayout(e, template)} />));
-        }
+        addedTypes.concat(noteTypes).map(template => template.treeViewChecked = ComputedField.MakeFunction(`templateIsUsed(self,firstDoc)`, {}, { firstDoc }));
+        this._addedKeys && Array.from(this._addedKeys).filter(key => !noteTypes.some(nt => nt.title === key)).forEach(template => templateMenu.push(
+            <OtherToggle key={template} name={template} checked={templateName === template} toggle={e => this.toggleLayout(e, template)} />));
         return <ul className="template-list" style={{ display: "block" }}>
             <input placeholder="+ layout" ref={this._customRef} onKeyPress={this.onCustomKeypress} />
             {templateMenu}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
index 0a5ea3baf..1ec0542a3 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
@@ -75,6 +75,28 @@ interface PivotColumn {
     filters: string[];
 }
 
+export function computerPassLayout(
+    poolData: Map<string, PoolData>,
+    pivotDoc: Doc,
+    childDocs: Doc[],
+    filterDocs: Doc[],
+    childPairs: { layout: Doc, data?: Doc }[],
+    panelDim: number[],
+    viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[]
+) {
+    const docMap = new Map<Doc, ViewDefBounds>();
+    childDocs.forEach((doc, i) => {
+        docMap.set(doc, {
+            type: "doc",
+            x: NumCast(doc.x),
+            y: NumCast(doc.y),
+            width: doc[WidthSym](),
+            height: doc[HeightSym](),
+            payload: undefined
+        });
+    });
+    return normalizeResults(panelDim, 12, childPairs, docMap, poolData, viewDefsToJSX, [], 0, [], childDocs.filter(c => !filterDocs.includes(c)));
+}
 
 export function computerStarburstLayout(
     poolData: Map<string, PoolData>,
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
index a00311a9c..4e4e85e13 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
@@ -9,6 +9,8 @@
     height: 100%;
     transform-origin: left top;
     border-radius: inherit;
+    touch-action: none;
+    border-radius: inherit;
 }
 
 .collectionfreeformview-ease {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 5967f36f9..e8738b292 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -37,7 +37,7 @@ import { pageSchema } from "../../nodes/ImageBox";
 import PDFMenu from "../../pdf/PDFMenu";
 import { CollectionDockingView } from "../CollectionDockingView";
 import { CollectionSubView } from "../CollectionSubView";
-import { computePivotLayout, computeTimelineLayout, PoolData, ViewDefBounds, ViewDefResult, computerStarburstLayout } from "./CollectionFreeFormLayoutEngines";
+import { computePivotLayout, computeTimelineLayout, PoolData, ViewDefBounds, ViewDefResult, computerStarburstLayout, computerPassLayout } from "./CollectionFreeFormLayoutEngines";
 import { CollectionFreeFormRemoteCursors } from "./CollectionFreeFormRemoteCursors";
 import "./CollectionFreeFormView.scss";
 import MarqueeOptionsMenu from "./MarqueeOptionsMenu";
@@ -94,6 +94,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
     @observable.shallow _layoutElements: ViewDefResult[] = []; // shallow because some layout items (eg pivot labels) are just generated 'divs' and can't be frozen as observables
     @observable _clusterSets: (Doc[])[] = [];
 
+    @computed get fitToContentScaling() { return this.fitToContent ? NumCast(this.layoutDoc.fitToContentScaling, 1) : 1; }
     @computed get fitToContent() { return (this.props.fitToBox || this.Document._fitToBox) && !this.isAnnotationOverlay; }
     @computed get parentScaling() { return this.props.ContentScaling && this.fitToContent && !this.isAnnotationOverlay ? this.props.ContentScaling() : 1; }
     @computed get contentBounds() { return aggregateBounds(this._layoutElements.filter(e => e.bounds && !e.bounds.z).map(e => e.bounds!), NumCast(this.layoutDoc._xPadding, 10), NumCast(this.layoutDoc._yPadding, 10)); }
@@ -104,8 +105,9 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
     private easing = () => this.props.Document.panTransformType === "Ease";
     private panX = () => this.fitToContent ? (this.contentBounds.x + this.contentBounds.r) / 2 : this.Document._panX || 0;
     private panY = () => this.fitToContent ? (this.contentBounds.y + this.contentBounds.b) / 2 : this.Document._panY || 0;
-    private zoomScaling = () => (1 / this.parentScaling) * (this.fitToContent ?
-        Math.min(this.props.PanelHeight() / (this.contentBounds.b - this.contentBounds.y), this.props.PanelWidth() / (this.contentBounds.r - this.contentBounds.x)) :
+    private zoomScaling = () => (this.fitToContentScaling / this.parentScaling) * (this.fitToContent ?
+        Math.min(this.props.PanelHeight() / (this.contentBounds.b - this.contentBounds.y),
+            this.props.PanelWidth() / (this.contentBounds.r - this.contentBounds.x)) :
         this.Document.scale || 1)
 
     private centeringShiftX = () => !this.nativeWidth && !this.isAnnotationOverlay ? this.props.PanelWidth() / 2 / this.parentScaling : 0;  // shift so pan position is at center of window for non-overlay collections
@@ -971,7 +973,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
         const newPool = new Map<string, any>();
         const engine = StrCast(this.layoutDoc._layoutEngine) || this.props.layoutEngine?.();
         switch (engine) {
-            case "pass": break;
+            case "pass": return { newPool, computedElementData: this.doEngineLayout(newPool, computerPassLayout) }
             case "timeline": return { newPool, computedElementData: this.doEngineLayout(newPool, computeTimelineLayout) };
             case "pivot": return { newPool, computedElementData: this.doEngineLayout(newPool, computePivotLayout) };
             case "starburst": return { newPool, computedElementData: this.doEngineLayout(newPool, computerStarburstLayout) };
@@ -993,6 +995,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
         this._cachedPool.clear();
         Array.from(newPool.keys()).forEach(k => this._cachedPool.set(k, newPool.get(k)));
         const elements: ViewDefResult[] = computedElementData.slice();
+        const engine = this.props.layoutEngine?.() || this.props.Document._layoutEngine;
         this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).forEach(pair =>
             elements.push({
                 ele: <CollectionFreeFormDocumentView
@@ -1000,7 +1003,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
                     {...this.getChildDocumentViewProps(pair.layout, pair.data)}
                     dataProvider={this.childDataProvider}
                     LayoutDoc={this.childLayoutDocFunc}
-                    pointerEvents={this.props.layoutEngine?.() || this.props.Document._layoutEngine ? false : undefined}
+                    pointerEvents={engine && engine !== "starburst" ? false : undefined}
                     jitterRotation={NumCast(this.props.Document.jitterRotation)}
                     fitToBox={this.props.fitToBox || BoolCast(this.props.freezeChildDimensions)}
                     FreezeDimensions={BoolCast(this.props.freezeChildDimensions)}
@@ -1030,14 +1033,12 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
 
     layoutStarburst = action(() => {
         if (this.layoutDoc._layoutEngine === undefined) {
-            this.layoutDoc._layoutEngine = "starburst";
-            this.layoutDoc.overflow = "visible";
+            Doc.makeStarburst(this.layoutDoc);
         } else {
-
-            this.layoutDoc._layoutEngine = "pass";
+            this.layoutDoc._layoutEngine = undefined;
             this.layoutDoc.overflow = "hidden";
+            this.layoutDoc._fitToBox = undefined;
         }
-
     });
     layoutDocsInGrid = () => {
         UndoManager.RunInBatch(() => {
@@ -1141,7 +1142,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
     @computed get marqueeView() {
         return <MarqueeView {...this.props} nudge={this.nudge} activeDocuments={this.getActiveDocuments} selectDocuments={this.selectDocuments} addDocument={this.addDocument}
             addLiveTextDocument={this.addLiveTextBox} getContainerTransform={this.getContainerTransform} getTransform={this.getTransform} isAnnotationOverlay={this.isAnnotationOverlay}>
-            <CollectionFreeFormViewPannableContents centeringShiftX={this.centeringShiftX} centeringShiftY={this.centeringShiftY}
+            <CollectionFreeFormViewPannableContents centeringShiftX={this.centeringShiftX} centeringShiftY={this.centeringShiftY} shifted={!this.nativeHeight && !this.isAnnotationOverlay}
                 easing={this.easing} zoomScaling={this.zoomScaling} panX={this.panX} panY={this.panY}>
                 {this.children}
             </CollectionFreeFormViewPannableContents>
@@ -1216,6 +1217,7 @@ interface CollectionFreeFormViewPannableContentsProps {
     zoomScaling: () => number;
     easing: () => boolean;
     children: () => JSX.Element[];
+    shifted: boolean;
 }
 
 @observer
@@ -1227,7 +1229,11 @@ class CollectionFreeFormViewPannableContents extends React.Component<CollectionF
         const panx = -this.props.panX();
         const pany = -this.props.panY();
         const zoom = this.props.zoomScaling();
-        return <div className={freeformclass} style={{ touchAction: "none", borderRadius: "inherit", transform: `translate(${cenx}px, ${ceny}px) scale(${zoom}) translate(${panx}px, ${pany}px)` }}>
+        return <div className={freeformclass}
+            style={{
+                width: this.props.shifted ? 0 : undefined, height: this.props.shifted ? 0 : undefined,
+                transform: `translate(${cenx}px, ${ceny}px) scale(${zoom}) translate(${panx}px, ${pany}px)`
+            }}>
             {this.props.children()}
         </div>;
     }
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 454c3a5f2..2518ac629 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -20,6 +20,7 @@ import { CognitiveServices } from "../../../cognitive_services/CognitiveServices
 import { RichTextField } from "../../../../new_fields/RichTextField";
 import { CollectionView } from "../CollectionView";
 import { FormattedTextBox } from "../../nodes/FormattedTextBox";
+import { ScriptField } from "../../../../new_fields/ScriptField";
 
 interface MarqueeViewProps {
     getContainerTransform: () => Transform;
@@ -332,6 +333,25 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
         return newCollection;
     }
 
+    @action
+    pileup = (e: KeyboardEvent | React.PointerEvent | undefined) => {
+        const bounds = this.Bounds;
+        const selected = this.marqueeSelect(false);
+        selected.map(d => {
+            this.props.removeDocument(d);
+            d.x = NumCast(d.x) - bounds.left - bounds.width / 2;
+            d.y = NumCast(d.y) - bounds.top - bounds.height / 2;
+            d.displayTimecode = undefined;  // bcz: this should be automatic somehow.. along with any other properties that were logically associated with the original collection
+            return d;
+        });
+        const newCollection = this.getCollection(selected, false);
+        this.props.addDocument(newCollection);
+        this.props.selectDocuments([newCollection], []);
+        MarqueeOptionsMenu.Instance.fadeOut(true);
+        this.hideMarquee();
+        Doc.makeStarburst(newCollection);
+    }
+
     @action
     collection = (e: KeyboardEvent | React.PointerEvent | undefined) => {
         const bounds = this.Bounds;
@@ -476,7 +496,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
             this.delete();
             e.stopPropagation();
         }
-        if (e.key === "c" || e.key === "b" || e.key === "t" || e.key === "s" || e.key === "S") {
+        if (e.key === "c" || e.key === "b" || e.key === "t" || e.key === "s" || e.key === "S" || e.key === "p") {
             this._commandExecuted = true;
             e.stopPropagation();
             e.preventDefault();
@@ -490,6 +510,9 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
             if (e.key === "b") {
                 this.background(e);
             }
+            if (e.key === "p") {
+                this.pileup(e);
+            }
             this.cleanupInteractions(false);
         }
     }
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index f05366a15..6c8e44e95 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -562,7 +562,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         let docLayoutTemplate: Opt<Doc>;
         const iconViews = DocListCast(Cast(Doc.UserDoc()["template-icons"], Doc, null)?.data);
         const templBtns = DocListCast(Cast(Doc.UserDoc()["template-buttons"], Doc, null)?.data);
-        const noteTypes = DocListCast(Cast(Doc.UserDoc().noteTypes, Doc, null)?.data);
+        const noteTypes = DocListCast(Cast(Doc.UserDoc()["template-notes"], Doc, null)?.data);
         const clickFuncs = DocListCast(Cast(Doc.UserDoc().clickFuncs, Doc, null)?.data);
         const allTemplates = iconViews.concat(templBtns).concat(noteTypes).concat(clickFuncs).map(btnDoc => (btnDoc.dragFactory as Doc) || btnDoc).filter(doc => doc.isTemplateDoc);
         // bcz: this is hacky -- want to have different templates be applied depending on the "type" of a document.  but type is not reliable and there could be other types of template searches so this should be generalized
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index fd7462a10..6ed2a1b9e 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -405,7 +405,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
         !this.props.Document.rootDocument && funcs.push({
             description: "Make Template", event: () => {
                 this.props.Document.isTemplateDoc = makeTemplate(this.props.Document, true);
-                Doc.AddDocToList(Cast(Doc.UserDoc().noteTypes, Doc, null), "data", this.props.Document);
+                Doc.AddDocToList(Cast(Doc.UserDoc()["template-notes"], Doc, null), "data", this.props.Document);
             }, icon: "eye"
         });
         funcs.push({ description: "Toggle Single Line", event: () => this.props.Document._singleLine = !this.props.Document._singleLine, icon: "expand-arrows-alt" });
@@ -433,9 +433,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
         const change = cm.findByDescription("Change Perspective...");
         const changeItems: ContextMenuProps[] = change && "subitems" in change ? change.subitems : [];
 
-        const noteTypesDoc = Cast(Doc.UserDoc().noteTypes, Doc, null);
-        const noteTypes = DocListCast(noteTypesDoc?.data);
-        noteTypes.forEach(note => {
+        const noteTypesDoc = Cast(Doc.UserDoc()["template-notes"], Doc, null);
+        DocListCast(noteTypesDoc?.data).forEach(note => {
             changeItems.push({
                 description: StrCast(note.title), event: () => {
                     Doc.setNativeView(this.props.Document);
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index 66ddf64c9..4e383e468 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -317,7 +317,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum
         if (field instanceof HtmlField) {
             view = <span id="webBox-htmlSpan" dangerouslySetInnerHTML={{ __html: field.html }} />;
         } else if (field instanceof WebField) {
-            view = <iframe ref={this._iframeRef} onLoad={this.iframeLoaded} src={Utils.CorsProxy(field.url.href)} style={{ position: "absolute", width: "100%", height: "100%", top: 0 }} />;
+            const url = this.layoutDoc.UseCors ? Utils.CorsProxy(field.url.href) : field.url.href;
+            view = <iframe ref={this._iframeRef} onLoad={this.iframeLoaded} src={url} style={{ position: "absolute", width: "100%", height: "100%", top: 0 }} />;
         } else {
             view = <iframe ref={this._iframeRef} src={"https://crossorigin.me/https://cs.brown.edu"} style={{ position: "absolute", width: "100%", height: "100%", top: 0 }} />;
         }
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
index 5f790f886..1373431b8 100644
--- a/src/new_fields/Doc.ts
+++ b/src/new_fields/Doc.ts
@@ -14,7 +14,7 @@ import { PrefetchProxy, ProxyField } from "./Proxy";
 import { FieldId, RefField } from "./RefField";
 import { RichTextField } from "./RichTextField";
 import { listSpec } from "./Schema";
-import { ComputedField } from "./ScriptField";
+import { ComputedField, ScriptField } from "./ScriptField";
 import { Cast, FieldValue, NumCast, StrCast, ToConstructor, ScriptCast } from "./Types";
 import { deleteProperty, getField, getter, makeEditable, makeReadOnly, setter, updateFunction } from "./util";
 import { Docs } from "../client/documents/Documents";
@@ -913,6 +913,22 @@ export namespace Doc {
         return false;
     }
 
+    export function makeStarburst(newCollection: Doc) {
+        newCollection._layoutEngine = "starburst";
+        newCollection._fitToBox = true;
+        newCollection.overflow = "visible";
+        const script = "if (self._layoutEngine === 'starburst') {" +
+            "     self._layoutEngine = 'pass';" +
+            "     self.overflow = undefined;" +
+            "     self.fitToContentScaling=undefined;" +
+            " } else {" +
+            "    self._layoutEngine = 'starburst';" +
+            "    self.overflow = 'visible';" +
+            "    self.fitToContentScaling=10;" +
+            " };";
+        newCollection.onClick = ScriptField.MakeScript(script, { self: Doc.name });
+    }
+
 
     export async function addFieldEnumerations(doc: Opt<Doc>, enumeratedFieldKey: string, enumerations: { title: string, _backgroundColor?: string, color?: string }[]) {
         let optionsCollection = await DocServer.GetRefField(enumeratedFieldKey);
diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts
index 82efe5d4e..afde524b6 100644
--- a/src/server/authentication/models/current_user_utils.ts
+++ b/src/server/authentication/models/current_user_utils.ts
@@ -96,31 +96,50 @@ export class CurrentUserUtils {
 
     // setup the different note type skins
     static setupNoteTemplates(doc: Doc) {
-        if (doc.noteTypes === undefined) {
-            const taskStatusValues = [
-                { title: "todo", _backgroundColor: "blue", color: "white" },
-                { title: "in progress", _backgroundColor: "yellow", color: "black" },
-                { title: "completed", _backgroundColor: "green", color: "white" }
-            ];
-            const noteTemplates = [
-                Docs.Create.TextDocument("", { title: "text", style: "Note", isTemplateDoc: true, backgroundColor: "yellow" }),
-                Docs.Create.TextDocument("", { title: "text", style: "Idea", isTemplateDoc: true, backgroundColor: "pink" }),
-                Docs.Create.TextDocument("", { title: "text", style: "Topic", isTemplateDoc: true, backgroundColor: "lightBlue" }),
-                Docs.Create.TextDocument("", { title: "text", style: "Person", isTemplateDoc: true, backgroundColor: "lightGreen" }),
-                Docs.Create.TextDocument("", {
-                    title: "text", style: "Todo", isTemplateDoc: true, backgroundColor: "orange", _autoHeight: false, _height: 100, _showCaption: "caption",
-                    layout: FormattedTextBox.LayoutString("Todo"), caption: RichTextField.DashField("taskStatus")
-                })
-            ];
+        if (doc["template-note-Note"] === undefined) {
+            const noteView = Docs.Create.TextDocument("", { title: "text", style: "Note", isTemplateDoc: true, backgroundColor: "yellow" });
+            noteView.isTemplateDoc = makeTemplate(noteView, true, "Note");
+            doc["template-note-Note"] = new PrefetchProxy(noteView);
+        }
+        if (doc["template-note-Idea"] === undefined) {
+            const noteView = Docs.Create.TextDocument("", { title: "text", style: "Idea", backgroundColor: "pink" });
+            noteView.isTemplateDoc = makeTemplate(noteView, true, "Idea");
+            doc["template-note-Idea"] = new PrefetchProxy(noteView);
+        }
+        if (doc["template-note-Topic"] === undefined) {
+            const noteView = Docs.Create.TextDocument("", { title: "text", style: "Topic", backgroundColor: "lightBlue" });
+            noteView.isTemplateDoc = makeTemplate(noteView, true, "Topic");
+            doc["template-note-Topic"] = new PrefetchProxy(noteView);
+        }
+        if (doc["template-note-Todo"] === undefined) {
+            const noteView = Docs.Create.TextDocument("", {
+                title: "text", style: "Todo", backgroundColor: "orange", _autoHeight: false, _height: 100, _showCaption: "caption",
+                layout: FormattedTextBox.LayoutString("Todo"), caption: RichTextField.DashField("taskStatus")
+            });
+            noteView.isTemplateDoc = makeTemplate(noteView, true, "Todo");
+            doc["template-note-Todo"] = new PrefetchProxy(noteView);
+        }
+        const taskStatusValues = [
+            { title: "todo", _backgroundColor: "blue", color: "white" },
+            { title: "in progress", _backgroundColor: "yellow", color: "black" },
+            { title: "completed", _backgroundColor: "green", color: "white" }
+        ];
+        if (doc.fieldTypes === undefined) {
             doc.fieldTypes = Docs.Create.TreeDocument([], { title: "field enumerations" });
-            Doc.addFieldEnumerations(Doc.GetProto(noteTemplates[4]), "taskStatus", taskStatusValues);
-            doc.noteTypes = new PrefetchProxy(Docs.Create.TreeDocument(noteTemplates.map(nt => makeTemplate(nt, true, StrCast(nt.style)) ? nt : nt),
+            Doc.addFieldEnumerations(Doc.GetProto(doc["template-note-Todo"] as any as Doc), "taskStatus", taskStatusValues);
+        }
+
+        if (doc["template-notes"] === undefined) {
+            doc["template-notes"] = new PrefetchProxy(Docs.Create.TreeDocument([doc["template-note-Note"] as any as Doc,
+            doc["template-note-Idea"] as any as Doc, doc["template-note-Topic"] as any as Doc, doc["template-note-Todo"] as any as Doc],
                 { title: "Note Layouts", _height: 75 }));
         } else {
-            DocListCast(Cast(doc.noteTypes, Doc, null)?.data); // prefetch templates
+            const noteTypes = Cast(doc["template-notes"], Doc, null);
+            DocListCastAsync(noteTypes).then(list => noteTypes.data = new List<Doc>([doc["template-note-Note"] as any as Doc,
+            doc["template-note-Idea"] as any as Doc, doc["template-note-Topic"] as any as Doc, doc["template-note-Todo"] as any as Doc]));
         }
 
-        return doc.noteTypes as Doc;
+        return doc["template-notes"] as Doc;
     }
 
     // creates Note templates, and initial "user" templates
-- 
cgit v1.2.3-70-g09d2


From 08fbeaf2ebf4ee2466fa156e6e8c663d2bef7864 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Fri, 24 Apr 2020 00:06:48 -0400
Subject: moved iconify code into Doc from DocumentView.  fixed up starburst a
 bit.

---
 .../apis/google_docs/GooglePhotosClientUtils.ts    | 21 +++---
 src/client/util/RichTextSchema.tsx                 |  2 +-
 src/client/views/DocumentDecorations.tsx           | 16 +---
 src/client/views/TemplateMenu.tsx                  |  2 +-
 .../views/collections/CollectionTreeView.tsx       | 15 ++--
 .../CollectionFreeFormLayoutEngines.tsx            |  4 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 31 +++++---
 .../collections/collectionFreeForm/MarqueeView.tsx | 24 ++++--
 src/client/views/nodes/DocumentView.tsx            | 85 ++++++----------------
 src/client/views/nodes/FormattedTextBox.tsx        | 25 +++----
 src/client/views/pdf/PDFViewer.tsx                 | 43 ++++++-----
 src/new_fields/Doc.ts                              | 85 ++++++++++++++++++++--
 src/new_fields/RichTextUtils.ts                    | 32 ++++----
 13 files changed, 208 insertions(+), 177 deletions(-)

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

diff --git a/src/client/apis/google_docs/GooglePhotosClientUtils.ts b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
index 7c4137f59..8c0149a89 100644
--- a/src/client/apis/google_docs/GooglePhotosClientUtils.ts
+++ b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
@@ -1,19 +1,18 @@
-import { Utils } from "../../../Utils";
-import { ImageField } from "../../../new_fields/URLField";
-import { Cast, StrCast } from "../../../new_fields/Types";
-import { Doc, Opt, DocListCastAsync } from "../../../new_fields/Doc";
+import { AssertionError } from "assert";
+import { EditorState } from "prosemirror-state";
+import { Doc, DocListCastAsync, Opt } from "../../../new_fields/Doc";
 import { Id } from "../../../new_fields/FieldSymbols";
-import Photos = require('googlephotos');
 import { RichTextField } from "../../../new_fields/RichTextField";
 import { RichTextUtils } from "../../../new_fields/RichTextUtils";
-import { EditorState } from "prosemirror-state";
-import { FormattedTextBox } from "../../views/nodes/FormattedTextBox";
+import { Cast, StrCast } from "../../../new_fields/Types";
+import { ImageField } from "../../../new_fields/URLField";
+import { MediaItem, NewMediaItemResult } from "../../../server/apis/google/SharedTypes";
+import { Utils } from "../../../Utils";
 import { Docs, DocumentOptions } from "../../documents/Documents";
-import { NewMediaItemResult, MediaItem } from "../../../server/apis/google/SharedTypes";
-import { AssertionError } from "assert";
-import { DocumentView } from "../../views/nodes/DocumentView";
 import { Networking } from "../../Network";
+import { FormattedTextBox } from "../../views/nodes/FormattedTextBox";
 import GoogleAuthenticationManager from "../GoogleAuthenticationManager";
+import Photos = require('googlephotos');
 
 export namespace GooglePhotos {
 
@@ -340,7 +339,7 @@ export namespace GooglePhotos {
                 const url = data.url.href;
                 const target = Doc.MakeAlias(source);
                 const description = parseDescription(target, descriptionKey);
-                await DocumentView.makeCustomViewClicked(target, Docs.Create.FreeformDocument);
+                await Doc.makeCustomViewClicked(target, Docs.Create.FreeformDocument);
                 media.push({ url, description });
             }
             if (media.length) {
diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index d23962d5c..b88a7b017 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -780,7 +780,7 @@ export class DashDocView {
                     if (dashDocBase instanceof Doc) {
                         const aliasedDoc = Doc.MakeAlias(dashDocBase, docid + alias);
                         aliasedDoc.layoutKey = "layout";
-                        node.attrs.fieldKey && DocumentView.makeCustomViewClicked(aliasedDoc, Docs.Create.StackingDocument, node.attrs.fieldKey, undefined);
+                        node.attrs.fieldKey && Doc.makeCustomViewClicked(aliasedDoc, Docs.Create.StackingDocument, node.attrs.fieldKey, undefined);
                         self.doRender(aliasedDoc, removeDoc, node, view, getPos);
                     }
                 });
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index e4ceb75bd..312acd5b2 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -193,7 +193,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
             const selectedDocs = SelectionManager.SelectedDocuments();
             if (selectedDocs.length) {
                 //CollectionDockingView.Instance?.OpenFullScreen(selectedDocs[0], selectedDocs[0].props.LibraryPath);
-                CollectionDockingView.AddRightSplit(selectedDocs[0].props.Document, selectedDocs[0].props.LibraryPath);
+                CollectionDockingView.AddRightSplit(Doc.MakeAlias(selectedDocs[0].props.Document), selectedDocs[0].props.LibraryPath);
             }
         }
         SelectionManager.DeselectAll();
@@ -202,19 +202,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
     @action
     onIconifyClick = (e: PointerEvent): void => {
         if (e.button === 0) {
-            const selectedDocs = SelectionManager.SelectedDocuments().map(sd => sd);
-            selectedDocs.map(dv => {
-                const layoutKey = Cast(dv.props.Document.layoutKey, "string", null);
-                const collapse = layoutKey !== "layout_icon";
-                if (collapse) {
-                    dv.switchViews(collapse, "icon");
-                    if (layoutKey && layoutKey !== "layout") dv.props.Document.deiconifyLayout = layoutKey.replace("layout_", "");
-                } else {
-                    const deiconifyLayout = Cast(dv.props.Document.deiconifyLayout, "string", null);
-                    dv.switchViews(deiconifyLayout ? true : false, deiconifyLayout);
-                    dv.props.Document.deiconifyLayout = undefined;
-                }
-            });
+            SelectionManager.SelectedDocuments().forEach(dv => dv?.iconify());
         }
         SelectionManager.DeselectAll();
     }
diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx
index e3c749a4d..4c84a22ad 100644
--- a/src/client/views/TemplateMenu.tsx
+++ b/src/client/views/TemplateMenu.tsx
@@ -169,7 +169,7 @@ Scripting.addGlobal(function switchView(doc: Doc, template: Doc | undefined) {
         template = Cast(template.dragFactory, Doc, null);
     }
     const templateTitle = StrCast(template?.title);
-    return templateTitle && DocumentView.makeCustomViewClicked(doc, Docs.Create.FreeformDocument, templateTitle, template);
+    return templateTitle && Doc.makeCustomViewClicked(doc, Docs.Create.FreeformDocument, templateTitle, template);
 });
 
 Scripting.addGlobal(function templateIsUsed(templateDoc: Doc, selDoc: Doc) {
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index a052d045c..ca2004b79 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -3,13 +3,14 @@ import { faAngleRight, faArrowsAltH, faBell, faCamera, faCaretDown, faCaretRight
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { action, computed, observable, runInAction, untracked } from "mobx";
 import { observer } from "mobx-react";
-import { Doc, DocListCast, Field, HeightSym, WidthSym, DataSym, Opt } from '../../../new_fields/Doc';
+import { DataSym, Doc, DocListCast, Field, HeightSym, Opt, WidthSym } from '../../../new_fields/Doc';
 import { Id } from '../../../new_fields/FieldSymbols';
 import { List } from '../../../new_fields/List';
+import { RichTextField } from '../../../new_fields/RichTextField';
 import { Document, listSpec } from '../../../new_fields/Schema';
 import { ComputedField, ScriptField } from '../../../new_fields/ScriptField';
 import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../new_fields/Types';
-import { emptyFunction, emptyPath, returnFalse, Utils, returnOne, returnZero, returnTransparent, returnTrue, simulateMouseClick } from '../../../Utils';
+import { emptyFunction, emptyPath, returnFalse, returnOne, returnTrue, returnZero, simulateMouseClick, Utils } from '../../../Utils';
 import { Docs, DocUtils } from '../../documents/Documents';
 import { DocumentType } from "../../documents/DocumentTypes";
 import { DocumentManager } from '../../util/DocumentManager';
@@ -24,16 +25,14 @@ import { ContextMenuProps } from '../ContextMenuItem';
 import { EditableView } from "../EditableView";
 import { MainView } from '../MainView';
 import { ContentFittingDocumentView } from '../nodes/ContentFittingDocumentView';
+import { DocumentView } from '../nodes/DocumentView';
 import { ImageBox } from '../nodes/ImageBox';
 import { KeyValueBox } from '../nodes/KeyValueBox';
-import { ScriptBox } from '../ScriptBox';
 import { Templates } from '../Templates';
-import { CollectionSubView, SubCollectionViewProps } from "./CollectionSubView";
+import { CollectionSubView } from "./CollectionSubView";
 import "./CollectionTreeView.scss";
+import { CollectionViewType } from './CollectionView';
 import React = require("react");
-import { CollectionViewType, CollectionView } from './CollectionView';
-import { RichTextField } from '../../../new_fields/RichTextField';
-import { DocumentView } from '../nodes/DocumentView';
 
 
 export interface TreeViewProps {
@@ -768,7 +767,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(() => DocumentView.makeCustomViewClicked(this.props.Document, undefined, "onCheckedClick"), "edit onCheckedClick"), icon: "edit"
+            description: "Edit onChecked Script", event: () => UndoManager.RunInBatch(() => Doc.makeCustomViewClicked(this.props.Document, undefined, "onCheckedClick"), "edit onCheckedClick"), icon: "edit"
         });
         !existingOnClick && ContextMenu.Instance.addItem({ description: "OnClick...", subitems: onClicks, icon: "hand-point-right" });
     }
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
index 1ec0542a3..57f62347a 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
@@ -113,8 +113,8 @@ export function computerStarburstLayout(
         const deg = i / childDocs.length * Math.PI * 2;
         docMap.set(doc, {
             type: "doc",
-            x: Math.sin(deg) * burstDim[0] / 3 - NumCast(pivotDoc.starburstX),
-            y: Math.cos(deg) * burstDim[1] / 3 - NumCast(pivotDoc.starburstY),
+            x: Math.cos(deg) * (burstDim[0] / 3) - doc[WidthSym]() / 2,
+            y: Math.sin(deg) * (burstDim[1] / 3) - doc[HeightSym]() / 2,
             width: doc[WidthSym](),
             height: doc[HeightSym](),
             payload: undefined
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index e8738b292..63199ccbb 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -885,13 +885,20 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
         };
     }
 
-    addDocTab = (doc: Doc, where: string) => {
+    addDocTab = action((doc: Doc, where: string) => {
+        if (where === "inParent") {
+            const pt = this.getTransform().transformPoint(NumCast(doc.x), NumCast(doc.y));
+            doc.x = pt[0];
+            doc.y = pt[1];
+            this.props.addDocument(doc);
+            return true;
+        }
         if (where === "inPlace" && this.layoutDoc.isInPlaceContainer) {
             this.dataDoc[this.props.fieldKey] = new List<Doc>([doc]);
             return true;
         }
         return this.props.addDocTab(doc, where);
-    }
+    })
     getCalculatedPositions(params: { doc: Doc, index: number, collection: Doc, docs: Doc[], state: any }): PoolData {
         const result = this.Document.arrangeScript?.script.run(params, console.log);
         if (result?.success) {
@@ -1031,15 +1038,15 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
         super.setCursorPosition(this.getTransform().transformPoint(e.clientX, e.clientY));
     }
 
-    layoutStarburst = action(() => {
-        if (this.layoutDoc._layoutEngine === undefined) {
-            Doc.makeStarburst(this.layoutDoc);
-        } else {
-            this.layoutDoc._layoutEngine = undefined;
-            this.layoutDoc.overflow = "hidden";
-            this.layoutDoc._fitToBox = undefined;
-        }
-    });
+    promoteCollection = undoBatch(action(() => {
+        this.childDocs.forEach(doc => {
+            const scr = this.getTransform().inverse().transformPoint(NumCast(doc.x), NumCast(doc.y));
+            doc.x = scr?.[0];
+            doc.y = scr?.[1];
+            this.props.addDocTab(doc, "inParent") && this.props.removeDocument(doc);
+        })
+        this.props.ContainingCollectionView?.removeDocument(this.props.Document);
+    }));
     layoutDocsInGrid = () => {
         UndoManager.RunInBatch(() => {
             const docs = this.childLayoutPairs;
@@ -1073,7 +1080,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
         optionItems.push({ description: `${this.Document._LODdisable ? "Enable LOD" : "Disable LOD"}`, event: () => this.Document._LODdisable = !this.Document._LODdisable, icon: "table" });
         optionItems.push({ description: `${this.fitToContent ? "Unset" : "Set"} Fit To Container`, event: () => this.Document._fitToBox = !this.fitToContent, icon: !this.fitToContent ? "expand-arrows-alt" : "compress-arrows-alt" });
         optionItems.push({ description: `${this.Document.useClusters ? "Uncluster" : "Use Clusters"}`, event: () => this.updateClusters(!this.Document.useClusters), icon: "braille" });
-        optionItems.push({ description: "Arrange Starburst", event: this.layoutStarburst, icon: "table" });
+        this.props.ContainingCollectionView && optionItems.push({ description: "Promote Collection", event: this.promoteCollection, icon: "table" });
         optionItems.push({ description: "Arrange contents in grid", event: this.layoutDocsInGrid, icon: "table" });
         // layoutItems.push({ description: "Analyze Strokes", event: this.analyzeStrokes, icon: "paint-brush" });
         optionItems.push({ description: "Jitter Rotation", event: action(() => this.props.Document.jitterRotation = (this.props.Document.jitterRotation ? 0 : 10)), icon: "paint-brush" });
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 2518ac629..d8e73a820 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -335,21 +335,33 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
 
     @action
     pileup = (e: KeyboardEvent | React.PointerEvent | undefined) => {
-        const bounds = this.Bounds;
         const selected = this.marqueeSelect(false);
-        selected.map(d => {
+        SelectionManager.DeselectAll();
+
+        let w = 0, h = 0;
+        selected.forEach((d, i) => {
+            Doc.iconify(d);
+            w = Math.max(d[WidthSym](), w);
+            h = Math.max(d[HeightSym](), h);
+        });
+        selected.forEach((d, i) => {
             this.props.removeDocument(d);
-            d.x = NumCast(d.x) - bounds.left - bounds.width / 2;
-            d.y = NumCast(d.y) - bounds.top - bounds.height / 2;
+            d.x = Math.cos(Math.PI * 2 * i / selected.length) * 10 - w / 2;
+            d.y = Math.sin(Math.PI * 2 * i / selected.length) * 10 - h / 2;
             d.displayTimecode = undefined;  // bcz: this should be automatic somehow.. along with any other properties that were logically associated with the original collection
-            return d;
         });
         const newCollection = this.getCollection(selected, false);
+        newCollection.x = NumCast(newCollection.x) + NumCast(newCollection._width) / 2 - 55;
+        newCollection.y = NumCast(newCollection.y) + NumCast(newCollection._height) / 2 - 55;
+        newCollection._width = newCollection._height = 110;
+        //newCollection.borderRounding = "40px";
+        newCollection.jitterRotation = 10;
+        newCollection._backgroundColor = "yellow";
         this.props.addDocument(newCollection);
         this.props.selectDocuments([newCollection], []);
         MarqueeOptionsMenu.Instance.fadeOut(true);
         this.hideMarquee();
-        Doc.makeStarburst(newCollection);
+        Doc.pileup(newCollection);
     }
 
     @action
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 6c8e44e95..cc8773ed5 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -307,7 +307,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
                     UndoManager.RunInBatch(func, "on click");
                 } else func();
             } else if (this.Document["onClick-rawScript"] && !StrCast(Doc.LayoutField(this.layoutDoc))?.includes("ScriptingBox")) {// bcz: hack? don't edit a script if you're clicking on a scripting box itself
-                UndoManager.RunInBatch(() => DocumentView.makeCustomViewClicked(this.props.Document, undefined, "onClick"), "edit onClick");
+                UndoManager.RunInBatch(() => Doc.makeCustomViewClicked(this.props.Document, undefined, "onClick"), "edit onClick");
                 //ScriptBox.EditButtonScript("On Button Clicked ...", this.props.Document, "onClick", e.clientX, e.clientY), "on button click");
             } else if (this.Document.isLinkButton) {
                 DocListCast(this.props.Document.links).length && this.followLinkClick(e.altKey, e.ctrlKey, e.shiftKey);
@@ -403,6 +403,19 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         }
     }
 
+    public iconify() {
+        const layoutKey = Cast(this.props.Document.layoutKey, "string", null);
+        const collapse = layoutKey !== "layout_icon";
+        if (collapse) {
+            this.switchViews(collapse, "icon");
+            if (layoutKey && layoutKey !== "layout") this.props.Document.deiconifyLayout = layoutKey.replace("layout_", "");
+        } else {
+            const deiconifyLayout = Cast(this.props.Document.deiconifyLayout, "string", null);
+            this.switchViews(deiconifyLayout ? true : false, deiconifyLayout);
+            this.props.Document.deiconifyLayout = undefined;
+        }
+    }
+
     @action
     handle2PointersMove = (e: TouchEvent, me: InteractionUtils.MultiTouchEvent<TouchEvent>) => {
         const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true);
@@ -547,56 +560,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     @undoBatch
     deleteClicked = (): void => { SelectionManager.DeselectAll(); this.props.removeDocument?.(this.props.Document); }
 
-    // applies a custom template to a document.  the template is identified by it's short name (e.g, slideView not layout_slideView)
-    static makeCustomViewClicked = (doc: Doc, creator: Opt<(documents: Array<Doc>, options: DocumentOptions, id?: string) => Doc>, templateSignature: string = "custom", docLayoutTemplate?: Doc) => {
-        const batch = UndoManager.StartBatch("makeCustomViewClicked");
-        runInAction(() => {
-            doc.layoutKey = "layout_" + templateSignature;
-            if (doc[doc.layoutKey] === undefined) {
-                DocumentView.createCustomView(doc, creator, templateSignature, docLayoutTemplate);
-            }
-        });
-        batch.end();
-    }
-    static findTemplate(templateName: string, type: string, signature: string) {
-        let docLayoutTemplate: Opt<Doc>;
-        const iconViews = DocListCast(Cast(Doc.UserDoc()["template-icons"], Doc, null)?.data);
-        const templBtns = DocListCast(Cast(Doc.UserDoc()["template-buttons"], Doc, null)?.data);
-        const noteTypes = DocListCast(Cast(Doc.UserDoc()["template-notes"], Doc, null)?.data);
-        const clickFuncs = DocListCast(Cast(Doc.UserDoc().clickFuncs, Doc, null)?.data);
-        const allTemplates = iconViews.concat(templBtns).concat(noteTypes).concat(clickFuncs).map(btnDoc => (btnDoc.dragFactory as Doc) || btnDoc).filter(doc => doc.isTemplateDoc);
-        // bcz: this is hacky -- want to have different templates be applied depending on the "type" of a document.  but type is not reliable and there could be other types of template searches so this should be generalized
-        // first try to find a template that matches the specific document type (<typeName>_<templateName>).  otherwise, fallback to a general match on <templateName>
-        !docLayoutTemplate && allTemplates.forEach(tempDoc => StrCast(tempDoc.title) === templateName + "_" + type && (docLayoutTemplate = tempDoc));
-        !docLayoutTemplate && allTemplates.forEach(tempDoc => StrCast(tempDoc.title) === templateName && (docLayoutTemplate = tempDoc));
-        return docLayoutTemplate;
-    }
-    static createCustomView = (doc: Doc, creator: Opt<(documents: Array<Doc>, options: DocumentOptions, id?: string) => Doc>, templateSignature: string = "custom", docLayoutTemplate?: Doc) => {
-        const templateName = templateSignature.replace(/\(.*\)/, "");
-        docLayoutTemplate = docLayoutTemplate || DocumentView.findTemplate(templateName, StrCast(doc.type), templateSignature);
-
-        const customName = "layout_" + templateSignature;
-        const _width = NumCast(doc._width);
-        const _height = NumCast(doc._height);
-        const options = { title: "data", backgroundColor: StrCast(doc.backgroundColor), _autoHeight: true, _width, x: -_width / 2, y: - _height / 2, _showSidebar: false };
-
-        let fieldTemplate: Opt<Doc>;
-        if (doc.data instanceof RichTextField || typeof (doc.data) === "string") {
-            fieldTemplate = Docs.Create.TextDocument("", options);
-        } else if (doc.data instanceof PdfField) {
-            fieldTemplate = Docs.Create.PdfDocument("http://www.msn.com", options);
-        } else if (doc.data instanceof VideoField) {
-            fieldTemplate = Docs.Create.VideoDocument("http://www.cs.brown.edu", options);
-        } else if (doc.data instanceof AudioField) {
-            fieldTemplate = Docs.Create.AudioDocument("http://www.cs.brown.edu", options);
-        } else if (doc.data instanceof ImageField) {
-            fieldTemplate = Docs.Create.ImageDocument("http://www.cs.brown.edu", options);
-        }
-        const docTemplate = docLayoutTemplate || creator?.(fieldTemplate ? [fieldTemplate] : [], { title: customName + "(" + doc.title + ")", isTemplateDoc: true, _width: _width + 20, _height: Math.max(100, _height + 45) });
-
-        fieldTemplate && Doc.MakeMetadataFieldTemplate(fieldTemplate, docTemplate ? Doc.GetProto(docTemplate) : docTemplate);
-        docTemplate && Doc.ApplyTemplateTo(docTemplate, doc, customName, undefined);
-    }
 
     @undoBatch
     toggleLinkButtonBehavior = (): void => {
@@ -667,15 +630,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         this.Document.isLinkButton = true;
     }
 
-    @undoBatch
-    @action
-    setCustomView = (custom: boolean, layout: string): void => {
-        Doc.setNativeView(this.props.Document);
-        if (custom) {
-            DocumentView.makeCustomViewClicked(this.props.Document, Docs.Create.StackingDocument, layout, undefined);
-        }
-    }
-
     @undoBatch
     @action
     toggleBackground = (temporary: boolean): void => {
@@ -747,7 +701,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         onClicks.push({ description: this.Document.ignoreClick ? "Select" : "Do Nothing", event: () => this.Document.ignoreClick = !this.Document.ignoreClick, icon: this.Document.ignoreClick ? "unlock" : "lock" });
         onClicks.push({ description: this.Document.isLinkButton ? "Remove Follow Behavior" : "Follow Link in Place", event: this.toggleFollowInPlace, icon: "concierge-bell" });
         onClicks.push({ description: this.Document.isLinkButton || this.Document.onClick ? "Remove Click Behavior" : "Follow Link", event: this.toggleLinkButtonBehavior, icon: "concierge-bell" });
-        onClicks.push({ description: "Edit onClick Script", event: () => UndoManager.RunInBatch(() => DocumentView.makeCustomViewClicked(this.props.Document, undefined, "onClick"), "edit onClick"), icon: "edit" });
+        onClicks.push({ description: "Edit onClick Script", event: () => UndoManager.RunInBatch(() => Doc.makeCustomViewClicked(this.props.Document, undefined, "onClick"), "edit onClick"), icon: "edit" });
         !existingOnClick && cm.addItem({ description: "OnClick...", subitems: onClicks, icon: "hand-point-right" });
 
         const funcs: ContextMenuProps[] = [];
@@ -1114,7 +1068,14 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     @computed get ignorePointerEvents() {
         return this.props.pointerEvents === false || (this.Document.isBackground && !this.isSelected() && !SelectionManager.GetIsDragging()) || (this.Document.type === DocumentType.INK && InkingControl.Instance.selectedTool !== InkTool.None);
     }
-
+    @undoBatch
+    @action
+    setCustomView = (custom: boolean, layout: string): void => {
+        Doc.setNativeView(this.props.Document);
+        if (custom) {
+            Doc.makeCustomViewClicked(this.props.Document, Docs.Create.StackingDocument, layout, undefined);
+        }
+    }
     @observable _animate = 0;
     switchViews = action((custom: boolean, view: string) => {
         SelectionManager.SetIsDragging(true);
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 6ed2a1b9e..f71343176 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -13,22 +13,24 @@ import { EditorState, NodeSelection, Plugin, TextSelection, Transaction } from "
 import { ReplaceStep } from 'prosemirror-transform';
 import { EditorView } from "prosemirror-view";
 import { DateField } from '../../../new_fields/DateField';
-import { DataSym, Doc, DocListCastAsync, Field, HeightSym, Opt, WidthSym, DocListCast } from "../../../new_fields/Doc";
+import { DataSym, Doc, DocListCast, DocListCastAsync, Field, HeightSym, Opt, WidthSym } from "../../../new_fields/Doc";
 import { documentSchema } from '../../../new_fields/documentSchemas';
 import { Id } from '../../../new_fields/FieldSymbols';
 import { InkTool } from '../../../new_fields/InkField';
+import { PrefetchProxy } from '../../../new_fields/Proxy';
 import { RichTextField } from "../../../new_fields/RichTextField";
 import { RichTextUtils } from '../../../new_fields/RichTextUtils';
 import { createSchema, makeInterface } from "../../../new_fields/Schema";
-import { Cast, NumCast, StrCast, BoolCast, DateCast } from "../../../new_fields/Types";
+import { Cast, DateCast, NumCast, StrCast } from "../../../new_fields/Types";
 import { TraceMobx } from '../../../new_fields/util';
-import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, numberRange, returnOne, Utils, returnTrue, returnZero } from '../../../Utils';
+import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, numberRange, returnOne, returnZero, Utils } from '../../../Utils';
 import { GoogleApiClientUtils, Pulls, Pushes } from '../../apis/google_docs/GoogleApiClientUtils';
 import { DocServer } from "../../DocServer";
 import { Docs, DocUtils } from '../../documents/Documents';
 import { DocumentType } from '../../documents/DocumentTypes';
 import { DictationManager } from '../../util/DictationManager';
 import { DragManager } from "../../util/DragManager";
+import { makeTemplate } from '../../util/DropConverter';
 import buildKeymap from "../../util/ProsemirrorExampleTransfer";
 import RichTextMenu from '../../util/RichTextMenu';
 import { RichTextRules } from "../../util/RichTextRules";
@@ -46,9 +48,6 @@ import { FieldView, FieldViewProps } from "./FieldView";
 import "./FormattedTextBox.scss";
 import { FormattedTextBoxComment, formattedTextBoxCommentPlugin } from './FormattedTextBoxComment';
 import React = require("react");
-import { PrefetchProxy } from '../../../new_fields/Proxy';
-import { makeTemplate } from '../../util/DropConverter';
-import { DocumentView } from './DocumentView';
 
 library.add(faEdit);
 library.add(faSmile, faTextHeight, faUpload);
@@ -436,24 +435,24 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
         const noteTypesDoc = Cast(Doc.UserDoc()["template-notes"], Doc, null);
         DocListCast(noteTypesDoc?.data).forEach(note => {
             changeItems.push({
-                description: StrCast(note.title), event: () => {
+                description: StrCast(note.title), event: undoBatch(() => {
                     Doc.setNativeView(this.props.Document);
-                    DocumentView.makeCustomViewClicked(this.rootDoc, Docs.Create.TreeDocument, StrCast(note.title), note);
-                }, icon: "eye"
+                    Doc.makeCustomViewClicked(this.rootDoc, Docs.Create.TreeDocument, StrCast(note.title), note);
+                }), icon: "eye"
             });
         });
-        changeItems.push({ description: "FreeForm", event: () => DocumentView.makeCustomViewClicked(this.rootDoc, Docs.Create.FreeformDocument, "freeform"), icon: "eye" });
+        changeItems.push({ description: "FreeForm", event: undoBatch(() => Doc.makeCustomViewClicked(this.rootDoc, Docs.Create.FreeformDocument, "freeform"), "change view"), icon: "eye" });
         !change && cm.addItem({ description: "Change Perspective...", subitems: changeItems, icon: "external-link-alt" });
 
         const open = cm.findByDescription("Open New Perspective...");
         const openItems: ContextMenuProps[] = open && "subitems" in open ? open.subitems : [];
 
         openItems.push({
-            description: "FreeForm", event: () => {
+            description: "FreeForm", event: undoBatch(() => {
                 const alias = Doc.MakeAlias(this.rootDoc);
-                DocumentView.makeCustomViewClicked(alias, Docs.Create.FreeformDocument, "freeform");
+                Doc.makeCustomViewClicked(alias, Docs.Create.FreeformDocument, "freeform");
                 this.props.addDocTab(alias, "onRight");
-            }, icon: "eye"
+            }), icon: "eye"
         });
         !open && cm.addItem({ description: "Open New Perspective...", subitems: openItems, icon: "external-link-alt" });
 
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 46b60b554..acaa4363e 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -1,37 +1,36 @@
-import { action, computed, IReactionDisposer, observable, reaction, trace, runInAction } from "mobx";
+import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx";
 import { observer } from "mobx-react";
 import * as Pdfjs from "pdfjs-dist";
 import "pdfjs-dist/web/pdf_viewer.css";
+import * as rp from "request-promise";
 import { Dictionary } from "typescript-collections";
-import { Doc, DocListCast, FieldResult, WidthSym, Opt, HeightSym } from "../../../new_fields/Doc";
-import { Id, Copy } from "../../../new_fields/FieldSymbols";
+import { Doc, DocListCast, FieldResult, HeightSym, Opt, WidthSym } from "../../../new_fields/Doc";
+import { documentSchema } from "../../../new_fields/documentSchemas";
+import { Id } from "../../../new_fields/FieldSymbols";
+import { InkTool } from "../../../new_fields/InkField";
 import { List } from "../../../new_fields/List";
-import { makeInterface, createSchema } from "../../../new_fields/Schema";
-import { ScriptField, ComputedField } from "../../../new_fields/ScriptField";
-import { Cast, NumCast, StrCast } from "../../../new_fields/Types";
-import { smoothScroll, Utils, emptyFunction, returnOne, intersectRect, addStyleSheet, addStyleSheetRule, clearStyleSheetRules, returnZero, emptyPath } from "../../../Utils";
+import { createSchema, makeInterface } from "../../../new_fields/Schema";
+import { ScriptField } from "../../../new_fields/ScriptField";
+import { Cast, NumCast } from "../../../new_fields/Types";
+import { PdfField } from "../../../new_fields/URLField";
+import { TraceMobx } from "../../../new_fields/util";
+import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, emptyPath, intersectRect, returnZero, smoothScroll, Utils } from "../../../Utils";
 import { Docs, DocUtils } from "../../documents/Documents";
+import { DocumentType } from "../../documents/DocumentTypes";
 import { DragManager } from "../../util/DragManager";
 import { CompiledScript, CompileScript } from "../../util/Scripting";
-import { Transform } from "../../util/Transform";
-import PDFMenu from "./PDFMenu";
-import "./PDFViewer.scss";
-import React = require("react");
-import * as rp from "request-promise";
-import { CollectionView } from "../collections/CollectionView";
-import Annotation from "./Annotation";
-import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView";
 import { SelectionManager } from "../../util/SelectionManager";
+import { Transform } from "../../util/Transform";
 import { undoBatch } from "../../util/UndoManager";
+import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView";
+import { CollectionView } from "../collections/CollectionView";
 import { ViewBoxAnnotatableComponent } from "../DocComponent";
-import { DocumentType } from "../../documents/DocumentTypes";
-import { documentSchema } from "../../../new_fields/documentSchemas";
 import { DocumentDecorations } from "../DocumentDecorations";
 import { InkingControl } from "../InkingControl";
-import { InkTool } from "../../../new_fields/InkField";
-import { TraceMobx } from "../../../new_fields/util";
-import { PdfField } from "../../../new_fields/URLField";
-import { DocumentView } from "../nodes/DocumentView";
+import Annotation from "./Annotation";
+import PDFMenu from "./PDFMenu";
+import "./PDFViewer.scss";
+import React = require("react");
 const PDFJSViewer = require("pdfjs-dist/web/pdf_viewer");
 const pdfjsLib = require("pdfjs-dist");
 
@@ -574,7 +573,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu
         const targetDoc = Docs.Create.TextDocument("", { _width: 200, _height: 200, title: "Note linked to " + this.props.Document.title });
         Doc.GetProto(targetDoc).data = new List<Doc>([clipDoc]);
         clipDoc.rootDocument = targetDoc;
-        DocumentView.makeCustomViewClicked(targetDoc, Docs.Create.StackingDocument, "slideView", undefined);
+        Doc.makeCustomViewClicked(targetDoc, Docs.Create.StackingDocument, "slideView", undefined);
         targetDoc.layoutKey = "layout";
         // const targetDoc = Docs.Create.TextDocument("", { _width: 200, _height: 200, title: "Note linked to " + this.props.Document.title });
         // Doc.GetProto(targetDoc).snipped = this.dataDoc[this.props.fieldKey][Copy]();
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
index 1373431b8..cc929c040 100644
--- a/src/new_fields/Doc.ts
+++ b/src/new_fields/Doc.ts
@@ -17,7 +17,8 @@ import { listSpec } from "./Schema";
 import { ComputedField, ScriptField } from "./ScriptField";
 import { Cast, FieldValue, NumCast, StrCast, ToConstructor, ScriptCast } from "./Types";
 import { deleteProperty, getField, getter, makeEditable, makeReadOnly, setter, updateFunction } from "./util";
-import { Docs } from "../client/documents/Documents";
+import { Docs, DocumentOptions } from "../client/documents/Documents";
+import { PdfField, VideoField, AudioField, ImageField } from "./URLField";
 
 export namespace Field {
     export function toKeyValueString(doc: Doc, key: string): string {
@@ -913,18 +914,86 @@ export namespace Doc {
         return false;
     }
 
-    export function makeStarburst(newCollection: Doc) {
-        newCollection._layoutEngine = "starburst";
-        newCollection._fitToBox = true;
+    // applies a custom template to a document.  the template is identified by it's short name (e.g, slideView not layout_slideView)
+    export function makeCustomViewClicked(doc: Doc, creator: Opt<(documents: Array<Doc>, options: DocumentOptions, id?: string) => Doc>, templateSignature: string = "custom", docLayoutTemplate?: Doc) {
+        const batch = UndoManager.StartBatch("makeCustomViewClicked");
+        runInAction(() => {
+            doc.layoutKey = "layout_" + templateSignature;
+            if (doc[doc.layoutKey] === undefined) {
+                createCustomView(doc, creator, templateSignature, docLayoutTemplate);
+            }
+        });
+        batch.end();
+    }
+    export function findTemplate(templateName: string, type: string, signature: string) {
+        let docLayoutTemplate: Opt<Doc>;
+        const iconViews = DocListCast(Cast(Doc.UserDoc()["template-icons"], Doc, null)?.data);
+        const templBtns = DocListCast(Cast(Doc.UserDoc()["template-buttons"], Doc, null)?.data);
+        const noteTypes = DocListCast(Cast(Doc.UserDoc()["template-notes"], Doc, null)?.data);
+        const clickFuncs = DocListCast(Cast(Doc.UserDoc().clickFuncs, Doc, null)?.data);
+        const allTemplates = iconViews.concat(templBtns).concat(noteTypes).concat(clickFuncs).map(btnDoc => (btnDoc.dragFactory as Doc) || btnDoc).filter(doc => doc.isTemplateDoc);
+        // bcz: this is hacky -- want to have different templates be applied depending on the "type" of a document.  but type is not reliable and there could be other types of template searches so this should be generalized
+        // first try to find a template that matches the specific document type (<typeName>_<templateName>).  otherwise, fallback to a general match on <templateName>
+        !docLayoutTemplate && allTemplates.forEach(tempDoc => StrCast(tempDoc.title) === templateName + "_" + type && (docLayoutTemplate = tempDoc));
+        !docLayoutTemplate && allTemplates.forEach(tempDoc => StrCast(tempDoc.title) === templateName && (docLayoutTemplate = tempDoc));
+        return docLayoutTemplate;
+    }
+    export function createCustomView(doc: Doc, creator: Opt<(documents: Array<Doc>, options: DocumentOptions, id?: string) => Doc>, templateSignature: string = "custom", docLayoutTemplate?: Doc) {
+        const templateName = templateSignature.replace(/\(.*\)/, "");
+        docLayoutTemplate = docLayoutTemplate || findTemplate(templateName, StrCast(doc.type), templateSignature);
+
+        const customName = "layout_" + templateSignature;
+        const _width = NumCast(doc._width);
+        const _height = NumCast(doc._height);
+        const options = { title: "data", backgroundColor: StrCast(doc.backgroundColor), _autoHeight: true, _width, x: -_width / 2, y: - _height / 2, _showSidebar: false };
+
+        let fieldTemplate: Opt<Doc>;
+        if (doc.data instanceof RichTextField || typeof (doc.data) === "string") {
+            fieldTemplate = Docs.Create.TextDocument("", options);
+        } else if (doc.data instanceof PdfField) {
+            fieldTemplate = Docs.Create.PdfDocument("http://www.msn.com", options);
+        } else if (doc.data instanceof VideoField) {
+            fieldTemplate = Docs.Create.VideoDocument("http://www.cs.brown.edu", options);
+        } else if (doc.data instanceof AudioField) {
+            fieldTemplate = Docs.Create.AudioDocument("http://www.cs.brown.edu", options);
+        } else if (doc.data instanceof ImageField) {
+            fieldTemplate = Docs.Create.ImageDocument("http://www.cs.brown.edu", options);
+        }
+        const docTemplate = docLayoutTemplate || creator?.(fieldTemplate ? [fieldTemplate] : [], { title: customName + "(" + doc.title + ")", isTemplateDoc: true, _width: _width + 20, _height: Math.max(100, _height + 45) });
+
+        fieldTemplate && Doc.MakeMetadataFieldTemplate(fieldTemplate, docTemplate ? Doc.GetProto(docTemplate) : docTemplate);
+        docTemplate && Doc.ApplyTemplateTo(docTemplate, doc, customName, undefined);
+    }
+    export function makeCustomView(doc: Doc, custom: boolean, layout: string) {
+        Doc.setNativeView(doc);
+        if (custom) {
+            makeCustomViewClicked(doc, Docs.Create.StackingDocument, layout, undefined);
+        }
+    }
+    export function iconify(doc: Doc) {
+        const layoutKey = Cast(doc.layoutKey, "string", null);
+        Doc.makeCustomViewClicked(doc, Docs.Create.StackingDocument, "icon", undefined);
+        if (layoutKey && layoutKey !== "layout") doc.deiconifyLayout = layoutKey.replace("layout_", "");
+    }
+
+    export function pileup(newCollection: Doc) {
+        newCollection._layoutEngine = "pass";
         newCollection.overflow = "visible";
         const script = "if (self._layoutEngine === 'starburst') {" +
-            "     self._layoutEngine = 'pass';" +
             "     self.overflow = undefined;" +
-            "     self.fitToContentScaling=undefined;" +
+            "     self.x = self.x + self._width/2 - 55; " +
+            "     self.y = self.y + self._height/2 - 55; " +
+            "     self._width = self._height = 110;" +
+            "     self._layoutEngine = 'pass';" +
             " } else {" +
-            "    self._layoutEngine = 'starburst';" +
             "    self.overflow = 'visible';" +
-            "    self.fitToContentScaling=10;" +
+            "    !self.starburstRadius && (self.starburstRadius = 500);" +
+            "    if (self._layoutEngine === 'pass') { " +
+            "       self.x = self.x + self._width/2 - 12.5; " +
+            "       self.y = self.y + self._height/2 - 12.5; " +
+            "    }; " +
+            "    self._width = self._height = 25;" +
+            "    self._layoutEngine = 'starburst';" +
             " };";
         newCollection.onClick = ScriptField.MakeScript(script, { self: Doc.name });
     }
diff --git a/src/new_fields/RichTextUtils.ts b/src/new_fields/RichTextUtils.ts
index c211b3d3c..635fd053d 100644
--- a/src/new_fields/RichTextUtils.ts
+++ b/src/new_fields/RichTextUtils.ts
@@ -1,23 +1,21 @@
-import { EditorState, Transaction, TextSelection } from "prosemirror-state";
-import { Node, Fragment, Mark } from "prosemirror-model";
-import { RichTextField } from "./RichTextField";
+import { AssertionError } from "assert";
 import { docs_v1 } from "googleapis";
-import { GoogleApiClientUtils } from "../client/apis/google_docs/GoogleApiClientUtils";
-import { FormattedTextBox } from "../client/views/nodes/FormattedTextBox";
-import { Opt, Doc } from "./Doc";
-import Color = require('color');
+import { Fragment, Mark, Node } from "prosemirror-model";
 import { sinkListItem } from "prosemirror-schema-list";
-import { Utils } from "../Utils";
-import { Docs } from "../client/documents/Documents";
-import { schema } from "../client/util/RichTextSchema";
+import { EditorState, TextSelection, Transaction } from "prosemirror-state";
+import { GoogleApiClientUtils } from "../client/apis/google_docs/GoogleApiClientUtils";
 import { GooglePhotos } from "../client/apis/google_docs/GooglePhotosClientUtils";
 import { DocServer } from "../client/DocServer";
-import { Cast, StrCast } from "./Types";
-import { Id } from "./FieldSymbols";
-import { DocumentView } from "../client/views/nodes/DocumentView";
-import { AssertionError } from "assert";
+import { Docs } from "../client/documents/Documents";
 import { Networking } from "../client/Network";
-import { extname } from "path";
+import { schema } from "../client/util/RichTextSchema";
+import { FormattedTextBox } from "../client/views/nodes/FormattedTextBox";
+import { Utils } from "../Utils";
+import { Doc, Opt } from "./Doc";
+import { Id } from "./FieldSymbols";
+import { RichTextField } from "./RichTextField";
+import { Cast, StrCast } from "./Types";
+import Color = require('color');
 
 export namespace RichTextUtils {
 
@@ -274,7 +272,7 @@ export namespace RichTextUtils {
             const backingDocId = StrCast(textNote[guid]);
             if (!backingDocId) {
                 const backingDoc = Docs.Create.ImageDocument(agnostic, { _width: 300, _height: 300 });
-                DocumentView.makeCustomViewClicked(backingDoc, Docs.Create.FreeformDocument);
+                Doc.makeCustomViewClicked(backingDoc, Docs.Create.FreeformDocument);
                 docid = backingDoc[Id];
                 textNote[guid] = docid;
             } else {
@@ -403,7 +401,7 @@ export namespace RichTextUtils {
                                     let exported = (await Cast(linkDoc.anchor2, Doc))!;
                                     if (!exported.customLayout) {
                                         exported = Doc.MakeAlias(exported);
-                                        DocumentView.makeCustomViewClicked(exported, Docs.Create.FreeformDocument);
+                                        Doc.makeCustomViewClicked(exported, Docs.Create.FreeformDocument);
                                         linkDoc.anchor2 = exported;
                                     }
                                     url = Utils.shareUrl(exported[Id]);
-- 
cgit v1.2.3-70-g09d2


From 308ae1521cbd58126d35239cfdd5a138f6fa1fe0 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Sat, 25 Apr 2020 15:40:07 -0400
Subject: got rid of focus mechanism on click.  fixed shift-click to
 multiselect.  fixed pile view interactions.  added FollowLink on click menu
 items.

---
 src/client/util/DragManager.ts                     |  9 +++-
 src/client/util/ProsemirrorExampleTransfer.ts      | 18 +++++--
 src/client/views/ContextMenu.tsx                   |  9 ++++
 src/client/views/TemplateMenu.tsx                  |  5 +-
 .../views/collections/CollectionPileView.tsx       | 38 +++++++++++--
 src/client/views/collections/CollectionView.tsx    |  2 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 20 +++----
 src/client/views/nodes/DocumentView.tsx            | 63 +++++++++++++++-------
 src/client/views/nodes/FormattedTextBox.tsx        |  4 +-
 src/client/views/nodes/FormattedTextBoxComment.tsx |  1 -
 src/client/views/nodes/LinkAnchorBox.tsx           |  1 -
 src/client/views/pdf/Annotation.tsx                |  1 -
 12 files changed, 124 insertions(+), 47 deletions(-)

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

diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index 42a78a4bf..6bb44f7d6 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -83,6 +83,7 @@ export namespace DragManager {
     }
     export let AbortDrag: () => void = emptyFunction;
     export type MoveFunction = (document: Doc, targetCollection: Doc | undefined, addDocument: (document: Doc) => boolean) => boolean;
+    export type RemoveFunction = (document: Doc) => boolean;
 
     export interface DragDropDisposer { (): void; }
     export interface DragOptions {
@@ -138,6 +139,7 @@ export namespace DragManager {
         userDropAction: dropActionType;
         embedDoc?: boolean;
         moveDocument?: MoveFunction;
+        removeDocument?: RemoveFunction;
         isSelectionMove?: boolean; // indicates that an explicitly selected Document is being dragged.  this will suppress onDragStart scripts
     }
     export class LinkDragData {
@@ -351,12 +353,17 @@ export namespace DragManager {
 
         let lastX = downX;
         let lastY = downY;
+        let alias = "alias";
         const moveHandler = (e: PointerEvent) => {
             e.preventDefault(); // required or dragging text menu link item ends up dragging the link button as native drag/drop
             if (dragData instanceof DocumentDragData) {
                 dragData.userDropAction = e.ctrlKey && e.altKey ? "copy" : e.ctrlKey ? "alias" : undefined;
             }
             if (e.shiftKey && CollectionDockingView.Instance && dragData.droppedDocuments.length === 1) {
+                !dragData.dropAction && (dragData.dropAction = alias);
+                if (dragData.dropAction === "move") {
+                    dragData.removeDocument?.(dragData.draggedDocuments[0]);
+                }
                 AbortDrag();
                 finishDrag?.(new DragCompleteEvent(true, dragData));
                 CollectionDockingView.Instance.StartOtherDrag({
@@ -366,7 +373,7 @@ export namespace DragManager {
                     button: 0
                 }, dragData.droppedDocuments);
             }
-            //TODO: Why can't we use e.movementX and e.movementY?
+            alias = "move";
             const moveX = e.pageX - lastX;
             const moveY = e.pageY - lastY;
             lastX = e.pageX;
diff --git a/src/client/util/ProsemirrorExampleTransfer.ts b/src/client/util/ProsemirrorExampleTransfer.ts
index 680f48f70..356f20ce6 100644
--- a/src/client/util/ProsemirrorExampleTransfer.ts
+++ b/src/client/util/ProsemirrorExampleTransfer.ts
@@ -7,7 +7,7 @@ import { splitListItem, wrapInList, } from "prosemirror-schema-list";
 import { EditorState, Transaction, TextSelection } from "prosemirror-state";
 import { SelectionManager } from "./SelectionManager";
 import { Docs } from "../documents/Documents";
-import { NumCast, BoolCast, Cast } from "../../new_fields/Types";
+import { NumCast, BoolCast, Cast, StrCast } from "../../new_fields/Types";
 import { Doc } from "../../new_fields/Doc";
 import { FormattedTextBox } from "../views/nodes/FormattedTextBox";
 import { Id } from "../../new_fields/FieldSymbols";
@@ -153,10 +153,16 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, props: any
         const layoutDoc = props.Document;
         const originalDoc = layoutDoc.rootDocument || layoutDoc;
         if (originalDoc instanceof Doc) {
+            const layoutKey = StrCast(originalDoc.layoutKey);
             const newDoc = Docs.Create.TextDocument("", {
-                layout: Cast(originalDoc.layout, Doc, null) || FormattedTextBox.DefaultLayout, _singleLine: BoolCast(originalDoc._singleLine),
+                layout: Cast(originalDoc.layout, Doc, null) || FormattedTextBox.DefaultLayout,
+                layoutKey,
+                _singleLine: BoolCast(originalDoc._singleLine),
                 x: NumCast(originalDoc.x), y: NumCast(originalDoc.y) + NumCast(originalDoc._height) + 10, _width: NumCast(layoutDoc._width), _height: NumCast(layoutDoc._height)
             });
+            if (layoutKey !== "layout" && originalDoc[layoutKey] instanceof Doc) {
+                newDoc[layoutKey] = originalDoc[layoutKey];
+            }
             FormattedTextBox.SelectOnLoad = newDoc[Id];
             props.addDocument(newDoc);
         }
@@ -171,10 +177,16 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, props: any
         const layoutDoc = props.Document;
         const originalDoc = layoutDoc.rootDocument || layoutDoc;
         if (force || props.Document._singleLine) {
+            const layoutKey = StrCast(originalDoc.layoutKey);
             const newDoc = Docs.Create.TextDocument("", {
-                layout: Cast(originalDoc.layout, Doc, null) || FormattedTextBox.DefaultLayout, _singleLine: BoolCast(originalDoc._singleLine),
+                layout: Cast(originalDoc.layout, Doc, null) || FormattedTextBox.DefaultLayout,
+                layoutKey,
+                _singleLine: BoolCast(originalDoc._singleLine),
                 x: NumCast(originalDoc.x) + NumCast(originalDoc._width) + 10, y: NumCast(originalDoc.y), _width: NumCast(layoutDoc._width), _height: NumCast(layoutDoc._height)
             });
+            if (layoutKey !== "layout" && originalDoc[layoutKey] instanceof Doc) {
+                newDoc[layoutKey] = originalDoc[layoutKey];
+            }
             FormattedTextBox.SelectOnLoad = newDoc[Id];
             props.addDocument(newDoc);
             return true;
diff --git a/src/client/views/ContextMenu.tsx b/src/client/views/ContextMenu.tsx
index 4d04d4e89..5b66b63ed 100644
--- a/src/client/views/ContextMenu.tsx
+++ b/src/client/views/ContextMenu.tsx
@@ -99,6 +99,15 @@ export class ContextMenu extends React.Component {
         }
     }
     @action
+    moveAfter(item: ContextMenuProps, after: ContextMenuProps) {
+        if (this.findByDescription(after.description)) {
+            const curInd = this._items.findIndex((i) => i.description === item.description);
+            this._items.splice(curInd, 1);
+            const afterInd = this._items.findIndex((i) => i.description === after.description);
+            this._items.splice(afterInd + 1, 0, item);
+        }
+    }
+    @action
     setDefaultItem(prefix: string, item: (name: string) => void) {
         this._defaultPrefix = prefix;
         this._defaultItem = item;
diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx
index 4c84a22ad..665ab4e41 100644
--- a/src/client/views/TemplateMenu.tsx
+++ b/src/client/views/TemplateMenu.tsx
@@ -14,6 +14,7 @@ import { returnTrue, emptyFunction, returnFalse, returnOne, emptyPath, returnZer
 import { Transform } from "../util/Transform";
 import { ScriptField, ComputedField } from "../../new_fields/ScriptField";
 import { Scripting } from "../util/Scripting";
+import { List } from "../../new_fields/List";
 
 @observer
 class TemplateToggle extends React.Component<{ template: Template, checked: boolean, toggle: (event: React.ChangeEvent<HTMLInputElement>, template: Template) => void }> {
@@ -106,8 +107,8 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> {
 
     return100 = () => 100;
     @computed get scriptField() {
-        return ScriptField.MakeScript("switchView(firstDoc, this)", { this: Doc.name, heading: "string", checked: "string", containingTreeView: Doc.name, firstDoc: Doc.name },
-            { firstDoc: this.props.docViews[0].props.Document });
+        return ScriptField.MakeScript("docs.map(d => switchView(d, this))", { this: Doc.name, heading: "string", checked: "string", containingTreeView: Doc.name, firstDoc: Doc.name },
+            { docs: new List<Doc>(this.props.docViews.map(dv => dv.props.Document)) });
     }
     render() {
         const firstDoc = this.props.docViews[0].props.Document;
diff --git a/src/client/views/collections/CollectionPileView.tsx b/src/client/views/collections/CollectionPileView.tsx
index 511efe0a7..410b16ec7 100644
--- a/src/client/views/collections/CollectionPileView.tsx
+++ b/src/client/views/collections/CollectionPileView.tsx
@@ -11,6 +11,7 @@ import "./CollectionPileView.scss";
 import React = require("react");
 import { setupMoveUpEvents, emptyFunction, returnFalse } from "../../../Utils";
 import { SelectionManager } from "../../util/SelectionManager";
+import { UndoManager } from "../../util/UndoManager";
 
 @observer
 export class CollectionPileView extends CollectionSubView(doc => doc) {
@@ -34,7 +35,10 @@ export class CollectionPileView extends CollectionSubView(doc => doc) {
     layoutEngine = () => this._layoutEngine;
 
     @computed get contents() {
-        return <div className="collectionPileView-innards" style={{ width: "100%", pointerEvents: this._contentsActive && (this.props.active() || this.layoutEngine() === "starburst") ? undefined : "none" }} >
+        return <div className="collectionPileView-innards" style={{
+            width: "100%",
+            pointerEvents: this.layoutEngine() !== "pass" && (this.props.active() || this.layoutEngine() === "starburst") ? undefined : "none"
+        }} >
             <CollectionFreeFormView {...this.props} layoutEngine={this.layoutEngine} />
         </div>;
     }
@@ -71,9 +75,32 @@ export class CollectionPileView extends CollectionSubView(doc => doc) {
         }
     });
 
+    _undoBatch: UndoManager.Batch | undefined;
     pointerDown = (e: React.PointerEvent) => {
+        let dist = 0;
         // this._lastTap should be set to 0, and this._doubleTap should be set to false in the class header
-        setupMoveUpEvents(this, e, returnFalse, emptyFunction, emptyFunction, false, false); // this sets _doubleTap
+        setupMoveUpEvents(this, e, (e: PointerEvent, down: number[], delta: number[]) => {
+            if (this.layoutEngine() === "pass" && this.childDocs.length && this.props.isSelected(true)) {
+                dist += Math.sqrt(delta[0] * delta[0] + delta[1] * delta[1]);
+                if (dist > 100) {
+                    if (!this._undoBatch) {
+                        this._undoBatch = UndoManager.StartBatch("layout pile");
+                    }
+                    const doc = this.childDocs[0];
+                    doc.x = e.clientX;
+                    doc.y = e.clientY;
+                    this.props.addDocTab(doc, "inParent") && this.props.removeDocument(doc);
+                    dist = 0;
+                }
+            }
+            return false;
+        }, () => {
+            this._undoBatch?.end();
+            this._undoBatch = undefined;
+            if (!this.childDocs.length) {
+                this.props.ContainingCollectionView?.removeDocument(this.props.Document);
+            }
+        }, emptyFunction, false, this.layoutEngine() === "pass" && this.props.isSelected(true)); // this sets _doubleTap
     }
 
     onClick = (e: React.MouseEvent) => {
@@ -81,10 +108,11 @@ export class CollectionPileView extends CollectionSubView(doc => doc) {
             SelectionManager.DeselectAll();
             this.toggleStarburst();
             e.stopPropagation();
-        } else if (this.layoutEngine() === "pass") {
-            runInAction(() => this._contentsActive = false);
-            setTimeout(action(() => this._contentsActive = true), 300);
         }
+        // else if (this.layoutEngine() === "pass") {
+        //     runInAction(() => this._contentsActive = false);
+        //     setTimeout(action(() => this._contentsActive = true), 300);
+        // }
     }
 
     render() {
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index f82c6d8a6..2bd37bda5 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -225,7 +225,7 @@ export class CollectionView extends Touchable<FieldViewProps> {
         if (!e.isPropagationStopped() && this.props.Document[Id] !== CurrentUserUtils.MainDocId) { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7
 
             this.setupViewTypes("Change Perspective...", (vtype => { this.props.Document._viewType = vtype; return this.props.Document; }), true);
-            this.setupViewTypes("New Perspective...", vtype => {
+            this.setupViewTypes("Add a Perspective...", vtype => {
                 const newRendition = Doc.MakeAlias(this.props.Document);
                 newRendition._viewType = vtype;
                 this.props.addDocTab(newRendition, "onRight");
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index af9d3c5be..6a3764bb7 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -821,18 +821,18 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
 
             const savedState = { px: this.Document._panX, py: this.Document._panY, s: this.Document.scale, pt: this.Document.panTransformType };
 
-            if (!willZoom && DocumentView._focusHack.length) {
-                Doc.BrushDoc(this.props.Document);
-                !doc.z && NumCast(this.layoutDoc.scale) < 1 && this.scaleAtPt(DocumentView._focusHack, 1); // [NumCast(doc.x), NumCast(doc.y)], 1);
-            } else {
-                if (DocListCast(this.dataDoc[this.props.fieldKey]).includes(doc)) {
-                    if (!doc.z) this.setPan(newPanX, newPanY, "Ease", true); // docs that are floating in their collection can't be panned to from their collection -- need to propagate the pan to a parent freeform somehow
-                }
-                Doc.BrushDoc(this.props.Document);
-                this.props.focus(this.props.Document);
-                willZoom && this.setScaleToZoom(layoutdoc, scale);
+            // if (!willZoom && DocumentView._focusHack.length) {
+            //     Doc.BrushDoc(this.props.Document);
+            //     !doc.z && NumCast(this.layoutDoc.scale) < 1 && this.scaleAtPt(DocumentView._focusHack, 1); // [NumCast(doc.x), NumCast(doc.y)], 1);
+            // } else {
+            if (DocListCast(this.dataDoc[this.props.fieldKey]).includes(doc)) {
+                if (!doc.z) this.setPan(newPanX, newPanY, "Ease", true); // docs that are floating in their collection can't be panned to from their collection -- need to propagate the pan to a parent freeform somehow
             }
+            Doc.BrushDoc(this.props.Document);
+            this.props.focus(this.props.Document);
+            willZoom && this.setScaleToZoom(layoutdoc, scale);
             Doc.linkFollowHighlight(doc);
+            //}
 
             afterFocus && setTimeout(() => {
                 if (afterFocus?.()) {
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 06bd40992..f9d9ca9a1 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -234,6 +234,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
             const [left, top] = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).inverse().transformPoint(0, 0);
             dragData.offset = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).transformDirection(x - left, y - top);
             dragData.dropAction = dropAction;
+            dragData.removeDocument = this.props.removeDocument;
             dragData.moveDocument = this.props.moveDocument;//  this.Document.onDragStart ? undefined : this.props.moveDocument;
             dragData.dragDivName = this.props.dragDivName;
             DragManager.StartDocumentDrag([this._mainCont.current], dragData, x, y, { hideSource: !dropAction && !this.Document.onDragStart });
@@ -309,19 +310,19 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
             } else if (this.Document["onClick-rawScript"] && !StrCast(Doc.LayoutField(this.layoutDoc))?.includes("ScriptingBox")) {// bcz: hack? don't edit a script if you're clicking on a scripting box itself
                 UndoManager.RunInBatch(() => Doc.makeCustomViewClicked(this.props.Document, undefined, "onClick"), "edit onClick");
                 //ScriptBox.EditButtonScript("On Button Clicked ...", this.props.Document, "onClick", e.clientX, e.clientY), "on button click");
-            } else if (this.Document.isLinkButton) {
+            } else if (this.Document.isLinkButton && !e.shiftKey && !e.ctrlKey) {
                 DocListCast(this.props.Document.links).length && this.followLinkClick(e.altKey, e.ctrlKey, e.shiftKey);
             } else {
                 if ((this.props.Document.onDragStart || (this.props.Document.rootDocument)) && !(e.ctrlKey || e.button > 0)) {  // onDragStart implies a button doc that we don't want to select when clicking.   RootDocument & isTEmplaetForField implies we're clicking on part of a template instance and we want to select the whole template, not the part
                     stopPropagate = false; // don't stop propagation for field templates -- want the selection to propagate up to the root document of the template
                 } else {
-                    if (this.props.Document.type === DocumentType.RTF) {
-                        DocumentView._focusHack = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY) || [0, 0];
-                        DocumentView._focusHack = [DocumentView._focusHack[0] + NumCast(this.props.Document.x), DocumentView._focusHack[1] + NumCast(this.props.Document.y)];
+                    // if (this.props.Document.type === DocumentType.RTF) {
+                    //     DocumentView._focusHack = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY) || [0, 0];
+                    //     DocumentView._focusHack = [DocumentView._focusHack[0] + NumCast(this.props.Document.x), DocumentView._focusHack[1] + NumCast(this.props.Document.y)];
 
-                        this.props.focus(this.props.Document, false);
-                    }
-                    SelectionManager.SelectDoc(this, e.ctrlKey);
+                    //     this.props.focus(this.props.Document, false);
+                    // }
+                    SelectionManager.SelectDoc(this, e.ctrlKey || e.shiftKey);
                 }
                 preventDefault = false;
             }
@@ -329,7 +330,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
             preventDefault && e.preventDefault();
         }
     }
-    static _focusHack: number[] = []; // bcz :this will get fixed...
 
     // follows a link - if the target is on screen, it highlights/pans to it.
     // if the target isn't onscreen, then it will open up the target in a tab, on the right, or in place
@@ -344,11 +344,11 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
                     finished && setTimeout(finished, 0); // finished() needs to be called right after hackToCallFinishAfterFocus(), but there's no callback for that so we use the hacky timeout.  
                     return false; // we must return false here so that the zoom to the document is not reversed.  If it weren't for needing to call finished(), we wouldn't need this function at all since not having it is equivalent to returning false
                 };
-                this.props.addDocTab(doc, where) && this.props.focus(doc, true, undefined, hackToCallFinishAfterFocus); //  add the target and focus on it.
+                this.props.addDocTab(doc, where) && this.props.focus(doc, BoolCast(this.Document.followLinkZoom, true), undefined, hackToCallFinishAfterFocus); //  add the target and focus on it.
                 return where !== "inPlace"; // return true to reset the initial focus&zoom (return false for 'inPlace' since resetting the initial focus&zoom will negate the zoom into the target)
             };
             // first focus & zoom onto this (the clicked document).  Then execute the function to focus on the target
-            this.props.focus(this.props.Document, true, 1, targetFocusAfterDocFocus);
+            this.props.focus(this.props.Document, BoolCast(this.Document.followLinkZoom, true), 1, targetFocusAfterDocFocus);
         };
         await DocumentManager.Instance.FollowLink(undefined, this.props.Document, createViewFunc, shiftKey, this.props.ContainingCollectionDoc, batch.end, altKey ? true : undefined);
     }
@@ -569,6 +569,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
             this.Document.onClick = undefined;
         } else {
             this.Document.isLinkButton = true;
+            this.Document.followLinkZoom = false;
             this.Document.followLinkLocation = undefined;
         }
     }
@@ -579,10 +580,24 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
             this.Document.isLinkButton = false;
         } else {
             this.Document.isLinkButton = true;
+            this.Document.followLinkZoom = true;
             this.Document.followLinkLocation = "inPlace";
         }
     }
 
+    @undoBatch
+    toggleFollowOnRight = (): void => {
+        if (this.Document.isLinkButton) {
+            this.Document.isLinkButton = false;
+        } else {
+            this.Document.isLinkButton = true;
+            this.Document.followLinkZoom = false;
+            const first = DocListCast(this.Document.links).find(d => d instanceof Doc);
+            first && (first.hidden = true);
+            this.Document.followLinkLocation = "onRight";
+        }
+    }
+
     @undoBatch
     @action
     drop = async (e: Event, de: DragManager.DropEvent) => {
@@ -679,20 +694,27 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
             cm.addItem({ description: item.label, event: () => item.script.script.run({ this: this.layoutDoc, self: this.rootDoc }), icon: "sticky-note" }));
 
 
-        const existing = cm.findByDescription("Options...");
-        const layoutItems: ContextMenuProps[] = existing && "subitems" in existing ? existing.subitems : [];
-        layoutItems.push({ description: `${this.Document._chromeStatus !== "disabled" ? "Hide" : "Show"} Chrome`, event: () => this.Document._chromeStatus = (this.Document._chromeStatus !== "disabled" ? "disabled" : "enabled"), icon: "project-diagram" });
-        layoutItems.push({ description: `${this.Document._autoHeight ? "Variable Height" : "Auto Height"}`, event: () => this.layoutDoc._autoHeight = !this.layoutDoc._autoHeight, icon: "plus" });
-        layoutItems.push({ description: this.Document.lockedPosition ? "Unlock Position" : "Lock Position", event: this.toggleLockPosition, icon: BoolCast(this.Document.lockedPosition) ? "unlock" : "lock" });
-        layoutItems.push({ description: this.Document.lockedTransform ? "Unlock Transform" : "Lock Transform", event: this.toggleLockTransform, icon: BoolCast(this.Document.lockedTransform) ? "unlock" : "lock" });
-        !existing && cm.addItem({ description: "Options...", subitems: layoutItems, icon: "compass" });
-
-        const open = cm.findByDescription("New Perspective...");
+        let open = cm.findByDescription("Add a Perspective...");
         const openItems: ContextMenuProps[] = open && "subitems" in open ? open.subitems : [];
         openItems.push({ description: "Open Fields     ", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), "onRight"), icon: "layer-group" });
         templateDoc && openItems.push({ description: "Open Template   ", event: () => this.props.addDocTab(templateDoc, "onRight"), icon: "eye" });
-        !open && cm.addItem({ description: "New Perspective...", subitems: openItems, icon: "external-link-alt" });
+        if (!open) {
+            open = { description: "Add a Perspective....", subitems: openItems, icon: "external-link-alt" };
+            cm.addItem(open);
+        }
+
+        let options = cm.findByDescription("Options...");
+        const optionItems: ContextMenuProps[] = options && "subitems" in options ? options.subitems : [];
+        optionItems.push({ description: `${this.Document._chromeStatus !== "disabled" ? "Hide" : "Show"} Chrome`, event: () => this.Document._chromeStatus = (this.Document._chromeStatus !== "disabled" ? "disabled" : "enabled"), icon: "project-diagram" });
+        optionItems.push({ description: `${this.Document._autoHeight ? "Variable Height" : "Auto Height"}`, event: () => this.layoutDoc._autoHeight = !this.layoutDoc._autoHeight, icon: "plus" });
+        optionItems.push({ description: this.Document.lockedPosition ? "Unlock Position" : "Lock Position", event: this.toggleLockPosition, icon: BoolCast(this.Document.lockedPosition) ? "unlock" : "lock" });
+        optionItems.push({ description: this.Document.lockedTransform ? "Unlock Transform" : "Lock Transform", event: this.toggleLockTransform, icon: BoolCast(this.Document.lockedTransform) ? "unlock" : "lock" });
+        if (!options) {
+            options = { description: "Options...", subitems: optionItems, icon: "compass" };
+            cm.addItem(options);
+        }
 
+        cm.moveAfter(options, open);
 
         const existingOnClick = cm.findByDescription("OnClick...");
         const onClicks: ContextMenuProps[] = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : [];
@@ -700,6 +722,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         onClicks.push({ description: "Toggle Detail", event: () => this.Document.onClick = ScriptField.MakeScript(`toggleDetail(self, "${this.props.Document.layoutKey}")`), icon: "window-restore" });
         onClicks.push({ description: this.Document.ignoreClick ? "Select" : "Do Nothing", event: () => this.Document.ignoreClick = !this.Document.ignoreClick, icon: this.Document.ignoreClick ? "unlock" : "lock" });
         onClicks.push({ description: this.Document.isLinkButton ? "Remove Follow Behavior" : "Follow Link in Place", event: this.toggleFollowInPlace, icon: "concierge-bell" });
+        onClicks.push({ description: this.Document.isLinkButton ? "Remove Follow Behavior" : "Follow Link on Right", event: this.toggleFollowOnRight, icon: "concierge-bell" });
         onClicks.push({ description: this.Document.isLinkButton || this.Document.onClick ? "Remove Click Behavior" : "Follow Link", event: this.toggleLinkButtonBehavior, icon: "concierge-bell" });
         onClicks.push({ description: "Edit onClick Script", event: () => UndoManager.RunInBatch(() => Doc.makeCustomViewClicked(this.props.Document, undefined, "onClick"), "edit onClick"), icon: "edit" });
         !existingOnClick && cm.addItem({ description: "OnClick...", subitems: onClicks, icon: "hand-point-right" });
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index a018f17e8..d7cfbae9f 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -444,7 +444,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
         changeItems.push({ description: "FreeForm", event: undoBatch(() => Doc.makeCustomViewClicked(this.rootDoc, Docs.Create.FreeformDocument, "freeform"), "change view"), icon: "eye" });
         !change && cm.addItem({ description: "Change Perspective...", subitems: changeItems, icon: "external-link-alt" });
 
-        const open = cm.findByDescription("New Perspective...");
+        const open = cm.findByDescription("Add a Perspective...");
         const openItems: ContextMenuProps[] = open && "subitems" in open ? open.subitems : [];
 
         openItems.push({
@@ -454,7 +454,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
                 this.props.addDocTab(alias, "onRight");
             }), icon: "eye"
         });
-        !open && cm.addItem({ description: "New Perspective...", subitems: openItems, icon: "external-link-alt" });
+        !open && cm.addItem({ description: "Add a Perspective...", subitems: openItems, icon: "external-link-alt" });
 
     }
 
diff --git a/src/client/views/nodes/FormattedTextBoxComment.tsx b/src/client/views/nodes/FormattedTextBoxComment.tsx
index 41df5b3c1..a9f76ae8f 100644
--- a/src/client/views/nodes/FormattedTextBoxComment.tsx
+++ b/src/client/views/nodes/FormattedTextBoxComment.tsx
@@ -88,7 +88,6 @@ export class FormattedTextBoxComment {
                     if (FormattedTextBoxComment.linkDoc.type !== DocumentType.LINK) {
                         textBox.props.addDocTab(FormattedTextBoxComment.linkDoc, e.ctrlKey ? "inTab" : "onRight");
                     } else {
-                        DocumentView._focusHack = [];
                         DocumentManager.Instance.FollowLink(FormattedTextBoxComment.linkDoc, textBox.props.Document,
                             (doc: Doc, followLinkLocation: string) => textBox.props.addDocTab(doc, e.ctrlKey ? "inTab" : followLinkLocation));
                     }
diff --git a/src/client/views/nodes/LinkAnchorBox.tsx b/src/client/views/nodes/LinkAnchorBox.tsx
index eb647d0e4..707b9d12a 100644
--- a/src/client/views/nodes/LinkAnchorBox.tsx
+++ b/src/client/views/nodes/LinkAnchorBox.tsx
@@ -75,7 +75,6 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps, LinkAnch
             anchorContainerDoc && this.props.bringToFront(anchorContainerDoc, false);
             if (anchorContainerDoc && !this.layoutDoc.onClick && !this._isOpen) {
                 this._timeout = setTimeout(action(() => {
-                    DocumentView._focusHack = [];
                     DocumentManager.Instance.FollowLink(this.rootDoc, anchorContainerDoc, document => this.props.addDocTab(document, StrCast(this.layoutDoc.linkOpenLocation, "inTab")), false);
                     this._editing = false;
                 }), 300 - (Date.now() - this._lastTap));
diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx
index eaf80d252..672d3adb8 100644
--- a/src/client/views/pdf/Annotation.tsx
+++ b/src/client/views/pdf/Annotation.tsx
@@ -98,7 +98,6 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> {
         else if (e.button === 0) {
             const annoGroup = await Cast(this.props.document.group, Doc);
             if (annoGroup) {
-                DocumentView._focusHack = [];
                 DocumentManager.Instance.FollowLink(undefined, annoGroup, (doc, followLinkLocation) => this.props.addDocTab(doc, e.ctrlKey ? "inTab" : followLinkLocation), false, undefined);
                 e.stopPropagation();
             }
-- 
cgit v1.2.3-70-g09d2

