import { action, observable, ObservableSet, runInAction } from 'mobx'; import { AnimationSym, Doc, Opt } from '../../fields/Doc'; import { Id } from '../../fields/FieldSymbols'; import { listSpec } from '../../fields/Schema'; import { Cast, DocCast, StrCast } from '../../fields/Types'; import { AudioField } from '../../fields/URLField'; import { returnFalse } from '../../Utils'; import { CollectionViewType, DocumentType } from '../documents/DocumentTypes'; import { CollectionDockingView } from '../views/collections/CollectionDockingView'; import { CollectionFreeFormView } from '../views/collections/collectionFreeForm'; import { CollectionView } from '../views/collections/CollectionView'; import { LightboxView } from '../views/LightboxView'; import { DocFocusOptions, DocumentView, OpenWhereMod, ViewAdjustment } from '../views/nodes/DocumentView'; import { LinkAnchorBox } from '../views/nodes/LinkAnchorBox'; import { ScriptingGlobals } from './ScriptingGlobals'; import { SelectionManager } from './SelectionManager'; const { Howl } = require('howler'); export class DocumentManager { //global holds all of the nodes (regardless of which collection they're in) @observable public DocumentViews = new Set(); @observable public LinkAnchorBoxViews: DocumentView[] = []; @observable public RecordingEvent = 0; @observable public LinkedDocumentViews: { a: DocumentView; b: DocumentView; l: Doc }[] = []; private static _instance: DocumentManager; public static get Instance(): DocumentManager { return this._instance || (this._instance = new this()); } //private constructor so no other class can create a nodemanager private constructor() {} private _viewRenderedCbs: { doc: Doc; func: (dv: DocumentView) => any }[] = []; public AddViewRenderedCb = (doc: Opt, func: (dv: DocumentView) => any) => { if (doc) { const dv = this.getDocumentViewById(doc[Id]); this._viewRenderedCbs.push({ doc, func }); if (dv) { this.callAddViewFuncs(dv); return true; } } else { func(undefined as any); } return false; }; callAddViewFuncs = (view: DocumentView) => { const callFuncs = this._viewRenderedCbs.filter(vc => vc.doc === view.rootDoc); if (callFuncs.length) { this._viewRenderedCbs = this._viewRenderedCbs.filter(vc => !callFuncs.includes(vc)); const intTimer = setInterval( () => { if (!view.ComponentView?.incrementalRendering?.()) { callFuncs.forEach(cf => cf.func(view)); clearInterval(intTimer); } }, view.ComponentView?.incrementalRendering?.() ? 0 : 100 ); } }; @action public AddView = (view: DocumentView) => { //console.log("MOUNT " + view.props.Document.title + "/" + view.props.LayoutTemplateString); if (view.props.LayoutTemplateString?.includes(LinkAnchorBox.name)) { const viewAnchorIndex = view.props.LayoutTemplateString.includes('anchor2') ? 'anchor2' : 'anchor1'; const link = view.rootDoc; this.LinkAnchorBoxViews?.filter(dv => Doc.AreProtosEqual(dv.rootDoc, link) && !dv.props.LayoutTemplateString?.includes(viewAnchorIndex)).forEach(otherView => this.LinkedDocumentViews.push({ a: viewAnchorIndex === 'anchor2' ? otherView : view, b: viewAnchorIndex === 'anchor2' ? view : otherView, l: link, }) ); this.LinkAnchorBoxViews.push(view); // this.LinkedDocumentViews.forEach(view => console.log(" LV = " + view.a.props.Document.title + "/" + view.a.props.LayoutTemplateString + " --> " + // view.b.props.Document.title + "/" + view.b.props.LayoutTemplateString)); } else { this.DocumentViews.add(view); } this.callAddViewFuncs(view); }; public RemoveView = action((view: DocumentView) => { this.LinkedDocumentViews.slice().forEach( action(pair => { if (pair.a === view || pair.b === view) { const li = this.LinkedDocumentViews.indexOf(pair); li !== -1 && this.LinkedDocumentViews.splice(li, 1); } }) ); if (view.props.LayoutTemplateString?.includes(LinkAnchorBox.name)) { const index = this.LinkAnchorBoxViews.indexOf(view); this.LinkAnchorBoxViews.splice(index, 1); } else { this.DocumentViews.delete(view); } SelectionManager.DeselectView(view); }); //gets all views public getDocumentViewsById(id: string) { const toReturn: DocumentView[] = []; Array.from(DocumentManager.Instance.DocumentViews).map(view => { if (view.rootDoc[Id] === id) { toReturn.push(view); } }); if (toReturn.length === 0) { Array.from(DocumentManager.Instance.DocumentViews).map(view => { const doc = view.rootDoc.proto; if (doc && doc[Id] && doc[Id] === id) { toReturn.push(view); } }); } return toReturn; } public getAllDocumentViews(doc: Doc) { return this.getDocumentViewsById(doc[Id]); } public getDocumentViewById(id: string, preferredCollection?: CollectionView): DocumentView | undefined { if (!id) return undefined; let toReturn: DocumentView | undefined; const passes = preferredCollection ? [preferredCollection, undefined] : [undefined]; for (const pass of passes) { Array.from(DocumentManager.Instance.DocumentViews).map(view => { if (view.rootDoc[Id] === id && (!pass || view.props.ContainingCollectionView === preferredCollection)) { toReturn = view; return; } }); if (!toReturn) { Array.from(DocumentManager.Instance.DocumentViews).map(view => { const doc = view.rootDoc.proto; if (doc && doc[Id] === id && (!pass || view.props.ContainingCollectionView === preferredCollection)) { toReturn = view; } }); } else { break; } } return toReturn; } public getDocumentView(toFind: Doc, preferredCollection?: CollectionView): DocumentView | undefined { const found = // Array.from(DocumentManager.Instance.DocumentViews).find( // dv => // ((dv.rootDoc.data as any)?.url?.href && (dv.rootDoc.data as any)?.url?.href === (toFind.data as any)?.url?.href) || // ((DocCast(dv.rootDoc.annotationOn)?.data as any)?.url?.href && (DocCast(dv.rootDoc.annotationOn)?.data as any)?.url?.href === (DocCast(toFind.annotationOn)?.data as any)?.url?.href) // )?.rootDoc ?? toFind; return this.getDocumentViewById(found[Id], preferredCollection); } public getLightboxDocumentView = (toFind: Doc, originatingDoc: Opt = undefined): DocumentView | undefined => { const views: DocumentView[] = []; Array.from(DocumentManager.Instance.DocumentViews).map(view => LightboxView.IsLightboxDocView(view.docViewPath) && Doc.AreProtosEqual(view.rootDoc, toFind) && views.push(view)); return views?.find(view => view.ContentDiv?.getBoundingClientRect().width && view.props.focus !== returnFalse) || views?.find(view => view.props.focus !== returnFalse) || (views.length ? views[0] : undefined); }; public getFirstDocumentView = (toFind: Doc, originatingDoc: Opt = undefined): DocumentView | undefined => { if (LightboxView.LightboxDoc) return DocumentManager.Instance.getLightboxDocumentView(toFind, originatingDoc); const views = this.getDocumentViews(toFind); //.filter(view => view.rootDoc !== originatingDoc); return views?.find(view => view.ContentDiv?.getBoundingClientRect().width && view.props.focus !== returnFalse) || views?.find(view => view.props.focus !== returnFalse) || (views.length ? views[0] : undefined); }; public getDocumentViews(toFindIn: Doc): DocumentView[] { const toFind = // Array.from(DocumentManager.Instance.DocumentViews).find( // dv => // ((dv.rootDoc.data as any)?.url?.href && (dv.rootDoc.data as any)?.url?.href === (toFindIn.data as any)?.url?.href) || // ((DocCast(dv.rootDoc.annotationOn)?.data as any)?.url?.href && (DocCast(dv.rootDoc.annotationOn)?.data as any)?.url?.href === (DocCast(toFindIn.annotationOn)?.data as any)?.url?.href) // )?.rootDoc ?? toFindIn; const toReturn: DocumentView[] = []; const docViews = Array.from(DocumentManager.Instance.DocumentViews).filter(view => !LightboxView.IsLightboxDocView(view.docViewPath)); const lightViews = Array.from(DocumentManager.Instance.DocumentViews).filter(view => LightboxView.IsLightboxDocView(view.docViewPath)); // heuristic to return the "best" documents first: // choose a document in the lightbox first // choose an exact match over an alias match lightViews.map(view => view.rootDoc === toFind && toReturn.push(view)); lightViews.map(view => view.rootDoc !== toFind && Doc.AreProtosEqual(view.rootDoc, toFind) && toReturn.push(view)); docViews.map(view => view.rootDoc === toFind && toReturn.push(view)); docViews.map(view => view.rootDoc !== toFind && Doc.AreProtosEqual(view.rootDoc, toFind) && toReturn.push(view)); return toReturn; } static GetContextPath(doc: Opt, includeExistingViews?: boolean) { if (!doc) return []; const srcContext = Cast(doc.context, Doc, null) ?? Cast(Cast(doc.annotationOn, Doc, null)?.context, Doc, null); var containerDocContext = srcContext ? [srcContext] : []; while ( containerDocContext.length && containerDocContext[0]?.context && DocCast(containerDocContext[0].context)?.viewType !== CollectionViewType.Docking && (includeExistingViews || !DocumentManager.Instance.getDocumentView(containerDocContext[0])) ) { containerDocContext = [Cast(containerDocContext[0].context, Doc, null), ...containerDocContext]; } return containerDocContext; } static playAudioAnno(doc: Doc) { const anno = Cast(doc[Doc.LayoutFieldKey(doc) + '-audioAnnotations'], listSpec(AudioField), null)?.lastElement(); if (anno) { if (anno instanceof AudioField) { new Howl({ src: [anno.url.href], format: ['mp3'], autoplay: true, loop: false, volume: 0.5, }); } } } public static removeOverlayViews() { DocumentManager._overlayViews?.forEach(action(view => (view.textHtmlOverlay = undefined))); DocumentManager._overlayViews?.clear(); } static _overlayViews = new ObservableSet(); static addView = (doc: Doc, finished?: () => void) => { CollectionDockingView.AddSplit(doc, OpenWhereMod.right); finished?.(); }; public jumpToDocument = ( targetDoc: Doc, // document to display options: DocFocusOptions, // options for how to navigate to target createViewFunc = DocumentManager.addView, // how to create a view of the doc if it doesn't exist docContextPath: Doc[], // context to load that should contain the target finished?: () => void ): void => { const originalTarget = options.originalTarget ?? targetDoc; const docView = this.getFirstDocumentView(targetDoc, options.originatingDoc); const annotatedDoc = Cast(targetDoc.annotationOn, Doc, null); const resolvedTarget = targetDoc.type === DocumentType.MARKER ? annotatedDoc ?? docView?.rootDoc ?? targetDoc : docView?.rootDoc ?? targetDoc; // if target is a marker, then focus toggling should apply to the document it's on since the marker itself doesn't have a hidden field var wasHidden = resolvedTarget.hidden; if (wasHidden) { runInAction(() => { resolvedTarget.hidden = false; // if the target is hidden, un-hide it here. docView?.props.bringToFront(resolvedTarget); }); } const focusAndFinish = action((didFocus: boolean) => { const finalTargetDoc = resolvedTarget; if (options.toggleTarget) { if (!didFocus && !wasHidden) { // don't toggle the hidden state if the doc was already un-hidden as part of this document traversal finalTargetDoc.hidden = !finalTargetDoc.hidden; } } else { finalTargetDoc.hidden && (finalTargetDoc.hidden = undefined); !options.noSelect && docView?.select(false); } if (targetDoc.textHtml && options.zoomTextSelections) { const containerView = DocumentManager.Instance.getFirstDocumentView(finalTargetDoc); if (containerView) { containerView.htmlOverlayEffect = StrCast(options?.effect?.presEffect, StrCast(options?.effect?.followLinkAnimEffect)); containerView.textHtmlOverlay = StrCast(targetDoc.textHtml); DocumentManager._overlayViews.add(containerView); if (Doc.UnhighlightTimer) { Doc.AddUnHighlightWatcher(() => { DocumentManager.removeOverlayViews(); containerView.htmlOverlayEffect = ''; }); } else setTimeout(() => (containerView.htmlOverlayEffect = '')); } } finished?.(); }); const annoContainerView = (!wasHidden || resolvedTarget !== annotatedDoc) && annotatedDoc && this.getFirstDocumentView(annotatedDoc); if (annoContainerView) { if (annoContainerView.props.Document.layoutKey === 'layout_icon') { return annoContainerView.iconify(() => DocumentManager.Instance.AddViewRenderedCb(targetDoc, () => this.jumpToDocument(targetDoc, { ...options, originalTarget, toggleTarget: false }, createViewFunc, docContextPath, finished)), 30); } if (!docView && targetDoc.type !== DocumentType.MARKER) { annoContainerView.focus(targetDoc, {}); // this allows something like a PDF view to remove its doc filters to expose the target so that it can be found in the retry code below } } const contextDoc = docContextPath.length ? docContextPath[0] : undefined; const remainingDocContext = docContextPath.length ? docContextPath.slice(1) : []; const targetDocContext = contextDoc || annotatedDoc; const targetDocContextView = (targetDocContext && this.getFirstDocumentView(targetDocContext)) || (wasHidden && annoContainerView); // if we have an annotation container and the target was hidden, then try again because we just un-hid the document above const focusView = !docView && targetDoc.type === DocumentType.MARKER && annoContainerView ? annoContainerView : docView; if (focusView) { // if (focusView.rootDoc === originalTarget) { if (!options.noSelect) Doc.linkFollowHighlight(focusView.rootDoc, undefined, options.effect); //TODO:glr make this a setting in PresBox else { Doc.linkFollowHighlight(focusView.rootDoc, undefined, options.effect); //TODO:glr make this a setting in PresBox focusView.rootDoc[AnimationSym] = options.effect; if (Doc.UnhighlightTimer) { Doc.AddUnHighlightWatcher(action(() => (focusView.rootDoc[AnimationSym] = undefined))); } } //} if (options.playAudio) DocumentManager.playAudioAnno(focusView.rootDoc); const doFocus = (forceDidFocus: boolean) => focusView.focus(originalTarget, { ...options, originalTarget, afterFocus: (didFocus: boolean) => new Promise(res => { focusAndFinish(forceDidFocus || didFocus); res(ViewAdjustment.doNothing); }), }); if (focusView.props.Document.layoutKey === 'layout_icon' && focusView.rootDoc.type !== DocumentType.SCRIPTING) { focusView.iconify(() => doFocus(true)); } else { doFocus(false); } } else { if (!targetDocContext) { // we don't have a view and there's no context specified ... create a new view of the target using the dockFunc or default createViewFunc(Doc.BrushDoc(targetDoc), () => focusAndFinish(true)); // bcz: should we use this?: Doc.MakeAlias(targetDoc))); } else { // otherwise try to get a view of the context of the target if (targetDocContextView) { // we found a context view and aren't forced to create a new one ... focus on the context first.. wasHidden = wasHidden || targetDocContextView.rootDoc.hidden; targetDocContextView.rootDoc.hidden = false; // make sure context isn't hidden if (targetDocContext.layoutKey === 'layout_icon') { return targetDocContextView.iconify( () => DocumentManager.Instance.AddViewRenderedCb(targetDoc, () => this.jumpToDocument(resolvedTarget ?? targetDoc, { ...options /* originalTarget - needed? */ }, createViewFunc, docContextPath, finished)), 30 ); } const contextFocusTime = options.zoomTime ? options.zoomTime / 2 : 500; const remainingFocustime = options.zoomTime ? options.zoomTime - contextFocusTime : undefined; targetDocContextView.setViewTransition('transform', contextFocusTime); // this makes focusing on contexts run in parallel -- jutmp to document below makes them run sequentially this.AddViewRenderedCb(targetDoc, () => this.jumpToDocument(targetDoc, { ...options, zoomTime: remainingFocustime }, createViewFunc, remainingDocContext, finished)); targetDocContextView.props.focus(targetDocContextView.rootDoc, { ...options, zoomTime: contextFocusTime, // originalTarget, // needed? afterFocus: async () => { // now find the target document within the context if (targetDoc._timecodeToShow) { // if the target has a timecode, it should show up once the (presumed) video context scrubs to the display timecode; targetDocContext._currentTimecode = targetDoc.anchorTimecodeToShow; finished?.(); } else { // otherwise, just look for the target document in this context view now that we've focused the context view if (this.getFirstDocumentView(resolvedTarget)) { // test again for the target view snce we presumably created the context above by focusing on it this.jumpToDocument(targetDoc, { ...options, zoomTime: remainingFocustime }, createViewFunc, remainingDocContext, finished); } else if (targetDoc.layout) { // there will no layout for a TEXTANCHOR type document createViewFunc(Doc.BrushDoc(targetDoc), finished); // create a new view of the target } } return ViewAdjustment.doNothing; }, }); } else { if (docContextPath.length && docContextPath[0]?.layoutKey === 'layout_icon') { Doc.deiconifyView(docContextPath[0]); this.jumpToDocument(targetDoc, options, createViewFunc, docContextPath, finished); } else { // there's no context view so we need to create one first and try again when that finishes createViewFunc( targetDocContext, // after creating the context, this calls the finish function that will retry looking for the target () => this.jumpToDocument(targetDoc, { ...options }, (doc: Doc, finished?: () => void) => doc !== targetDocContext && createViewFunc(doc, finished), remainingDocContext, finished) ); } } } } }; } export function DocFocusOrOpen(doc: Doc, collectionDoc?: Doc) { const cv = collectionDoc && DocumentManager.Instance.getDocumentView(collectionDoc); const dv = DocumentManager.Instance.getDocumentView(doc, (cv?.ComponentView as CollectionFreeFormView)?.props.CollectionView); if (dv && Doc.AreProtosEqual(dv.props.Document, doc)) { dv.props.focus(dv.props.Document, { willPanZoom: true }); Doc.linkFollowHighlight(dv?.props.Document, false); } else { const context = doc.context !== Doc.MyFilesystem && Cast(doc.context, Doc, null); const showDoc = context || doc; CollectionDockingView.AddSplit(Doc.BestAlias(showDoc), OpenWhereMod.right) && context && setTimeout(() => DocumentManager.Instance.getDocumentView(Doc.GetProto(doc))?.focus(doc, {})); } } ScriptingGlobals.add(DocFocusOrOpen);