From 081632757af3c2ec2e4482c1e5fe710c8ee6cad8 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 11 Aug 2022 19:01:37 -0400
Subject: added better ui and backend support for mask ink strokes. added frame
 #'s ui for seeing current animation frame.

---
 src/client/views/collections/CollectionMenu.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

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

diff --git a/src/client/views/collections/CollectionMenu.tsx b/src/client/views/collections/CollectionMenu.tsx
index 8432619de..eb55650e4 100644
--- a/src/client/views/collections/CollectionMenu.tsx
+++ b/src/client/views/collections/CollectionMenu.tsx
@@ -1025,7 +1025,7 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionView
                                         <FontAwesomeIcon icon={'caret-left'} size={'lg'} />
                                     </div>
                                 </Tooltip>
-                                <Tooltip key="num" title={<div className="dash-tooltip">Toggle View All</div>} placement="bottom">
+                                <Tooltip key="num" title={<div className="dash-tooltip">Frame number</div>} placement="bottom">
                                     <div
                                         className="numKeyframe"
                                         style={{ color: this.props.docView.ComponentView?.getKeyFrameEditing?.() ? 'white' : 'black', backgroundColor: this.props.docView.ComponentView?.getKeyFrameEditing?.() ? '#5B9FDD' : '#AEDDF8' }}
-- 
cgit v1.2.3-70-g09d2


From 542a057edbdd4661cfb5ef5d07058aa93b47a113 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 24 Aug 2022 11:16:46 -0400
Subject: cleaned up some of the pin with view code.  added a pin button to
 menu bar when tab is selected to avoid needing to use context menu

---
 src/client/util/CurrentUserUtils.ts                |   5 +-
 src/client/util/SelectionManager.ts                |   3 +
 src/client/views/collections/CollectionMenu.tsx    |  44 ---
 src/client/views/collections/TabDocView.tsx        |  39 +--
 .../collectionFreeForm/CollectionFreeFormView.tsx  |   3 +
 src/client/views/nodes/button/FontIconBox.tsx      |  96 ++----
 .../views/nodes/formattedText/RichTextMenu.tsx     |   4 +-
 src/client/views/nodes/trails/PresBox.tsx          | 358 +++++----------------
 8 files changed, 124 insertions(+), 428 deletions(-)

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

diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 17d58595c..99a8c895f 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -267,7 +267,7 @@ export class CurrentUserUtils {
             {key: "Script",     creator: opts => Docs.Create.ScriptingDocument(null, opts), opts: { _width: 200, _height: 250, }},
             // {key: "DataViz",    creator: opts => Docs.Create.DataVizDocument(opts),         opts: { _width: 300, _height: 300 }},
             {key: "Header",     creator: headerTemplate,                                    opts: { _width: 300, _height: 70, _headerPointerEvents: "all", _headerHeight: 12, _headerFontSize: 9, _autoHeight: true,}},
-            {key: "Presentation",creator: Docs.Create.PresDocument,                         opts: { _width: 400, _height: 500, _viewType: CollectionViewType.Stacking, targetDropAction: "alias" as any, _chromeHidden: true, boxShadow: "0 0" }},
+            {key: "Presentation",creator: Docs.Create.PresDocument,                         opts: { _width: 400, _height: 500, _viewType: CollectionViewType.Stacking, targetDropAction: "alias" as any, treeViewHideTitle: true,  _chromeHidden: true, boxShadow: "0 0" }},
             {key: "Tab",        creator: opts => Docs.Create.FreeformDocument([], opts),    opts: { _width: 500, _height: 800, _backgroundGridShow: true, }},
             {key: "Slide",      creator: opts => Docs.Create.TreeDocument([], opts),        opts: { _width: 300, _height: 200, _viewType: CollectionViewType.Tree, 
                                                                                                 treeViewHasOverlay: true, _fontSize: "20px", _autoHeight: true, 
@@ -675,8 +675,9 @@ export class CurrentUserUtils {
                     CollectionViewType.Carousel3D, CollectionViewType.Linear, CollectionViewType.Map,
                     CollectionViewType.Grid, CollectionViewType.NoteTaking]),
               title: "Perspective",                    toolTip: "View",                 btnType: ButtonType.DropdownList,                                                                                    ignoreClick: true, width: 100, scripts: { script: 'setView(value, _readOnly_)'}},
+            { title: "Pin",     icon: "map-pin",       toolTip: "Pin View to Trail",    btnType: ButtonType.ClickButton,         funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "tab")'},                        width: 20,  scripts: { onClick: 'pinWithView(_readOnly_)'}},
             { title: "Back",    icon: "chevron-left",  toolTip: "Prev Animation Frame", btnType: ButtonType.ClickButton,         funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()'}, width: 20,  scripts: { onClick: 'prevKeyFrame(_readOnly_)'}},
-            { title: "Num",     icon: "",              toolTip: "Frame Number",         btnType: ButtonType.TextButton,          funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()', buttonText: 'selectedDocs()?.lastElement().currentFrame.toString()'}, width: 20,  scripts: {}},
+            { title: "Num",     icon: "",              toolTip: "Frame Number",         btnType: ButtonType.TextButton,          funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()', buttonText: 'selectedDocs()?.lastElement()?.currentFrame.toString()'}, width: 20,  scripts: {}},
             { title: "Fwd",     icon: "chevron-right", toolTip: "Next Animation Frame", btnType: ButtonType.ClickButton,         funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()'}, width: 20,  scripts: { onClick: 'nextKeyFrame(_readOnly_)'}},
             { title: "Fill",    icon: "fill-drip",     toolTip: "Background Fill Color",btnType: ButtonType.ColorButton,         funcs: {hidden: '!SelectionManager_selectedDocType()'},                     ignoreClick: true, width: 20,  scripts: { script:  'return setBackgroundColor(value, _readOnly_)'}}, // Only when a document is selected
             { title: "Header",  icon: "heading",       toolTip: "Header Color",         btnType: ButtonType.ColorButton,         funcs: {hidden: '!SelectionManager_selectedDocType()'},                     ignoreClick: true,             scripts: { script:  'return setHeaderColor(value, _readOnly_)'}},
diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts
index 1c84af94a..7a555d5f8 100644
--- a/src/client/util/SelectionManager.ts
+++ b/src/client/util/SelectionManager.ts
@@ -101,6 +101,9 @@ export namespace SelectionManager {
     }
 }
 ScriptingGlobals.add(function SelectionManager_selectedDocType(docType?: DocumentType, colType?: CollectionViewType, checkContext?: boolean) {
+    if (colType === ('tab' as any)) {
+        return SelectionManager.Views().lastElement()?.props.renderDepth === 0;
+    }
     let selected = (sel => (checkContext ? DocCast(sel?.context) : sel))(SelectionManager.SelectedSchemaDoc() ?? SelectionManager.Docs().lastElement());
     return docType ? selected?.type === docType : colType ? selected?.viewType === colType : true;
 });
diff --git a/src/client/views/collections/CollectionMenu.tsx b/src/client/views/collections/CollectionMenu.tsx
index eb55650e4..0dc30e0fd 100644
--- a/src/client/views/collections/CollectionMenu.tsx
+++ b/src/client/views/collections/CollectionMenu.tsx
@@ -586,49 +586,6 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu
         );
     }
 
-    @undoBatch
-    @action
-    pinWithView = (targetDoc: Opt<Doc>) => {
-        if (targetDoc) {
-            TabDocView.PinDoc(targetDoc);
-            const presArray: Doc[] = PresBox.Instance?.sortArray();
-            const size: number = PresBox.Instance?._selectedArray.size;
-            const presSelected: Doc | undefined = presArray && size ? presArray[size - 1] : undefined;
-            const activeDoc = presSelected ? PresBox.Instance?.childDocs[PresBox.Instance?.childDocs.indexOf(presSelected) + 1] : PresBox.Instance?.childDocs[PresBox.Instance?.childDocs.length - 1];
-            if (targetDoc.type === DocumentType.PDF || targetDoc.type === DocumentType.RTF || targetDoc.type === DocumentType.WEB || targetDoc._viewType === CollectionViewType.Stacking || targetDoc._viewType === CollectionViewType.NoteTaking) {
-                const scroll = targetDoc._scrollTop;
-                activeDoc.presPinView = true;
-                activeDoc.presPinViewScroll = scroll;
-            } else if ((targetDoc.type === DocumentType.COL && targetDoc._viewType === CollectionViewType.Freeform) || targetDoc.type === DocumentType.IMG || targetDoc.type === DocumentType.MAP) {
-                const x = targetDoc._panX;
-                const y = targetDoc._panY;
-                const scale = targetDoc._viewScale;
-                activeDoc.presPinView = true;
-                activeDoc.presPinViewX = x;
-                activeDoc.presPinViewY = y;
-                activeDoc.presPinViewScale = scale;
-            } else if (targetDoc.type === DocumentType.VID) {
-                activeDoc.presPinView = true;
-            } else if (targetDoc.type === DocumentType.COMPARISON) {
-                const width = targetDoc._clipWidth;
-                activeDoc.presPinClipWidth = width;
-                activeDoc.presPinView = true;
-            }
-        }
-    };
-
-    @computed
-    get pinWithViewButton() {
-        const presPinWithViewIcon = <img src={`/assets/pinWithView.png`} style={{ margin: 'auto', width: 19 }} />;
-        return !this.selectedDoc ? null : (
-            <Tooltip title={<div className="dash-tooltip">{'Pin with current view'}</div>} placement="top">
-                <button className="antimodeMenu-button" style={{ justifyContent: 'center' }} onClick={() => this.pinWithView(this.selectedDoc)}>
-                    {presPinWithViewIcon}
-                </button>
-            </Tooltip>
-        );
-    }
-
     @undoBatch
     onAlias = () => {
         if (this.selectedDoc && this.selectedDocumentView) {
@@ -722,7 +679,6 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu
                         {this.aliasButton}
                         {/* {this.pinButton} */}
                         {this.toggleOverlayButton}
-                        {this.pinWithViewButton}
                         <div className="collectionMenu-divider" key="divider2"></div>
                         {this.subChrome}
                         <div className="collectionMenu-divider" key="divider3"></div>
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index e147f34d2..73574bdb3 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -242,43 +242,8 @@ export class TabDocView extends React.Component<TabDocViewProps> {
                 const size: number = PresBox.Instance?._selectedArray.size;
                 const presSelected: Doc | undefined = presArray && size ? presArray[size - 1] : undefined;
                 const duration = NumCast(doc[`${Doc.LayoutFieldKey(pinDoc)}-duration`], null);
-                // If pinWithView option set then update scale and x / y props of slide
-                if (pinProps?.pinWithView) {
-                    const viewProps = pinProps.pinWithView;
-                    pinDoc.presPinView = true;
-                    pinDoc.presPinViewX = viewProps.bounds.left + viewProps.bounds.width / 2;
-                    pinDoc.presPinViewY = viewProps.bounds.top + viewProps.bounds.height / 2;
-                    pinDoc.presPinViewScale = viewProps.scale;
-                    pinDoc.contentBounds = new List<number>([viewProps.bounds.left, viewProps.bounds.top, viewProps.bounds.left + viewProps.bounds.width, viewProps.bounds.top + viewProps.bounds.height]);
-                }
-                if (pinProps?.pinDocView) {
-                    const scrollable = [DocumentType.PDF, DocumentType.RTF, DocumentType.WEB].includes(pinDoc.type as any) || pinDoc._viewType === CollectionViewType.Stacking;
-                    const pannable: boolean = (pinDoc.type === DocumentType.COL && doc._viewType === CollectionViewType.Freeform) || doc.type === DocumentType.IMG;
-                    if (scrollable) {
-                        const scroll = doc._scrollTop;
-                        pinDoc.presPinView = true;
-                        pinDoc.presPinViewScroll = scroll;
-                    } else if ([DocumentType.AUDIO, DocumentType.VID].includes(doc.type as any)) {
-                        pinDoc.presPinView = true;
-                        pinDoc.presStartTime = doc._currentTimecode;
-                        pinDoc.presEndTime = NumCast(doc._currentTimecode) + 0.1;
-                    } else if (pannable) {
-                        pinDoc.presPinView = true;
-                        pinDoc.presPinViewX = pinDoc._panX;
-                        pinDoc.presPinViewY = pinDoc._panY;
-                        pinDoc.presPinViewScale = pinDoc._viewScale;
-                        const pw = NumCast(pinProps.panelWidth);
-                        const ph = NumCast(pinProps.panelHeight);
-                        const ps = NumCast(pinDoc._viewScale);
-                        if (pw && ph && ps) {
-                            pinDoc.contentBounds = new List<number>([NumCast(pinDoc.panX) - pw / 2 / ps, NumCast(pinDoc.panY) - ph / 2 / ps, NumCast(pinDoc.panX) + pw / 2 / ps, NumCast(pinDoc.panY) + ph / 2 / ps]);
-                        }
-                    } else if (doc.type === DocumentType.COMPARISON) {
-                        const width = doc._clipWidth;
-                        pinDoc.presPinClipWidth = width;
-                        pinDoc.presPinView = true;
-                    }
-                }
+
+                PresBox.pinDocView(pinDoc, pinProps);
                 pinDoc.onClick = ScriptField.MakeFunction('navigateToDoc(self.presentationTargetDoc, self)');
                 Doc.AddDocToList(curPres, 'data', pinDoc, presSelected);
                 if (!pinProps?.audioRange && duration !== undefined) {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 03beaf65e..052cbd3bb 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -2213,3 +2213,6 @@ ScriptingGlobals.add(function nextKeyFrame(readOnly: boolean) {
 ScriptingGlobals.add(function prevKeyFrame(readOnly: boolean) {
     !readOnly && (SelectionManager.Views()[0].ComponentView as CollectionFreeFormView)?.changeKeyFrame(true);
 });
+ScriptingGlobals.add(function pinWithView(readOnly: boolean) {
+    !readOnly && SelectionManager.Views().forEach(view => TabDocView.PinDoc(view.rootDoc, { pinDocView: true, panelWidth: view.props.PanelWidth(), panelHeight: view.props.PanelHeight() }));
+});
diff --git a/src/client/views/nodes/button/FontIconBox.tsx b/src/client/views/nodes/button/FontIconBox.tsx
index fc5bf86f4..c72b5ca9b 100644
--- a/src/client/views/nodes/button/FontIconBox.tsx
+++ b/src/client/views/nodes/button/FontIconBox.tsx
@@ -254,16 +254,13 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
         const backgroundColor = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor);
 
         const script = ScriptCast(this.rootDoc.script);
-        if (!script) {
-            return null;
-        }
 
         let noviceList: string[] = [];
         let text: string | undefined;
         let dropdown = true;
         let icon: IconProp = 'caret-down';
         try {
-            if (script.script.originalScript.startsWith('setView')) {
+            if (script?.script.originalScript.startsWith('setView')) {
                 const selected = SelectionManager.Docs().lastElement();
                 if (selected) {
                     if (StrCast(selected.type) === DocumentType.COL) {
@@ -279,7 +276,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
                     text = 'User Default';
                 }
                 noviceList = [CollectionViewType.Freeform, CollectionViewType.Schema, CollectionViewType.Stacking];
-            } else if (script.script.originalScript.startsWith('setFont')) {
+            } else if (script?.script.originalScript.startsWith('setFont')) {
                 const editorView = RichTextMenu.Instance?.TextView?.EditorView;
                 text = StrCast((editorView ? RichTextMenu.Instance : Doc.UserDoc()).fontFamily);
                 noviceList = ['Roboto', 'Times New Roman', 'Arial', 'Georgia', 'Comic Sans MS', 'Tahoma', 'Impact', 'Crimson Text'];
@@ -289,11 +286,9 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
         }
 
         // Get items to place into the list
-        const list = this.buttonList.map(value => {
-            if (Doc.noviceMode && !noviceList.includes(value)) {
-                return;
-            }
-            return (
+        const list = this.buttonList
+            .filter(value => !Doc.noviceMode || noviceList.includes(value))
+            .map(value => (
                 <div
                     className="list-item"
                     key={`${value}`}
@@ -304,8 +299,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
                     onClick={() => script.script.run({ value }).result}>
                     {value[0].toUpperCase() + value.slice(1)}
                 </div>
-            );
-        });
+            ));
 
         const label =
             !this.label || !FontIconBox.GetShowLabels() ? null : (
@@ -451,7 +445,6 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
     @computed get defaultButton() {
         const color = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Color);
         const backgroundColor = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor);
-        const active: string = StrCast(this.rootDoc.dropDownOpen);
         return (
             <div className={`menuButton ${this.type}`} onContextMenu={this.specificContextMenu} style={{ backgroundColor: 'transparent', borderBottomLeftRadius: this.dropdown ? 0 : undefined }}>
                 <div className="menuButton-wrap">
@@ -487,89 +480,50 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
     render() {
         const color = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Color);
         const backgroundColor = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor);
-        const label =
-            !this.label || !FontIconBox.GetShowLabels() ? null : (
-                <div className="fontIconBox-label" style={{ color, backgroundColor }}>
-                    {this.label}
-                </div>
-            );
-
-        const menuLabel =
+        const label = (noBackground: boolean = false) =>
             !this.label || !FontIconBox.GetShowLabels() ? null : (
-                <div className="fontIconBox-label" style={{ color, backgroundColor: 'transparent' }}>
+                <div className="fontIconBox-label" style={{ color, backgroundColor: noBackground ? 'transparent' : backgroundColor }}>
                     {this.label}
                 </div>
             );
-
         // TODO:glr Add label of button type
-        let button: JSX.Element | null = this.defaultButton;
+        let button: JSX.Element = this.defaultButton;
 
+        // prettier-ignore
         switch (this.type) {
-            case ButtonType.TextButton:
-                button = (
+            case ButtonType.DropdownList: return this.dropdownListButton;
+            case ButtonType.ColorButton: return this.colorButton; 
+            case ButtonType.NumberButton: return this.numberButton; 
+            case ButtonType.EditableText: return this.editableText;
+            case ButtonType.DropdownButton: button = this.dropdownButton; break;
+            case ButtonType.ToggleButton: button = this.toggleButton; break;
+            case ButtonType.TextButton: button = (
                     <div className={`menuButton ${this.type}`} style={{ color, backgroundColor, opacity: 1, gridAutoColumns: `${NumCast(this.rootDoc._height)} auto` }}>
                         {this.Icon(color)}
                         {StrCast(this.rootDoc.buttonText) ? <div className="button-text">{StrCast(this.rootDoc.buttonText)}</div> : null}
-                        {label}
-                    </div>
-                );
-                // button = <TextButton {...buttonProps}></TextButton>
-                break;
-            case ButtonType.EditableText:
-                button = this.editableText;
-                break;
-            case ButtonType.NumberButton:
-                button = this.numberButton;
-                break;
-            case ButtonType.DropdownButton:
-                button = this.dropdownButton;
-                break;
-            case ButtonType.DropdownList:
-                button = this.dropdownListButton;
-                break;
-            case ButtonType.ColorButton:
-                button = this.colorButton;
-                break;
-            case ButtonType.ToolButton:
-                button = (
-                    <div className={`menuButton ${this.type + (FontIconBox.GetShowLabels() ? 'Label' : '')}`} style={{ opacity: 1, backgroundColor, color }}>
-                        {this.Icon(color)}
-                        {label}
+                        {label()}
                     </div>
                 );
                 break;
-            case ButtonType.ToggleButton:
-                button = this.toggleButton;
-                // button = <ToggleButton {...buttonProps}></ToggleButton>
-                break;
             case ButtonType.ClickButton:
-                button = (
-                    <div className={`menuButton ${this.type + (FontIconBox.GetShowLabels() ? 'Label' : '')}`} style={{ color, backgroundColor, opacity: 1 }}>
+            case ButtonType.ToolButton: button = (
+                    <div className={`menuButton ${this.type + (FontIconBox.GetShowLabels() ? 'Label' : '')}`} style={{ backgroundColor, color, opacity: 1 }}>
                         {this.Icon(color)}
-                        {label}
+                        {label()}
                     </div>
                 );
                 break;
-            case ButtonType.MenuButton:
-                button = (
+            case ButtonType.MenuButton: button = (
                     <div className={`menuButton ${this.type}`} style={{ color, backgroundColor }}>
                         {this.Icon(color)}
-                        {menuLabel}
+                        {label(true)}
                         <FontIconBadge value={Cast(this.Document.badgeValue, 'string', null)} />
                     </div>
                 );
                 break;
-            default:
-                break;
         }
 
-        const retval =
-            !this.layoutDoc.toolTip || this.type === ButtonType.DropdownList || this.type === ButtonType.ColorButton || this.type === ButtonType.NumberButton || this.type === ButtonType.EditableText ? (
-                button
-            ) : button !== null ? (
-                <Tooltip title={<div className="dash-tooltip">{StrCast(this.layoutDoc.toolTip)}</div>}>{button}</Tooltip>
-            ) : null;
-        return retval;
+        return !this.layoutDoc.toolTip ? button : <Tooltip title={<div className="dash-tooltip">{StrCast(this.layoutDoc.toolTip)}</div>}>{button}</Tooltip>;
     }
 }
 
@@ -698,7 +652,7 @@ ScriptingGlobals.add(function setFontHighlight(color?: string, checkResult?: boo
 ScriptingGlobals.add(function setFontSize(size: string | number, checkResult?: boolean) {
     const editorView = RichTextMenu.Instance?.TextView?.EditorView;
     if (checkResult) {
-        return RichTextMenu.Instance.fontSize.replace('px', '');
+        return RichTextMenu.Instance?.fontSize.replace('px', '');
     }
     if (typeof size === 'number') size = size.toString();
     if (size && Number(size).toString() === size) size += 'px';
diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx
index 2a77210ae..0cbe60c0c 100644
--- a/src/client/views/nodes/formattedText/RichTextMenu.tsx
+++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx
@@ -16,7 +16,7 @@ import { SelectionManager } from '../../../util/SelectionManager';
 import { undoBatch, UndoManager } from '../../../util/UndoManager';
 import { AntimodeMenu, AntimodeMenuProps } from '../../AntimodeMenu';
 import { FieldViewProps } from '../FieldView';
-import { FormattedTextBox, FormattedTextBoxProps } from './FormattedTextBox';
+import { FormattedTextBox } from './FormattedTextBox';
 import { updateBullets } from './ProsemirrorExampleTransfer';
 import './RichTextMenu.scss';
 import { schema } from './schema_rts';
@@ -29,7 +29,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
     private _linkToRef = React.createRef<HTMLInputElement>();
 
     @observable public view?: EditorView;
-    public editorProps: (FieldViewProps & FormattedTextBoxProps) | undefined;
+    public editorProps: FieldViewProps | undefined;
 
     public _brushMap: Map<string, Set<Mark>> = new Map();
 
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index 05e09361b..eb40089ec 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -358,20 +358,24 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         if (bestTarget) this._navTimer = PresBox.navigateToDoc(bestTarget, activeItem, false);
     };
 
+    static pinDataTypes(target: Doc) {
+        const scrollable = [DocumentType.PDF, DocumentType.RTF, DocumentType.WEB].includes(target.type as any) || target._viewType === CollectionViewType.Stacking;
+        const pannable = [DocumentType.IMG].includes(target.type as any) || (target.type === DocumentType.COL && target._viewType === CollectionViewType.Freeform);
+        const temporal = [DocumentType.AUDIO, DocumentType.VID].includes(target.type as any);
+        const clippable = [DocumentType.COMPARISON].includes(target.type as any);
+        return { scrollable, pannable, temporal, clippable };
+    }
     // navigates to the bestTarget document by making sure it is on screen,
     // then it applies the view specs stored in activeItem to
     @action
     static navigateToDoc(bestTarget: Doc, activeItem: Doc, jumpToDoc: boolean) {
-        if (bestTarget.type === DocumentType.PDF || bestTarget.type === DocumentType.WEB || bestTarget.type === DocumentType.RTF || bestTarget._viewType === CollectionViewType.Stacking) {
-            bestTarget._viewTransition = activeItem.presTransition ? `transform ${activeItem.presTransition}ms` : 'all 0.5s';
-            bestTarget._scrollTop = activeItem.presPinViewScroll;
-        } else if (bestTarget.type === DocumentType.COMPARISON) {
-            bestTarget._clipWidth = activeItem.presPinClipWidth;
-        } else if ([DocumentType.AUDIO, DocumentType.VID].includes(bestTarget.type as any)) {
-            bestTarget._currentTimecode = activeItem.presStartTime;
-        } else {
+        bestTarget._viewTransition = activeItem.presTransition ? `transform ${activeItem.presTransition}ms` : 'all 0.5s';
+        const { scrollable, pannable, temporal, clippable } = this.pinDataTypes(bestTarget);
+        if (clippable) bestTarget._clipWidth = activeItem.presPinClipWidth;
+        if (temporal) bestTarget._currentTimecode = activeItem.presStartTime;
+        if (scrollable) bestTarget._scrollTop = activeItem.presPinViewScroll;
+        if (pannable) {
             const contentBounds = Cast(activeItem.contentBounds, listSpec('number'));
-            bestTarget._viewTransition = activeItem.presTransition ? `transform ${activeItem.presTransition}ms` : 'all 0.5s';
             if (contentBounds) {
                 bestTarget._panX = (contentBounds[0] + contentBounds[2]) / 2;
                 bestTarget._panY = (contentBounds[1] + contentBounds[3]) / 2;
@@ -388,6 +392,43 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         return setTimeout(() => (bestTarget._viewTransition = undefined), activeItem.presTransition ? NumCast(activeItem.presTransition) + 10 : 510);
     }
 
+    /// copies values from the targetDoc (which is the prototype of the pinDoc) to
+    /// reserved fields on the pinDoc so that those values can be restored to the
+    /// target doc when navigating to it.
+    @action
+    static pinDocView(pinDoc: Doc, pinProps: PinProps | undefined) {
+        if (pinProps?.pinWithView) {
+            // If pinWithView option set then update scale and x / y props of slide
+            const bounds = pinProps.pinWithView.bounds;
+            pinDoc.presPinView = true;
+            pinDoc.presPinViewX = bounds.left + bounds.width / 2;
+            pinDoc.presPinViewY = bounds.top + bounds.height / 2;
+            pinDoc.presPinViewScale = pinProps.pinWithView.scale;
+            pinDoc.contentBounds = new List<number>([bounds.left, bounds.top, bounds.left + bounds.width, bounds.top + bounds.height]);
+        }
+        if (pinProps?.pinDocView) {
+            const { scrollable, pannable, temporal, clippable } = this.pinDataTypes(pinDoc);
+            pinDoc.presPinView = (pinProps?.pinWithView ? true : false) || scrollable || temporal || pannable || clippable;
+
+            if (scrollable) pinDoc.presPinViewScroll = pinDoc._scrollTop;
+            else if (clippable) pinDoc.presPinClipWidth = pinDoc._clipWidth;
+            else if (temporal) pinDoc.presEndTime = NumCast((pinDoc.presStartTime = pinDoc._currentTimecode)) + 0.1;
+            else if (pannable) {
+                const panX = NumCast(pinDoc._panX);
+                const panY = NumCast(pinDoc._panY);
+                const pw = NumCast(pinProps.panelWidth);
+                const ph = NumCast(pinProps.panelHeight);
+                const ps = NumCast(pinDoc._viewScale);
+                if (pw && ph && ps) {
+                    pinDoc.contentBounds = new List<number>([panX - pw / 2 / ps, panY - ph / 2 / ps, panX + pw / 2 / ps, panY + ph / 2 / ps]);
+                }
+                pinDoc.presPinViewX = panX;
+                pinDoc.presPinViewY = panY;
+                pinDoc.presPinViewScale = ps;
+            }
+        }
+    }
+
     /**
      * This method makes sure that cursor navigates to the element that
      * has the option open and last in the group.
@@ -1540,26 +1581,15 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         }
     }
 
-    @computed get effectDirection(): string {
-        let effect = '';
+    @computed get effectDirection() {
+        // prettier-ignore
         switch (this.activeItem.presEffectDirection) {
-            case 'left':
-                effect = 'Enter from left';
-                break;
-            case 'right':
-                effect = 'Enter from right';
-                break;
-            case 'top':
-                effect = 'Enter from top';
-                break;
-            case 'bottom':
-                effect = 'Enter from bottom';
-                break;
-            default:
-                effect = 'Enter from center';
-                break;
+            case 'left': return 'Enter from left'; 
+            case 'right': return 'Enter from right'; 
+            case 'top':  return'Enter from top';
+            case 'bottom': return 'Enter from bottom';
         }
-        return effect;
+        return 'Enter from center';
     }
 
     @undoBatch
@@ -1582,179 +1612,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         });
     };
 
-    @computed get presPinViewOptionsDropdown() {
-        const activeItem: Doc = this.activeItem;
-        const targetDoc: Doc = this.targetDoc;
-        const presPinWithViewIcon = <img src="/assets/pinWithView.png" style={{ margin: 'auto', width: 16, filter: 'invert(1)' }} />;
-        return (
-            <>
-                {this.panable || this.scrollable || this.targetDoc.type === DocumentType.COMPARISON ? 'Pinned view' : null}
-                <div className="ribbon-doubleButton">
-                    <Tooltip
-                        title={
-                            <>
-                                <div className="dash-tooltip">{activeItem.presPinView ? 'Turn off pin with view' : 'Turn on pin with view'}</div>
-                            </>
-                        }>
-                        <div
-                            className="ribbon-toggle"
-                            style={{ width: 20, padding: 0, backgroundColor: activeItem.presPinView ? Colors.LIGHT_BLUE : '' }}
-                            onClick={() => {
-                                activeItem.presPinView = !activeItem.presPinView;
-                                targetDoc.presPinView = activeItem.presPinView;
-                                if (activeItem.presPinView) {
-                                    if (targetDoc.type === DocumentType.PDF || targetDoc.type === DocumentType.RTF || targetDoc.type === DocumentType.WEB || targetDoc._viewType === CollectionViewType.Stacking) {
-                                        const scroll = targetDoc._scrollTop;
-                                        activeItem.presPinView = true;
-                                        activeItem.presPinViewScroll = scroll;
-                                    } else if ([DocumentType.AUDIO, DocumentType.VID].includes(targetDoc.type as any)) {
-                                        activeItem.presStartTime = targetDoc._currentTimecode;
-                                        activeItem.presEndTime = NumCast(targetDoc._currentTimecode) + 0.1;
-                                    } else if ((targetDoc.type === DocumentType.COL && targetDoc._viewType === CollectionViewType.Freeform) || targetDoc.type === DocumentType.IMG) {
-                                        const x = targetDoc._panX;
-                                        const y = targetDoc._panY;
-                                        const scale = targetDoc._viewScale;
-                                        activeItem.presPinView = true;
-                                        activeItem.presPinViewX = x;
-                                        activeItem.presPinViewY = y;
-                                        activeItem.presPinViewScale = scale;
-                                    } else if (targetDoc.type === DocumentType.COMPARISON) {
-                                        const width = targetDoc._clipWidth;
-                                        activeItem.presPinClipWidth = width;
-                                        activeItem.presPinView = true;
-                                    }
-                                }
-                            }}>
-                            {presPinWithViewIcon}
-                        </div>
-                    </Tooltip>
-                    {activeItem.presPinView ? (
-                        <Tooltip
-                            title={
-                                <>
-                                    <div className="dash-tooltip">{'Update the pinned view with the view of the selected document'}</div>
-                                </>
-                            }>
-                            <div
-                                className="ribbon-button"
-                                onClick={() => {
-                                    if (targetDoc.type === DocumentType.PDF || targetDoc.type === DocumentType.WEB || targetDoc.type === DocumentType.RTF) {
-                                        const scroll = targetDoc._scrollTop;
-                                        activeItem.presPinViewScroll = scroll;
-                                    } else if ([DocumentType.AUDIO, DocumentType.VID].includes(targetDoc.type as any)) {
-                                        activeItem.presStartTime = targetDoc._currentTimecode;
-                                        activeItem.presStartTime = NumCast(targetDoc._currentTimecode) + 0.1;
-                                    } else if (targetDoc.type === DocumentType.COMPARISON) {
-                                        const clipWidth = targetDoc._clipWidth;
-                                        activeItem.presPinClipWidth = clipWidth;
-                                    } else {
-                                        const x = targetDoc._panX;
-                                        const y = targetDoc._panY;
-                                        const scale = targetDoc._viewScale;
-                                        activeItem.presPinViewX = x;
-                                        activeItem.presPinViewY = y;
-                                        activeItem.presPinViewScale = scale;
-                                    }
-                                }}>
-                                Update
-                            </div>
-                        </Tooltip>
-                    ) : null}
-                </div>
-            </>
-        );
-    }
-
-    @computed get panOptionsDropdown() {
-        const activeItem: Doc = this.activeItem;
-        const targetDoc: Doc = this.targetDoc;
-        return (
-            <>
-                {this.panable ? (
-                    <div style={{ display: activeItem.presPinView ? 'block' : 'none' }}>
-                        <div className="ribbon-doubleButton" style={{ marginRight: 10 }}>
-                            <div className="presBox-subheading">Pan X</div>
-                            <div className="ribbon-property" style={{ paddingRight: 0, paddingLeft: 0 }}>
-                                <input
-                                    className="presBox-input"
-                                    style={{ textAlign: 'left', width: 50 }}
-                                    type="number"
-                                    value={NumCast(activeItem.presPinViewX)}
-                                    onKeyDown={e => e.stopPropagation()}
-                                    onChange={action((e: React.ChangeEvent<HTMLInputElement>) => {
-                                        const val = e.target.value;
-                                        activeItem.presPinViewX = Number(val);
-                                    })}
-                                />
-                            </div>
-                        </div>
-                        <div className="ribbon-doubleButton" style={{ marginRight: 10 }}>
-                            <div className="presBox-subheading">Pan Y</div>
-                            <div className="ribbon-property" style={{ paddingRight: 0, paddingLeft: 0 }}>
-                                <input
-                                    className="presBox-input"
-                                    style={{ textAlign: 'left', width: 50 }}
-                                    type="number"
-                                    value={NumCast(activeItem.presPinViewY)}
-                                    onKeyDown={e => e.stopPropagation()}
-                                    onChange={action((e: React.ChangeEvent<HTMLInputElement>) => {
-                                        const val = e.target.value;
-                                        activeItem.presPinViewY = Number(val);
-                                    })}
-                                />
-                            </div>
-                        </div>
-                        <div className="ribbon-doubleButton" style={{ marginRight: 10 }}>
-                            <div className="presBox-subheading">Scale</div>
-                            <div className="ribbon-property" style={{ paddingRight: 0, paddingLeft: 0 }}>
-                                <input
-                                    className="presBox-input"
-                                    style={{ textAlign: 'left', width: 50 }}
-                                    type="number"
-                                    value={NumCast(activeItem.presPinViewScale)}
-                                    onKeyDown={e => e.stopPropagation()}
-                                    onChange={action((e: React.ChangeEvent<HTMLInputElement>) => {
-                                        const val = e.target.value;
-                                        activeItem.presPinViewScale = Number(val);
-                                    })}
-                                />
-                            </div>
-                        </div>
-                    </div>
-                ) : null}
-            </>
-        );
-    }
-
-    @computed get scrollOptionsDropdown() {
-        const activeItem: Doc = this.activeItem;
-        const targetDoc: Doc = this.targetDoc;
-        return (
-            <>
-                {this.scrollable ? (
-                    <div style={{ display: activeItem.presPinView ? 'block' : 'none' }}>
-                        <div className="ribbon-doubleButton" style={{ marginRight: 10 }}>
-                            <div className="presBox-subheading">Scroll</div>
-                            <div className="ribbon-property" style={{ paddingRight: 0, paddingLeft: 0 }}>
-                                <input
-                                    className="presBox-input"
-                                    style={{ textAlign: 'left', width: 50 }}
-                                    type="number"
-                                    value={NumCast(activeItem.presPinViewScroll)}
-                                    onKeyDown={e => e.stopPropagation()}
-                                    onChange={action((e: React.ChangeEvent<HTMLInputElement>) => {
-                                        const val = e.target.value;
-                                        activeItem.presPinViewScroll = Number(val);
-                                    })}
-                                />
-                            </div>
-                        </div>
-                    </div>
-                ) : null}
-            </>
-        );
-    }
-
     @computed get mediaStopSlides() {
         const activeItem: Doc = this.activeItem;
         const list = this.childDocs.map((doc, i) => {
@@ -2102,42 +1959,23 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     };
 
     createTemplate = (layout: string, input?: string) => {
-        const activeItem: Doc = this.activeItem;
-        const targetDoc: Doc = this.targetDoc;
-        let x = 0;
-        let y = 0;
-        if (activeItem && targetDoc) {
-            x = NumCast(targetDoc.x);
-            y = NumCast(targetDoc.y) + NumCast(targetDoc._height) + 20;
-        }
-        let doc = undefined;
-        const title = Docs.Create.TextDocument('Click to change title', { title: 'Slide title', _width: 380, _height: 60, x: 10, y: 58, _fontSize: '24pt' });
-        const subtitle = Docs.Create.TextDocument('Click to change subtitle', { title: 'Slide subtitle', _width: 380, _height: 50, x: 10, y: 118, _fontSize: '16pt' });
-        const header = Docs.Create.TextDocument('Click to change header', { title: 'Slide header', _width: 380, _height: 65, x: 10, y: 80, _fontSize: '20pt' });
-        const contentTitle = Docs.Create.TextDocument('Click to change title', { title: 'Slide title', _width: 380, _height: 60, x: 10, y: 10, _fontSize: '24pt' });
-        const content = Docs.Create.TextDocument('Click to change text', { title: 'Slide text', _width: 380, _height: 145, x: 10, y: 70, _fontSize: '14pt' });
-        const content1 = Docs.Create.TextDocument('Click to change text', { title: 'Column 1', _width: 185, _height: 140, x: 10, y: 80, _fontSize: '14pt' });
-        const content2 = Docs.Create.TextDocument('Click to change text', { title: 'Column 2', _width: 185, _height: 140, x: 205, y: 80, _fontSize: '14pt' });
+        const x = this.activeItem && this.targetDoc ? NumCast(this.targetDoc.x) : 0;
+        const y = this.activeItem && this.targetDoc ? NumCast(this.targetDoc.y) + NumCast(this.targetDoc._height) + 20 : 0;
+        const title = () => Docs.Create.TextDocument('Click to change title', { title: 'Slide title', _width: 380, _height: 60, x: 10, y: 58, _fontSize: '24pt' });
+        const subtitle = () => Docs.Create.TextDocument('Click to change subtitle', { title: 'Slide subtitle', _width: 380, _height: 50, x: 10, y: 118, _fontSize: '16pt' });
+        const header = () => Docs.Create.TextDocument('Click to change header', { title: 'Slide header', _width: 380, _height: 65, x: 10, y: 80, _fontSize: '20pt' });
+        const contentTitle = () => Docs.Create.TextDocument('Click to change title', { title: 'Slide title', _width: 380, _height: 60, x: 10, y: 10, _fontSize: '24pt' });
+        const content = () => Docs.Create.TextDocument('Click to change text', { title: 'Slide text', _width: 380, _height: 145, x: 10, y: 70, _fontSize: '14pt' });
+        const content1 = () => Docs.Create.TextDocument('Click to change text', { title: 'Column 1', _width: 185, _height: 140, x: 10, y: 80, _fontSize: '14pt' });
+        const content2 = () => Docs.Create.TextDocument('Click to change text', { title: 'Column 2', _width: 185, _height: 140, x: 205, y: 80, _fontSize: '14pt' });
+        // prettier-ignore
         switch (layout) {
-            case 'blank':
-                doc = Docs.Create.FreeformDocument([], { title: input ? input : 'Blank slide', _width: 400, _height: 225, x: x, y: y });
-                break;
-            case 'title':
-                doc = Docs.Create.FreeformDocument([title, subtitle], { title: input ? input : 'Title slide', _width: 400, _height: 225, _fitContentsToBox: true, x: x, y: y });
-                break;
-            case 'header':
-                doc = Docs.Create.FreeformDocument([header], { title: input ? input : 'Section header', _width: 400, _height: 225, _fitContentsToBox: true, x: x, y: y });
-                break;
-            case 'content':
-                doc = Docs.Create.FreeformDocument([contentTitle, content], { title: input ? input : 'Title and content', _width: 400, _height: 225, _fitContentsToBox: true, x: x, y: y });
-                break;
-            case 'twoColumns':
-                doc = Docs.Create.FreeformDocument([contentTitle, content1, content2], { title: input ? input : 'Title and two columns', _width: 400, _height: 225, _fitContentsToBox: true, x: x, y: y });
-                break;
-            default:
-                break;
+            case 'blank': return Docs.Create.FreeformDocument([], { title: input ? input : 'Blank slide', _width: 400, _height: 225, x, y });
+            case 'title': return Docs.Create.FreeformDocument([title(), subtitle()], { title: input ? input : 'Title slide', _width: 400, _height: 225, _fitContentsToBox: true, x, y });
+            case 'header': return Docs.Create.FreeformDocument([header()], { title: input ? input : 'Section header', _width: 400, _height: 225, _fitContentsToBox: true, x, y });
+            case 'content': return Docs.Create.FreeformDocument([contentTitle(), content()], { title: input ? input : 'Title and content', _width: 400, _height: 225, _fitContentsToBox: true, x, y });
+            case 'twoColumns': return Docs.Create.FreeformDocument([contentTitle(), content1(), content2()], { title: input ? input : 'Title and two columns', _width: 400, _height: 225, _fitContentsToBox: true, x, y })
         }
-        return doc;
     };
 
     // Dropdown that appears when the user wants to begin presenting (either minimize or sidebar view)
@@ -2201,42 +2039,22 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     /**
      * Returns the collection type as a string for headers
      */
-    @computed get stringType(): string {
-        const activeItem: Doc = this.activeItem;
-        const targetDoc: Doc = this.targetDoc;
-        let type: string = '';
-        if (activeItem) {
-            switch (targetDoc.type) {
-                case DocumentType.PDF:
-                    type = 'PDF';
-                    break;
-                case DocumentType.RTF:
-                    type = 'Text node';
-                    break;
-                case DocumentType.COL:
-                    type = 'Collection';
-                    break;
-                case DocumentType.AUDIO:
-                    type = 'Audio';
-                    break;
-                case DocumentType.VID:
-                    type = 'Video';
-                    break;
-                case DocumentType.IMG:
-                    type = 'Image';
-                    break;
-                case DocumentType.WEB:
-                    type = 'Web page';
-                    break;
-                case DocumentType.MAP:
-                    type = 'Map';
-                    break;
-                default:
-                    type = 'Other node';
-                    break;
+    @computed get stringType() {
+        if (this.activeItem) {
+            // prettier-ignore
+            switch (this.targetDoc.type) {
+                case DocumentType.PDF: return 'PDF';
+                case DocumentType.RTF: return 'Text node';
+                case DocumentType.COL: return 'Collection';
+                case DocumentType.AUDIO: return 'Audio';
+                case DocumentType.VID: return 'Video';
+                case DocumentType.IMG: return 'Image';
+                case DocumentType.WEB: return 'Web page';
+                case DocumentType.MAP: return 'Map';
+                default: return 'Other node';
             }
         }
-        return type;
+        return '';
     }
 
     @observable private openActiveColorPicker: boolean = false;
@@ -2845,10 +2663,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             case DocumentType.PDF || DocumentType.RTF || DocumentType.WEB:
                 this.updateList(activeItem.frameList);
                 break;
-            case DocumentType.COL:
-                break;
-            default:
-                break;
         }
     };
 
-- 
cgit v1.2.3-70-g09d2


From 9263422913f30b54922f3c0d7290e36d4a509455 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Fri, 26 Aug 2022 12:43:00 -0400
Subject: extending pres trails to allow ink animation without using keyframes.

---
 src/client/views/MainView.tsx                      |  2 +-
 src/client/views/collections/CollectionMenu.tsx    | 20 +++----
 src/client/views/collections/TabDocView.tsx        |  2 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  1 -
 .../views/nodes/CollectionFreeFormDocumentView.tsx | 10 ++--
 src/client/views/nodes/trails/PresBox.tsx          | 67 +++++++++++++++-------
 6 files changed, 61 insertions(+), 41 deletions(-)

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

diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 06be4d194..515faa316 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -154,10 +154,10 @@ export class MainView extends React.Component {
         if (!MainView.Live) {
             DocServer.setPlaygroundFields([
                 'dataTransition',
+                'viewTransition',
                 'treeViewOpen',
                 'showSidebar',
                 'sidebarWidthPercent',
-                'viewTransition',
                 'panX',
                 'panY',
                 'fitWidth',
diff --git a/src/client/views/collections/CollectionMenu.tsx b/src/client/views/collections/CollectionMenu.tsx
index 0dc30e0fd..6a0f69359 100644
--- a/src/client/views/collections/CollectionMenu.tsx
+++ b/src/client/views/collections/CollectionMenu.tsx
@@ -726,18 +726,16 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionView
     }
 
     public static gotoKeyFrame(doc: Doc, newFrame: number) {
-        if (!doc) {
-            return;
-        }
-        const dataField = doc[Doc.LayoutFieldKey(doc)];
-        const childDocs = DocListCast(dataField);
-        const currentFrame = Cast(doc._currentFrame, 'number', null);
-        if (currentFrame === undefined) {
-            doc._currentFrame = 0;
-            CollectionFreeFormDocumentView.setupKeyframes(childDocs, 0);
+        if (doc) {
+            const childDocs = DocListCast(doc[Doc.LayoutFieldKey(doc)]);
+            const currentFrame = Cast(doc._currentFrame, 'number', null);
+            if (currentFrame === undefined) {
+                doc._currentFrame = 0;
+                CollectionFreeFormDocumentView.setupKeyframes(childDocs, 0);
+            }
+            CollectionFreeFormDocumentView.updateKeyframe(childDocs, currentFrame || 0);
+            doc._currentFrame = newFrame === undefined ? 0 : Math.max(0, newFrame);
         }
-        CollectionFreeFormDocumentView.updateKeyframe(childDocs, currentFrame || 0);
-        doc._currentFrame = newFrame === undefined ? 0 : Math.max(0, newFrame);
     }
 
     @undoBatch
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index 73574bdb3..f7b48adf6 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -243,7 +243,7 @@ export class TabDocView extends React.Component<TabDocViewProps> {
                 const presSelected: Doc | undefined = presArray && size ? presArray[size - 1] : undefined;
                 const duration = NumCast(doc[`${Doc.LayoutFieldKey(pinDoc)}-duration`], null);
 
-                PresBox.pinDocView(pinDoc, pinProps);
+                PresBox.pinDocView(pinDoc, pinProps, doc);
                 pinDoc.onClick = ScriptField.MakeFunction('navigateToDoc(self.presentationTargetDoc, self)');
                 Doc.AddDocToList(curPres, 'data', pinDoc, presSelected);
                 if (!pinProps?.audioRange && duration !== undefined) {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index c44b33ed0..210370d39 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -278,7 +278,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 const vals = CollectionFreeFormDocumentView.getValues(d, NumCast(d.activeFrame, 1000), false); // get non-default values for everything else
                 vals.x = x + NumCast(pvals.x) - dropPos[0];
                 vals.y = y + NumCast(pvals.y) - dropPos[1];
-                vals._scrollTop = this.Document.editScrollProgressivize ? pvals._scrollTop : undefined;
                 CollectionFreeFormDocumentView.setValues(NumCast(this.Document._currentFrame), d, vals);
             } else {
                 d.x = x + NumCast(d.x) - dropPos[0];
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index efaecb8d2..82557d36b 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -90,14 +90,14 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
 
     public static getValues(doc: Doc, time: number, fillIn: boolean = true) {
         return CollectionFreeFormDocumentView.animFields.reduce((p, val) => {
-            p[val.key] = Cast(`${val}-indexed`, listSpec('number'), fillIn ? [NumCast(doc[val.key], val.val)] : []).reduce((p, v, i) => ((i <= Math.round(time) && v !== undefined) || p === undefined ? v : p), undefined as any as number);
+            p[val.key] = Cast(doc[`${val.key}-indexed`], listSpec('number'), fillIn ? [NumCast(doc[val.key], val.val)] : []).reduce((p, v, i) => ((i <= Math.round(time) && v !== undefined) || p === undefined ? v : p), undefined as any as number);
             return p;
         }, {} as { [val: string]: Opt<number> });
     }
 
     public static getStringValues(doc: Doc, time: number) {
         return CollectionFreeFormDocumentView.animStringFields.reduce((p, val) => {
-            p[val] = Cast(`${val}-indexed`, listSpec('string'), [StrCast(doc[val])]).reduce((p, v, i) => ((i <= Math.round(time) && v !== undefined) || p === undefined ? v : p), undefined as any as string);
+            p[val] = Cast(doc[`${val}-indexed`], listSpec('string'), [StrCast(doc[val])]).reduce((p, v, i) => ((i <= Math.round(time) && v !== undefined) || p === undefined ? v : p), undefined as any as string);
             return p;
         }, {} as { [val: string]: Opt<string> });
     }
@@ -140,8 +140,8 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
         );
     }
 
-    public static gotoKeyframe(docs: Doc[]) {
-        docs.forEach(doc => (doc._viewTransition = doc.dataTransition = 'all 1s'));
+    public static gotoKeyframe(docs: Doc[], duration = 1000) {
+        docs.forEach(doc => (doc._viewTransition = doc.dataTransition = `all ${duration}ms`));
         setTimeout(
             () =>
                 docs.forEach(doc => {
@@ -226,7 +226,6 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
             PanelWidth: this.panelWidth,
             PanelHeight: this.panelHeight,
         };
-        const mixBlendMode = undefined; // (StrCast(this.layoutDoc.mixBlendMode) as any) || (typeof background === 'string' && background && !background.startsWith('linear') && DashColor(background).alpha() !== 1 ? 'multiply' : undefined);
         return (
             <div
                 className={'collectionFreeFormDocumentView-container'}
@@ -238,7 +237,6 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
                     transformOrigin: '50% 50%',
                     transition: this.dataProvider?.transition ?? (this.props.dataTransition ? this.props.dataTransition : this.dataProvider ? this.dataProvider.transition : StrCast(this.layoutDoc.dataTransition)),
                     zIndex: this.ZInd,
-                    mixBlendMode: mixBlendMode,
                     display: this.ZInd === -99 ? 'none' : undefined,
                 }}>
                 {this.props.renderCutoffProvider(this.props.Document) ? (
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index eb40089ec..f254eaba6 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -6,7 +6,7 @@ import { observer } from 'mobx-react';
 import { ColorState, SketchPicker } from 'react-color';
 import { Bounce, Fade, Flip, LightSpeed, Roll, Rotate, Zoom } from 'react-reveal';
 import { Doc, DocListCast, DocListCastAsync, FieldResult } from '../../../../fields/Doc';
-import { InkTool } from '../../../../fields/InkField';
+import { InkField, InkTool } from '../../../../fields/InkField';
 import { List } from '../../../../fields/List';
 import { listSpec } from '../../../../fields/Schema';
 import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../../fields/Types';
@@ -19,8 +19,7 @@ import { SelectionManager } from '../../../util/SelectionManager';
 import { SettingsManager } from '../../../util/SettingsManager';
 import { undoBatch, UndoManager } from '../../../util/UndoManager';
 import { CollectionDockingView } from '../../collections/CollectionDockingView';
-import { MarqueeViewBounds } from '../../collections/collectionFreeForm';
-import { CollectionFreeFormViewChrome } from '../../collections/CollectionMenu';
+import { CollectionFreeFormView, MarqueeViewBounds } from '../../collections/collectionFreeForm';
 import { CollectionView } from '../../collections/CollectionView';
 import { TabDocView } from '../../collections/TabDocView';
 import { ViewBoxBaseComponent } from '../../DocComponent';
@@ -30,6 +29,8 @@ import { CollectionFreeFormDocumentView } from '../CollectionFreeFormDocumentVie
 import { FieldView, FieldViewProps } from '../FieldView';
 import './PresBox.scss';
 import { PresEffect, PresMovement, PresStatus } from './PresEnums';
+import { Copy } from '../../../../fields/FieldSymbols';
+import { CollectionFreeFormViewChrome } from '../../collections/CollectionMenu';
 
 export interface PinProps {
     audioRange?: boolean;
@@ -317,8 +318,15 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             const activeItem: Doc = this.activeItem;
             const targetDoc: Doc = this.targetDoc;
             if (activeItem.presActiveFrame !== undefined) {
+                const transTime = NumCast(activeItem.presDuration, 500);
                 const context = DocCast(DocCast(activeItem.presentationTargetDoc).context);
-                context && CollectionFreeFormViewChrome.gotoKeyFrame(context, NumCast(activeItem.presActiveFrame));
+                if (context) {
+                    const contextView = DocumentManager.Instance.getFirstDocumentView(context);
+                    if (contextView?.ComponentView) {
+                        CollectionFreeFormDocumentView.gotoKeyframe((contextView.ComponentView as CollectionFreeFormView).childDocs.slice(), transTime);
+                        context._currentFrame = NumCast(activeItem.presActiveFrame);
+                    }
+                }
             }
             if (from?.mediaStopTriggerList && this.layoutDoc.presStatus !== PresStatus.Edit) {
                 DocListCast(from.mediaStopTriggerList).forEach(this.stopTempMedia);
@@ -353,8 +361,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     navigateToView = (targetDoc: Doc, activeItem: Doc) => {
         clearTimeout(this._navTimer);
         const bestTarget = DocumentManager.Instance.getFirstDocumentView(targetDoc)?.props.Document;
-        if (bestTarget) console.log(bestTarget.title, bestTarget.type);
-        else console.log('no best target');
         if (bestTarget) this._navTimer = PresBox.navigateToDoc(bestTarget, activeItem, false);
     };
 
@@ -363,17 +369,29 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         const pannable = [DocumentType.IMG].includes(target.type as any) || (target.type === DocumentType.COL && target._viewType === CollectionViewType.Freeform);
         const temporal = [DocumentType.AUDIO, DocumentType.VID].includes(target.type as any);
         const clippable = [DocumentType.COMPARISON].includes(target.type as any);
-        return { scrollable, pannable, temporal, clippable };
+        const dataview = [DocumentType.INK].includes(target.type as any) && target.activeFrame === undefined;
+        return { scrollable, pannable, temporal, clippable, dataview };
     }
     // navigates to the bestTarget document by making sure it is on screen,
     // then it applies the view specs stored in activeItem to
     @action
     static navigateToDoc(bestTarget: Doc, activeItem: Doc, jumpToDoc: boolean) {
-        bestTarget._viewTransition = activeItem.presTransition ? `transform ${activeItem.presTransition}ms` : 'all 0.5s';
-        const { scrollable, pannable, temporal, clippable } = this.pinDataTypes(bestTarget);
+        const transTime = NumCast(activeItem.presDuration, 500);
+        const presTransitionTime = `all ${transTime}ms`;
+        bestTarget._viewTransition = presTransitionTime;
+        const { scrollable, pannable, temporal, clippable, dataview } = this.pinDataTypes(bestTarget);
         if (clippable) bestTarget._clipWidth = activeItem.presPinClipWidth;
         if (temporal) bestTarget._currentTimecode = activeItem.presStartTime;
         if (scrollable) bestTarget._scrollTop = activeItem.presPinViewScroll;
+        if (dataview) {
+            bestTarget._dataTransition = presTransitionTime;
+            bestTarget.data = (activeItem.presData as any as InkField)[Copy]();
+            bestTarget.x = NumCast(activeItem.presX);
+            bestTarget.y = NumCast(activeItem.presY);
+            bestTarget.width = NumCast(activeItem.presWidth);
+            bestTarget.height = NumCast(activeItem.presHeight);
+        }
+
         if (pannable) {
             const contentBounds = Cast(activeItem.contentBounds, listSpec('number'));
             if (contentBounds) {
@@ -389,14 +407,17 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 bestTarget._viewScale = activeItem.presPinViewScale;
             }
         }
-        return setTimeout(() => (bestTarget._viewTransition = undefined), activeItem.presTransition ? NumCast(activeItem.presTransition) + 10 : 510);
+        return setTimeout(() => {
+            bestTarget._viewTransition = undefined;
+            if (dataview) bestTarget._dataTransition = undefined;
+        }, transTime + 10);
     }
 
     /// copies values from the targetDoc (which is the prototype of the pinDoc) to
     /// reserved fields on the pinDoc so that those values can be restored to the
     /// target doc when navigating to it.
     @action
-    static pinDocView(pinDoc: Doc, pinProps: PinProps | undefined) {
+    static pinDocView(pinDoc: Doc, pinProps: PinProps | undefined, targetDoc: Doc) {
         if (pinProps?.pinWithView) {
             // If pinWithView option set then update scale and x / y props of slide
             const bounds = pinProps.pinWithView.bounds;
@@ -407,13 +428,20 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             pinDoc.contentBounds = new List<number>([bounds.left, bounds.top, bounds.left + bounds.width, bounds.top + bounds.height]);
         }
         if (pinProps?.pinDocView) {
-            const { scrollable, pannable, temporal, clippable } = this.pinDataTypes(pinDoc);
-            pinDoc.presPinView = (pinProps?.pinWithView ? true : false) || scrollable || temporal || pannable || clippable;
+            const { scrollable, pannable, temporal, clippable, dataview } = this.pinDataTypes(pinDoc);
+            pinDoc.presPinView = (pinProps?.pinWithView ? true : false) || scrollable || temporal || pannable || clippable || dataview || pinProps.activeFrame !== undefined;
 
             if (scrollable) pinDoc.presPinViewScroll = pinDoc._scrollTop;
-            else if (clippable) pinDoc.presPinClipWidth = pinDoc._clipWidth;
-            else if (temporal) pinDoc.presEndTime = NumCast((pinDoc.presStartTime = pinDoc._currentTimecode)) + 0.1;
-            else if (pannable) {
+            if (clippable) pinDoc.presPinClipWidth = pinDoc._clipWidth;
+            if (temporal) pinDoc.presEndTime = NumCast((pinDoc.presStartTime = pinDoc._currentTimecode)) + 0.1;
+            if (dataview) {
+                pinDoc.presData = (targetDoc.data as InkField)[Copy]();
+                pinDoc.presX = NumCast(targetDoc.x);
+                pinDoc.presY = NumCast(targetDoc.y);
+                pinDoc.presWidth = NumCast(targetDoc.width);
+                pinDoc.presHeight = NumCast(targetDoc.height);
+            }
+            if (pannable) {
                 const panX = NumCast(pinDoc._panX);
                 const panY = NumCast(pinDoc._panY);
                 const pw = NumCast(pinProps.panelWidth);
@@ -493,8 +521,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         // After navigating to the document, if it is added as a presPinView then it will
         // adjust the pan and scale to that of the pinView when it was added.
         if (activeItem.presPinView) {
-            console.log(targetDoc.title);
-            console.log('presPinView in PresBox.tsx:420');
             // if targetDoc is not displayed but one of its aliases is, then we need to modify that alias, not the original target
             this.navigateToView(targetDoc, activeItem);
         }
@@ -866,10 +892,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     //Regular click
     @action
     selectElement = async (doc: Doc) => {
-        const context = Cast(doc.context, Doc, null);
         this.gotoDocument(this.childDocs.indexOf(doc), this.activeItem);
-        if (doc.presPinView || doc.presentationTargetDoc === this.layoutDoc.presCollection) setTimeout(() => this.updateCurrentPresentation(context), 0);
-        else this.updateCurrentPresentation(context);
+        if (doc.presPinView || doc.presentationTargetDoc === this.layoutDoc.presCollection) setTimeout(() => this.updateCurrentPresentation(DocCast(doc.context)), 0);
+        else this.updateCurrentPresentation(DocCast(doc.context));
     };
 
     //Command click
-- 
cgit v1.2.3-70-g09d2


From f847c6d554f9dcecbd6c3024f712510f341daf67 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 12 Sep 2022 11:36:15 -0400
Subject: made annotation pen available in lightbox.

---
 src/client/views/GestureOverlay.scss            |   5 +-
 src/client/views/GestureOverlay.tsx             |  43 +++++-----
 src/client/views/LightboxView.scss              |  80 ++++++++++++-------
 src/client/views/LightboxView.tsx               | 101 ++++++++++++++----------
 src/client/views/MainView.tsx                   |  48 +++++------
 src/client/views/collections/CollectionMenu.tsx |   1 -
 src/client/views/nodes/DocumentView.tsx         |   9 +--
 7 files changed, 159 insertions(+), 128 deletions(-)

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

diff --git a/src/client/views/GestureOverlay.scss b/src/client/views/GestureOverlay.scss
index f5cbbffb1..bfe2d5c64 100644
--- a/src/client/views/GestureOverlay.scss
+++ b/src/client/views/GestureOverlay.scss
@@ -1,8 +1,9 @@
 .gestureOverlay-cont {
-    width: 100vw;
-    height: 100vh;
+    width: 100%;
+    height: 100%;
     position: absolute;
     touch-action: none;
+    top: 0;
 
     .pointerBubbles {
         width: 100%;
diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx
index 6f28ef9eb..850688e7e 100644
--- a/src/client/views/GestureOverlay.tsx
+++ b/src/client/views/GestureOverlay.tsx
@@ -1,7 +1,6 @@
 import React = require('react');
 import * as fitCurve from 'fit-curve';
-import { action, computed, observable, runInAction } from 'mobx';
-import { observer } from 'mobx-react';
+import { action, computed, observable, runInAction, trace } from 'mobx';
 import { Doc } from '../../fields/Doc';
 import { InkData, InkTool } from '../../fields/InkField';
 import { List } from '../../fields/List';
@@ -41,9 +40,13 @@ import { RadialMenu } from './nodes/RadialMenu';
 import HorizontalPalette from './Palette';
 import { Touchable } from './Touchable';
 import TouchScrollableMenu, { TouchScrollableMenuItem } from './TouchScrollableMenu';
+import { observer } from 'mobx-react';
 
+interface GestureOverlayProps {
+    isActive: boolean;
+}
 @observer
-export class GestureOverlay extends Touchable {
+export class GestureOverlay extends Touchable<GestureOverlayProps> {
     static Instance: GestureOverlay;
 
     @observable public InkShape: string = '';
@@ -83,7 +86,7 @@ export class GestureOverlay extends Touchable {
 
     protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
 
-    constructor(props: Readonly<{}>) {
+    constructor(props: any) {
         super(props);
 
         GestureOverlay.Instance = this;
@@ -495,7 +498,7 @@ export class GestureOverlay extends Touchable {
             }
 
             this._strokes = [];
-            this._points = [];
+            this._points.length = 0;
             this._possibilities = [];
             document.removeEventListener('touchend', this.handleHandUp);
         }
@@ -619,14 +622,14 @@ export class GestureOverlay extends Touchable {
 
         // get the two targets at the ends of the line
         const ep1 = this._points[0];
-        const ep2 = this._points[this._points.length - 1];
+        const ep2 = this._points.lastElement();
         const target1 = document.elementFromPoint(ep1.X, ep1.Y);
         const target2 = document.elementFromPoint(ep2.X, ep2.Y);
 
         const ge = new CustomEvent<GestureUtils.GestureEvent>('dashOnGesture', {
             bubbles: true,
             detail: {
-                points: this._points,
+                points: this._points.slice(),
                 gesture: GestureUtils.Gestures.Line,
                 bounds: B,
             },
@@ -652,8 +655,8 @@ export class GestureOverlay extends Touchable {
             if (this.Tool !== ToolglassTools.None && xInGlass && yInGlass) {
                 switch (this.Tool) {
                     case ToolglassTools.InkToText:
-                        this._strokes.push(new Array(...this._points));
-                        this._points = [];
+                        this._strokes.push(this._points.slice());
+                        this._points.length = 0;
                         CognitiveServices.Inking.Appliers.InterpretStrokes(this._strokes).then(results => {
                             const wordResults = results.filter((r: any) => r.category === 'line');
                             const possibilities: string[] = [];
@@ -675,7 +678,7 @@ export class GestureOverlay extends Touchable {
                         break;
                     case ToolglassTools.IgnoreGesture:
                         this.dispatchGesture(GestureUtils.Gestures.Stroke);
-                        this._points = [];
+                        this._points.length = 0;
                         break;
                 }
             }
@@ -683,7 +686,7 @@ export class GestureOverlay extends Touchable {
             else if (this.InkShape) {
                 this.makePolygon(this.InkShape, false);
                 this.dispatchGesture(GestureUtils.Gestures.Stroke);
-                this._points = [];
+                this._points.length = 0;
                 if (!CollectionFreeFormViewChrome.Instance?._keepPrimitiveMode) {
                     this.InkShape = '';
                     Doc.ActiveTool = InkTool.None;
@@ -743,15 +746,16 @@ export class GestureOverlay extends Touchable {
                         (controlPoints[0].X - controlPoints.lastElement().X) * (controlPoints[0].X - controlPoints.lastElement().X) + (controlPoints[0].Y - controlPoints.lastElement().Y) * (controlPoints[0].Y - controlPoints.lastElement().Y)
                     );
                     if (controlPoints.length > 4 && dist < 10) controlPoints[controlPoints.length - 1] = controlPoints[0];
-                    this._points = controlPoints;
+                    this._points.length = 0;
+                    this._points.push(...controlPoints);
                     this.dispatchGesture(GestureUtils.Gestures.Stroke);
                     // TODO: nda - check inks to group here
                     checkInksToGroup();
                 }
-                this._points = [];
+                this._points.length = 0;
             }
         } else {
-            this._points = [];
+            this._points.length = 0;
         }
         CollectionFreeFormViewChrome.Instance?.primCreated();
     };
@@ -803,7 +807,7 @@ export class GestureOverlay extends Touchable {
                 }
             }
         }
-        this._points = [];
+        this._points.length = 0;
         switch (shape) {
             //must push an extra point in the end so InteractionUtils knows pointer is up.
             //must be (points[0].X,points[0]-1)
@@ -922,7 +926,7 @@ export class GestureOverlay extends Touchable {
                 new CustomEvent<GestureUtils.GestureEvent>('dashOnGesture', {
                     bubbles: true,
                     detail: {
-                        points: stroke ?? this._points,
+                        points: stroke ?? this._points.slice(),
                         gesture: gesture as any,
                         bounds: this.getBounds(stroke ?? this._points),
                         text: data,
@@ -1067,8 +1071,8 @@ export class GestureOverlay extends Touchable {
 
     render() {
         return (
-            <div className="gestureOverlay-cont" ref={this._overlayRef} onPointerDown={this.onPointerDown} onTouchStart={this.onReactTouchStart}>
-                {this.showMobileInkOverlay ? <MobileInkOverlay /> : <></>}
+            <div className="gestureOverlay-cont" style={{ pointerEvents: this.props.isActive ? 'all' : 'none' }} ref={this._overlayRef} onPointerDown={this.onPointerDown} onTouchStart={this.onReactTouchStart}>
+                {this.showMobileInkOverlay ? <MobileInkOverlay /> : null}
                 {this.elements}
 
                 <div
@@ -1091,7 +1095,8 @@ export class GestureOverlay extends Touchable {
                         pointerEvents: 'none',
                         touchAction: 'none',
                         display: this.showBounds ? 'unset' : 'none',
-                    }}></div>
+                    }}
+                />
                 <TouchScrollableMenu options={this._possibilities} bounds={this.svgBounds} selectedIndex={this._selectedIndex} x={this._menuX} y={this._menuY} />
             </div>
         );
diff --git a/src/client/views/LightboxView.scss b/src/client/views/LightboxView.scss
index 5d42cd97f..ae5dc9902 100644
--- a/src/client/views/LightboxView.scss
+++ b/src/client/views/LightboxView.scss
@@ -1,35 +1,55 @@
-
-    .lightboxView-navBtn {
-        margin: auto;
-        position: absolute;
-        right: 10;
-        top: 10;
-        background: transparent;
-        border-radius: 8;
-        color:white;
-        opacity: 0.7;
-        width: 35; 
-        &:hover {
-            opacity: 1;
-        }
+.lightboxView-navBtn {
+    margin: auto;
+    position: absolute;
+    right: 10;
+    top: 10;
+    background: transparent;
+    border-radius: 8;
+    color: white;
+    opacity: 0.7;
+    width: 25;
+    flex-direction: column;
+    display: flex;
+    &:hover {
+        opacity: 1;
     }
-    .lightboxView-tabBtn {
-        margin: auto;
-        position: absolute;
-        right: 35;
-        top: 10;
-        background: transparent;
-        border-radius: 8;
-        color:white;
-        opacity: 0.7;
-        width: 35;
-        &:hover {
-            opacity: 1;
-        }
+}
+.lightboxView-tabBtn {
+    margin: auto;
+    position: absolute;
+    right: 38;
+    top: 10;
+    background: transparent;
+    border-radius: 8;
+    color: white;
+    opacity: 0.7;
+    width: 25;
+    flex-direction: column;
+    display: flex;
+    &:hover {
+        opacity: 1;
+    }
+}
+.lightboxView-penBtn {
+    margin: auto;
+    position: absolute;
+    right: 70;
+    top: 10;
+    background: transparent;
+    border-radius: 8;
+    color: white;
+    opacity: 0.7;
+    width: 25;
+    flex-direction: column;
+    display: flex;
+    &:hover {
+        opacity: 1;
     }
+}
 .lightboxView-frame {
-    position: absolute; 
-    top: 0; left: 0;
+    position: absolute;
+    top: 0;
+    left: 0;
     width: 100%;
     height: 100%;
     background: #000000bb;
@@ -51,4 +71,4 @@
             }
         }
     }
-}
\ No newline at end of file
+}
diff --git a/src/client/views/LightboxView.tsx b/src/client/views/LightboxView.tsx
index 25354a09d..5613e82fb 100644
--- a/src/client/views/LightboxView.tsx
+++ b/src/client/views/LightboxView.tsx
@@ -4,6 +4,7 @@ import { observer } from 'mobx-react';
 import 'normalize.css';
 import * as React from 'react';
 import { Doc, DocListCast, Opt } from '../../fields/Doc';
+import { InkTool } from '../../fields/InkField';
 import { Cast, NumCast, StrCast } from '../../fields/Types';
 import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue } from '../../Utils';
 import { DocUtils } from '../documents/Documents';
@@ -14,6 +15,7 @@ import { Transform } from '../util/Transform';
 import { CollectionDockingView } from './collections/CollectionDockingView';
 import { CollectionStackedTimeline } from './collections/CollectionStackedTimeline';
 import { TabDocView } from './collections/TabDocView';
+import { GestureOverlay } from './GestureOverlay';
 import './LightboxView.scss';
 import { DocumentView } from './nodes/DocumentView';
 import { DefaultStyleProvider, wavyBorderPath } from './StyleProvider';
@@ -267,45 +269,48 @@ export class LightboxView extends React.Component<LightboxViewProps> {
                         clipPath: `path('${Doc.UserDoc().renderStyle === 'comic' ? wavyBorderPath(this.lightboxWidth(), this.lightboxHeight()) : undefined}')`,
                     }}>
                     {/* <CollectionMenu /> TODO:glr This is where it would go*/}
-                    <DocumentView
-                        ref={action((r: DocumentView | null) => {
-                            LightboxView._docView = r !== null ? r : undefined;
-                            r &&
-                                setTimeout(
-                                    action(() => {
-                                        const target = LightboxView._docTarget;
-                                        const doc = LightboxView._doc;
-                                        const targetView = target && DocumentManager.Instance.getLightboxDocumentView(target);
-                                        if (doc === r.props.Document && (!target || target === doc)) r.ComponentView?.shrinkWrap?.();
-                                        //else target?.focus(target, { willZoom: true, scale: 0.9, instant: true }); // bcz: why was this here?  it breaks smooth navigation in lightbox using 'next' button
-                                    })
-                                );
-                        })}
-                        Document={LightboxView.LightboxDoc}
-                        DataDoc={undefined}
-                        LayoutTemplate={LightboxView.LightboxDocTemplate}
-                        addDocument={undefined}
-                        isDocumentActive={returnFalse}
-                        isContentActive={returnTrue}
-                        addDocTab={this.addDocTab}
-                        pinToPres={TabDocView.PinDoc}
-                        rootSelected={returnTrue}
-                        docViewPath={returnEmptyDoclist}
-                        docFilters={this.docFilters}
-                        removeDocument={undefined}
-                        styleProvider={DefaultStyleProvider}
-                        ScreenToLocalTransform={this.lightboxScreenToLocal}
-                        PanelWidth={this.lightboxWidth}
-                        PanelHeight={this.lightboxHeight}
-                        focus={DocUtils.DefaultFocus}
-                        whenChildContentsActiveChanged={emptyFunction}
-                        bringToFront={emptyFunction}
-                        docRangeFilters={returnEmptyFilter}
-                        searchFilterDocs={returnEmptyDoclist}
-                        ContainingCollectionView={undefined}
-                        ContainingCollectionDoc={undefined}
-                        renderDepth={0}
-                    />
+
+                    <GestureOverlay isActive={true}>
+                        <DocumentView
+                            ref={action((r: DocumentView | null) => {
+                                LightboxView._docView = r !== null ? r : undefined;
+                                r &&
+                                    setTimeout(
+                                        action(() => {
+                                            const target = LightboxView._docTarget;
+                                            const doc = LightboxView._doc;
+                                            const targetView = target && DocumentManager.Instance.getLightboxDocumentView(target);
+                                            if (doc === r.props.Document && (!target || target === doc)) r.ComponentView?.shrinkWrap?.();
+                                            //else target?.focus(target, { willZoom: true, scale: 0.9, instant: true }); // bcz: why was this here?  it breaks smooth navigation in lightbox using 'next' button
+                                        })
+                                    );
+                            })}
+                            Document={LightboxView.LightboxDoc}
+                            DataDoc={undefined}
+                            LayoutTemplate={LightboxView.LightboxDocTemplate}
+                            addDocument={undefined}
+                            isDocumentActive={returnFalse}
+                            isContentActive={returnTrue}
+                            addDocTab={this.addDocTab}
+                            pinToPres={TabDocView.PinDoc}
+                            rootSelected={returnTrue}
+                            docViewPath={returnEmptyDoclist}
+                            docFilters={this.docFilters}
+                            removeDocument={undefined}
+                            styleProvider={DefaultStyleProvider}
+                            ScreenToLocalTransform={this.lightboxScreenToLocal}
+                            PanelWidth={this.lightboxWidth}
+                            PanelHeight={this.lightboxHeight}
+                            focus={DocUtils.DefaultFocus}
+                            whenChildContentsActiveChanged={emptyFunction}
+                            bringToFront={emptyFunction}
+                            docRangeFilters={returnEmptyFilter}
+                            searchFilterDocs={returnEmptyDoclist}
+                            ContainingCollectionView={undefined}
+                            ContainingCollectionDoc={undefined}
+                            renderDepth={0}
+                        />
+                    </GestureOverlay>
                 </div>
 
                 {this.navBtn(
@@ -332,6 +337,15 @@ export class LightboxView extends React.Component<LightboxViewProps> {
                     this.future()?.length.toString()
                 )}
                 <LightboxTourBtn navBtn={this.navBtn} future={this.future} stepInto={this.stepInto} tourMap={this.tourMap} />
+                <div
+                    className="lightboxView-navBtn"
+                    title={'toggle fit width'}
+                    onClick={e => {
+                        e.stopPropagation();
+                        LightboxView.LightboxDoc!._fitWidth = !LightboxView.LightboxDoc!._fitWidth;
+                    }}>
+                    <FontAwesomeIcon icon={LightboxView.LightboxDoc?._fitWidth ? 'arrows-alt-h' : 'arrows-alt-v'} size="2x" />
+                </div>
                 <div
                     className="lightboxView-tabBtn"
                     title={'open in tab'}
@@ -345,13 +359,14 @@ export class LightboxView extends React.Component<LightboxViewProps> {
                     <FontAwesomeIcon icon={'file-download'} size="2x" />
                 </div>
                 <div
-                    className="lightboxView-navBtn"
-                    title={'toggle fit width'}
+                    className="lightboxView-penBtn"
+                    title="toggle pen annotation"
+                    style={{ background: Doc.ActiveTool === InkTool.Pen ? 'white' : undefined }}
                     onClick={e => {
                         e.stopPropagation();
-                        LightboxView.LightboxDoc!._fitWidth = !LightboxView.LightboxDoc!._fitWidth;
+                        Doc.ActiveTool = Doc.ActiveTool === InkTool.Pen ? InkTool.None : InkTool.Pen;
                     }}>
-                    <FontAwesomeIcon icon={LightboxView.LightboxDoc?._fitWidth ? 'arrows-alt-h' : 'arrows-alt-v'} size="2x" />
+                    <FontAwesomeIcon color={Doc.ActiveTool === InkTool.Pen ? 'black' : 'white'} icon={'pen'} size="2x" />
                 </div>
             </div>
         );
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 45281ed69..d7b526d22 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -86,7 +86,7 @@ export class MainView extends React.Component {
         return this._hideUI ? 0 : 27;
     } //  27 comes form lm.config.defaultConfig.dimensions.headerHeight in goldenlayout.js
     @computed private get topOfDashUI() {
-        return this._hideUI ? 0 : Number(DASHBOARD_SELECTOR_HEIGHT.replace('px', ''));
+        return this._hideUI || LightboxView.LightboxDoc ? 0 : Number(DASHBOARD_SELECTOR_HEIGHT.replace('px', ''));
     }
     @computed private get topOfHeaderBarDoc() {
         return this.topOfDashUI;
@@ -610,7 +610,7 @@ export class MainView extends React.Component {
 
     @computed get dockingContent() {
         return (
-            <GestureOverlay>
+            <GestureOverlay isActive={LightboxView.LightboxDoc ? false : true}>
                 <div
                     key="docking"
                     className={`mainView-dockingContent${this._leftMenuFlyoutWidth ? '-flyout' : ''}`}
@@ -657,21 +657,16 @@ export class MainView extends React.Component {
         const locationFields = doc._viewType === CollectionViewType.Docking ? ['dashboard'] : location.split(':');
         const locationParams = locationFields.length > 1 ? locationFields[1] : '';
         if (doc.dockingConfig) return DashboardView.openDashboard(doc);
+        // prettier-ignore
         switch (locationFields[0]) {
-            case 'dashboard':
-                return DashboardView.openDashboard(doc);
-            case 'close':
-                return CollectionDockingView.CloseSplit(doc, locationParams);
-            case 'fullScreen':
-                return CollectionDockingView.OpenFullScreen(doc);
-            case 'lightbox':
-                return LightboxView.AddDocTab(doc, location);
-            case 'toggle':
-                return CollectionDockingView.ToggleSplit(doc, locationParams);
-            case 'inPlace':
-            case 'add':
             default:
-                return CollectionDockingView.AddSplit(doc, locationParams);
+            case 'inPlace':
+            case 'add':        return CollectionDockingView.AddSplit(doc, locationParams);
+            case 'dashboard':  return DashboardView.openDashboard(doc);
+            case 'close':      return CollectionDockingView.CloseSplit(doc, locationParams);
+            case 'fullScreen': return CollectionDockingView.OpenFullScreen(doc);
+            case 'lightbox':   return LightboxView.AddDocTab(doc, location);
+            case 'toggle':     return CollectionDockingView.ToggleSplit(doc, locationParams);
         }
     };
 
@@ -716,7 +711,7 @@ export class MainView extends React.Component {
 
     @computed get leftMenuPanel() {
         return (
-            <div key="menu" className="mainView-leftMenuPanel">
+            <div key="menu" className="mainView-leftMenuPanel" style={{ display: LightboxView.LightboxDoc ? 'none' : undefined }}>
                 <DocumentView
                     Document={Doc.MyLeftSidebarMenu}
                     DataDoc={undefined}
@@ -991,19 +986,16 @@ export class MainView extends React.Component {
                 {LinkDocPreview.LinkInfo ? <LinkDocPreview {...LinkDocPreview.LinkInfo} /> : null}
 
                 {((page: string) => {
+                    // prettier-ignore
                     switch (page) {
-                        case 'dashboard':
                         default:
-                            return (
-                                <>
-                                    <div style={{ position: 'relative', display: this._hideUI || LightboxView.LightboxDoc ? 'none' : undefined, zIndex: 2001 }}>
-                                        <CollectionMenu panelWidth={this.topMenuWidth} panelHeight={this.topMenuHeight} />
-                                    </div>
-                                    {this.mainDashboardArea}
-                                </>
-                            );
-                        case 'home':
-                            return <DashboardView />;
+                        case 'dashboard': return (<>
+                                        <div key="dashdiv" style={{ position: 'relative', display: this._hideUI || LightboxView.LightboxDoc ? 'none' : undefined, zIndex: 2001 }}>
+                                            <CollectionMenu panelWidth={this.topMenuWidth} panelHeight={this.topMenuHeight} />
+                                        </div>
+                                        {this.mainDashboardArea}
+                                    </> );
+                        case 'home': return <DashboardView />;
                     }
                 })(Doc.ActivePage)}
 
@@ -1020,7 +1012,7 @@ export class MainView extends React.Component {
                 <InkTranscription />
                 {this.snapLines}
                 <div className="mainView-webRef" ref={this.makeWebRef} />
-                <LightboxView PanelWidth={this._windowWidth} PanelHeight={this._windowHeight} maxBorder={[200, 50]} />
+                <LightboxView key="lightbox" PanelWidth={this._windowWidth} PanelHeight={this._windowHeight} maxBorder={[200, 50]} />
             </div>
         );
     }
diff --git a/src/client/views/collections/CollectionMenu.tsx b/src/client/views/collections/CollectionMenu.tsx
index 6a0f69359..46e8494ab 100644
--- a/src/client/views/collections/CollectionMenu.tsx
+++ b/src/client/views/collections/CollectionMenu.tsx
@@ -34,7 +34,6 @@ import { CollectionFreeFormDocumentView } from '../nodes/CollectionFreeFormDocum
 import { DocumentView } from '../nodes/DocumentView';
 import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
 import { RichTextMenu } from '../nodes/formattedText/RichTextMenu';
-import { PresBox } from '../nodes/trails/PresBox';
 import { DefaultStyleProvider } from '../StyleProvider';
 import { CollectionDockingView } from './CollectionDockingView';
 import { CollectionLinearView } from './collectionLinear';
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 113574a64..e628c2e44 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1,8 +1,9 @@
 import { IconProp } from '@fortawesome/fontawesome-svg-core';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { Tooltip } from '@material-ui/core';
 import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
-import { AclAdmin, AclEdit, AclPrivate, DataSym, Doc, DocListCast, Field, HeightSym, Opt, StrListCast, WidthSym } from '../../../fields/Doc';
+import { AclAdmin, AclEdit, AclPrivate, DataSym, Doc, DocListCast, Field, Opt, StrListCast, WidthSym } from '../../../fields/Doc';
 import { Document } from '../../../fields/documentSchemas';
 import { Id } from '../../../fields/FieldSymbols';
 import { InkTool } from '../../../fields/InkField';
@@ -10,7 +11,7 @@ import { List } from '../../../fields/List';
 import { ObjectField } from '../../../fields/ObjectField';
 import { listSpec } from '../../../fields/Schema';
 import { ScriptField } from '../../../fields/ScriptField';
-import { BoolCast, Cast, DocCast, ImageCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
+import { BoolCast, Cast, ImageCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
 import { AudioField } from '../../../fields/URLField';
 import { GetEffectiveAcl, SharingPermissions, TraceMobx } from '../../../fields/util';
 import { MobileInterface } from '../../../mobile/MobileInterface';
@@ -20,6 +21,7 @@ import { DocServer } from '../../DocServer';
 import { Docs, DocUtils } from '../../documents/Documents';
 import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes';
 import { Networking } from '../../Network';
+import { DictationManager } from '../../util/DictationManager';
 import { DocumentManager } from '../../util/DocumentManager';
 import { DragManager, dropActionType } from '../../util/DragManager';
 import { InteractionUtils } from '../../util/InteractionUtils';
@@ -37,7 +39,6 @@ import { ContextMenu } from '../ContextMenu';
 import { ContextMenuProps } from '../ContextMenuItem';
 import { DocComponent } from '../DocComponent';
 import { EditableView } from '../EditableView';
-import { InkingStroke } from '../InkingStroke';
 import { LightboxView } from '../LightboxView';
 import { StyleProp } from '../StyleProvider';
 import { CollectionFreeFormDocumentView } from './CollectionFreeFormDocumentView';
@@ -52,8 +53,6 @@ import { RadialMenu } from './RadialMenu';
 import { ScriptingBox } from './ScriptingBox';
 import { PresBox } from './trails/PresBox';
 import React = require('react');
-import { DictationManager } from '../../util/DictationManager';
-import { Tooltip } from '@material-ui/core';
 const { Howl } = require('howler');
 
 interface Window {
-- 
cgit v1.2.3-70-g09d2


From 5df8e9041e7a0e8ac98f0c911a2b603c0d8e89d3 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 20 Oct 2022 11:07:04 -0400
Subject: added double-tap to keep ink primitive mode.  cleaned up circle
 drawing and some other gesture code.

---
 src/client/util/CurrentUserUtils.ts                | 14 ++---
 src/client/util/InteractionUtils.tsx               |  6 +-
 src/client/views/GestureOverlay.tsx                | 69 +++++++---------------
 src/client/views/collections/CollectionMenu.tsx    |  7 ++-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  3 +-
 src/client/views/nodes/DocumentView.tsx            |  2 +-
 src/client/views/nodes/button/FontIconBox.tsx      | 51 +++++++++-------
 7 files changed, 68 insertions(+), 84 deletions(-)

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

diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index bfa868dbc..2bc464127 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -46,7 +46,7 @@ interface Button {
     buttonText?: string;
 
     // fields that do not correspond to DocumentOption fields
-    scripts?: { script?: string; onClick?: string; }
+    scripts?: { script?: string; onClick?: string; onDoubleClick?: string }
     funcs?: { [key:string]: string };
     subMenu?: Button[];
 }
@@ -628,13 +628,13 @@ export class CurrentUserUtils {
 
     static inkTools():Button[] {
         return [
-            { title: "Pen",    toolTip: "Pen (Ctrl+P)",          btnType: ButtonType.ToggleButton, icon: "pen-nib",                     scripts: {onClick:'{ return setActiveTool("pen", _readOnly_);}' }},
-            { title: "Write",  toolTip: "Write (Ctrl+Shift+P)",  btnType: ButtonType.ToggleButton, icon: "pen",                         scripts: {onClick:'{ return setActiveTool("write", _readOnly_);}'} },
-            { title: "Eraser", toolTip: "Eraser (Ctrl+E)",       btnType: ButtonType.ToggleButton, icon: "eraser",                      scripts: {onClick:'{ return setActiveTool("eraser", _readOnly_);}' }},
+            { title: "Pen",    toolTip: "Pen (Ctrl+P)",          btnType: ButtonType.ToggleButton, icon: "pen-nib",                     scripts: {onClick:'{ return setActiveTool("pen", false, _readOnly_);}' }},
+            { title: "Write",  toolTip: "Write (Ctrl+Shift+P)",  btnType: ButtonType.ToggleButton, icon: "pen",                         scripts: {onClick:'{ return setActiveTool("write", false, _readOnly_);}'} },
+            { title: "Eraser", toolTip: "Eraser (Ctrl+E)",       btnType: ButtonType.ToggleButton, icon: "eraser",                      scripts: {onClick:'{ return setActiveTool("eraser", false, _readOnly_);}' }},
             // { title: "Highlighter", toolTip: "Highlighter (Ctrl+H)", btnType: ButtonType.ToggleButton, icon: "highlighter", scripts:{onClick: 'setActiveTool("highlighter")'} },
-            { title: "Circle", toolTip: "Circle (Ctrl+Shift+C)", btnType: ButtonType.ToggleButton, icon: "circle",                      scripts: {onClick:`{ return setActiveTool("${GestureUtils.Gestures.Circle}", _readOnly_);}`} },
-            { title: "Square", toolTip: "Square (Ctrl+Shift+S)", btnType: ButtonType.ToggleButton, icon: "square",                      scripts: {onClick:`{ return setActiveTool("${GestureUtils.Gestures.Rectangle}", _readOnly_);}`} },
-            { title: "Line",   toolTip: "Line (Ctrl+Shift+L)",   btnType: ButtonType.ToggleButton, icon: "minus",                       scripts: {onClick:`{ return setActiveTool("${GestureUtils.Gestures.Line}", _readOnly_);}`} },
+            { title: "Circle", toolTip: "Circle (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "circle",                      scripts: {onClick:`{ return setActiveTool("${GestureUtils.Gestures.Circle}", false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool("${GestureUtils.Gestures.Circle}", true, _readOnly_);}`} },
+            { title: "Square", toolTip: "Square (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "square",                      scripts: {onClick:`{ return setActiveTool("${GestureUtils.Gestures.Rectangle}", false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool("${GestureUtils.Gestures.Rectangle}", true, _readOnly_);}`} },
+            { title: "Line",   toolTip: "Line (double tap to lock mode)",   btnType: ButtonType.ToggleButton, icon: "minus",                       scripts: {onClick:`{ return setActiveTool("${GestureUtils.Gestures.Line}", false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool("${GestureUtils.Gestures.Line}", true, _readOnly_);}`} },
             { title: "Mask",   toolTip: "Mask",                  btnType: ButtonType.ToggleButton, icon: "user-circle",                 scripts: {onClick:'{ return setIsInkMask(_readOnly_);}'} },
             { title: "Fill",   toolTip: "Fill color",            btnType: ButtonType.ColorButton,  icon: "fill-drip",ignoreClick: true, scripts: {script: '{ return setFillColor(value, _readOnly_);}'} },
             { title: "Width",  toolTip: "Stroke width",          btnType: ButtonType.NumberButton,                   ignoreClick: true, scripts: {script: '{ return setStrokeWidth(value, _readOnly_);}'}, numBtnType: NumButtonType.Slider, numBtnMin: 1},
diff --git a/src/client/util/InteractionUtils.tsx b/src/client/util/InteractionUtils.tsx
index 6622b498d..3cdf4dbd2 100644
--- a/src/client/util/InteractionUtils.tsx
+++ b/src/client/util/InteractionUtils.tsx
@@ -242,16 +242,16 @@ export namespace InteractionUtils {
                 const centerX = (Math.max(left, right) + Math.min(left, right)) / 2;
                 const centerY = (Math.max(top, bottom) + Math.min(top, bottom)) / 2;
                 const radius = Math.max(centerX - Math.min(left, right), centerY - Math.min(top, bottom));
-                for (var x = Math.min(left, right); x < Math.max(left, right); x++) {
+                for (var x = centerX - radius; x < centerX + radius; x++) {
                     const y = Math.sqrt(Math.pow(radius, 2) - Math.pow(x - centerX, 2)) + centerY;
                     points.push({ X: x, Y: y });
                 }
-                for (var x = Math.max(left, right); x > Math.min(left, right); x--) {
+                for (var x = centerX + radius; x > centerX - radius; x--) {
                     const y = Math.sqrt(Math.pow(radius, 2) - Math.pow(x - centerX, 2)) + centerY;
                     const newY = centerY - (y - centerY);
                     points.push({ X: x, Y: newY });
                 }
-                points.push({ X: Math.min(left, right), Y: Math.sqrt(Math.pow(radius, 2) - Math.pow(Math.min(left, right) - centerX, 2)) + centerY });
+                points.push({ X: centerX - radius, Y: Math.sqrt(Math.pow(radius, 2) - Math.pow(-radius, 2)) + centerY });
                 break;
 
             case GestureUtils.Gestures.Line:
diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx
index 362aa3c86..a29073f14 100644
--- a/src/client/views/GestureOverlay.tsx
+++ b/src/client/views/GestureOverlay.tsx
@@ -16,7 +16,6 @@ import { InteractionUtils } from '../util/InteractionUtils';
 import { ScriptingGlobals } from '../util/ScriptingGlobals';
 import { SelectionManager } from '../util/SelectionManager';
 import { Transform } from '../util/Transform';
-import { CollectionFreeFormViewChrome } from './collections/CollectionMenu';
 import './GestureOverlay.scss';
 import {
     ActiveArrowEnd,
@@ -53,6 +52,7 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> {
     @observable public SavedColor?: string;
     @observable public SavedWidth?: number;
     @observable public Tool: ToolglassTools = ToolglassTools.None;
+    @observable public KeepPrimitiveMode = false; // for whether primitive selection enters a one-shot or persistent mode
 
     @observable private _thumbX?: number;
     @observable private _thumbY?: number;
@@ -616,29 +616,16 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> {
         return false;
     };
 
-    handleLineGesture = (): boolean => {
-        const actionPerformed = false;
-        const B = this.svgBounds;
-
-        // get the two targets at the ends of the line
-        const ep1 = this._points[0];
-        const ep2 = this._points.lastElement();
-        const target1 = document.elementFromPoint(ep1.X, ep1.Y);
-        const target2 = document.elementFromPoint(ep2.X, ep2.Y);
-
-        const ge = new CustomEvent<GestureUtils.GestureEvent>('dashOnGesture', {
-            bubbles: true,
-            detail: {
-                points: this._points.slice(),
-                gesture: GestureUtils.Gestures.Line,
-                bounds: B,
-            },
-        });
-        target1?.dispatchEvent(ge);
-        target2?.dispatchEvent(ge);
-        return actionPerformed;
-    };
-
+    @action primCreated() {
+        if (!this.KeepPrimitiveMode) {
+            this.InkShape = undefined;
+            //get out of ink mode after each stroke=
+            //if (Doc.ActiveTool === InkTool.Highlighter && GestureOverlay.Instance.SavedColor) SetActiveInkColor(GestureOverlay.Instance.SavedColor);
+            Doc.ActiveTool = InkTool.None;
+            // SetActiveArrowStart('none');
+            // SetActiveArrowEnd('none');
+        }
+    }
     @action
     onPointerUp = (e: PointerEvent) => {
         if (this._points.length > 1) {
@@ -654,7 +641,6 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> {
                 switch (this.Tool) {
                     case ToolglassTools.InkToText:
                         this._strokes.push(this._points.slice());
-                        this._points.length = 0;
                         CognitiveServices.Inking.Appliers.InterpretStrokes(this._strokes).then(results => {
                             const wordResults = results.filter((r: any) => r.category === 'line');
                             const possibilities: string[] = [];
@@ -676,7 +662,6 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> {
                         break;
                     case ToolglassTools.IgnoreGesture:
                         this.dispatchGesture(GestureUtils.Gestures.Stroke);
-                        this._points.length = 0;
                         break;
                 }
             }
@@ -684,11 +669,7 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> {
             else if (this.InkShape) {
                 this.makeBezierPolygon(this.InkShape, false);
                 this.dispatchGesture(this.InkShape);
-                this._points.length = 0;
-                if (!CollectionFreeFormViewChrome.Instance?._keepPrimitiveMode) {
-                    this.InkShape = undefined;
-                    Doc.ActiveTool = InkTool.None;
-                }
+                this.primCreated();
             }
             // if we're not drawing in a toolglass try to recognize as gesture
             else {
@@ -697,18 +678,13 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> {
                 let actionPerformed = false;
                 if (Doc.UserDoc().recognizeGestures && result && result.Score > 0.7) {
                     switch (result.Name) {
+                        case GestureUtils.Gestures.Line:
                         case GestureUtils.Gestures.Triangle:
                         case GestureUtils.Gestures.Rectangle:
                         case GestureUtils.Gestures.Circle:
                             this.makeBezierPolygon(result.Name, true);
                             actionPerformed = this.dispatchGesture(result.Name);
                             break;
-                        case GestureUtils.Gestures.Line:
-                            if (!(actionPerformed = this.handleLineGesture())) {
-                                this.makeBezierPolygon(result.Name, true);
-                                actionPerformed = this.dispatchGesture(GestureUtils.Gestures.Stroke);
-                            }
-                            break;
                         case GestureUtils.Gestures.Scribble:
                             console.log('scribble');
                             break;
@@ -741,12 +717,9 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> {
                     // TODO: nda - check inks to group here
                     checkInksToGroup();
                 }
-                this._points.length = 0;
             }
-        } else {
-            this._points.length = 0;
         }
-        CollectionFreeFormViewChrome.Instance?.primCreated();
+        this._points.length = 0;
     };
 
     makeBezierPolygon = (shape: string, gesture: boolean) => {
@@ -901,17 +874,17 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> {
         return false;
     };
 
-    dispatchGesture = (gesture: GestureUtils.Gestures, stroke?: InkData, data?: any) => {
-        const target = document.elementFromPoint((stroke ?? this._points)[0].X, (stroke ?? this._points)[0].Y);
+    dispatchGesture = (gesture: GestureUtils.Gestures, stroke?: InkData, text?: any) => {
+        const points = (stroke ?? this._points).slice();
         return (
-            target?.dispatchEvent(
+            document.elementFromPoint(points[0].X, points[0].Y)?.dispatchEvent(
                 new CustomEvent<GestureUtils.GestureEvent>('dashOnGesture', {
                     bubbles: true,
                     detail: {
-                        points: stroke ?? this._points.slice(),
-                        gesture: gesture as any,
-                        bounds: this.getBounds(stroke ?? this._points),
-                        text: data,
+                        points,
+                        gesture,
+                        bounds: this.getBounds(points),
+                        text,
                     },
                 })
             ) || false
diff --git a/src/client/views/collections/CollectionMenu.tsx b/src/client/views/collections/CollectionMenu.tsx
index 46e8494ab..db81f28f6 100644
--- a/src/client/views/collections/CollectionMenu.tsx
+++ b/src/client/views/collections/CollectionMenu.tsx
@@ -40,6 +40,7 @@ import { CollectionLinearView } from './collectionLinear';
 import './CollectionMenu.scss';
 import { COLLECTION_BORDER_WIDTH } from './CollectionView';
 import { TabDocView } from './TabDocView';
+import { GestureUtils } from '../../../pen-gestures/GestureUtils';
 
 interface CollectionMenuProps {
     panelHeight: () => number;
@@ -767,7 +768,7 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionView
     private _draw = ['∿', '=', '⎯', '→', '↔︎', 'ロ', 'O'];
     private _head = ['', '', '', '', 'arrow', '', ''];
     private _end = ['', '', '', 'arrow', 'arrow', '', ''];
-    private _shapePrims = ['', '', 'line', 'line', 'line', 'rectangle', 'circle'];
+    private _shapePrims = ['', '', 'line', 'line', 'line', 'rectangle', 'circle'] as GestureUtils.Gestures[];
     private _title = ['pen', 'highlighter', 'line', 'line with arrow', 'line with double arrows', 'square', 'circle'];
     private _faName = ['pen-fancy', 'highlighter', 'minus', 'long-arrow-alt-right', 'arrows-alt-h', 'square', 'circle'];
     @observable _selectedPrimitive = this._shapePrims.length;
@@ -848,13 +849,13 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionView
                 SetActiveArrowEnd(this._end[i]);
                 SetActiveBezierApprox('300');
 
-                GestureOverlay.Instance.InkShape = this._shapePrims[i];
+                if (GestureOverlay.Instance) GestureOverlay.Instance.InkShape = this._shapePrims[i];
             } else {
                 this._selectedPrimitive = this._shapePrims.length;
                 Doc.ActiveTool = InkTool.None;
                 SetActiveArrowStart('');
                 SetActiveArrowEnd('');
-                GestureOverlay.Instance.InkShape = '';
+                if (GestureOverlay.Instance) GestureOverlay.Instance.InkShape = undefined;
                 SetActiveBezierApprox('0');
             }
             e.stopPropagation();
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 983cf4f9b..7b93408c3 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -568,9 +568,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     @undoBatch
     onGesture = (e: Event, ge: GestureUtils.GestureEvent) => {
         switch (ge.gesture) {
-            case GestureUtils.Gestures.Line:
-                break;
             default:
+            case GestureUtils.Gestures.Line:
             case GestureUtils.Gestures.Circle:
             case GestureUtils.Gestures.Rectangle:
             case GestureUtils.Gestures.Triangle:
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 040e03150..16b5809fa 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -655,7 +655,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
         }
         this._downX = e.clientX;
         this._downY = e.clientY;
-        if (Doc.ActiveTool === InkTool.None && !(this.props.Document.rootDocument && !(e.ctrlKey || e.button > 0))) {
+        if ((Doc.ActiveTool === InkTool.None || this.props.addDocTab === returnFalse) && !(this.props.Document.rootDocument && !(e.ctrlKey || e.button > 0))) {
             // if this is part of a template, let the event go up to the tempalte root unless right/ctrl clicking
             if (
                 (this.props.isDocumentActive?.() || this.layoutDoc.onDragStart) &&
diff --git a/src/client/views/nodes/button/FontIconBox.tsx b/src/client/views/nodes/button/FontIconBox.tsx
index fd0c0d141..883c4460b 100644
--- a/src/client/views/nodes/button/FontIconBox.tsx
+++ b/src/client/views/nodes/button/FontIconBox.tsx
@@ -771,31 +771,42 @@ export function createInkGroup(inksToGroup?: Doc[], isSubGroup?: boolean) {
     CollectionFreeFormView.collectionsWithUnprocessedInk.clear();
 }
 
-ScriptingGlobals.add(function setActiveTool(tool: InkTool | GestureUtils.Gestures, checkResult?: boolean) {
+function setActiveTool(tool: InkTool | GestureUtils.Gestures, keepPrim: boolean, checkResult?: boolean) {
     InkTranscription.Instance?.createInkGroup();
     if (checkResult) {
-        return (Doc.ActiveTool === tool && !GestureOverlay.Instance?.InkShape) || GestureOverlay.Instance?.InkShape === tool ? Colors.MEDIUM_BLUE : 'transparent';
-    }
-    if (Object.values(GestureUtils.Gestures).includes(tool as any)) {
-        if (GestureOverlay.Instance.InkShape === tool) {
-            Doc.ActiveTool = InkTool.None;
-            GestureOverlay.Instance.InkShape = undefined;
-        } else {
-            Doc.ActiveTool = InkTool.Pen;
-            GestureOverlay.Instance.InkShape = tool as GestureUtils.Gestures;
+        return (Doc.ActiveTool === tool && !GestureOverlay.Instance?.InkShape) || GestureOverlay.Instance?.InkShape === tool
+            ? GestureOverlay.Instance?.KeepPrimitiveMode || ![GestureUtils.Gestures.Circle, GestureUtils.Gestures.Line, GestureUtils.Gestures.Rectangle].includes(tool as GestureUtils.Gestures)
+                ? Colors.MEDIUM_BLUE
+                : Colors.MEDIUM_BLUE_ALT
+            : 'transparent';
+    }
+    runInAction(() => {
+        if (GestureOverlay.Instance) {
+            GestureOverlay.Instance.KeepPrimitiveMode = keepPrim;
         }
-    } else if (tool) {
-        // pen or eraser
-        if (Doc.ActiveTool === tool && !GestureOverlay.Instance.InkShape) {
-            Doc.ActiveTool = InkTool.None;
+        if (Object.values(GestureUtils.Gestures).includes(tool as any)) {
+            if (GestureOverlay.Instance.InkShape === tool) {
+                Doc.ActiveTool = InkTool.None;
+                GestureOverlay.Instance.InkShape = undefined;
+            } else {
+                Doc.ActiveTool = InkTool.Pen;
+                GestureOverlay.Instance.InkShape = tool as GestureUtils.Gestures;
+            }
+        } else if (tool) {
+            // pen or eraser
+            if (Doc.ActiveTool === tool && !GestureOverlay.Instance.InkShape) {
+                Doc.ActiveTool = InkTool.None;
+            } else {
+                Doc.ActiveTool = tool as any;
+                GestureOverlay.Instance.InkShape = undefined;
+            }
         } else {
-            Doc.ActiveTool = tool as any;
-            GestureOverlay.Instance.InkShape = undefined;
+            Doc.ActiveTool = InkTool.None;
         }
-    } else {
-        Doc.ActiveTool = InkTool.None;
-    }
-});
+    });
+}
+
+ScriptingGlobals.add(setActiveTool, 'sets the active ink tool mode');
 
 // toggle: Set overlay status of selected document
 ScriptingGlobals.add(function setIsInkMask(checkResult?: boolean) {
-- 
cgit v1.2.3-70-g09d2


From 613daac016c367205ff1afddd81b7b9111c52d33 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 5 Dec 2022 22:45:22 -0500
Subject: cleaning up following links and pres item following so that view
 transitions don't interfere when clicking quickly (eg through animation
 frames).  changed animations to animate multi-level zooming parallel.

---
 src/client/util/CurrentUserUtils.ts                |   2 +-
 src/client/util/DocumentManager.ts                 | 145 ++++++------
 src/client/util/LinkFollower.ts                    |   2 +-
 src/client/views/LightboxView.tsx                  |   8 +-
 src/client/views/PropertiesButtons.tsx             |   8 +-
 src/client/views/PropertiesView.tsx                |   5 +-
 .../views/collections/CollectionDockingView.tsx    |   3 +-
 src/client/views/collections/CollectionMenu.tsx    |  11 +-
 src/client/views/collections/TabDocView.tsx        |  16 +-
 .../CollectionFreeFormLinkView.tsx                 |   6 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  39 ++--
 .../collections/collectionFreeForm/MarqueeView.tsx |  14 +-
 .../views/nodes/CollectionFreeFormDocumentView.tsx |  57 ++---
 src/client/views/nodes/DocumentView.tsx            |  54 ++++-
 src/client/views/nodes/ImageBox.tsx                |   2 +-
 src/client/views/nodes/PDFBox.tsx                  |   2 +-
 src/client/views/nodes/VideoBox.tsx                |   2 +-
 src/client/views/nodes/button/FontIconBox.tsx      |   8 +-
 src/client/views/nodes/trails/PresBox.tsx          | 253 ++++++---------------
 19 files changed, 280 insertions(+), 357 deletions(-)

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

diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index d13209c4f..9dfc91e3f 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -667,7 +667,7 @@ export class CurrentUserUtils {
               title: "Perspective",                    toolTip: "View",                 btnType: ButtonType.DropdownList,                                                                                    ignoreClick: true, width: 100, scripts: { script: 'setView(value, _readOnly_)'}},
             { title: "Pin",     icon: "map-pin",       toolTip: "Pin View to Trail",    btnType: ButtonType.ClickButton,         funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "tab")'},                        width: 20,  scripts: { onClick: 'pinWithView(_readOnly_, altKey)'}},
             { title: "Back",    icon: "chevron-left",  toolTip: "Prev Animation Frame", btnType: ButtonType.ClickButton,         funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()'}, width: 20,  scripts: { onClick: 'prevKeyFrame(_readOnly_)'}},
-            { title: "Num",     icon: "",              toolTip: "Frame Number",         btnType: ButtonType.TextButton,          funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()', buttonText: 'selectedDocs()?.lastElement()?.currentFrame?.toString()'}, width: 20,  scripts: {}},
+            { title: "Num",     icon: "",              toolTip: "Frame Number",         btnType: ButtonType.TextButton,          funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()'}, width: 20, buttonText: 'selectedDocs()?.lastElement()?.currentFrame?.toString()', scripts: { script: 'return curKeyFrame(_readOnly_)'}},
             { title: "Fwd",     icon: "chevron-right", toolTip: "Next Animation Frame", btnType: ButtonType.ClickButton,         funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()'}, width: 20,  scripts: { onClick: 'nextKeyFrame(_readOnly_)'}},
             { title: "Fill",    icon: "fill-drip",     toolTip: "Background Fill Color",btnType: ButtonType.ColorButton,         funcs: {hidden: '!SelectionManager_selectedDocType()'},                     ignoreClick: true, width: 20,  scripts: { script:  'return setBackgroundColor(value, _readOnly_)'}}, // Only when a document is selected
             { title: "Header",  icon: "heading",       toolTip: "Header Color",         btnType: ButtonType.ColorButton,         funcs: {hidden: '!SelectionManager_selectedDocType()'},                     ignoreClick: true,             scripts: { script:  'return setHeaderColor(value, _readOnly_)'}},
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 1b63b615b..d2368f4f6 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -1,19 +1,19 @@
 import { action, observable, runInAction } from 'mobx';
-import { Doc, DocListCast, DocListCastAsync, Opt } from '../../fields/Doc';
+import { Doc, Opt } from '../../fields/Doc';
 import { Id } from '../../fields/FieldSymbols';
-import { Cast, DocCast } from '../../fields/Types';
+import { listSpec } from '../../fields/Schema';
+import { Cast } from '../../fields/Types';
+import { AudioField } from '../../fields/URLField';
 import { returnFalse } from '../../Utils';
 import { DocumentType } from '../documents/DocumentTypes';
-import { LightboxView } from '../views/LightboxView';
-import { DocFocusOptions, DocumentView, OpenWhereMod, ViewAdjustment } from '../views/nodes/DocumentView';
-import { LinkAnchorBox } from '../views/nodes/LinkAnchorBox';
 import { CollectionDockingView } from '../views/collections/CollectionDockingView';
 import { CollectionFreeFormView } from '../views/collections/collectionFreeForm';
 import { CollectionView } from '../views/collections/CollectionView';
+import { LightboxView } from '../views/LightboxView';
+import { DocFocusOptions, DocumentView, OpenWhereMod, ViewAdjustment } from '../views/nodes/DocumentView';
+import { LinkAnchorBox } from '../views/nodes/LinkAnchorBox';
 import { ScriptingGlobals } from './ScriptingGlobals';
 import { SelectionManager } from './SelectionManager';
-import { listSpec } from '../../fields/Schema';
-import { AudioField } from '../../fields/URLField';
 const { Howl } = require('howler');
 
 export class DocumentManager {
@@ -31,6 +31,30 @@ export class DocumentManager {
     //private constructor so no other class can create a nodemanager
     private constructor() {}
 
+    private _viewRenderedCbs: { doc: Doc; func: (dv: DocumentView) => any }[] = [];
+    public AddViewRenderedCb = (doc: Doc, func: (dv: DocumentView) => any) => {
+        const dv = this.getDocumentViewById(doc[Id]);
+        this._viewRenderedCbs.push({ doc, func });
+        if (dv) {
+            this.callAddViewFuncs(dv);
+        }
+    };
+    callAddViewFuncs = (view: DocumentView) => {
+        const callFuncs = this._viewRenderedCbs.filter(vc => vc.doc === view.rootDoc);
+        if (callFuncs.length) {
+            this._viewRenderedCbs = this._viewRenderedCbs.filter(vc => callFuncs.includes(vc));
+            const intTimer = setInterval(
+                () => {
+                    if (!view.ComponentView?.incrementalRendering?.()) {
+                        callFuncs.forEach(cf => cf.func(view));
+                        clearInterval(intTimer);
+                    }
+                },
+                view.ComponentView?.incrementalRendering?.() ? 0 : 100
+            );
+        }
+    };
+
     @action
     public AddView = (view: DocumentView) => {
         //console.log("MOUNT " + view.props.Document.title + "/" + view.props.LayoutTemplateString);
@@ -50,6 +74,7 @@ export class DocumentManager {
         } else {
             this.DocumentViews.add(view);
         }
+        this.callAddViewFuncs(view);
     };
     public RemoveView = action((view: DocumentView) => {
         this.LinkedDocumentViews.slice().forEach(
@@ -173,7 +198,7 @@ export class DocumentManager {
         targetDoc: Doc, // document to display
         options: DocFocusOptions, // options for how to navigate to target
         createViewFunc = DocumentManager.addView, // how to create a view of the doc if it doesn't exist
-        docContext: Doc[], // context to load that should contain the target
+        docContextPath: Doc[], // context to load that should contain the target
         finished?: () => void
     ): void => {
         const originalTarget = options.originalTarget ?? targetDoc;
@@ -201,29 +226,20 @@ export class DocumentManager {
             finished?.();
         };
         const annoContainerView = (!wasHidden || resolvedTarget !== annotatedDoc) && annotatedDoc && this.getFirstDocumentView(annotatedDoc);
-        const contextDocs = docContext.length ? DocListCast(docContext[0].data) : undefined;
-        const contextDoc = contextDocs?.find(doc => Doc.AreProtosEqual(doc, targetDoc) || Doc.AreProtosEqual(doc, annotatedDoc)) ? docContext.lastElement() : undefined;
-        const targetDocContext = contextDoc || annotatedDoc;
-        const targetDocContextView = (targetDocContext && this.getFirstDocumentView(targetDocContext)) || (wasHidden && annoContainerView); // if we have an annotation container and the target was hidden, then try again because we just un-hid the document above
-        const focusView = !docView && targetDoc.type === DocumentType.MARKER && annoContainerView ? annoContainerView : docView;
         if (annoContainerView) {
             if (annoContainerView.props.Document.layoutKey === 'layout_icon') {
-                annoContainerView.iconify(() =>
-                    annoContainerView.focus(targetDoc, {
-                        ...options,
-                        originalTarget,
-                        afterFocus: (didFocus: boolean) =>
-                            new Promise<ViewAdjustment>(res => {
-                                focusAndFinish(true);
-                                res(ViewAdjustment.doNothing);
-                            }),
-                    })
-                );
-                return;
-            } else if (!docView && targetDoc.type !== DocumentType.MARKER) {
+                return annoContainerView.iconify(() => DocumentManager.Instance.AddViewRenderedCb(targetDoc, () => this.jumpToDocument(resolvedTarget ?? targetDoc, { ...options, toggleTarget: false }, createViewFunc, docContextPath, finished)), 30);
+            }
+            if (!docView && targetDoc.type !== DocumentType.MARKER) {
                 annoContainerView.focus(targetDoc, {}); // this allows something like a PDF view to remove its doc filters to expose the target so that it can be found in the retry code below
             }
         }
+
+        const contextDoc = docContextPath.length ? docContextPath[0] : undefined;
+        const remainingDocContext = docContextPath.length ? docContextPath.slice(1) : [];
+        const targetDocContext = contextDoc || annotatedDoc;
+        const targetDocContextView = (targetDocContext && this.getFirstDocumentView(targetDocContext)) || (wasHidden && annoContainerView); // if we have an annotation container and the target was hidden, then try again because we just un-hid the document above
+        const focusView = !docView && targetDoc.type === DocumentType.MARKER && annoContainerView ? annoContainerView : docView;
         if (focusView) {
             !options.noSelect && Doc.linkFollowHighlight(focusView.rootDoc, undefined, targetDoc); //TODO:glr make this a setting in PresBox
             if (options.playAudio) DocumentManager.playAudioAnno(focusView.rootDoc);
@@ -252,56 +268,53 @@ export class DocumentManager {
                     // we found a context view and aren't forced to create a new one ... focus on the context first..
                     wasHidden = wasHidden || targetDocContextView.rootDoc.hidden;
                     targetDocContextView.rootDoc.hidden = false; // make sure context isn't hidden
-                    targetDocContext._viewTransition = 'transform 500ms';
+
+                    if (targetDocContext.layoutKey === 'layout_icon') {
+                        return targetDocContextView.iconify(
+                            () => DocumentManager.Instance.AddViewRenderedCb(targetDoc, () => this.jumpToDocument(resolvedTarget ?? targetDoc, { ...options /* originalTarget - needed? */ }, createViewFunc, docContextPath, finished)),
+                            30
+                        );
+                    }
+
+                    const contextFocusTime = options.zoomTime ? options.zoomTime / 2 : 500;
+                    const remainingFocustime = options.zoomTime ? options.zoomTime - contextFocusTime : undefined;
+                    targetDocContextView.setViewTransition('transform', contextFocusTime);
+                    // this makes focusing on contexts run in parallel -- jutmp to document below makes them run sequentially
+                    this.AddViewRenderedCb(targetDoc, () => this.jumpToDocument(targetDoc, { ...options, zoomTime: remainingFocustime }, createViewFunc, remainingDocContext, finished));
                     targetDocContextView.props.focus(targetDocContextView.rootDoc, {
                         ...options,
+                        zoomTime: contextFocusTime,
                         // originalTarget, // needed?
                         afterFocus: async () => {
-                            targetDocContext._viewTransition = undefined;
-                            if (targetDocContext.layoutKey === 'layout_icon') {
-                                targetDocContextView.iconify(() => this.jumpToDocument(resolvedTarget ?? targetDoc, { ...options /* originalTarget - needed?*/ }, createViewFunc, docContext, finished));
+                            // now find the target document within the context
+                            if (targetDoc._timecodeToShow) {
+                                // if the target has a timecode, it should show up once the (presumed) video context scrubs to the display timecode;
+                                targetDocContext._currentTimecode = targetDoc.anchorTimecodeToShow;
+                                finished?.();
+                            } else {
+                                // otherwise, just look for the target document in this context view now that we've focused the context view
+                                if (this.getFirstDocumentView(resolvedTarget)) {
+                                    // test again for the target view snce we presumably created the context above by focusing on it
+                                    this.jumpToDocument(targetDoc, { ...options, zoomTime: remainingFocustime }, createViewFunc, remainingDocContext, finished);
+                                } else if (targetDoc.layout) {
+                                    // there will no layout for a TEXTANCHOR type document
+                                    createViewFunc(Doc.BrushDoc(targetDoc), finished); //  create a new view of the target
+                                }
                             }
                             return ViewAdjustment.doNothing;
                         },
                     });
-
-                    // now find the target document within the context
-                    if (targetDoc._timecodeToShow) {
-                        // if the target has a timecode, it should show up once the (presumed) video context scrubs to the display timecode;
-                        targetDocContext._currentTimecode = targetDoc.anchorTimecodeToShow;
-                        finished?.();
-                    } else {
-                        // no timecode means we need to find the context view and focus on our target
-                        const retryDocView = this.getFirstDocumentView(resolvedTarget); // test again for the target view snce we presumably created the context above by focusing on it
-                        if (retryDocView) {
-                            // we found the target in the context.
-                            Doc.linkFollowHighlight(retryDocView.rootDoc);
-                            retryDocView.focus(targetDoc, {
-                                ...options,
-                                // originalTarget -- needed?
-                                afterFocus: (didFocus: boolean) =>
-                                    new Promise<ViewAdjustment>(res => {
-                                        !options.noSelect && focusAndFinish(true);
-                                        res(ViewAdjustment.doNothing);
-                                    }),
-                            }); // focus on the target in the context
-                        } else if (targetDoc.layout) {
-                            // there will no layout for a TEXTANCHOR type document
-                            createViewFunc(Doc.BrushDoc(targetDoc), finished); //  create a new view of the target
-                        }
-                    }
                 } else {
-                    if (docContext.length && docContext[0]?.layoutKey === 'layout_icon') {
-                        const docContextView = this.getFirstDocumentView(docContext[0]);
-                        if (docContextView) {
-                            return docContextView.iconify(() => this.jumpToDocument(targetDoc, { ...options, originalTarget }, createViewFunc, docContext.slice(1, docContext.length), finished));
-                        }
+                    if (docContextPath.length && docContextPath[0]?.layoutKey === 'layout_icon') {
+                        Doc.deiconifyView(docContextPath[0]);
+                        this.jumpToDocument(targetDoc, options, createViewFunc, docContextPath, finished);
+                    } else {
+                        // there's no context view so we need to create one first and try again when that finishes
+                        createViewFunc(
+                            targetDocContext, // after creating the context, this calls the finish function that will retry looking for the target
+                            () => this.jumpToDocument(targetDoc, { ...options }, (doc: Doc, finished?: () => void) => doc !== targetDocContext && createViewFunc(doc, finished), remainingDocContext, finished)
+                        );
                     }
-                    // there's no context view so we need to create one first and try again when that finishes
-                    createViewFunc(
-                        targetDocContext, // after creating the context, this calls the finish function that will retry looking for the target
-                        () => this.jumpToDocument(targetDoc, { ...options, originalTarget }, (doc: Doc, finished?: () => void) => doc !== targetDocContext && createViewFunc(doc, finished), docContext, finished)
-                    );
                 }
             }
         }
diff --git a/src/client/util/LinkFollower.ts b/src/client/util/LinkFollower.ts
index 4f742817a..aec4db1df 100644
--- a/src/client/util/LinkFollower.ts
+++ b/src/client/util/LinkFollower.ts
@@ -48,7 +48,7 @@ export class LinkFollower {
                             });
                         } else {
                             finished?.();
-                            res(where !== 'inPlace' || BoolCast(sourceDoc.followLinkZoom) ? ViewAdjustment.resetView : ViewAdjustment.doNothing); // for 'inPlace'  resetting the initial focus&zoom would negate the zoom into the target
+                            res(where !== OpenWhere.inPlace || BoolCast(sourceDoc.followLinkZoom) ? ViewAdjustment.resetView : ViewAdjustment.doNothing); // for 'inPlace'  resetting the initial focus&zoom would negate the zoom into the target
                         }
                     }, 100);
                 });
diff --git a/src/client/views/LightboxView.tsx b/src/client/views/LightboxView.tsx
index 1f58763d1..cf0a2fcfb 100644
--- a/src/client/views/LightboxView.tsx
+++ b/src/client/views/LightboxView.tsx
@@ -50,7 +50,6 @@ export class LightboxView extends React.Component<LightboxViewProps> {
             this.LightboxDoc._panY = this._savedState.panY;
             this.LightboxDoc._scrollTop = this._savedState.scrollTop;
             this.LightboxDoc._viewScale = this._savedState.scale;
-            this.LightboxDoc._viewTransition = undefined;
         }
         if (!doc) {
             this._docFilters && (this._docFilters.length = 0);
@@ -167,7 +166,7 @@ export class LightboxView extends React.Component<LightboxViewProps> {
         if (targetDocView && target) {
             const l = DocUtils.MakeLinkToActiveAudio(() => targetDocView.ComponentView?.getAnchor?.() || target).lastElement();
             l && (Cast(l.anchor2, Doc, null).backgroundColor = 'lightgreen');
-            targetDocView.focus(target, { originalTarget: target, willZoom: true, scale: 0.9 });
+            targetDocView.focus(target, { originalTarget: target, willZoom: true, zoomScale: 0.9 });
             if (LightboxView._history?.lastElement().target !== target) LightboxView._history?.push({ doc, target });
         } else {
             if (!target && LightboxView.path.length) {
@@ -177,7 +176,6 @@ export class LightboxView extends React.Component<LightboxViewProps> {
                     LightboxView.LightboxDoc._panY = saved.panY;
                     LightboxView.LightboxDoc._viewScale = saved.scale;
                     LightboxView.LightboxDoc._scrollTop = saved.scrollTop;
-                    LightboxView.LightboxDoc._viewTransition = undefined;
                 }
                 const pop = LightboxView.path.pop();
                 if (pop) {
@@ -211,7 +209,7 @@ export class LightboxView extends React.Component<LightboxViewProps> {
         if (docView) {
             LightboxView._docTarget = target;
             if (!target) docView.ComponentView?.shrinkWrap?.();
-            else docView.focus(target, { willZoom: true, scale: 0.9 });
+            else docView.focus(target, { willZoom: true, zoomScale: 0.9 });
         } else {
             LightboxView.SetLightboxDoc(doc, target);
         }
@@ -290,7 +288,7 @@ export class LightboxView extends React.Component<LightboxViewProps> {
                                             const target = LightboxView._docTarget;
                                             const doc = LightboxView._doc;
                                             //const targetView = target && DocumentManager.Instance.getLightboxDocumentView(target);
-                                            if (doc === r.props.Document && (!target || target === doc)) r.ComponentView?.shrinkWrap?.();
+                                            //if (doc === r.props.Document && (!target || target === doc)) r.ComponentView?.shrinkWrap?.();
                                             //else target?.focus(target, { willZoom: true, scale: 0.9, instant: true }); // bcz: why was this here?  it breaks smooth navigation in lightbox using 'next' button
                                         })
                                     );
diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx
index 656a56a15..66c3ed439 100644
--- a/src/client/views/PropertiesButtons.tsx
+++ b/src/client/views/PropertiesButtons.tsx
@@ -14,7 +14,7 @@ import { SelectionManager } from '../util/SelectionManager';
 import { undoBatch } from '../util/UndoManager';
 import { Colors } from './global/globalEnums';
 import { InkingStroke } from './InkingStroke';
-import { DocumentView } from './nodes/DocumentView';
+import { DocumentView, OpenWhere } from './nodes/DocumentView';
 import { VideoBox } from './nodes/VideoBox';
 import { pasteImageBitmap } from './nodes/WebBoxRenderer';
 import './PropertiesButtons.scss';
@@ -117,7 +117,7 @@ export class PropertiesButtons extends React.Component<{}, {}> {
             'In Place',
             'isInPlaceContainer',
             on => `${on ? 'Make' : 'Remove'} in place container flag`,
-            on => 'window',
+            on => 'window-restore',
             onClick => {
                 SelectionManager.Views().forEach(dv => {
                     const containerDoc = dv.rootDoc;
@@ -129,7 +129,7 @@ export class PropertiesButtons extends React.Component<{}, {}> {
                         containerDoc._forceActive =
                         containerDoc._isInPlaceContainer =
                             !containerDoc._isInPlaceContainer;
-                    containerDoc.followLinkLocation = containerDoc._isInPlaceContainer ? 'inPlace' : undefined;
+                    containerDoc.followLinkLocation = containerDoc._isInPlaceContainer ? OpenWhere.inPlace : undefined;
                     containerDoc._xPadding = containerDoc._yPadding = containerDoc._isInPlaceContainer ? 10 : undefined;
                     const menuDoc = DocListCast(dv.dataDoc[dv.props.fieldKey ?? Doc.LayoutFieldKey(containerDoc)]).lastElement();
                     if (menuDoc) {
@@ -139,7 +139,7 @@ export class PropertiesButtons extends React.Component<{}, {}> {
                         }
                         DocListCast(menuDoc[Doc.LayoutFieldKey(menuDoc)]).forEach(menuItem => {
                             menuItem.followLinkAudio = menuItem.followAllLinks = menuItem._isLinkButton = true;
-                            menuItem._followLinkLocation = 'inPlace';
+                            menuItem._followLinkLocation = OpenWhere.inPlace;
                         });
                     }
                 });
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index f7cc32cff..bc08e920a 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -1409,7 +1409,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
     });
 
     @undoBatch
-    changeFollowBehavior = action((follow: string) => this.selectedDoc && (this.selectedDoc.followLinkLocation = follow));
+    changeFollowBehavior = action((follow: string) => this.sourceAnchor && (this.sourceAnchor.followLinkLocation = follow));
 
     @undoBatch
     changeAnimationBehavior = action((behavior: string) => this.sourceAnchor && (this.sourceAnchor.linkAnimEffect = behavior));
@@ -1612,7 +1612,8 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
                                                 <option value="add:right">Opening in new right pane</option>
                                                 <option value="replace:left">Replacing left tab</option>
                                                 <option value="replace:right">Replacing right tab</option>
-                                                <option value="fullScreen">Opening full screen</option>
+                                                <option value="fullScreen">Overlaying current tab</option>
+                                                <option value="lightbox">Opening in lightbox</option>
                                                 <option value="add">Opening in new tab</option>
                                                 <option value="replace">Replacing current tab</option>
                                                 <option value="inPlace">Opening in place</option>
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 8cbe548c7..ffc004df6 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -191,7 +191,7 @@ export class CollectionDockingView extends CollectionSubView() {
 
         if (!pullSide && stack) {
             stack.addChild(docContentConfig, undefined);
-            stack.setActiveContentItem(stack.contentItems[stack.contentItems.length - 1]);
+            setTimeout(() => stack.setActiveContentItem(stack.contentItems[stack.contentItems.length - 1]));
         } else {
             const newContentItem = () => {
                 const newItem = glayRoot.layoutManager.createContentItem({ type: 'stack', content: [docContentConfig] }, instance._goldenLayout);
@@ -389,6 +389,7 @@ export class CollectionDockingView extends CollectionSubView() {
                 const className = typeof htmlTarget.className === 'string' ? htmlTarget.className : '';
                 if (!className.includes('lm_close') && !className.includes('lm_maximise')) {
                     this._flush = UndoManager.StartBatch('golden layout edit');
+                    DocServer.UPDATE_SERVER_CACHE();
                 }
             }
         }
diff --git a/src/client/views/collections/CollectionMenu.tsx b/src/client/views/collections/CollectionMenu.tsx
index db81f28f6..e2594b6ae 100644
--- a/src/client/views/collections/CollectionMenu.tsx
+++ b/src/client/views/collections/CollectionMenu.tsx
@@ -31,7 +31,7 @@ import { Colors } from '../global/globalEnums';
 import { ActiveFillColor, ActiveInkColor, SetActiveArrowEnd, SetActiveArrowStart, SetActiveBezierApprox, SetActiveFillColor, SetActiveInkColor, SetActiveInkWidth } from '../InkingStroke';
 import { LightboxView } from '../LightboxView';
 import { CollectionFreeFormDocumentView } from '../nodes/CollectionFreeFormDocumentView';
-import { DocumentView } from '../nodes/DocumentView';
+import { DocumentView, OpenWhereMod } from '../nodes/DocumentView';
 import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
 import { RichTextMenu } from '../nodes/formattedText/RichTextMenu';
 import { DefaultStyleProvider } from '../StyleProvider';
@@ -569,7 +569,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu
     startRecording = () => {
         const doc = Docs.Create.ScreenshotDocument({ title: 'screen recording', _fitWidth: true, _width: 400, _height: 200, mediaState: 'pendingRecording' });
         //Doc.AddDocToList(Doc.MyOverlayDocs, undefined, doc);
-        CollectionDockingView.AddSplit(doc, 'right');
+        CollectionDockingView.AddSplit(doc, OpenWhereMod.right);
     };
 
     @computed
@@ -733,11 +733,12 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionView
                 doc._currentFrame = 0;
                 CollectionFreeFormDocumentView.setupKeyframes(childDocs, 0);
             }
-            CollectionFreeFormDocumentView.updateKeyframe(childDocs, currentFrame || 0);
+            CollectionFreeFormDocumentView.updateKeyframe(undefined, childDocs, currentFrame || 0);
             doc._currentFrame = newFrame === undefined ? 0 : Math.max(0, newFrame);
         }
     }
 
+    _keyTimer: NodeJS.Timeout | undefined;
     @undoBatch
     @action
     nextKeyframe = (): void => {
@@ -746,7 +747,7 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionView
             this.document._currentFrame = 0;
             CollectionFreeFormDocumentView.setupKeyframes(this.childDocs, 0);
         }
-        CollectionFreeFormDocumentView.updateKeyframe(this.childDocs, currentFrame || 0);
+        this._keyTimer = CollectionFreeFormDocumentView.updateKeyframe(this._keyTimer, this.childDocs, currentFrame || 0);
         this.document._currentFrame = Math.max(0, (currentFrame || 0) + 1);
         this.document.lastFrame = Math.max(NumCast(this.document._currentFrame), NumCast(this.document.lastFrame));
     };
@@ -758,7 +759,7 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionView
             this.document._currentFrame = 0;
             CollectionFreeFormDocumentView.setupKeyframes(this.childDocs, 0);
         }
-        CollectionFreeFormDocumentView.gotoKeyframe(this.childDocs.slice());
+        this._keyTimer = CollectionFreeFormDocumentView.gotoKeyframe(this._keyTimer, this.childDocs.slice());
         this.document._currentFrame = Math.max(0, (currentFrame || 0) - 1);
     };
 
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index 13984171c..af1fb175a 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -196,11 +196,12 @@ export class TabDocView extends React.Component<TabDocViewProps> {
                 () => SelectionManager.Views().some(v => v.topMost && v.props.Document === doc),
                 action(selected => {
                     if (selected) this._activated = true;
-                    const toggle = tab.element[0].children[1].children[0] as HTMLInputElement;
+                    const toggle = tab.element[0].children[2].children[0] as HTMLInputElement;
                     selected && tab.contentItem !== tab.header.parent.getActiveContentItem() && UndoManager.RunInBatch(() => tab.header.parent.setActiveContentItem(tab.contentItem), 'tab switch');
-                    // toggle.style.fontWeight = selected ? "bold" : "";
+                    toggle.style.fontWeight = selected ? 'bold' : '';
                     // toggle.style.textTransform = selected ? "uppercase" : "";
-                })
+                }),
+                { fireImmediately: true }
             );
 
             // highlight the tab when the tab document is brushed in any part of the UI
@@ -378,14 +379,7 @@ export class TabDocView extends React.Component<TabDocViewProps> {
         if (options?.willZoom !== false && shrinkwrap && this._document) {
             const focusSpeed = options.zoomTime ?? 500;
             shrinkwrap();
-            this._document._viewTransition = `transform ${focusSpeed}ms`;
-            setTimeout(
-                action(() => {
-                    this._document!._viewTransition = undefined;
-                    options?.afterFocus?.(false);
-                }),
-                focusSpeed
-            );
+            this._view?.setViewTransition('transform', focusSpeed, () => options?.afterFocus?.(false));
         } else {
             options?.afterFocus?.(false);
         }
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
index be20bf207..c57810a98 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
@@ -270,12 +270,12 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
                     </marker>
                     <filter id="outline">
                         <feMorphology in="SourceAlpha" result="expanded" operator="dilate" radius="1" />
-                        <feFlood flood-color={`${Colors.DARK_GRAY}`} />
+                        <feFlood floodColor={`${Colors.DARK_GRAY}`} />
                         <feComposite in2="expanded" operator="in" />
                         <feComposite in="SourceGraphic" />
                     </filter>
                     <filter x="0" y="0" width="1" height="1" id={`${link[Id] + 'background'}`}>
-                        <feFlood flood-color={`${StrCast(link._backgroundColor, 'white')}`} result="bg" />
+                        <feFlood floodColor={`${StrCast(link._backgroundColor, 'white')}`} result="bg" />
                         <feMerge>
                             <feMergeNode in="bg" />
                             <feMergeNode in="SourceGraphic" />
@@ -286,7 +286,7 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
                     filter={LinkManager.currentLink === link ? 'url(#outline)' : ''}
                     fill="pink"
                     stroke="antiquewhite"
-                    stroke-width="4"
+                    strokeWidth="4"
                     className="collectionfreeformlinkview-linkLine"
                     style={{ pointerEvents: 'all', opacity: this._opacity, stroke, strokeWidth }}
                     onClick={this.onClickLine}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 5fb3c1ac6..b8ff59a14 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1,4 +1,5 @@
 import { Bezier } from 'bezier-js';
+import { Colors } from 'browndash-components';
 import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import { computedFn } from 'mobx-utils';
@@ -47,7 +48,7 @@ import { StyleProp } from '../../StyleProvider';
 import { CollectionSubView } from '../CollectionSubView';
 import { TreeViewType } from '../CollectionTreeView';
 import { TabDocView } from '../TabDocView';
-import { computePivotLayout, computePassLayout as computePassLayout, computeStarburstLayout, computeTimelineLayout, PoolData, ViewDefBounds, ViewDefResult } from './CollectionFreeFormLayoutEngines';
+import { computePassLayout, computePivotLayout, computeStarburstLayout, computeTimelineLayout, PoolData, ViewDefBounds, ViewDefResult } from './CollectionFreeFormLayoutEngines';
 import { CollectionFreeFormRemoteCursors } from './CollectionFreeFormRemoteCursors';
 import './CollectionFreeFormView.scss';
 import { MarqueeView } from './MarqueeView';
@@ -108,7 +109,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     }
 
     @observable.shallow _layoutElements: ViewDefResult[] = []; // shallow because some layout items (eg pivot labels) are just generated 'divs' and can't be frozen as observables
-    @observable _viewTransition: number = 0; // sets the pan/zoom transform ease time- used by nudge(), focus() etc to smoothly zoom/pan.  set to 0 to use document's transition time or default of 0
+    @observable _panZoomTransition: number = 0; // sets the pan/zoom transform ease time- used by nudge(), focus() etc to smoothly zoom/pan.  set to 0 to use document's transition time or default of 0
     @observable _hLines: number[] | undefined;
     @observable _vLines: number[] | undefined;
     @observable _firstRender = true; // this turns off rendering of the collection's content so that there's instant feedback when a tab is switched of what content will be shown.
@@ -176,6 +177,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         return this.getContainerTransform().translate(-this.cachedCenteringShiftX, -this.cachedCenteringShiftY).transform(this.cachedGetLocalTransform);
     }
 
+    _keyTimer: NodeJS.Timeout | undefined;
     changeKeyFrame = (back = false) => {
         const currentFrame = Cast(this.Document._currentFrame, 'number', null);
         if (currentFrame === undefined) {
@@ -183,14 +185,17 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             CollectionFreeFormDocumentView.setupKeyframes(this.childDocs, 0);
         }
         if (back) {
-            CollectionFreeFormDocumentView.gotoKeyframe(this.childDocs.slice());
+            this._keyTimer = CollectionFreeFormDocumentView.gotoKeyframe(this._keyTimer, this.childDocs.slice());
             this.Document._currentFrame = Math.max(0, (currentFrame || 0) - 1);
         } else {
-            CollectionFreeFormDocumentView.updateKeyframe(this.childDocs, currentFrame || 0);
+            this._keyTimer = CollectionFreeFormDocumentView.updateKeyframe(this._keyTimer, this.childDocs, currentFrame || 0);
             this.Document._currentFrame = Math.max(0, (currentFrame || 0) + 1);
             this.Document.lastFrame = Math.max(NumCast(this.Document._currentFrame), NumCast(this.Document.lastFrame));
         }
     };
+    @observable _keyframeEditing = false;
+    @action setKeyFrameEditing = (set: boolean) => (this._keyframeEditing = set);
+    getKeyFrameEditing = () => this._keyframeEditing;
     onBrowseClickHandler = () => this.props.onBrowseClick?.() || ScriptCast(this.layoutDoc.onBrowseClick);
     onChildClickHandler = () => this.props.childClickScript || ScriptCast(this.Document.onChildClick);
     onChildDoubleClickHandler = () => this.props.childDoubleClickScript || ScriptCast(this.Document.onChildDoubleClick);
@@ -712,6 +717,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
 
     @action
     pan = (e: PointerEvent | React.Touch | { clientX: number; clientY: number }): void => {
+        this.props.DocumentView?.().clearViewTransition();
         const [dx, dy] = this.getTransform().transformDirection(e.clientX - this._lastX, e.clientY - this._lastY);
         this.setPan(NumCast(this.Document._panX) - dx, NumCast(this.Document._panY) - dy, 0, true);
         this._lastX = e.clientX;
@@ -1079,7 +1085,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             }
         }
         if (!this.layoutDoc._lockedTransform || LightboxView.LightboxDoc || DocListCast(Doc.MyOverlayDocs?.data).includes(this.Document)) {
-            this._viewTransition = panTime;
+            this._panZoomTransition = panTime;
             const scale = this.getLocalTransform().inverse().Scale;
             const minScale = NumCast(this.rootDoc._viewScaleMin, 1);
             const minPanX = NumCast(this.rootDoc._panXMin, 0);
@@ -1098,7 +1104,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             this.setPan(NumCast(this.layoutDoc._panX) + ((this.props.PanelWidth() / 2) * x) / this.zoomScaling(), NumCast(this.layoutDoc._panY) + ((this.props.PanelHeight() / 2) * -y) / this.zoomScaling(), nudgeTime, true);
             this._lastNudge && clearTimeout(this._lastNudge);
             this._lastNudge = setTimeout(
-                action(() => (this._viewTransition = 0)),
+                action(() => (this._panZoomTransition = 0)),
                 nudgeTime
             );
             return true;
@@ -1127,7 +1133,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     @action
     zoomSmoothlyAboutPt(docpt: number[], scale: number, transitionTime = 500) {
         if (this.Document._isGroup) return;
-        this._viewTransition = transitionTime;
+        this._panZoomTransition = transitionTime;
         const screenXY = this.getTransform().inverse().transformPoint(docpt[0], docpt[1]);
         this.layoutDoc[this.scaleFieldKey] = scale;
         const newScreenXY = this.getTransform().inverse().transformPoint(docpt[0], docpt[1]);
@@ -1135,7 +1141,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         const newpan = this.getTransform().transformDirection(scrDelta.x, scrDelta.y);
         this.layoutDoc._panX = NumCast(this.layoutDoc._panX) - newpan[0];
         this.layoutDoc._panY = NumCast(this.layoutDoc._panY) - newpan[1];
-        return new Promise<number>(res => setTimeout(() => res(runInAction(() => (this._viewTransition = 0))), this._viewTransition)); // set transition to be smooth, then reset
+        return new Promise<number>(res => setTimeout(() => res(runInAction(() => (this._panZoomTransition = 0))), this._panZoomTransition)); // set transition to be smooth, then reset
     }
 
     focusDocument = (doc: Doc, options: DocFocusOptions) => {
@@ -1185,14 +1191,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 doc.hidden && Doc.UnHighlightDoc(doc);
                 const resetView = options?.afterFocus ? await options?.afterFocus(moved) : ViewAdjustment.doNothing;
                 if (resetView) {
-                    const restoreState = (!LightboxView.LightboxDoc || LightboxView.LightboxDoc === this.props.Document) && savedState;
+                    const restoreState = savedState;
                     if (typeof restoreState !== 'boolean') {
                         this.Document._panX = restoreState.panX;
                         this.Document._panY = restoreState.panY;
                         this.Document[this.scaleFieldKey] = restoreState.scale;
                     }
                 }
-                runInAction(() => (this._viewTransition = 0));
+                runInAction(() => (this._panZoomTransition = 0));
                 return resetView;
             };
             const xf = !cantTransform
@@ -1364,7 +1370,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             z: Cast(z, 'number'),
             color: Cast(color, 'string') ? StrCast(color) : this.props.styleProvider?.(childDoc, this.props, StyleProp.Color),
             backgroundColor: Cast(backgroundColor, 'string') ? StrCast(backgroundColor) : this.props.styleProvider?.(childDoc, this.props, StyleProp.BackgroundColor),
-            opacity: Cast(opacity, 'number') ?? this.props.styleProvider?.(childDoc, this.props, StyleProp.Opacity),
+            opacity: this._keyframeEditing ? 1 : Cast(opacity, 'number') ?? this.props.styleProvider?.(childDoc, this.props, StyleProp.Opacity),
             zIndex: Cast(zIndex, 'number'),
             width: Cast(childDocLayout._width, 'number'),
             height: Cast(childDocLayout._height, 'number'),
@@ -1541,7 +1547,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     scrollFocus = (anchor: Doc, options: DocFocusOptions) => {
         const focusSpeed = options.instant ? 0 : options.zoomTime ?? 500;
         return PresBox.restoreTargetDocView(
-            this.rootDoc, //
+            this.props.DocumentView?.(), //
             { pinDocLayout: BoolCast(anchor.presPinDocLayout) },
             anchor,
             focusSpeed,
@@ -1869,6 +1875,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         DragManager.SetSnapLines(horizLines, vertLines);
     };
 
+    incrementalRendering = () => this.childDocs.filter(doc => !this._renderCutoffData.get(doc[Id])).length !== 0;
+
     incrementalRender = action(() => {
         if (!LightboxView.LightboxDoc || LightboxView.IsLightboxDocView(this.props.docViewPath())) {
             const unrendered = this.childDocs.filter(doc => !this._renderCutoffData.get(doc[Id]));
@@ -1944,7 +1952,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                         zoomScaling={this.zoomScaling}
                         presPaths={this.showPresPaths}
                         presPinView={BoolCast(this.Document.presPinView)}
-                        transition={this._viewTransition ? `transform ${this._viewTransition}ms` : Cast(this.layoutDoc._viewTransition, 'string', null)}
+                        transition={this._panZoomTransition ? `transform ${this._panZoomTransition}ms` : Cast(this.layoutDoc._viewTransition, 'string', null)}
                         viewDefDivClick={this.props.viewDefDivClick}>
                         {this.children}
                     </CollectionFreeFormViewPannableContents>
@@ -2265,6 +2273,11 @@ ScriptingGlobals.add(function nextKeyFrame(readOnly: boolean) {
 ScriptingGlobals.add(function prevKeyFrame(readOnly: boolean) {
     !readOnly && (SelectionManager.Views()[0].ComponentView as CollectionFreeFormView)?.changeKeyFrame(true);
 });
+ScriptingGlobals.add(function curKeyFrame(readOnly: boolean) {
+    const selView = SelectionManager.Views();
+    if (readOnly) return selView[0].ComponentView?.getKeyFrameEditing?.() ? Colors.MEDIUM_BLUE : undefined;
+    runInAction(() => selView[0].ComponentView?.setKeyFrameEditing?.(!selView[0].ComponentView?.getKeyFrameEditing?.()));
+});
 ScriptingGlobals.add(function pinWithView(readOnly: boolean, pinDocContent: boolean) {
     !readOnly && SelectionManager.Views().forEach(view => TabDocView.PinDoc(view.rootDoc, { pinDocContent, pinViewport: MarqueeView.CurViewBounds(view.rootDoc, view.props.PanelWidth(), view.props.PanelHeight()) }));
 });
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 9df3e195f..7c1137292 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -19,7 +19,6 @@ import { undoBatch, UndoManager } from '../../../util/UndoManager';
 import { ContextMenu } from '../../ContextMenu';
 import { OpenWhere } from '../../nodes/DocumentView';
 import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox';
-import { PresBox } from '../../nodes/trails/PresBox';
 import { VideoBox } from '../../nodes/VideoBox';
 import { pasteImageBitmap } from '../../nodes/WebBoxRenderer';
 import { PreviewCursor } from '../../PreviewCursor';
@@ -244,7 +243,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
         if (!(e.nativeEvent as any).marqueeHit) {
             (e.nativeEvent as any).marqueeHit = true;
             // allow marquee if right click OR alt+left click OR in adding presentation slide & left key drag mode
-            if (e.button === 2 || (e.button === 0 && e.altKey) || (PresBox.startMarquee && e.button === 0)) {
+            if (e.button === 2 || (e.button === 0 && e.altKey)) {
                 // if (e.altKey || (MarqueeView.DragMarquee && this.props.active(true))) {
                 this.setPreviewCursor(e.clientX, e.clientY, true, false);
                 // (!e.altKey) && e.stopPropagation(); // bcz: removed so that you can alt-click on button in a collection to switch link following behaviors.
@@ -273,9 +272,6 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
             this.cleanupInteractions(true); // stop listening for events if another lower-level handle (e.g. another Marquee) has stopPropagated this
         }
         e.altKey && e.preventDefault();
-        if (PresBox.startMarquee) {
-            e.stopPropagation();
-        }
     };
 
     @action
@@ -296,11 +292,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
             document.removeEventListener('pointerdown', hideMarquee, true);
             document.removeEventListener('wheel', hideMarquee, true);
         };
-        if (PresBox.startMarquee) {
-            this.pinWithView();
-            PresBox.startMarquee = false;
-        }
-        if (!this._commandExecuted && Math.abs(this.Bounds.height * this.Bounds.width) > 100 && !PresBox.startMarquee) {
+        if (!this._commandExecuted && Math.abs(this.Bounds.height * this.Bounds.width) > 100) {
             MarqueeOptionsMenu.Instance.createCollection = this.collection;
             MarqueeOptionsMenu.Instance.delete = this.delete;
             MarqueeOptionsMenu.Instance.summarize = this.summary;
@@ -686,7 +678,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
                 className="marqueeView"
                 style={{
                     overflow: StrCast(this.props.Document._overflow),
-                    cursor: [InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool) || this._visible || PresBox.startMarquee ? 'crosshair' : 'pointer',
+                    cursor: [InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool) || this._visible ? 'crosshair' : 'pointer',
                 }}
                 onDragOver={e => e.preventDefault()}
                 onScroll={e => (e.currentTarget.scrollTop = e.currentTarget.scrollLeft = 0)}
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index 868822fbf..bf1f13a06 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -115,45 +115,30 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
         });
     }
 
-    public static updateKeyframe(docs: Doc[], time: number, targetDoc?: Doc) {
+    public static updateKeyframe(timer: NodeJS.Timeout | undefined, docs: Doc[], time: number, targetDoc?: Doc) {
+        if (timer) clearTimeout(timer);
+        const newTimer = DocumentView.SetViewTransition(docs, 'all', 1000, undefined, true);
         const timecode = Math.round(time);
-        docs.forEach(
-            action(doc => {
-                doc._viewTransition = doc.dataTransition = 'all 1s';
-                CollectionFreeFormDocumentView.animFields.forEach(val => {
-                    const findexed = Cast(doc[`${val.key}-indexed`], listSpec('number'), null);
-                    findexed?.length <= timecode + 1 && findexed.push(undefined as any as number);
-                });
-                CollectionFreeFormDocumentView.animStringFields.forEach(val => {
-                    const findexed = Cast(doc[`${val}-indexed`], listSpec('string'), null);
-                    findexed?.length <= timecode + 1 && findexed.push(undefined as any as string);
-                });
-                CollectionFreeFormDocumentView.animDataFields(doc).forEach(val => {
-                    const findexed = Cast(doc[`${val}-indexed`], listSpec(InkField), null);
-                    findexed?.length <= timecode + 1 && findexed.push(undefined as any);
-                });
-            })
-        );
-        setTimeout(
-            () =>
-                docs.forEach(doc => {
-                    doc._viewTransition = undefined;
-                    doc.dataTransition = 'inherit';
-                }),
-            1010
-        );
+        docs.forEach(doc => {
+            CollectionFreeFormDocumentView.animFields.forEach(val => {
+                const findexed = Cast(doc[`${val.key}-indexed`], listSpec('number'), null);
+                findexed?.length <= timecode + 1 && findexed.push(undefined as any as number);
+            });
+            CollectionFreeFormDocumentView.animStringFields.forEach(val => {
+                const findexed = Cast(doc[`${val}-indexed`], listSpec('string'), null);
+                findexed?.length <= timecode + 1 && findexed.push(undefined as any as string);
+            });
+            CollectionFreeFormDocumentView.animDataFields(doc).forEach(val => {
+                const findexed = Cast(doc[`${val}-indexed`], listSpec(InkField), null);
+                findexed?.length <= timecode + 1 && findexed.push(undefined as any);
+            });
+        });
+        return newTimer;
     }
 
-    public static gotoKeyframe(docs: Doc[], duration = 1000) {
-        docs.forEach(doc => (doc._viewTransition = doc.dataTransition = `all ${duration}ms`));
-        setTimeout(
-            () =>
-                docs.forEach(doc => {
-                    doc._viewTransition = undefined;
-                    doc.dataTransition = 'inherit';
-                }),
-            1010
-        );
+    public static gotoKeyframe(timer: NodeJS.Timeout | undefined, docs: Doc[], duration = 1000) {
+        if (timer) clearTimeout(timer);
+        return DocumentView.SetViewTransition(docs, 'all', duration, undefined, true);
     }
 
     public static setupZoom(doc: Doc, targDoc: Doc) {
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index fb20887cb..4abfef563 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -127,6 +127,7 @@ export interface DocComponentView {
     Pause?: () => void;
     setFocus?: () => void;
     componentUI?: (boundsLeft: number, boundsTop: number) => JSX.Element | null;
+    incrementalRendering?: () => void;
     fieldKey?: string;
     annotationKey?: string;
     getTitle?: () => string;
@@ -238,7 +239,7 @@ export interface DocumentViewInternalProps extends DocumentViewProps {
 @observer
 export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps>() {
     public static SelectAfterContextMenu = true; //  whether a document should be selected after it's contextmenu is triggered.
-    _animateScaleTime = 300; // milliseconds;
+    @observable _animateScaleTime: Opt<number>; // milliseconds for animating between views.  defaults to 300 if not uset
     @observable _animateScalingTo = 0;
     @observable _pendingDoubleClick = false;
     private _disposers: { [name: string]: IReactionDisposer } = {};
@@ -259,6 +260,9 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
     private get topMost() {
         return this.props.renderDepth === 0 && !LightboxView.LightboxDoc;
     }
+    public get animateScaleTime() {
+        return this._animateScaleTime ?? 300;
+    }
     public get displayName() {
         return 'DocumentView(' + this.props.Document.title + ')';
     } // this makes mobx trace() statements more descriptive
@@ -1396,7 +1400,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                         fontFamily: StrCast(this.Document._fontFamily, 'inherit'),
                         fontSize: Cast(this.Document._fontSize, 'string', null),
                         transform: this._animateScalingTo ? `scale(${this._animateScalingTo})` : undefined,
-                        transition: !this._animateScalingTo ? StrCast(this.Document.dataTransition) : `transform ${this._animateScaleTime / 1000}s ease-${this._animateScalingTo < 1 ? 'in' : 'out'}`,
+                        transition: !this._animateScalingTo ? StrCast(this.Document.dataTransition) : `transform ${this.animateScaleTime / 1000}s ease-${this._animateScalingTo < 1 ? 'in' : 'out'}`,
                     }}>
                     {this.innards}
                     {this.onClickHandler && this.props.ContainingCollectionView?.props.Document._viewType === CollectionViewType.Time ? <div className="documentView-contentBlocker" /> : null}
@@ -1476,7 +1480,37 @@ export class DocumentView extends React.Component<DocumentViewProps> {
         return 'DocumentView(' + this.props.Document?.title + ')';
     } // this makes mobx trace() statements more descriptive
     public ContentRef = React.createRef<HTMLDivElement>();
+    public ViewTimer: NodeJS.Timeout | undefined; // timer for res
     private _disposers: { [name: string]: IReactionDisposer } = {};
+    public clearViewTransition = () => {
+        this.ViewTimer && clearTimeout(this.ViewTimer);
+        this.rootDoc._viewTransition = undefined;
+    };
+    public setViewTransition = (transProp: string, timeInMs: number, afterTrans?: () => void, dataTrans = false) => {
+        this.rootDoc._viewTransition = `${transProp} ${timeInMs}ms`;
+        if (dataTrans) this.rootDoc._dataTransition = `${transProp} ${timeInMs}ms`;
+        this.ViewTimer && clearTimeout(this.ViewTimer);
+        return (this.ViewTimer = setTimeout(() => {
+            this.rootDoc._viewTransition = undefined;
+            this.rootDoc._dataTransition = 'inherit';
+            afterTrans?.();
+        }, timeInMs + 10));
+    };
+    public static SetViewTransition(docs: Doc[], transProp: string, timeInMs: number, afterTrans?: () => void, dataTrans = false) {
+        docs.forEach(doc => {
+            doc._viewTransition = `${transProp} ${timeInMs}ms`;
+            dataTrans && (doc.dataTransition = `${transProp} ${timeInMs}ms`);
+        });
+        return setTimeout(
+            () =>
+                docs.forEach(doc => {
+                    doc._viewTransition = undefined;
+                    dataTrans && (doc.dataTransition = 'inherit');
+                    afterTrans?.();
+                }),
+            timeInMs + 10
+        );
+    }
 
     public static showBackLinks(linkSource: Doc) {
         const docid = Doc.CurrentUserEmail + Doc.GetProto(linkSource)[Id] + '-pivotish';
@@ -1617,15 +1651,21 @@ export class DocumentView extends React.Component<DocumentViewProps> {
         return { left, top, right, bottom, center: this.ComponentView?.getCenter?.(xf) };
     };
 
-    public iconify(finished?: () => void) {
+    public iconify(finished?: () => void, animateTime?: number) {
         this.ComponentView?.updateIcon?.();
+        const animTime = this.docView?._animateScaleTime;
+        runInAction(() => this.docView && animateTime !== undefined && (this.docView._animateScaleTime = animateTime));
+        const finalFinished = action(() => {
+            finished?.();
+            this.docView && (this.docView._animateScaleTime = animTime);
+        });
         const layoutKey = Cast(this.Document.layoutKey, 'string', null);
         if (layoutKey !== 'layout_icon') {
-            this.switchViews(true, 'icon', finished);
+            this.switchViews(true, 'icon', finalFinished);
             if (layoutKey && layoutKey !== 'layout' && layoutKey !== 'layout_icon') this.Document.deiconifyLayout = layoutKey.replace('layout_', '');
         } else {
             const deiconifyLayout = Cast(this.Document.deiconifyLayout, 'string', null);
-            this.switchViews(deiconifyLayout ? true : false, deiconifyLayout, finished);
+            this.switchViews(deiconifyLayout ? true : false, deiconifyLayout, finalFinished);
             this.Document.deiconifyLayout = undefined;
             this.props.bringToFront(this.rootDoc);
         }
@@ -1651,10 +1691,10 @@ export class DocumentView extends React.Component<DocumentViewProps> {
                         this.docView && (this.docView._animateScalingTo = 0);
                         finished?.();
                     }),
-                    this.docView!._animateScaleTime - 10
+                    this.docView ? Math.max(0, this.docView.animateScaleTime - 10) : 0
                 );
             }),
-            this.docView!._animateScaleTime - 10
+            this.docView ? Math.max(0, this.docView?.animateScaleTime - 10) : 0
         );
     });
 
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 416107859..19f5e9e29 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -65,7 +65,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
     scrollFocus = (anchor: Doc, options: DocFocusOptions) => {
         const focusSpeed = options.instant ? 0 : options.zoomTime ?? 500;
         return PresBox.restoreTargetDocView(
-            this.rootDoc, //
+            this.props.DocumentView?.(), //
             { pinDocLayout: BoolCast(anchor.presPinDocLayout) },
             anchor,
             focusSpeed,
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index b19c4a9e2..88aac67a7 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -209,7 +209,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
         }
         if (this._sidebarRef?.current?.makeDocUnfiltered(doc)) return 1;
         this._initialScrollTarget = doc;
-        PresBox.restoreTargetDocView(this.rootDoc, {}, doc, options.zoomTime ?? 500, { pannable: doc.presPinData ? true : false });
+        PresBox.restoreTargetDocView(this.props.DocumentView?.(), {}, doc, options.zoomTime ?? 500, { pannable: doc.presPinData ? true : false });
         return this._pdfViewer?.scrollFocus(doc, NumCast(doc.presPinViewScroll, NumCast(doc.y)), options) ?? (didToggle ? 1 : undefined);
     };
     getAnchor = () => {
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 607cd6187..a8f78edd5 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -961,7 +961,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
     // renders CollectionStackedTimeline
     @computed get renderTimeline() {
         return (
-            <div className="videoBox-stackPanel" style={{ transition: this.transition, height: `${100 - this.heightPercent}%` }}>
+            <div className="videoBox-stackPanel" style={{ transition: this.transition, height: `${100 - this.heightPercent}%`, display: this.heightPercent === 100 ? 'none' : '' }}>
                 <CollectionStackedTimeline
                     ref={action((r: any) => (this._stackedTimeline = r))}
                     {...this.props}
diff --git a/src/client/views/nodes/button/FontIconBox.tsx b/src/client/views/nodes/button/FontIconBox.tsx
index fe8c85e5e..e477d7ae2 100644
--- a/src/client/views/nodes/button/FontIconBox.tsx
+++ b/src/client/views/nodes/button/FontIconBox.tsx
@@ -510,8 +510,12 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
             case ButtonType.EditableText: return this.editableText;
             case ButtonType.DropdownButton: button = this.dropdownButton; break;
             case ButtonType.ToggleButton: button = this.toggleButton; break;
-            case ButtonType.TextButton: button = (
-                    <div className={`menuButton ${this.type}`} style={{ color, backgroundColor, opacity: 1, gridAutoColumns: `${NumCast(this.rootDoc._height)} auto` }}>
+            case ButtonType.TextButton: 
+                    // Script for checking the outcome of the toggle
+                    const script = ScriptCast(this.rootDoc.script);
+                    const checkResult = script?.script.run({ _readOnly_: true }).result;
+                    button = (
+                    <div className={`menuButton ${this.type}`} style={{ color, backgroundColor:checkResult ?? backgroundColor, opacity: 1, gridAutoColumns: `${NumCast(this.rootDoc._height)} auto` }}>
                         {this.Icon(color)}
                         {StrCast(this.rootDoc.buttonText) ? <div className="button-text">{StrCast(this.rootDoc.buttonText)}</div> : null}
                         {label()}
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index 4073677f3..169f51dac 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -5,7 +5,7 @@ import { action, computed, IReactionDisposer, observable, ObservableSet, reactio
 import { observer } from 'mobx-react';
 import { ColorState, SketchPicker } from 'react-color';
 import { Bounce, Fade, Flip, LightSpeed, Roll, Rotate, Zoom } from 'react-reveal';
-import { Doc, DocListCast, FieldResult, StrListCast } from '../../../../fields/Doc';
+import { Doc, DocListCast, FieldResult, Opt, StrListCast } from '../../../../fields/Doc';
 import { Copy, Id } from '../../../../fields/FieldSymbols';
 import { InkTool } from '../../../../fields/InkField';
 import { List } from '../../../../fields/List';
@@ -13,9 +13,9 @@ import { ObjectField } from '../../../../fields/ObjectField';
 import { listSpec } from '../../../../fields/Schema';
 import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../../fields/Types';
 import { AudioField } from '../../../../fields/URLField';
-import { emptyFunction, returnFalse, returnOne, setupMoveUpEvents, StopEvent } from '../../../../Utils';
+import { returnFalse, returnOne, setupMoveUpEvents, StopEvent } from '../../../../Utils';
 import { DocServer } from '../../../DocServer';
-import { Docs, DocumentOptions } from '../../../documents/Documents';
+import { Docs } from '../../../documents/Documents';
 import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes';
 import { DocumentManager } from '../../../util/DocumentManager';
 import { ScriptingGlobals } from '../../../util/ScriptingGlobals';
@@ -30,12 +30,11 @@ import { ViewBoxBaseComponent } from '../../DocComponent';
 import { Colors } from '../../global/globalEnums';
 import { LightboxView } from '../../LightboxView';
 import { CollectionFreeFormDocumentView } from '../CollectionFreeFormDocumentView';
+import { DocFocusOptions, DocumentView, OpenWhere, OpenWhereMod } from '../DocumentView';
 import { FieldView, FieldViewProps } from '../FieldView';
 import { ScriptingBox } from '../ScriptingBox';
 import './PresBox.scss';
 import { PresEffect, PresEffectDirection, PresMovement, PresStatus } from './PresEnums';
-import { map } from 'bluebird';
-import { DocFocusOptions, OpenWhere, OpenWhereMod } from '../DocumentView';
 const { Howl } = require('howler');
 
 export interface PinProps {
@@ -97,7 +96,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     public selectedArray = new ObservableSet<Doc>();
 
     @observable public static Instance: PresBox;
-    @observable static startMarquee: boolean = false; // onclick "+ new slide" in presentation mode, set as true, then when marquee selection finish, onPointerUp automatically triggers PinWithView
 
     @observable _isChildActive = false;
     @observable _moveOnFromAudio: boolean = true;
@@ -140,7 +138,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         return Cast(this.activeItem?.presentationTargetDoc, Doc, null);
     }
     @computed get scrollable() {
-        if (this.targetDoc.type === DocumentType.PDF || this.targetDoc.type === DocumentType.WEB || this.targetDoc.type === DocumentType.RTF || this.targetDoc._viewType === CollectionViewType.Stacking) return true;
+        if ([DocumentType.PDF, DocumentType.WEB, DocumentType.RTF].includes(this.targetDoc.type as DocumentType) || this.targetDoc._viewType === CollectionViewType.Stacking) return true;
         return false;
     }
     @computed get panable() {
@@ -214,16 +212,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         PresBox.Instance = this;
     };
 
-    // There are still other internal frames and should go through all frames before going to next slide
-    nextInternalFrame = (targetDoc: Doc, activeItem: Doc) => {
-        const currentFrame = Cast(targetDoc?._currentFrame, 'number', null);
-        const childDocs = DocListCast(targetDoc[Doc.LayoutFieldKey(targetDoc)]);
-        targetDoc._viewTransition = 'all 1s';
-        setTimeout(() => (targetDoc._viewTransition = undefined), 1010);
-        this.nextKeyframe(targetDoc, activeItem);
-        targetDoc.keyFrameEditing = true;
-    };
-
     _mediaTimer!: [NodeJS.Timeout, Doc];
     // 'Play on next' for audio or video therefore first navigate to the audio/video before it should be played
     startTempMedia = (targetDoc: Doc, activeItem: Doc) => {
@@ -279,23 +267,17 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         const targetDoc: Doc = this.targetDoc;
         const prevItem = Cast(this.childDocs[Math.max(0, this.itemIndex - 1)], Doc, null);
         const prevTargetDoc = Cast(prevItem.presentationTargetDoc, Doc, null);
-        const lastFrame = Cast(targetDoc.lastFrame, 'number', null);
-        const curFrame = NumCast(targetDoc._currentFrame);
         let prevSelected = this.itemIndex;
         // Functionality for group with up
         let didZoom = activeItem.presMovement;
         for (; prevSelected > 0 && this.childDocs[Math.max(0, prevSelected - 1)].groupWithUp; prevSelected--) {
             didZoom = didZoom === 'none' ? this.childDocs[prevSelected].presMovement : didZoom;
         }
-        if (lastFrame !== undefined && curFrame >= 1) {
-            // Case 1: There are still other frames and should go through all frames before going to previous slide
-            this.prevKeyframe(targetDoc, activeItem);
-        } else if (activeItem && this.childDocs[this.itemIndex - 1] !== undefined) {
+        if (activeItem && this.childDocs[this.itemIndex - 1] !== undefined) {
             // Case 2: There are no other frames so it should go to the previous slide
             prevSelected = Math.max(0, prevSelected - 1);
             this.nextSlide(prevSelected);
             this.rootDoc._itemIndex = prevSelected;
-            if (NumCast(prevTargetDoc.lastFrame) > 0) prevTargetDoc._currentFrame = NumCast(prevTargetDoc.lastFrame);
         } else if (this.childDocs[this.itemIndex - 1] === undefined && this.layoutDoc.presLoop) {
             // Case 3: Pres loop is on so it should go to the last slide
             this.gotoDocument(this.childDocs.length - 1, activeItem);
@@ -313,14 +295,13 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             this.rootDoc._itemIndex = index;
             const activeItem: Doc = this.activeItem;
             const targetDoc: Doc = this.targetDoc;
-            let focusSpeed = 500;
             if (activeItem.presActiveFrame !== undefined) {
                 const transTime = NumCast(activeItem.presTransition, 500);
                 const context = DocCast(DocCast(activeItem.presentationTargetDoc).context);
                 if (context) {
-                    const contextView = DocumentManager.Instance.getFirstDocumentView(context);
-                    if (contextView?.ComponentView) {
-                        CollectionFreeFormDocumentView.gotoKeyframe((contextView.ComponentView as CollectionFreeFormView).childDocs.slice(), transTime);
+                    const ffview = DocumentManager.Instance.getFirstDocumentView(context)?.ComponentView as CollectionFreeFormView;
+                    if (ffview) {
+                        this._keyTimer = CollectionFreeFormDocumentView.gotoKeyframe(this._keyTimer, ffview.childDocs.slice(), transTime);
                         context._currentFrame = NumCast(activeItem.presActiveFrame);
                     }
                 }
@@ -335,9 +316,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             if (this.layoutDoc.presStatus !== PresStatus.Edit && (targetDoc.type === DocumentType.AUDIO || targetDoc.type === DocumentType.VID) && activeItem.mediaStart === 'auto') {
                 this.startTempMedia(targetDoc, activeItem);
             }
-            if (targetDoc?.lastFrame !== undefined) {
-                targetDoc._currentFrame = 0;
-            }
             if (!group) this.clearSelectedArray();
             this.childDocs[index] && this.addToSelectedArray(this.childDocs[index]); //Update selected array
             this.turnOffEdit();
@@ -345,14 +323,15 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             this.onHideDocument(); //Handles hide after/before
         }
     });
-    static pinDataTypes(target: Doc): { scrollable?: boolean; pannable?: boolean; temporal?: boolean; clippable?: boolean; dataview?: boolean; textview?: boolean; poslayoutview?: boolean; dataannos?: boolean } {
-        const scrollable = [DocumentType.PDF, DocumentType.RTF, DocumentType.WEB].includes(target.type as any) || target._viewType === CollectionViewType.Stacking;
-        const pannable = [DocumentType.IMG, DocumentType.PDF].includes(target.type as any) || (target.type === DocumentType.COL && target._viewType === CollectionViewType.Freeform);
-        const temporal = [DocumentType.AUDIO, DocumentType.VID].includes(target.type as any);
-        const clippable = [DocumentType.COMPARISON].includes(target.type as any);
-        const dataview = [DocumentType.INK, DocumentType.COL, DocumentType.IMG].includes(target.type as any) && target.activeFrame === undefined;
-        const poslayoutview = [DocumentType.COL].includes(target.type as any) && target.activeFrame === undefined;
-        const textview = [DocumentType.RTF].includes(target.type as any) && target.activeFrame === undefined;
+    static pinDataTypes(target?: Doc): { scrollable?: boolean; pannable?: boolean; temporal?: boolean; clippable?: boolean; dataview?: boolean; textview?: boolean; poslayoutview?: boolean; dataannos?: boolean } {
+        const targetType = target?.type as any;
+        const scrollable = [DocumentType.PDF, DocumentType.RTF, DocumentType.WEB].includes(targetType) || target?._viewType === CollectionViewType.Stacking;
+        const pannable = [DocumentType.IMG, DocumentType.PDF].includes(targetType) || (targetType === DocumentType.COL && target?._viewType === CollectionViewType.Freeform);
+        const temporal = [DocumentType.AUDIO, DocumentType.VID].includes(targetType);
+        const clippable = [DocumentType.COMPARISON].includes(targetType);
+        const dataview = [DocumentType.INK, DocumentType.COL, DocumentType.IMG].includes(targetType) && target?.activeFrame === undefined;
+        const poslayoutview = [DocumentType.COL].includes(targetType) && target?.activeFrame === undefined;
+        const textview = [DocumentType.RTF].includes(targetType) && target?.activeFrame === undefined;
         const dataannos = false;
         return { scrollable, pannable, temporal, clippable, dataview, textview, poslayoutview, dataannos };
     }
@@ -360,8 +339,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     @action
     playAnnotation = (anno: AudioField) => {};
     @action
-    static restoreTargetDocView(bestTarget: Doc, pinProps: PinProps | undefined, activeItem: Doc, transTime: number, pinDataTypes = this.pinDataTypes(bestTarget)) {
-        const presTransitionTime = `all ${transTime}ms`;
+    static restoreTargetDocView(bestTargetView: Opt<DocumentView>, pinProps: PinProps | undefined, activeItem: Doc, transTime: number, pinDataTypes = this.pinDataTypes(bestTargetView?.rootDoc)) {
+        if (!bestTargetView) return;
+        const bestTarget = bestTargetView.rootDoc;
         let changed = false;
         if (pinProps?.pinDocLayout) {
             if (
@@ -420,7 +400,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 .map(str => JSON.parse(str) as { id: string; x: number; y: number; w: number; h: number })
                 .forEach(data => {
                     const doc = DocServer.GetCachedRefField(data.id) as Doc;
-                    doc._dataTransition = presTransitionTime;
+                    doc._dataTransition = `all ${transTime}ms`;
                     doc.x = data.x;
                     doc.y = data.y;
                     doc._width = data.w;
@@ -456,8 +436,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             }
         }
         if (changed) {
-            bestTarget._viewTransition = presTransitionTime;
-            return setTimeout(() => (bestTarget._viewTransition = undefined), transTime + 10);
+            return bestTargetView.setViewTransition('all', transTime);
         }
     }
 
@@ -519,8 +498,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             pinDoc.presPinViewBounds = new List<number>([bounds.left, bounds.top, bounds.left + bounds.width, bounds.top + bounds.height]);
         }
     }
-
-    static _navTimer: NodeJS.Timeout | undefined;
     /**
      * This method makes sure that cursor navigates to the element that
      * has the option open and last in the group.
@@ -549,55 +526,59 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         const selViewCache = Array.from(this.selectedArray);
         const dragViewCache = Array.from(this._dragArray);
         const eleViewCache = Array.from(this._eleArray);
-        const self = this;
         const resetSelection = action(() => {
-            const presDocView = DocumentManager.Instance.getDocumentView(self.rootDoc);
+            const presDocView = DocumentManager.Instance.getDocumentView(this.rootDoc);
             if (presDocView) SelectionManager.SelectView(presDocView, false);
-            self.rootDoc.presStatus = presStatus;
-            self.clearSelectedArray();
-            selViewCache.forEach(doc => self.addToSelectedArray(doc));
-            self._dragArray.splice(0, self._dragArray.length, ...dragViewCache);
-            self._eleArray.splice(0, self._eleArray.length, ...eleViewCache);
+            this.rootDoc.presStatus = presStatus;
+            this.clearSelectedArray();
+            selViewCache.forEach(doc => this.addToSelectedArray(doc));
+            this._eleArray.splice(0, this._eleArray.length, ...eleViewCache);
         });
-        const openInTab = (doc: Doc, finished?: () => void) => {
-            (collectionDocView ?? this).props.addDocTab(doc, OpenWhere.add);
+        const createDocView = (doc: Doc, finished?: () => void) => {
+            DocumentManager.Instance.AddViewRenderedCb(doc, () => finished?.());
+            (collectionDocView ?? this).props.addDocTab(doc, OpenWhere.lightbox);
             this.layoutDoc.presCollection = targetDoc;
-            // this still needs some fixing
-            setTimeout(resetSelection, 500);
-            if (doc !== targetDoc) {
-                setTimeout(finished ?? emptyFunction, 100); /// give it some time to create the targetDoc if we're opening up its context
-            } else {
-                finished?.();
-            }
         };
-        PresBox.NavigateToTarget(targetDoc, activeItem, openInTab, srcContext, includesDoc || tab ? undefined : resetSelection);
+        PresBox.NavigateToTarget(targetDoc, activeItem, createDocView, srcContext, includesDoc || tab ? undefined : resetSelection);
     };
 
-    static NavigateToTarget(targetDoc: Doc, activeItem: Doc, openInTab: any, srcContext: Doc, finished?: () => void) {
+    static NavigateToTarget(targetDoc: Doc, activeItem: Doc, createDocView: any, srcContext: Doc, finished?: () => void) {
+        if (activeItem.presMovement === PresMovement.None && targetDoc.type === DocumentType.SCRIPTING) {
+            (DocumentManager.Instance.getFirstDocumentView(targetDoc)?.ComponentView as ScriptingBox)?.onRun?.();
+            return;
+        }
+        const restoreLayout = () => {
+            // After navigating to the document, if it is added as a presPinView then it will
+            // adjust the pan and scale to that of the pinView when it was added.
+            const pinDocLayout = (BoolCast(activeItem.presPinLayout) || BoolCast(activeItem.presPinView)) && DocCast(targetDoc.context)?._currentFrame === undefined;
+            if (activeItem.presPinData || activeItem.presPinView || pinDocLayout) {
+                // targetDoc may or may not be displayed.  so get the first available document (or alias) view that matches targetDoc and use it
+                PresBox.restoreTargetDocView(DocumentManager.Instance.getFirstDocumentView(targetDoc), { pinDocLayout }, activeItem, NumCast(activeItem.presTransition, 500));
+            }
+        };
         // If openDocument is selected then it should open the document for the user
         if (activeItem.openDocument) {
             LightboxView.SetLightboxDoc(targetDoc); // openInTab(targetDoc);
-        } else if (targetDoc && activeItem.presMovement !== PresMovement.None) {
-            LightboxView.SetLightboxDoc(undefined);
-            const options: DocFocusOptions = {
-                willZoom: activeItem.presMovement !== PresMovement.Pan,
-                zoomScale: NumCast(activeItem.presZoom, 1),
-                zoomTime: activeItem.presMovement === PresMovement.Jump ? 0 : NumCast(activeItem.presTransition, 500),
-                noSelect: true,
-                originatingDoc: activeItem,
-            };
-            DocumentManager.Instance.jumpToDocument(targetDoc, options, openInTab, srcContext ? [srcContext] : [], finished);
-        } else if (activeItem.presMovement === PresMovement.None && targetDoc.type === DocumentType.SCRIPTING) {
-            (DocumentManager.Instance.getFirstDocumentView(targetDoc)?.ComponentView as ScriptingBox)?.onRun?.();
-        }
-        // After navigating to the document, if it is added as a presPinView then it will
-        // adjust the pan and scale to that of the pinView when it was added.
-        const pinDocLayout = (BoolCast(activeItem.presPinLayout) || BoolCast(activeItem.presPinView)) && DocCast(targetDoc.context)?._currentFrame === undefined;
-        if (activeItem.presPinData || activeItem.presPinView || pinDocLayout) {
-            PresBox._navTimer && clearTimeout(PresBox._navTimer);
-            // targetDoc may or may not be displayed.  this gets the first available document (or alias) view that matches targetDoc
-            const bestTargetView = DocumentManager.Instance.getFirstDocumentView(targetDoc);
-            if (bestTargetView?.props.Document) PresBox._navTimer = PresBox.restoreTargetDocView(bestTargetView?.props.Document, { pinDocLayout }, activeItem, NumCast(activeItem.presTransition, 500));
+            setTimeout(restoreLayout);
+        } else {
+            if (targetDoc && activeItem.presMovement !== PresMovement.None) {
+                LightboxView.SetLightboxDoc(undefined);
+                const options: DocFocusOptions = {
+                    willZoom: activeItem.presMovement !== PresMovement.Pan,
+                    zoomScale: NumCast(activeItem.presZoom, 1),
+                    zoomTime: activeItem.presMovement === PresMovement.Jump ? 0 : NumCast(activeItem.presTransition, 500),
+                    noSelect: true,
+                    originatingDoc: activeItem,
+                };
+
+                var containerDocContext = srcContext ? [srcContext] : [];
+                while (containerDocContext.length && !DocumentManager.Instance.getDocumentView(containerDocContext[0]) && containerDocContext[0].context) {
+                    containerDocContext = [Cast(containerDocContext[0].context, Doc, null), ...containerDocContext];
+                }
+
+                DocumentManager.Instance.jumpToDocument(targetDoc, options, createDocView, containerDocContext, finished);
+            }
+            restoreLayout();
         }
     }
 
@@ -1824,32 +1805,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         return undefined;
     };
 
-    // Case in which the document has keyframes to navigate to next key frame
-    @action
-    nextKeyframe = (tagDoc: Doc, curDoc: Doc): void => {
-        const childDocs = DocListCast(tagDoc[Doc.LayoutFieldKey(tagDoc)]);
-        const currentFrame = Cast(tagDoc._currentFrame, 'number', null);
-        if (currentFrame === undefined) {
-            tagDoc._currentFrame = 0;
-            // CollectionFreeFormDocumentView.setupScroll(tagDoc, 0);
-            // CollectionFreeFormDocumentView.setupKeyframes(childDocs, 0);
-        }
-        CollectionFreeFormDocumentView.updateKeyframe(childDocs, currentFrame || 0, tagDoc);
-        tagDoc._currentFrame = Math.max(0, (currentFrame || 0) + 1);
-        tagDoc.lastFrame = Math.max(NumCast(tagDoc._currentFrame), NumCast(tagDoc.lastFrame));
-    };
-
-    @action
-    prevKeyframe = (tagDoc: Doc, actItem: Doc): void => {
-        const childDocs = DocListCast(tagDoc[Doc.LayoutFieldKey(tagDoc)]);
-        const currentFrame = Cast(tagDoc._currentFrame, 'number', null);
-        if (currentFrame === undefined) {
-            tagDoc._currentFrame = 0;
-            // CollectionFreeFormDocumentView.setupKeyframes(childDocs, 0);
-        }
-        CollectionFreeFormDocumentView.gotoKeyframe(childDocs.slice());
-        tagDoc._currentFrame = Math.max(0, (currentFrame || 0) - 1);
-    };
+    _keyTimer: NodeJS.Timeout | undefined;
 
     /**
      * Returns the collection type as a string for headers
@@ -2022,76 +1978,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         );
     }
 
-    @action
-    getList = (list: any): List<number> => list;
-
-    @action
-    updateList = (list: any): List<number> => {
-        const targetDoc: Doc = this.targetDoc;
-        const x: List<number> = list;
-        x[x.length - 1] = NumCast(targetDoc._scrollY);
-        return x;
-    };
-
-    @action
-    newFrame = () => {
-        const activeItem: Doc = this.activeItem;
-        const type: string = StrCast(this.targetDoc.type);
-        if (!activeItem.frameList) activeItem.frameList = new List<number>();
-        switch (type) {
-            case DocumentType.PDF || DocumentType.RTF || DocumentType.WEB:
-                this.updateList(activeItem.frameList);
-                break;
-        }
-    };
-
-    @computed get frameListHeader() {
-        return (
-            <div className="frameList-header">
-                &nbsp; Frames {this.panable ? <i>Panable</i> : this.scrollable ? <i>Scrollable</i> : null}
-                <div className={'frameList-headerButtons'}>
-                    <Tooltip title={<div className="dash-tooltip">{'Add frame by example'}</div>}>
-                        <div
-                            className={'headerButton'}
-                            onClick={e => {
-                                e.stopPropagation();
-                                this.newFrame();
-                            }}>
-                            <FontAwesomeIcon icon={'plus'} onPointerDown={e => e.stopPropagation()} />
-                        </div>
-                    </Tooltip>
-                    <Tooltip title={<div className="dash-tooltip">{'Edit in collection'}</div>}>
-                        <div className={'headerButton'} onClick={e => e.stopPropagation()}>
-                            <FontAwesomeIcon icon={'edit'} onPointerDown={e => e.stopPropagation()} />
-                        </div>
-                    </Tooltip>
-                </div>
-            </div>
-        );
-    }
-
-    @computed get frameList() {
-        const frameList: List<number> = this.getList(this.activeItem.frameList);
-        return !frameList ? null : (
-            <div className="frameList-container">
-                {frameList.map(value => (
-                    <div className="framList-item" />
-                ))}
-            </div>
-        );
-    }
-
-    @computed get playButtonFrames() {
-        const targetDoc = this.targetDoc;
-        return !this.targetDoc ? null : (
-            <div className="presPanel-button-frame" style={{ display: targetDoc.lastFrame !== undefined && targetDoc.lastFrame >= 0 ? 'inline-flex' : 'none' }}>
-                <div>{NumCast(targetDoc._currentFrame)}</div>
-                <div className="presPanel-divider" style={{ border: 'solid 0.5px white', height: '60%' }}></div>
-                <div>{NumCast(targetDoc.lastFrame)}</div>
-            </div>
-        );
-    }
-
     @computed get playButtons() {
         const presEnd: boolean = !this.layoutDoc.presLoop && this.itemIndex === this.childDocs.length - 1;
         const presStart: boolean = !this.layoutDoc.presLoop && this.itemIndex === 0;
@@ -2143,7 +2029,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 </Tooltip>
                 <div className="presPanel-button-text" onClick={() => this.gotoDocument(0, this.activeItem)} style={{ display: this.props.PanelWidth() > 250 ? 'inline-flex' : 'none' }}>
                     Slide {this.itemIndex + 1} / {this.childDocs.length}
-                    {this.playButtonFrames}
                 </div>
                 <div className="presPanel-divider"></div>
                 {this.props.PanelWidth() > 250 ? (
@@ -2202,9 +2087,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         clearTimeout(this._presTimer);
     };
 
-    @action
-    startMarqueeCreateSlide = () => (PresBox.startMarquee = true);
-
     AddToMap = (treeViewDoc: Doc, index: number[]): Doc[] => {
         var indexNum = 0;
         for (let i = 0; i < index.length; i++) {
@@ -2271,7 +2153,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                     </Tooltip>
                     <div className="presPanel-button-text">
                         Slide {this.itemIndex + 1} / {this.childDocs.length}
-                        {this.playButtonFrames}
                     </div>
                     <div className="presPanel-divider" />
                     <div className="presPanel-button-text" onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, returnFalse, this.exitClicked, false, false)}>
-- 
cgit v1.2.3-70-g09d2


From ea4588185f165f66a4d596d51a8a8ad955985762 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 22 Feb 2023 15:58:20 -0500
Subject: fixed pres hide before/after, select w/o nav, and updating contents..
 made collections fitWidth by default so that they resize like a normal
 window. Made freeform pan/zoom animate when changing keyframes.  allow
 nesting of keyframe collections..  made presEffects work even if there's no
 transition movement. made nested collection timelines follow parent by
 default, but with ability to change ordering.  fixed treeView incremental
 render.  treat doc anchors are preselements instead of delegating. don't
 highlight ink when dragging ctrl pts.

---
 src/client/util/CurrentUserUtils.ts                |   4 +-
 src/client/views/InkControlPtHandles.tsx           | 268 +++++++++++----------
 src/client/views/InkTangentHandles.tsx             | 114 +++++----
 src/client/views/InkingStroke.tsx                  |  24 +-
 src/client/views/MainView.tsx                      |   2 +-
 src/client/views/PropertiesView.tsx                |  14 +-
 src/client/views/collections/CollectionMenu.tsx    |   9 +-
 .../views/collections/CollectionTreeView.tsx       |   2 +-
 src/client/views/collections/TabDocView.tsx        |   2 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  54 ++++-
 .../collections/collectionFreeForm/MarqueeView.tsx |   1 +
 .../views/nodes/CollectionFreeFormDocumentView.tsx |  28 +--
 src/client/views/nodes/DocumentView.tsx            |  16 +-
 src/client/views/nodes/button/FontIconBox.tsx      |   2 +-
 src/client/views/nodes/trails/PresBox.tsx          |  38 +--
 src/client/views/nodes/trails/PresElementBox.tsx   | 118 +++------
 src/fields/util.ts                                 |   1 +
 17 files changed, 345 insertions(+), 352 deletions(-)

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

diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 3d95cb947..67c730089 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -258,7 +258,7 @@ export class CurrentUserUtils {
               }[] = [
             {key: "Note",       creator: opts => Docs.Create.TextDocument("", opts),        opts: { _width: 200, _autoHeight: true }},
             {key: "Noteboard",  creator: opts => Docs.Create.NoteTakingDocument([], opts),  opts: { _width: 250, _height: 200 }},
-            {key: "Collection", creator: opts => Docs.Create.FreeformDocument([], opts),    opts: { _width: 150, _height: 100 }},
+            {key: "Collection", creator: opts => Docs.Create.FreeformDocument([], opts),    opts: { _width: 150, _height: 100, _fitWidth: true }},
             {key: "Equation",   creator: opts => Docs.Create.EquationDocument(opts),        opts: { _width: 300, _height: 35, _backgroundGridShow: true, }},
             {key: "Webpage",    creator: opts => Docs.Create.WebDocument("",opts),          opts: { _width: 400, _height: 512, _nativeWidth: 850, useCors: true, }},
             {key: "Comparison", creator: Docs.Create.ComparisonDocument,                    opts: { _width: 300, _height: 300 }},
@@ -669,7 +669,7 @@ export class CurrentUserUtils {
               title: "Perspective",                    toolTip: "View",                 btnType: ButtonType.DropdownList,                                                                                    ignoreClick: true, width: 100, scripts: { script: 'setView(value, _readOnly_)'}},
             { title: "Pin",     icon: "map-pin",       toolTip: "Pin View to Trail",    btnType: ButtonType.ClickButton,         funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "tab")'},                        width: 20,  scripts: { onClick: 'pinWithView(_readOnly_, altKey)'}},
             { title: "Back",    icon: "chevron-left",  toolTip: "Prev Animation Frame", btnType: ButtonType.ClickButton,         funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()'}, width: 20,  scripts: { onClick: 'prevKeyFrame(_readOnly_)'}},
-            { title: "Num",icon: "",toolTip: "Frame Number (click to toggle edit mode)",btnType: ButtonType.TextButton,          funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()', buttonText: 'selectedDocs()?.lastElement()?.currentFrame?.toString()'}, width: 20, scripts: { script: '{ return curKeyFrame(_readOnly_);}'}},
+            { title: "Num",icon: "",toolTip: "Frame Number (click to toggle edit mode)",btnType: ButtonType.TextButton,          funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()', buttonText: 'selectedDocs()?.lastElement()?.currentFrame?.toString()'}, width: 20, scripts: { onClick: '{ return curKeyFrame(_readOnly_);}'}},
             { title: "Fwd",     icon: "chevron-right", toolTip: "Next Animation Frame", btnType: ButtonType.ClickButton,         funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()'}, width: 20,  scripts: { onClick: 'nextKeyFrame(_readOnly_)'}},
             { title: "Fill",    icon: "fill-drip",     toolTip: "Background Fill Color",btnType: ButtonType.ColorButton,         funcs: {hidden: '!SelectionManager_selectedDocType()'},                     ignoreClick: true, width: 20,  scripts: { script:  'return setBackgroundColor(value, _readOnly_)'}}, // Only when a document is selected
             { title: "Header",  icon: "heading",       toolTip: "Header Color",         btnType: ButtonType.ColorButton,         funcs: {hidden: '!SelectionManager_selectedDocType()'},                     ignoreClick: true,             scripts: { script:  'return setHeaderColor(value, _readOnly_)'}},
diff --git a/src/client/views/InkControlPtHandles.tsx b/src/client/views/InkControlPtHandles.tsx
index d036a636a..9447b2e72 100644
--- a/src/client/views/InkControlPtHandles.tsx
+++ b/src/client/views/InkControlPtHandles.tsx
@@ -1,23 +1,23 @@
-import React = require("react");
-import { action, observable } from "mobx";
-import { observer } from "mobx-react";
-import { Doc } from "../../fields/Doc";
-import { ControlPoint, InkData, PointData, InkField } from "../../fields/InkField";
-import { List } from "../../fields/List";
-import { listSpec } from "../../fields/Schema";
-import { Cast, NumCast } from "../../fields/Types";
-import { setupMoveUpEvents, returnFalse } from "../../Utils";
-import { Transform } from "../util/Transform";
-import { UndoManager } from "../util/UndoManager";
-import { Colors } from "./global/globalEnums";
-import { InkingStroke } from "./InkingStroke";
-import { InkStrokeProperties } from "./InkStrokeProperties";
-import { DocumentView } from "./nodes/DocumentView";
-import { SelectionManager } from "../util/SelectionManager";
+import React = require('react');
+import { action, observable } from 'mobx';
+import { observer } from 'mobx-react';
+import { Doc } from '../../fields/Doc';
+import { ControlPoint, InkData, PointData, InkField } from '../../fields/InkField';
+import { List } from '../../fields/List';
+import { listSpec } from '../../fields/Schema';
+import { Cast, NumCast } from '../../fields/Types';
+import { setupMoveUpEvents, returnFalse } from '../../Utils';
+import { Transform } from '../util/Transform';
+import { UndoManager } from '../util/UndoManager';
+import { Colors } from './global/globalEnums';
+import { InkingStroke } from './InkingStroke';
+import { InkStrokeProperties } from './InkStrokeProperties';
+import { DocumentView } from './nodes/DocumentView';
+import { SelectionManager } from '../util/SelectionManager';
 
 export interface InkControlProps {
     inkDoc: Doc;
-    inkView: DocumentView;
+    inkView: InkingStroke;
     inkCtrlPoints: InkData;
     screenCtrlPoints: InkData;
     screenSpaceLineWidth: number;
@@ -26,16 +26,16 @@ export interface InkControlProps {
 
 @observer
 export class InkControlPtHandles extends React.Component<InkControlProps> {
-
     @observable private _overControl = -1;
-
-    @observable controlUndo: UndoManager.Batch | undefined;
+    get docView() {
+        return this.props.inkView.props.docViewPath().lastElement();
+    }
 
     componentDidMount() {
-        document.addEventListener("keydown", this.onDelete, true);
+        document.addEventListener('keydown', this.onDelete, true);
     }
     componentWillUnmount() {
-        document.removeEventListener("keydown", this.onDelete, true);
+        document.removeEventListener('keydown', this.onDelete, true);
     }
     /**
      * Handles the movement of a selected control point when the user clicks and drags.
@@ -43,30 +43,32 @@ export class InkControlPtHandles extends React.Component<InkControlProps> {
      */
     @action
     onControlDown = (e: React.PointerEvent, controlIndex: number): void => {
-        const ptFromScreen = this.props.inkView.ComponentView?.ptFromScreen;
+        const ptFromScreen = this.props.inkView.ptFromScreen;
         if (ptFromScreen) {
             const order = controlIndex % 4;
             const handleIndexA = ((order === 3 ? controlIndex - 1 : controlIndex - 2) + this.props.inkCtrlPoints.length) % this.props.inkCtrlPoints.length;
             const handleIndexB = (order === 3 ? controlIndex + 2 : controlIndex + 1) % this.props.inkCtrlPoints.length;
-            const brokenIndices = Cast(this.props.inkDoc.brokenInkIndices, listSpec("number"));
+            const brokenIndices = Cast(this.props.inkDoc.brokenInkIndices, listSpec('number'));
             const wasSelected = InkStrokeProperties.Instance._currentPoint === controlIndex;
             if (!wasSelected) InkStrokeProperties.Instance._currentPoint = -1;
             const origInk = this.props.inkCtrlPoints.slice();
-            setupMoveUpEvents(this, e,
+            setupMoveUpEvents(
+                this,
+                e,
                 action((e: PointerEvent, down: number[], delta: number[]) => {
-                    if (!this.controlUndo) this.controlUndo = UndoManager.StartBatch("drag ink ctrl pt");
+                    if (!this.props.inkView.controlUndo) this.props.inkView.controlUndo = UndoManager.StartBatch('drag ink ctrl pt');
                     const inkMoveEnd = ptFromScreen({ X: delta[0], Y: delta[1] });
                     const inkMoveStart = ptFromScreen({ X: 0, Y: 0 });
-                    InkStrokeProperties.Instance.moveControlPtHandle(this.props.inkView, inkMoveEnd.X - inkMoveStart.X, inkMoveEnd.Y - inkMoveStart.Y, controlIndex, origInk);
+                    InkStrokeProperties.Instance.moveControlPtHandle(this.docView, inkMoveEnd.X - inkMoveStart.X, inkMoveEnd.Y - inkMoveStart.Y, controlIndex, origInk);
                     return false;
                 }),
                 action(() => {
-                    if (this.controlUndo) {
-                        InkStrokeProperties.Instance.snapControl(this.props.inkView, controlIndex);
+                    if (this.props.inkView.controlUndo) {
+                        InkStrokeProperties.Instance.snapControl(this.docView, controlIndex);
                     }
-                    this.controlUndo?.end();
-                    this.controlUndo = undefined;
-                    UndoManager.FilterBatches(["data", "x", "y", "width", "height"]);
+                    this.props.inkView.controlUndo?.end();
+                    this.props.inkView.controlUndo = undefined;
+                    UndoManager.FilterBatches(['data', 'x', 'y', 'width', 'height']);
                 }),
                 action((e: PointerEvent, doubleTap: boolean | undefined) => {
                     const equivIndex = controlIndex === 0 ? this.props.inkCtrlPoints.length - 1 : controlIndex === this.props.inkCtrlPoints.length - 1 ? 0 : controlIndex;
@@ -76,44 +78,52 @@ export class InkControlPtHandles extends React.Component<InkControlProps> {
                             else this.props.inkDoc.brokenInkIndices = new List<number>([controlIndex]);
                         } else {
                             if (brokenIndices?.includes(equivIndex)) {
-                                if (!this.controlUndo) this.controlUndo = UndoManager.StartBatch("make smooth");
-                                InkStrokeProperties.Instance.snapHandleTangent(this.props.inkView, equivIndex, handleIndexA, handleIndexB);
+                                if (!this.props.inkView.controlUndo) this.props.inkView.controlUndo = UndoManager.StartBatch('make smooth');
+                                InkStrokeProperties.Instance.snapHandleTangent(this.docView, equivIndex, handleIndexA, handleIndexB);
                             }
                             if (equivIndex !== controlIndex && brokenIndices?.includes(controlIndex)) {
-                                if (!this.controlUndo) this.controlUndo = UndoManager.StartBatch("make smooth");
-                                InkStrokeProperties.Instance.snapHandleTangent(this.props.inkView, controlIndex, handleIndexA, handleIndexB);
+                                if (!this.props.inkView.controlUndo) this.props.inkView.controlUndo = UndoManager.StartBatch('make smooth');
+                                InkStrokeProperties.Instance.snapHandleTangent(this.docView, controlIndex, handleIndexA, handleIndexB);
                             }
                         }
-                        this.controlUndo?.end();
-                        this.controlUndo = undefined;
+                        this.props.inkView.controlUndo?.end();
+                        this.props.inkView.controlUndo = undefined;
                     }
                     this.changeCurrPoint(controlIndex);
-                }), undefined, undefined, () => wasSelected && this.changeCurrPoint(-1));
+                }),
+                undefined,
+                undefined,
+                () => wasSelected && this.changeCurrPoint(-1)
+            );
         }
-    }
+    };
     /**
      * Updates whether a user has hovered over a particular control point or point that could be added
      * on click.
      */
-    @action onEnterControl = (i: number) => { this._overControl = i; };
-    @action onLeaveControl = () => { this._overControl = -1; };
+    @action onEnterControl = (i: number) => {
+        this._overControl = i;
+    };
+    @action onLeaveControl = () => {
+        this._overControl = -1;
+    };
 
     /**
      * Deletes the currently selected point.
      */
     @action
     onDelete = (e: KeyboardEvent) => {
-        if (["-", "Backspace", "Delete"].includes(e.key)) {
-            InkStrokeProperties.Instance.deletePoints(this.props.inkView, e.shiftKey);
+        if (['-', 'Backspace', 'Delete'].includes(e.key)) {
+            InkStrokeProperties.Instance.deletePoints(this.docView, e.shiftKey);
             e.stopPropagation();
         }
-    }
+    };
 
     /**
      * Changes the current selected control point.
      */
     @action
-    changeCurrPoint = (i: number) => InkStrokeProperties.Instance._currentPoint = i
+    changeCurrPoint = (i: number) => (InkStrokeProperties.Instance._currentPoint = i);
 
     render() {
         // Accessing the current ink's data and extracting all control points.
@@ -133,101 +143,115 @@ export class InkControlPtHandles extends React.Component<InkControlProps> {
 
         const closed = InkingStroke.IsClosed(inkData);
         const nearestScreenPt = this.props.nearestScreenPt();
-        const TagType = (broken?: boolean) => broken ? "rect" : "circle";
-        const hdl = (control: { X: number, Y: number, I: number }, scale: number, color: string) => {
-            const broken = Cast(this.props.inkDoc.brokenInkIndices, listSpec("number"))?.includes(control.I);
+        const TagType = (broken?: boolean) => (broken ? 'rect' : 'circle');
+        const hdl = (control: { X: number; Y: number; I: number }, scale: number, color: string) => {
+            const broken = Cast(this.props.inkDoc.brokenInkIndices, listSpec('number'))?.includes(control.I);
             const Tag = TagType((control.I === 0 || control.I === inkData.length - 1) && !closed) as keyof JSX.IntrinsicElements;
-            return <Tag key={control.I.toString() + scale}
-                x={control.X - this.props.screenSpaceLineWidth * 2 * scale}
-                y={control.Y - this.props.screenSpaceLineWidth * 2 * scale}
-                cx={control.X}
-                cy={control.Y}
-                r={this.props.screenSpaceLineWidth * 2 * scale}
-                opacity={this.controlUndo ? 0.15 : 1}
-                height={this.props.screenSpaceLineWidth * 4 * scale}
-                width={this.props.screenSpaceLineWidth * 4 * scale}
-                strokeWidth={this.props.screenSpaceLineWidth / 2}
-                stroke={Colors.MEDIUM_BLUE}
-                fill={broken ? Colors.MEDIUM_BLUE : color}
-                onPointerDown={(e: React.PointerEvent) => this.onControlDown(e, control.I)}
-                onMouseEnter={() => this.onEnterControl(control.I)}
-                onMouseLeave={this.onLeaveControl}
-                pointerEvents="all"
-                cursor="default"
-            />;
-        };
-        return (<svg>
-            {!nearestScreenPt ? (null) :
-                <circle key={"npt"}
-                    cx={nearestScreenPt.X}
-                    cy={nearestScreenPt.Y}
-                    r={this.props.screenSpaceLineWidth * 2}
-                    fill={"#00007777"}
-                    stroke={"#00007777"}
-                    strokeWidth={0}
-                    pointerEvents="none"
+            return (
+                <Tag
+                    key={control.I.toString() + scale}
+                    x={control.X - this.props.screenSpaceLineWidth * 2 * scale}
+                    y={control.Y - this.props.screenSpaceLineWidth * 2 * scale}
+                    cx={control.X}
+                    cy={control.Y}
+                    r={this.props.screenSpaceLineWidth * 2 * scale}
+                    opacity={this.props.inkView.controlUndo ? 0.15 : 1}
+                    height={this.props.screenSpaceLineWidth * 4 * scale}
+                    width={this.props.screenSpaceLineWidth * 4 * scale}
+                    strokeWidth={this.props.screenSpaceLineWidth / 2}
+                    stroke={Colors.MEDIUM_BLUE}
+                    fill={broken ? Colors.MEDIUM_BLUE : color}
+                    onPointerDown={(e: React.PointerEvent) => this.onControlDown(e, control.I)}
+                    onMouseEnter={() => this.onEnterControl(control.I)}
+                    onMouseLeave={this.onLeaveControl}
+                    pointerEvents="all"
+                    cursor="default"
                 />
-            }
-            {sreenCtrlPoints.map(control => hdl(control, this._overControl !== control.I ? 1 : 3 / 2, Colors.WHITE))}
-        </svg>
+            );
+        };
+        return (
+            <svg>
+                {!nearestScreenPt ? null : <circle key={'npt'} cx={nearestScreenPt.X} cy={nearestScreenPt.Y} r={this.props.screenSpaceLineWidth * 2} fill={'#00007777'} stroke={'#00007777'} strokeWidth={0} pointerEvents="none" />}
+                {sreenCtrlPoints.map(control => hdl(control, this._overControl !== control.I ? 1 : 3 / 2, Colors.WHITE))}
+            </svg>
         );
     }
 }
 
-
 export interface InkEndProps {
     inkDoc: Doc;
-    inkView: DocumentView;
+    inkView: InkingStroke;
     screenSpaceLineWidth: number;
     startPt: PointData;
     endPt: PointData;
 }
 @observer
 export class InkEndPtHandles extends React.Component<InkEndProps> {
-    @observable controlUndo: UndoManager.Batch | undefined;
     @observable _overStart: boolean = false;
     @observable _overEnd: boolean = false;
 
     @action
-    dragRotate = (e: React.PointerEvent, p1: () => { X: number, Y: number }, p2: () => { X: number, Y: number }) => {
-        setupMoveUpEvents(this, e, (e) => {
-            if (!this.controlUndo) this.controlUndo = UndoManager.StartBatch("stretch ink");
-            // compute stretch factor by finding scaling along axis between start and end points
-            const v1 = { X: p1().X - p2().X, Y: p1().Y - p2().Y };
-            const v2 = { X: e.clientX - p2().X, Y: e.clientY - p2().Y };
-            const v1len = Math.sqrt(v1.X * v1.X + v1.Y * v1.Y);
-            const v2len = Math.sqrt(v2.X * v2.X + v2.Y * v2.Y);
-            const scaling = v2len / v1len;
-            const v1n = { X: v1.X / v1len, Y: v1.Y / v1len };
-            const v2n = { X: v2.X / v2len, Y: v2.Y / v2len };
-            const angle = Math.acos(v1n.X * v2n.X + v1n.Y * v2n.Y) * Math.sign(v1.X * v2.Y - v2.X * v1.Y);
-            InkStrokeProperties.Instance.stretchInk(SelectionManager.Views(), scaling, p2(), v1n, e.shiftKey);
-            InkStrokeProperties.Instance.rotateInk(SelectionManager.Views(), angle, p2());
-            return false;
-        }, action(() => {
-            this.controlUndo?.end();
-            this.controlUndo = undefined;
-            UndoManager.FilterBatches(["data", "x", "y", "width", "height"]);
-        }), returnFalse);
-    }
+    dragRotate = (e: React.PointerEvent, p1: () => { X: number; Y: number }, p2: () => { X: number; Y: number }) => {
+        setupMoveUpEvents(
+            this,
+            e,
+            action(e => {
+                if (!this.props.inkView.controlUndo) this.props.inkView.controlUndo = UndoManager.StartBatch('stretch ink');
+                // compute stretch factor by finding scaling along axis between start and end points
+                const v1 = { X: p1().X - p2().X, Y: p1().Y - p2().Y };
+                const v2 = { X: e.clientX - p2().X, Y: e.clientY - p2().Y };
+                const v1len = Math.sqrt(v1.X * v1.X + v1.Y * v1.Y);
+                const v2len = Math.sqrt(v2.X * v2.X + v2.Y * v2.Y);
+                const scaling = v2len / v1len;
+                const v1n = { X: v1.X / v1len, Y: v1.Y / v1len };
+                const v2n = { X: v2.X / v2len, Y: v2.Y / v2len };
+                const angle = Math.acos(v1n.X * v2n.X + v1n.Y * v2n.Y) * Math.sign(v1.X * v2.Y - v2.X * v1.Y);
+                InkStrokeProperties.Instance.stretchInk(SelectionManager.Views(), scaling, p2(), v1n, e.shiftKey);
+                InkStrokeProperties.Instance.rotateInk(SelectionManager.Views(), angle, p2());
+                return false;
+            }),
+            action(() => {
+                this.props.inkView.controlUndo?.end();
+                this.props.inkView.controlUndo = undefined;
+                UndoManager.FilterBatches(['data', 'x', 'y', 'width', 'height']);
+            }),
+            returnFalse
+        );
+    };
 
     render() {
-        const hdl = (key: string, pt: PointData, dragFunc: (e: React.PointerEvent) => void) => <circle key={key}
-            cx={pt.X}
-            cy={pt.Y}
-            r={this.props.screenSpaceLineWidth * 2}
-            fill={this._overStart ? "#aaaaaa" : "#99999977"}
-            stroke={"#00007777"}
-            strokeWidth={0}
-            onPointerLeave={action(() => this._overStart = false)}
-            onPointerEnter={action(() => this._overStart = true)}
-            onPointerDown={dragFunc}
-            pointerEvents="all"
-        />;
-        return (<svg>
-            {hdl("start", this.props.startPt, (e: React.PointerEvent) => this.dragRotate(e, () => this.props.startPt, () => this.props.endPt))}
-            {hdl("end", this.props.endPt, (e: React.PointerEvent) => this.dragRotate(e, () => this.props.endPt, () => this.props.startPt))}
-        </svg>
+        const hdl = (key: string, pt: PointData, dragFunc: (e: React.PointerEvent) => void) => (
+            <circle
+                key={key}
+                cx={pt.X}
+                cy={pt.Y}
+                r={this.props.screenSpaceLineWidth * 2}
+                fill={this._overStart ? '#aaaaaa' : '#99999977'}
+                stroke={'#00007777'}
+                strokeWidth={0}
+                onPointerLeave={action(() => (this._overStart = false))}
+                onPointerEnter={action(() => (this._overStart = true))}
+                onPointerDown={dragFunc}
+                pointerEvents="all"
+            />
+        );
+        return (
+            <svg>
+                {hdl('start', this.props.startPt, (e: React.PointerEvent) =>
+                    this.dragRotate(
+                        e,
+                        () => this.props.startPt,
+                        () => this.props.endPt
+                    )
+                )}
+                {hdl('end', this.props.endPt, (e: React.PointerEvent) =>
+                    this.dragRotate(
+                        e,
+                        () => this.props.endPt,
+                        () => this.props.startPt
+                    )
+                )}
+            </svg>
         );
     }
-}
\ No newline at end of file
+}
diff --git a/src/client/views/InkTangentHandles.tsx b/src/client/views/InkTangentHandles.tsx
index ae35bc980..c4a2f603e 100644
--- a/src/client/views/InkTangentHandles.tsx
+++ b/src/client/views/InkTangentHandles.tsx
@@ -1,21 +1,21 @@
-import React = require("react");
-import { action } from "mobx";
-import { observer } from "mobx-react";
-import { Doc } from "../../fields/Doc";
-import { HandleLine, HandlePoint, InkData } from "../../fields/InkField";
-import { List } from "../../fields/List";
-import { listSpec } from "../../fields/Schema";
-import { Cast } from "../../fields/Types";
-import { emptyFunction, setupMoveUpEvents } from "../../Utils";
-import { Transform } from "../util/Transform";
-import { UndoManager } from "../util/UndoManager";
-import { Colors } from "./global/globalEnums";
-import { InkingStroke } from "./InkingStroke";
-import { InkStrokeProperties } from "./InkStrokeProperties";
-import { DocumentView } from "./nodes/DocumentView";
+import React = require('react');
+import { action } from 'mobx';
+import { observer } from 'mobx-react';
+import { Doc } from '../../fields/Doc';
+import { HandleLine, HandlePoint, InkData } from '../../fields/InkField';
+import { List } from '../../fields/List';
+import { listSpec } from '../../fields/Schema';
+import { Cast } from '../../fields/Types';
+import { emptyFunction, setupMoveUpEvents } from '../../Utils';
+import { Transform } from '../util/Transform';
+import { UndoManager } from '../util/UndoManager';
+import { Colors } from './global/globalEnums';
+import { InkingStroke } from './InkingStroke';
+import { InkStrokeProperties } from './InkStrokeProperties';
+import { DocumentView } from './nodes/DocumentView';
 export interface InkHandlesProps {
     inkDoc: Doc;
-    inkView: DocumentView;
+    inkView: InkingStroke;
     screenCtrlPoints: InkData;
     screenSpaceLineWidth: number;
     ScreenToLocalTransform: () => Transform;
@@ -23,49 +23,51 @@ export interface InkHandlesProps {
 
 @observer
 export class InkTangentHandles extends React.Component<InkHandlesProps> {
+    get docView() {
+        return this.props.inkView.props.docViewPath().lastElement();
+    }
     /**
      * Handles the movement of a selected handle point when the user clicks and drags.
      * @param handleNum The index of the currently selected handle point.
      */
     onHandleDown = (e: React.PointerEvent, handleIndex: number): void => {
-        const ptFromScreen = this.props.inkView.ComponentView?.ptFromScreen;
-        if (!ptFromScreen) return;
-        const screenScale = this.props.ScreenToLocalTransform().Scale;
-        var controlUndo: UndoManager.Batch | undefined;
         const order = handleIndex % 4;
         const oppositeHandleRawIndex = order === 1 ? handleIndex - 3 : handleIndex + 3;
         const oppositeHandleIndex = (oppositeHandleRawIndex < 0 ? this.props.screenCtrlPoints.length + oppositeHandleRawIndex : oppositeHandleRawIndex) % this.props.screenCtrlPoints.length;
         const controlIndex = (order === 1 ? handleIndex - 1 : handleIndex + 2) % this.props.screenCtrlPoints.length;
-        setupMoveUpEvents(this, e,
+        setupMoveUpEvents(
+            this,
+            e,
             (e: PointerEvent, down: number[], delta: number[]) => {
-                if (!controlUndo) controlUndo = UndoManager.StartBatch("DocDecs move tangent");
+                if (!this.props.inkView.controlUndo) this.props.inkView.controlUndo = UndoManager.StartBatch('DocDecs move tangent');
                 if (e.altKey) this.onBreakTangent(controlIndex);
-                const inkMoveEnd = ptFromScreen({ X: delta[0], Y: delta[1] });
-                const inkMoveStart = ptFromScreen({ X: 0, Y: 0 });
-                InkStrokeProperties.Instance.moveTangentHandle(this.props.inkView, -(inkMoveEnd.X - inkMoveStart.X), -(inkMoveEnd.Y - inkMoveStart.Y), handleIndex, oppositeHandleIndex, controlIndex);
+                const inkMoveEnd = this.props.inkView.ptFromScreen({ X: delta[0], Y: delta[1] });
+                const inkMoveStart = this.props.inkView.ptFromScreen({ X: 0, Y: 0 });
+                InkStrokeProperties.Instance.moveTangentHandle(this.docView, -(inkMoveEnd.X - inkMoveStart.X), -(inkMoveEnd.Y - inkMoveStart.Y), handleIndex, oppositeHandleIndex, controlIndex);
                 return false;
-            }, () => {
-                controlUndo?.end();
-                UndoManager.FilterBatches(["data", "x", "y", "width", "height"]);
-            }, emptyFunction
+            },
+            () => {
+                this.props.inkView.controlUndo?.end();
+                UndoManager.FilterBatches(['data', 'x', 'y', 'width', 'height']);
+            },
+            emptyFunction
         );
-    }
+    };
 
     /**
-     * Breaks tangent handle movement when ‘Alt’ key is held down. Adds the current handle index and 
+     * Breaks tangent handle movement when ‘Alt’ key is held down. Adds the current handle index and
      * its matching (opposite) handle to a list of broken handle indices.
      * @param handleNum The index of the currently selected handle point.
      */
     @action
     onBreakTangent = (controlIndex: number) => {
         const closed = InkingStroke.IsClosed(this.props.screenCtrlPoints);
-        const brokenIndices = Cast(this.props.inkDoc.brokenInkIndices, listSpec("number"));
-        if (!brokenIndices?.includes(controlIndex) &&
-            ((controlIndex > 0 && controlIndex < this.props.screenCtrlPoints.length - 1) || closed)) {
+        const brokenIndices = Cast(this.props.inkDoc.brokenInkIndices, listSpec('number'));
+        if (!brokenIndices?.includes(controlIndex) && ((controlIndex > 0 && controlIndex < this.props.screenCtrlPoints.length - 1) || closed)) {
             if (brokenIndices) brokenIndices.push(controlIndex);
             else this.props.inkDoc.brokenInkIndices = new List<number>([controlIndex]);
         }
-    }
+    };
 
     render() {
         // Accessing the current ink's data and extracting all handle points and handle lines.
@@ -81,10 +83,18 @@ export class InkTangentHandles extends React.Component<InkHandlesProps> {
             // Adding first and last (single) handle lines.
             if (closed) {
                 tangentLines.push({ X1: data[data.length - 2].X, Y1: data[data.length - 2].Y, X2: data[0].X, Y2: data[0].Y, X3: data[1].X, Y3: data[1].Y, dot1: 0, dot2: data.length - 1 });
-            }
-            else {
+            } else {
                 tangentLines.push({ X1: data[0].X, Y1: data[0].Y, X2: data[0].X, Y2: data[0].Y, X3: data[1].X, Y3: data[1].Y, dot1: 0, dot2: 0 });
-                tangentLines.push({ X1: data[data.length - 2].X, Y1: data[data.length - 2].Y, X2: data[data.length - 1].X, Y2: data[data.length - 1].Y, X3: data[data.length - 1].X, Y3: data[data.length - 1].Y, dot1: data.length - 1, dot2: data.length - 1 });
+                tangentLines.push({
+                    X1: data[data.length - 2].X,
+                    Y1: data[data.length - 2].Y,
+                    X2: data[data.length - 1].X,
+                    Y2: data[data.length - 1].Y,
+                    X3: data[data.length - 1].X,
+                    Y3: data[data.length - 1].Y,
+                    dot1: data.length - 1,
+                    dot2: data.length - 1,
+                });
             }
             for (let i = 2; i < data.length - 4; i += 4) {
                 tangentLines.push({ X1: data[i].X, Y1: data[i].Y, X2: data[i + 1].X, Y2: data[i + 1].Y, X3: data[i + 3].X, Y3: data[i + 3].Y, dot1: i + 1, dot2: i + 2 });
@@ -94,7 +104,7 @@ export class InkTangentHandles extends React.Component<InkHandlesProps> {
 
         return (
             <>
-                {tangentHandles.map((pts, i) =>
+                {tangentHandles.map((pts, i) => (
                     <svg height="10" width="10" key={`hdl${i}`}>
                         <circle
                             cx={pts.X}
@@ -106,25 +116,31 @@ export class InkTangentHandles extends React.Component<InkHandlesProps> {
                             onPointerDown={e => this.onHandleDown(e, pts.I)}
                             pointerEvents="all"
                             cursor="default"
-                            display={(pts.dot1 === InkStrokeProperties.Instance._currentPoint || pts.dot2 === InkStrokeProperties.Instance._currentPoint) ? "inherit" : "none"} />
-                    </svg>)}
+                            display={pts.dot1 === InkStrokeProperties.Instance._currentPoint || pts.dot2 === InkStrokeProperties.Instance._currentPoint ? 'inherit' : 'none'}
+                        />
+                    </svg>
+                ))}
                 {tangentLines.map((pts, i) => {
-                    const tangentLine = (x1: number, y1: number, x2: number, y2: number) =>
+                    const tangentLine = (x1: number, y1: number, x2: number, y2: number) => (
                         <line
                             x1={x1}
                             y1={y1}
                             x2={x2}
                             y2={y2}
                             stroke={Colors.MEDIUM_BLUE}
-                            strokeDasharray={"1 1"}
+                            strokeDasharray={'1 1'}
                             strokeWidth={1}
-                            display={(pts.dot1 === InkStrokeProperties.Instance._currentPoint || pts.dot2 === InkStrokeProperties.Instance._currentPoint) ? "inherit" : "none"} />;
-                    return <svg height="100" width="100" key={`line${i}`}>
-                        {tangentLine(pts.X1, pts.Y1, pts.X2, pts.Y2)}
-                        {tangentLine(pts.X2, pts.Y2, pts.X3, pts.Y3)}
-                    </svg>;
+                            display={pts.dot1 === InkStrokeProperties.Instance._currentPoint || pts.dot2 === InkStrokeProperties.Instance._currentPoint ? 'inherit' : 'none'}
+                        />
+                    );
+                    return (
+                        <svg height="100" width="100" key={`line${i}`}>
+                            {tangentLine(pts.X1, pts.Y1, pts.X2, pts.Y2)}
+                            {tangentLine(pts.X2, pts.Y2, pts.X3, pts.Y3)}
+                        </svg>
+                    );
                 })}
             </>
         );
     }
-}
\ No newline at end of file
+}
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index 8291ff3f1..76c1aa80a 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -124,6 +124,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() {
     public static toggleMask = action((inkDoc: Doc) => {
         inkDoc.isInkMask = !inkDoc.isInkMask;
     });
+    @observable controlUndo: UndoManager.Batch | undefined;
     /**
      * Drags the a simple bezier segment of the stroke.
      * Also adds a control point when double clicking on the stroke.
@@ -143,15 +144,15 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() {
         const { nearestSeg } = InkStrokeProperties.nearestPtToStroke(screenPts, { X: e.clientX, Y: e.clientY });
         const controlIndex = nearestSeg;
         const wasSelected = InkStrokeProperties.Instance._currentPoint === controlIndex;
-        var controlUndo: UndoManager.Batch | undefined;
         const isEditing = InkStrokeProperties.Instance._controlButton && this.props.isSelected();
+        this.controlUndo = undefined;
         setupMoveUpEvents(
             this,
             e,
             !isEditing
                 ? returnFalse
                 : action((e: PointerEvent, down: number[], delta: number[]) => {
-                      if (!controlUndo) controlUndo = UndoManager.StartBatch('drag ink ctrl pt');
+                      if (!this.controlUndo) this.controlUndo = UndoManager.StartBatch('drag ink ctrl pt');
                       const inkMoveEnd = this.ptFromScreen({ X: delta[0], Y: delta[1] });
                       const inkMoveStart = this.ptFromScreen({ X: 0, Y: 0 });
                       InkStrokeProperties.Instance.moveControlPtHandle(inkView, inkMoveEnd.X - inkMoveStart.X, inkMoveEnd.Y - inkMoveStart.Y, controlIndex);
@@ -161,8 +162,8 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() {
             !isEditing
                 ? returnFalse
                 : action(() => {
-                      controlUndo?.end();
-                      controlUndo = undefined;
+                      this.controlUndo?.end();
+                      this.controlUndo = undefined;
                       UndoManager.FilterBatches(['data', 'x', 'y', 'width', 'height']);
                   }),
             action((e: PointerEvent, doubleTap: boolean | undefined) => {
@@ -304,7 +305,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() {
         return SnappingManager.GetIsDragging() ? null : !InkStrokeProperties.Instance._controlButton ? (
             !this.props.isSelected() || InkingStroke.IsClosed(inkData) ? null : (
                 <div className="inkstroke-UI" style={{ clip: `rect(${boundsTop}px, 10000px, 10000px, ${boundsLeft}px)` }}>
-                    <InkEndPtHandles inkView={this.props.docViewPath().lastElement()} inkDoc={inkDoc} startPt={screenPts[0]} endPt={screenPts.lastElement()} screenSpaceLineWidth={screenSpaceCenterlineStrokeWidth} />
+                    <InkEndPtHandles inkView={this} inkDoc={inkDoc} startPt={screenPts[0]} endPt={screenPts.lastElement()} screenSpaceLineWidth={screenSpaceCenterlineStrokeWidth} />
                 </div>
             )
         ) : (
@@ -331,15 +332,8 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() {
                     1.0,
                     false
                 )}
-                <InkControlPtHandles
-                    inkView={this.props.docViewPath().lastElement()}
-                    inkDoc={inkDoc}
-                    inkCtrlPoints={inkData}
-                    screenCtrlPoints={screenHdlPts}
-                    nearestScreenPt={this.nearestScreenPt}
-                    screenSpaceLineWidth={screenSpaceCenterlineStrokeWidth}
-                />
-                <InkTangentHandles inkView={this.props.docViewPath().lastElement()} inkDoc={inkDoc} screenCtrlPoints={screenHdlPts} screenSpaceLineWidth={screenSpaceCenterlineStrokeWidth} ScreenToLocalTransform={this.screenToLocal} />
+                <InkControlPtHandles inkView={this} inkDoc={inkDoc} inkCtrlPoints={inkData} screenCtrlPoints={screenHdlPts} nearestScreenPt={this.nearestScreenPt} screenSpaceLineWidth={screenSpaceCenterlineStrokeWidth} />
+                <InkTangentHandles inkView={this} inkDoc={inkDoc} screenCtrlPoints={screenHdlPts} screenSpaceLineWidth={screenSpaceCenterlineStrokeWidth} ScreenToLocalTransform={this.screenToLocal} />
             </div>
         );
     };
@@ -389,7 +383,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() {
             1.0,
             false
         );
-        const highlight = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Highlighting);
+        const highlight = !this.controlUndo && this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Highlighting);
         const highlightIndex = highlight?.highlightIndex;
         const highlightColor = highlight?.highlightIndex ? highlight?.highlightColor : StrCast(this.layoutDoc.strokeOutlineColor, !closed && fillColor && fillColor !== 'transparent' ? StrCast(this.layoutDoc.color, 'transparent') : 'transparent');
         // Invisible polygonal line that enables the ink to be selected by the user.
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 895ed9bda..625dc2748 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -606,7 +606,7 @@ export class MainView extends React.Component {
     @computed get mainDocView() {
         return (
             <>
-                {this._hideUI ? null : this.headerBarDocView}
+                {this._hideUI || !this.headerBarDocHeight?.() ? null : this.headerBarDocView}
                 <DocumentView
                     key="main"
                     Document={this.mainContainer!}
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index a2bc37095..203b42cbc 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -1814,12 +1814,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
                         </div>
                         {!selectedItem ? null : (
                             <div className="propertiesView-presTrails">
-                                <div
-                                    className="propertiesView-presTrails-title"
-                                    onPointerDown={action(() => {
-                                        this.openPresTransitions = !this.openPresTransitions;
-                                    })}
-                                    style={{ backgroundColor: this.openPresTransitions ? 'black' : '' }}>
+                                <div className="propertiesView-presTrails-title" onPointerDown={action(() => (this.openPresTransitions = !this.openPresTransitions))} style={{ backgroundColor: this.openPresTransitions ? 'black' : '' }}>
                                     &nbsp; <FontAwesomeIcon style={{ alignSelf: 'center' }} icon={'rocket'} /> &nbsp; Transitions
                                     <div className="propertiesView-presTrails-title-icon">
                                         <FontAwesomeIcon icon={this.openPresTransitions ? 'caret-down' : 'caret-right'} size="lg" color="white" />
@@ -1830,12 +1825,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
                         )}
                         {!selectedItem || (type !== DocumentType.VID && type !== DocumentType.AUDIO) ? null : (
                             <div className="propertiesView-presTrails">
-                                <div
-                                    className="propertiesView-presTrails-title"
-                                    onPointerDown={action(() => {
-                                        this.openSlideOptions = !this.openSlideOptions;
-                                    })}
-                                    style={{ backgroundColor: this.openSlideOptions ? 'black' : '' }}>
+                                <div className="propertiesView-presTrails-title" onPointerDown={action(() => (this.openSlideOptions = !this.openSlideOptions))} style={{ backgroundColor: this.openSlideOptions ? 'black' : '' }}>
                                     &nbsp; <FontAwesomeIcon style={{ alignSelf: 'center' }} icon={type === DocumentType.AUDIO ? 'file-audio' : 'file-video'} /> &nbsp; {type === DocumentType.AUDIO ? 'Audio Options' : 'Video Options'}
                                     <div className="propertiesView-presTrails-title-icon">
                                         <FontAwesomeIcon icon={this.openSlideOptions ? 'caret-down' : 'caret-right'} size="lg" color="white" />
diff --git a/src/client/views/collections/CollectionMenu.tsx b/src/client/views/collections/CollectionMenu.tsx
index e2594b6ae..c83f4e689 100644
--- a/src/client/views/collections/CollectionMenu.tsx
+++ b/src/client/views/collections/CollectionMenu.tsx
@@ -15,6 +15,7 @@ import { RichTextField } from '../../../fields/RichTextField';
 import { listSpec } from '../../../fields/Schema';
 import { ScriptField } from '../../../fields/ScriptField';
 import { BoolCast, Cast, NumCast, StrCast } from '../../../fields/Types';
+import { GestureUtils } from '../../../pen-gestures/GestureUtils';
 import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, setupMoveUpEvents, Utils } from '../../../Utils';
 import { Docs } from '../../documents/Documents';
 import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes';
@@ -40,7 +41,7 @@ import { CollectionLinearView } from './collectionLinear';
 import './CollectionMenu.scss';
 import { COLLECTION_BORDER_WIDTH } from './CollectionView';
 import { TabDocView } from './TabDocView';
-import { GestureUtils } from '../../../pen-gestures/GestureUtils';
+import { CollectionFreeFormView } from './collectionFreeForm';
 
 interface CollectionMenuProps {
     panelHeight: () => number;
@@ -733,7 +734,7 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionView
                 doc._currentFrame = 0;
                 CollectionFreeFormDocumentView.setupKeyframes(childDocs, 0);
             }
-            CollectionFreeFormDocumentView.updateKeyframe(undefined, childDocs, currentFrame || 0);
+            CollectionFreeFormView.updateKeyframe(undefined, [...childDocs, doc], currentFrame || 0);
             doc._currentFrame = newFrame === undefined ? 0 : Math.max(0, newFrame);
         }
     }
@@ -747,7 +748,7 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionView
             this.document._currentFrame = 0;
             CollectionFreeFormDocumentView.setupKeyframes(this.childDocs, 0);
         }
-        this._keyTimer = CollectionFreeFormDocumentView.updateKeyframe(this._keyTimer, this.childDocs, currentFrame || 0);
+        this._keyTimer = CollectionFreeFormView.updateKeyframe(this._keyTimer, [...this.childDocs, this.document], currentFrame || 0);
         this.document._currentFrame = Math.max(0, (currentFrame || 0) + 1);
         this.document.lastFrame = Math.max(NumCast(this.document._currentFrame), NumCast(this.document.lastFrame));
     };
@@ -759,7 +760,7 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionView
             this.document._currentFrame = 0;
             CollectionFreeFormDocumentView.setupKeyframes(this.childDocs, 0);
         }
-        this._keyTimer = CollectionFreeFormDocumentView.gotoKeyframe(this._keyTimer, this.childDocs.slice());
+        this._keyTimer = CollectionFreeFormView.gotoKeyframe(this._keyTimer, [...this.childDocs, this.document], 1000);
         this.document._currentFrame = Math.max(0, (currentFrame || 0) - 1);
     };
 
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index 456f2a13d..330e116d1 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -265,7 +265,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
         const dropAction = StrCast(this.doc.childDropAction) as dropActionType;
         const addDoc = (doc: Doc | Doc[], relativeTo?: Doc, before?: boolean) => this.addDoc(doc, relativeTo, before);
         const moveDoc = (d: Doc | Doc[], target: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => this.props.moveDocument?.(d, target, addDoc) || false;
-        if (this.treeChildren.length < this._renderCount) setTimeout(action(() => (this._renderCount = Math.min(this.treeChildren.length, this._renderCount + 20))));
+        if (this._renderCount < this.treeChildren.length) setTimeout(action(() => (this._renderCount = Math.min(this.treeChildren.length, this._renderCount + 20))));
         return TreeView.GetChildElements(
             this.treeChildren,
             this,
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index d6bc0a4b2..fa648eb44 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -248,7 +248,7 @@ export class TabDocView extends React.Component<TabDocViewProps> {
                 return;
             }
             const anchorDoc = DocumentManager.Instance.getDocumentView(doc)?.ComponentView?.getAnchor?.(false);
-            const pinDoc = Doc.MakeDelegate(anchorDoc ?? doc);
+            const pinDoc = anchorDoc && anchorDoc !== doc ? anchorDoc : Doc.MakeDelegate(doc);
             pinDoc.presentationTargetDoc = anchorDoc ?? doc;
             pinDoc.title = doc.title + ' - Slide';
             pinDoc.data = new List<Doc>(); // the children of the alias' layout are the presentation slide children.  the alias' data field might be children of a collection, PDF data, etc -- in any case we don't want the tree view to "see" this data
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 4d6e0dff2..e5b4b76a8 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -54,6 +54,7 @@ import './CollectionFreeFormView.scss';
 import { MarqueeView } from './MarqueeView';
 import React = require('react');
 import { DocumentDecorations } from '../../DocumentDecorations';
+import { PresEffect } from '../../nodes/trails';
 
 export type collectionFreeformViewProps = {
     annotationLayerHostsContent?: boolean; // whether to force scaling of content (needed by ImageBox)
@@ -150,7 +151,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             ? { x: cb[0], y: cb[1], r: cb[2], b: cb[3] }
             : this.props.contentBounds?.() ??
                   aggregateBounds(
-                      this._layoutElements.filter(e => e.bounds && !e.bounds.z).map(e => e.bounds!),
+                      this._layoutElements.filter(e => e.bounds && e.bounds.width && !e.bounds.z).map(e => e.bounds!),
                       NumCast(this.layoutDoc._xPadding, 10),
                       NumCast(this.layoutDoc._yPadding, 10)
                   );
@@ -185,6 +186,32 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     }
 
     _keyTimer: NodeJS.Timeout | undefined;
+
+    public static gotoKeyframe(timer: NodeJS.Timeout | undefined, docs: Doc[], duration: number) {
+        if (timer) clearTimeout(timer);
+        return DocumentView.SetViewTransition(docs, 'all', duration, undefined, true);
+    }
+    public static updateKeyframe(timer: NodeJS.Timeout | undefined, docs: Doc[], time: number) {
+        if (timer) clearTimeout(timer);
+        const newTimer = DocumentView.SetViewTransition(docs, 'all', 1000, undefined, true);
+        const timecode = Math.round(time);
+        docs.forEach(doc => {
+            CollectionFreeFormDocumentView.animFields.forEach(val => {
+                const findexed = Cast(doc[`${val.key}-indexed`], listSpec('number'), null);
+                findexed?.length <= timecode + 1 && findexed.push(undefined as any as number);
+            });
+            CollectionFreeFormDocumentView.animStringFields.forEach(val => {
+                const findexed = Cast(doc[`${val}-indexed`], listSpec('string'), null);
+                findexed?.length <= timecode + 1 && findexed.push(undefined as any as string);
+            });
+            CollectionFreeFormDocumentView.animDataFields(doc).forEach(val => {
+                const findexed = Cast(doc[`${val}-indexed`], listSpec(InkField), null);
+                findexed?.length <= timecode + 1 && findexed.push(undefined as any);
+            });
+        });
+        return newTimer;
+    }
+
     changeKeyFrame = (back = false) => {
         const currentFrame = Cast(this.Document._currentFrame, 'number', null);
         if (currentFrame === undefined) {
@@ -192,10 +219,10 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             CollectionFreeFormDocumentView.setupKeyframes(this.childDocs, 0);
         }
         if (back) {
-            this._keyTimer = CollectionFreeFormDocumentView.gotoKeyframe(this._keyTimer, this.childDocs.slice());
+            this._keyTimer = CollectionFreeFormView.gotoKeyframe(this._keyTimer, [...this.childDocs, this.layoutDoc], 1000);
             this.Document._currentFrame = Math.max(0, (currentFrame || 0) - 1);
         } else {
-            this._keyTimer = CollectionFreeFormDocumentView.updateKeyframe(this._keyTimer, this.childDocs, currentFrame || 0);
+            this._keyTimer = CollectionFreeFormView.updateKeyframe(this._keyTimer, [...this.childDocs, this.layoutDoc], currentFrame || 0);
             this.Document._currentFrame = Math.max(0, (currentFrame || 0) + 1);
             this.Document.lastFrame = Math.max(NumCast(this.Document._currentFrame), NumCast(this.Document.lastFrame));
         }
@@ -1204,7 +1231,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             }
             // focus on the document in the collection
             const didMove = !cantTransform && !doc.z && (panX !== savedState.panX || panY !== savedState.panY || scale !== savedState.scale);
-            const focusSpeed = options?.instant ? 0 : didMove ? options.zoomTime ?? 500 : 0;
+            const focusSpeed = options?.instant ? 0 : didMove || DocCast(options.effect)?.presEffect !== PresEffect.None ? options.zoomTime ?? 500 : 0;
             // glr: freeform transform speed can be set by adjusting presTransition field - needs a way of knowing when presentation is not active...
             if (didMove) {
                 options.zoomScale && scale && (this.Document[this.scaleFieldKey] = scale);
@@ -1392,12 +1419,13 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     });
 
     getCalculatedPositions(params: { pair: { layout: Doc; data?: Doc }; index: number; collection: Doc }): PoolData {
-        const curFrame = Cast(this.Document._currentFrame, 'number');
         const childDoc = params.pair.layout;
         const childDocLayout = Doc.Layout(childDoc);
+        const layoutFrameNumber = Cast(this.Document._currentFrame, 'number'); // frame number that container is at which determines layout frame values
+        const contentFrameNumber = Cast(childDocLayout._currentFrame, 'number', layoutFrameNumber ?? null); // frame number that content is at which determines what content is displayed
         const { z, zIndex } = childDoc;
-        const { backgroundColor, color } = curFrame === undefined ? { backgroundColor: undefined, color: undefined } : CollectionFreeFormDocumentView.getStringValues(childDoc, curFrame);
-        const { x, y, opacity } = curFrame === undefined ? { x: childDoc.x, y: childDoc.y, opacity: this.props.childOpacity?.() } : CollectionFreeFormDocumentView.getValues(childDoc, curFrame);
+        const { backgroundColor, color } = contentFrameNumber === undefined ? { backgroundColor: undefined, color: undefined } : CollectionFreeFormDocumentView.getStringValues(childDoc, contentFrameNumber);
+        const { x, y, opacity } = layoutFrameNumber === undefined ? { x: childDoc.x, y: childDoc.y, opacity: this.props.childOpacity?.() } : CollectionFreeFormDocumentView.getValues(childDoc, layoutFrameNumber);
         return {
             x: Number.isNaN(NumCast(x)) ? 0 : NumCast(x),
             y: Number.isNaN(NumCast(y)) ? 0 : NumCast(y),
@@ -1543,13 +1571,15 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         const elements = computedElementData.slice();
         Array.from(newPool.entries())
             .filter(entry => this.isCurrent(entry[1].pair.layout))
-            .forEach((entry, i) =>
+            .forEach((entry, i) => {
+                const childData: ViewDefBounds = this.childDataProvider(entry[1].pair.layout, entry[1].replica);
+                const childSize = this.childSizeProvider(entry[1].pair.layout, entry[1].replica);
                 elements.push({
                     ele: this.getChildDocView(entry[1]),
-                    bounds: this.childDataProvider(entry[1].pair.layout, entry[1].replica),
+                    bounds: childData.opacity === 0 ? { ...childData, width: 0, height: 0 } : { ...childData, width: childSize.width, height: childSize.height },
                     inkMask: BoolCast(entry[1].pair.layout.isInkMask) ? NumCast(entry[1].pair.layout.opacity, 1) : -1,
-                })
-            );
+                });
+            });
 
         if (this.props.isAnnotationOverlay && this.props.Document[this.scaleFieldKey]) {
             // don't zoom out farther than 1-1 if it's a bounded item (image, video, pdf), otherwise don't allow zooming in closer than 1-1 if it's a text sidebar
@@ -1988,7 +2018,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                         zoomScaling={this.zoomScaling}
                         presPaths={this.showPresPaths}
                         presPinView={BoolCast(this.Document.presPinView)}
-                        transition={this._panZoomTransition ? `transform ${this._panZoomTransition}ms` : Cast(this.layoutDoc._viewTransition, 'string', null)}
+                        transition={this._panZoomTransition ? `transform ${this._panZoomTransition}ms` : Cast(this.layoutDoc._viewTransition, 'string', Cast(this.props.DocumentView?.()?.rootDoc._viewTransition, 'string', null))}
                         viewDefDivClick={this.props.viewDefDivClick}>
                         {this.children}
                     </CollectionFreeFormViewPannableContents>
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index bc3b17cd9..bd33c4d80 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -397,6 +397,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
         newCollection.forceActive = makeGroup;
         newCollection.x = this.Bounds.left;
         newCollection.y = this.Bounds.top;
+        newCollection.fitwidth = true;
         selected.forEach(d => (d.context = newCollection));
         this.hideMarquee();
         return newCollection;
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index f8ef87fb1..ba510e1dc 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -40,6 +40,8 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
         { key: '_rotation', val: 0 },
         { key: '_scrollTop' },
         { key: 'opacity', val: 1 },
+        { key: '_currentFrame' },
+        { key: 'viewScale', val: 1 },
         { key: 'viewScale', val: 1 },
         { key: 'panX' },
         { key: 'panY' },
@@ -115,32 +117,6 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
         });
     }
 
-    public static updateKeyframe(timer: NodeJS.Timeout | undefined, docs: Doc[], time: number, targetDoc?: Doc) {
-        if (timer) clearTimeout(timer);
-        const newTimer = DocumentView.SetViewTransition(docs, 'all', 1000, undefined, true);
-        const timecode = Math.round(time);
-        docs.forEach(doc => {
-            CollectionFreeFormDocumentView.animFields.forEach(val => {
-                const findexed = Cast(doc[`${val.key}-indexed`], listSpec('number'), null);
-                findexed?.length <= timecode + 1 && findexed.push(undefined as any as number);
-            });
-            CollectionFreeFormDocumentView.animStringFields.forEach(val => {
-                const findexed = Cast(doc[`${val}-indexed`], listSpec('string'), null);
-                findexed?.length <= timecode + 1 && findexed.push(undefined as any as string);
-            });
-            CollectionFreeFormDocumentView.animDataFields(doc).forEach(val => {
-                const findexed = Cast(doc[`${val}-indexed`], listSpec(InkField), null);
-                findexed?.length <= timecode + 1 && findexed.push(undefined as any);
-            });
-        });
-        return newTimer;
-    }
-
-    public static gotoKeyframe(timer: NodeJS.Timeout | undefined, docs: Doc[], duration = 1000) {
-        if (timer) clearTimeout(timer);
-        return DocumentView.SetViewTransition(docs, 'all', duration, undefined, true);
-    }
-
     public static setupZoom(doc: Doc, targDoc: Doc) {
         const width = new List<number>();
         const height = new List<number>();
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 3d89566ee..c3b0412de 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -17,7 +17,7 @@ import { BoolCast, Cast, DocCast, ImageCast, NumCast, ScriptCast, StrCast } from
 import { AudioField } from '../../../fields/URLField';
 import { GetEffectiveAcl, SharingPermissions, TraceMobx } from '../../../fields/util';
 import { MobileInterface } from '../../../mobile/MobileInterface';
-import { emptyFunction, isTargetChildOf as isParentOf, lightOrDark, OmitKeys, returnEmptyString, returnFalse, returnTrue, returnVal, simulateMouseClick, Utils } from '../../../Utils';
+import { emptyFunction, isTargetChildOf as isParentOf, lightOrDark, OmitKeys, returnEmptyString, returnFalse, returnNone, returnTrue, returnVal, simulateMouseClick, Utils } from '../../../Utils';
 import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils';
 import { DocServer } from '../../DocServer';
 import { Docs, DocUtils } from '../../documents/Documents';
@@ -252,6 +252,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
     private _disposers: { [name: string]: IReactionDisposer } = {};
     private _downX: number = 0;
     private _downY: number = 0;
+    private _downTime: number = 0;
     private _firstX: number = -1;
     private _firstY: number = -1;
     private _lastTap: number = 0;
@@ -610,7 +611,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
             let preventDefault = true;
             const isScriptBox = () => StrCast(Doc.LayoutField(this.layoutDoc))?.includes(ScriptingBox.name);
             (this.rootDoc._raiseWhenDragged === undefined ? DragManager.GetRaiseWhenDragged() : this.rootDoc._raiseWhenDragged) && this.props.bringToFront(this.rootDoc);
-            if (this._doubleTap && (this.props.Document.type !== DocumentType.FONTICON || this.onDoubleClickHandler)) {
+            if (this._doubleTap && (![DocumentType.FONTICON, DocumentType.PRES].includes(this.props.Document.type as any) || this.onDoubleClickHandler)) {
                 // && !this.onClickHandler?.script) { // disable double-click to show full screen for things that have an on click behavior since clicking them twice can be misinterpreted as a double click
                 if (this._timeout) {
                     clearTimeout(this._timeout);
@@ -718,6 +719,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
         );
         this._downX = e.clientX;
         this._downY = e.clientY;
+        this._downTime = Date.now();
         if ((Doc.ActiveTool === InkTool.None || this.props.addDocTab === returnFalse) && !(this.props.Document.rootDocument && !(e.ctrlKey || e.button > 0))) {
             // if this is part of a template, let the event go up to the tempalte root unless right/ctrl clicking
             if (
@@ -777,11 +779,10 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
         this._cursorTimer && clearTimeout(this._cursorTimer);
         this._cursorPress = false;
 
+        const now = Date.now();
         if (this.onPointerUpHandler?.script && !InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) {
             this.onPointerUpHandler.script.run({ self: this.rootDoc, this: this.layoutDoc }, console.log);
-        } else {
-            const now = Date.now();
-            console.log(now - this._lastTap);
+        } else if (now - this._downTime < 300) {
             this._doubleTap = now - this._lastTap < 600 && e.button === 0 && Math.abs(e.clientX - this._downX) < 2 && Math.abs(e.clientY - this._downY) < 2;
             // bcz: this is a placeholder.  documents, when selected, should stopPropagation on doubleClicks if they want to keep the DocumentView from getting them
             if (!this.props.isSelected(true) || ![DocumentType.PDF, DocumentType.RTF].includes(StrCast(this.rootDoc.type) as any)) this._lastTap = Date.now(); // don't want to process the start of a double tap if the doucment is selected
@@ -1154,7 +1155,9 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                 className="documentView-contentsView"
                 style={{
                     pointerEvents:
-                        (this.props.pointerEvents?.() as any) ?? this.rootDoc.layoutKey === 'layout_icon'
+                        this.opacity === 0
+                            ? 'none'
+                            : (this.props.pointerEvents?.() as any) ?? this.rootDoc.layoutKey === 'layout_icon'
                             ? 'none'
                             : (this.props.contentPointerEvents as any)
                             ? (this.props.contentPointerEvents as any)
@@ -1181,6 +1184,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                 <DocumentContentsView
                     key={1}
                     {...this.props}
+                    pointerEvents={this.opacity === 0 ? returnNone : this.props.pointerEvents}
                     docViewPath={this.props.viewPath}
                     thumbShown={this.thumbShown}
                     isHovering={this.props.isHovering}
diff --git a/src/client/views/nodes/button/FontIconBox.tsx b/src/client/views/nodes/button/FontIconBox.tsx
index b352f3790..93761633c 100644
--- a/src/client/views/nodes/button/FontIconBox.tsx
+++ b/src/client/views/nodes/button/FontIconBox.tsx
@@ -179,7 +179,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
                         className="list-item"
                         key={`${value}`}
                         style={{
-                            backgroundColor: value === checkResult ? Colors.LIGHT_BLUE : undefined,
+                            backgroundColor: value.toString() === checkResult ? Colors.LIGHT_BLUE : undefined,
                         }}
                         onClick={() => setValue(value)}>
                         {value}
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index e07517113..929bf1230 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -1,10 +1,10 @@
 import React = require('react');
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Tooltip } from '@material-ui/core';
-import { action, computed, IReactionDisposer, observable, ObservableSet, observe, reaction, runInAction } from 'mobx';
+import { action, computed, IReactionDisposer, observable, ObservableSet, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import { ColorState, SketchPicker } from 'react-color';
-import { AnimationSym, Doc, DocListCast, FieldResult, HighlightSym, Opt, StrListCast } from '../../../../fields/Doc';
+import { AnimationSym, Doc, DocListCast, FieldResult, Opt, StrListCast } from '../../../../fields/Doc';
 import { Copy, Id } from '../../../../fields/FieldSymbols';
 import { InkTool } from '../../../../fields/InkField';
 import { List } from '../../../../fields/List';
@@ -14,7 +14,7 @@ import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../../fields/Ty
 import { AudioField } from '../../../../fields/URLField';
 import { emptyFunction, emptyPath, returnFalse, returnOne, setupMoveUpEvents, StopEvent } from '../../../../Utils';
 import { DocServer } from '../../../DocServer';
-import { Docs, DocumentOptions } from '../../../documents/Documents';
+import { Docs } from '../../../documents/Documents';
 import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes';
 import { DocumentManager } from '../../../util/DocumentManager';
 import { ScriptingGlobals } from '../../../util/ScriptingGlobals';
@@ -23,19 +23,17 @@ import { SettingsManager } from '../../../util/SettingsManager';
 import { undoBatch, UndoManager } from '../../../util/UndoManager';
 import { CollectionDockingView } from '../../collections/CollectionDockingView';
 import { CollectionFreeFormView, computeTimelineLayout, MarqueeViewBounds } from '../../collections/collectionFreeForm';
+import { CollectionStackedTimeline } from '../../collections/CollectionStackedTimeline';
 import { CollectionView } from '../../collections/CollectionView';
 import { TabDocView } from '../../collections/TabDocView';
 import { ViewBoxBaseComponent } from '../../DocComponent';
 import { Colors } from '../../global/globalEnums';
 import { LightboxView } from '../../LightboxView';
-import { CollectionFreeFormDocumentView } from '../CollectionFreeFormDocumentView';
 import { DocFocusOptions, DocumentView, OpenWhere, OpenWhereMod } from '../DocumentView';
 import { FieldView, FieldViewProps } from '../FieldView';
 import { ScriptingBox } from '../ScriptingBox';
 import './PresBox.scss';
 import { PresEffect, PresEffectDirection, PresMovement, PresStatus } from './PresEnums';
-import { CollectionStackedTimeline } from '../../collections/CollectionStackedTimeline';
-import { PresElementBox } from './PresElementBox';
 const { Howl } = require('howler');
 
 export interface PinProps {
@@ -296,7 +294,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 if (context) {
                     const ffview = DocumentManager.Instance.getFirstDocumentView(context)?.ComponentView as CollectionFreeFormView;
                     if (ffview?.childDocs) {
-                        this._keyTimer = CollectionFreeFormDocumentView.gotoKeyframe(this._keyTimer, ffview.childDocs.slice(), transTime);
+                        this._keyTimer = CollectionFreeFormView.gotoKeyframe(this._keyTimer, ffview.childDocs, transTime);
                         context._currentFrame = NumCast(activeFrame);
                     }
                 }
@@ -598,7 +596,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                     tagDoc.opacity = 1;
                 }
             }
-            const hidingIndBef = itemIndexes.find(item => item >= this.itemIndex);
+            const hidingIndBef = itemIndexes.find(item => item >= this.itemIndex) ?? itemIndexes.slice().reverse().lastElement();
             if (curDoc.presHideBefore && index === hidingIndBef) {
                 if (index > this.itemIndex) {
                     tagDoc.opacity = 0;
@@ -606,10 +604,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                     tagDoc.opacity = 1;
                 }
             }
-            const hidingIndAft = itemIndexes
-                .slice()
-                .reverse()
-                .find(item => item < this.itemIndex);
+            const hidingIndAft =
+                itemIndexes
+                    .slice()
+                    .reverse()
+                    .find(item => item <= this.itemIndex) ?? itemIndexes.lastElement();
             if (curDoc.presHideAfter && index === hidingIndAft) {
                 if (index < this.itemIndex) {
                     tagDoc.opacity = 0;
@@ -875,7 +874,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     @action
     selectElement = async (doc: Doc, noNav = false) => {
         CollectionStackedTimeline.CurrentlyPlaying?.map((clip, i) => DocumentManager.Instance.getDocumentView(clip)?.ComponentView?.Pause?.());
-        !noNav && this.gotoDocument(this.childDocs.indexOf(doc), this.activeItem);
+        if (noNav) {
+            const index = this.childDocs.indexOf(doc);
+            if (index >= 0 && index < this.childDocs.length) {
+                this.rootDoc._itemIndex = index;
+            }
+        } else this.gotoDocument(this.childDocs.indexOf(doc), this.activeItem);
         this.updateCurrentPresentation(DocCast(doc.context));
     };
 
@@ -911,19 +915,19 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
 
     //regular click
     @action
-    regularSelect = (doc: Doc, ref: HTMLElement, drag: HTMLElement, focus: boolean, selectPres = true, noNav = false) => {
+    regularSelect = (doc: Doc, ref: HTMLElement, drag: HTMLElement, noNav: boolean, selectPres = true) => {
         this.clearSelectedArray();
         this.addToSelectedArray(doc);
         this._eleArray.splice(0, this._eleArray.length, ref);
         this._dragArray.splice(0, this._dragArray.length, drag);
-        focus && this.selectElement(doc, noNav);
+        this.selectElement(doc, noNav);
         selectPres && this.selectPres();
     };
 
-    modifierSelect = (doc: Doc, ref: HTMLElement, drag: HTMLElement, focus: boolean, cmdClick: boolean, shiftClick: boolean, noNav: boolean = false) => {
+    modifierSelect = (doc: Doc, ref: HTMLElement, drag: HTMLElement, noNav: boolean, cmdClick: boolean, shiftClick: boolean) => {
         if (cmdClick) this.multiSelect(doc, ref, drag);
         else if (shiftClick) this.shiftSelect(doc, ref, drag);
-        else this.regularSelect(doc, ref, drag, focus, noNav);
+        else this.regularSelect(doc, ref, drag, noNav);
     };
 
     static keyEventsWrapper = (e: KeyboardEvent) => PresBox.Instance?.keyEvents(e);
diff --git a/src/client/views/nodes/trails/PresElementBox.tsx b/src/client/views/nodes/trails/PresElementBox.tsx
index f265c1315..40535c8cb 100644
--- a/src/client/views/nodes/trails/PresElementBox.tsx
+++ b/src/client/views/nodes/trails/PresElementBox.tsx
@@ -139,7 +139,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 onClick={e => {
                     e.stopPropagation();
                     e.preventDefault();
-                    this.presBoxView?.modifierSelect(doc, this._itemRef.current!, this._dragRef.current!, !e.shiftKey && !e.ctrlKey && !e.metaKey, e.ctrlKey || e.metaKey, e.shiftKey);
+                    this.presBoxView?.modifierSelect(doc, this._itemRef.current!, this._dragRef.current!, e.shiftKey || e.ctrlKey || e.metaKey, e.ctrlKey || e.metaKey, e.shiftKey);
                     this.presExpandDocumentClick();
                 }}>
                 <div className="presItem-groupNum">{`${ind + 1}.`}</div>
@@ -177,22 +177,14 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
         const element = e.target as any;
         e.stopPropagation();
         e.preventDefault();
-        if (element && !(e.ctrlKey || e.metaKey)) {
-            if (this.selectedArray?.has(this.rootDoc)) {
-                this.selectedArray.size === 1 && this.presBoxView?.regularSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, false, false);
-                setupMoveUpEvents(this, e, this.startDrag, emptyFunction, emptyFunction);
-            } else {
-                setupMoveUpEvents(
-                    this,
-                    e,
-                    (e: PointerEvent) => {
-                        this.presBoxView?.regularSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, false, false);
-                        return this.startDrag(e);
-                    },
-                    emptyFunction,
-                    emptyFunction
-                );
-            }
+        if (element && !(e.ctrlKey || e.metaKey || e.button === 2)) {
+            this.presBoxView?.regularSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, true, false);
+            setupMoveUpEvents(this, e, this.startDrag, emptyFunction, e => {
+                e.stopPropagation();
+                e.preventDefault();
+                this.presBoxView?.modifierSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, e.shiftKey || e.ctrlKey || e.metaKey, e.ctrlKey || e.metaKey, e.shiftKey);
+                this.presBoxView?.activeItem && this.showRecording(this.presBoxView?.activeItem);
+            });
         }
     };
 
@@ -314,50 +306,14 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
     /**
      * Method called for updating the view of the currently selected document
      *
-     * @param targetDoc
+     * @param presTargetDoc
      * @param activeItem
      */
     @undoBatch
     @action
-    updateCapturedViewContents = (targetDoc: Doc, activeItem: Doc) => {
-        switch (targetDoc.type) {
-            case DocumentType.PDF:
-            case DocumentType.WEB:
-            case DocumentType.RTF:
-                const scroll = targetDoc._scrollTop;
-                activeItem.presPinViewScroll = scroll;
-                if (targetDoc.type === DocumentType.RTF) {
-                    activeItem.presData = targetDoc[Doc.LayoutFieldKey(targetDoc)] instanceof RichTextField ? (targetDoc[Doc.LayoutFieldKey(targetDoc)] as RichTextField)[Copy]() : targetDoc.text;
-                }
-                break;
-            case DocumentType.INK:
-                activeItem.presData = targetDoc[Doc.LayoutFieldKey(targetDoc)] instanceof InkField ? (targetDoc[Doc.LayoutFieldKey(targetDoc)] as InkField)[Copy]() : targetDoc.data;
-                break;
-            case DocumentType.VID:
-            case DocumentType.AUDIO:
-                activeItem.presStartTime = targetDoc._currentTimecode;
-                break;
-            case DocumentType.COMPARISON:
-                const clipWidth = targetDoc._clipWidth;
-                activeItem.presPinClipWidth = clipWidth;
-                break;
-            case DocumentType.COL:
-                activeItem.presPinLayoutData = new List<string>(DocListCast(targetDoc[Doc.LayoutFieldKey(targetDoc)]).map(d => JSON.stringify({ id: d[Id], x: NumCast(d.x), y: NumCast(d.y), w: NumCast(d._width), h: NumCast(d._height) })));
-            default:
-                const bestView = DocumentManager.Instance.getFirstDocumentView(targetDoc);
-                if (activeItem.presPinViewBounds && bestView) {
-                    const bounds = MarqueeView.CurViewBounds(targetDoc, bestView.props.PanelWidth(), bestView.props.PanelHeight());
-                    activeItem.presPinView = true;
-                    activeItem.presPinViewScale = NumCast(targetDoc._viewScale, 1);
-                    activeItem.presPinViewX = bounds.left + bounds.width / 2;
-                    activeItem.presPinViewY = bounds.top + bounds.height / 2;
-                    activeItem.presPinViewBounds = new List<number>([bounds.left, bounds.top, bounds.left + bounds.width, bounds.top + bounds.height]);
-                } else {
-                    activeItem.presPinViewX = targetDoc._panX;
-                    activeItem.presPinViewY = targetDoc._panY;
-                    activeItem.presPinViewScale = targetDoc._viewScale;
-                }
-        }
+    updateCapturedViewContents = (presTargetDoc: Doc, activeItem: Doc) => {
+        const target = DocCast(presTargetDoc.annotationOn) ?? presTargetDoc;
+        PresBox.pinDocView(activeItem, { pinDocContent: true, pinData: PresBox.pinDataTypes(target) }, target);
     };
 
     @computed get recordingIsInOverlay() {
@@ -465,24 +421,26 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
         const activeItem: Doc = this.rootDoc;
 
         const items: JSX.Element[] = [];
-        if (activeItem.presPinLayout) {
-            items.push(
-                <Tooltip key="slide" title={<div className="dash-tooltip">Update captured doc layout</div>}>
-                    <div className="slideButton" onClick={() => this.updateCapturedContainerLayout(targetDoc, activeItem)} style={{ fontWeight: 700, display: 'flex' }}>
-                        L
-                    </div>
-                </Tooltip>
-            );
-        }
-        if (activeItem.presPinData || activeItem.presPinView) {
-            items.push(
-                <Tooltip key="flex" title={<div className="dash-tooltip">Update captured doc content</div>}>
-                    <div className="slideButton" onClick={() => this.updateCapturedViewContents(targetDoc, activeItem)} style={{ fontWeight: 700, display: 'flex' }}>
-                        C
-                    </div>
-                </Tooltip>
-            );
-        }
+        items.push(
+            <Tooltip key="slide" title={<div className="dash-tooltip">Update captured doc layout</div>}>
+                <div
+                    className="slideButton"
+                    onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => this.updateCapturedContainerLayout(targetDoc, activeItem), true)}
+                    style={{ opacity: activeItem.presPinLayout ? 1 : 0.5, fontWeight: 700, display: 'flex' }}>
+                    L
+                </div>
+            </Tooltip>
+        );
+        items.push(
+            <Tooltip key="flex" title={<div className="dash-tooltip">Update captured doc content</div>}>
+                <div
+                    className="slideButton"
+                    onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => this.updateCapturedViewContents(targetDoc, activeItem))}
+                    style={{ opacity: activeItem.presPinData || activeItem.presPinView ? 1 : 0.5, fontWeight: 700, display: 'flex' }}>
+                    C
+                </div>
+            </Tooltip>
+        );
         if (!Doc.noviceMode) {
             items.push(
                 <Tooltip key="slash" title={<div className="dash-tooltip">{this.recordingIsInOverlay ? 'Hide Recording' : `${PresElementBox.videoIsRecorded(activeItem) ? 'Show' : 'Start'} recording`}</div>}>
@@ -557,15 +515,9 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
                     paddingTop: NumCast(this.layoutDoc._yPadding, this.props.yPadding),
                     paddingBottom: NumCast(this.layoutDoc._yPadding, this.props.yPadding),
                 }}
-                onClick={e => {
-                    e.stopPropagation();
-                    e.preventDefault();
-                    this.presBoxView?.modifierSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, !e.shiftKey && !e.ctrlKey && !e.metaKey, e.ctrlKey || e.metaKey, e.shiftKey);
-                    this.showRecording(activeItem);
-                }}
                 onDoubleClick={action(e => {
                     this.toggleProperties();
-                    this.presBoxView?.regularSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, true);
+                    this.presBoxView?.regularSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, false);
                 })}
                 onPointerOver={this.onPointerOver}
                 onPointerLeave={this.onPointerLeave}
@@ -599,7 +551,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                 onPointerDown={e => {
                                     e.stopPropagation();
                                     if (this._itemRef.current && this._dragRef.current) {
-                                        this.presBoxView?.modifierSelect(activeItem, this._itemRef.current, this._dragRef.current, false, false, false, true);
+                                        this.presBoxView?.modifierSelect(activeItem, this._itemRef.current, this._dragRef.current, true, false, false);
                                     }
                                 }}
                                 onClick={e => e.stopPropagation()}>{`${this.indexInPres + 1}.  `}</div>
diff --git a/src/fields/util.ts b/src/fields/util.ts
index 3a7484cfd..6024705ec 100644
--- a/src/fields/util.ts
+++ b/src/fields/util.ts
@@ -1,3 +1,4 @@
+import { forEach } from 'lodash';
 import { $mobx, action, observable, runInAction, trace } from 'mobx';
 import { computedFn } from 'mobx-utils';
 import { DocServer } from '../client/DocServer';
-- 
cgit v1.2.3-70-g09d2


From 3e93857b4dfdc09a4dce7ec12ca0dbc60b727fb1 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 22 Feb 2023 16:16:45 -0500
Subject: fixes for restoring layouts in presentations and link following.

---
 src/client/views/collections/CollectionMenu.tsx               |  2 +-
 .../collections/collectionFreeForm/CollectionFreeFormView.tsx |  2 +-
 src/client/views/nodes/ImageBox.tsx                           |  2 +-
 src/client/views/nodes/trails/PresElementBox.tsx              | 11 +++++------
 4 files changed, 8 insertions(+), 9 deletions(-)

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

diff --git a/src/client/views/collections/CollectionMenu.tsx b/src/client/views/collections/CollectionMenu.tsx
index c83f4e689..17f02711d 100644
--- a/src/client/views/collections/CollectionMenu.tsx
+++ b/src/client/views/collections/CollectionMenu.tsx
@@ -746,7 +746,7 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionView
         const currentFrame = Cast(this.document._currentFrame, 'number', null);
         if (currentFrame === undefined) {
             this.document._currentFrame = 0;
-            CollectionFreeFormDocumentView.setupKeyframes(this.childDocs, 0);
+            CollectionFreeFormDocumentView.setupKeyframes(this.c hildDocs, 0);
         }
         this._keyTimer = CollectionFreeFormView.updateKeyframe(this._keyTimer, [...this.childDocs, this.document], currentFrame || 0);
         this.document._currentFrame = Math.max(0, (currentFrame || 0) + 1);
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index e5b4b76a8..132538ea4 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1612,7 +1612,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         const focusSpeed = options.instant ? 0 : options.zoomTime ?? 500;
         return PresBox.restoreTargetDocView(
             this.props.DocumentView?.(), //
-            { pinDocLayout: BoolCast(anchor.presPinDocLayout) },
+            { pinDocLayout: BoolCast(anchor.presPinLayout) },
             anchor,
             focusSpeed,
             {
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index c66f2a67b..6e616931d 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -76,7 +76,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         const focusSpeed = options.instant ? 0 : options.zoomTime ?? 500;
         return PresBox.restoreTargetDocView(
             this.props.DocumentView?.(), //
-            { pinDocLayout: BoolCast(anchor.presPinDocLayout) },
+            { pinDocLayout: BoolCast(anchor.presPinLayout) },
             anchor,
             focusSpeed,
             !anchor.presPinData
diff --git a/src/client/views/nodes/trails/PresElementBox.tsx b/src/client/views/nodes/trails/PresElementBox.tsx
index 40535c8cb..d19b78dbc 100644
--- a/src/client/views/nodes/trails/PresElementBox.tsx
+++ b/src/client/views/nodes/trails/PresElementBox.tsx
@@ -3,20 +3,17 @@ import { Tooltip } from '@material-ui/core';
 import { action, computed, IReactionDisposer, observable, reaction } from 'mobx';
 import { observer } from 'mobx-react';
 import { Doc, DocListCast, HeightSym, Opt, WidthSym } from '../../../../fields/Doc';
-import { Copy, Id } from '../../../../fields/FieldSymbols';
-import { InkField } from '../../../../fields/InkField';
+import { Id } from '../../../../fields/FieldSymbols';
 import { List } from '../../../../fields/List';
-import { RichTextField } from '../../../../fields/RichTextField';
 import { Cast, DocCast, NumCast, StrCast } from '../../../../fields/Types';
 import { emptyFunction, returnEmptyDoclist, returnFalse, returnTrue, setupMoveUpEvents } from '../../../../Utils';
 import { Docs, DocUtils } from '../../../documents/Documents';
-import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes';
+import { CollectionViewType } from '../../../documents/DocumentTypes';
 import { DocumentManager } from '../../../util/DocumentManager';
 import { DragManager } from '../../../util/DragManager';
 import { SettingsManager } from '../../../util/SettingsManager';
 import { Transform } from '../../../util/Transform';
 import { undoBatch } from '../../../util/UndoManager';
-import { MarqueeView } from '../../collections/collectionFreeForm';
 import { ViewBoxBaseComponent } from '../../DocComponent';
 import { EditableView } from '../../EditableView';
 import { Colors } from '../../global/globalEnums';
@@ -296,12 +293,14 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
      */
     @undoBatch
     @action
-    updateCapturedContainerLayout = (targetDoc: Doc, activeItem: Doc) => {
+    updateCapturedContainerLayout = (presTargetDoc: Doc, activeItem: Doc) => {
+        const targetDoc = DocCast(presTargetDoc.annotationOn) ?? presTargetDoc;
         activeItem.presX = NumCast(targetDoc.x);
         activeItem.presY = NumCast(targetDoc.y);
         activeItem.presRot = NumCast(targetDoc.rotation);
         activeItem.presWidth = NumCast(targetDoc.width);
         activeItem.presHeight = NumCast(targetDoc.height);
+        activeItem.presPinLayout = true;
     };
     /**
      * Method called for updating the view of the currently selected document
-- 
cgit v1.2.3-70-g09d2


From c7878fc9b8df619d77c1323725022997d93c9789 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 22 Feb 2023 19:31:43 -0500
Subject: capture viewType as part of pinning a document/making an anchor to
 simplify VIewSpec restoration

---
 src/client/views/collections/CollectionMenu.tsx           |  2 +-
 src/client/views/collections/CollectionTimeView.tsx       |  2 --
 .../collectionFreeForm/CollectionFreeFormView.tsx         | 13 ++++++++-----
 src/client/views/nodes/DocumentView.tsx                   | 13 ++++++-------
 src/client/views/nodes/trails/PresBox.tsx                 | 15 +++++++++++++--
 5 files changed, 28 insertions(+), 17 deletions(-)

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

diff --git a/src/client/views/collections/CollectionMenu.tsx b/src/client/views/collections/CollectionMenu.tsx
index 17f02711d..c83f4e689 100644
--- a/src/client/views/collections/CollectionMenu.tsx
+++ b/src/client/views/collections/CollectionMenu.tsx
@@ -746,7 +746,7 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionView
         const currentFrame = Cast(this.document._currentFrame, 'number', null);
         if (currentFrame === undefined) {
             this.document._currentFrame = 0;
-            CollectionFreeFormDocumentView.setupKeyframes(this.c hildDocs, 0);
+            CollectionFreeFormDocumentView.setupKeyframes(this.childDocs, 0);
         }
         this._keyTimer = CollectionFreeFormView.updateKeyframe(this._keyTimer, [...this.childDocs, this.document], currentFrame || 0);
         this.document._currentFrame = Math.max(0, (currentFrame || 0) + 1);
diff --git a/src/client/views/collections/CollectionTimeView.tsx b/src/client/views/collections/CollectionTimeView.tsx
index 0ec21cc51..8ec683e6d 100644
--- a/src/client/views/collections/CollectionTimeView.tsx
+++ b/src/client/views/collections/CollectionTimeView.tsx
@@ -16,7 +16,6 @@ import { ScriptingGlobals } from '../../util/ScriptingGlobals';
 import { ContextMenu } from '../ContextMenu';
 import { ContextMenuProps } from '../ContextMenuItem';
 import { EditableView } from '../EditableView';
-import { ViewSpecPrefix } from '../nodes/DocumentView';
 import { computePivotLayout, computeTimelineLayout, ViewDefBounds } from './collectionFreeForm/CollectionFreeFormLayoutEngines';
 import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormView';
 import { CollectionSubView } from './CollectionSubView';
@@ -43,7 +42,6 @@ export class CollectionTimeView extends CollectionSubView() {
         proto.pivotField = this.pivotField;
         proto.docFilters = ObjectField.MakeCopy(this.layoutDoc._docFilters as ObjectField) || new List<string>([]);
         proto.docRangeFilters = ObjectField.MakeCopy(this.layoutDoc._docRangeFilters as ObjectField) || new List<string>([]);
-        proto[ViewSpecPrefix + '_viewType'] = this.layoutDoc._viewType;
 
         if (addAsAnnotation) {
             // when added as an annotation, links to anchors can be found as links to the document even if the anchors are not rendered
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 132538ea4..d69e6b810 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -38,7 +38,7 @@ import { GestureOverlay } from '../../GestureOverlay';
 import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, ActiveIsInkMask, InkingStroke, SetActiveInkColor, SetActiveInkWidth } from '../../InkingStroke';
 import { LightboxView } from '../../LightboxView';
 import { CollectionFreeFormDocumentView } from '../../nodes/CollectionFreeFormDocumentView';
-import { DocFocusOptions, DocumentView, DocumentViewProps, OpenWhere, ViewAdjustment, ViewSpecPrefix } from '../../nodes/DocumentView';
+import { DocFocusOptions, DocumentView, DocumentViewProps, OpenWhere, ViewAdjustment } from '../../nodes/DocumentView';
 import { FieldViewProps } from '../../nodes/FieldView';
 import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox';
 import { PresBox } from '../../nodes/trails/PresBox';
@@ -1617,6 +1617,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             focusSpeed,
             {
                 pannable: anchor.presPinData ? true : false,
+                viewType: anchor.presPinViewType ? true : false,
             }
         )
             ? focusSpeed
@@ -1627,11 +1628,13 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         if (this.props.Document.annotationOn) {
             return this.rootDoc;
         }
+
+        // create an anchor that saves information about the current state of the freeform view (pan, zoom, view type)
         const anchor = Docs.Create.TextanchorDocument({ title: 'ViewSpec - ' + StrCast(this.layoutDoc._viewType), presTransition: 500, annotationOn: this.rootDoc });
-        PresBox.pinDocView(anchor, { pinData: { pannable: true } }, this.rootDoc);
-        const proto = Doc.GetProto(anchor);
-        proto[ViewSpecPrefix + '_viewType'] = this.layoutDoc._viewType;
-        proto.docFilters = ObjectField.MakeCopy(this.layoutDoc.docFilters as ObjectField) || new List<string>([]);
+        PresBox.pinDocView(anchor, { pinData: { pannable: true, viewType: true } }, this.rootDoc);
+        // bcz TODO: saving document filters should be part of pinDocView...
+        Doc.GetProto(anchor).docFilters = ObjectField.MakeCopy(this.layoutDoc.docFilters as ObjectField) || new List<string>([]);
+
         if (addAsAnnotation) {
             if (Cast(this.dataDoc[this.props.fieldKey + '-annotations'], listSpec(Doc), null) !== undefined) {
                 Cast(this.dataDoc[this.props.fieldKey + '-annotations'], listSpec(Doc), []).push(anchor);
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index c3b0412de..5bdb994c5 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -98,8 +98,6 @@ export enum OpenWhereMod {
     bottom = 'bottom',
 }
 
-export const ViewSpecPrefix = 'viewSpec'; // field prefix for anchor fields that are immediately copied over to the target document when link is followed.  Other anchor properties will be copied over in the specific setViewSpec() method on their view (which allows for seting preview values instead of writing to the document)
-
 export interface DocFocusOptions {
     originalTarget?: Doc; // set in JumpToDocument, used by TabDocView to determine whether to fit contents to tab
     willPan?: boolean; // determines whether to pan to target document
@@ -585,13 +583,14 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
 
     focus = (anchor: Doc, options: DocFocusOptions) => {
         LightboxView.SetCookie(StrCast(anchor['cookies-set']));
-        // copying over VIEW fields immediately allows the view type to switch to create the right _componentView
-        Array.from(Object.keys(Doc.GetProto(anchor)))
-            .filter(key => key.startsWith(ViewSpecPrefix))
-            .forEach(spec => (this.layoutDoc[spec.replace(ViewSpecPrefix, '')] = (field => (field instanceof ObjectField ? ObjectField.MakeCopy(field) : field))(anchor[spec])));
-        // after a render the general viewSpec should have created the right _componentView, so after a timeout, call the componentview to update its specific view specs
+
+        // Restore viewing specification of target by reading them out of the anchor and applying to the target doc.
+        // after a render the correct _componentView should be created, so after a timeout, call the componentview to update its specific view specs
+        // bcz: TODO: all viewing specs should be restored via the anchor doc in PresBox.restoreTargetDocView called from scrollFocus() below.
         setTimeout(() => this._componentView?.setViewSpec?.(anchor, LinkDocPreview.LinkInfo ? true : false));
         const focusSpeed = this._componentView?.scrollFocus?.(anchor, { ...options, instant: options?.instant || LinkDocPreview.LinkInfo ? true : false });
+
+        // FOCUS: navigate through the display hierarchy making sure the target is in view
         const endFocus = focusSpeed === undefined ? options?.afterFocus : async (moved: boolean) => options?.afterFocus?.(true) ?? ViewAdjustment.doNothing;
         const startTime = Date.now();
         this.props.focus(options?.docTransform ? anchor : this.rootDoc, {
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index 929bf1230..32fcc0fae 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -48,6 +48,7 @@ export interface PinProps {
     pinData?: {
         scrollable?: boolean | undefined;
         pannable?: boolean | undefined;
+        viewType?: boolean | undefined;
         temporal?: boolean | undefined;
         clippable?: boolean | undefined;
         dataview?: boolean | undefined;
@@ -316,7 +317,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             this.onHideDocument(); //Handles hide after/before
         }
     });
-    static pinDataTypes(target?: Doc): { scrollable?: boolean; pannable?: boolean; temporal?: boolean; clippable?: boolean; dataview?: boolean; textview?: boolean; poslayoutview?: boolean; dataannos?: boolean } {
+    static pinDataTypes(target?: Doc): { scrollable?: boolean; pannable?: boolean; viewType?: boolean; temporal?: boolean; clippable?: boolean; dataview?: boolean; textview?: boolean; poslayoutview?: boolean; dataannos?: boolean } {
         const targetType = target?.type as any;
         const scrollable = [DocumentType.PDF, DocumentType.RTF, DocumentType.WEB].includes(targetType) || target?._viewType === CollectionViewType.Stacking;
         const pannable = [DocumentType.IMG, DocumentType.PDF].includes(targetType) || (targetType === DocumentType.COL && target?._viewType === CollectionViewType.Freeform);
@@ -325,8 +326,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         const dataview = [DocumentType.INK, DocumentType.COL, DocumentType.IMG].includes(targetType) && target?.activeFrame === undefined;
         const poslayoutview = [DocumentType.COL].includes(targetType) && target?.activeFrame === undefined;
         const textview = [DocumentType.RTF].includes(targetType) && target?.activeFrame === undefined;
+        const viewType = targetType === DocumentType.COL;
         const dataannos = false;
-        return { scrollable, pannable, temporal, clippable, dataview, textview, poslayoutview, dataannos };
+        return { scrollable, pannable, viewType, temporal, clippable, dataview, textview, poslayoutview, dataannos };
     }
 
     @action
@@ -366,6 +368,13 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 changed = true;
             }
         }
+        if (pinDataTypes.viewType && activeItem.presPinViewType !== undefined) {
+            if (bestTarget._viewType !== activeItem.presPinViewType) {
+                bestTarget._viewType = activeItem.presPinViewType;
+                changed = true;
+            }
+        }
+
         if (pinDataTypes.scrollable) {
             if (bestTarget._scrollTop !== activeItem.presPinViewScroll) {
                 bestTarget._scrollTop = activeItem.presPinViewScroll;
@@ -454,6 +463,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 pinProps.pinData.scrollable ||
                 pinProps.pinData.temporal ||
                 pinProps.pinData.pannable ||
+                pinProps.pinData.viewType ||
                 pinProps.pinData.clippable ||
                 pinProps.pinData.dataview ||
                 pinProps.pinData.textview ||
@@ -472,6 +482,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             if (pinProps.pinData.scrollable) pinDoc.presPinViewScroll = targetDoc._scrollTop;
             if (pinProps.pinData.clippable) pinDoc.presPinClipWidth = targetDoc._clipWidth;
             if (pinProps.pinData.poslayoutview) pinDoc.presPinLayoutData = new List<string>(DocListCast(targetDoc.presData).map(d => JSON.stringify({ id: d[Id], x: NumCast(d.x), y: NumCast(d.y), w: NumCast(d._width), h: NumCast(d._height) })));
+            if (pinProps.pinData.viewType) pinDoc.presPinViewType = targetDoc._viewType;
             if (pinProps.pinData.pannable) {
                 pinDoc.presPinViewX = NumCast(targetDoc._panX);
                 pinDoc.presPinViewY = NumCast(targetDoc._panY);
-- 
cgit v1.2.3-70-g09d2

