diff options
Diffstat (limited to 'src/client/views/collections/collectionFreeForm')
6 files changed, 310 insertions, 508 deletions
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss index 8cbda310a..858719a08 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss @@ -1,7 +1,6 @@ .collectionfreeformlinkview-linkLine { stroke: black; opacity: 0.8; - pointer-events: all; stroke-width: 3px; transition: opacity 0.5s ease-in; fill: transparent; diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx index c81bd068c..ae5688b48 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx @@ -1,14 +1,14 @@ +import { action, computed, IReactionDisposer, observable, reaction } from "mobx"; import { observer } from "mobx-react"; import { Doc } from "../../../../fields/Doc"; -import { Utils, setupMoveUpEvents, emptyFunction, returnFalse } from '../../../../Utils'; +import { Id } from "../../../../fields/FieldSymbols"; +import { NumCast, StrCast } from "../../../../fields/Types"; +import { emptyFunction, setupMoveUpEvents, Utils } from '../../../../Utils'; +import { DocumentType } from "../../../documents/DocumentTypes"; +import { SnappingManager } from "../../../util/SnappingManager"; import { DocumentView } from "../../nodes/DocumentView"; import "./CollectionFreeFormLinkView.scss"; import React = require("react"); -import { DocumentType } from "../../../documents/DocumentTypes"; -import { observable, action, reaction, IReactionDisposer, trace, computed } from "mobx"; -import { StrCast, Cast, NumCast } from "../../../../fields/Types"; -import { Id } from "../../../../fields/FieldSymbols"; -import { SnappingManager } from "../../../util/SnappingManager"; export interface CollectionFreeFormLinkViewProps { A: DocumentView; @@ -103,7 +103,7 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo const top = rect.top, height = rect.height; var el = el.parentNode; while (el && el !== document.body) { - rect = el?.getBoundingClientRect(); + rect = el.getBoundingClientRect?.(); if (rect?.width) { if (top <= rect.bottom === false && getComputedStyle(el).overflow === "hidden") return rect.bottom; // Check if the element is out of view due to a container scrolling diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx index 6a1a41ca7..4dab8f15b 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx @@ -1,15 +1,14 @@ -import { computed, trace } from "mobx"; +import { computed } from "mobx"; import { observer } from "mobx-react"; import { Doc } from "../../../../fields/Doc"; import { Id } from "../../../../fields/FieldSymbols"; +import { Utils } from "../../../../Utils"; +import { DocumentType } from "../../../documents/DocumentTypes"; import { DocumentManager } from "../../../util/DocumentManager"; import { DocumentView } from "../../nodes/DocumentView"; import "./CollectionFreeFormLinksView.scss"; import { CollectionFreeFormLinkView } from "./CollectionFreeFormLinkView"; import React = require("react"); -import { Utils, emptyFunction } from "../../../../Utils"; -import { DocumentType } from "../../../documents/DocumentTypes"; -import { SnappingManager } from "../../../util/SnappingManager"; @observer export class CollectionFreeFormLinksView extends React.Component { @@ -28,10 +27,8 @@ export class CollectionFreeFormLinksView extends React.Component { } return drawnPairs; }, [] as { a: DocumentView, b: DocumentView, l: Doc[] }[]); - return connections.filter(c => - c.a.props.Document.type === DocumentType.LINK - && !c.a.props.treeViewDoc?.treeViewHideLinkLines && !c.b.props.treeViewDoc?.treeViewHideLinkLines - ).map(c => <CollectionFreeFormLinkView key={Utils.GenerateGuid()} A={c.a} B={c.b} LinkDocs={c.l} />); + return connections.filter(c => c.a.props.Document.type === DocumentType.LINK) + .map(c => <CollectionFreeFormLinkView key={Utils.GenerateGuid()} A={c.a} B={c.b} LinkDocs={c.l} />); } render() { diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx index 548ad78a5..9f6938e67 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx @@ -1,16 +1,16 @@ +import { computed } from "mobx"; import { observer } from "mobx-react"; import * as mobxUtils from 'mobx-utils'; import CursorField from "../../../../fields/CursorField"; +import { FieldResult } from "../../../../fields/Doc"; +import { List } from "../../../../fields/List"; import { listSpec } from "../../../../fields/Schema"; import { Cast } from "../../../../fields/Types"; import { CurrentUserUtils } from "../../../util/CurrentUserUtils"; -import { CollectionViewProps } from "../CollectionSubView"; +import { CollectionViewProps } from "../CollectionView"; import "./CollectionFreeFormView.scss"; import React = require("react"); import v5 = require("uuid/v5"); -import { computed } from "mobx"; -import { FieldResult } from "../../../../fields/Doc"; -import { List } from "../../../../fields/List"; @observer export class CollectionFreeFormRemoteCursors extends React.Component<CollectionViewProps> { diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 0ba13192d..f934fcd92 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -1,7 +1,7 @@ -import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction } from "mobx"; +import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction, trace } from "mobx"; import { observer } from "mobx-react"; import { computedFn } from "mobx-utils"; -import { Doc, DocListCast, HeightSym, Opt, WidthSym } from "../../../../fields/Doc"; +import { Doc, DocListCast, HeightSym, Opt, WidthSym, StrListCast } from "../../../../fields/Doc"; import { collectionSchema, documentSchema } from "../../../../fields/documentSchemas"; import { Id } from "../../../../fields/FieldSymbols"; import { InkData, InkField, InkTool } from "../../../../fields/InkField"; @@ -12,7 +12,7 @@ import { ScriptField } from "../../../../fields/ScriptField"; import { BoolCast, Cast, FieldValue, NumCast, ScriptCast, StrCast } from "../../../../fields/Types"; import { TraceMobx } from "../../../../fields/util"; import { GestureUtils } from "../../../../pen-gestures/GestureUtils"; -import { aggregateBounds, intersectRect, returnFalse, returnOne, returnZero, setupMoveUpEvents, Utils, returnVal } from "../../../../Utils"; +import { aggregateBounds, intersectRect, returnFalse, returnOne, returnZero, setupMoveUpEvents, Utils, returnVal, returnTrue } from "../../../../Utils"; import { CognitiveServices } from "../../../cognitive_services/CognitiveServices"; import { DocServer } from "../../../DocServer"; import { Docs, DocUtils } from "../../../documents/Documents"; @@ -31,9 +31,9 @@ import { COLLECTION_BORDER_WIDTH } from "../../../views/globalCssVariables.scss" import { Timeline } from "../../animationtimeline/Timeline"; import { ContextMenu } from "../../ContextMenu"; import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth } from "../../InkingStroke"; -import { CollectionFreeFormDocumentView } from "../../nodes/CollectionFreeFormDocumentView"; +import { CollectionFreeFormDocumentView, CollectionFreeFormDocumentViewProps } from "../../nodes/CollectionFreeFormDocumentView"; import { DocumentLinksButton } from "../../nodes/DocumentLinksButton"; -import { DocumentViewProps, DocAfterFocusFunc } from "../../nodes/DocumentView"; +import { DocumentViewProps, DocAfterFocusFunc, DocumentView } from "../../nodes/DocumentView"; import { FormattedTextBox } from "../../nodes/formattedText/FormattedTextBox"; import { pageSchema } from "../../nodes/ImageBox"; import { PresBox } from "../../nodes/PresBox"; @@ -47,7 +47,9 @@ import { MarqueeOptionsMenu } from "./MarqueeOptionsMenu"; import { MarqueeView } from "./MarqueeView"; import React = require("react"); import { CurrentUserUtils } from "../../../util/CurrentUserUtils"; -import { isUndefined } from "lodash"; +import { StyleProp, StyleLayers } from "../../StyleProvider"; +import { DocumentDecorations } from "../../DocumentDecorations"; +import { FieldViewProps } from "../../nodes/FieldView"; export const panZoomSchema = createSchema({ _panX: "number", @@ -81,6 +83,8 @@ export type collectionFreeformViewProps = { @observer export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, Partial<collectionFreeformViewProps>>(PanZoomDocument) { + public get displayName() { return "CollectionFreeFormView(" + this.props.Document.title?.toString() + ")"; } // this makes mobx trace() statements more descriptive + private _lastX: number = 0; private _lastY: number = 0; private _downX: number = 0; @@ -95,24 +99,29 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P private _layoutPoolData = new ObservableMap<string, PoolData>(); private _layoutSizeData = new ObservableMap<string, { width?: number, height?: number }>(); private _cachedPool: Map<string, PoolData> = new Map(); + private _lastTap = 0; + private _nudgeTime = 0; + private _thumbIdentifier?: number; + + @observable private _hLines: number[] | undefined; + @observable private _vLines: number[] | undefined; @observable private _pullCoords: number[] = [0, 0]; @observable private _pullDirection: string = ""; + @observable private _showAnimTimeline = false; + @observable ChildDrag: DocumentView | undefined; // child document view being dragged. needed to update drop areas of groups when a group item is dragged. - public get displayName() { return "CollectionFreeFormView(" + this.props.Document.title?.toString() + ")"; } // this makes mobx trace() statements more descriptive @observable.shallow _layoutElements: ViewDefResult[] = []; // shallow because some layout items (eg pivot labels) are just generated 'divs' and can't be frozen as observables @observable _clusterSets: (Doc[])[] = []; @observable _timelineRef = React.createRef<Timeline>(); - @observable _marqueeRef = React.createRef<HTMLDivElement>(); - @observable canPanX: boolean = true; - @observable canPanY: boolean = true; + @computed get backgroundActive() { return this.props.layerProvider?.(this.layoutDoc) === false && (this.props.ContainingCollectionView?.active() || this.props.active()); } @computed get fitToContentScaling() { return this.fitToContent ? NumCast(this.layoutDoc.fitToContentScaling, 1) : 1; } - @computed get fitToContent() { return (this.props.fitToBox || this.Document._fitToBox) && !this.isAnnotationOverlay; } - @computed get parentScaling() { return this.props.ContentScaling && this.fitToContent ? this.props.ContentScaling() : 1; } + @computed get fitToContent() { return (this.props.fitContentsToDoc || this.Document._fitToBox) && !this.isAnnotationOverlay; } + @computed get parentScaling() { return 1; } @computed get contentBounds() { return aggregateBounds(this._layoutElements.filter(e => e.bounds && !e.bounds.z).map(e => e.bounds!), NumCast(this.layoutDoc._xPadding, 10), NumCast(this.layoutDoc._yPadding, 10)); } - @computed get nativeWidth() { return this.fitToContent ? 0 : returnVal(this.props.NativeWidth?.(), Doc.NativeWidth(this.Document)); } - @computed get nativeHeight() { return this.fitToContent ? 0 : returnVal(this.props.NativeHeight?.(), Doc.NativeHeight(this.Document)); } + @computed get nativeWidth() { return this.fitToContent ? 0 : Doc.NativeWidth(this.Document); } + @computed get nativeHeight() { return this.fitToContent ? 0 : Doc.NativeHeight(this.Document); } 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; } @@ -155,39 +164,26 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P FormattedTextBox.SelectOnLoad = newBox[Id];// track the new text box so we can give it a prop that tells it to focus itself when it's displayed this.addDocument(newBox); } + addDocument = action((newBox: Doc | Doc[]) => { let retVal = false; if (newBox instanceof Doc) { - retVal = this.props.addDocument(newBox); + retVal = this.props.addDocument?.(newBox) || false; retVal && this.bringToFront(newBox); retVal && this.updateCluster(newBox); } else { - retVal = this.props.addDocument(newBox); + retVal = this.props.addDocument?.(newBox) || false; // bcz: deal with clusters } if (retVal) { const newBoxes = (newBox instanceof Doc) ? [newBox] : newBox; for (const newBox of newBoxes) { if (newBox.activeFrame !== undefined) { - const x = newBox.x; - const y = newBox.y; - const w = newBox._width; - const h = newBox._height; - delete newBox["x-indexed"]; - delete newBox["y-indexed"]; - delete newBox["w-indexed"]; - delete newBox["h-indexed"]; - delete newBox["opacity-indexed"]; - delete newBox._width; - delete newBox._height; - delete newBox.x; - delete newBox.y; - delete newBox.opacity; + const vals = CollectionFreeFormDocumentView.animFields.map(field => newBox[field]); + CollectionFreeFormDocumentView.animFields.forEach(field => delete newBox[`${field}-indexed`]); + CollectionFreeFormDocumentView.animFields.forEach(field => delete newBox[field]); delete newBox.activeFrame; - newBox.x = x; - newBox.y = y; - newBox._width = w; - newBox._height = h; + CollectionFreeFormDocumentView.animFields.forEach((field, i) => field !== "opacity" && (newBox[field] = vals[i])); } } if (this.Document._currentFrame !== undefined && !this.props.isAnnotationOverlay) { @@ -199,7 +195,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P private selectDocuments = (docs: Doc[]) => { SelectionManager.DeselectAll(); - docs.map(doc => DocumentManager.Instance.getDocumentView(doc, this.props.CollectionView)).map(dv => dv && SelectionManager.SelectDoc(dv, true)); + docs.map(doc => DocumentManager.Instance.getDocumentView(doc, this.props.CollectionView)).map(dv => dv && SelectionManager.SelectView(dv, true)); } public isCurrent(doc: Doc) { return (Math.abs(NumCast(doc.displayTimecode, -1) - NumCast(this.Document._currentTimecode, -1)) < 1.5 || NumCast(doc.displayTimecode, -1) === -1); } @@ -211,8 +207,28 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P return (pt => super.onExternalDrop(e, { x: pt[0], y: pt[1] }))(this.getTransform().transformPoint(e.pageX, e.pageY)); } + updateGroupBounds = () => { + if (!this.props.Document._isGroup) return; + const clist = this.childDocs.map(cd => ({ x: NumCast(cd.x), y: NumCast(cd.y), width: cd[WidthSym](), height: cd[HeightSym]() })); + const cbounds = aggregateBounds(clist, 0, 0); + const c = [NumCast(this.layoutDoc.x) + this.layoutDoc[WidthSym]() / 2, NumCast(this.layoutDoc.y) + this.layoutDoc[HeightSym]() / 2]; + const p = [NumCast(this.layoutDoc._panX), NumCast(this.layoutDoc._panY)]; + const pbounds = { + x: (cbounds.x - p[0]) * this.zoomScaling() + c[0], y: (cbounds.y - p[1]) * this.zoomScaling() + c[1], + r: (cbounds.r - p[0]) * this.zoomScaling() + c[0], b: (cbounds.b - p[1]) * this.zoomScaling() + c[1] + }; + + this.layoutDoc._width = (pbounds.r - pbounds.x); + this.layoutDoc._height = (pbounds.b - pbounds.y); + this.layoutDoc._panX = (cbounds.r + cbounds.x) / 2; + this.layoutDoc._panY = (cbounds.b + cbounds.y) / 2; + this.layoutDoc.x = pbounds.x; + this.layoutDoc.y = pbounds.y; + } + @action internalDocDrop(e: Event, de: DragManager.DropEvent, docDragData: DragManager.DocumentDragData, xp: number, yp: number) { + if (!this.ChildDrag && this.props.layerProvider?.(this.props.Document) !== false && this.props.Document._isGroup) return false; if (!super.onInternalDrop(e, de)) return false; const refDoc = docDragData.droppedDocuments[0]; const [xpo, ypo] = this.getTransformOverlay().transformPoint(de.x, de.y); @@ -222,14 +238,17 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P const zsorted = this.childLayoutPairs.map(pair => pair.layout).slice().sort((doc1, doc2) => NumCast(doc1.zIndex) - NumCast(doc2.zIndex)); zsorted.forEach((doc, index) => doc.zIndex = doc.isInkMask ? 5000 : index + 1); const dvals = CollectionFreeFormDocumentView.getValues(refDoc, NumCast(refDoc.activeFrame, 1000)); - const dropPos = this.Document._currentFrame !== undefined ? [dvals.x, dvals.y] : [NumCast(refDoc.x), NumCast(refDoc.y)]; + const dropPos = this.Document._currentFrame !== undefined ? [dvals.x || 0, dvals.y || 0] : [NumCast(refDoc.x), NumCast(refDoc.y)]; for (let i = 0; i < docDragData.droppedDocuments.length; i++) { const d = docDragData.droppedDocuments[i]; const layoutDoc = Doc.Layout(d); if (this.Document._currentFrame !== undefined) { CollectionFreeFormDocumentView.setupKeyframes([d], this.Document._currentFrame, false); const vals = CollectionFreeFormDocumentView.getValues(d, NumCast(d.activeFrame, 1000)); - CollectionFreeFormDocumentView.setValues(this.Document._currentFrame, d, x + vals.x - dropPos[0], y + vals.y - dropPos[1], vals.h, vals.w, this.Document.editScrollProgressivize ? vals.scroll : undefined, vals.opacity); + vals.x = x + (vals.x || 0) - dropPos[0]; + vals.y = y + (vals.y || 0) - dropPos[1]; + vals._scrollTop = this.Document.editScrollProgressivize ? vals._scrollTop : undefined; + CollectionFreeFormDocumentView.setValues(this.Document._currentFrame, d, vals); } else { d.x = x + NumCast(d.x) - dropPos[0]; d.y = y + NumCast(d.y) - dropPos[1]; @@ -237,9 +256,11 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P 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); - !Cast(d, listSpec("string"), []).includes("background") && (d._raiseWhenDragged === undefined ? Doc.UserDoc()._raiseWhenDragged : d._raiseWhenDragged) && (d.zIndex = zsorted.length + 1 + i); // bringToFront + !StrListCast(d.layers).includes(StyleLayers.Background) && (d._raiseWhenDragged === undefined ? Doc.UserDoc()._raiseWhenDragged : d._raiseWhenDragged) && (d.zIndex = zsorted.length + 1 + i); // bringToFront } + this.updateGroupBounds(); + (docDragData.droppedDocuments.length === 1 || de.shiftKey) && this.updateClusterDocs(docDragData.droppedDocuments); return true; } @@ -267,8 +288,8 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P return false; } else { const source = Docs.Create.TextDocument("", { _width: 200, _height: 75, x: xp, y: yp, title: "dropped annotation" }); - this.props.addDocument(source); - linkDragData.linkDocument = DocUtils.MakeLink({ doc: source }, { doc: linkDragData.linkSourceDocument }, "doc annotation", ""); // TODODO this is where in text links get passed + this.props.addDocument?.(source); + de.complete.linkDocument = DocUtils.MakeLink({ doc: source }, { doc: linkDragData.linkSourceDocument }, "doc annotation", ""); // TODODO this is where in text links get passed e.stopPropagation(); return true; } @@ -277,13 +298,9 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P @action onInternalDrop = (e: Event, de: DragManager.DropEvent) => { const [xp, yp] = this.getTransform().transformPoint(de.x, de.y); - if (this.isAnnotationOverlay !== true && de.complete.linkDragData) { - return this.internalLinkDrop(e, de, de.complete.linkDragData, xp, yp); - } else if (de.complete.annoDragData?.dropDocument && super.onInternalDrop(e, de)) { - return this.internalPdfAnnoDrop(e, de.complete.annoDragData, xp, yp); - } else if (de.complete.docDragData?.droppedDocuments.length && this.internalDocDrop(e, de, de.complete.docDragData, xp, yp)) { - return true; - } + if (this.isAnnotationOverlay !== true && de.complete.linkDragData) return this.internalLinkDrop(e, de, de.complete.linkDragData, xp, yp); + if (de.complete.annoDragData?.dropDocument && super.onInternalDrop(e, de)) return this.internalPdfAnnoDrop(e, de.complete.annoDragData, xp, yp); + if (de.complete.docDragData?.droppedDocuments.length) return this.internalDocDrop(e, de, de.complete.docDragData, xp, yp); return false; } @@ -301,6 +318,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P return cluster; }, -1); } + tryDragCluster(e: PointerEvent | TouchEvent, cluster: number) { if (cluster !== -1) { const ptsParent = e instanceof PointerEvent ? e : e.targetTouches.item(0); @@ -309,7 +327,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P const clusterDocs = eles.map(ele => DocumentManager.Instance.getDocumentView(ele, this.props.CollectionView)!); const de = new DragManager.DocumentDragData(eles); de.moveDocument = this.props.moveDocument; - const [left, top] = clusterDocs[0].props.ScreenToLocalTransform().scale(clusterDocs[0].props.ContentScaling()).inverse().transformPoint(0, 0); + const { left, top } = clusterDocs[0].getBounds() || { left: 0, top: 0 }; de.offset = this.getTransform().transformDirection(ptsParent.clientX - left, ptsParent.clientY - top); de.dropAction = e.ctrlKey || e.altKey ? "alias" : undefined; DragManager.StartDocumentDrag(clusterDocs.map(v => v.ContentDiv!), de, ptsParent.clientX, ptsParent.clientY, { hideSource: !de.dropAction }); @@ -367,10 +385,10 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P updateCluster(doc: Doc) { const childLayouts = this.childLayoutPairs.map(pair => pair.layout); if (this.props.Document._useClusters) { - this._clusterSets.map(set => Doc.IndexOf(doc, set) !== -1 && set.splice(Doc.IndexOf(doc, set), 1)); + this._clusterSets.forEach(set => Doc.IndexOf(doc, set) !== -1 && set.splice(Doc.IndexOf(doc, set), 1)); const preferredInd = NumCast(doc.cluster); doc.cluster = -1; - this._clusterSets.map((set, i) => set.map(member => { + this._clusterSets.forEach((set, i) => set.forEach(member => { if (doc.cluster === -1 && Doc.IndexOf(member, childLayouts) !== -1 && Doc.overlapping(doc, member, this._clusterDistance)) { doc.cluster = i; } @@ -378,7 +396,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P if (doc.cluster === -1 && preferredInd !== -1 && this._clusterSets.length > preferredInd && (!this._clusterSets[preferredInd] || !this._clusterSets[preferredInd].filter(member => Doc.IndexOf(member, childLayouts) !== -1).length)) { doc.cluster = preferredInd; } - this._clusterSets.map((set, i) => { + this._clusterSets.forEach((set, i) => { if (doc.cluster === -1 && !set.filter(member => Doc.IndexOf(member, childLayouts) !== -1).length) { doc.cluster = i; } @@ -393,27 +411,26 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P } } - getClusterColor = (doc: Opt<Doc>, renderDepth: number, property: string, layerProvider?: (doc: Doc, assign?: boolean) => boolean) => { - let clusterColor = this.props.styleProvider?.(doc, this.props.renderDepth + 1, property, layerProvider); - if (property !== "backgroundColor") return clusterColor; + getClusterColor = (doc: Opt<Doc>, props: Opt<DocumentViewProps | FieldViewProps>, property: string) => { + let styleProp = this.props.styleProvider?.(doc, props, property); // bcz: check 'props' used to be renderDepth + 1 + if (property !== StyleProp.BackgroundColor) return styleProp; const cluster = NumCast(doc?.cluster); if (this.Document._useClusters) { if (this._clusterSets.length <= cluster) { - setTimeout(() => doc && this.updateCluster(doc), 0); + setTimeout(() => doc && this.updateCluster(doc)); } else { // choose a cluster color from a palette const colors = ["#da42429e", "#31ea318c", "rgba(197, 87, 20, 0.55)", "#4a7ae2c4", "rgba(216, 9, 255, 0.5)", "#ff7601", "#1dffff", "yellow", "rgba(27, 130, 49, 0.55)", "rgba(0, 0, 0, 0.268)"]; - clusterColor = colors[cluster % colors.length]; + 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 && set.filter(s => !Cast(s.layers, listSpec("string"), []).includes("background")).map(s => clusterColor = StrCast(s.backgroundColor)); - set && set.filter(s => Cast(s.layers, listSpec("string"), []).includes("background")).map(s => clusterColor = StrCast(s.backgroundColor)); + set && set.filter(s => !StrListCast(s.layers).includes(StyleLayers.Background)).map(s => styleProp = StrCast(s.backgroundColor)); + set && set.filter(s => StrListCast(s.layers).includes(StyleLayers.Background)).map(s => styleProp = StrCast(s.backgroundColor)); } - } else if (doc && NumCast(doc.group, -1) !== -1) clusterColor = "gray"; - return clusterColor; + } //else if (doc && NumCast(doc.group, -1) !== -1) styleProp = "gray"; + return styleProp; } - @action onPointerDown = (e: React.PointerEvent): void => { if (e.nativeEvent.cancelBubble || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE) || InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || (Doc.GetSelectedTool() === InkTool.Highlighter || Doc.GetSelectedTool() === InkTool.Pen)) { @@ -421,13 +438,6 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P } this._hitCluster = this.pickCluster(this.getTransform().transformPoint(e.clientX, e.clientY)); if (e.button === 0 && (!e.shiftKey || this._hitCluster !== -1) && !e.altKey && !e.ctrlKey && this.props.active(true)) { - - // if (!this.props.Document.aliasOf && !this.props.ContainingCollectionView) { - // this.props.addDocTab(this.props.Document, "replace"); - // e.stopPropagation(); - // e.preventDefault(); - // return; - // } document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); document.addEventListener("pointermove", this.onPointerMove); @@ -457,12 +467,6 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P this.addMoveListeners(); this.removeEndListeners(); this.addEndListeners(); - // if (Doc.SelectedTool() === InkTool.Highlighter || Doc.SelectedTool() === InkTool.Pen) { - // e.stopPropagation(); - // e.preventDefault(); - // const point = this.getTransform().transformPoint(pt.pageX, pt.pageY); - // this._points.push({ X: point[0], Y: point[1] }); - // } if (Doc.GetSelectedTool() === InkTool.None) { this._lastX = pt.pageX; this._lastY = pt.pageY; @@ -507,7 +511,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P return pass; }); this.addDocument(Docs.Create.FreeformDocument(sel, { title: "nested collection", x: bounds.x, y: bounds.y, _width: bWidth, _height: bHeight, _panX: 0, _panY: 0 })); - sel.forEach(d => this.props.removeDocument(d)); + sel.forEach(d => this.props.removeDocument?.(d)); e.stopPropagation(); break; case GestureUtils.Gestures.StartBracket: @@ -525,12 +529,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P if (sets.length && sets[0]) { this._wordPalette.clear(); const colors = setDocs.map(sd => FieldValue(sd.color) as string); - sets.forEach((st: string, i: number) => { - const words = st.split(","); - words.forEach(word => { - this._wordPalette.set(word, colors[i]); - }); - }); + sets.forEach((st: string, i: number) => st.split(",").forEach(word => this._wordPalette.set(word, colors[i]))); } const inks = this.getActiveDocuments().filter(doc => { if (doc.type === "ink") { @@ -593,27 +592,20 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P } } - _lastTap = 0; - - @action onPointerUp = (e: PointerEvent): void => { - if (InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) return; - - document.removeEventListener("pointermove", this.onPointerMove); - document.removeEventListener("pointerup", this.onPointerUp); - this.removeMoveListeners(); - this.removeEndListeners(); + if (!InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) { + document.removeEventListener("pointermove", this.onPointerMove); + document.removeEventListener("pointerup", this.onPointerUp); + this.removeMoveListeners(); + this.removeEndListeners(); + } } onClick = (e: React.MouseEvent) => { if (this.layoutDoc.targetScale && (Math.abs(e.pageX - this._downX) < 3 && Math.abs(e.pageY - this._downY) < 3)) { - if (Date.now() - this._lastTap < 300) { - runInAction(() => { - DocumentLinksButton.StartLink = undefined; - DocumentLinksButton.StartLinkView = undefined; - }); - const docpt = this.getTransform().transformPoint(e.clientX, e.clientY); - this.scaleAtPt(docpt, 1); + if (Date.now() - this._lastTap < 300) { // reset zoom of freeform view to 1-to-1 on a double click + runInAction(() => DocumentLinksButton.StartLink = DocumentLinksButton.StartLinkView = undefined); + this.scaleAtPt(this.getTransform().transformPoint(e.clientX, e.clientY), 1); e.stopPropagation(); e.preventDefault(); } @@ -623,9 +615,6 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P @action pan = (e: PointerEvent | React.Touch | { clientX: number, clientY: number }): void => { - // bcz: theres should be a better way of doing these than referencing these static instances directly - MarqueeOptionsMenu.Instance?.fadeOut(true);// I think it makes sense for the marquee menu to go away when panned. -syip2 - const [dx, dy] = this.getTransform().transformDirection(e.clientX - this._lastX, e.clientY - this._lastY); this.setPan((this.Document._panX || 0) - dx, (this.Document._panY || 0) - dy, undefined, true); this._lastX = e.clientX; @@ -644,6 +633,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P return; } if (!e.cancelBubble) { + if (this.props.Document._isGroup) return; // groups don't pan when dragged -- instead let the event go through to allow the group itself to drag if (Doc.GetSelectedTool() === 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 @@ -766,7 +756,6 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P } } - this.removeMoveListeners(); this.addMoveListeners(); this.removeEndListeners(); @@ -794,7 +783,6 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P this.removeEndListeners(); } - @action zoom = (pointX: number, pointY: number, deltaY: number): void => { let deltaScale = deltaY > 0 ? (1 / 1.05) : 1.05; @@ -865,7 +853,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P } bringToFront = action((doc: Doc, sendToBack?: boolean) => { - if (sendToBack || Cast(doc.layers, listSpec("string"), []).includes("background")) { + if (sendToBack || StrListCast(doc.layers).includes(StyleLayers.Background)) { doc.zIndex = 0; } else if (doc.isInkMask) { doc.zIndex = 5000; @@ -916,7 +904,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P !dontCenter && this.props.focus(doc); afterFocus && setTimeout(afterFocus, delay); } else { - const contextHgt = Doc.AreProtosEqual(annotOn, this.props.Document) && this.props.VisibleHeight ? this.props.VisibleHeight() : NumCast(annotOn._height); + const contextHgt = NumCast(annotOn._height); const curScroll = NumCast(this.props.Document._scrollTop); let scrollTo = curScroll; if (curScroll + contextHgt < NumCast(doc.y)) { @@ -932,7 +920,6 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P } else { !dontCenter && delay && this.props.focus(this.props.Document); afterFocus?.(!dontCenter && delay ? true : false); - } } @@ -947,7 +934,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P newState.initializers![this.Document[Id]] = { panX: newPanX, panY: newPanY }; HistoryUtil.pushState(newState); - if (DocListCast(this.dataDoc[this.props.annotationsKey || this.props.fieldKey]).includes(doc)) { + if (DocListCast(this.dataDoc[this.props.fieldKey]).includes(doc)) { // glr: freeform transform speed can be set by adjusting presTransition field - needs a way of knowing when presentation is not active... if (!doc.z) this.setPan(newPanX, newPanY, doc.focusSpeed || doc.focusSpeed === 0 ? `transform ${doc.focusSpeed}ms` : "transform 500ms", 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 } @@ -979,11 +966,8 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P pw && ph && (this.Document[this.scaleFieldKey] = scale * Math.min(pw / NumCast(doc._width), ph / NumCast(doc._height))); } - @computed get libraryPath() { return this.props.LibraryPath ? [...this.props.LibraryPath, this.props.Document] : []; } - @computed get backgroundActive() { return this.props.layerProvider?.(this.layoutDoc) === false && (this.props.ContainingCollectionView?.active() || this.props.active()); } onChildClickHandler = () => this.props.childClickScript || ScriptCast(this.Document.onChildClick); onChildDoubleClickHandler = () => this.props.childDoubleClickScript || ScriptCast(this.Document.onChildDoubleClick); - backgroundHalo = computedFn(function (doc: Doc) { return BoolCast(this.Document._useClusters) || (NumCast(doc.group, -1) !== -1); }).bind(this); parentActive = (outsideReaction: boolean) => this.props.active(outsideReaction) || this.props.parentActive?.(outsideReaction) || this.backgroundActive || this.layoutDoc._viewType === CollectionViewType.Pile ? true : false; getChildDocumentViewProps(childLayout: Doc, childData?: Doc): DocumentViewProps { return { @@ -993,34 +977,29 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P pinToPres: this.props.pinToPres, whenActiveChanged: this.props.whenActiveChanged, parentActive: this.parentActive, - fitToBox: false, DataDoc: childData, Document: childLayout, - LibraryPath: this.libraryPath, - LayoutTemplate: childLayout.z ? undefined : this.props.ChildLayoutTemplate, - LayoutTemplateString: childLayout.z ? undefined : this.props.ChildLayoutString, - FreezeDimensions: this.props.freezeChildDimensions, - setupDragLines: this.setupDragLines, - dontRegisterView: this.props.dontRegisterView, + ContainingCollectionView: this.props.CollectionView, + ContainingCollectionDoc: this.props.Document, + LayoutTemplate: childLayout.z ? undefined : this.props.childLayoutTemplate, + LayoutTemplateString: childLayout.z ? undefined : this.props.childLayoutString, rootSelected: childData ? this.rootSelected : returnFalse, - dropAction: StrCast(this.props.Document.childDropAction) as dropActionType, onClick: this.onChildClickHandler, onDoubleClick: this.onChildDoubleClickHandler, ScreenToLocalTransform: childLayout.z ? this.getTransformOverlay : this.getTransform, - renderDepth: this.props.renderDepth + 1, PanelWidth: childLayout[WidthSym], PanelHeight: childLayout[HeightSym], - ContentScaling: returnOne, - ContainingCollectionView: this.props.CollectionView, - ContainingCollectionDoc: this.props.Document, docFilters: this.docFilters, docRangeFilters: this.docRangeFilters, searchFilterDocs: this.searchFilterDocs, focus: this.focusDocument, styleProvider: this.getClusterColor, - backgroundHalo: this.backgroundHalo, + freezeDimensions: this.props.childFreezeDimensions, + dropAction: StrCast(this.props.Document.childDropAction) as dropActionType, bringToFront: this.bringToFront, addDocTab: this.addDocTab, + renderDepth: this.props.renderDepth + 1, + dontRegisterView: this.props.dontRegisterView, }; } @@ -1030,14 +1009,14 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P const pt = this.getTransform().transformPoint(NumCast(doc.x), NumCast(doc.y)); doc.x = pt[0]; doc.y = pt[1]; - return this.props.addDocument(doc); + return this.props.addDocument?.(doc) || false; } else { (doc as any as Doc[]).forEach(doc => { const pt = this.getTransform().transformPoint(NumCast(doc.x), NumCast(doc.y)); doc.x = pt[0]; doc.y = pt[1]; }); - return this.props.addDocument(doc); + return this.props.addDocument?.(doc) || false; } } if (where === "inPlace" && this.layoutDoc.isInPlaceContainer) { @@ -1046,9 +1025,11 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P } return this.props.addDocTab(doc, where); }); + getCalculatedPositions(params: { pair: { layout: Doc, data?: Doc }, index: number, collection: Doc, docs: Doc[], state: any }): PoolData { const layoutDoc = Doc.Layout(params.pair.layout); - const { x, y, opacity } = this.Document._currentFrame === undefined ? params.pair.layout : + const { x, y, opacity } = this.Document._currentFrame === undefined ? + { x: params.pair.layout.x, y: params.pair.layout.y, opacity: this.props.styleProvider?.(params.pair.layout, this.props, StyleProp.Opacity) } : CollectionFreeFormDocumentView.getValues(params.pair.layout, this.Document._currentFrame || 0); const { z, color, zIndex } = params.pair.layout; return { @@ -1095,6 +1076,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P childDataProvider = computedFn(function childDataProvider(this: any, doc: Doc, replica: string) { return this._layoutPoolData.get(doc[Id] + (replica || "")); }.bind(this)); + childSizeProvider = computedFn(function childSizeProvider(this: any, doc: Doc, replica: string) { return this._layoutSizeData.get(doc[Id] + (replica || "")); }.bind(this)); @@ -1162,6 +1144,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P key={entry[1].pair.layout[Id] + (entry[1].replica || "")} {...this.getChildDocumentViewProps(entry[1].pair.layout, entry[1].pair.data)} replica={entry[1].replica} + CollectionFreeFormView={this} dataProvider={this.childDataProvider} sizeProvider={this.childSizeProvider} layerProvider={this.props.layerProvider} @@ -1170,8 +1153,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P (this.props.viewDefDivClick || (engine === "pass" && !this.props.isSelected(true))) ? "none" : undefined} jitterRotation={NumCast(this.props.Document._jitterRotation) || ((Doc.UserDoc().renderStyle === "comic" ? 10 : 0))} //fitToBox={this.props.fitToBox || BoolCast(this.props.freezeChildDimensions)} // bcz: check this - fitToBox={BoolCast(this.props.freezeChildDimensions)} // bcz: check this - FreezeDimensions={BoolCast(this.props.freezeChildDimensions)} + freezeDimensions={BoolCast(this.props.childFreezeDimensions)} />, bounds: this.childDataProvider(entry[1].pair.layout, entry[1].replica) })); @@ -1191,7 +1173,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P this._layoutComputeReaction = reaction(() => this.doLayoutComputation, (elements) => this._layoutElements = elements || [], { fireImmediately: true, name: "doLayout" }); - if (!this.props.annotationsKey) { + if (!this.props.isAnnotationOverlay) { this._boundsReaction = reaction(() => this.contentBounds, bounds => (!this.fitToContent && this._layoutElements?.length) && setTimeout(() => { const rbounds = Cast(this.Document._renderContentBounds, listSpec("number"), [0, 0, 0, 0]); @@ -1218,9 +1200,6 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P // super.setCursorPosition(this.getTransform().transformPoint(e.clientX, e.clientY)); } - - // <div ref={this._marqueeRef}> - @action onDragAutoScroll = (e: CustomEvent<React.DragEvent>) => { if ((e as any).handlePan || this.props.isAnnotationOverlay) return; @@ -1252,7 +1231,6 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P this.props.ContainingCollectionView?.removeDocument(this.props.Document); })); - @undoBatch layoutDocsInGrid = action(() => { const docs = this.childLayoutPairs; @@ -1277,7 +1255,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P @undoBatch @action toggleNativeDimensions = () => { - Doc.toggleNativeDimensions(this.layoutDoc, this.props.ContentScaling(), this.props.NativeWidth?.() || 0, this.props.NativeHeight?.() || 0); + Doc.toggleNativeDimensions(this.layoutDoc, 1, this.nativeWidth, this.nativeHeight); } @undoBatch @@ -1286,23 +1264,22 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P this.layoutDoc._lockedTransform = this.layoutDoc._lockedTransform ? undefined : true; } - private thumbIdentifier?: number; - onContextMenu = (e: React.MouseEvent) => { - if (this.props.annotationsKey || this.props.Document.annotationOn || !ContextMenu.Instance) return; + if (this.props.isAnnotationOverlay || this.props.Document.annotationOn || !ContextMenu.Instance) return; 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" }); + 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; !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; !viewctrls && ContextMenu.Instance.addItem({ description: "UI Controls...", subitems: viewCtrlItems, icon: "eye" }); @@ -1310,15 +1287,12 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P const options = ContextMenu.Instance.findByDescription("Options..."); const optionItems = options && "subitems" in options ? options.subitems : []; !this.props.isAnnotationOverlay && !Doc.UserDoc().noviceMode && - optionItems.push({ description: (this.showTimeline ? "Close" : "Open") + " Animation Timeline", event: action(() => this.showTimeline = !this.showTimeline), icon: "eye" }); - this.props.ContainingCollectionView && - optionItems.push({ description: "Move Items Out of Collection", event: this.promoteCollection, icon: "table" }); + optionItems.push({ description: (this._showAnimTimeline ? "Close" : "Open") + " Animation Timeline", event: action(() => this._showAnimTimeline = !this._showAnimTimeline), icon: "eye" }); optionItems.push({ description: this.layoutDoc._lockedTransform ? "Unlock Transform" : "Lock Transform", event: this.toggleLockTransform, icon: this.layoutDoc._lockedTransform ? "unlock" : "lock" }); 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) { 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..."); @@ -1347,11 +1321,10 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P const [xx, yy] = this.props.ScreenToLocalTransform().transformPoint(x, y); doc.x = xx, doc.y = yy; this.props.addDocument?.(doc); - setTimeout(() => { + 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. + }), 2000); // need to give solr some time to update so that this query will find any link docs we've added. } } } @@ -1359,57 +1332,38 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P input.click(); } - - @observable showTimeline = false; - - intersectRect(r1: { left: number, top: number, width: number, height: number }, - r2: { left: number, top: number, width: number, height: number }) { - return !(r2.left > r1.left + r1.width || r2.left + r2.width < r1.left || r2.top > r1.top + r1.height || r2.top + r2.height < r1.top); - } - @action setupDragLines = (snapToDraggedDoc: boolean = false) => { const activeDocs = this.getActiveDocuments(); - if (activeDocs.length > 50) { - DragManager.SetSnapLines([], []); - return; - } const size = this.getTransform().transformDirection(this.props.PanelWidth(), this.props.PanelHeight()); const selRect = { left: this.panX() - size[0] / 2, top: this.panY() - size[1] / 2, width: size[0], height: size[1] }; const docDims = (doc: Doc) => ({ left: NumCast(doc.x), top: NumCast(doc.y), width: NumCast(doc._width), height: NumCast(doc._height) }); - const isDocInView = (doc: Doc, rect: { left: number, top: number, width: number, height: number }) => { - if (this.intersectRect(docDims(doc), rect)) { - snappableDocs.push(doc); - } - }; - const snappableDocs: Doc[] = []; // the set of documents in the visible viewport that we will try to snap to; + 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]) }; - this.getActiveDocuments().filter(doc => !Cast(doc.layers, listSpec("string"), []).includes("background") && doc.z === undefined).map(doc => isDocInView(doc, selRect)); // first see if there are any foreground docs to snap to - !snappableDocs.length && this.getActiveDocuments().filter(doc => doc.z === undefined).map(doc => isDocInView(doc, selRect)); // if not, see if there are background docs to snap to - !snappableDocs.length && this.getActiveDocuments().filter(doc => doc.z !== undefined).map(doc => isDocInView(doc, otherBounds)); // if not, then why not snap to floating docs + let snappableDocs = activeDocs.filter(doc => !StrListCast(doc.layers).includes(StyleLayers.Background) && 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 const horizLines: number[] = []; const vertLines: number[] = []; + const invXf = this.getTransform().inverse(); snappableDocs.filter(doc => snapToDraggedDoc || !DragManager.docsBeingDragged.includes(Cast(doc.rootDocument, Doc, null) || doc)).forEach(doc => { const { left, top, width, height } = docDims(doc); - const topLeftInScreen = this.getTransform().inverse().transformPoint(left, top); - const docSize = this.getTransform().inverse().transformDirection(width, height); + const topLeftInScreen = invXf.transformPoint(left, top); + const docSize = invXf.transformDirection(width, height); horizLines.push(topLeftInScreen[1], topLeftInScreen[1] + docSize[1] / 2, topLeftInScreen[1] + docSize[1]); // horiz center line vertLines.push(topLeftInScreen[0], topLeftInScreen[0] + docSize[0] / 2, topLeftInScreen[0] + docSize[0]);// right line }); DragManager.SetSnapLines(horizLines, vertLines); } + onPointerOver = (e: React.PointerEvent) => { - if (SnappingManager.GetIsDragging()) { - this.setupDragLines(e.ctrlKey || e.shiftKey); - } + (DocumentDecorations.Instance.Interacting || (this.props.layerProvider?.(this.props.Document) !== false && SnappingManager.GetIsDragging())) && this.setupDragLines(e.ctrlKey || e.shiftKey); e.stopPropagation(); } - @observable private _hLines: number[] | undefined; - @observable private _vLines: number[] | undefined; - private childViews = () => { const children = typeof this.props.children === "function" ? (this.props.children as any)() as JSX.Element[] : []; return [ @@ -1424,13 +1378,13 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P eles.push(<CollectionFreeFormRemoteCursors {...this.props} key="remoteCursors" />); return eles; } + @computed get placeholder() { return <div className="collectionfreeformview-placeholder" style={{ background: this.Document.backgroundColor }}> <span className="collectionfreeformview-placeholderSpan">{this.props.Document.title?.toString()}</span> </div>; } - _nudgeTime = 0; nudge = action((x: number, y: number) => { if (this.props.ContainingCollectionDoc?._viewType !== CollectionViewType.Freeform || this.props.ContainingCollectionDoc._panX !== undefined) { // bcz: this isn't ideal, but want to try it out... @@ -1448,7 +1402,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P return divisions < 60 ? gridSpace : this.chooseGridSpace(gridSpace * 10); } - @computed get grid() { + @computed get backgroundGrid() { const gridSpace = this.chooseGridSpace(NumCast(this.layoutDoc["_backgroundGrid-spacing"], 50)); const shiftX = (this.props.isAnnotationOverlay ? 0 : -this.panX() % gridSpace - gridSpace) * this.zoomScaling(); const shiftY = (this.props.isAnnotationOverlay ? 0 : -this.panY() % gridSpace - gridSpace) * this.zoomScaling(); @@ -1480,20 +1434,21 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P } }} />; } + trySelectCluster = (addToSel: boolean) => { if (this._hitCluster !== -1) { - if (!addToSel) { - SelectionManager.DeselectAll(); - } + !addToSel && SelectionManager.DeselectAll(); const eles = this.childLayoutPairs.map(pair => pair.layout).filter(cd => (this.props.Document._useClusters ? NumCast(cd.cluster) : NumCast(cd.group, -1)) === this._hitCluster); this.selectDocuments(eles); return true; } return false; } + @computed get marqueeView() { return <MarqueeView {...this.props} + ungroup={this.props.Document._isGroup ? this.promoteCollection : undefined} nudge={this.isAnnotationOverlay || this.props.renderDepth > 0 ? undefined : this.nudge} addDocTab={this.addDocTab} trySelectCluster={this.trySelectCluster} @@ -1505,7 +1460,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P getTransform={this.getTransform} isAnnotationOverlay={this.isAnnotationOverlay}> <div ref={this._marqueeRef}> - {this.layoutDoc["_backgroundGrid-show"] ? this.grid : (null)} + {this.layoutDoc["_backgroundGrid-show"] && (!SnappingManager.GetIsDragging() || !Doc.UserDoc().showSnapLines) ? this.backgroundGrid : (null)} <CollectionFreeFormViewPannableContents isAnnotationOverlay={this.isAnnotationOverlay} centeringShiftX={this.centeringShiftX} @@ -1519,15 +1474,14 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P {this.children} </CollectionFreeFormViewPannableContents> </div> - {this.showTimeline ? <Timeline ref={this._timelineRef} {...this.props} /> : (null)} + {this._showAnimTimeline ? <Timeline ref={this._timelineRef} {...this.props} /> : (null)} </MarqueeView>; } - @computed get contentScaling() { - if (this.props.annotationsKey && !this.props.forceScaling) return 0; - const nw = returnVal(this.props.NativeWidth?.(), Doc.NativeWidth(this.Document)); - const nh = returnVal(this.props.NativeHeight?.(), Doc.NativeHeight(this.Document)); + if (this.props.isAnnotationOverlay && !this.props.forceScaling) return 0; + const nw = this.nativeWidth; + const nh = this.nativeHeight; const hscale = nh ? this.props.PanelHeight() / nh : 1; const wscale = nw ? this.props.PanelWidth() / nw : 1; return wscale < hscale ? wscale : hscale; @@ -1553,7 +1507,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P width: this.contentScaling ? `${100 / this.contentScaling}%` : "", height: this.contentScaling ? `${100 / this.contentScaling}%` : this.isAnnotationOverlay ? (this.props.Document.scrollHeight ? this.Document.scrollHeight : "100%") : this.props.PanelHeight() }}> - {this.Document._freeformLOD && !this.props.active() && !this.props.isAnnotationOverlay && !this.props.annotationsKey && this.props.renderDepth > 0 ? + {this.Document._freeformLOD && !this.props.active() && !this.props.isAnnotationOverlay && this.props.renderDepth > 0 ? this.placeholder : this.marqueeView} {!this.props.noOverlay ? <CollectionFreeFormOverlayView elements={this.elementFunc} /> : (null)} @@ -1574,6 +1528,17 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P {this._vLines?.map(l => <line y1="0" x1={l} y2="1000" x2={l} stroke="black" />)} </svg> </div>} + + {this.props.Document._isGroup && SnappingManager.GetIsDragging() && (this.ChildDrag || this.props.layerProvider?.(this.props.Document) === false) ? + <div className="collectionFreeForm-groupDropper" ref={this.createDashEventsTarget} style={{ + width: this.ChildDrag ? "10000" : "100%", + height: this.ChildDrag ? "10000" : "100%", + left: this.ChildDrag ? "-5000" : 0, + top: this.ChildDrag ? "-5000" : 0, + position: "absolute", + background: "#0009930", + pointerEvents: "all" + }} /> : (null)} </div >; } } diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index eb781ed0a..d20d1abfc 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -1,31 +1,33 @@ import { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; -import { AclAddonly, AclAdmin, AclEdit, DataSym, Doc, DocListCast, Opt } from "../../../../fields/Doc"; +import { AclAddonly, AclAdmin, AclEdit, DataSym, Doc, 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 { GetEffectiveAcl } from "../../../../fields/util"; -import { Utils } from "../../../../Utils"; +import { Utils, intersectRect, returnFalse } 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, PresMovement } from "../../nodes/PresBox"; import { PreviewCursor } from "../../PreviewCursor"; import { CollectionDockingView } from "../CollectionDockingView"; import { SubCollectionViewProps } from "../CollectionSubView"; -import { CollectionView, CollectionViewType } from "../CollectionView"; +import { CollectionView } from "../CollectionView"; import { MarqueeOptionsMenu } from "./MarqueeOptionsMenu"; import "./MarqueeView.scss"; import React = require("react"); -import { Id } from "../../../../fields/FieldSymbols"; -import { CurrentUserUtils } from "../../../util/CurrentUserUtils"; -import { PresBox, PresMovement } from "../../nodes/PresBox"; +import { StyleLayers } from "../../StyleProvider"; interface MarqueeViewProps { getContainerTransform: () => Transform; @@ -36,22 +38,31 @@ interface MarqueeViewProps { isSelected: () => boolean; trySelectCluster: (addToSel: boolean) => boolean; nudge?: (x: number, y: number) => boolean; + ungroup?: () => void; setPreviewCursor?: (func: (x: number, y: number, drag: boolean) => void) => void; } - @observer export class MarqueeView extends React.Component<SubCollectionViewProps & MarqueeViewProps> { + private _commandExecuted = false; @observable public static DragMarquee = false; @observable _lastX: number = 0; @observable _lastY: number = 0; @observable _downX: number = 0; @observable _downY: number = 0; @observable _visible: boolean = false; - _commandExecuted = false; - @observable _pointsX: number[] = []; - @observable _pointsY: number[] = []; - @observable _freeHand: boolean = false; + @observable _lassoPts: [number, number][] = []; + @observable _lassoFreehand: boolean = false; + + @computed get Transform() { return this.props.getTransform(); } + @computed get Bounds() { + const topLeft = this.Transform.transformPoint(this._downX < this._lastX ? this._downX : this._lastX, this._downY < this._lastY ? this._downY : this._lastY); + 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]) }; + } + get inkDoc() { return this.props.Document; } + get ink() { return Cast(this.props.Document.ink, InkField); } + set ink(value: Opt<InkField>) { this.props.Document.ink = value; } componentDidMount() { this.props.setPreviewCursor?.(this.setPreviewCursor); @@ -64,11 +75,9 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque document.removeEventListener("pointermove", this.onPointerMove, true); } document.removeEventListener("keydown", this.marqueeCommand, true); - if (hideMarquee) { - this._visible = false; - } - this._pointsX = []; - this._pointsY = []; + hideMarquee && this.hideMarquee(); + + this._lassoPts = []; } @undoBatch @@ -77,7 +86,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque //make textbox and add it to this collection // tslint:disable-next-line:prefer-const const cm = ContextMenu.Instance; - const [x, y] = this.props.getTransform().transformPoint(this._downX, this._downY); + const [x, y] = this.Transform.transformPoint(this._downX, this._downY); if (e.key === "?") { cm.setDefaultItem("?", (str: string) => this.props.addDocTab( Docs.Create.WebDocument(`https://bing.com/search?q=${str}`, { _fitWidth: true, _width: 400, x, y, _height: 512, _nativeWidth: 850, isAnnotating: false, title: "bing", useCors: true }), "add:right")); @@ -85,8 +94,12 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque cm.displayMenu(this._downX, this._downY); e.stopPropagation(); } else - if (e.key === ":") { - DocUtils.addDocumentCreatorMenuItems(this.props.addLiveTextDocument, this.props.addDocument, x, y); + if (e.key === "u" && this.props.ungroup) { + e.stopPropagation(); + this.props.ungroup(); + } + else if (e.key === ":") { + DocUtils.addDocumentCreatorMenuItems(this.props.addLiveTextDocument, this.props.addDocument || returnFalse, x, y); cm.displayMenu(this._downX, this._downY); e.stopPropagation(); @@ -114,8 +127,8 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque ns.map(line => { const indent = line.search(/\S|$/); const newBox = Docs.Create.TextDocument(line, { _width: 200, _height: 35, x: x + indent / 3 * 10, y: ypos, title: line }); - this.props.addDocument(newBox); - ypos += 40 * this.props.getTransform().Scale; + this.props.addDocument?.(newBox); + ypos += 40 * this.Transform.Scale; }); })(); e.stopPropagation(); @@ -136,11 +149,11 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque slide.x = x; slide.y = y; FormattedTextBox.SelectOnLoad = slide[Id]; - this.props.addDocument(slide); + this.props.addDocument?.(slide); //setTimeout(() => SelectionManager.SelectDoc(DocumentManager.Instance.getDocumentView(slide)!, false)); e.stopPropagation(); - } else if (!e.ctrlKey && !e.metaKey && SelectionManager.SelectedDocuments().length < 2) { - FormattedTextBox.SelectOnLoadChar = FormattedTextBox.DefaultLayout && !this.props.ChildLayoutString ? e.key : ""; + } else if (!e.ctrlKey && !e.metaKey && SelectionManager.Views().length < 2) { + FormattedTextBox.SelectOnLoadChar = FormattedTextBox.DefaultLayout && !this.props.childLayoutString ? e.key : ""; FormattedTextBox.LiveTextUndo = UndoManager.StartBatch("live text batch"); this.props.addLiveTextDocument(CurrentUserUtils.GetNewTextDoc("-typed text-", x, y, 200, 100, this.props.xMargin === 0)); e.stopPropagation(); @@ -184,9 +197,10 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque } const newCol = Docs.Create.SchemaDocument([...(groupAttr ? [new SchemaHeaderField("_group", "#f1efeb")] : []), ...columns.filter(c => c).map(c => new SchemaHeaderField(c, "#f1efeb"))], docList, { x: x, y: y, title: "droppedTable", _width: 300, _height: 100 }); - this.props.addDocument(newCol); + this.props.addDocument?.(newCol); } } + @action onPointerDown = (e: React.PointerEvent): void => { this._downX = this._lastX = e.clientX; @@ -210,13 +224,12 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque onPointerMove = (e: PointerEvent): void => { this._lastX = e.pageX; this._lastY = e.pageY; - this._pointsX.push(e.clientX); - this._pointsY.push(e.clientY); + this._lassoPts.push([e.clientX, e.clientY]); if (!e.cancelBubble) { if (Math.abs(this._lastX - this._downX) > Utils.DRAG_THRESHOLD || Math.abs(this._lastY - this._downY) > Utils.DRAG_THRESHOLD) { if (!this._commandExecuted) { - this._visible = true; + this.showMarquee(); } e.stopPropagation(); e.preventDefault(); @@ -267,6 +280,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque e.preventDefault(); } } + clearSelection() { if (window.getSelection) { window.getSelection()?.removeAllRanges(); } else if (document.getSelection()) { document.getSelection()?.empty(); } @@ -314,66 +328,36 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque } } - intersectRect(r1: { left: number, top: number, width: number, height: number }, - r2: { left: number, top: number, width: number, height: number }) { - return !(r2.left > r1.left + r1.width || r2.left + r2.width < r1.left || r2.top > r1.top + r1.height || r2.top + r2.height < r1.top); - } - - @computed - get Bounds() { - const left = this._downX < this._lastX ? this._downX : this._lastX; - const top = this._downY < this._lastY ? this._downY : this._lastY; - const topLeft = this.props.getTransform().transformPoint(left, top); - const size = this.props.getTransform().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]) }; - } - - get inkDoc() { - return this.props.Document; - } - - get ink() { // ink will be stored on the extension doc for the field (fieldKey) where the container's data is stored. - return Cast(this.props.Document.ink, InkField); - } - - set ink(value: InkField | undefined) { - this.props.Document.ink = value; - } - @action - showMarquee = () => { - this._visible = true; - } + showMarquee = () => { this._visible = true; } @action - hideMarquee = () => { - this._visible = false; - } + hideMarquee = () => { this._visible = false; } @undoBatch @action delete = () => { const selected = this.marqueeSelect(false); SelectionManager.DeselectAll(); - selected.forEach(doc => this.props.removeDocument(doc)); + selected.forEach(doc => this.props.removeDocument?.(doc)); this.cleanupInteractions(false); MarqueeOptionsMenu.Instance.fadeOut(true); this.hideMarquee(); } - getCollection = action((selected: Doc[], creator: Opt<(documents: Array<Doc>, options: DocumentOptions, id?: string) => Doc>, layers: string[]) => { + getCollection = action((selected: Doc[], creator: Opt<(documents: Array<Doc>, options: DocumentOptions, id?: string) => Doc>, layers: string[], 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 = "nested freeform"; + Doc.GetProto(doc).title = makeGroup ? "grouping" : "nested freeform"; doc._panX = doc._panY = 0; return doc; })(Doc.MakeCopy(Doc.UserDoc().emptyCollection as Doc, true)); newCollection.system = undefined; newCollection.layers = new List<string>(layers); - newCollection.backgroundColor = this.props.isAnnotationOverlay ? "#00000015" : layers.includes("background") ? "cyan" : undefined; newCollection._width = this.Bounds.width; newCollection._height = this.Bounds.height; + newCollection._isGroup = makeGroup; newCollection.x = this.Bounds.left; newCollection.y = this.Bounds.top; selected.forEach(d => d.context = newCollection); @@ -385,19 +369,18 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque pileup = (e: KeyboardEvent | React.PointerEvent | undefined) => { const selected = this.marqueeSelect(false); SelectionManager.DeselectAll(); - selected.forEach(d => this.props.removeDocument(d)); + 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); - this.props.addDocument(newCollection!); + this.props.addDocument?.(newCollection!); this.props.selectDocuments([newCollection!]); MarqueeOptionsMenu.Instance.fadeOut(true); this.hideMarquee(); } - @undoBatch @action + @undoBatch + @action pinWithView = (e: KeyboardEvent | React.PointerEvent | undefined) => { const doc = this.props.Document; - const bounds = this.Bounds; - const selected = this.marqueeSelect(false); const curPres = Cast(Doc.UserDoc().activePresentation, Doc) as Doc; if (curPres) { if (doc === curPres) { alert("Cannot pin presentation document to itself"); return; } @@ -407,9 +390,9 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque pinDoc.groupWithUp = false; pinDoc.context = curPres; pinDoc.title = doc.title + " - Slide"; - const presArray: Doc[] = PresBox.Instance?.sortArray(); - const size: number = PresBox.Instance?._selectedArray.size; - const presSelected: Doc | undefined = presArray && size ? presArray[size - 1] : undefined; + 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)) { @@ -420,14 +403,10 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque const index = PresBox.Instance?.childDocs.indexOf(pinDoc); index && (curPres._itemIndex = index); if (e instanceof KeyboardEvent ? e.key === "c" : true) { - const x = this.Bounds.left + this.Bounds.width / 2; - const y = this.Bounds.top + this.Bounds.height / 2; - const panelWidth: number = this.props.PanelWidth(); - const panelHeight: number = this.props.PanelHeight(); - const scale = Math.min(Number(panelWidth) / this.Bounds.width, Number(panelHeight) / this.Bounds.height); + const scale = Math.min(this.props.PanelWidth() / this.Bounds.width, this.props.PanelHeight() / this.Bounds.height); pinDoc.presPinView = true; - pinDoc.presPinViewX = x; - pinDoc.presPinViewY = y; + pinDoc.presPinViewX = this.Bounds.left + this.Bounds.width / 2; + pinDoc.presPinViewY = this.Bounds.top + this.Bounds.height / 2; pinDoc.presPinViewScale = scale; } } @@ -435,11 +414,11 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque this.hideMarquee(); } - @undoBatch @action - collection = (e: KeyboardEvent | React.PointerEvent | undefined) => { - const bounds = this.Bounds; + @undoBatch + @action + collection = (e: KeyboardEvent | React.PointerEvent | undefined, group?: boolean) => { const selected = this.marqueeSelect(false); - if (e instanceof KeyboardEvent ? e.key === "c" : true) { + if (e instanceof KeyboardEvent ? "cg".includes(e.key) : true) { selected.map(action(d => { const dx = NumCast(d.x); const dy = NumCast(d.y); @@ -447,46 +426,36 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque delete d.y; delete d.activeFrame; delete d.displayTimecode; // bcz: this should be automatic somehow.. along with any other properties that were logically associated with the original collection - d.x = dx - bounds.left - bounds.width / 2; - d.y = dy - bounds.top - bounds.height / 2; + d.x = dx - this.Bounds.left - this.Bounds.width / 2; + d.y = dy - this.Bounds.top - this.Bounds.height / 2; return d; })); - this.props.removeDocument(selected); + this.props.removeDocument?.(selected); } - const newCollection = this.getCollection(selected, (e as KeyboardEvent)?.key === "t" ? Docs.Create.StackingDocument : undefined, []); - this.props.addDocument(newCollection); + 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); this.hideMarquee(); } - @undoBatch @action + @undoBatch + @action syntaxHighlight = (e: KeyboardEvent | React.PointerEvent | undefined) => { const selected = this.marqueeSelect(false); if (e instanceof KeyboardEvent ? e.key === "i" : true) { - const inks = selected.filter(s => s.proto?.type === "ink"); - const setDocs = selected.filter(s => s.proto?.type === "text" && s.color); - const sets = setDocs.map((sd) => { - return Cast(sd.data, RichTextField)?.Text as string; - }); + const inks = selected.filter(s => s.proto?.type === DocumentType.INK); + const setDocs = selected.filter(s => s.proto?.type === DocumentType.RTF && s.color); + const sets = setDocs.map((sd) => Cast(sd.data, RichTextField)?.Text as string); const colors = setDocs.map(sd => FieldValue(sd.color) as string); const wordToColor = new Map<string, string>(); - sets.forEach((st: string, i: number) => { - const words = st.split(","); - words.forEach(word => { - wordToColor.set(word, colors[i]); - }); - }); + sets.forEach((st: string, i: number) => st.split(",").forEach(word => wordToColor.set(word, colors[i]))); const strokes: InkData[] = []; - inks.forEach(i => { - const d = Cast(i.data, InkField); - const x = NumCast(i.x); - const y = NumCast(i.y); + inks.filter(i => Cast(i.data, InkField)).forEach(i => { + const d = Cast(i.data, InkField, null); const left = Math.min(...d?.inkData.map(pd => pd.X) ?? [0]); const top = Math.min(...d?.inkData.map(pd => pd.Y) ?? [0]); - if (d) { - strokes.push(d.inkData.map(pd => ({ X: pd.X + x - left, Y: pd.Y + y - top }))); - } + strokes.push(d.inkData.map(pd => ({ X: pd.X + NumCast(i.x) - left, Y: pd.Y + NumCast(i.y) - top }))); }); CognitiveServices.Inking.Appliers.InterpretStrokes(strokes).then((results) => { // const wordResults = results.filter((r: any) => r.category === "inkWord"); @@ -531,23 +500,21 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque // } const lines = results.filter((r: any) => r.category === "line"); const text = lines.map((l: any) => l.recognizedText).join("\r\n"); - this.props.addDocument(Docs.Create.TextDocument(text, { _width: this.Bounds.width, _height: this.Bounds.height, x: this.Bounds.left + this.Bounds.width, y: this.Bounds.top, title: text })); + this.props.addDocument?.(Docs.Create.TextDocument(text, { _width: this.Bounds.width, _height: this.Bounds.height, x: this.Bounds.left + this.Bounds.width, y: this.Bounds.top, title: text })); }); } } - @undoBatch @action + @undoBatch + @action summary = (e: KeyboardEvent | React.PointerEvent | undefined) => { - const bounds = this.Bounds; - const selected = this.marqueeSelect(false); - selected.map(d => { - this.props.removeDocument(d); - d.x = NumCast(d.x) - bounds.left; - d.y = NumCast(d.y) - bounds.top; - d.page = -1; + const selected = this.marqueeSelect(false).map(d => { + this.props.removeDocument?.(d); + d.x = NumCast(d.x) - this.Bounds.left; + d.y = NumCast(d.y) - this.Bounds.top; return d; }); - const summary = Docs.Create.TextDocument("", { x: bounds.left, y: bounds.top, _width: 200, _height: 200, _fitToBox: true, _showSidebar: true, title: "overview" }); + 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"); @@ -559,18 +526,18 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque this.props.addLiveTextDocument(summary); MarqueeOptionsMenu.Instance.fadeOut(true); } + @action background = (e: KeyboardEvent | React.PointerEvent | undefined) => { - const newCollection = this.getCollection([], undefined, ["background"]); - this.props.addDocument(newCollection); + const newCollection = this.getCollection([], undefined, [StyleLayers.Background], undefined); + this.props.addDocument?.(newCollection); MarqueeOptionsMenu.Instance.fadeOut(true); this.hideMarquee(); - setTimeout(() => this.props.selectDocuments([newCollection]), 0); + setTimeout(() => this.props.selectDocuments([newCollection])); } @undoBatch - @action - marqueeCommand = async (e: KeyboardEvent) => { + marqueeCommand = action((e: KeyboardEvent) => { if (this._commandExecuted || (e as any).propagationIsStopped) { return; } @@ -581,83 +548,31 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque this.delete(); e.stopPropagation(); } - if (e.key === "c" || e.key === "b" || e.key === "t" || e.key === "s" || e.key === "S" || e.key === "p") { + if ("cbtsSpg".indexOf(e.key) !== -1) { this._commandExecuted = true; e.stopPropagation(); e.preventDefault(); (e as any).propagationIsStopped = true; - if (e.key === "c" || e.key === "t") { - this.collection(e); - } - if (e.key === "s" || e.key === "S") { - this.summary(e); - } - if (e.key === "b") { - this.background(e); - } - if (e.key === "p") { - this.pileup(e); - } + if (e.key === "g") this.collection(e, true); + if (e.key === "c" || e.key === "t") this.collection(e); + if (e.key === "s" || e.key === "S") this.summary(e); + if (e.key === "b") this.background(e); + if (e.key === "p") this.pileup(e); this.cleanupInteractions(false); } if (e.key === "r" || e.key === " ") { this._commandExecuted = true; e.stopPropagation(); e.preventDefault(); - this.changeFreeHand(true); + this._lassoFreehand = !this._lassoFreehand; } - } + }); - @action - changeFreeHand = (x: boolean) => { - this._freeHand = !this._freeHand; - } - // @action - // marqueeInkSelect(ink: Map<any, any>) { - // let idata = new Map(); - // let centerShiftX = 0 - (this.Bounds.left + this.Bounds.width / 2); // moves each point by the offset that shifts the selection's center to the origin. - // let centerShiftY = 0 - (this.Bounds.top + this.Bounds.height / 2); - // ink.forEach((value: PointData, key: string, map: any) => { - // if (InkingCanvas.IntersectStrokeRect(value, this.Bounds)) { - // // let transform = this.props.container.props.ScreenToLocalTransform().scale(this.props.container.props.ContentScaling()); - // idata.set(key, - // { - // pathData: value.pathData.map(val => { - // let tVal = this.props.getTransform().inverse().transformPoint(val.x, val.y); - // return { x: tVal[0], y: tVal[1] }; - // // return { x: val.x + centerShiftX, y: val.y + centerShiftY } - // }), - // color: value.color, - // width: value.width, - // tool: value.tool, - // page: -1 - // }); - // } - // }); - // // InkSelectDecorations.Instance.SetSelected(idata); - // return idata; - // } - - // @action - // marqueeInkDelete(ink?: Map<any, any>) { - // // bcz: this appears to work but when you restart all the deleted strokes come back -- InkField isn't observing its changes so they aren't written to the DB. - // // ink.forEach((value: StrokeData, key: string, map: any) => - // // InkingCanvas.IntersectStrokeRect(value, this.Bounds) && ink.delete(key)); - - // if (ink) { - // let idata = new Map(); - // ink.forEach((value: PointData, key: string, map: any) => - // !InkingCanvas.IntersectStrokeRect(value, this.Bounds) && idata.set(key, value)); - // this.ink = new InkField(idata); - // } - // } touchesLine(r1: { left: number, top: number, width: number, height: number }) { - for (var i = 0; i < this._pointsX.length; i++) { - const topLeft = this.props.getTransform().transformPoint(this._pointsX[i], this._pointsY[i]); - if (topLeft[0] > r1.left && - topLeft[0] < r1.left + r1.width && - topLeft[1] > r1.top && - topLeft[1] < r1.top + r1.height) { + for (const lassoPt of this._lassoPts) { + const topLeft = this.Transform.transformPoint(lassoPt[0], lassoPt[1]); + if (r1.left < topLeft[0] && topLeft[0] < r1.left + r1.width && + r1.top < topLeft[1] && topLeft[1] < r1.top + r1.height) { return true; } } @@ -665,30 +580,22 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque } boundingShape(r1: { left: number, top: number, width: number, height: number }) { - const trueLeft = this.props.getTransform().transformPoint(Math.min(...this._pointsX), Math.min(...this._pointsY))[0]; - const trueTop = this.props.getTransform().transformPoint(Math.min(...this._pointsX), Math.min(...this._pointsY))[1]; - const trueRight = this.props.getTransform().transformPoint(Math.max(...this._pointsX), Math.max(...this._pointsY))[0]; - const trueBottom = this.props.getTransform().transformPoint(Math.max(...this._pointsX), Math.max(...this._pointsY))[1]; - - if (r1.left > trueLeft && r1.top > trueTop && r1.left + r1.width < trueRight && r1.top + r1.height < trueBottom) { - var hasTop = false; - var hasLeft = false; - var hasBottom = false; - var hasRight = false; - for (var i = 0; i < this._pointsX.length; i++) { - const truePoint = this.props.getTransform().transformPoint(this._pointsX[i], this._pointsY[i]); - if (!hasLeft && (truePoint[0] > trueLeft && truePoint[0] < r1.left) && (truePoint[1] > r1.top && truePoint[1] < r1.top + r1.height)) { - hasLeft = true; - } - if (!hasTop && (truePoint[1] > trueTop && truePoint[1] < r1.top) && (truePoint[0] > r1.left && truePoint[0] < r1.left + r1.width)) { - hasTop = true; - } - if (!hasRight && (truePoint[0] < trueRight && truePoint[0] > r1.left + r1.width) && (truePoint[1] > r1.top && truePoint[1] < r1.top + r1.height)) { - hasRight = true; - } - if (!hasBottom && (truePoint[1] < trueBottom && truePoint[1] > r1.top + r1.height) && (truePoint[0] > r1.left && truePoint[0] < r1.left + r1.width)) { - hasBottom = true; - } + const xs = this._lassoPts.map(pair => pair[0]); + const ys = this._lassoPts.map(pair => pair[1]); + const tl = this.Transform.transformPoint(Math.min(...xs), Math.min(...ys)); + const br = this.Transform.transformPoint(Math.max(...xs), Math.max(...ys)); + + if (r1.left > tl[0] && r1.top > tl[1] && r1.left + r1.width < br[0] && r1.top + r1.height < br[1]) { + let hasTop = false; + let hasLeft = false; + let hasBottom = false; + let hasRight = false; + for (const lassoPt of this._lassoPts) { + const truePoint = this.Transform.transformPoint(lassoPt[0], lassoPt[1]); + hasLeft = hasLeft || (truePoint[0] > tl[0] && truePoint[0] < r1.left) && (truePoint[1] > r1.top && truePoint[1] < r1.top + r1.height); + hasTop = hasTop || (truePoint[1] > tl[1] && truePoint[1] < r1.top) && (truePoint[0] > r1.left && truePoint[0] < r1.left + r1.width); + hasRight = hasRight || (truePoint[0] < br[0] && truePoint[0] > r1.left + r1.width) && (truePoint[1] > r1.top && truePoint[1] < r1.top + r1.height); + hasBottom = hasBottom || (truePoint[1] < br[1] && truePoint[1] > r1.top + r1.height) && (truePoint[0] > r1.left && truePoint[0] < r1.left + r1.width); if (hasTop && hasLeft && hasBottom && hasRight) { return true; } @@ -696,111 +603,45 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque } return false; } + marqueeSelect(selectBackgrounds: boolean = true) { - const selRect = this.Bounds; const selection: Doc[] = []; - this.props.activeDocuments().filter(doc => this.props.layerProvider?.(doc) !== false && !doc.z).map(doc => { + const selectFunc = (doc: Doc) => { const layoutDoc = Doc.Layout(doc); - const x = NumCast(doc.x); - const y = NumCast(doc.y); - const w = NumCast(layoutDoc._width); - const h = NumCast(layoutDoc._height); - if (this._freeHand === false) { - if (this.intersectRect({ left: x, top: y, width: w, height: h }, selRect)) { - selection.push(doc); - } + const bounds = { left: NumCast(doc.x), top: NumCast(doc.y), width: NumCast(layoutDoc._width), height: NumCast(layoutDoc._height) }; + if (!this._lassoFreehand) { + intersectRect(bounds, this.Bounds) && selection.push(doc); } else { - if (this.touchesLine({ left: x, top: y, width: w, height: h }) || - this.boundingShape({ left: x, top: y, width: w, height: h })) { - selection.push(doc); - } + (this.touchesLine(bounds) || this.boundingShape(bounds)) && selection.push(doc); } - }); - if (!selection.length && selectBackgrounds) { - this.props.activeDocuments().filter(doc => doc.z === undefined).map(doc => { - const layoutDoc = Doc.Layout(doc); - const x = NumCast(doc.x); - const y = NumCast(doc.y); - const w = NumCast(layoutDoc._width); - const h = NumCast(layoutDoc._height); - if (this.intersectRect({ left: x, top: y, width: w, height: h }, selRect)) { - selection.push(doc); - } - }); - } - if (!selection.length) { - const left = this._downX < this._lastX ? this._downX : this._lastX; - const top = this._downY < this._lastY ? this._downY : this._lastY; - const topLeft = this.props.getContainerTransform().transformPoint(left, top); - const size = this.props.getContainerTransform().transformDirection(this._lastX - this._downX, this._lastY - this._downY); - const otherBounds = { left: topLeft[0], top: topLeft[1], width: Math.abs(size[0]), height: Math.abs(size[1]) }; - this.props.activeDocuments().filter(doc => doc.z !== undefined).map(doc => { - const layoutDoc = Doc.Layout(doc); - const x = NumCast(doc.x); - const y = NumCast(doc.y); - const w = NumCast(layoutDoc._width); - const h = NumCast(layoutDoc._height); - if (this._freeHand === false) { - if (this.intersectRect({ left: x, top: y, width: w, height: h }, selRect)) { - selection.push(doc); - } - } else { - if (this.touchesLine({ left: x, top: y, width: w, height: h }) || - this.boundingShape({ left: x, top: y, width: w, height: h })) { - selection.push(doc); - } - } - }); - } + }; + this.props.activeDocuments().filter(doc => this.props.layerProvider?.(doc) !== false && !doc.z).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; } - @computed - get marqueeDiv() { - const p = this._visible ? this.props.getContainerTransform().transformPoint(this._downX < this._lastX ? this._downX : this._lastX, this._downY < this._lastY ? this._downY : this._lastY) : [0, 0]; - const v = this.props.getContainerTransform().transformDirection(this._lastX - this._downX, this._lastY - this._downY); - /** - * @RE - The commented out span below - * This contains the "C for collection, ..." text on marquees. - * Commented out by syip2 when the marquee menu was added. - */ - if (!this._freeHand) { - return <div className="marquee" style={{ - transform: `translate(${p[0]}px, ${p[1]}px)`, - width: `${Math.abs(v[0])}`, - height: `${Math.abs(v[1])}`, zIndex: 2000 - }} > - <span className="marquee-legend"></span> - </div>; - - } else { - var str: string = ""; - for (var i = 0; i < this._pointsX.length; i++) { - const pt = this.props.getContainerTransform().transformPoint(this._pointsX[i], this._pointsY[i]); - str += pt[0].toString(); - str += ","; - str += pt[1].toString(); - str += (" "); - } - - //hardcoded height and width. - return <div className="marquee" style={{ zIndex: 2000 }}> - <svg height={2000} width={2000}> - <polyline - points={str} - fill="none" - stroke="black" - strokeWidth="1" - strokeDasharray="3" - /> - </svg> - </div>; - } + @computed get marqueeDiv() { + const cpt = this._lassoFreehand || !this._visible ? [0, 0] : [this._downX < this._lastX ? this._downX : this._lastX, this._downY < this._lastY ? this._downY : this._lastY]; + const p = this.props.getContainerTransform().transformPoint(cpt[0], cpt[1]); + const v = this._lassoFreehand ? [0, 0] : this.props.getContainerTransform().transformDirection(this._lastX - this._downX, this._lastY - this._downY); + return <div className="marquee" style={{ + transform: `translate(${p[0]}px, ${p[1]}px)`, + width: Math.abs(v[0]), + height: Math.abs(v[1]), + zIndex: 2000 + }}> {this._lassoFreehand ? + <svg height={2000} width={2000}> + <polyline points={this._lassoPts.reduce((s, pt) => s + pt[0] + "," + pt[1] + " ", "")} fill="none" stroke="black" strokeWidth="1" strokeDasharray="3" /> + </svg> + : + <span className="marquee-legend" />} + </div>; } render() { return <div className="marqueeView" - style={{ overflow: !this.props.ContainingCollectionView && this.props.annotationsKey ? "visible" : StrCast(this.props.Document._overflow), cursor: MarqueeView.DragMarquee && this ? "crosshair" : "hand" }} + style={{ overflow: (!this.props.ContainingCollectionView && this.props.isAnnotationOverlay) ? "visible" : StrCast(this.props.Document._overflow), cursor: MarqueeView.DragMarquee && this ? "crosshair" : "hand" }} onDragOver={e => e.preventDefault()} onScroll={(e) => e.currentTarget.scrollTop = e.currentTarget.scrollLeft = 0} onClick={this.onClick} onPointerDown={this.onPointerDown}> {this._visible ? this.marqueeDiv : null} |
