aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorbobzel <zzzman@gmail.com>2023-12-07 10:13:20 -0500
committerbobzel <zzzman@gmail.com>2023-12-07 10:13:20 -0500
commit0d4c4ba17f90cf80403e6c65d2402125537cbd6b (patch)
tree0096af1c8ce92837028723ebf29d42796e22dad6
parentdd1db35513257abc6f36da5f8608afdde1bc4dd8 (diff)
parent3d3878f721c0c86d59e2d1201990d9336b6283ed (diff)
merged infoUI
-rw-r--r--src/client/util/LinkManager.ts4
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoState.tsx88
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoUI.tsx173
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss22
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx42
-rw-r--r--src/client/views/nodes/DocumentView.tsx6
-rw-r--r--src/fields/ScriptField.ts2
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;
},