diff options
Diffstat (limited to 'src')
7 files changed, 316 insertions, 21 deletions
diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts index 608184596..3e98ea379 100644 --- a/src/client/util/LinkManager.ts +++ b/src/client/util/LinkManager.ts @@ -163,8 +163,8 @@ export class LinkManager { public getAllRelatedLinks(anchor: Doc) { return this.relatedLinker(anchor); } // finds all links that contain the given anchor - public getAllDirectLinks(anchor: Doc): Doc[] { - return Array.from(Doc.GetProto(anchor)[DirectLinks]); + public getAllDirectLinks(anchor?: Doc): Doc[] { + return anchor ? Array.from(Doc.GetProto(anchor)[DirectLinks]) : []; } // finds all links that contain the given anchor relatedLinker = computedFn(function relatedLinker(this: any, anchor: Doc): Doc[] { diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoState.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoState.tsx new file mode 100644 index 000000000..3ba7aedef --- /dev/null +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoState.tsx @@ -0,0 +1,88 @@ +import { IReactionDisposer, reaction } from 'mobx'; +import { observer } from 'mobx-react'; +import * as React from 'react'; +import './CollectionFreeFormView.scss'; + +/** + * An Fsa Arc. The first array element is a test condition function that will be observed. + * The second array element is a function that will be invoked when the first test function + * returns a truthy value + */ +export type infoArc = [() => any, (res?: any) => infoState]; + +export const StateMessage = Symbol('StateMessage'); +export const StateEntryFunc = Symbol('StateEntryFunc'); +export class infoState { + [StateMessage]: string = ''; + [StateEntryFunc]?: () => any; + [key: string]: infoArc; + constructor(message: string, arcs: { [key: string]: infoArc }, entryFunc?: () => any) { + this[StateMessage] = message; + Object.assign(this, arcs); + this[StateEntryFunc] = entryFunc; + } +} + +/** + * Create an FSA state. + * @param msg the message displayed when in this state + * @param arcs an object with fields containing @infoArcs (an object with field names indicating the arc transition and + * field values being a tuple of an arc transition trigger function (that returns a truthy value when the arc should fire), + * and an arc transition action function (that sets the next state) + * @param entryFunc a function to call when entering the state + * @returns an FSA state + */ +export function InfoState( + msg: string, // + arcs: { [key: string]: infoArc }, + entryFunc?: () => any +) { + return new infoState(msg, arcs, entryFunc); +} + +export interface CollectionFreeFormInfoStateProps { + infoState: infoState; + next: (state: infoState) => any; +} + +@observer +export class CollectionFreeFormInfoState extends React.Component<CollectionFreeFormInfoStateProps> { + _disposers: IReactionDisposer[] = []; + + get State() { + return this.props.infoState; + } + get Arcs() { + return Object.keys(this.State).map(key => this.State[key]); + } + + clearState = () => this._disposers.map(disposer => disposer()); + initState = () => + (this._disposers = this.Arcs.map(arc => ({ test: arc[0], act: arc[1] })).map(arc => { + return reaction( + // + arc.test, + res => { + if (res) { + const next = arc.act(res); + this.props.next(next); + } + }, + { fireImmediately: true } + ); + })); + + componentDidMount(): void { + this.initState(); + } + componentDidUpdate() { + this.clearState(); + this.initState(); + } + componentWillUnmount(): void { + this.clearState(); + } + render() { + return <div className="infoUI">{this.State[StateMessage]}</div>; + } +} diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoUI.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoUI.tsx new file mode 100644 index 000000000..1265dc2de --- /dev/null +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoUI.tsx @@ -0,0 +1,173 @@ +import { IReactionDisposer, computed, observable, reaction, action, runInAction } 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 * as React from 'react'; +import { CollectionFreeFormView } from './CollectionFreeFormView'; +import { NumCast } from '../../../../fields/Types'; +import { LinkManager } from '../../../util/LinkManager'; +import { InkTool } from '../../../../fields/InkField'; +import { LinkDocPreview } from '../../nodes/LinkDocPreview'; +import { DocumentLinksButton } from '../../nodes/DocumentLinksButton'; +import { DocumentManager } from '../../../util/DocumentManager'; +import { CollectionFreeFormInfoState, infoState, StateMessage, infoArc, StateEntryFunc, InfoState } from './CollectionFreeFormInfoState'; +import { string32 } from 'pdfjs-dist/types/src/shared/util'; +import { any } from 'bluebird'; + +export interface CollectionFreeFormInfoUIProps { + Document: Doc; + Freeform: CollectionFreeFormView; +} + +@observer +export class CollectionFreeFormInfoUI extends React.Component<CollectionFreeFormInfoUIProps> { + private _disposers: { [name: string]: IReactionDisposer } = {}; + + @observable currState!: infoState; + constructor(props: any) { + super(props); + this.setCurrState(this.setupStates()); + } + + setCurrState = (state: infoState) => { + if (state) { + runInAction(() => (this.currState = state)); + this.currState[StateEntryFunc]?.(); + } + }; + + setupStates = () => { + // state entry functions + const setBackground = (col: string) => () => (this.props.Freeform.layoutDoc.backgroundColor = col); + // arc transition trigger conditions + const firstDoc = () => this.props.Freeform.childDocs.lastElement(); + const numDocs = () => this.props.Freeform.childDocs.length; + const numDocLinks = () => LinkManager.Instance.getAllDirectLinks(firstDoc())?.length; + const linkMenuOpen = () => DocumentLinksButton.LinkEditorDocView; + + // set of states. + const start = InfoState('Click to create Object', { + docCreated: [() => numDocs(), () => oneDoc], + }, setBackground("blue")); // prettier-ignore + + const oneDoc = InfoState('Create a second doc', { + docCreated: [() => numDocs() > 1, () => multipleDocs], + docDeleted: [() => numDocs() < 1, () => start], + }, setBackground("green")); // prettier-ignore + + const multipleDocs = InfoState('Create a link', { + linkCreated: [() => numDocLinks(), () => madeLink], + docsRemoved: [() => numDocs() < 2, () => oneDoc], + }, setBackground("orange")); // prettier-ignore + + const madeLink = InfoState('View links', { + linkCreated: [() => !numDocLinks(), () => multipleDocs], + linksViewed: [() => linkMenuOpen(), (res) => { alert("Yay"+ res); return completed;}], + }, setBackground("yellow")); // prettier-ignore + + const completed = InfoState('You did it!', { + linkDeleted: [() => !numDocLinks(), () => multipleDocs], + docsRemoved: [() => numDocs() < 2, () => oneDoc], + }, setBackground("white")); // prettier-ignore + + return start; + }; + + /* + componentDidMount(): void { + this._disposers.reaction1 = reaction( + () => this.props.Freeform.childDocs.slice(), + docs => { + if (docs.length === 1) { + this.firstDoc = docs[0]; + this.firstDocPos = { x: NumCast(this.firstDoc.x), y: NumCast(this.firstDoc.y) }; + this.message = 'Hello world! You can drag and drop to move your document around.'; + } else if (docs.length === 2) { + this.message = 'Great job. To create a new link between them, click the link icon on both documents.'; + } else { + // this.message = 'Click anywhere and begin typing to create your first text document!'; + } + }, + { fireImmediately: true } + ); + this._disposers.reaction2 = reaction( + () => ({ x: NumCast(this.firstDoc?.x), y: NumCast(this.firstDoc?.y), links: this.firstDoc && LinkManager.Instance.getAllDirectLinks(this.firstDoc) }), + ({ x, y, links }) => { + if ((x && x != this.firstDocPos.x) || (y && y != this.firstDocPos.y)) { + this.message = 'Great moves. Try creating a second document.'; + } + if (links && links.length > 0) { + this.message = 'You made your first link! You can view your links by selecting the blue dot.'; + } + }, + { fireImmediately: true } + ); + this._disposers.reaction3 = reaction( + () => ({ activeTool: Doc.ActiveTool, viewingLinks: DocumentLinksButton.LinkEditorDocView }), + ({ activeTool, viewingLinks }) => { + if (activeTool == InkTool.Pen) { + this.message = "You're in pen mode! Click and drag to draw your first masterpiece."; + } + if (viewingLinks) { + this.message = 'To edit your links, click the pencil icon.'; + } + if (Doc.ActiveTool === InkTool.Pen) { + this.message = 'Editing links'; + } + }, + { fireImmediately: true } + ); + this._disposers.reaction4 = reaction( + () => ({ startLink: DocumentLinksButton.StartLink, endLink: Doc.UserDoc().links }), + ({ startLink, endLink }) => { + if (startLink) { + this.message = "You've started a link."; + } else if (endLink) { + this.message = "You've completed a link."; + } + } + ); + this._disposers.reaction5 = reaction( + () => ({ pin: Doc.ActivePresentation?.data, trails: DocumentManager.Instance.DocumentViews.find(view => view.Document === Doc.MyTrails) }), + ({ pin, trails }) => { + // if (pin) { + // this.message = 'You pinned your doc to a trail.'; + // } + if (trails) { + this.message = 'This is your trails tab.'; + } + } + ); + this._disposers.reaction6 = reaction( + () => ({ presentationMode: Doc.ActivePresentation?.presentation_status }), + ({ presentationMode }) => { + if (presentationMode === 'edit') { + this.message = 'You are editing your presentation.'; + } else if (presentationMode === 'manual') { + this.message = 'Manual presentation mode'; + } else if (presentationMode === 'auto') { + this.message = 'Auto presentation mode'; + } + } + ); + } + + componentWillUnmount(): void { + Object.values(this._disposers).forEach(disposer => disposer?.()); + } + + // stop reaction from what it's currently doing + // this._disposers.reaction1(); + + @observable message = 'Click anywhere and begin typing to create your first document!'; + @observable firstDoc: Doc | undefined; + @observable secondDoc: Doc | undefined; + */ + firstDocPos = { x: 0, y: 0 }; + + render() { + return <CollectionFreeFormInfoState next={this.setCurrState} infoState={this.currState} />; + } +} diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss index 250760bd5..1b596ab65 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss @@ -255,3 +255,25 @@ background-color: rgba($color: #000000, $alpha: 0.4); position: absolute; } + +.infoUI { + position: absolute; + display: flex; + top: 0; + // height: 100%; + // width: 100%; + // width:fit-content; + // width:-webkit-fit-content; + // width:-moz-fit-content; + // bottom: 46px; + + overflow: auto; + + color: white; + background-color: #5075ef; + border-radius: 5px; + box-shadow: 2px 2px 5px black; + + margin: 15px; + padding: 10px; +} diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index fbad01cad..03d302f39 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -3,6 +3,7 @@ import { Colors } from 'browndash-components'; import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import { computedFn } from 'mobx-utils'; +import * as React from 'react'; import { DateField } from '../../../../fields/DateField'; import { Doc, DocListCast, Opt } from '../../../../fields/Doc'; import { DocData, Height, Width } from '../../../../fields/DocSymbols'; @@ -45,12 +46,12 @@ import { StyleProp } from '../../StyleProvider'; import { CollectionSubView } from '../CollectionSubView'; import { TreeViewType } from '../CollectionTreeView'; import { CollectionFreeFormBackgroundGrid } from './CollectionFreeFormBackgroundGrid'; +import { CollectionFreeFormInfoUI } from './CollectionFreeFormInfoUI'; 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'; -import * as React from 'react'; export type collectionFreeformViewProps = { NativeWidth?: () => number; @@ -814,23 +815,26 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection eraserMax.X >= inkViewBounds.left && eraserMax.Y >= inkViewBounds.top ) - .reduce((intersections, { inkStroke, inkView }) => { - const { inkData } = inkStroke.inkScaledData(); - // Convert from screen space to ink space for the intersection. - const prevPointInkSpace = inkStroke.ptFromScreen(lastPoint); - const currPointInkSpace = inkStroke.ptFromScreen(currPoint); - for (var i = 0; i < inkData.length - 3; i += 4) { - const rawIntersects = InkField.Segment(inkData, i).intersects({ - // compute all unique intersections - p1: { x: prevPointInkSpace.X, y: prevPointInkSpace.Y }, - p2: { x: currPointInkSpace.X, y: currPointInkSpace.Y }, - }); - const intersects = Array.from(new Set(rawIntersects as (number | string)[])); // convert to more manageable union array type - // return tuples of the inkingStroke intersected, and the t value of the intersection - intersections.push(...intersects.map(t => ({ inkView, t: +t + Math.floor(i / 4) }))); // convert string t's to numbers and add start of curve segment to convert from local t value to t value along complete curve - } - return intersections; - }, [] as { t: number; inkView: DocumentView }[]); + .reduce( + (intersections, { inkStroke, inkView }) => { + const { inkData } = inkStroke.inkScaledData(); + // Convert from screen space to ink space for the intersection. + const prevPointInkSpace = inkStroke.ptFromScreen(lastPoint); + const currPointInkSpace = inkStroke.ptFromScreen(currPoint); + for (var i = 0; i < inkData.length - 3; i += 4) { + const rawIntersects = InkField.Segment(inkData, i).intersects({ + // compute all unique intersections + p1: { x: prevPointInkSpace.X, y: prevPointInkSpace.Y }, + p2: { x: currPointInkSpace.X, y: currPointInkSpace.Y }, + }); + const intersects = Array.from(new Set(rawIntersects as (number | string)[])); // convert to more manageable union array type + // return tuples of the inkingStroke intersected, and the t value of the intersection + intersections.push(...intersects.map(t => ({ inkView, t: +t + Math.floor(i / 4) }))); // convert string t's to numbers and add start of curve segment to convert from local t value to t value along complete curve + } + return intersections; + }, + [] as { t: number; inkView: DocumentView }[] + ); }; /** @@ -1428,6 +1432,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection return anchor; }; + infoUI = () => <CollectionFreeFormInfoUI Document={this.Document} Freeform={this} />; + componentDidMount() { this.props.setContentView?.(this); super.componentDidMount?.(); diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index f6a14eab1..f2a910023 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -136,6 +136,7 @@ export interface DocComponentView { componentUI?: (boundsLeft: number, boundsTop: number) => JSX.Element | null; dragStarting?: (snapToDraggedDoc: boolean, showGroupDragTarget: boolean, visited: Set<Doc>) => void; incrementalRendering?: () => void; + infoUI?: () => JSX.Element; getCenter?: (xf: Transform) => { X: number; Y: number }; screenBounds?: () => Opt<{ left: number; top: number; right: number; bottom: number; center?: { X: number; Y: number } }>; ptToScreen?: (pt: { X: number; Y: number }) => { X: number; Y: number }; @@ -1645,6 +1646,10 @@ export class DocumentView extends React.Component<DocumentViewProps> { ); } + @computed get infoUI() { + return this.ComponentView?.infoUI?.(); + } + render() { TraceMobx(); const xshift = Math.abs(this.Xshift) <= 0.001 ? this.props.PanelWidth() : undefined; @@ -1679,6 +1684,7 @@ export class DocumentView extends React.Component<DocumentViewProps> { ref={action((r: DocumentViewInternal | null) => r && (this.docView = r))} /> {this.htmlOverlay} + {this.infoUI} </div> )} diff --git a/src/fields/ScriptField.ts b/src/fields/ScriptField.ts index 85cc88a87..62690a9fb 100644 --- a/src/fields/ScriptField.ts +++ b/src/fields/ScriptField.ts @@ -15,7 +15,7 @@ function optional(propSchema: PropSchema) { return custom( value => { if (value !== undefined) { - return propSchema.serializer(value); + return propSchema.serializer(value, '', undefined); // this function only takes one parameter, but I think its typescript typings are messed up to take 3 } return SKIP; }, |