From 0b38b0629496973d6c4571208710096deb91b7d7 Mon Sep 17 00:00:00 2001 From: srichman333 Date: Fri, 24 Nov 2023 17:59:13 -0500 Subject: merge --- .../CollectionFreeFormBackgroundGrid.tsx | 75 +++ .../CollectionFreeFormLayoutEngines.tsx | 17 +- .../CollectionFreeFormLinkView.tsx | 20 +- .../CollectionFreeFormPannableContents.tsx | 60 ++ .../collectionFreeForm/CollectionFreeFormView.tsx | 660 ++++++--------------- .../collections/collectionFreeForm/MarqueeView.tsx | 46 +- 6 files changed, 349 insertions(+), 529 deletions(-) create mode 100644 src/client/views/collections/collectionFreeForm/CollectionFreeFormBackgroundGrid.tsx create mode 100644 src/client/views/collections/collectionFreeForm/CollectionFreeFormPannableContents.tsx (limited to 'src/client/views/collections/collectionFreeForm') diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormBackgroundGrid.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormBackgroundGrid.tsx new file mode 100644 index 000000000..00505dbe3 --- /dev/null +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormBackgroundGrid.tsx @@ -0,0 +1,75 @@ +import { observer } from 'mobx-react'; +import { Doc } from '../../../../fields/Doc'; +import { NumCast } from '../../../../fields/Types'; +import './CollectionFreeFormView.scss'; +import React = require('react'); + +export interface CollectionFreeFormViewBackgroundGridProps { + panX: () => number; + panY: () => number; + PanelWidth: () => number; + PanelHeight: () => number; + color: () => string; + isAnnotationOverlay?: boolean; + nativeDimScaling: () => number; + zoomScaling: () => number; + layoutDoc: Doc; + cachedCenteringShiftX: number; + cachedCenteringShiftY: number; +} +@observer +export class CollectionFreeFormBackgroundGrid extends React.Component { + chooseGridSpace = (gridSpace: number): number => { + if (!this.props.zoomScaling()) return gridSpace; + const divisions = this.props.PanelWidth() / this.props.zoomScaling() / gridSpace; + return divisions < 90 ? gridSpace : this.chooseGridSpace(gridSpace * 2); + }; + render() { + const gridSpace = this.chooseGridSpace(NumCast(this.props.layoutDoc['_backgroundGrid-spacing'], 50)); + const shiftX = (this.props.isAnnotationOverlay ? 0 : (-this.props.panX() % gridSpace) - gridSpace) * this.props.zoomScaling(); + const shiftY = (this.props.isAnnotationOverlay ? 0 : (-this.props.panY() % gridSpace) - gridSpace) * this.props.zoomScaling(); + const renderGridSpace = gridSpace * this.props.zoomScaling(); + const w = this.props.PanelWidth() / this.props.nativeDimScaling() + 2 * renderGridSpace; + const h = this.props.PanelHeight() / this.props.nativeDimScaling() + 2 * renderGridSpace; + const strokeStyle = this.props.color(); + return !this.props.nativeDimScaling() ? null : ( + { + const ctx = el?.getContext('2d'); + if (ctx) { + const Cx = this.props.cachedCenteringShiftX % renderGridSpace; + const Cy = this.props.cachedCenteringShiftY % renderGridSpace; + ctx.lineWidth = Math.min(1, Math.max(0.5, this.props.zoomScaling())); + ctx.setLineDash(gridSpace > 50 ? [3, 3] : [1, 5]); + ctx.clearRect(0, 0, w, h); + if (ctx) { + ctx.strokeStyle = strokeStyle; + ctx.fillStyle = strokeStyle; + ctx.beginPath(); + if (this.props.zoomScaling() > 1) { + for (let x = Cx - renderGridSpace; x <= w - Cx; x += renderGridSpace) { + ctx.moveTo(x, Cy - h); + ctx.lineTo(x, Cy + h); + } + for (let y = Cy - renderGridSpace; y <= h - Cy; y += renderGridSpace) { + ctx.moveTo(Cx - w, y); + ctx.lineTo(Cx + w, y); + } + } else { + for (let x = Cx - renderGridSpace; x <= w - Cx; x += renderGridSpace) + for (let y = Cy - renderGridSpace; y <= h - Cy; y += renderGridSpace) { + ctx.fillRect(Math.round(x), Math.round(y), 1, 1); + } + } + ctx.stroke(); + } + } + }} + /> + ); + } +} diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx index d93e44ab7..403fba67b 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx @@ -1,5 +1,4 @@ import { Doc, Field, FieldResult } from '../../../../fields/Doc'; -import { Height, Width } from '../../../../fields/DocSymbols'; import { Id, ToString } from '../../../../fields/FieldSymbols'; import { ObjectField } from '../../../../fields/ObjectField'; import { RefField } from '../../../../fields/RefField'; @@ -29,6 +28,8 @@ export interface ViewDefBounds { } export interface PoolData { + pair: { layout: Doc; data?: Doc }; + replica: string; x: number; y: number; z?: number; @@ -41,9 +42,7 @@ export interface PoolData { opacity?: number; transition?: string; highlight?: boolean; - replica: string; - pointerEvents?: string; // without this, toggling lockPosition of a group/collection in a freeform view won't update until something else invalidates the freeform view's documents forcing -- this is a problem with doLayoutComputation which makes a performance test to insure somethingChanged - pair: { layout: Doc; data?: Doc }; + pointerEvents?: string; } export interface ViewDefResult { @@ -91,8 +90,8 @@ export function computePassLayout(poolData: Map, pivotDoc: Doc docMap.set(layout[Id], { x: NumCast(layout.x), y: NumCast(layout.y), - width: layout[Width](), - height: layout[Height](), + width: NumCast(layout._width), + height: NumCast(layout._height), pair: { layout, data }, transition: 'all .3s', replica: '', @@ -106,8 +105,8 @@ export function computeStarburstLayout(poolData: Map, pivotDoc const burstDiam = [NumCast(pivotDoc._width), NumCast(pivotDoc._height)]; const burstScale = NumCast(pivotDoc._starburstDocScale, 1); childPairs.forEach(({ layout, data }, i) => { - const aspect = layout[Height]() / layout[Width](); - const docSize = Math.min(Math.min(400, layout[Width]()), Math.min(400, layout[Width]()) / aspect) * burstScale; + const aspect = NumCast(layout._height) / NumCast(layout._width); + const docSize = Math.min(Math.min(400, NumCast(layout._width)), Math.min(400, NumCast(layout._width)) / aspect) * burstScale; const deg = (i / childPairs.length) * Math.PI * 2; docMap.set(layout[Id], { x: Math.min(burstDiam[0] / 2 - docSize, Math.max(-burstDiam[0] / 2, (Math.cos(deg) * burstDiam[0]) / 2 - docSize / 2)), @@ -156,7 +155,7 @@ export function computePivotLayout(poolData: Map, pivotDoc: Do x: 0, y: 0, zIndex: 0, - width: 0, // should make doc hidden in CollectionFreefromDocumentView + width: 0, // should make doc hidden in CollectionFreeFormDocumentView height: 0, pair, replica: '', diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx index 24a758d8c..aca6df3c9 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx @@ -1,7 +1,7 @@ import { action, computed, IReactionDisposer, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import { Doc, Field } from '../../../../fields/Doc'; -import { DocCss } from '../../../../fields/DocSymbols'; +import { Brushed, DocCss } from '../../../../fields/DocSymbols'; import { Id } from '../../../../fields/FieldSymbols'; import { List } from '../../../../fields/List'; import { Cast, NumCast, StrCast } from '../../../../fields/Types'; @@ -223,8 +223,8 @@ export class CollectionFreeFormLinkView extends React.Component - - + + diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormPannableContents.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormPannableContents.tsx new file mode 100644 index 000000000..856e195a3 --- /dev/null +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormPannableContents.tsx @@ -0,0 +1,60 @@ +import { computed } from 'mobx'; +import { observer } from 'mobx-react'; +import { Doc } from '../../../../fields/Doc'; +import { ScriptField } from '../../../../fields/ScriptField'; +import { PresBox } from '../../nodes/trails/PresBox'; +import './CollectionFreeFormView.scss'; +import React = require('react'); +import { CollectionFreeFormView } from './CollectionFreeFormView'; + +export interface CollectionFreeFormPannableContentsProps { + rootDoc: Doc; + viewDefDivClick?: ScriptField; + children?: React.ReactNode | undefined; + transition?: string; + isAnnotationOverlay: boolean | undefined; + transform: () => string; + brushedView: () => { panX: number; panY: number; width: number; height: number } | undefined; +} + +@observer +export class CollectionFreeFormPannableContents extends React.Component { + @computed get presPaths() { + return CollectionFreeFormView.ShowPresPaths ? PresBox.Instance.pathLines(this.props.rootDoc) : null; + } + // rectangle highlight used when following trail/link to a region of a collection that isn't a document + showViewport = (viewport: { panX: number; panY: number; width: number; height: number } | undefined) => + !viewport ? null : ( +
+ ); + + render() { + return ( +
{ + const target = e.target as any; + if (getComputedStyle(target)?.overflow === 'visible') { + target.scrollTop = target.scrollLeft = 0; // if collection is visible, scrolling messes things up since there are no scroll bars + } + }} + style={{ + transform: this.props.transform(), + transition: this.props.transition, + width: this.props.isAnnotationOverlay ? undefined : 0, // if not an overlay, then this will be the size of the collection, but panning and zooming will move it outside the visible border of the collection and make it selectable. This problem shows up after zooming/panning on a background collection -- you can drag the collection by clicking on apparently empty space outside the collection + }}> + {this.props.children} + {this.presPaths} + {this.showViewport(this.props.brushedView())} +
+ ); + } +} diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index da0f7c893..e350c35cc 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -9,20 +9,18 @@ import { DocData, Height, Width } from '../../../../fields/DocSymbols'; import { Id } from '../../../../fields/FieldSymbols'; import { InkData, InkField, InkTool, PointData, Segment } from '../../../../fields/InkField'; import { List } from '../../../../fields/List'; -import { RichTextField } from '../../../../fields/RichTextField'; import { listSpec } from '../../../../fields/Schema'; import { ScriptField } from '../../../../fields/ScriptField'; -import { BoolCast, Cast, DocCast, FieldValue, NumCast, ScriptCast, StrCast } from '../../../../fields/Types'; +import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types'; import { ImageField } from '../../../../fields/URLField'; import { TraceMobx } from '../../../../fields/util'; import { GestureUtils } from '../../../../pen-gestures/GestureUtils'; -import { aggregateBounds, DashColor, emptyFunction, intersectRect, lightOrDark, returnFalse, returnZero, setupMoveUpEvents, Utils } from '../../../../Utils'; +import { aggregateBounds, DashColor, emptyFunction, intersectRect, lightOrDark, OmitKeys, returnFalse, returnZero, setupMoveUpEvents, Utils } from '../../../../Utils'; import { CognitiveServices } from '../../../cognitive_services/CognitiveServices'; import { Docs, DocUtils } from '../../../documents/Documents'; import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes'; import { DocumentManager } from '../../../util/DocumentManager'; import { DragManager, dropActionType } from '../../../util/DragManager'; -import { InteractionUtils } from '../../../util/InteractionUtils'; import { FollowLinkScript } from '../../../util/LinkFollower'; import { ReplayMovements } from '../../../util/ReplayMovements'; import { ScriptingGlobals } from '../../../util/ScriptingGlobals'; @@ -31,13 +29,13 @@ import { freeformScrollMode } from '../../../util/SettingsManager'; import { SnappingManager } from '../../../util/SnappingManager'; import { Transform } from '../../../util/Transform'; import { undoBatch, UndoManager } from '../../../util/UndoManager'; -import { COLLECTION_BORDER_WIDTH } from '../../../views/global/globalCssVariables.scss'; import { Timeline } from '../../animationtimeline/Timeline'; import { ContextMenu } from '../../ContextMenu'; import { GestureOverlay } from '../../GestureOverlay'; -import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, ActiveIsInkMask, InkingStroke, SetActiveInkColor, SetActiveInkWidth } from '../../InkingStroke'; +import { CtrlKey } from '../../GlobalKeyHandler'; +import { ActiveInkWidth, InkingStroke, SetActiveInkColor, SetActiveInkWidth } from '../../InkingStroke'; import { LightboxView } from '../../LightboxView'; -import { CollectionFreeFormDocumentView } from '../../nodes/CollectionFreeFormDocumentView'; +import { CollectionFreeFormDocumentView, CollectionFreeFormDocumentViewWrapper } from '../../nodes/CollectionFreeFormDocumentView'; import { DocFocusOptions, DocumentView, DocumentViewProps, OpenWhere } from '../../nodes/DocumentView'; import { FieldViewProps } from '../../nodes/FieldView'; import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox'; @@ -46,8 +44,9 @@ import { CreateImage } from '../../nodes/WebBoxRenderer'; import { StyleProp } from '../../StyleProvider'; import { CollectionSubView } from '../CollectionSubView'; import { TreeViewType } from '../CollectionTreeView'; -import { TabDocView } from '../TabDocView'; +import { CollectionFreeFormBackgroundGrid } from './CollectionFreeFormBackgroundGrid'; import { computePassLayout, computePivotLayout, computeStarburstLayout, computeTimelineLayout, PoolData, ViewDefBounds, ViewDefResult } from './CollectionFreeFormLayoutEngines'; +import { CollectionFreeFormPannableContents } from './CollectionFreeFormPannableContents'; import { CollectionFreeFormRemoteCursors } from './CollectionFreeFormRemoteCursors'; import './CollectionFreeFormView.scss'; import { MarqueeView } from './MarqueeView'; @@ -81,16 +80,10 @@ export class CollectionFreeFormView extends CollectionSubView = new Map(); private _clusterDistance: number = 75; private _hitCluster: number = -1; private _disposers: { [name: string]: IReactionDisposer } = {}; private _renderCutoffData = observable.map(); - private _layoutPoolData = observable.map(); - private _layoutSizeData = observable.map(); - private _cachedPool: Map = new Map(); private _batch: UndoManager.Batch | undefined = undefined; private _brushtimer: any; private _brushtimer1: any; @@ -110,9 +103,6 @@ export class CollectionFreeFormView extends CollectionSubView(); @observable _marqueeViewRef = React.createRef(); - @observable GroupChildDrag: boolean = false; // child document view being dragged. needed to update drop areas of groups when a group item is dragged. @observable _brushedView: { width: number; height: number; panX: number; panY: number } | undefined; // highlighted region of freeform canvas used by presentations to indicate a region + @observable GroupChildDrag: boolean = false; // child document view being dragged. needed to update drop areas of groups when a group item is dragged. - @computed get views() { + @computed get contentViews() { const viewsMask = this._layoutElements.filter(ele => ele.bounds && !ele.bounds.z && ele.inkMask !== -1 && ele.inkMask !== undefined).map(ele => ele.ele); const renderableEles = this._layoutElements.filter(ele => ele.bounds && !ele.bounds.z && (ele.inkMask === -1 || ele.inkMask === undefined)).map(ele => ele.ele); if (viewsMask.length) renderableEles.push(
(ele.inkMask ?? 0) > 0) ? '' : '-empty'}`}>{viewsMask}
); @@ -170,19 +160,15 @@ export class CollectionFreeFormView extends CollectionSubView this.freeformData()?.bounds.cx ?? NumCast(this.Document[this.panXFieldKey], NumCast(Cast(this.Document.resolvedDataDoc, Doc, null)?.freeform_panX, 1)); panY = () => this.freeformData()?.bounds.cy ?? NumCast(this.Document[this.panYFieldKey], NumCast(Cast(this.Document.resolvedDataDoc, Doc, null)?.freeform_panY, 1)); zoomScaling = () => this.freeformData()?.scale ?? NumCast(Doc.Layout(this.Document)[this.scaleFieldKey], NumCast(Cast(this.Document.resolvedDataDoc, Doc, null)?.[this.scaleFieldKey], 1)); - contentTransform = () => + PanZoomCenterXf = () => this.props.isAnnotationOverlay && 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(); - getContainerTransform = () => this.cachedGetContainerTransform.copy(); + ScreenToLocalXf = () => this.screenToLocalXf.copy(); getActiveDocuments = () => this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map(pair => pair.layout); isAnyChildContentActive = () => this.props.isAnyChildContentActive(); addLiveTextBox = (newBox: Doc) => { @@ -260,7 +244,7 @@ export class CollectionFreeFormView extends CollectionSubView { SelectionManager.DeselectAll(); - docs.map(doc => DocumentManager.Instance.getDocumentView(doc, this.props.DocumentView?.())).map(dv => dv && SelectionManager.SelectView(dv, true)); + docs.map(doc => DocumentManager.Instance.getDocumentView(doc, this.props.DocumentView?.())).forEach(dv => dv && SelectionManager.SelectView(dv, true)); }; addDocument = (newBox: Doc | Doc[]) => { let retVal = false; @@ -331,19 +315,18 @@ export class CollectionFreeFormView extends CollectionSubView> => { - return new Promise>(res => { + getView = async (doc: Doc): Promise> => + new Promise>(res => { if (doc.hidden && this._lightboxDoc !== doc) doc.hidden = false; const findDoc = (finish: (dv: DocumentView) => void) => DocumentManager.Instance.AddViewRenderedCb(doc, dv => finish(dv)); findDoc(dv => res(dv)); }); - }; @action internalDocDrop(e: Event, de: DragManager.DropEvent, docDragData: DragManager.DocumentDragData, xp: number, yp: number) { if (!super.onInternalDrop(e, de)) return false; const refDoc = docDragData.droppedDocuments[0]; - const [xpo, ypo] = this.getContainerTransform().transformPoint(de.x, de.y); + const [xpo, ypo] = this.props.ScreenToLocalTransform().transformPoint(de.x, de.y); const z = NumCast(refDoc.z); const x = (z ? xpo : xp) - docDragData.offset[0]; const y = (z ? ypo : yp) - docDragData.offset[1]; @@ -358,16 +341,17 @@ export class CollectionFreeFormView extends CollectionSubView { - const [xp, yp] = this.getTransform().transformPoint(de.x, de.y); + const [xp, yp] = this.screenToLocalXf.transformPoint(de.x, de.y); if (de.complete.annoDragData?.dragDocument && super.onInternalDrop(e, de)) return this.internalAnchorAnnoDrop(e, de.complete.annoDragData, xp, yp); else if (de.complete.linkDragData) return this.internalLinkDrop(e, de, de.complete.linkDragData, xp, yp); else if (de.complete.docDragData?.droppedDocuments.length) return this.internalDocDrop(e, de, de.complete.docDragData, xp, yp); return false; }; - onExternalDrop = (e: React.DragEvent) => (([x, y]) => super.onExternalDrop(e, { x, y }))(this.getTransform().transformPoint(e.pageX, e.pageY)); - + onExternalDrop = (e: React.DragEvent) => (([x, y]) => super.onExternalDrop(e, { x, y }))(this.screenToLocalXf.transformPoint(e.pageX, e.pageY)); + + static overlapping(doc1: Doc, doc2: Doc, clusterDistance: number) { + const doc2Layout = Doc.Layout(doc2); + const doc1Layout = Doc.Layout(doc1); + const x2 = NumCast(doc2.x) - clusterDistance; + const y2 = NumCast(doc2.y) - clusterDistance; + const w2 = NumCast(doc2Layout._width) + clusterDistance; + const h2 = NumCast(doc2Layout._height) + clusterDistance; + const x = NumCast(doc1.x) - clusterDistance; + const y = NumCast(doc1.y) - clusterDistance; + const w = NumCast(doc1Layout._width) + clusterDistance; + const h = NumCast(doc1Layout._height) + clusterDistance; + return doc1.z === doc2.z && intersectRect({ left: x, top: y, width: w, height: h }, { left: x2, top: y2, width: w2, height: h2 }); + } pickCluster(probe: number[]) { return this.childLayoutPairs .map(pair => pair.layout) @@ -477,16 +474,16 @@ export class CollectionFreeFormView extends CollectionSubView pair.layout).filter(cd => (this.props.Document._freeform_useClusters ? NumCast(cd.layout_cluster) : NumCast(cd.group, -1)) === cluster); const clusterDocs = eles.map(ele => DocumentManager.Instance.getDocumentView(ele, this.props.DocumentView?.())!); const { left, top } = clusterDocs[0].getBounds() || { left: 0, top: 0 }; const de = new DragManager.DocumentDragData(eles, e.ctrlKey || e.altKey ? 'embed' : undefined); de.moveDocument = this.props.moveDocument; - de.offset = this.getTransform().transformDirection(ptsParent.clientX - left, ptsParent.clientY - top); + de.offset = this.screenToLocalXf.transformDirection(ptsParent.clientX - left, ptsParent.clientY - top); DragManager.StartDocumentDrag( clusterDocs.map(v => v.ContentDiv!), de, @@ -519,7 +516,7 @@ export class CollectionFreeFormView extends CollectionSubView this._clusterSets.map((set, i) => set.map(member => { - if (docFirst.layout_cluster === -1 && Doc.IndexOf(member, childLayouts) !== -1 && Doc.overlapping(doc, member, this._clusterDistance)) { + if (docFirst.layout_cluster === -1 && Doc.IndexOf(member, childLayouts) !== -1 && CollectionFreeFormView.overlapping(doc, member, this._clusterDistance)) { docFirst.layout_cluster = i; } }) @@ -552,7 +549,7 @@ export class CollectionFreeFormView extends CollectionSubView { const childLayouts = this.childLayoutPairs.map(pair => pair.layout); if (this.props.Document._freeform_useClusters) { this._clusterSets.forEach(set => Doc.IndexOf(doc, set) !== -1 && set.splice(Doc.IndexOf(doc, set), 1)); @@ -560,7 +557,7 @@ export class CollectionFreeFormView extends CollectionSubView set.forEach(member => { - if (doc.layout_cluster === -1 && Doc.IndexOf(member, childLayouts) !== -1 && Doc.overlapping(doc, member, this._clusterDistance)) { + if (doc.layout_cluster === -1 && Doc.IndexOf(member, childLayouts) !== -1 && CollectionFreeFormView.overlapping(doc, member, this._clusterDistance)) { doc.layout_cluster = i; } }) @@ -581,9 +578,9 @@ export class CollectionFreeFormView extends CollectionSubView, props: Opt, property: string) => { + clusterStyleProvider = (doc: Opt, props: Opt, property: string) => { let styleProp = this.props.styleProvider?.(doc, props, property); // bcz: check 'props' used to be renderDepth + 1 switch (property) { case StyleProp.BackgroundColor: @@ -619,41 +616,27 @@ export class CollectionFreeFormView extends CollectionSubView { - 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]); - - this._batch?.end(); - } - }; - @action onPointerDown = (e: React.PointerEvent): void => { this._downX = this._lastX = e.pageX; this._downY = this._lastY = e.pageY; this._downTime = Date.now(); - if (e.button === 0 && !e.altKey && !e.ctrlKey && this.props.isContentActive(true)) { - if ( - !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) - ) { + const scrollMode = e.altKey ? (Doc.UserDoc().freeformScrollMode === freeformScrollMode.Pan ? freeformScrollMode.Zoom : freeformScrollMode.Pan) : Doc.UserDoc().freeformScrollMode; + if (e.button === 0 && (!(e.ctrlKey && !e.metaKey) || scrollMode !== freeformScrollMode.Pan) && this.props.isContentActive(true)) { + if (!this.props.Document._isGroup) { + // group freeforms don't pan when dragged -- instead let the event go through to allow the group itself to drag // prettier-ignore switch (Doc.ActiveTool) { - case InkTool.Highlighter: break; + case InkTool.Highlighter: break; 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.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: this._batch = UndoManager.StartBatch('collectionErase'); setupMoveUpEvents(this, e, this.onEraserMove, this.onEraserUp, emptyFunction); break; case InkTool.None: if (!(this.props.layoutEngine?.() || StrCast(this.layoutDoc._layoutEngine))) { - this._hitCluster = this.pickCluster(this.getTransform().transformPoint(e.clientX, e.clientY)); + this._hitCluster = this.pickCluster(this.screenToLocalXf.transformPoint(e.clientX, e.clientY)); setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, emptyFunction, this._hitCluster !== -1 ? true : false, false); } break; @@ -662,29 +645,6 @@ export class CollectionFreeFormView extends CollectionSubView) => { - // const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true); - const pt = me.changedTouches[0]; - if (pt) { - this._hitCluster = this.pickCluster(this.getTransform().transformPoint(pt.clientX, pt.clientY)); - if (!e.shiftKey && !e.altKey && !e.ctrlKey && this.props.isContentActive(true)) { - this.removeMoveListeners(); - this.addMoveListeners(); - this.removeEndListeners(); - this.addEndListeners(); - if (Doc.ActiveTool === InkTool.None) { - this._lastX = pt.pageX; - this._lastY = pt.pageY; - e.preventDefault(); - e.stopPropagation(); - } else { - e.preventDefault(); - } - } - } - }; - public unprocessedDocs: Doc[] = []; public static collectionsWithUnprocessedInk = new Set(); @undoBatch @@ -697,25 +657,16 @@ export class CollectionFreeFormView extends CollectionSubView p.X)), Math.max(...ge.points.map(p => p.Y))); - const setDocs = this.getActiveDocuments().filter(s => DocCast(s.proto)?.type === DocumentType.RTF && s.color); - const sets = setDocs.map(sd => Cast(sd.text, RichTextField)?.Text as string); - if (sets.length && sets[0]) { - this._wordPalette.clear(); - const colors = setDocs.map(sd => FieldValue(sd.color) as string); - 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') { - const l = NumCast(doc.x); - const r = l + doc[Width](); - const t = NumCast(doc.y); - const b = t + doc[Height](); - const pass = !(this._inkToTextStartX! > r || end[0] < l || this._inkToTextStartY! > b || end[1] < t); - return pass; - } - return false; - }); - // const inkFields = inks.map(i => Cast(i.data, InkField)); - const strokes: InkData[] = []; - inks.forEach(i => { - const d = Cast(i.data, InkField); - const x = NumCast(i.x); - const y = NumCast(i.y); - 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 }))); - } + const strokes = this.getActiveDocuments() + .filter(doc => doc.type === DocumentType.INK) + .map(i => { + const d = Cast(i.stroke, InkField); + const x = NumCast(i.x) - Math.min(...(d?.inkData.map(pd => pd.X) ?? [0])); + const y = NumCast(i.y) - Math.min(...(d?.inkData.map(pd => pd.Y) ?? [0])); + return !d ? [] : d.inkData.map(pd => ({ X: x + pd.X, Y: y + pd.Y })); }); - CognitiveServices.Inking.Appliers.InterpretStrokes(strokes).then(results => { - const wordResults = results.filter((r: any) => r.category === 'inkWord'); - for (const word of wordResults) { - const indices: number[] = word.strokeIds; - indices.forEach(i => { - const otherInks: Doc[] = []; - indices.forEach(i2 => i2 !== i && otherInks.push(inks[i2])); - inks[i].relatedInks = new List(otherInks); - const uniqueColors: string[] = []; - Array.from(this._wordPalette.values()).forEach(c => uniqueColors.indexOf(c) === -1 && uniqueColors.push(c)); - inks[i].alternativeColors = new List(uniqueColors); - if (this._wordPalette.has(word.recognizedText.toLowerCase())) { - inks[i].color = this._wordPalette.get(word.recognizedText.toLowerCase()); - } else if (word.alternates) { - for (const alt of word.alternates) { - if (this._wordPalette.has(alt.recognizedString.toLowerCase())) { - inks[i].color = this._wordPalette.get(alt.recognizedString.toLowerCase()); - break; - } - } - } - }); - } - }); - this._inkToTextStartX = end[0]; - } + CognitiveServices.Inking.Appliers.InterpretStrokes(strokes).then(results => {}); break; case GestureUtils.Gestures.Text: if (ge.text) { - const B = this.getTransform().transformPoint(ge.points[0].X, ge.points[0].Y); + const B = this.screenToLocalXf.transformPoint(ge.points[0].X, ge.points[0].Y); this.addDocument(Docs.Create.TextDocument(ge.text, { title: ge.text, x: B[0], y: B[1] })); e.stopPropagation(); } @@ -810,7 +712,7 @@ export class CollectionFreeFormView extends CollectionSubView { + pan = (e: PointerEvent): void => { + const ctrlKey = e.ctrlKey && !e.shiftKey; + const shiftKey = e.shiftKey && !e.ctrlKey; PresBox.Instance?.pauseAutoPres(); this.props.DocumentView?.().clearViewTransition(); - const [dx, dy] = this.getTransform().transformDirection(e.clientX - this._lastX, e.clientY - this._lastY); - this.setPan(NumCast(this.Document[this.panXFieldKey]) - dx, NumCast(this.Document[this.panYFieldKey]) - dy, 0, true); + const [dxi, dyi] = this.screenToLocalXf.transformDirection(e.clientX - this._lastX, e.clientY - this._lastY); + const { x: dx, y: dy } = Utils.rotPt(dxi, dyi, this.props.ScreenToLocalTransform().Rotate); + this.setPan(NumCast(this.Document[this.panXFieldKey]) - (ctrlKey ? 0 : dx), NumCast(this.Document[this.panYFieldKey]) - (shiftKey ? 0 : dy), 0, true); this._lastX = e.clientX; this._lastY = e.clientY; }; @@ -874,20 +779,15 @@ export class CollectionFreeFormView extends CollectionSubView { - if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) return false; - if (InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) { - Doc.ActiveTool = InkTool.None; - } else { - if (this.tryDragCluster(e, this._hitCluster)) { - e.stopPropagation(); // we're moving a cluster, so stop propagation and return true to end panning and let the document drag take over - return true; - } - // pan the view if this is a regular collection, or it's an overlay and the overlay is zoomed (otherwise, there's nothing to pan) - if (!this.props.isAnnotationOverlay || 1 - NumCast(this.rootDoc._freeform_scale_min, 1) / this.getLocalTransform().inverse().Scale) { - this.pan(e); - e.stopPropagation(); // if we are actually panning, stop propagation -- this will preven things like the overlayView from dragging the document while we're panning - } + onPointerMove = (e: PointerEvent) => { + if (this.tryDragCluster(e, this._hitCluster)) { + e.stopPropagation(); // we're moving a cluster, so stop propagation and return true to end panning and let the document drag take over + return true; + } + // pan the view if this is a regular collection, or it's an overlay and the overlay is zoomed (otherwise, there's nothing to pan) + if (!this.props.isAnnotationOverlay || 1 - NumCast(this.rootDoc._freeform_scale_min, 1) / this.zoomScaling()) { + this.pan(e); + e.stopPropagation(); // if we are actually panning, stop propagation -- this will preven things like the overlayView from dragging the document while we're panning } return false; }; @@ -1025,18 +925,13 @@ export class CollectionFreeFormView extends CollectionSubView { - this.removeMoveListeners(); - this.removeEndListeners(); - }; - @action zoom = (pointX: number, pointY: number, deltaY: number): void => { if (this.Document._isGroup || this.Document[(this.props.viewField ?? '_') + 'freeform_noZoom']) return; let deltaScale = deltaY > 0 ? 1 / 1.05 : 1.05; if (deltaScale < 0) deltaScale = -deltaScale; - const [x, y] = this.getTransform().transformPoint(pointX, pointY); - const invTransform = this.getLocalTransform().inverse(); + const [x, y] = this.screenToLocalXf.transformPoint(pointX, pointY); + const invTransform = this.panZoomXf.inverse(); if (deltaScale * invTransform.Scale > 20) { deltaScale = 20 / invTransform.Scale; } @@ -1067,11 +962,17 @@ export class CollectionFreeFormView extends CollectionSubView this.props.PanelHeight() / this.nativeDimScaling + 1e-4; - switch (!e.ctrlKey ? Doc.UserDoc().freeformScrollMode : freeformScrollMode.Pan) { + switch ( + !e.ctrlKey && !e.shiftKey && !e.metaKey && !e.altKey ?// + Doc.UserDoc().freeformScrollMode : // no modifiers, do assigned mode + e.ctrlKey && !CtrlKey? // otherwise, if ctrl key (pinch gesture) try to zoom else pan + freeformScrollMode.Zoom : freeformScrollMode.Pan // prettier-ignore + ) { case freeformScrollMode.Pan: - // if ctrl is selected then zoom - if (!e.ctrlKey && this.props.isContentActive(true)) { - this.scrollPan({ deltaX: -e.deltaX * this.getTransform().Scale, deltaY: e.shiftKey ? 0 : -e.deltaY * this.getTransform().Scale }); + if (((!e.metaKey && !e.altKey) || Doc.UserDoc().freeformScrollMode === freeformScrollMode.Zoom) && this.props.isContentActive(true)) { + const deltaX = e.shiftKey ? e.deltaX : e.ctrlKey ? 0 : e.deltaX; + const deltaY = e.shiftKey ? 0 : e.ctrlKey ? e.deltaY : e.deltaY; + this.scrollPan({ deltaX: -deltaX * this.screenToLocalXf.Scale, deltaY: e.shiftKey ? 0 : -deltaY * this.screenToLocalXf.Scale }); break; } default: @@ -1093,7 +994,7 @@ export class CollectionFreeFormView extends CollectionSubView pair.layout).filter(doc => doc instanceof Doc); const measuredDocs = docs - .map(doc => ({ pos: this.childPositionProviderUnmemoized(doc, ''), size: this.childSizeProviderUnmemoized(doc, '') })) + .map(doc => ({ pos: { x: NumCast(doc.x), y: NumCast(doc.y) }, size: { width: NumCast(doc.width), height: NumCast(doc.height) } })) .filter(({ pos, size }) => pos && size) .map(({ pos, size }) => ({ pos: pos!, size: size! })); if (measuredDocs.length) { @@ -1110,11 +1011,11 @@ export class CollectionFreeFormView extends CollectionSubView= panX + panelWidMax / 2) panX = ranges.xrange.max + (this.props.originTopLeft ? 0 : panelWidMax / 2); else if (ranges.xrange.max <= panX - panelWidMin / 2) panX = ranges.xrange.min - (this.props.originTopLeft ? panelWidMax / 2 : panelWidMin / 2); if (ranges.yrange.min >= panY + panelHgtMax / 2) panY = ranges.yrange.max + (this.props.originTopLeft ? 0 : panelHgtMax / 2); @@ -1124,7 +1025,7 @@ export class CollectionFreeFormView extends CollectionSubView { const layoutdoc = Doc.Layout(doc); const pt = xf.transformPoint(NumCast(doc.x), NumCast(doc.y)); - const pt2 = xf.transformPoint(NumCast(doc.x) + layoutdoc[Width](), NumCast(doc.y) + layoutdoc[Height]()); + const pt2 = xf.transformPoint(NumCast(doc.x) + NumCast(layoutdoc._width), NumCast(doc.y) + NumCast(layoutdoc._height)); 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 !== undefined) { @@ -1284,11 +1185,10 @@ export class CollectionFreeFormView extends CollectionSubView { - const pt = this.getTransform().transformPoint(NumCast(doc.x), NumCast(doc.y)); + const pt = this.screenToLocalXf.transformPoint(NumCast(doc.x), NumCast(doc.y)); doc.x = pt[0]; doc.y = pt[1]; return doc; @@ -1400,9 +1300,9 @@ export class CollectionFreeFormView extends CollectionSubView this._layoutPoolData.get(doc[Id] + (replica || '')); - childDataProvider = computedFn( - function childDataProvider(this: any, doc: Doc, replica: string) { - return this.childPositionProviderUnmemoized(doc, replica); - }.bind(this) - ); - - childSizeProviderUnmemoized = (doc: Doc, replica: string) => this._layoutSizeData.get(doc[Id] + (replica || '')); - childSizeProvider = computedFn( - function childSizeProvider(this: any, doc: Doc, replica: string) { - return this.childSizeProviderUnmemoized(doc, replica); - }.bind(this) - ); - doEngineLayout( poolData: Map, engine: (poolData: Map, pivotDoc: Doc, childPairs: { layout: Doc; data?: Doc }[], panelDim: number[], viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[], engineProps: any) => ViewDefResult[] @@ -1508,55 +1394,18 @@ export class CollectionFreeFormView extends CollectionSubView, computedElementData: ViewDefResult[]) => { - const array = Array.from(newPool.entries()); - this._lastPoolSize = array.length; - for (const entry of array) { - const lastPos = this._cachedPool.get(entry[0]); // last computed pos - const newPos = entry[1]; - if ( - !lastPos || - newPos.color !== lastPos.color || - newPos.backgroundColor !== lastPos.backgroundColor || - newPos.opacity !== lastPos.opacity || - newPos.x !== lastPos.x || - newPos.y !== lastPos.y || - newPos.z !== lastPos.z || - newPos.rotation !== lastPos.rotation || - newPos.zIndex !== lastPos.zIndex || - newPos.transition !== lastPos.transition || - newPos.pointerEvents !== lastPos.pointerEvents - ) { - this._layoutPoolData.set(entry[0], newPos); - } - if (!lastPos || newPos.height !== lastPos.height || newPos.width !== lastPos.width) { - this._layoutSizeData.set(entry[0], { width: newPos.width, height: newPos.height }); - } - } - // by returning undefined, we prevent an edit being made to layoutElements when nothing has happened - // this short circuit, prevents lots of downstream mobx invalidations which would have no effect but cause - // a distinct lag at the start of dragging. - // The reason we're here in the first place without a change is that when dragging a document, - // filters are changed on the annotation layers (eg. WebBox) which invalidate the childDoc list - // for the overlay views -- however, in many cases, this filter change doesn't actually affect anything - // (e.g, no annotations, or only opaque annotations). - this._cachedPool.clear(); - Array.from(newPool.entries()).forEach(k => this._cachedPool.set(k[0], k[1])); const elements = computedElementData.slice(); Array.from(newPool.entries()) .filter(entry => this.isCurrent(entry[1].pair.layout)) - .forEach((entry, i) => { - const childData: ViewDefBounds = this.childDataProvider(entry[1].pair.layout, entry[1].replica); - const childSize = this.childSizeProvider(entry[1].pair.layout, entry[1].replica); + .forEach(entry => elements.push({ ele: this.getChildDocView(entry[1]), - bounds: childData.opacity === 0 ? { ...childData, width: 0, height: 0 } : { ...childData, width: childSize.width, height: childSize.height }, + bounds: (entry[1].opacity === 0 ? { ...entry[1], width: 0, height: 0 } : { ...entry[1] }) as any, inkMask: BoolCast(entry[1].pair.layout.stroke_isInkMask) ? NumCast(entry[1].pair.layout.opacity, 1) : -1, - }); - }); + }) + ); this.Document._freeform_useClusters && !this._clusterSets.length && this.childDocs.length && this.updateClusters(true); return elements; @@ -1586,14 +1435,14 @@ export class CollectionFreeFormView extends CollectionSubView { if (this.Document._isGroup && this.childDocs.length === this.childDocList?.length) { - const clist = this.childDocs.map(cd => ({ x: NumCast(cd.x), y: NumCast(cd.y), width: cd[Width](), height: cd[Height]() })); + const clist = this.childDocs.map(cd => ({ x: NumCast(cd.x), y: NumCast(cd.y), width: NumCast(cd._width), height: NumCast(cd._height) })); return aggregateBounds(clist, NumCast(this.layoutDoc._xPadding), NumCast(this.layoutDoc._yPadding)); } return undefined; }, cbounds => { if (cbounds) { - const c = [NumCast(this.layoutDoc.x) + this.layoutDoc[Width]() / 2, NumCast(this.layoutDoc.y) + this.layoutDoc[Height]() / 2]; + const c = [NumCast(this.layoutDoc.x) + NumCast(this.layoutDoc._width) / 2, NumCast(this.layoutDoc.y) + NumCast(this.layoutDoc._height) / 2]; const p = [NumCast(this.layoutDoc[this.panXFieldKey]), NumCast(this.layoutDoc[this.panYFieldKey])]; const pbounds = { x: cbounds.x - p[0] + c[0], @@ -1621,7 +1470,7 @@ export class CollectionFreeFormView extends CollectionSubView this.doInternalLayoutComputation, - ({ newPool, computedElementData }) => (this._layoutElements = this.doLayoutComputation(newPool, computedElementData)), - { fireImmediately: true, name: 'layoutComputationReaction' } - ); - this._disposers.active = reaction( () => this.isContentActive(), // if autoreset is on, then whenever the view is selected, it will be restored to it default pan/zoom positions active => !SnappingManager.GetIsDragging() && this.rootDoc[this.autoResetFieldKey] && active && this.resetView() ); }) ); + this._disposers.layoutElements = reaction( + // layoutElements can't be a computed value because doLayoutComputation() is an action that has side effect of updating clusters + () => this.doInternalLayoutComputation, + computation => (this._layoutElements = this.doLayoutComputation(computation.newPool, computation.computedElementData)), + { fireImmediately: true } + ); } static replaceCanvases(oldDiv: HTMLElement, newDiv: HTMLElement) { @@ -1680,8 +1529,8 @@ export class CollectionFreeFormView extends CollectionSubView { this.dataDoc.icon = new ImageField(iconFile); - this.dataDoc['icon_nativeWidth'] = nativeWidth; - this.dataDoc['icon_nativeHeight'] = nativeHeight; + this.dataDoc.icon_nativeWidth = nativeWidth; + this.dataDoc.icon_nativeHeight = nativeHeight; } ); @@ -1726,6 +1575,7 @@ export class CollectionFreeFormView extends CollectionSubView disposer?.()); } @@ -1738,7 +1588,7 @@ export class CollectionFreeFormView extends CollectionSubView { const childDocs = this.childDocs.slice(); childDocs.forEach(doc => { - const scr = this.getTransform().inverse().transformPoint(NumCast(doc.x), NumCast(doc.y)); + const scr = this.screenToLocalXf.inverse().transformPoint(NumCast(doc.x), NumCast(doc.y)); doc.x = scr?.[0]; doc.y = scr?.[1]; }); @@ -1802,20 +1652,16 @@ export class CollectionFreeFormView extends CollectionSubView (Doc.UserDoc().defaultTextLayout = undefined), icon: 'eye' }); - appearanceItems.push({ description: `Pin View`, event: () => TabDocView.PinDoc(this.rootDoc, { pinViewport: MarqueeView.CurViewBounds(this.rootDoc, this.props.PanelWidth(), this.props.PanelHeight()) }), icon: 'map-pin' }); + appearanceItems.push({ description: `Pin View`, event: () => this.props.pinToPres(this.rootDoc, { pinViewport: MarqueeView.CurViewBounds(this.rootDoc, this.props.PanelWidth(), this.props.PanelHeight()) }), icon: 'map-pin' }); !Doc.noviceMode && appearanceItems.push({ description: `update icon`, event: this.updateIcon, icon: 'compress-arrows-alt' }); this.props.renderDepth && appearanceItems.push({ description: 'Ungroup collection', event: this.promoteCollection, icon: 'table' }); - this.props.Document._isGroup && this.Document.transcription && appearanceItems.push({ description: 'Ink to text', event: () => this.transcribeStrokes(false), icon: 'font' }); + this.props.Document._isGroup && this.Document.transcription && appearanceItems.push({ description: 'Ink to text', event: this.transcribeStrokes, icon: 'font' }); !Doc.noviceMode ? appearanceItems.push({ description: 'Arrange contents in grid', event: this.layoutDocsInGrid, icon: 'table' }) : null; + !Doc.noviceMode ? appearanceItems.push({ description: (this.Document._freeform_useClusters ? 'Hide' : 'Show') + ' Clusters', event: () => this.updateClusters(!this.Document._freeform_useClusters), icon: 'braille' }) : 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.noviceMode ? viewCtrlItems.push({ description: (this.Document._freeform_useClusters ? 'Hide' : 'Show') + ' Clusters', event: () => this.updateClusters(!this.Document._freeform_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 && @@ -1834,16 +1680,13 @@ export class CollectionFreeFormView extends CollectionSubView { + transcribeStrokes = () => { 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; + const text = StrCast(this.props.Document.transcription); + const lines = text.split('\n'); + const height = 30 + 15 * lines.length; - this.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 })); - } + this.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 })); } }; @@ -1853,31 +1696,24 @@ export class CollectionFreeFormView extends CollectionSubView()) => { + dragStarting = (snapToDraggedDoc: boolean = false, showGroupDragTarget: boolean = true, visited = new Set()) => { if (visited.has(this.rootDoc)) return; visited.add(this.rootDoc); showGroupDragTarget && (this.GroupChildDrag = BoolCast(this.Document._isGroup)); - if (this.rootDoc._isGroup && this.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView) { - this.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView.dragStarting(snapToDraggedDoc, false, visited); - } const activeDocs = this.getActiveDocuments(); - const size = this.getTransform().transformDirection(this.props.PanelWidth(), this.props.PanelHeight()); + const size = this.screenToLocalXf.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 }) => intersectRect(docDims(doc), rect); const snappableDocs = activeDocs.filter(doc => doc.z === undefined && isDocInView(doc, selRect)); // first see if there are any foreground docs to snap to - activeDocs.forEach( - doc => - doc._isGroup && - SnappingManager.GetIsResizing() !== doc && - !DragManager.docsBeingDragged.includes(doc) && - (DocumentManager.Instance.getDocumentView(doc)?.ComponentView as CollectionFreeFormView)?.dragStarting(snapToDraggedDoc, false, visited) - ); + activeDocs + .filter(doc => doc._isGroup && SnappingManager.GetIsResizing() !== doc && !DragManager.docsBeingDragged.includes(doc)) + .forEach(doc => DocumentManager.Instance.getDocumentView(doc)?.ComponentView?.dragStarting?.(snapToDraggedDoc, false, visited)); const horizLines: number[] = []; const vertLines: number[] = []; - const invXf = this.getTransform().inverse(); + const invXf = this.screenToLocalXf.inverse(); snappableDocs .filter(doc => !doc._isGroup && (snapToDraggedDoc || (SnappingManager.GetIsResizing() !== doc && !DragManager.docsBeingDragged.includes(doc)))) .forEach(doc => { @@ -1904,15 +1740,16 @@ export class CollectionFreeFormView extends CollectionSubView !this._renderCutoffData.get(doc[Id])) && setTimeout(this.incrementalRender, 1); }); - get children() { - this.incrementalRender(); - const children = typeof this.props.children === 'function' ? ((this.props.children as any)() as JSX.Element[]) : this.props.children ? [this.props.children] : []; - return [...children, ...this.views, ]; + // if a freeform view has any children, then the children will likely consist of a single child + // which will be a DocumentView. In this sitation, this freeform views acts as an annotation overlay for + // the underlying DocumentView and will pan and scoll with the underlying Documen tView. + @computed get underlayViews() { + return this.props.children ? [this.props.children] : []; } @computed get placeholder() { return ( -
+
{this.props.Document.annotationOn ? '' : this.props.Document.title?.toString()}
); @@ -1921,7 +1758,7 @@ export class CollectionFreeFormView extends CollectionSubView this._brushedView; gridColor = () => DashColor(lightOrDark(this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor))) - .fade(0.6) + .fade(0.5) .toString(); @computed get backgroundGrid() { return ( @@ -1943,16 +1780,19 @@ export class CollectionFreeFormView extends CollectionSubView - {this.children} - + {this.underlayViews} + {this.contentViews} + + ); } @computed get marqueeView() { @@ -1970,8 +1810,8 @@ export class CollectionFreeFormView extends CollectionSubView @@ -1993,17 +1833,14 @@ export class CollectionFreeFormView extends CollectionSubView this.nativeDimScaling; @action - brushView = (viewport: { width: number; height: number; panX: number; panY: number }, transTime: number) => { + brushView = (viewport: { width: number; height: number; panX: number; panY: number }, transTime: number, holdTime: number = 2500) => { this._brushtimer1 && clearTimeout(this._brushtimer1); this._brushtimer && clearTimeout(this._brushtimer); this._brushedView = undefined; this._brushtimer1 = setTimeout( action(() => { this._brushedView = { ...viewport, panX: viewport.panX - viewport.width / 2, panY: viewport.panY - viewport.height / 2 }; - this._brushtimer = setTimeout( - action(() => (this._brushedView = undefined)), - 2500 - ); + this._brushtimer = setTimeout(action(() => (this._brushedView = undefined)), holdTime); // prettier-ignore }), transTime + 1 ); @@ -2080,139 +1917,10 @@ export class CollectionFreeFormView extends CollectionSubView ViewDefResult[]; -} - -@observer -class CollectionFreeFormOverlayView extends React.Component { - render() { - return this.props - .elements() - .filter(ele => ele.bounds?.z) - .map(ele => ele.ele); - } -} - -interface CollectionFreeFormViewPannableContentsProps { - rootDoc: Doc; - viewDefDivClick?: ScriptField; - children?: React.ReactNode | undefined; - transition?: string; - isAnnotationOverlay: boolean | undefined; - transform: () => string; - brushedView: () => { panX: number; panY: number; width: number; height: number } | undefined; -} - -@observer -class CollectionFreeFormViewPannableContents extends React.Component { - @computed get presPaths() { - return CollectionFreeFormView.ShowPresPaths ? PresBox.Instance.pathLines(this.props.rootDoc) : null; - } - // rectangle highlight used when following trail/link to a region of a collection that isn't a document - showViewport = (viewport: { panX: number; panY: number; width: number; height: number } | undefined) => - !viewport ? null : ( -
- ); - - render() { - return ( -
{ - const target = e.target as any; - if (getComputedStyle(target)?.overflow === 'visible') { - target.scrollTop = target.scrollLeft = 0; // if collection is visible, scrolling messes things up since there are no scroll bars - } - }} - style={{ - transform: this.props.transform(), - transition: this.props.transition, - width: this.props.isAnnotationOverlay ? undefined : 0, // if not an overlay, then this will be the size of the collection, but panning and zooming will move it outside the visible border of the collection and make it selectable. This problem shows up after zooming/panning on a background collection -- you can drag the collection by clicking on apparently empty space outside the collection - }}> - {this.props.children} - {this.presPaths} - {this.showViewport(this.props.brushedView())} -
- ); - } -} - -interface CollectionFreeFormViewBackgroundGridProps { - panX: () => number; - panY: () => number; - PanelWidth: () => number; - PanelHeight: () => number; - color: () => string; - isAnnotationOverlay?: boolean; - nativeDimScaling: () => number; - zoomScaling: () => number; - layoutDoc: Doc; - cachedCenteringShiftX: number; - cachedCenteringShiftY: number; -} @observer -class CollectionFreeFormBackgroundGrid extends React.Component { - chooseGridSpace = (gridSpace: number): number => { - if (!this.props.zoomScaling()) return gridSpace; - const divisions = this.props.PanelWidth() / this.props.zoomScaling() / gridSpace; - return divisions < 90 ? gridSpace : this.chooseGridSpace(gridSpace * 2); - }; +class CollectionFreeFormOverlayView extends React.Component<{ elements: () => ViewDefResult[] }> { render() { - const gridSpace = this.chooseGridSpace(NumCast(this.props.layoutDoc['_backgroundGrid-spacing'], 50)); - const shiftX = (this.props.isAnnotationOverlay ? 0 : (-this.props.panX() % gridSpace) - gridSpace) * this.props.zoomScaling(); - const shiftY = (this.props.isAnnotationOverlay ? 0 : (-this.props.panY() % gridSpace) - gridSpace) * this.props.zoomScaling(); - const renderGridSpace = gridSpace * this.props.zoomScaling(); - const w = this.props.PanelWidth() / this.props.nativeDimScaling() + 2 * renderGridSpace; - const h = this.props.PanelHeight() / this.props.nativeDimScaling() + 2 * renderGridSpace; - const strokeStyle = this.props.color(); - return !this.props.nativeDimScaling() ? null : ( - { - const ctx = el?.getContext('2d'); - if (ctx) { - const Cx = this.props.cachedCenteringShiftX % renderGridSpace; - const Cy = this.props.cachedCenteringShiftY % renderGridSpace; - ctx.lineWidth = Math.min(1, Math.max(0.5, this.props.zoomScaling())); - ctx.setLineDash(gridSpace > 50 ? [3, 3] : [1, 5]); - ctx.clearRect(0, 0, w, h); - if (ctx) { - ctx.strokeStyle = strokeStyle; - ctx.fillStyle = strokeStyle; - ctx.beginPath(); - if (this.props.zoomScaling() > 1) { - for (let x = Cx - renderGridSpace; x <= w - Cx; x += renderGridSpace) { - ctx.moveTo(x, Cy - h); - ctx.lineTo(x, Cy + h); - } - for (let y = Cy - renderGridSpace; y <= h - Cy; y += renderGridSpace) { - ctx.moveTo(Cx - w, y); - ctx.lineTo(Cx + w, y); - } - } else { - for (let x = Cx - renderGridSpace; x <= w - Cx; x += renderGridSpace) - for (let y = Cy - renderGridSpace; y <= h - Cy; y += renderGridSpace) { - ctx.fillRect(Math.round(x), Math.round(y), 1, 1); - } - } - ctx.stroke(); - } - } - }} - /> - ); + return this.props.elements().filter(ele => ele.bounds?.z).map(ele => ele.ele); // prettier-ignore } } @@ -2224,13 +1932,13 @@ export function CollectionBrowseClick(dv: DocumentView, clientX: number, clientY if (!focused) { const selfFfview = !dv.rootDoc._isGroup && dv.ComponentView instanceof CollectionFreeFormView ? dv.ComponentView : undefined; let containers = dv.props.docViewPath(); - let parFfview = dv.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView; + let parFfview = dv.CollectionFreeFormView; for (var cont of containers) { - parFfview = parFfview ?? cont.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView; + parFfview = parFfview ?? cont.CollectionFreeFormView; } - while (parFfview?.rootDoc._isGroup) parFfview = parFfview.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView; + while (parFfview?.rootDoc._isGroup) parFfview = parFfview.props.DocumentView?.().CollectionFreeFormView; const ffview = selfFfview && selfFfview.rootDoc[selfFfview.scaleFieldKey] !== 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), ffview?.isAnnotationOverlay ? 1 : 0.5, browseTransitionTime); + ffview?.zoomSmoothlyAboutPt(ffview.screenToLocalXf.transformPoint(clientX, clientY), ffview?.isAnnotationOverlay ? 1 : 0.5, browseTransitionTime); Doc.linkFollowHighlight(dv?.props.Document, false); } }); @@ -2260,8 +1968,8 @@ ScriptingGlobals.add(function pinWithView(pinContent: boolean) { ); }); ScriptingGlobals.add(function bringToFront() { - SelectionManager.Views().forEach(view => view.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView.bringToFront(view.rootDoc)); + SelectionManager.Views().forEach(view => view.CollectionFreeFormView?.bringToFront(view.rootDoc)); }); ScriptingGlobals.add(function sendToBack(doc: Doc) { - SelectionManager.Views().forEach(view => view.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView.bringToFront(view.rootDoc, true)); + SelectionManager.Views().forEach(view => view.CollectionFreeFormView?.bringToFront(view.rootDoc, true)); }); diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index edcc17afd..2092ecb7a 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -22,7 +22,6 @@ import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox'; import { pasteImageBitmap } from '../../nodes/WebBoxRenderer'; import { PreviewCursor } from '../../PreviewCursor'; import { SubCollectionViewProps } from '../CollectionSubView'; -import { TabDocView } from '../TabDocView'; import { MarqueeOptionsMenu } from './MarqueeOptionsMenu'; import './MarqueeView.scss'; import React = require('react'); @@ -78,15 +77,6 @@ export class MarqueeView extends React.Component) { - this.props.Document.ink = value; - } componentDidMount() { this.props.setPreviewCursor?.(this.setPreviewCursor); @@ -218,22 +208,15 @@ export class MarqueeView extends React.Component { - // if (this.props.pointerEvents?.() === 'none') return; this._downX = this._lastX = e.clientX; 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 OR in adding presentation slide & left key drag mode - if (e.button === 2 || (e.button === 0 && (e.altKey || (this.props.isContentActive?.(true) && Doc.UserDoc().freeformScrollMode === freeformScrollMode.Pan)))) { - // if (e.altKey || (MarqueeView.DragMarquee && this.props.active(true))) { - this.setPreviewCursor(e.clientX, e.clientY, true, false, this.props.Document); - // (!e.altKey) && e.stopPropagation(); // bcz: removed so that you can alt-click on button in a collection to switch link following behaviors. - e.preventDefault(); - // } - // bcz: do we need this? it kills the context menu on the main collection if !altKey - // e.stopPropagation(); - } else PreviewCursor.Visible = false; - } + + const scrollMode = e.altKey ? (Doc.UserDoc().freeformScrollMode === freeformScrollMode.Pan ? freeformScrollMode.Zoom : freeformScrollMode.Pan) : Doc.UserDoc().freeformScrollMode; + // allow marquee if right drag/meta drag, or pan mode + if (e.button === 2 || e.metaKey || scrollMode === freeformScrollMode.Pan) { + this.setPreviewCursor(e.clientX, e.clientY, true, false, this.props.Document); + e.preventDefault(); + } else PreviewCursor.Visible = false; }; @action @@ -242,7 +225,7 @@ export class MarqueeView extends React.Component Utils.DRAG_THRESHOLD || Math.abs(this._lastY - this._downY) > Utils.DRAG_THRESHOLD) { + if (!Utils.isClick(this._lastX, this._lastY, this._downX, this._downY, Date.now())) { if (!this._commandExecuted) { this.showMarquee(); } @@ -262,8 +245,6 @@ export class MarqueeView extends React.Component { - TabDocView.PinDoc(this.props.Document, { pinViewport: this.Bounds }); + this.props.pinToPres(this.props.Document, { pinViewport: this.Bounds }); MarqueeOptionsMenu.Instance.fadeOut(true); this.hideMarquee(); }; -- cgit v1.2.3-70-g09d2