import { computed, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { Doc, DocListCast } from '../../../fields/Doc'; import { Id } from '../../../fields/FieldSymbols'; import { ObjectField } from '../../../fields/ObjectField'; import { ScriptField } from '../../../fields/ScriptField'; import { BoolCast, Cast, ScriptCast, StrCast } from '../../../fields/Types'; import { TraceMobx } from '../../../fields/util'; import { returnEmptyString } from '../../../Utils'; import { DocUtils } from '../../documents/Documents'; import { CollectionViewType } from '../../documents/DocumentTypes'; import { BranchCreate, BranchTask } from '../../documents/Gitlike'; import { ImageUtils } from '../../util/Import & Export/ImageUtils'; import { InteractionUtils } from '../../util/InteractionUtils'; import { ContextMenu } from '../ContextMenu'; import { ContextMenuProps } from '../ContextMenuItem'; import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../DocComponent'; import { FieldView, FieldViewProps } from '../nodes/FieldView'; import { CollectionCarousel3DView } from './CollectionCarousel3DView'; import { CollectionCarouselView } from './CollectionCarouselView'; import { CollectionDockingView } from './CollectionDockingView'; import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormView'; import { CollectionGridView } from './collectionGrid/CollectionGridView'; import { CollectionLinearView } from './collectionLinear'; import { CollectionMulticolumnView } from './collectionMulticolumn/CollectionMulticolumnView'; import { CollectionMultirowView } from './collectionMulticolumn/CollectionMultirowView'; import { CollectionNoteTakingView } from './CollectionNoteTakingView'; import { CollectionPileView } from './CollectionPileView'; import { CollectionSchemaView } from './collectionSchema/CollectionSchemaView'; import { CollectionStackingView } from './CollectionStackingView'; import { SubCollectionViewProps } from './CollectionSubView'; import { CollectionTimeView } from './CollectionTimeView'; import { CollectionTreeView } from './CollectionTreeView'; import './CollectionView.scss'; export const COLLECTION_BORDER_WIDTH = 2; const path = require('path'); interface CollectionViewProps_ extends FieldViewProps { isAnnotationOverlay?: boolean; // is the collection an annotation overlay (eg an overlay on an image/video/etc) isAnnotationOverlayScrollable?: boolean; // whether the annotation overlay can be vertically scrolled (just for tree views, currently) layoutEngine?: () => string; setPreviewCursor?: (func: (x: number, y: number, drag: boolean, hide: boolean) => void) => void; setBrushViewer?: (func?: (view: { width: number; height: number; panX: number; panY: number }) => void) => void; // property overrides for child documents childDocuments?: Doc[]; // used to override the documents shown by the sub collection to an explicit list (see LinkBox) childDocumentsActive?: () => boolean; // whether child documents can be dragged if collection can be dragged (eg., in a when a Pile document is in startburst mode) childFitWidth?: (child: Doc) => boolean; childShowTitle?: () => string; childOpacity?: () => number; childContextMenuItems?: () => { script: ScriptField; label: string }[]; childHideTitle?: () => boolean; // whether to hide the documentdecorations title for children childHideDecorationTitle?: () => boolean; childHideResizeHandles?: () => boolean; childLayoutTemplate?: () => Doc | undefined; // specify a layout Doc template to use for children of the collection childXPadding?: number; childYPadding?: number; childLayoutString?: string; childIgnoreNativeSize?: boolean; childClickScript?: ScriptField; childDoubleClickScript?: ScriptField; //TODO: [AL] add these fields AddToMap?: (treeViewDoc: Doc, index: number[]) => Doc[]; RemFromMap?: (treeViewDoc: Doc, index: number[]) => Doc[]; hierarchyIndex?: number[]; // hierarchical index of a document up to the rendering root (primarily used for tree views) } export interface CollectionViewProps extends React.PropsWithChildren {} @observer export class CollectionView extends ViewBoxAnnotatableComponent() { public static LayoutString(fieldStr: string) { return FieldView.LayoutString(CollectionView, fieldStr); } @observable private static _safeMode = false; public static SetSafeMode(safeMode: boolean) { this._safeMode = safeMode; } protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer; constructor(props: any) { super(props); runInAction(() => (this._annotationKeySuffix = returnEmptyString)); } get collectionViewType(): CollectionViewType | undefined { const viewField = StrCast(this.layoutDoc._viewType); if (CollectionView._safeMode) { switch (viewField) { case CollectionViewType.Freeform: case CollectionViewType.Schema: return CollectionViewType.Tree; case CollectionViewType.Invalid: return CollectionViewType.Freeform; } } return viewField as any as CollectionViewType; } showIsTagged = () => { return null; // this section would display an icon in the bototm right of a collection to indicate that all // photos had been processed through Google's content analysis API and Google's tags had been // assigned to the documents googlePhotosTags field. // const children = DocListCast(this.rootDoc[this.props.fieldKey]); // const imageProtos = children.filter(doc => Cast(doc.data, ImageField)).map(Doc.GetProto); // const allTagged = imageProtos.length > 0 && imageProtos.every(image => image.googlePhotosTags); // return !allTagged ? (null) : ; //this.isContentActive(); }; screenToLocalTransform = () => (this.props.renderDepth ? this.props.ScreenToLocalTransform() : this.props.ScreenToLocalTransform().scale(this.props.PanelWidth() / this.bodyPanelWidth())); // prettier-ignore private renderSubView = (type: CollectionViewType | undefined, props: SubCollectionViewProps) => { TraceMobx(); if (type === undefined) return null; switch (type) { default: case CollectionViewType.Freeform: return ; case CollectionViewType.Docking: return ; case CollectionViewType.Schema: return ; case CollectionViewType.Docking: return ; case CollectionViewType.Tree: return ; case CollectionViewType.Multicolumn: return ; case CollectionViewType.Multirow: return ; case CollectionViewType.Linear: return ; case CollectionViewType.Pile: return ; case CollectionViewType.Carousel: return ; case CollectionViewType.Carousel3D: return ; case CollectionViewType.Stacking: return ; case CollectionViewType.NoteTaking: return ; case CollectionViewType.Masonry: return ; case CollectionViewType.Time: return ; case CollectionViewType.Grid: return ; //case CollectionViewType.Staff: return ; } }; setupViewTypes(category: string, func: (viewType: CollectionViewType) => Doc, addExtras: boolean) { const subItems: ContextMenuProps[] = []; subItems.push({ description: 'Freeform', event: () => func(CollectionViewType.Freeform), icon: 'signature' }); if (addExtras && CollectionView._safeMode) { ContextMenu.Instance.addItem({ description: 'Test Freeform', event: () => func(CollectionViewType.Invalid), icon: 'project-diagram' }); } subItems.push({ description: 'Schema', event: () => func(CollectionViewType.Schema), icon: 'th-list' }); subItems.push({ description: 'Tree', event: () => func(CollectionViewType.Tree), icon: 'tree' }); subItems.push({ description: 'Stacking', event: () => (func(CollectionViewType.Stacking)._autoHeight = true), icon: 'ellipsis-v' }); subItems.push({ description: 'Notetaking', event: () => (func(CollectionViewType.NoteTaking)._autoHeight = true), icon: 'ellipsis-v' }); subItems.push({ description: 'Multicolumn', event: () => func(CollectionViewType.Multicolumn), icon: 'columns' }); subItems.push({ description: 'Multirow', event: () => func(CollectionViewType.Multirow), icon: 'columns' }); subItems.push({ description: 'Masonry', event: () => func(CollectionViewType.Masonry), icon: 'columns' }); subItems.push({ description: 'Carousel', event: () => func(CollectionViewType.Carousel), icon: 'columns' }); subItems.push({ description: '3D Carousel', event: () => func(CollectionViewType.Carousel3D), icon: 'columns' }); !Doc.noviceMode && subItems.push({ description: 'Pivot/Time', event: () => func(CollectionViewType.Time), icon: 'columns' }); !Doc.noviceMode && subItems.push({ description: 'Map', event: () => func(CollectionViewType.Map), icon: 'globe-americas' }); subItems.push({ description: 'Grid', event: () => func(CollectionViewType.Grid), icon: 'th-list' }); if (!Doc.IsSystem(this.rootDoc) && this.rootDoc._viewType !== CollectionViewType.Docking && !this.rootDoc.isGroup && !this.rootDoc.annotationOn) { const existingVm = ContextMenu.Instance.findByDescription(category); const catItems = existingVm && 'subitems' in existingVm ? existingVm.subitems : []; catItems.push({ description: 'Add a Perspective...', addDivider: true, noexpand: true, subitems: subItems, icon: 'eye' }); !existingVm && ContextMenu.Instance.addItem({ description: category, subitems: catItems, icon: 'eye' }); } } onContextMenu = (e: React.MouseEvent): void => { const cm = ContextMenu.Instance; if (cm && !e.isPropagationStopped() && this.rootDoc[Id] !== Doc.MainDocId) { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7 this.setupViewTypes( 'UI Controls...', vtype => { const newRendition = Doc.MakeAlias(this.rootDoc); newRendition._viewType = vtype; this.props.addDocTab(newRendition, 'add:right'); return newRendition; }, false ); const options = cm.findByDescription('Options...'); const optionItems = options && 'subitems' in options ? options.subitems : []; !Doc.noviceMode ? optionItems.splice(0, 0, { description: `${this.rootDoc.forceActive ? 'Select' : 'Force'} Contents Active`, event: () => (this.rootDoc.forceActive = !this.rootDoc.forceActive), icon: 'project-diagram' }) : null; if (this.rootDoc.childLayout instanceof Doc) { optionItems.push({ description: 'View Child Layout', event: () => this.props.addDocTab(this.rootDoc.childLayout as Doc, 'add:right'), icon: 'project-diagram' }); } if (this.rootDoc.childClickedOpenTemplateView instanceof Doc) { optionItems.push({ description: 'View Child Detailed Layout', event: () => this.props.addDocTab(this.rootDoc.childClickedOpenTemplateView as Doc, 'add:right'), icon: 'project-diagram' }); } !Doc.noviceMode && optionItems.push({ description: `${this.rootDoc.isInPlaceContainer ? 'Unset' : 'Set'} inPlace Container`, event: () => (this.rootDoc.isInPlaceContainer = !this.rootDoc.isInPlaceContainer), icon: 'project-diagram' }); if (!Doc.noviceMode && false) { optionItems.push({ description: 'Create Branch', event: async () => this.props.addDocTab(await BranchCreate(this.rootDoc), 'add:right'), icon: 'project-diagram', }); optionItems.push({ description: 'Pull Master', event: () => BranchTask(this.rootDoc, 'pull'), icon: 'project-diagram', }); optionItems.push({ description: 'Merge Branches', event: () => BranchTask(this.rootDoc, 'merge'), icon: 'project-diagram', }); } !options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'hand-point-right' }); if (!Doc.noviceMode && !this.rootDoc.annotationOn) { const existingOnClick = cm.findByDescription('OnClick...'); const onClicks = existingOnClick && 'subitems' in existingOnClick ? existingOnClick.subitems : []; const funcs = [ { key: 'onChildClick', name: 'On Child Clicked' }, { key: 'onChildDoubleClick', name: 'On Child Double Clicked' }, ]; funcs.map(func => onClicks.push({ description: `Edit ${func.name} script`, icon: 'edit', event: (obj: any) => { const alias = Doc.MakeAlias(this.rootDoc); DocUtils.makeCustomViewClicked(alias, undefined, func.key); this.props.addDocTab(alias, 'add:right'); }, }) ); DocListCast(Cast(Doc.UserDoc()['clickFuncs-child'], Doc, null)?.data).forEach(childClick => onClicks.push({ description: `Set child ${childClick.title}`, icon: 'edit', event: () => (Doc.GetProto(this.rootDoc)[StrCast(childClick.targetScriptKey)] = ObjectField.MakeCopy(ScriptCast(childClick.data))), }) ); !Doc.IsSystem(this.rootDoc) && !existingOnClick && cm.addItem({ description: 'OnClick...', noexpand: true, subitems: onClicks, icon: 'mouse-pointer' }); } if (!Doc.noviceMode) { const more = cm.findByDescription('More...'); const moreItems = more && 'subitems' in more ? more.subitems : []; moreItems.push({ description: 'Export Image Hierarchy', icon: 'columns', event: () => ImageUtils.ExportHierarchyToFileSystem(this.rootDoc) }); !more && cm.addItem({ description: 'More...', subitems: moreItems, icon: 'hand-point-right' }); } } }; bodyPanelWidth = () => this.props.PanelWidth(); childHideResizeHandles = () => this.props.childHideResizeHandles?.() ?? BoolCast(this.Document.childHideResizeHandles); childHideDecorationTitle = () => this.props.childHideDecorationTitle?.() ?? BoolCast(this.Document.childHideDecorationTitle); childLayoutTemplate = () => this.props.childLayoutTemplate?.() || Cast(this.rootDoc.childLayoutTemplate, Doc, null); @computed get childLayoutString() { return StrCast(this.rootDoc.childLayoutString); } isContentActive = (outsideReaction?: boolean) => this.props.isContentActive(); render() { TraceMobx(); const props: SubCollectionViewProps = { ...this.props, addDocument: this.addDocument, moveDocument: this.moveDocument, removeDocument: this.removeDocument, isContentActive: this.isContentActive, isAnyChildContentActive: this.isAnyChildContentActive, whenChildContentsActiveChanged: this.whenChildContentsActiveChanged, PanelWidth: this.bodyPanelWidth, PanelHeight: this.props.PanelHeight, ScreenToLocalTransform: this.screenToLocalTransform, childLayoutTemplate: this.childLayoutTemplate, childLayoutString: this.childLayoutString, childHideResizeHandles: this.childHideResizeHandles, childHideDecorationTitle: this.childHideDecorationTitle, CollectionView: this, }; return (
{this.showIsTagged()} {this.renderSubView(this.collectionViewType, props)}
); } }