diff options
| author | bobzel <zzzman@gmail.com> | 2022-06-30 15:13:54 -0400 |
|---|---|---|
| committer | bobzel <zzzman@gmail.com> | 2022-06-30 15:13:54 -0400 |
| commit | cf6de0bb501c2e3b64269494d6c0e0305c775eb3 (patch) | |
| tree | e041e9fd5136ae4d359b6d476bc9ae172e109f6b /src/client/views/collections/collectionFreeForm | |
| parent | bb02d3a052efdbf25d1069059a92b7a9d9cc1708 (diff) | |
| parent | ea6e63648b21c46672b1b7cb1da0cbaa6857d0c1 (diff) | |
Merge branch 'master' into parker
Diffstat (limited to 'src/client/views/collections/collectionFreeForm')
6 files changed, 427 insertions, 264 deletions
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx index 9fed82dae..9de2cfcf9 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx @@ -111,23 +111,25 @@ export function computerStarburstLayout( viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[], engineProps: any ) { + const mustFit = pivotDoc[WidthSym]() !== panelDim[0]; // if a panel size is set that's not the same as the pivot doc's size, then assume this is in a panel for a content fitting view (like a grid) in which case everything must be scaled to stay within the panel const docMap = new Map<string, PoolData>(); - const burstRadius = [NumCast(pivotDoc._starburstRadius, panelDim[0]), NumCast(pivotDoc._starburstRadius, panelDim[1])]; - const docScale = NumCast(pivotDoc._starburstDocScale); - const docSize = docScale * 100; // assume a icon sized at 100 - const scaleDim = [burstRadius[0] + docSize, burstRadius[1] + docSize]; + const docSize = mustFit ? panelDim[0] * .33 : 75; // assume an icon sized at 75 + const burstRadius = mustFit ? panelDim : [NumCast(pivotDoc._starburstRadius, panelDim[0]) - docSize, NumCast(pivotDoc._starburstRadius, panelDim[1]) - docSize]; + const scaleDim = [burstRadius[0] * 2 + docSize, burstRadius[1] * 2 + docSize]; childPairs.forEach(({ layout, data }, i) => { + const docSize = layout.layoutKey === "layout_icon" ? (mustFit ? panelDim[0] * .33 : 75) : 400; // assume a icon sized at 75 const deg = i / childPairs.length * Math.PI * 2; docMap.set(layout[Id], { - x: Math.cos(deg) * (burstRadius[0] / 3) - docScale * layout[WidthSym]() / 2, - y: Math.sin(deg) * (burstRadius[1] / 3) - docScale * layout[HeightSym]() / 2, - width: docScale * layout[WidthSym](), - height: docScale * layout[HeightSym](), + x: Math.cos(deg) * burstRadius[0] - docSize / 2, + y: Math.sin(deg) * burstRadius[1] - docSize * layout[HeightSym]() / layout[WidthSym]() / 2, + width: docSize,//layout[WidthSym](), + height: docSize * layout[HeightSym]() / layout[WidthSym](), + zIndex: NumCast(layout.zIndex), pair: { layout, data }, replica: "" }); }); - const divider = { type: "div", color: "transparent", x: -burstRadius[0] / 3, y: 0, width: 15, height: 15, payload: undefined }; + const divider = { type: "div", color: "transparent", x: -burstRadius[0], y: 0, width: 15, height: 15, payload: undefined }; return normalizeResults(scaleDim, 12, docMap, poolData, viewDefsToJSX, [], 0, [divider]); } @@ -145,7 +147,7 @@ export function computePivotLayout( const pivotColumnGroups = new Map<FieldResult<Field>, PivotColumn>(); let nonNumbers = 0; - const pivotFieldKey = toLabel(engineProps?.pivotField ?? pivotDoc._pivotField); + const pivotFieldKey = toLabel(engineProps?.pivotField ?? pivotDoc._pivotField) || "author"; childPairs.map(pair => { const lval = pivotFieldKey === "#" || pivotFieldKey === "tags" ? Array.from(Object.keys(Doc.GetProto(pair.layout))).filter(k => k.startsWith("#")).map(k => k.substring(1)) : Cast(pair.layout[pivotFieldKey], listSpec("string"), null); @@ -265,7 +267,13 @@ export function computePivotLayout( }); const dividers = sortedPivotKeys.map((key, i) => - ({ type: "div", color: "lightGray", x: i * pivotAxisWidth * (numCols * expander + gap) - pivotAxisWidth * (expander - 1) / 2, y: -maxColHeight + pivotAxisWidth, width: pivotAxisWidth * numCols * expander, height: maxColHeight, payload: pivotColumnGroups.get(key)!.filters })); + ({ + type: "div", color: "lightGray", + x: i * pivotAxisWidth * (numCols * expander + gap) - pivotAxisWidth * (expander - 1) / 2, + y: -maxColHeight + pivotAxisWidth, width: pivotAxisWidth * numCols * expander, + height: maxColHeight, + payload: pivotColumnGroups.get(key)!.filters + })); groupNames.push(...dividers); return normalizeResults(panelDim, max_text, docMap, poolData, viewDefsToJSX, groupNames, 0, []); } @@ -402,7 +410,7 @@ function normalizeResults( const grpEles = groupNames.map(gn => ({ x: gn.x, y: gn.y, width: gn.width, height: gn.height }) as ViewDefBounds); const docEles = Array.from(docMap.entries()).map(ele => ele[1]); const aggBounds = aggregateBounds(extras.concat(grpEles.concat(docEles.map(de => ({ ...de, type: "doc", payload: "" })))).filter(e => e.zIndex !== -99), 0, 0); - aggBounds.r = Math.max(minWidth, aggBounds.r - aggBounds.x); + aggBounds.r = aggBounds.x + Math.max(minWidth, aggBounds.r - aggBounds.x); const wscale = panelDim[0] / (aggBounds.r - aggBounds.x); let scale = wscale * (aggBounds.b - aggBounds.y) > panelDim[1] ? (panelDim[1]) / (aggBounds.b - aggBounds.y) : wscale; if (Number.isNaN(scale)) scale = 1; diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx index f5a5492e3..5f890c810 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx @@ -3,7 +3,7 @@ import { observer } from "mobx-react"; import { Doc, Field } from "../../../../fields/Doc"; import { Id } from "../../../../fields/FieldSymbols"; import { List } from "../../../../fields/List"; -import { NumCast } from "../../../../fields/Types"; +import { Cast, NumCast } from "../../../../fields/Types"; import { emptyFunction, setupMoveUpEvents, Utils } from '../../../../Utils'; import { LinkManager } from "../../../util/LinkManager"; import { SelectionManager } from "../../../util/SelectionManager"; @@ -30,7 +30,14 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo componentWillUnmount() { this._anchorDisposer?.(); } @action timeout = action(() => (Date.now() < this._start++ + 1000) && (this._timeout = setTimeout(this.timeout, 25))); componentDidMount() { - this._anchorDisposer = reaction(() => [this.props.A.props.ScreenToLocalTransform(), this.props.B.props.ScreenToLocalTransform()], + this._anchorDisposer = reaction(() => [ + this.props.A.props.ScreenToLocalTransform(), + Cast(Cast(Cast(this.props.A.rootDoc, Doc, null)?.anchor1, Doc, null)?.annotationOn, Doc, null)?.scrollTop, + Cast(Cast(Cast(this.props.A.rootDoc, Doc, null)?.anchor1, Doc, null)?.annotationOn, Doc, null)?._highlights, + this.props.B.props.ScreenToLocalTransform(), + Cast(Cast(Cast(this.props.A.rootDoc, Doc, null)?.anchor2, Doc, null)?.annotationOn, Doc, null)?.scrollTop, + Cast(Cast(Cast(this.props.A.rootDoc, Doc, null)?.anchor2, Doc, null)?.annotationOn, Doc, null)?._highlights, + ], action(() => { this._start = Date.now(); this._timeout && clearTimeout(this._timeout); @@ -45,14 +52,10 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo if (SnappingManager.GetIsDragging() || !A.ContentDiv || !B.ContentDiv) return; setTimeout(action(() => this._opacity = 0.75), 0); // since the render code depends on querying the Dom through getBoudndingClientRect, we need to delay triggering render() setTimeout(action(() => (!LinkDocs.length || !linkDoc.linkDisplay) && (this._opacity = 0.05)), 750); // this will unhighlight the link line. - const acont = A.ContentDiv.getElementsByClassName("linkAnchorBox-cont"); - const bcont = B.ContentDiv.getElementsByClassName("linkAnchorBox-cont"); - const adiv = acont.length ? acont[0] : A.ContentDiv; - const bdiv = bcont.length ? bcont[0] : B.ContentDiv; - const a = adiv.getBoundingClientRect(); - const b = bdiv.getBoundingClientRect(); - const { left: aleft, top: atop, width: awidth, height: aheight } = adiv.parentElement!.getBoundingClientRect(); - const { left: bleft, top: btop, width: bwidth, height: bheight } = bdiv.parentElement!.getBoundingClientRect(); + const a = A.ContentDiv.getBoundingClientRect(); + const b = B.ContentDiv.getBoundingClientRect(); + const { left: aleft, top: atop, width: awidth, height: aheight } = A.ContentDiv.parentElement!.getBoundingClientRect(); + const { left: bleft, top: btop, width: bwidth, height: bheight } = B.ContentDiv.parentElement!.getBoundingClientRect(); const apt = Utils.closestPtBetweenRectangles(aleft, atop, awidth, aheight, bleft, btop, bwidth, bheight, a.left + a.width / 2, a.top + a.height / 2); const bpt = Utils.closestPtBetweenRectangles(bleft, btop, bwidth, bheight, aleft, atop, awidth, aheight, apt.point.x, apt.point.y); @@ -75,6 +78,8 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo const mpy = mp[1] / A.props.PanelHeight(); if (mpx >= 0 && mpx <= 1) linkDoc.anchor1_x = mpx * 100; if (mpy >= 0 && mpy <= 1) linkDoc.anchor1_y = mpy * 100; + if (getComputedStyle(targetAhyperlink).fontSize === "0px") linkDoc.opacity = 0; + else linkDoc.opacity = 1; } if (!targetBhyperlink) { if (linkDoc.linkAutoMove) { @@ -88,6 +93,8 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo const mpy = mp[1] / B.props.PanelHeight(); if (mpx >= 0 && mpx <= 1) linkDoc.anchor2_x = mpx * 100; if (mpy >= 0 && mpy <= 1) linkDoc.anchor2_y = mpy * 100; + if (getComputedStyle(targetBhyperlink).fontSize === "0px") linkDoc.opacity = 0; + else linkDoc.opacity = 1; } } @@ -154,25 +161,6 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo this.toggleProperties(); } - // componentToHex = (c: number) => { - // let hex = c.toString(16); - // return hex.length == 1 ? "0" + hex : hex; - // } - - // rgbToHex = (rgbString: string) => { - // if (rgbString != "black") { - // const splitString = rgbString.split(/rgb|\(|\)|,| /) - // let values: number[] = [] - // splitString.forEach(elt => { - // if (elt) { - // values.push(parseInt(elt)) - // } - // }) - // return "#" + this.componentToHex(values[0]) + this.componentToHex(values[1]) + this.componentToHex(values[2]); - // } - // return "#000000" - // } - @computed.struct get renderData() { this._start; SnappingManager.GetIsDragging(); const { A, B, LinkDocs } = this.props; @@ -190,32 +178,18 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo if (!a.width || !b.width) return undefined; const aDocBounds = (A.props as any).DocumentView?.().getBounds() || { left: 0, right: 0, top: 0, bottom: 0 }; const bDocBounds = (B.props as any).DocumentView?.().getBounds() || { left: 0, right: 0, top: 0, bottom: 0 }; - const acentX = (a.left + a.right) / 2; - const acentY = (a.top + a.bottom) / 2; - const bcentX = (b.left + b.right) / 2; - const bcentY = (b.top + b.bottom) / 2; - const pt1Arc = ((acentX - aDocBounds.left) > 0.1 && (aDocBounds.right - acentX) > 0.1) || - ((acentY - aDocBounds.top) > 0.1 && (aDocBounds.bottom - acentY) > 0.1); - const pt2Arc = ((bcentX - bDocBounds.left) > 0.1 && (bDocBounds.right - bcentX) > 0.1) || - ((bcentY - bDocBounds.top) > 0.1 && (bDocBounds.bottom - bcentY) > 0.1); - const atop2 = this.visibleY(adiv); - const btop2 = this.visibleY(bdiv); const aleft = this.visibleX(adiv); const bleft = this.visibleX(bdiv); const clipped = aleft !== a.left || atop !== a.top || bleft !== b.left || btop !== b.top; const pt1 = [aleft + a.width / 2, atop + a.height / 2]; const pt2 = [bleft + b.width / 2, btop + b.width / 2]; - const pt1vec = [pt1[0] - (aDocBounds.left + aDocBounds.right) / 2, pt1[1] - (aDocBounds.top + aDocBounds.bottom) / 2]; - const pt2vec = [pt2[0] - (bDocBounds.left + bDocBounds.right) / 2, pt2[1] - (bDocBounds.top + bDocBounds.bottom) / 2]; + const pt1vec = [(bDocBounds.left + bDocBounds.right) / 2 - pt1[0], (bDocBounds.top + bDocBounds.bottom) / 2 - pt1[1]]; + const pt2vec = [(aDocBounds.left + aDocBounds.right) / 2 - pt2[0], (aDocBounds.top + aDocBounds.bottom) / 2 - pt2[1]]; const pt1len = Math.sqrt((pt1vec[0] * pt1vec[0]) + (pt1vec[1] * pt1vec[1])); const pt2len = Math.sqrt((pt2vec[0] * pt2vec[0]) + (pt2vec[1] * pt2vec[1])); const ptlen = Math.sqrt((pt1[0] - pt2[0]) * (pt1[0] - pt2[0]) + (pt1[1] - pt2[1]) * (pt1[1] - pt2[1])) / 2; - const pt1norm = clipped ? [0, 0] : !pt1Arc ? [pt1vec[0] / pt1len * ptlen, pt1vec[1] / pt1len * ptlen] : - Math.abs(acentY - aDocBounds.top) < 0.01 || - Math.abs(acentY - aDocBounds.bottom) < 0.01 ? [0, (pt2[1] - pt1[1]) / 2] : [(pt2[0] - pt1[0]) / 2, 0]; - const pt2norm = clipped ? [0, 0] : !pt2Arc ? [pt2vec[0] / pt2len * ptlen, pt2vec[1] / pt2len * ptlen] : - Math.abs(bcentY - bDocBounds.top) < 0.01 || - Math.abs(bcentY - bDocBounds.bottom) < 0.01 ? [0, (pt1[1] - pt2[1]) / 2] : [(pt1[0] - pt2[0]) / 2, 0]; + const pt1norm = clipped ? [0, 0] : [pt1vec[0] / pt1len * ptlen, pt1vec[1] / pt1len * ptlen]; + const pt2norm = clipped ? [0, 0] : [pt2vec[0] / pt2len * ptlen, pt2vec[1] / pt2len * ptlen]; const pt1normlen = Math.sqrt(pt1norm[0] * pt1norm[0] + pt1norm[1] * pt1norm[1]) || 1; const pt2normlen = Math.sqrt(pt2norm[0] * pt2norm[0] + pt2norm[1] * pt2norm[1]) || 1; const pt1normalized = [pt1norm[0] / pt1normlen, pt1norm[1] / pt1normlen]; @@ -253,7 +227,7 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo this.props.LinkDocs[0].displayArrow = false; } - return !a.width || !b.width || ((!this.props.LinkDocs[0].linkDisplay) && !aActive && !bActive) ? (null) : (<> + return this.props.LinkDocs[0].opacity === 0 || !a.width || !b.width || ((!this.props.LinkDocs[0].linkDisplay) && !aActive && !bActive) ? (null) : (<> <defs> <marker id="arrowhead" markerWidth="4" markerHeight="3" refX="0" refY="1.5" orient="auto"> diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index e2ea81392..b9da4faa4 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -3,15 +3,16 @@ import { action, computed, IReactionDisposer, observable, reaction, runInAction import { observer } from "mobx-react"; import { computedFn } from "mobx-utils"; import { DateField } from "../../../../fields/DateField"; -import { Doc, HeightSym, Opt, StrListCast, WidthSym } from "../../../../fields/Doc"; +import { DataSym, Doc, DocListCast, HeightSym, Opt, StrListCast, WidthSym } from "../../../../fields/Doc"; import { Id } from "../../../../fields/FieldSymbols"; import { InkData, InkField, InkTool, PointData, Segment } from "../../../../fields/InkField"; import { List } from "../../../../fields/List"; import { ObjectField } from "../../../../fields/ObjectField"; import { RichTextField } from "../../../../fields/RichTextField"; -import { createSchema, listSpec } from "../../../../fields/Schema"; +import { listSpec } from "../../../../fields/Schema"; import { ScriptField } from "../../../../fields/ScriptField"; import { BoolCast, Cast, FieldValue, NumCast, ScriptCast, StrCast } from "../../../../fields/Types"; +import { ImageField } from "../../../../fields/URLField"; import { TraceMobx } from "../../../../fields/util"; import { GestureUtils } from "../../../../pen-gestures/GestureUtils"; import { aggregateBounds, emptyFunction, intersectRect, returnFalse, setupMoveUpEvents, Utils } from "../../../../Utils"; @@ -25,6 +26,8 @@ import { DragManager, dropActionType } from "../../../util/DragManager"; import { HistoryUtil } from "../../../util/History"; import { InteractionUtils } from "../../../util/InteractionUtils"; import { LinkManager } from "../../../util/LinkManager"; +import { RecordingApi } from "../../../util/RecordingApi"; +import { ScriptingGlobals } from "../../../util/ScriptingGlobals"; import { SearchUtil } from "../../../util/SearchUtil"; import { SelectionManager } from "../../../util/SelectionManager"; import { ColorScheme } from "../../../util/SettingsManager"; @@ -39,31 +42,24 @@ import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveIn import { LightboxView } from "../../LightboxView"; import { CollectionFreeFormDocumentView } from "../../nodes/CollectionFreeFormDocumentView"; import { DocFocusOptions, DocumentView, DocumentViewProps, ViewAdjustment, ViewSpecPrefix } from "../../nodes/DocumentView"; +import { FieldViewProps } from "../../nodes/FieldView"; import { FormattedTextBox } from "../../nodes/formattedText/FormattedTextBox"; import { PresBox } from "../../nodes/trails/PresBox"; -import { StyleLayers, StyleProp } from "../../StyleProvider"; +import { VideoBox } from "../../nodes/VideoBox"; +import { CreateImage } from "../../nodes/WebBoxRenderer"; +import { StyleProp } from "../../StyleProvider"; import { CollectionDockingView } from "../CollectionDockingView"; import { CollectionSubView } from "../CollectionSubView"; +import { TreeViewType } from "../CollectionTreeView"; import { CollectionViewType } from "../CollectionView"; +import { TabDocView } from "../TabDocView"; import { computePivotLayout, computerPassLayout, computerStarburstLayout, computeTimelineLayout, PoolData, ViewDefBounds, ViewDefResult } from "./CollectionFreeFormLayoutEngines"; import { CollectionFreeFormRemoteCursors } from "./CollectionFreeFormRemoteCursors"; import "./CollectionFreeFormView.scss"; import { MarqueeView } from "./MarqueeView"; import React = require("react"); +import e = require("connect-flash"); -export const panZoomSchema = createSchema({ - _panX: "number", - _panY: "number", - _currentTimecode: "number", - _timecodeToShow: "number", - _currentFrame: "number", - _useClusters: "boolean", - _viewTransition: "string", - _xPadding: "number", // pixels of padding on left/right of collectionfreeformview contents when fitToBox is set - _yPadding: "number", // pixels of padding on left/right of collectionfreeformview contents when fitToBox is set - _fitToBox: "boolean", - scrollHeight: "number" // this will be set when the collection is an annotation overlay for a PDF/Webpage -}); export type collectionFreeformViewProps = { annotationLayerHostsContent?: boolean; // whether to force scaling of content (needed by ImageBox) @@ -72,6 +68,7 @@ export type collectionFreeformViewProps = { scaleField?: string; noOverlay?: boolean; // used to suppress docs in the overlay (z) layer (ie, for minimap since overlay doesn't scale) engineProps?: any; + dontScaleFilter?: (doc: Doc) => boolean; // whether this collection should scale documents to fit their panel vs just scrolling them dontRenderDocuments?: boolean; // used for annotation overlays which need to distribute documents into different freeformviews with different mixBlendModes depending on whether they are transparent or not. // However, this screws up interactions since only the top layer gets events. so we render the freeformview a 3rd time with all documents in order to get interaction events (eg., marquee) but we don't actually want to display the documents. }; @@ -98,6 +95,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection private _lastTap = 0; private _batch: UndoManager.Batch | undefined = undefined; + // private isWritingMode: boolean = true; + // private writingModeDocs: Doc[] = []; + private get isAnnotationOverlay() { return this.props.isAnnotationOverlay; } private get scaleFieldKey() { return this.props.scaleField || "_viewScale"; } private get borderWidth() { return this.isAnnotationOverlay ? 0 : COLLECTION_BORDER_WIDTH; } @@ -114,12 +114,11 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection @observable _deleteList: DocumentView[] = []; @observable _timelineRef = React.createRef<Timeline>(); @observable _marqueeRef = React.createRef<HTMLDivElement>(); + @observable _marqueeViewRef = React.createRef<MarqueeView>(); @observable _keyframeEditing = false; @observable ChildDrag: DocumentView | undefined; // child document view being dragged. needed to update drop areas of groups when a group item is dragged. @computed get views() { return this._layoutElements.filter(ele => ele.bounds && !ele.bounds.z).map(ele => ele.ele); } - @computed get backgroundEvents() { return this.props.layerProvider?.(this.layoutDoc) === false && SnappingManager.GetIsDragging(); } - @computed get backgroundActive() { return this.props.layerProvider?.(this.layoutDoc) === false && this.props.isContentActive(); } @computed get fitToContentVals() { return { bounds: { ...this.contentBounds, cx: (this.contentBounds.x + this.contentBounds.r) / 2, cy: (this.contentBounds.y + this.contentBounds.b) / 2 }, @@ -128,16 +127,20 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection this.props.PanelWidth() / (this.contentBounds.r - this.contentBounds.x)) }; } - @computed get fitToContent() { return (this.props.fitContentsToDoc?.() || this.Document._fitToBox) && !this.isAnnotationOverlay; } - @computed get contentBounds() { return aggregateBounds(this._layoutElements.filter(e => e.bounds && !e.bounds.z).map(e => e.bounds!), NumCast(this.layoutDoc._xPadding, 10), NumCast(this.layoutDoc._yPadding, 10)); } - @computed get nativeWidth() { return this.fitToContent ? 0 : Doc.NativeWidth(this.Document); } - @computed get nativeHeight() { return this.fitToContent ? 0 : Doc.NativeHeight(this.Document); } + @computed get fitContentsToBox() { return (this.props.fitContentsToBox?.() || this.Document._fitContentsToBox) && !this.isAnnotationOverlay; } + @computed get contentBounds() { + const cb = Cast(this.rootDoc.contentBounds, listSpec("number")); + return cb ? {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!), NumCast(this.layoutDoc._xPadding, 10), NumCast(this.layoutDoc._yPadding, 10)); + } + @computed get nativeWidth() { return this.fitContentsToBox ? 0 : Doc.NativeWidth(this.Document, Cast(this.Document.resolvedDataDoc, Doc, null)); } + @computed get nativeHeight() { return this.fitContentsToBox ? 0 : Doc.NativeHeight(this.Document, Cast(this.Document.resolvedDataDoc, Doc, null)); } @computed get cachedCenteringShiftX(): number { - const scaling = this.fitToContent || !this.contentScaling ? 1 : this.contentScaling; + const scaling = this.fitContentsToBox || !this.contentScaling ? 1 : this.contentScaling; return this.props.isAnnotationOverlay ? 0 : this.props.PanelWidth() / 2 / scaling; // shift so pan position is at center of window for non-overlay collections } @computed get cachedCenteringShiftY(): number { - const scaling = this.fitToContent || !this.contentScaling ? 1 : this.contentScaling; + const scaling = this.fitContentsToBox || !this.contentScaling ? 1 : this.contentScaling; return this.props.isAnnotationOverlay ? 0 : this.props.PanelHeight() / 2 / scaling;// shift so pan position is at center of window for non-overlay collections } @computed get cachedGetLocalTransform(): Transform { @@ -150,8 +153,24 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection return this.getContainerTransform().translate(- this.cachedCenteringShiftX, - this.cachedCenteringShiftY).transform(this.cachedGetLocalTransform); } + changeKeyFrame = (back=false) => { + const currentFrame = Cast(this.Document._currentFrame, "number", null); + if (currentFrame === undefined) { + this.Document._currentFrame = 0; + CollectionFreeFormDocumentView.setupKeyframes(this.childDocs, 0); + } + if (back) { + CollectionFreeFormDocumentView.gotoKeyframe(this.childDocs.slice()); + this.Document._currentFrame = Math.max(0, (currentFrame || 0) - 1); + } else { + CollectionFreeFormDocumentView.updateKeyframe(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)); + } + } @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); elementFunc = () => this._layoutElements; @@ -161,11 +180,13 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection this.layoutDoc._panY = vals.bounds.cy; this.layoutDoc._viewScale = vals.scale; } - freeformData = (force?: boolean) => !this._firstRender && (this.fitToContent || force) ? this.fitToContentVals : undefined; - reverseNativeScaling = () => this.fitToContent ? true : false; - panX = () => this.freeformData()?.bounds.cx ?? NumCast(this.Document._panX); - panY = () => this.freeformData()?.bounds.cy ?? NumCast(this.Document._panY); - zoomScaling = () => (this.freeformData()?.scale ?? NumCast(this.Document[this.scaleFieldKey], 1)); + freeformData = (force?: boolean) => !this._firstRender && (this.fitContentsToBox || force) ? this.fitToContentVals : undefined; + reverseNativeScaling = () => this.fitContentsToBox ? true : false; + // panx, pany, zoomscale all attempt to get values first from the layout controller, then from the layout/dataDoc (or template layout doc), and finally from the resolved template data document. + // this search order, for example, allows icons of cropped images to find the panx/pany/zoom on the cropped image's data doc instead of the usual layout doc because the zoom/panX/panY define the cropped image + panX = () => this.freeformData()?.bounds.cx ?? NumCast(this.Document._panX, NumCast(Cast(this.Document.resolvedDataDoc, Doc, null)?.panX, 1)); + panY = () => this.freeformData()?.bounds.cy ?? NumCast(this.Document._panY, NumCast(Cast(this.Document.resolvedDataDoc, Doc, null)?.panY, 1)); + zoomScaling = () => this.freeformData()?.scale ?? NumCast(Doc.Layout(this.Document)[this.scaleFieldKey], NumCast(Cast(this.Document.resolvedDataDoc, Doc, null)?.[this.scaleFieldKey], 1)); contentTransform = () => !this.cachedCenteringShiftX && !this.cachedCenteringShiftY && this.zoomScaling() === 1 ? "" : `translate(${this.cachedCenteringShiftX}px, ${this.cachedCenteringShiftY}px) scale(${this.zoomScaling()}) translate(${-this.panX()}px, ${-this.panY()}px)`; getTransform = () => this.cachedGetTransform.copy(); getLocalTransform = () => this.cachedGetLocalTransform.copy(); @@ -218,7 +239,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection @action internalDocDrop(e: Event, de: DragManager.DropEvent, docDragData: DragManager.DocumentDragData, xp: number, yp: number) { - if (!de.embedKey && !this.ChildDrag && this.props.layerProvider?.(this.props.Document) !== false && this.props.Document._isGroup) return false; + if (!de.embedKey && !this.ChildDrag && this.rootDoc._isGroup) return false; if (!super.onInternalDrop(e, de)) return false; const refDoc = docDragData.droppedDocuments[0]; const [xpo, ypo] = this.getContainerTransform().transformPoint(de.x, de.y); @@ -247,7 +268,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection const nd = [Doc.NativeWidth(layoutDoc), Doc.NativeHeight(layoutDoc)]; layoutDoc._width = NumCast(layoutDoc._width, 300); layoutDoc._height = NumCast(layoutDoc._height, nd[0] && nd[1] ? nd[1] / nd[0] * NumCast(layoutDoc._width) : 300); - !StrListCast(d._layerTags).includes(StyleLayers.Background) && (d._raiseWhenDragged === undefined ? Doc.UserDoc()._raiseWhenDragged : d._raiseWhenDragged) && (d.zIndex = zsorted.length + 1 + i); // bringToFront + (d._raiseWhenDragged === undefined ? DragManager.GetRaiseWhenDragged(): d._raiseWhenDragged) && (d.zIndex = zsorted.length + 1 + i); // bringToFront } (docDragData.droppedDocuments.length === 1 || de.shiftKey) && this.updateClusterDocs(docDragData.droppedDocuments); @@ -275,7 +296,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection if (!linkDragData.linkDragView.props.CollectionFreeFormDocumentView?.() || linkDragData.dragDocument.context !== this.props.Document) { // if the source doc view's context isn't this same freeformcollectionlinkDragData.dragDocument.context === this.props.Document const source = Docs.Create.TextDocument("", { _width: 200, _height: 75, x: xp, y: yp, title: "dropped annotation" }); this.props.addDocument?.(source); - de.complete.linkDocument = DocUtils.MakeLink({ doc: source }, { doc: linkDragData.linkSourceGetAnchor() }, "doc annotation", ""); // TODODO this is where in text links get passed + de.complete.linkDocument = DocUtils.MakeLink({ doc: linkDragData.linkSourceGetAnchor() }, { doc: source }, "annotated by:annotation of", ""); // TODODO this is where in text links get passed } e.stopPropagation(); // do nothing if link is dropped into any freeform view parent of dragged document return true; @@ -414,8 +435,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection styleProp = colors[cluster % colors.length]; const set = this._clusterSets[cluster]?.filter(s => s.backgroundColor); // override the cluster color with an explicitly set color on a non-background document. then override that with an explicitly set color on a background document - set?.filter(s => !StrListCast(s._layerTags).includes(StyleLayers.Background)).map(s => styleProp = StrCast(s.backgroundColor)); - set?.filter(s => StrListCast(s._layerTags).includes(StyleLayers.Background)).map(s => styleProp = StrCast(s.backgroundColor)); + set?.map(s => styleProp = StrCast(s.backgroundColor)); } } //else if (doc && NumCast(doc.group, -1) !== -1) styleProp = "gray"; return styleProp; @@ -432,6 +452,19 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection } @action + onPenUp = (e: PointerEvent): void => { + if (!InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) { + document.removeEventListener("pointerup", this.onPenUp); + const currentCol = DocListCast(this.rootDoc.currentInkDoc) + const rootDocList = DocListCast(this.rootDoc.data); + currentCol.push(rootDocList[rootDocList.length - 1]); + console.log(currentCol); + + this._batch?.end(); + } + } + + @action onPointerDown = (e: React.PointerEvent): void => { this._downX = this._lastX = e.pageX; this._downY = this._lastY = e.pageY; @@ -440,9 +473,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection !this.props.Document._isGroup && // group freeforms don't pan when dragged -- instead let the event go through to allow the group itself to drag !InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE) && !InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) { - switch (CurrentUserUtils.SelectedTool) { + switch (CurrentUserUtils.ActiveTool) { case InkTool.Highlighter: - case InkTool.Pen: break; // the GestureOverlay handles ink stroke input -- either as gestures, or drying as ink strokes that are added to document views + break; + // TODO: nda - this where we want to create the new "writingDoc" collection that we add strokes to + case InkTool.Write: + break; + case InkTool.Pen: + break; // the GestureOverlay handles ink stroke input -- either as gestures, or drying as ink strokes that are added to document views case InkTool.Eraser: document.addEventListener("pointermove", this.onEraserMove); document.addEventListener("pointerup", this.onEraserUp); @@ -451,9 +489,11 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection e.preventDefault(); break; case InkTool.None: - this._hitCluster = this.pickCluster(this.getTransform().transformPoint(e.clientX, e.clientY)); - document.addEventListener("pointermove", this.onPointerMove); - document.addEventListener("pointerup", this.onPointerUp); + if (!(this.props.layoutEngine?.() || StrCast(this.layoutDoc._layoutEngine))) { + this._hitCluster = this.pickCluster(this.getTransform().transformPoint(e.clientX, e.clientY)); + document.addEventListener("pointermove", this.onPointerMove); + document.addEventListener("pointerup", this.onPointerUp); + } break; } } @@ -472,7 +512,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection this.addMoveListeners(); this.removeEndListeners(); this.addEndListeners(); - if (CurrentUserUtils.SelectedTool === InkTool.None) { + if (CurrentUserUtils.ActiveTool === InkTool.None) { this._lastX = pt.pageX; this._lastY = pt.pageY; e.preventDefault(); @@ -486,14 +526,20 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection } } + public unprocessedDocs: Doc[] = []; + public static collectionsWithUnprocessedInk = new Set<CollectionFreeFormView>(); @undoBatch onGesture = (e: Event, ge: GestureUtils.GestureEvent) => { switch (ge.gesture) { case GestureUtils.Gestures.Stroke: const points = ge.points; const B = this.getTransform().transformBounds(ge.bounds.left, ge.bounds.top, ge.bounds.width, ge.bounds.height); - const inkDoc = Docs.Create.InkDocument(ActiveInkColor(), CurrentUserUtils.SelectedTool, ActiveInkWidth(), ActiveInkBezierApprox(), ActiveFillColor(), ActiveArrowStart(), ActiveArrowEnd(), ActiveDash(), points, + const inkDoc = Docs.Create.InkDocument(ActiveInkColor(), CurrentUserUtils.ActiveTool, ActiveInkWidth(), ActiveInkBezierApprox(), ActiveFillColor(), ActiveArrowStart(), ActiveArrowEnd(), ActiveDash(), points, { title: "ink stroke", x: B.x - ActiveInkWidth() / 2, y: B.y - ActiveInkWidth() / 2, _width: B.width + ActiveInkWidth(), _height: B.height + ActiveInkWidth() }); + if (CurrentUserUtils.ActiveTool === InkTool.Write) { + this.unprocessedDocs.push(inkDoc); + CollectionFreeFormView.collectionsWithUnprocessedInk.add(this); + } this.addDocument(inkDoc); e.stopPropagation(); break; @@ -618,7 +664,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection } onClick = (e: React.MouseEvent) => { - if ((Math.abs(e.pageX - this._downX) < 3 && Math.abs(e.pageY - this._downY) < 3)) { + if (this.onBrowseClickHandler()) { + if (this.props.DocumentView?.()) { + this.onBrowseClickHandler().script.run({ documentView: this.props.DocumentView(), clientX: e.clientX, clientY: e.clientY }); + } + e.stopPropagation(); + e.preventDefault(); + } + else if ((Math.abs(e.pageX - this._downX) < 3 && Math.abs(e.pageY - this._downY) < 3)) { if (e.shiftKey) { if (Date.now() - this._lastTap < 300) { // reset zoom of freeform view to 1-to-1 on a shift + double click this.zoomSmoothlyAboutPt(this.getTransform().transformPoint(e.clientX, e.clientY), 1); @@ -673,6 +726,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection onPointerMove = (e: PointerEvent): void => { if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) return; if (InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) { + CurrentUserUtils.ActiveTool = InkTool.None; if (this.props.isContentActive(true)) e.stopPropagation(); } else if (!e.cancelBubble) { if (this.tryDragCluster(e, this._hitCluster)) { @@ -795,7 +849,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection if (!e.cancelBubble) { const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true); if (myTouches[0]) { - if (CurrentUserUtils.SelectedTool === InkTool.None) { + if (CurrentUserUtils.ActiveTool === InkTool.None) { if (this.tryDragCluster(e, this._hitCluster)) { e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers e.preventDefault(); @@ -803,6 +857,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection document.removeEventListener("pointerup", this.onPointerUp); return; } + // TODO: nda - this allows us to pan collections with finger -> only want to do this when collection is selected' this.pan(myTouches[0]); } } @@ -931,8 +986,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection if (deltaScale * invTransform.Scale > 20) { deltaScale = 20 / invTransform.Scale; } - if (deltaScale * invTransform.Scale < 1 && this.isAnnotationOverlay) { - deltaScale = 1 / invTransform.Scale; + if (deltaScale * invTransform.Scale < NumCast(this.rootDoc._viewScaleMin, 1) && this.isAnnotationOverlay) { + deltaScale = NumCast(this.rootDoc._viewScaleMin, 1) / invTransform.Scale; } const localTransform = this.getLocalTransform().inverse().scaleAbout(deltaScale, x, y); @@ -945,7 +1000,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection @action onPointerWheel = (e: React.WheelEvent): void => { - if (this.layoutDoc._lockedTransform || (this.layoutDoc._fitWidth && this.layoutDoc.nativeHeight) || CurrentUserUtils.OverlayDocs.includes(this.props.Document) || this.props.Document.treeViewOutlineMode === "outline") return; + if (this.layoutDoc._Transform || (this.layoutDoc._fitWidth && this.layoutDoc.nativeHeight) || DocListCast(CurrentUserUtils.MyOverlayDocs?.data).includes(this.props.Document) || this.props.Document.treeViewOutlineMode === TreeViewType.outline) return; if (!e.ctrlKey && this.props.Document.scrollHeight !== undefined) { // things that can scroll vertically should do that instead of zooming e.stopPropagation(); } @@ -958,6 +1013,16 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection @action setPan(panX: number, panY: number, panTime: number = 0, clamp: boolean = false) { + // set the current respective FFview to the tab being panned. + (Doc.UserDoc()?.presentationMode === 'recording') && RecordingApi.Instance.setRecordingFFView(this); + // TODO: make this based off the specific recording FFView + (Doc.UserDoc()?.presentationMode === 'none') && RecordingApi.Instance.setPlayFFView(this); + if (Doc.UserDoc()?.presentationMode === 'watching') { + RecordingApi.Instance.pauseVideoAndMovements(); + Doc.UserDoc().presentationMode = 'none'; + // RecordingApi.Instance.pauseMovements() + } + if (!this.isAnnotationOverlay && clamp) { // this section wraps the pan position, horizontally and/or vertically whenever the content is panned out of the viewing bounds const docs = this.childLayoutPairs.map(pair => pair.layout).filter(doc => doc instanceof Doc); @@ -981,11 +1046,16 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection else if (ranges.yrange.max <= (panY - panelDim[1] / 2)) panY = ranges.yrange.min - panelDim[1] / 2; } } - if (!this.layoutDoc._lockedTransform || LightboxView.LightboxDoc || CurrentUserUtils.OverlayDocs.includes(this.Document)) { + if (!this.layoutDoc._lockedTransform || LightboxView.LightboxDoc || DocListCast(CurrentUserUtils.MyOverlayDocs?.data).includes(this.Document)) { this._viewTransition = panTime; const scale = this.getLocalTransform().inverse().Scale; - const newPanX = Math.min((1 - 1 / scale) * this.nativeWidth, Math.max(0, panX)); - const newPanY = Math.min((this.props.Document.scrollHeight !== undefined ? NumCast(this.Document.scrollHeight) : (1 - 1 / scale) * this.nativeHeight), Math.max(0, panY)); + const minScale = NumCast(this.rootDoc._viewScaleMin, 1); + const minPanX = NumCast(this.rootDoc._panXMin, 0); + const minPanY = NumCast(this.rootDoc._panYMin, 0); + const newPanX = Math.min( + minPanX + (1 - minScale / scale) * NumCast(this.rootDoc._panXMax, this.nativeWidth), Math.max(minPanX, panX)); + const newPanY = Math.min((this.props.Document.scrollHeight !== undefined ? NumCast(this.Document.scrollHeight) : + minPanY + (1 - minScale / scale) * NumCast(this.rootDoc._panYMax, this.nativeHeight)), Math.max(minPanY, panY)); !this.Document._verticalScroll && (this.Document._panX = this.isAnnotationOverlay ? newPanX : panX); !this.Document._horizontalScroll && (this.Document._panY = this.isAnnotationOverlay ? newPanY : panY); } @@ -1006,13 +1076,13 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection @action bringToFront = (doc: Doc, sendToBack?: boolean) => { - if (sendToBack || StrListCast(doc._layerTags).includes(StyleLayers.Background)) { + if (sendToBack) { doc.zIndex = 0; } else if (doc.isInkMask) { doc.zIndex = 5000; } else { - const docs = this.childLayoutPairs.map(pair => pair.layout); - docs.slice().sort((doc1, doc2) => NumCast(doc1.zIndex) - NumCast(doc2.zIndex)); + const docs = this.childLayoutPairs.map(pair => pair.layout).slice(); + docs.sort((doc1, doc2) => NumCast(doc1.zIndex) - NumCast(doc2.zIndex)); let zlast = docs.length ? Math.max(docs.length, NumCast(docs[docs.length - 1].zIndex)) : 1; if (zlast - docs.length > 100) { for (let i = 0; i < docs.length; i++) doc.zIndex = i + 1; @@ -1051,29 +1121,28 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection HistoryUtil.pushState(state); } } - if (SelectionManager.Views().length !== 1 || SelectionManager.Views()[0].Document !== doc) { - SelectionManager.DeselectAll(); - } + // if (SelectionManager.Views().length !== 1 || SelectionManager.Views()[0].Document !== doc) { + // SelectionManager.DeselectAll(); + // } if (this.props.Document.scrollHeight || this.props.Document.scrollTop !== undefined) { this.props.focus(doc, options); } else { const xfToCollection = options?.docTransform ?? Transform.Identity(); - const layoutdoc = Doc.Layout(doc); - const savedState = { panX: NumCast(this.Document._panX), panY: NumCast(this.Document._panY), scale: this.Document[this.scaleFieldKey] }; + const savedState = { panX: NumCast(this.Document._panX), panY: NumCast(this.Document._panY), scale: options?.willZoom ? this.Document[this.scaleFieldKey] : undefined }; const newState = HistoryUtil.getState(); - const cantTransform = this.props.isAnnotationOverlay || ((this.rootDoc._isGroup || this.layoutDoc._lockedTransform) && !LightboxView.LightboxDoc); - const { panX, panY, scale } = cantTransform ? savedState : this.calculatePanIntoView(layoutdoc, xfToCollection, options?.willZoom ? options?.scale || .75 : undefined); + const cantTransform = /*this.props.isAnnotationOverlay ||*/ ((this.rootDoc._isGroup || this.layoutDoc._lockedTransform) && !LightboxView.LightboxDoc); + const { panX, panY, scale } = cantTransform ? savedState : this.calculatePanIntoView(doc, xfToCollection, options?.willZoom ? options?.scale || .75 : undefined); if (!cantTransform) { // only pan and zoom to focus on a document if the document is not an annotation in an annotation overlay collection newState.initializers![this.Document[Id]] = { panX: panX, panY: panY }; HistoryUtil.pushState(newState); } // focus on the document in the collection - const didMove = !cantTransform && !doc.z && (panX !== savedState.panX || panY !== savedState.panY || scale !== undefined); + const didMove = !cantTransform && !doc.z && (panX !== savedState.panX || panY !== savedState.panY || scale !== savedState.scale); const focusSpeed = options?.instant ? 0 : didMove ? (doc.focusSpeed !== undefined ? Number(doc.focusSpeed) : 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) { - this.setPan(panX, panY, focusSpeed, true); // docs that are floating in their collection can't be panned to from their collection -- need to propagate the pan to a parent freeform somehow scale && (this.Document[this.scaleFieldKey] = scale); + this.setPan(panX, panY, focusSpeed, true); // docs that are floating in their collection can't be panned to from their collection -- need to propagate the pan to a parent freeform somehow } const startTime = Date.now(); @@ -1109,23 +1178,25 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection } calculatePanIntoView = (doc: Doc, xf: Transform, scale?: number) => { - const pw = this.props.PanelWidth() / NumCast(this.layoutDoc._viewScale, 1); - const ph = this.props.PanelHeight() / NumCast(this.layoutDoc._viewScale, 1); + const layoutdoc = Doc.Layout(doc); const pt = xf.transformPoint(NumCast(doc.x), NumCast(doc.y)); - const pt2 = xf.transformPoint(NumCast(doc.x) + doc[WidthSym](), NumCast(doc.y) + doc[HeightSym]()); - const bounds = { left: pt[0], right: pt2[0], top: pt[1], bot: pt2[1] }; - const cx = NumCast(this.layoutDoc._panX); - const cy = NumCast(this.layoutDoc._panY); - const screen = { left: cx - pw / 2, right: cx + pw / 2, top: cy - ph / 2, bot: cy + ph / 2 }; + const pt2 = xf.transformPoint(NumCast(doc.x) + layoutdoc[WidthSym](), NumCast(doc.y) + layoutdoc[HeightSym]()); + const bounds = { left: pt[0], right: pt2[0], top: pt[1], bot: pt2[1], width: pt2[0] - pt[0], height: pt2[1] - pt[1] }; if (scale) { - const maxZoom = 2; // sets the limit for how far we will zoom. this is useful for preventing small text boxes from filling the screen. So probably needs to be more sophisticated to consider more about the target and context + const maxZoom = 5; // sets the limit for how far we will zoom. this is useful for preventing small text boxes from filling the screen. So probably needs to be more sophisticated to consider more about the target and context + const newScale = Math.min(maxZoom, 1 / (this.contentScaling || 1) * scale * Math.min(this.props.PanelWidth() / Math.abs(bounds.width), this.props.PanelHeight() / Math.abs(bounds.height))); return { - panX: (bounds.left + bounds.right) / 2, - panY: (bounds.top + bounds.bot) / 2, - scale: Math.min(maxZoom, scale * Math.min(this.props.PanelWidth() / Math.abs(pt2[0] - pt[0]), this.props.PanelHeight() / Math.abs(pt2[1] - pt[1]))) + panX: this.props.isAnnotationOverlay ? bounds.left - (Doc.NativeWidth(this.layoutDoc) / newScale - bounds.width) / 2 : (bounds.left + bounds.right) / 2, + panY: this.props.isAnnotationOverlay ? bounds.top - (Doc.NativeHeight(this.layoutDoc) / newScale - bounds.height) / 2 : (bounds.top + bounds.bot) / 2, + scale: newScale }; } + const pw = this.props.PanelWidth() / NumCast(this.layoutDoc._viewScale, 1); + const ph = this.props.PanelHeight() / NumCast(this.layoutDoc._viewScale, 1); + const cx = NumCast(this.layoutDoc._panX); + const cy = NumCast(this.layoutDoc._panY); + const screen = { left: cx - pw / 2, right: cx + pw / 2, top: cy - ph / 2, bot: cy + ph / 2 }; if ((screen.right - screen.left) < (bounds.right - bounds.left) || (screen.bot - screen.top) < (bounds.bot - bounds.top)) { return { @@ -1142,16 +1213,43 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection isContentActive = () => this.props.isSelected() || this.props.isContentActive(); - getChildDocView(entry: PoolData, renderIndex: number) { + @undoBatch + @action + onKeyDown = (e: React.KeyboardEvent, fieldProps: FieldViewProps) => { + const docView = fieldProps.DocumentView?.(); + if (docView && (e.metaKey || e.ctrlKey || e.altKey || docView.rootDoc._singleLine) && ["Tab", "Enter"].includes(e.key)) { + e.stopPropagation?.(); + const below = !e.altKey && e.key !== "Tab"; + const layoutKey = StrCast(docView.LayoutFieldKey); + const newDoc = Doc.MakeCopy(docView.rootDoc, true); + const dataField = docView.rootDoc[Doc.LayoutFieldKey(newDoc)]; + newDoc[DataSym][Doc.LayoutFieldKey(newDoc)] = dataField === undefined || Cast(dataField, listSpec(Doc), null)?.length !== undefined ? new List<Doc>([]) : undefined; + if (below) newDoc.y = NumCast(docView.rootDoc.y) + NumCast(docView.rootDoc._height) + 10; + else newDoc.x = NumCast(docView.rootDoc.x) + NumCast(docView.rootDoc._width) + 10; + if (layoutKey !== "layout" && docView.rootDoc[layoutKey] instanceof Doc) { + newDoc[layoutKey] = docView.rootDoc[layoutKey]; + } + Doc.GetProto(newDoc).text = undefined; + FormattedTextBox.SelectOnLoad = newDoc[Id]; + return this.addDocument?.(newDoc); + } + } + pointerEvents = () => { + const engine = this.props.layoutEngine?.() || StrCast(this.props.Document._layoutEngine); + const pointerEvents = this.props.isContentActive() === false ? "none" : + this.props.childPointerEvents ? "all" : + (this.props.viewDefDivClick || (engine === "pass" && !this.props.isSelected(true))) ? "none" : this.props.pointerEvents?.(); + return pointerEvents; + } + getChildDocView(entry: PoolData) { const childLayout = entry.pair.layout; const childData = entry.pair.data; - const engine = this.props.layoutEngine?.() || StrCast(this.props.Document._layoutEngine); return <CollectionFreeFormDocumentView key={childLayout[Id] + (entry.replica || "")} DataDoc={childData} Document={childLayout} renderDepth={this.props.renderDepth + 1} replica={entry.replica} - renderIndex={renderIndex} + suppressSetHeight={this.layoutEngine ? true : false} renderCutoffProvider={this.renderCutoffProvider} ContainingCollectionView={this.props.CollectionView} ContainingCollectionDoc={this.props.Document} @@ -1160,15 +1258,17 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection LayoutTemplateString={childLayout.z ? undefined : this.props.childLayoutString} rootSelected={childData ? this.rootSelected : returnFalse} onClick={this.onChildClickHandler} + onKey={this.onKeyDown} onDoubleClick={this.onChildDoubleClickHandler} + onBrowseClick={this.onBrowseClickHandler} ScreenToLocalTransform={childLayout.z ? this.getContainerTransform : this.getTransform} PanelWidth={childLayout[WidthSym]} PanelHeight={childLayout[HeightSym]} docFilters={this.childDocFilters} docRangeFilters={this.childDocRangeFilters} searchFilterDocs={this.searchFilterDocs} + isDocumentActive={this.props.childDocumentsActive?.() ? this.props.isDocumentActive : this.isContentActive} isContentActive={emptyFunction} - isDocumentActive={this.props.childDocumentsActive ? this.props.isDocumentActive : this.isContentActive} focus={this.focusDocument} addDocTab={this.addDocTab} addDocument={this.props.addDocument} @@ -1178,18 +1278,16 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection whenChildContentsActiveChanged={this.props.whenChildContentsActiveChanged} docViewPath={this.props.docViewPath} styleProvider={this.getClusterColor} - layerProvider={this.props.layerProvider} dataProvider={this.childDataProvider} sizeProvider={this.childSizeProvider} - freezeDimensions={this.props.childFreezeDimensions} dropAction={StrCast(this.props.Document.childDropAction) as dropActionType} bringToFront={this.bringToFront} showTitle={this.props.childShowTitle} + dontScaleFilter={this.props.dontScaleFilter} dontRegisterView={this.props.dontRenderDocuments || this.props.dontRegisterView} - pointerEvents={this.props.isContentActive() === false ? "none" : this.backgroundActive || this.props.childPointerEvents ? "all" : - (this.props.viewDefDivClick || (engine === "pass" && !this.props.isSelected(true))) ? "none" : this.props.pointerEvents} + pointerEvents={this.pointerEvents} jitterRotation={this.props.styleProvider?.(childLayout, this.props, StyleProp.JitterRotation) || 0} - //fitToBox={this.props.fitToBox || BoolCast(this.props.freezeChildDimensions)} // bcz: check this + //fitContentsToBox={this.props.fitContentsToBox || BoolCast(this.props.freezeChildDimensions)} // bcz: check this />; } addDocTab = action((doc: Doc, where: string) => { @@ -1294,10 +1392,11 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection return [] as ViewDefResult[]; } + @computed get layoutEngine() { return this.props.layoutEngine?.() || StrCast(this.layoutDoc._layoutEngine); } @computed get doInternalLayoutComputation() { TraceMobx(); const newPool = new Map<string, PoolData>(); - switch (this.props.layoutEngine?.() || StrCast(this.layoutDoc._layoutEngine)) { + switch (this.layoutEngine) { case "pass": return { newPool, computedElementData: this.doEngineLayout(newPool, computerPassLayout) }; case "timeline": return { newPool, computedElementData: this.doEngineLayout(newPool, computeTimelineLayout) }; case "pivot": return { newPool, computedElementData: this.doEngineLayout(newPool, computePivotLayout) }; @@ -1327,13 +1426,13 @@ 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) => elements.push({ - ele: this.getChildDocView(entry[1], i), + ele: this.getChildDocView(entry[1]), bounds: this.childDataProvider(entry[1].pair.layout, entry[1].replica) })); - if (this.props.isAnnotationOverlay) { // 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 - if (this.props.scaleField) this.props.Document[this.scaleFieldKey] = Math.min(1, NumCast(this.props.Document[this.scaleFieldKey], 1)); - else this.props.Document[this.scaleFieldKey] = Math.max(1, NumCast(this.props.Document[this.scaleFieldKey])); + 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 + if (this.props.scaleField) this.props.Document[this.scaleFieldKey] = Math.min(1, this.zoomScaling()); + else this.props.Document[this.scaleFieldKey] = Math.max(1,this.zoomScaling()); // NumCast(this.props.Document[this.scaleFieldKey])); } this.Document._useClusters && !this._clusterSets.length && this.childDocs.length && this.updateClusters(true); @@ -1410,6 +1509,77 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection })); } + static replaceCanvases(oldDiv: HTMLElement, newDiv: HTMLElement) { + if (oldDiv.childNodes && newDiv) { + for (let i = 0; i < oldDiv.childNodes.length; i++) { + this.replaceCanvases(oldDiv.childNodes[i] as HTMLElement, newDiv.childNodes[i] as HTMLElement); + } + } + if (oldDiv instanceof HTMLCanvasElement) { + if (oldDiv.className === "collectionFreeFormView-grid") { + const newCan = newDiv as HTMLCanvasElement; + const parEle = newCan.parentElement as HTMLElement; + parEle.removeChild(newCan); + parEle.appendChild(document.createElement('div')) + } else { + const canvas = oldDiv; + const img = document.createElement('img'); // create a Image Element + img.src = canvas.toDataURL(); //image source + img.style.width = canvas.style.width; + img.style.height = canvas.style.height; + const newCan = newDiv as HTMLCanvasElement; + const parEle = newCan.parentElement as HTMLElement; + parEle.removeChild(newCan); + parEle.appendChild(img); + } + } + } + + updateIcon = () => CollectionFreeFormView.UpdateIcon( + this.layoutDoc[Id] + "-icon" + (new Date()).getTime(), + this.props.docViewPath().lastElement().ContentDiv!, + this.layoutDoc[WidthSym](), this.layoutDoc[HeightSym](), + this.props.PanelWidth(), this.props.PanelHeight(), 0, 1, false, "", + (iconFile, nativeWidth, nativeHeight) => { + this.dataDoc.icon = new ImageField(iconFile); + this.dataDoc["icon-nativeWidth"] = nativeWidth; + this.dataDoc["icon-nativeHeight"] = nativeHeight; + }); + + public static UpdateIcon( + filename:string, docViewContent:HTMLElement, + width: number, height: number, + panelWidth:number, panelHeight: number, + scrollTop:number, + realNativeHeight: number, + noSuffix: boolean, + replaceRootFilename: string| undefined, + cb:(iconFile:string, nativeWidth:number, nativeHeight:number) => any) + { + const newDiv = docViewContent.cloneNode(true) as HTMLDivElement; + newDiv.style.width = width.toString(); + newDiv.style.height = height.toString(); + this.replaceCanvases(docViewContent, newDiv); + const htmlString = new XMLSerializer().serializeToString(newDiv); + const nativeWidth = width; + const nativeHeight = height; + return CreateImage( + Utils.prepend(""), + document.styleSheets, + htmlString, + nativeWidth, + nativeWidth * panelHeight / panelWidth, + scrollTop * panelHeight / realNativeHeight + ).then + (async (data_url: any) => { + const returnedFilename = await VideoBox.convertDataUri(data_url, filename, noSuffix, replaceRootFilename); + cb(returnedFilename as string, nativeWidth, nativeHeight); + }) + .catch(function (error: any) { + console.error('oops, something went wrong!', error); + }); + } + componentWillUnmount() { Object.values(this._disposers).forEach(disposer => disposer?.()); this._marqueeRef.current?.removeEventListener("dashDragAutoScroll", this.onDragAutoScroll as any); @@ -1473,35 +1643,41 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection const appearance = ContextMenu.Instance.findByDescription("Appearance..."); const appearanceItems = appearance && "subitems" in appearance ? appearance.subitems : []; appearanceItems.push({ description: "Reset View", event: () => { this.props.Document._panX = this.props.Document._panY = 0; this.props.Document[this.scaleFieldKey] = 1; }, icon: "compress-arrows-alt" }); - !Doc.UserDoc().noviceMode && Doc.UserDoc().defaultTextLayout && appearanceItems.push({ description: "Reset default note style", event: () => Doc.UserDoc().defaultTextLayout = undefined, icon: "eye" }); - appearanceItems.push({ description: `${this.fitToContent ? "Make Zoomable" : "Scale to Window"}`, event: () => this.Document._fitToBox = !this.fitToContent, icon: !this.fitToContent ? "expand-arrows-alt" : "compress-arrows-alt" }); + !Doc.noviceMode && Doc.UserDoc().defaultTextLayout && appearanceItems.push({ description: "Reset default note style", event: () => Doc.UserDoc().defaultTextLayout = undefined, icon: "eye" }); + appearanceItems.push({ description: `${this.fitContentsToBox ? "Make Zoomable" : "Scale to Window"}`, event: () => this.Document._fitContentsToBox = !this.fitContentsToBox, icon: !this.fitContentsToBox ? "expand-arrows-alt" : "compress-arrows-alt" }); + appearanceItems.push({ description: `Pin View`, event: () => TabDocView.PinDoc(this.rootDoc, {pinDocView:true, panelWidth: this.props.PanelWidth(), panelHeight:this.props.PanelHeight()}), icon: "map-pin" }); + //appearanceItems.push({ description: `update icon`, event: this.updateIcon, icon: "compress-arrows-alt" }); this.props.ContainingCollectionView && appearanceItems.push({ description: "Ungroup collection", event: this.promoteCollection, icon: "table" }); - !Doc.UserDoc().noviceMode ? appearanceItems.push({ description: "Arrange contents in grid", event: this.layoutDocsInGrid, icon: "table" }) : null; + + this.props.Document._isGroup && this.Document.transcription && appearanceItems.push({ description: "Ink to text", event: () => this.transcribeStrokes(false), icon: "font" }); + + // this.props.Document._isGroup && this.childDocs.filter(s => s.type === DocumentType.INK).length > 0 && appearanceItems.push({ description: "Ink to math", event: () => this.transcribeStrokes(true), icon: "square-root-alt" }); + + !Doc.noviceMode ? appearanceItems.push({ description: "Arrange contents in grid", event: this.layoutDocsInGrid, icon: "table" }) : null; !appearance && ContextMenu.Instance.addItem({ description: "Appearance...", subitems: appearanceItems, icon: "eye" }); const viewctrls = ContextMenu.Instance.findByDescription("UI Controls..."); const viewCtrlItems = viewctrls && "subitems" in viewctrls ? viewctrls.subitems : []; - - !Doc.UserDoc().noviceMode ? viewCtrlItems.push({ description: (Doc.UserDoc().showSnapLines ? "Hide" : "Show") + " Snap Lines", event: () => Doc.UserDoc().showSnapLines = !Doc.UserDoc().showSnapLines, icon: "compress-arrows-alt" }) : null; - !Doc.UserDoc().noviceMode ? viewCtrlItems.push({ description: (this.Document._useClusters ? "Hide" : "Show") + " Clusters", event: () => this.updateClusters(!this.Document._useClusters), icon: "braille" }) : null; + !Doc.noviceMode ? viewCtrlItems.push({ description: (SnappingManager.GetShowSnapLines() ? "Hide" : "Show") + " Snap Lines", event: () => SnappingManager.SetShowSnapLines(!SnappingManager.GetShowSnapLines()), icon: "compress-arrows-alt" }) : null; + !Doc.noviceMode ? viewCtrlItems.push({ description: (this.Document._useClusters ? "Hide" : "Show") + " Clusters", event: () => this.updateClusters(!this.Document._useClusters), icon: "braille" }) : null; !viewctrls && ContextMenu.Instance.addItem({ description: "UI Controls...", subitems: viewCtrlItems, icon: "eye" }); const options = ContextMenu.Instance.findByDescription("Options..."); const optionItems = options && "subitems" in options ? options.subitems : []; - !this.props.isAnnotationOverlay && !Doc.UserDoc().noviceMode && + !this.props.isAnnotationOverlay && !Doc.noviceMode && optionItems.push({ description: (this._showAnimTimeline ? "Close" : "Open") + " Animation Timeline", event: action(() => this._showAnimTimeline = !this._showAnimTimeline), icon: "eye" }); this.props.renderDepth && optionItems.push({ description: "Use Background Color as Default", event: () => Cast(Doc.UserDoc().emptyCollection, Doc, null)._backgroundColor = StrCast(this.layoutDoc._backgroundColor), icon: "palette" }); - if (!Doc.UserDoc().noviceMode) { + if (!Doc.noviceMode) { optionItems.push({ description: (!Doc.NativeWidth(this.layoutDoc) || !Doc.NativeHeight(this.layoutDoc) ? "Freeze" : "Unfreeze") + " Aspect", event: this.toggleNativeDimensions, icon: "snowflake" }); - optionItems.push({ description: `${this.Document._freeformLOD ? "Enable LOD" : "Disable LOD"}`, event: () => this.Document._freeformLOD = !this.Document._freeformLOD, icon: "table" }); } !options && ContextMenu.Instance.addItem({ description: "Options...", subitems: optionItems, icon: "eye" }); const mores = ContextMenu.Instance.findByDescription("More..."); const moreItems = mores && "subitems" in mores ? mores.subitems : []; - if (!Doc.UserDoc().noviceMode) { + if (!Doc.noviceMode) { + e.persist(); moreItems.push({ description: "Export collection", icon: "download", event: async () => Doc.Zip(this.props.Document) }); - moreItems.push({ description: "Import exported collection", icon: "upload", event: ({ x, y }) => this.importDocument(x, y) }); + moreItems.push({ description: "Import exported collection", icon: "upload", event: ({ x, y }) => this.importDocument(e.clientX, e.clientY) }); } !mores && ContextMenu.Instance.addItem({ description: "More...", subitems: moreItems, icon: "eye" }); } @@ -1510,32 +1686,32 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection const input = document.createElement("input"); input.type = "file"; input.accept = ".zip"; - input.onchange = async _e => { - const upload = Utils.prepend("/uploadDoc"); - const formData = new FormData(); - const file = input.files && input.files[0]; - if (file) { - formData.append('file', file); - formData.append('remap', "true"); - const response = await fetch(upload, { method: "POST", body: formData }); - const json = await response.json(); - if (json !== "error") { - const doc = await DocServer.GetRefField(json); - if (doc instanceof Doc) { - const [xx, yy] = this.props.ScreenToLocalTransform().transformPoint(x, y); - doc.x = xx, doc.y = yy; - this.props.addDocument?.(doc); - setTimeout(() => - SearchUtil.Search(`{!join from=id to=proto_i}id:link*`, true, {}).then(docs => { - docs.docs.forEach(d => LinkManager.Instance.addLink(d)); - }), 2000); // need to give solr some time to update so that this query will find any link docs we've added. - } - } - } + input.onchange = _e => { + input.files && Doc.importDocument(input.files[0]).then(doc => { + if (doc instanceof Doc) { + const [xx, yy] = this.getTransform().transformPoint(x, y); + doc.x = xx, doc.y = yy; + this.props.addDocument?.(doc);} + }); }; input.click(); } + @undoBatch + @action + transcribeStrokes = (math: boolean) => { + if (this.props.Document._isGroup && this.props.Document.transcription) { + if (!math) { + const text = StrCast(this.props.Document.transcription); + + const lines = text.split("\n"); + const height = 30 + 15 * lines.length; + + this.props.ContainingCollectionView?.addDocument(Docs.Create.TextDocument(text, { title: lines[0], x: NumCast(this.layoutDoc.x) + NumCast(this.layoutDoc._width) + 20, y: NumCast(this.layoutDoc.y), _width: 200, _height: height })); + } + } + } + @action setupDragLines = (snapToDraggedDoc: boolean = false) => { const activeDocs = this.getActiveDocuments(); @@ -1545,7 +1721,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection const isDocInView = (doc: Doc, rect: { left: number, top: number, width: number, height: number }) => intersectRect(docDims(doc), rect); const otherBounds = { left: this.panX(), top: this.panY(), width: Math.abs(size[0]), height: Math.abs(size[1]) }; - let snappableDocs = activeDocs.filter(doc => !StrListCast(doc._layerTags).includes(StyleLayers.Background) && doc.z === undefined && isDocInView(doc, selRect)); // first see if there are any foreground docs to snap to + let snappableDocs = activeDocs.filter(doc => doc.z === undefined && isDocInView(doc, selRect)); // first see if there are any foreground docs to snap to !snappableDocs.length && (snappableDocs = activeDocs.filter(doc => doc.z === undefined && isDocInView(doc, selRect))); // if not, see if there are background docs to snap to !snappableDocs.length && (snappableDocs = activeDocs.filter(doc => doc.z !== undefined && isDocInView(doc, otherBounds))); // if not, then why not snap to floating docs @@ -1598,6 +1774,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection TraceMobx(); return <MarqueeView {...this.props} + ref={this._marqueeViewRef} ungroup={this.props.Document._isGroup ? this.promoteCollection : undefined} nudge={this.isAnnotationOverlay || this.props.renderDepth > 0 ? undefined : this.nudge} addDocTab={this.addDocTab} @@ -1611,7 +1788,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection isAnnotationOverlay={this.isAnnotationOverlay}> <div className="marqueeView-div" ref={this._marqueeRef} style={{ opacity: this.props.dontRenderDocuments ? 0 : undefined }}> {this.layoutDoc._backgroundGridShow ? - <CollectionFreeFormBackgroundGrid + <div><CollectionFreeFormBackgroundGrid // bcz : UGHH don't know why, but if we don't wrap in a div, then PDF's don't render whenn taking snapshot of a dashboard and the background grid is on!!? PanelWidth={this.props.PanelWidth} PanelHeight={this.props.PanelHeight} panX={this.panX} @@ -1621,7 +1798,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection isAnnotationOverlay={this.isAnnotationOverlay} cachedCenteringShiftX={this.cachedCenteringShiftX} cachedCenteringShiftY={this.cachedCenteringShiftY} - /> : (null)} + /></div> : (null)} <CollectionFreeFormViewPannableContents isAnnotationOverlay={this.isAnnotationOverlay} isAnnotationOverlayScrollable={this.props.isAnnotationOverlayScrollable} @@ -1670,12 +1847,12 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection onContextMenu={this.onContextMenu} style={{ pointerEvents: this.props.Document.type === DocumentType.MARKER ? "none" : // bcz: ugh.. this is here to prevent markers, which render as freeform views, from grabbing events -- need a better approach. - this.backgroundEvents ? "all" : this.props.pointerEvents as any, + (SnappingManager.GetIsDragging() && this.childDocs.includes(DragManager.docsBeingDragged.lastElement())) ? "all" : this.props.pointerEvents?.() as any, transform: `scale(${this.contentScaling || 1})`, width: `${100 / (this.contentScaling || 1)}%`, height: this.isAnnotationOverlay && this.Document.scrollHeight ? NumCast(this.Document.scrollHeight) : `${100 / (this.contentScaling || 1)}%`// : this.isAnnotationOverlay ? (this.Document.scrollHeight ? this.Document.scrollHeight : "100%") : this.props.PanelHeight() }}> - {this._firstRender || (this.Document._freeformLOD && !this.props.isContentActive() && !this.props.isAnnotationOverlay && this.props.renderDepth > 0) ? + {this._firstRender ? this.placeholder : this.marqueeView} {this.props.noOverlay ? (null) : <CollectionFreeFormOverlayView elements={this.elementFunc} />} @@ -1698,7 +1875,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection </svg> </div>} - {this.props.Document._isGroup && SnappingManager.GetIsDragging() && (this.ChildDrag || this.props.layerProvider?.(this.props.Document) === false) ? + {this.props.Document._isGroup && SnappingManager.GetIsDragging() && this.ChildDrag ? <div className="collectionFreeForm-groupDropper" ref={this.createGroupEventsTarget} style={{ width: this.ChildDrag ? "10000" : "100%", height: this.ChildDrag ? "10000" : "100%", @@ -1907,4 +2084,23 @@ class CollectionFreeFormBackgroundGrid extends React.Component<CollectionFreeFor } }} />; } -}
\ No newline at end of file +} + +export function CollectionBrowseClick(dv: DocumentView, clientX: number, clientY: number) { + SelectionManager.DeselectAll(); + dv.props.focus(dv.props.Document, { + willZoom: true, afterFocus: async (didMove) => { + if (!didMove) { + const selfFfview = dv.ComponentView instanceof CollectionFreeFormView ? dv.ComponentView : undefined; + const parFfview = dv.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView; + const ffview = selfFfview && selfFfview.rootDoc[selfFfview.props.scaleField || "_viewScale"] !== 0.5 ? selfFfview : parFfview; // if focus doc is a freeform that is not at it's default 0.5 scale, then zoom out on it. Otherwise, zoom out on the parent ffview + ffview?.zoomSmoothlyAboutPt(ffview.getTransform().transformPoint(clientX, clientY), 0.5); + } + return ViewAdjustment.doNothing; + } + }); + Doc.linkFollowHighlight(dv?.props.Document, false); +} +ScriptingGlobals.add(CollectionBrowseClick); +ScriptingGlobals.add(function nextKeyFrame(readOnly: boolean) { !readOnly && (SelectionManager.Views()[0].ComponentView as CollectionFreeFormView)?.changeKeyFrame(); }); +ScriptingGlobals.add(function prevKeyFrame(readOnly: boolean) { !readOnly && (SelectionManager.Views()[0].ComponentView as CollectionFreeFormView)?.changeKeyFrame(true); });
\ No newline at end of file diff --git a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx index 1f59f9732..8a8b528f6 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx @@ -14,7 +14,6 @@ export class MarqueeOptionsMenu extends AntimodeMenu<AntimodeMenuProps> { public createCollection: (e: KeyboardEvent | React.PointerEvent | undefined, group?: boolean) => void = unimplementedFunction; public delete: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction; public summarize: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction; - public inkToText: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction; public showMarquee: () => void = unimplementedFunction; public hideMarquee: () => void = unimplementedFunction; public pinWithView: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction; @@ -64,16 +63,6 @@ export class MarqueeOptionsMenu extends AntimodeMenu<AntimodeMenuProps> { </button> </Tooltip>, ]; - if (false && !SelectionManager.Views().some(v => v.props.Document.type !== DocumentType.INK)) { - buttons.push( - <Tooltip key="inkToText" title={<div className="dash-tooltip">Change to Text</div>} placement="bottom"> - <button - className="antimodeMenu-button" - onPointerDown={this.inkToText}> - <FontAwesomeIcon icon="font" size="lg" /> - </button> - </Tooltip>); - } return this.getElement(buttons); } }
\ No newline at end of file diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.scss b/src/client/views/collections/collectionFreeForm/MarqueeView.scss index 62510ce9d..41e4d6b6a 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.scss +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.scss @@ -10,6 +10,7 @@ user-select: none; } + .marqueeView:focus-within { overflow: hidden; } diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index b10b0912f..ab8a34d5a 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -1,38 +1,36 @@ import { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; -import { AclAugment, AclAdmin, AclEdit, DataSym, Doc, Opt } from "../../../../fields/Doc"; +import { AclAdmin, AclAugment, AclEdit, DataSym, Doc, DocListCastAsync, Opt } from "../../../../fields/Doc"; import { Id } from "../../../../fields/FieldSymbols"; import { InkData, InkField, InkTool } from "../../../../fields/InkField"; import { List } from "../../../../fields/List"; import { RichTextField } from "../../../../fields/RichTextField"; import { SchemaHeaderField } from "../../../../fields/SchemaHeaderField"; -import { Cast, FieldValue, NumCast, StrCast } from "../../../../fields/Types"; +import { Cast, DocCast, FieldValue, NumCast, StrCast } from "../../../../fields/Types"; +import { ImageField } from "../../../../fields/URLField"; import { GetEffectiveAcl } from "../../../../fields/util"; -import { Utils, intersectRect, returnFalse } from "../../../../Utils"; +import { intersectRect, returnFalse, Utils } from "../../../../Utils"; import { CognitiveServices } from "../../../cognitive_services/CognitiveServices"; import { Docs, DocumentOptions, DocUtils } from "../../../documents/Documents"; import { DocumentType } from "../../../documents/DocumentTypes"; import { CurrentUserUtils } from "../../../util/CurrentUserUtils"; -import { DocumentManager } from "../../../util/DocumentManager"; import { SelectionManager } from "../../../util/SelectionManager"; import { Transform } from "../../../util/Transform"; import { undoBatch, UndoManager } from "../../../util/UndoManager"; import { ContextMenu } from "../../ContextMenu"; import { FormattedTextBox } from "../../nodes/formattedText/FormattedTextBox"; -import { PresBox } from "../../nodes/trails/PresBox"; +import { PinViewProps, PresBox } from "../../nodes/trails/PresBox"; import { PresMovement } from "../../nodes/trails/PresEnums"; +import { VideoBox } from "../../nodes/VideoBox"; +import { pasteImageBitmap } from "../../nodes/WebBoxRenderer"; import { PreviewCursor } from "../../PreviewCursor"; import { CollectionDockingView } from "../CollectionDockingView"; import { SubCollectionViewProps } from "../CollectionSubView"; -import { CollectionView } from "../CollectionView"; +import { TreeView } from "../TreeView"; import { MarqueeOptionsMenu } from "./MarqueeOptionsMenu"; import "./MarqueeView.scss"; import React = require("react"); -import { StyleLayers } from "../../StyleProvider"; -import { TreeView } from "../TreeView"; -import { VideoBox } from "../../nodes/VideoBox"; -import { ImageField, WebField } from "../../../../fields/URLField"; -import { pasteImageBitmap } from "../../nodes/WebBoxRenderer"; +import { TabDocView } from "../TabDocView"; interface MarqueeViewProps { getContainerTransform: () => Transform; @@ -46,6 +44,14 @@ interface MarqueeViewProps { ungroup?: () => void; setPreviewCursor?: (func: (x: number, y: number, drag: boolean, hide: boolean) => void) => void; } + +export interface MarqueeViewBounds { + left: number; + top: number; + width: number; + height: number; +} + @observer export class MarqueeView extends React.Component<SubCollectionViewProps & MarqueeViewProps> { @@ -54,15 +60,18 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque @observable _lastY: number = 0; @observable _downX: number = 0; @observable _downY: number = 0; - @observable _visible: boolean = false; + @observable _visible: boolean = false; // selection rentangle for marquee selection/free hand lasso is visible @observable _lassoPts: [number, number][] = []; @observable _lassoFreehand: boolean = false; @computed get Transform() { return this.props.getTransform(); } @computed get Bounds() { + // nda - ternary argument to transformPoint is returning the lower of the downX/Y and lastX/Y and passing in as args x,y const topLeft = this.Transform.transformPoint(this._downX < this._lastX ? this._downX : this._lastX, this._downY < this._lastY ? this._downY : this._lastY); + // nda - args to transformDirection is just x and y diff btw downX/Y and lastX/Y const size = this.Transform.transformDirection(this._lastX - this._downX, this._lastY - this._downY); - return { left: topLeft[0], top: topLeft[1], width: Math.abs(size[0]), height: Math.abs(size[1]) }; + const bounds:MarqueeViewBounds = { left: topLeft[0], top: topLeft[1], width: Math.abs(size[0]), height: Math.abs(size[1]) } + return bounds; } get inkDoc() { return this.props.Document; } get ink() { return Cast(this.props.Document.ink, InkField); } @@ -147,7 +156,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque })); } else if (e.key === "s" && e.ctrlKey) { e.preventDefault(); - const slide = Doc.copyDragFactory(Doc.UserDoc().emptySlide as Doc)!; + const slide = DocUtils.copyDragFactory(DocCast(Doc.UserDoc().emptySlide))!; slide.x = x; slide.y = y; FormattedTextBox.SelectOnLoad = slide[Id]; @@ -209,8 +218,8 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque this._downY = this._lastY = e.clientY; if (!(e.nativeEvent as any).marqueeHit) { (e.nativeEvent as any).marqueeHit = true; - // allow marquee if right click OR alt+left click - if (e.button === 2 || (e.button === 0 && e.altKey)) { + // 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.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. @@ -241,6 +250,9 @@ 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 @@ -261,11 +273,14 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque document.removeEventListener("pointerdown", hideMarquee); document.removeEventListener("wheel", hideMarquee); }; - if (!this._commandExecuted && (Math.abs(this.Bounds.height * this.Bounds.width) > 100)) { + if (PresBox.startMarquee) { + this.pinWithView(); + PresBox.startMarquee = false; + } + if (!this._commandExecuted && (Math.abs(this.Bounds.height * this.Bounds.width) > 100) && !PresBox.startMarquee) { MarqueeOptionsMenu.Instance.createCollection = this.collection; MarqueeOptionsMenu.Instance.delete = this.delete; MarqueeOptionsMenu.Instance.summarize = this.summary; - MarqueeOptionsMenu.Instance.inkToText = this.syntaxHighlight; MarqueeOptionsMenu.Instance.showMarquee = this.showMarquee; MarqueeOptionsMenu.Instance.hideMarquee = this.hideMarquee; MarqueeOptionsMenu.Instance.jumpTo(e.clientX, e.clientY); @@ -315,7 +330,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque onClick = (e: React.MouseEvent): void => { if (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD) { - if (CurrentUserUtils.SelectedTool === InkTool.None) { + if (CurrentUserUtils.ActiveTool === InkTool.None) { if (!(e.nativeEvent as any).marqueeHit) { (e.nativeEvent as any).marqueeHit = true; if (!this.props.trySelectCluster(e.shiftKey)) { @@ -348,16 +363,15 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque this.hideMarquee(); } - getCollection = action((selected: Doc[], creator: Opt<(documents: Array<Doc>, options: DocumentOptions, id?: string) => Doc>, layers: string[], makeGroup: Opt<boolean>) => { + getCollection = action((selected: Doc[], creator: Opt<(documents: Array<Doc>, options: DocumentOptions, id?: string) => Doc>, makeGroup: Opt<boolean>) => { const newCollection = creator ? creator(selected, { title: "nested stack", }) : ((doc: Doc) => { Doc.GetProto(doc).data = new List<Doc>(selected); Doc.GetProto(doc).title = makeGroup ? "grouping" : "nested freeform"; - !this.props.isAnnotationOverlay && Doc.AddDocToList(Cast(Doc.UserDoc().myFileOrphans, Doc, null), "data", Doc.GetProto(doc)); + !this.props.isAnnotationOverlay && Doc.AddDocToList(CurrentUserUtils.MyFileOrphans, undefined, Doc.GetProto(doc)); doc._panX = doc._panY = 0; return doc; })(Doc.MakeCopy(Doc.UserDoc().emptyCollection as Doc, true)); newCollection.system = undefined; - newCollection._layerTags = new List<string>(layers); newCollection._width = this.Bounds.width; newCollection._height = this.Bounds.height; newCollection._isGroup = makeGroup; @@ -374,47 +388,30 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque const selected = this.marqueeSelect(false); SelectionManager.DeselectAll(); selected.forEach(d => this.props.removeDocument?.(d)); - const newCollection = DocUtils.pileup(selected, this.Bounds.left + this.Bounds.width / 2, this.Bounds.top + this.Bounds.height / 2); + const newCollection = DocUtils.pileup(selected, this.Bounds.left + this.Bounds.width / 2, this.Bounds.top + this.Bounds.height / 2)!; this.props.addDocument?.(newCollection); this.props.selectDocuments([newCollection]); MarqueeOptionsMenu.Instance.fadeOut(true); this.hideMarquee(); } + /** + * This triggers the TabDocView.PinDoc method which is the universal method + * used to pin documents to the currently active presentation trail. + * + * This one is unique in that it includes the bounds associated with marquee view. + */ @undoBatch @action - pinWithView = (e: KeyboardEvent | React.PointerEvent | undefined) => { - const doc = this.props.Document; - const curPres = Cast(Doc.UserDoc().activePresentation, Doc) as Doc; - if (curPres) { - if (doc === curPres) { alert("Cannot pin presentation document to itself"); return; } - const pinDoc = Doc.MakeAlias(doc); - pinDoc.presentationTargetDoc = doc; - pinDoc.presMovement = PresMovement.Zoom; - pinDoc.groupWithUp = false; - pinDoc.context = curPres; - pinDoc.title = doc.title + " - Slide"; - const presArray = PresBox.Instance?.sortArray(); - const size = PresBox.Instance?._selectedArray.size; - const presSelected = presArray && size ? presArray[size - 1] : undefined; - Doc.AddDocToList(curPres, "data", pinDoc, presSelected); - if (curPres.expandBoolean) pinDoc.presExpandInlineButton = true; - if (!DocumentManager.Instance.getDocumentView(curPres)) { - CollectionDockingView.AddSplit(curPres, "right"); - } - PresBox.Instance?._selectedArray.clear(); - pinDoc && PresBox.Instance?._selectedArray.set(pinDoc, undefined); //Updates selected array - const index = PresBox.Instance?.childDocs.indexOf(pinDoc); - index && (curPres._itemIndex = index); - if (e instanceof KeyboardEvent ? e.key === "c" : true) { - const scale = Math.min(this.props.PanelWidth() / this.Bounds.width, this.props.PanelHeight() / this.Bounds.height); - pinDoc.presPinView = true; - pinDoc.presPinViewX = this.Bounds.left + this.Bounds.width / 2; - pinDoc.presPinViewY = this.Bounds.top + this.Bounds.height / 2; - pinDoc.presPinViewScale = scale; - } - } - MarqueeOptionsMenu.Instance.fadeOut(true); + pinWithView = async () => { + const scale = Math.min(this.props.PanelWidth() / this.Bounds.width, this.props.PanelHeight() / this.Bounds.height); + const doc = this.props.Document; + const viewOptions:PinViewProps = { + bounds: this.Bounds, + scale: scale + }; + TabDocView.PinDoc(doc, {pinWithView: viewOptions}); + MarqueeOptionsMenu.Instance.fadeOut(true); this.hideMarquee(); } @@ -437,7 +434,8 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque })); this.props.removeDocument?.(selected); } - const newCollection = this.getCollection(selected, (e as KeyboardEvent)?.key === "t" ? Docs.Create.StackingDocument : undefined, [], group); + // TODO: nda - this is the code to actually get a new grouped collection + const newCollection = this.getCollection(selected, (e as KeyboardEvent)?.key === "t" ? Docs.Create.StackingDocument : undefined, group); this.props.addDocument?.(newCollection); this.props.selectDocuments([newCollection]); MarqueeOptionsMenu.Instance.fadeOut(true); @@ -519,22 +517,19 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque d.y = NumCast(d.y) - this.Bounds.top; return d; }); - const summary = Docs.Create.TextDocument("", { x: this.Bounds.left, y: this.Bounds.top, _width: 200, _height: 200, _fitToBox: true, _showSidebar: true, title: "overview" }); - const portal = Doc.MakeAlias(summary); - Doc.GetProto(summary)[Doc.LayoutFieldKey(summary) + "-annotations"] = new List<Doc>(selected); - Doc.GetProto(summary).layout_portal = CollectionView.LayoutString(Doc.LayoutFieldKey(summary) + "-annotations"); - summary._backgroundColor = "#e2ad32"; - portal.layoutKey = "layout_portal"; - portal.title = "document collection"; - DocUtils.MakeLink({ doc: summary }, { doc: portal }, "summarizing", ""); + const summary = Docs.Create.TextDocument("", { backgroundColor: "#e2ad32", x: this.Bounds.left, y: this.Bounds.top, isPushpin: true, _width: 200, _height: 200, _fitContentsToBox: true, _showSidebar: true, title: "overview" }); + const portal = Docs.Create.FreeformDocument(selected, { x: this.Bounds.left + 200, y: this.Bounds.top, isGroup: true, backgroundColor: "transparent" }); + DocUtils.MakeLink({ doc: summary }, { doc: portal }, "summary of:summarized by", ""); + portal.hidden = true; + this.props.addDocument?.(portal); this.props.addLiveTextDocument(summary); MarqueeOptionsMenu.Instance.fadeOut(true); } @action background = (e: KeyboardEvent | React.PointerEvent | undefined) => { - const newCollection = this.getCollection([], undefined, [StyleLayers.Background], undefined); + const newCollection = this.getCollection([], undefined, undefined); this.props.addDocument?.(newCollection); MarqueeOptionsMenu.Instance.fadeOut(true); this.hideMarquee(); @@ -620,7 +615,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque (this.touchesLine(bounds) || this.boundingShape(bounds)) && selection.push(doc); } }; - this.props.activeDocuments().filter(doc => this.props.layerProvider?.(doc) !== false && !doc.z).map(selectFunc); + this.props.activeDocuments().filter(doc => !doc.z && !doc._lockedPosition).map(selectFunc); if (!selection.length && selectBackgrounds) this.props.activeDocuments().filter(doc => doc.z === undefined).map(selectFunc); if (!selection.length) this.props.activeDocuments().filter(doc => doc.z !== undefined).map(selectFunc); return selection; @@ -648,7 +643,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque return <div className="marqueeView" style={{ overflow: StrCast(this.props.Document._overflow), - cursor: CurrentUserUtils.SelectedTool === InkTool.Pen ? "crosshair" : "pointer" + cursor: [InkTool.Pen, InkTool.Write].includes(CurrentUserUtils.ActiveTool) || this._visible || PresBox.startMarquee ? "crosshair" : "pointer" }} onDragOver={e => e.preventDefault()} |
