diff options
34 files changed, 701 insertions, 957 deletions
diff --git a/package-lock.json b/package-lock.json index b3fb00ab6..c956fd3e9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2832,8 +2832,7 @@ }, "ansi-regex": { "version": "2.1.1", - "bundled": true, - "optional": true + "bundled": true }, "aproba": { "version": "1.2.0", @@ -2851,13 +2850,11 @@ }, "balanced-match": { "version": "1.0.0", - "bundled": true, - "optional": true + "bundled": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2870,18 +2867,15 @@ }, "code-point-at": { "version": "1.1.0", - "bundled": true, - "optional": true + "bundled": true }, "concat-map": { "version": "0.0.1", - "bundled": true, - "optional": true + "bundled": true }, "console-control-strings": { "version": "1.1.0", - "bundled": true, - "optional": true + "bundled": true }, "core-util-is": { "version": "1.0.2", @@ -2984,8 +2978,7 @@ }, "inherits": { "version": "2.0.4", - "bundled": true, - "optional": true + "bundled": true }, "ini": { "version": "1.3.5", @@ -2995,7 +2988,6 @@ "is-fullwidth-code-point": { "version": "1.0.0", "bundled": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -3008,20 +3000,17 @@ "minimatch": { "version": "3.0.4", "bundled": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } }, "minimist": { "version": "1.2.5", - "bundled": true, - "optional": true + "bundled": true }, "minipass": { "version": "2.9.0", "bundled": true, - "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -3038,7 +3027,6 @@ "mkdirp": { "version": "0.5.3", "bundled": true, - "optional": true, "requires": { "minimist": "^1.2.5" } @@ -3094,8 +3082,7 @@ }, "npm-normalize-package-bin": { "version": "1.0.1", - "bundled": true, - "optional": true + "bundled": true }, "npm-packlist": { "version": "1.4.8", @@ -3120,8 +3107,7 @@ }, "number-is-nan": { "version": "1.0.1", - "bundled": true, - "optional": true + "bundled": true }, "object-assign": { "version": "4.1.1", @@ -3131,7 +3117,6 @@ "once": { "version": "1.4.0", "bundled": true, - "optional": true, "requires": { "wrappy": "1" } @@ -3200,8 +3185,7 @@ }, "safe-buffer": { "version": "5.1.2", - "bundled": true, - "optional": true + "bundled": true }, "safer-buffer": { "version": "2.1.2", @@ -3231,7 +3215,6 @@ "string-width": { "version": "1.0.2", "bundled": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -3249,7 +3232,6 @@ "strip-ansi": { "version": "3.0.1", "bundled": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -3288,13 +3270,11 @@ }, "wrappy": { "version": "1.0.2", - "bundled": true, - "optional": true + "bundled": true }, "yallist": { "version": "3.1.1", - "bundled": true, - "optional": true + "bundled": true } } } @@ -18074,4 +18054,4 @@ } } } -}
\ No newline at end of file +} diff --git a/src/client/apis/youtube/YoutubeBox.tsx b/src/client/apis/youtube/YoutubeBox.tsx index ce7f49e64..a7045ccb7 100644 --- a/src/client/apis/youtube/YoutubeBox.tsx +++ b/src/client/apis/youtube/YoutubeBox.tsx @@ -6,7 +6,6 @@ import { Utils } from "../../../Utils"; import { DocServer } from "../../DocServer"; import { Docs } from "../../documents/Documents"; import { DocumentDecorations } from "../../views/DocumentDecorations"; -import { InkingControl } from "../../views/InkingControl"; import { FieldView, FieldViewProps } from "../../views/nodes/FieldView"; import "../../views/nodes/WebBox.scss"; import "./YoutubeBox.scss"; @@ -156,14 +155,14 @@ export class YoutubeBox extends React.Component<FieldViewProps> { @action processVideoDetails = (videoDetails: any[]) => { this.videoDetails = videoDetails; - this.props.Document.cachedDetails = Docs.Get.FromJson({ data: videoDetails, title: "detailBackUp" }); + this.props.Document.cachedDetails = Doc.Get.FromJson({ data: videoDetails, title: "detailBackUp" }); } /** * The function that stores the search results in the props document. */ backUpSearchResults = (videos: any[]) => { - this.props.Document.cachedSearchResults = Docs.Get.FromJson({ data: videos, title: "videosBackUp" }); + this.props.Document.cachedSearchResults = Doc.Get.FromJson({ data: videos, title: "videosBackUp" }); } /** @@ -350,7 +349,7 @@ export class YoutubeBox extends React.Component<FieldViewProps> { const frozen = !this.props.isSelected() || DocumentDecorations.Instance.Interacting; - const classname = "webBox-cont" + (this.props.isSelected() && !InkingControl.Instance.selectedTool && !DocumentDecorations.Instance.Interacting ? "-interactive" : ""); + const classname = "webBox-cont" + (this.props.isSelected() && !Doc.selectedTool && !DocumentDecorations.Instance.Interacting ? "-interactive" : ""); return ( <> <div className={classname} > diff --git a/src/client/cognitive_services/CognitiveServices.ts b/src/client/cognitive_services/CognitiveServices.ts index b816d1617..6b0b3e029 100644 --- a/src/client/cognitive_services/CognitiveServices.ts +++ b/src/client/cognitive_services/CognitiveServices.ts @@ -1,7 +1,6 @@ import * as request from "request-promise"; import { Doc, Field } from "../../fields/Doc"; import { Cast } from "../../fields/Types"; -import { Docs } from "../documents/Documents"; import { Utils } from "../../Utils"; import { InkData } from "../../fields/InkField"; import { UndoManager } from "../util/UndoManager"; @@ -195,7 +194,7 @@ export namespace CognitiveServices { let results = await ExecuteQuery(Service.Handwriting, Manager, inkData); if (results) { results.recognitionUnits && (results = results.recognitionUnits); - target[keys[0]] = Docs.Get.FromJson({ data: results, title: "Ink Analysis" }); + target[keys[0]] = Doc.Get.FromJson({ data: results, title: "Ink Analysis" }); const recognizedText = results.map((item: any) => item.recognizedText); const recognizedObjects = results.map((item: any) => item.recognizedObject); const individualWords = recognizedText.filter((text: string) => text && text.split(" ").length === 1); diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 71bf8a516..994f8a147 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -9,11 +9,11 @@ import { ScriptingBox } from "../views/nodes/ScriptingBox"; import { VideoBox } from "../views/nodes/VideoBox"; import { WebBox } from "../views/nodes/WebBox"; import { OmitKeys, JSONUtils, Utils } from "../../Utils"; -import { Field, Doc, Opt, DocListCastAsync, FieldResult, DocListCast } from "../../fields/Doc"; +import { Field, Doc, Opt, DocListCastAsync, FieldResult, DocListCast, HeightSym, WidthSym } from "../../fields/Doc"; import { ImageField, VideoField, AudioField, PdfField, WebField, YoutubeField } from "../../fields/URLField"; import { HtmlField } from "../../fields/HtmlField"; import { List } from "../../fields/List"; -import { Cast, NumCast, StrCast } from "../../fields/Types"; +import { Cast, NumCast, StrCast, FieldValue } from "../../fields/Types"; import { DocServer } from "../DocServer"; import { dropActionType } from "../util/DragManager"; import { DateField } from "../../fields/DateField"; @@ -36,11 +36,9 @@ import { PresElementBox } from "../views/presentationview/PresElementBox"; import { DashWebRTCVideo } from "../views/webcam/DashWebRTCVideo"; import { QueryBox } from "../views/nodes/QueryBox"; import { ColorBox } from "../views/nodes/ColorBox"; -import { LinkAnchorBox } from "../views/nodes/LinkAnchorBox"; import { DocHolderBox } from "../views/nodes/DocHolderBox"; import { InkingStroke } from "../views/InkingStroke"; import { InkField } from "../../fields/InkField"; -import { InkingControl } from "../views/InkingControl"; import { RichTextField } from "../../fields/RichTextField"; import { extname } from "path"; import { MessageStore } from "../../server/Message"; @@ -49,6 +47,12 @@ import { ContextMenu } from "../views/ContextMenu"; import { LinkBox } from "../views/nodes/LinkBox"; import { ScreenshotBox } from "../views/nodes/ScreenshotBox"; import { ComparisonBox } from "../views/nodes/ComparisonBox"; +import { Id } from "../../fields/FieldSymbols"; +import { listSpec } from "../../fields/Schema"; +import { ObjectField } from "../../fields/ObjectField"; +import { RefField } from "../../fields/RefField"; +import { runInAction } from "mobx"; +import { UndoManager } from "../util/UndoManager"; const path = require('path'); export interface DocumentOptions { @@ -455,7 +459,7 @@ export namespace Docs { const doc = StackingDocument(deviceImages, { title, _LODdisable: true, hero: new ImageField(constructed[0].url) }); doc.nameAliases = new List<string>([title.toLowerCase()]); // add the parsed attributes to this main document - Docs.Get.FromJson({ data: device, appendToExisting: { targetDoc: Doc.GetProto(doc) } }); + Doc.Get.FromJson({ data: device, appendToExisting: { targetDoc: Doc.GetProto(doc) } }); Doc.AddDocToList(parentProto, "data", doc); } else if (errors) { console.log(errors); @@ -777,225 +781,6 @@ export namespace Docs { return InstanceFromProto(proto, undefined, options); } } - - export namespace Get { - - const primitives = ["string", "number", "boolean"]; - - export interface JsonConversionOpts { - data: any; - title?: string; - appendToExisting?: { targetDoc: Doc, fieldKey?: string }; - excludeEmptyObjects?: boolean; - } - - const defaultKey = "json"; - - /** - * This function takes any valid JSON(-like) data, i.e. parsed or unparsed, and at arbitrarily - * deep levels of nesting, converts the data and structure into nested documents with the appropriate fields. - * - * After building a hierarchy within / below a top-level document, it then returns that top-level parent. - * - * If we've received a string, treat it like valid JSON and try to parse it into an object. If this fails, the - * string is invalid JSON, so we should assume that the input is the result of a JSON.parse() - * call that returned a regular string value to be stored as a Field. - * - * If we've received something other than a string, since the caller might also pass in the results of a - * JSON.parse() call, valid input might be an object, an array (still typeof object), a boolean or a number. - * Anything else (like a function, etc. passed in naively as any) is meaningless for this operation. - * - * All TS/JS objects get converted directly to documents, directly preserving the key value structure. Everything else, - * lacking the key value structure, gets stored as a field in a wrapper document. - * - * @param data for convenience and flexibility, either a valid JSON string to be parsed, - * or the result of any JSON.parse() call. - * @param title an optional title to give to the highest parent document in the hierarchy. - * If whether this function creates a new document or appendToExisting is specified and that document already has a title, - * because this title field can be left undefined for the opposite behavior, including a title will overwrite the existing title. - * @param appendToExisting **if specified**, there are two cases, both of which return the target document: - * - * 1) the json to be converted can be represented as a document, in which case the target document will act as the root - * of the tree and receive all the conversion results as new fields on itself - * 2) the json can't be represented as a document, in which case the function will assign the field-level conversion - * results to either the specified key on the target document, or to its "json" key by default. - * - * If not specified, the function creates and returns a new entirely generic document (different from the Doc.Create calls) - * to act as the root of the tree. - * - * One might choose to specify this field if you want to write to a document returned from a Document.Create function call, - * say a TreeView document that will be rendered, not just an untyped, identityless doc that would otherwise be created - * from a default call to new Doc. - * - * @param excludeEmptyObjects whether non-primitive objects (TypeScript objects and arrays) should be converted even - * if they contain no data. By default, empty objects and arrays are ignored. - */ - export function FromJson({ data, title, appendToExisting, excludeEmptyObjects }: JsonConversionOpts): Opt<Doc> { - if (excludeEmptyObjects === undefined) { - excludeEmptyObjects = true; - } - if (data === undefined || data === null || ![...primitives, "object"].includes(typeof data)) { - return undefined; - } - let resolved: any; - try { - resolved = JSON.parse(typeof data === "string" ? data : JSON.stringify(data)); - } catch (e) { - return undefined; - } - let output: Opt<Doc>; - if (typeof resolved === "object" && !(resolved instanceof Array)) { - output = convertObject(resolved, excludeEmptyObjects, title, appendToExisting?.targetDoc); - } else { - const result = toField(resolved, excludeEmptyObjects); - if (appendToExisting) { - (output = appendToExisting.targetDoc)[appendToExisting.fieldKey || defaultKey] = result; - } else { - (output = new Doc).json = result; - } - } - title && output && (output.title = title); - return output; - } - - /** - * For each value of the object, recursively convert it to its appropriate field value - * and store the field at the appropriate key in the document if it is not undefined - * @param object the object to convert - * @returns the object mapped from JSON to field values, where each mapping - * might involve arbitrary recursion (since toField might itself call convertObject) - */ - const convertObject = (object: any, excludeEmptyObjects: boolean, title?: string, target?: Doc): Opt<Doc> => { - const hasEntries = Object.keys(object).length; - if (hasEntries || !excludeEmptyObjects) { - const resolved = target ?? new Doc; - if (hasEntries) { - let result: Opt<Field>; - Object.keys(object).map(key => { - // if excludeEmptyObjects is true, any qualifying conversions from toField will - // be undefined, and thus the results that would have - // otherwise been empty (List or Doc)s will just not be written - if (result = toField(object[key], excludeEmptyObjects, key)) { - resolved[key] = result; - } - }); - } - title && (resolved.title = title); - return resolved; - } - }; - - /** - * For each element in the list, recursively convert it to a document or other field - * and push the field to the list if it is not undefined - * @param list the list to convert - * @returns the list mapped from JSON to field values, where each mapping - * might involve arbitrary recursion (since toField might itself call convertList) - */ - const convertList = (list: Array<any>, excludeEmptyObjects: boolean): Opt<List<Field>> => { - const target = new List(); - let result: Opt<Field>; - // if excludeEmptyObjects is true, any qualifying conversions from toField will - // be undefined, and thus the results that would have - // otherwise been empty (List or Doc)s will just not be written - list.map(item => (result = toField(item, excludeEmptyObjects)) && target.push(result)); - if (target.length || !excludeEmptyObjects) { - return target; - } - }; - - const toField = (data: any, excludeEmptyObjects: boolean, title?: string): Opt<Field> => { - if (data === null || data === undefined) { - return undefined; - } - if (primitives.includes(typeof data)) { - return data; - } - if (typeof data === "object") { - return data instanceof Array ? convertList(data, excludeEmptyObjects) : convertObject(data, excludeEmptyObjects, title, undefined); - } - throw new Error(`How did ${data} of type ${typeof data} end up in JSON?`); - }; - - export function DocumentFromField(target: Doc, fieldKey: string, proto?: Doc, options?: DocumentOptions): Doc | undefined { - let created: Doc | undefined; - let layout: ((fieldKey: string) => string) | undefined; - const field = target[fieldKey]; - const resolved = options || {}; - if (field instanceof ImageField) { - created = Docs.Create.ImageDocument((field).url.href, resolved); - layout = ImageBox.LayoutString; - } else if (field instanceof Doc) { - created = field; - } else if (field instanceof VideoField) { - created = Docs.Create.VideoDocument((field).url.href, resolved); - layout = VideoBox.LayoutString; - } else if (field instanceof PdfField) { - created = Docs.Create.PdfDocument((field).url.href, resolved); - layout = PDFBox.LayoutString; - } else if (field instanceof AudioField) { - created = Docs.Create.AudioDocument((field).url.href, resolved); - layout = AudioBox.LayoutString; - } else if (field instanceof InkField) { - const { selectedColor, selectedWidth, selectedTool, selectedBezier } = InkingControl.Instance; - created = Docs.Create.InkDocument(selectedColor, selectedTool, selectedWidth, selectedBezier, (field).inkData, resolved); - layout = InkingStroke.LayoutString; - } else if (field instanceof List && field[0] instanceof Doc) { - created = Docs.Create.StackingDocument(DocListCast(field), resolved); - layout = CollectionView.LayoutString; - } else { - created = Docs.Create.TextDocument("", { ...{ _width: 200, _height: 25, _autoHeight: true }, ...resolved }); - layout = FormattedTextBox.LayoutString; - } - if (created) { - created.layout = layout?.(fieldKey); - created.title = fieldKey; - proto && created.proto && (created.proto = Doc.GetProto(proto)); - } - return created; - } - - export async function DocumentFromType(type: string, path: string, options: DocumentOptions): Promise<Opt<Doc>> { - let ctor: ((path: string, options: DocumentOptions) => (Doc | Promise<Doc | undefined>)) | undefined = undefined; - if (type.indexOf("image") !== -1) { - ctor = Docs.Create.ImageDocument; - if (!options._width) options._width = 300; - } - if (type.indexOf("video") !== -1) { - ctor = Docs.Create.VideoDocument; - if (!options._width) options._width = 600; - if (!options._height) options._height = options._width * 2 / 3; - } - if (type.indexOf("audio") !== -1) { - ctor = Docs.Create.AudioDocument; - } - if (type.indexOf("pdf") !== -1) { - ctor = Docs.Create.PdfDocument; - if (!options._width) options._width = 400; - if (!options._height) options._height = options._width * 1200 / 927; - } - if (type.indexOf("html") !== -1) { - if (path.includes(window.location.hostname)) { - const s = path.split('/'); - const id = s[s.length - 1]; - return DocServer.GetRefField(id).then(field => { - if (field instanceof Doc) { - const alias = Doc.MakeAlias(field); - alias.x = options.x || 0; - alias.y = options.y || 0; - alias._width = options._width || 300; - alias._height = options._height || options._width || 300; - return alias; - } - return undefined; - }); - } - ctor = Docs.Create.WebDocument; - options = { ...options, _nativeWidth: 850, _nativeHeight: 962, _width: 500, _height: 566, title: path, }; - } - return ctor ? ctor(path, options) : undefined; - } - } } export namespace DocUtils { @@ -1049,6 +834,155 @@ export namespace DocUtils { return linkDoc; } + + export function DocumentFromField(target: Doc, fieldKey: string, proto?: Doc, options?: DocumentOptions): Doc | undefined { + let created: Doc | undefined; + let layout: ((fieldKey: string) => string) | undefined; + const field = target[fieldKey]; + const resolved = options || {}; + if (field instanceof ImageField) { + created = Docs.Create.ImageDocument((field).url.href, resolved); + layout = ImageBox.LayoutString; + } else if (field instanceof Doc) { + created = field; + } else if (field instanceof VideoField) { + created = Docs.Create.VideoDocument((field).url.href, resolved); + layout = VideoBox.LayoutString; + } else if (field instanceof PdfField) { + created = Docs.Create.PdfDocument((field).url.href, resolved); + layout = PDFBox.LayoutString; + } else if (field instanceof AudioField) { + created = Docs.Create.AudioDocument((field).url.href, resolved); + layout = AudioBox.LayoutString; + } else if (field instanceof InkField) { + created = Docs.Create.InkDocument(InkingStroke.InkColor, Doc.selectedTool, InkingStroke.InkWidth, InkingStroke.InkBezierApprox, (field).inkData, resolved); + layout = InkingStroke.LayoutString; + } else if (field instanceof List && field[0] instanceof Doc) { + created = Docs.Create.StackingDocument(DocListCast(field), resolved); + layout = CollectionView.LayoutString; + } else { + created = Docs.Create.TextDocument("", { ...{ _width: 200, _height: 25, _autoHeight: true }, ...resolved }); + layout = FormattedTextBox.LayoutString; + } + if (created) { + created.layout = layout?.(fieldKey); + created.title = fieldKey; + proto && created.proto && (created.proto = Doc.GetProto(proto)); + } + return created; + } + + export async function DocumentFromType(type: string, path: string, options: DocumentOptions): Promise<Opt<Doc>> { + let ctor: ((path: string, options: DocumentOptions) => (Doc | Promise<Doc | undefined>)) | undefined = undefined; + if (type.indexOf("image") !== -1) { + ctor = Docs.Create.ImageDocument; + if (!options._width) options._width = 300; + } + if (type.indexOf("video") !== -1) { + ctor = Docs.Create.VideoDocument; + if (!options._width) options._width = 600; + if (!options._height) options._height = options._width * 2 / 3; + } + if (type.indexOf("audio") !== -1) { + ctor = Docs.Create.AudioDocument; + } + if (type.indexOf("pdf") !== -1) { + ctor = Docs.Create.PdfDocument; + if (!options._width) options._width = 400; + if (!options._height) options._height = options._width * 1200 / 927; + } + if (type.indexOf("html") !== -1) { + if (path.includes(window.location.hostname)) { + const s = path.split('/'); + const id = s[s.length - 1]; + return DocServer.GetRefField(id).then(field => { + if (field instanceof Doc) { + const alias = Doc.MakeAlias(field); + alias.x = options.x || 0; + alias.y = options.y || 0; + alias._width = options._width || 300; + alias._height = options._height || options._width || 300; + return alias; + } + return undefined; + }); + } + ctor = Docs.Create.WebDocument; + options = { ...options, _nativeWidth: 850, _nativeHeight: 962, _width: 500, _height: 566, title: path, }; + } + return ctor ? ctor(path, options) : undefined; + } + + export function makeClone(doc: Doc, cloneMap: Map<string, Doc>, rtfs: { copy: Doc, key: string, field: RichTextField }[]): Doc { + if (Doc.IsBaseProto(doc)) return doc; + if (cloneMap.get(doc[Id])) return cloneMap.get(doc[Id])!; + const copy = new Doc(undefined, true); + cloneMap.set(doc[Id], copy); + if (LinkManager.Instance.getAllLinks().includes(doc) && LinkManager.Instance.getAllLinks().indexOf(copy) === -1) LinkManager.Instance.addLink(copy); + const exclude = Cast(doc.excludeFields, listSpec("string"), []); + Object.keys(doc).forEach(key => { + if (exclude.includes(key)) return; + const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key])); + const field = ProxyField.WithoutProxy(() => doc[key]); + const copyObjectField = (field: ObjectField) => { + const list = Cast(doc[key], listSpec(Doc)); + if (list !== undefined && !(list instanceof Promise)) { + copy[key] = new List<Doc>(list.filter(d => d instanceof Doc).map(d => DocUtils.makeClone(d as Doc, cloneMap, rtfs))); + } else if (doc[key] instanceof Doc) { + copy[key] = key.includes("layout[") ? undefined : DocUtils.makeClone(doc[key] as Doc, cloneMap, rtfs); // reference documents except copy documents that are expanded teplate fields + } else { + copy[key] = ObjectField.MakeCopy(field); + if (field instanceof RichTextField) { + if (field.Data.includes('"docid":') || field.Data.includes('"targetId":') || field.Data.includes('"linkId":')) { + rtfs.push({ copy, key, field }); + } + } + } + }; + if (key === "proto") { + if (doc[key] instanceof Doc) { + copy[key] = DocUtils.makeClone(doc[key]!, cloneMap, rtfs); + } + } else { + if (field instanceof RefField) { + copy[key] = field; + } else if (cfield instanceof ComputedField) { + copy[key] = ComputedField.MakeFunction(cfield.script.originalScript); + (key === "links" && field instanceof ObjectField) && copyObjectField(field); + } else if (field instanceof ObjectField) { + copyObjectField(field); + } else if (field instanceof Promise) { + debugger; //This shouldn't happend... + } else { + copy[key] = field; + } + } + }); + Doc.SetInPlace(copy, "title", "CLONE: " + doc.title, true); + copy.cloneOf = doc; + cloneMap.set(doc[Id], copy); + return copy; + } + export function MakeClone(doc: Doc): Doc { + const cloneMap = new Map<string, Doc>(); + const rtfMap: { copy: Doc, key: string, field: RichTextField }[] = []; + const copy = DocUtils.makeClone(doc, cloneMap, rtfMap); + rtfMap.map(({ copy, key, field }) => { + const replacer = (match: any, attr: string, id: string, offset: any, string: any) => { + const mapped = cloneMap.get(id); + return attr + "\"" + (mapped ? mapped[Id] : id) + "\""; + }; + const replacer2 = (match: any, href: string, id: string, offset: any, string: any) => { + const mapped = cloneMap.get(id); + return href + (mapped ? mapped[Id] : id); + }; + const regex = `(${Utils.prepend("/doc/")})([^"]*)`; + const re = new RegExp(regex, "g"); + copy[key] = new RichTextField(field.Data.replace(/("docid":|"targetId":|"linkId":)"([^"]+)"/g, replacer).replace(re, replacer2), field.Text); + }); + return copy; + } + export function addDocumentCreatorMenuItems(docTextAdder: (d: Doc) => void, docAdder: (d: Doc) => void, x: number, y: number): void { ContextMenu.Instance.addItem({ description: "Add Note ...", @@ -1083,6 +1017,119 @@ export namespace DocUtils { })) as ContextMenuProps[], icon: "eye" }); + }// applies a custom template to a document. the template is identified by it's short name (e.g, slideView not layout_slideView) + export function makeCustomViewClicked(doc: Doc, creator: Opt<(documents: Array<Doc>, options: DocumentOptions, id?: string) => Doc>, templateSignature: string = "custom", docLayoutTemplate?: Doc) { + const batch = UndoManager.StartBatch("makeCustomViewClicked"); + runInAction(() => { + doc.layoutKey = "layout_" + templateSignature; + if (doc[doc.layoutKey] === undefined) { + createCustomView(doc, creator, templateSignature, docLayoutTemplate); + } + }); + batch.end(); + } + export function findTemplate(templateName: string, type: string, signature: string) { + let docLayoutTemplate: Opt<Doc>; + const iconViews = DocListCast(Cast(Doc.UserDoc()["template-icons"], Doc, null)?.data); + const templBtns = DocListCast(Cast(Doc.UserDoc()["template-buttons"], Doc, null)?.data); + const noteTypes = DocListCast(Cast(Doc.UserDoc()["template-notes"], Doc, null)?.data); + const clickFuncs = DocListCast(Cast(Doc.UserDoc().clickFuncs, Doc, null)?.data); + const allTemplates = iconViews.concat(templBtns).concat(noteTypes).concat(clickFuncs).map(btnDoc => (btnDoc.dragFactory as Doc) || btnDoc).filter(doc => doc.isTemplateDoc); + // bcz: this is hacky -- want to have different templates be applied depending on the "type" of a document. but type is not reliable and there could be other types of template searches so this should be generalized + // first try to find a template that matches the specific document type (<typeName>_<templateName>). otherwise, fallback to a general match on <templateName> + !docLayoutTemplate && allTemplates.forEach(tempDoc => StrCast(tempDoc.title) === templateName + "_" + type && (docLayoutTemplate = tempDoc)); + !docLayoutTemplate && allTemplates.forEach(tempDoc => StrCast(tempDoc.title) === templateName && (docLayoutTemplate = tempDoc)); + return docLayoutTemplate; + } + export function createCustomView(doc: Doc, creator: Opt<(documents: Array<Doc>, options: DocumentOptions, id?: string) => Doc>, templateSignature: string = "custom", docLayoutTemplate?: Doc) { + const templateName = templateSignature.replace(/\(.*\)/, ""); + docLayoutTemplate = docLayoutTemplate || findTemplate(templateName, StrCast(doc.type), templateSignature); + + const customName = "layout_" + templateSignature; + const _width = NumCast(doc._width); + const _height = NumCast(doc._height); + const options = { title: "data", backgroundColor: StrCast(doc.backgroundColor), _autoHeight: true, _width, x: -_width / 2, y: - _height / 2, _showSidebar: false }; + + let fieldTemplate: Opt<Doc>; + if (doc.data instanceof RichTextField || typeof (doc.data) === "string") { + fieldTemplate = Docs.Create.TextDocument("", options); + } else if (doc.data instanceof PdfField) { + fieldTemplate = Docs.Create.PdfDocument("http://www.msn.com", options); + } else if (doc.data instanceof VideoField) { + fieldTemplate = Docs.Create.VideoDocument("http://www.cs.brown.edu", options); + } else if (doc.data instanceof AudioField) { + fieldTemplate = Docs.Create.AudioDocument("http://www.cs.brown.edu", options); + } else if (doc.data instanceof ImageField) { + fieldTemplate = Docs.Create.ImageDocument("http://www.cs.brown.edu", options); + } + const docTemplate = docLayoutTemplate || creator?.(fieldTemplate ? [fieldTemplate] : [], { title: customName + "(" + doc.title + ")", isTemplateDoc: true, _width: _width + 20, _height: Math.max(100, _height + 45) }); + + fieldTemplate && Doc.MakeMetadataFieldTemplate(fieldTemplate, docTemplate ? Doc.GetProto(docTemplate) : docTemplate); + docTemplate && Doc.ApplyTemplateTo(docTemplate, doc, customName, undefined); + } + export function makeCustomView(doc: Doc, custom: boolean, layout: string) { + Doc.setNativeView(doc); + if (custom) { + makeCustomViewClicked(doc, Docs.Create.StackingDocument, layout, undefined); + } + } + export function iconify(doc: Doc) { + const layoutKey = Cast(doc.layoutKey, "string", null); + DocUtils.makeCustomViewClicked(doc, Docs.Create.StackingDocument, "icon", undefined); + if (layoutKey && layoutKey !== "layout" && layoutKey !== "layout_icon") doc.deiconifyLayout = layoutKey.replace("layout_", ""); + } + + export function pileup(docList: Doc[], x?: number, y?: number) { + let w = 0, h = 0; + runInAction(() => { + docList.forEach(d => { + DocUtils.iconify(d); + w = Math.max(d[WidthSym](), w); + h = Math.max(d[HeightSym](), h); + }); + h = Math.max(h, w * 4 / 3); // converting to an icon does not update the height right away. so this is a fallback hack to try to do something reasonable + docList.forEach((d, i) => { + d.x = Math.cos(Math.PI * 2 * i / docList.length) * 10 - w / 2; + d.y = Math.sin(Math.PI * 2 * i / docList.length) * 10 - h / 2; + d.displayTimecode = undefined; // bcz: this should be automatic somehow.. along with any other properties that were logically associated with the original collection + }); + }); + if (x !== undefined && y !== undefined) { + const newCollection = Docs.Create.PileDocument(docList, { title: "pileup", x: x - 55, y: y - 55, _width: 110, _height: 100, _LODdisable: true }); + newCollection.x = NumCast(newCollection.x) + NumCast(newCollection._width) / 2 - 55; + newCollection.y = NumCast(newCollection.y) + NumCast(newCollection._height) / 2 - 55; + newCollection._width = newCollection._height = 110; + //newCollection.borderRounding = "40px"; + newCollection._jitterRotation = 10; + newCollection._backgroundColor = "gray"; + newCollection._overflow = "visible"; + return newCollection; + } + } + + + export async function addFieldEnumerations(doc: Opt<Doc>, enumeratedFieldKey: string, enumerations: { title: string, _backgroundColor?: string, color?: string }[]) { + let optionsCollection = await DocServer.GetRefField(enumeratedFieldKey); + if (!(optionsCollection instanceof Doc)) { + optionsCollection = Docs.Create.StackingDocument([], { title: `${enumeratedFieldKey} field set` }, enumeratedFieldKey); + Doc.AddDocToList((Doc.UserDoc().fieldTypes as Doc), "data", optionsCollection as Doc); + } + const options = optionsCollection as Doc; + const targetDoc = doc && Doc.GetProto(Cast(doc.rootDocument, Doc, null) || doc); + const docFind = `options.data.find(doc => doc.title === (this.rootDocument||this)["${enumeratedFieldKey}"])?`; + targetDoc && (targetDoc.backgroundColor = ComputedField.MakeFunction(docFind + `._backgroundColor || "white"`, undefined, { options })); + targetDoc && (targetDoc.color = ComputedField.MakeFunction(docFind + `.color || "black"`, undefined, { options })); + targetDoc && (targetDoc.borderRounding = ComputedField.MakeFunction(docFind + `.borderRounding`, undefined, { options })); + enumerations.map(enumeration => { + const found = DocListCast(options.data).find(d => d.title === enumeration.title); + if (found) { + found._backgroundColor = enumeration._backgroundColor || found._backgroundColor; + found._color = enumeration.color || found._color; + } else { + Doc.AddDocToList(options, "data", Docs.Create.TextDocument(enumeration.title, enumeration)); + } + }); + return optionsCollection; } } diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 496099557..d2614a898 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -2,7 +2,7 @@ import { computed, observable, reaction } from "mobx"; import * as rp from 'request-promise'; import { Utils } from "../../Utils"; import { DocServer } from "../DocServer"; -import { Docs, DocumentOptions } from "../documents/Documents"; +import { Docs, DocumentOptions, DocUtils } from "../documents/Documents"; import { UndoManager } from "./UndoManager"; import { Doc, DocListCast, DocListCastAsync } from "../../fields/Doc"; import { List } from "../../fields/List"; @@ -221,7 +221,7 @@ export class CurrentUserUtils { ]; if (doc.fieldTypes === undefined) { doc.fieldTypes = Docs.Create.TreeDocument([], { title: "field enumerations" }); - Doc.addFieldEnumerations(Doc.GetProto(doc["template-note-Todo"] as any as Doc), "taskStatus", taskStatusValues); + DocUtils.addFieldEnumerations(Doc.GetProto(doc["template-note-Todo"] as any as Doc), "taskStatus", taskStatusValues); } if (doc["template-notes"] === undefined) { @@ -612,7 +612,7 @@ export class CurrentUserUtils { static setupDockedButtons(doc: Doc) { if (doc["dockedBtn-pen"] === undefined) { doc["dockedBtn-pen"] = CurrentUserUtils.ficon({ - onClick: ScriptField.MakeScript("activatePen(this.activePen.inkPen = sameDocs(this.activePen.inkPen, this) ? undefined : this, this.inkWidth, this.backgroundColor)"), + onClick: ScriptField.MakeScript("activatePen(this.activePen.inkPen = sameDocs(this.activePen.inkPen, this) ? undefined : this)"), author: "systemTemplates", title: "ink mode", icon: "pen-nib", ischecked: ComputedField.MakeFunction(`sameDocs(this.activePen.inkPen, this)`), activePen: doc }); } @@ -692,6 +692,8 @@ export class CurrentUserUtils { doc.title = Doc.CurrentUserEmail; doc.activePen = doc; doc.inkColor = StrCast(doc.backgroundColor, "rgb(0, 0, 0)"); + doc.inkWidth = StrCast(doc.inkWidth, "1"); + doc.inkBezier = StrCast(doc.inkBezier, ""); doc.fontSize = NumCast(doc.fontSize, 12); doc["constants-snapThreshold"] = NumCast(doc["constants-snapThreshold"], 10); // doc["constants-dragThreshold"] = NumCast(doc["constants-dragThreshold"], 4); // diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index 2a9c1633a..e7a14f9c5 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -210,7 +210,7 @@ export namespace DragManager { e.docDragData && (e.docDragData.droppedDocuments = dragData.draggedDocuments.map(d => !dragData.isSelectionMove && !dragData.userDropAction && ScriptCast(d.onDragStart) ? addAudioTag(ScriptCast(d.onDragStart).script.run({ this: d }).result) : dragData.userDropAction === "alias" || (!dragData.userDropAction && dragData.dropAction === "alias") ? Doc.MakeAlias(d) : - dragData.userDropAction === "copy" || (!dragData.userDropAction && dragData.dropAction === "copy") ? Doc.MakeClone(d) : d) + dragData.userDropAction === "copy" || (!dragData.userDropAction && dragData.dropAction === "copy") ? Doc.MakeDelegate(d) : d) ); e.docDragData?.droppedDocuments.forEach((drop: Doc, i: number) => (dragData?.removeDropProperties || []).concat(Cast(dragData.draggedDocuments[i].removeDropProperties, listSpec("string"), [])).map(prop => drop[prop] = undefined) diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx index 1e8f07049..4e5cde558 100644 --- a/src/client/util/Import & Export/DirectoryImportBox.tsx +++ b/src/client/util/Import & Export/DirectoryImportBox.tsx @@ -123,10 +123,10 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps> } const { accessPaths, exifData } = result; const path = Utils.prepend(accessPaths.agnostic.client); - const document = await Docs.Get.DocumentFromType(type, path, { _width: 300, title: name }); + const document = await Doc.Get.DocumentFromType(type, path, { _width: 300, title: name }); const { data, error } = exifData; if (document) { - Doc.GetProto(document).exif = error || Docs.Get.FromJson({ data }); + Doc.GetProto(document).exif = error || Doc.Get.FromJson({ data }); docs.push(document); } })); diff --git a/src/client/util/Import & Export/ImageUtils.ts b/src/client/util/Import & Export/ImageUtils.ts index 072e5f58a..0d12b39b8 100644 --- a/src/client/util/Import & Export/ImageUtils.ts +++ b/src/client/util/Import & Export/ImageUtils.ts @@ -20,7 +20,7 @@ export namespace ImageUtils { nativeHeight, exifData: { error, data } } = await Networking.PostToServer("/inspectImage", { source }); - document.exif = error || Docs.Get.FromJson({ data }); + document.exif = error || Doc.Get.FromJson({ data }); const proto = Doc.GetProto(document); proto["data-nativeWidth"] = nativeWidth; proto["data-nativeHeight"] = nativeHeight; diff --git a/src/client/util/InteractionUtils.tsx b/src/client/util/InteractionUtils.tsx index ab1ccb25a..3948611f0 100644 --- a/src/client/util/InteractionUtils.tsx +++ b/src/client/util/InteractionUtils.tsx @@ -1,7 +1,6 @@ import React = require("react"); import * as beziercurve from 'bezier-curve'; import * as fitCurve from 'fit-curve'; -import InkOptionsMenu from "../views/collections/collectionFreeForm/InkOptionsMenu"; export namespace InteractionUtils { export const MOUSETYPE = "mouse"; @@ -90,24 +89,12 @@ export namespace InteractionUtils { return myTouches; } - export function CreatePolyline(points: { X: number, Y: number }[], left: number, top: number, color: string, width: string, bezier: string) { + export function CreatePolyline(points: { X: number, Y: number }[], left: number, top: number, color: string, width: string, bezier: string, scalex: number, scaley: number, shape: string) { var pts = ""; - var shape = ""; - if (InkOptionsMenu.Instance._circle) { - shape = "circle"; - } else if (InkOptionsMenu.Instance._rectangle) { - shape = "rectangle"; - } else if (InkOptionsMenu.Instance._triangle) { - shape = "triangle"; - } else if (InkOptionsMenu.Instance._arrow) { - shape = "arrow"; - } else if (InkOptionsMenu.Instance._line) { - shape = "line"; - } - if (shape !== "") { + if (shape) { //if any of the shape are true const shapePts = makePolygon(shape, points); - pts = shapePts.reduce((acc: string, pt: { X: number, Y: number }) => acc + `${pt.X - left},${pt.Y - top} `, ""); + pts = shapePts.reduce((acc: string, pt: { X: number, Y: number }) => acc + `${pt.X * scalex - left * scalex},${pt.Y * scaley - top * scaley} `, ""); } else if (points.length > 1 && points[points.length - 1].X === points[0].X && points[points.length - 1].Y === points[0].Y) { //pointer is up (first and last points are the same) @@ -124,10 +111,10 @@ export namespace InteractionUtils { newPts.push({ X: point[0], Y: point[1] }); } } - pts = newPts.reduce((acc: string, pt: { X: number, Y: number }) => acc + `${pt.X - left},${pt.Y - top} `, ""); + pts = newPts.reduce((acc: string, pt: { X: number, Y: number }) => acc + `${pt.X * scalex - left * scalex},${pt.Y * scaley - top * scaley} `, ""); } else { //in the middle of drawing - pts = points.reduce((acc: string, pt: { X: number, Y: number }) => acc + `${pt.X - left},${pt.Y - top} `, ""); + pts = points.reduce((acc: string, pt: { X: number, Y: number }) => acc + `${pt.X * scalex - left * scalex},${pt.Y * scaley - top * scaley} `, ""); } return ( <polyline diff --git a/src/client/views/AntimodeMenu.tsx b/src/client/views/AntimodeMenu.tsx index f810361c6..9a6121d20 100644 --- a/src/client/views/AntimodeMenu.tsx +++ b/src/client/views/AntimodeMenu.tsx @@ -1,5 +1,4 @@ import React = require("react"); -import { observer } from "mobx-react"; import { observable, action } from "mobx"; import "./AntimodeMenu.scss"; diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx index 1ba9fcc32..c398b2633 100644 --- a/src/client/views/DocComponent.tsx +++ b/src/client/views/DocComponent.tsx @@ -2,8 +2,6 @@ import { Doc, Opt, DataSym, DocListCast } from '../../fields/Doc'; import { Touchable } from './Touchable'; import { computed, action, observable } from 'mobx'; import { Cast, BoolCast, ScriptCast } from '../../fields/Types'; -import { listSpec } from '../../fields/Schema'; -import { InkingControl } from './InkingControl'; import { InkTool } from '../../fields/InkField'; import { InteractionUtils } from '../util/InteractionUtils'; import { List } from '../../fields/List'; @@ -59,7 +57,7 @@ export function ViewBoxBaseComponent<P extends ViewBoxBaseProps, T>(schemaCtor: lookupField = (field: string) => ScriptCast(this.layoutDoc.lookupField)?.script.run({ self: this.layoutDoc, data: this.rootDoc, field: field, container: this.props.ContainingCollectionDoc }).result; - active = (outsideReaction?: boolean) => !this.props.Document.isBackground && (this.props.rootSelected(outsideReaction) || this.props.isSelected(outsideReaction) || this.props.renderDepth === 0 || this.layoutDoc.forceActive);// && !InkingControl.Instance.selectedTool; // bcz: inking state shouldn't affect static tools + active = (outsideReaction?: boolean) => !this.props.Document.isBackground && (this.props.rootSelected(outsideReaction) || this.props.isSelected(outsideReaction) || this.props.renderDepth === 0 || this.layoutDoc.forceActive);// && !Doc.selectedTool; // bcz: inking state shouldn't affect static tools protected multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer; } return Component; @@ -148,9 +146,9 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T } whenActiveChanged = action((isActive: boolean) => this.props.whenActiveChanged(this._isChildActive = isActive)); - active = (outsideReaction?: boolean) => ((InkingControl.Instance.selectedTool === InkTool.None && !this.props.Document.isBackground) && + active = (outsideReaction?: boolean) => ((Doc.selectedTool === InkTool.None && !this.props.Document.isBackground) && (this.props.rootSelected(outsideReaction) || this.props.isSelected(outsideReaction) || this._isChildActive || this.props.renderDepth === 0 || BoolCast((this.layoutDoc as any).forceActive)) ? true : false) - annotationsActive = (outsideReaction?: boolean) => (InkingControl.Instance.selectedTool !== InkTool.None || (this.props.Document.isBackground && this.props.active()) || + annotationsActive = (outsideReaction?: boolean) => (Doc.selectedTool !== InkTool.None || (this.props.Document.isBackground && this.props.active()) || (this.props.Document.forceActive || this.props.isSelected(outsideReaction) || this._isChildActive || this.props.renderDepth === 0) ? true : false) } return Component; diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx index 5714970c1..4ea75d7d7 100644 --- a/src/client/views/GestureOverlay.tsx +++ b/src/client/views/GestureOverlay.tsx @@ -1,38 +1,30 @@ import React = require("react"); -import { Touchable } from "./Touchable"; +import { action, computed, observable, runInAction } from "mobx"; import { observer } from "mobx-react"; -import "./GestureOverlay.scss"; -import { computed, observable, action, runInAction, IReactionDisposer, reaction, flow, trace } from "mobx"; +import { Doc } from "../../fields/Doc"; +import { InkData, InkTool } from "../../fields/InkField"; +import { Cast, FieldValue, NumCast } from "../../fields/Types"; +import MobileInkOverlay from "../../mobile/MobileInkOverlay"; +import MobileInterface from "../../mobile/MobileInterface"; import { GestureUtils } from "../../pen-gestures/GestureUtils"; +import { MobileInkOverlayContent } from "../../server/Message"; +import { emptyFunction, emptyPath, returnEmptyString, returnFalse, returnOne, returnTrue, returnZero } from "../../Utils"; +import { CognitiveServices } from "../cognitive_services/CognitiveServices"; +import { DocServer } from "../DocServer"; +import { DocUtils } from "../documents/Documents"; +import { CurrentUserUtils } from "../util/CurrentUserUtils"; import { InteractionUtils } from "../util/InteractionUtils"; -import { InkingControl } from "./InkingControl"; -import { InkTool, InkData } from "../../fields/InkField"; -import { Doc } from "../../fields/Doc"; import { LinkManager } from "../util/LinkManager"; -import { DocUtils, Docs } from "../documents/Documents"; -import { undoBatch } from "../util/UndoManager"; import { Scripting } from "../util/Scripting"; -import { FieldValue, Cast, NumCast, BoolCast } from "../../fields/Types"; -import { CurrentUserUtils } from "../util/CurrentUserUtils"; -import HorizontalPalette from "./Palette"; -import { Utils, emptyPath, emptyFunction, returnFalse, returnOne, returnEmptyString, returnTrue, numberRange, returnZero } from "../../Utils"; -import { DocumentView } from "./nodes/DocumentView"; import { Transform } from "../util/Transform"; -import { DocumentContentsView } from "./nodes/DocumentContentsView"; -import { CognitiveServices } from "../cognitive_services/CognitiveServices"; -import { DocServer } from "../DocServer"; -import htmlToImage from "html-to-image"; -import { ScriptField } from "../../fields/ScriptField"; -import { listSpec } from "../../fields/Schema"; -import { List } from "../../fields/List"; -import { CollectionViewType } from "./collections/CollectionView"; -import TouchScrollableMenu, { TouchScrollableMenuItem } from "./TouchScrollableMenu"; -import MobileInterface from "../../mobile/MobileInterface"; -import { MobileInkOverlayContent } from "../../server/Message"; -import MobileInkOverlay from "../../mobile/MobileInkOverlay"; +import "./GestureOverlay.scss"; +import { InkingControl } from "./InkingControl"; +import { DocumentView } from "./nodes/DocumentView"; import { RadialMenu } from "./nodes/RadialMenu"; -import { SelectionManager } from "../util/SelectionManager"; -import InkOptionsMenu from "./collections/collectionFreeForm/InkOptionsMenu"; +import HorizontalPalette from "./Palette"; +import { Touchable } from "./Touchable"; +import TouchScrollableMenu, { TouchScrollableMenuItem } from "./TouchScrollableMenu"; +import { InkingStroke } from "./InkingStroke"; @observer @@ -500,7 +492,7 @@ export default class GestureOverlay extends Touchable { @action onPointerDown = (e: React.PointerEvent) => { - if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || (InkingControl.Instance.selectedTool === InkTool.Highlighter || InkingControl.Instance.selectedTool === InkTool.Pen)) { + if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || (Doc.selectedTool === InkTool.Highlighter || Doc.selectedTool === InkTool.Pen)) { this._points.push({ X: e.clientX, Y: e.clientY }); e.stopPropagation(); e.preventDefault(); @@ -514,7 +506,7 @@ export default class GestureOverlay extends Touchable { @action onPointerMove = (e: PointerEvent) => { - if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || (InkingControl.Instance.selectedTool === InkTool.Highlighter || InkingControl.Instance.selectedTool === InkTool.Pen)) { + if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || (Doc.selectedTool === InkTool.Highlighter || Doc.selectedTool === InkTool.Pen)) { this._points.push({ X: e.clientX, Y: e.clientY }); e.stopPropagation(); e.preventDefault(); @@ -585,12 +577,11 @@ export default class GestureOverlay extends Touchable { //push first points to so interactionUtil knows pointer is up this._points.push({ X: this._points[0].X, Y: this._points[0].Y }); if (MobileInterface.Instance && MobileInterface.Instance.drawingInk) { - const { selectedColor, selectedWidth } = InkingControl.Instance; DocServer.Mobile.dispatchGesturePoints({ points: this._points, bounds: B, - color: selectedColor, - width: selectedWidth + color: InkingStroke.InkColor, + width: InkingStroke.InkWidth }); } @@ -633,21 +624,11 @@ export default class GestureOverlay extends Touchable { } } //if any of the shape is activated in the InkOptionsMenu - else if (InkOptionsMenu.Instance._circle || InkOptionsMenu.Instance._triangle || InkOptionsMenu.Instance._rectangle || InkOptionsMenu.Instance._line || InkOptionsMenu.Instance._arrow) { - if (InkOptionsMenu.Instance._circle) { - this.makePolygon("circle", false); - } else if (InkOptionsMenu.Instance._triangle) { - this.makePolygon("triangle", false); - } else if (InkOptionsMenu.Instance._rectangle) { - this.makePolygon("rectangle", false); - } else if (InkOptionsMenu.Instance._line) { - this.makePolygon("line", false); - } else if (InkOptionsMenu.Instance._arrow) { - this.makePolygon("arrow", false); - } + else if (InkingStroke.InkShape) { + this.makePolygon(InkingStroke.InkShape, false); this.dispatchGesture(GestureUtils.Gestures.Stroke); this._points = []; - InkOptionsMenu.Instance.allFalse(); + InkingStroke.InkShape = ""; } // if we're not drawing in a toolglass try to recognize as gesture else { @@ -827,11 +808,11 @@ export default class GestureOverlay extends Touchable { [this._strokes.map(l => { const b = this.getBounds(l); return <svg key={b.left} width={b.width} height={b.height} style={{ transform: `translate(${b.left}px, ${b.top}px)`, pointerEvents: "none", position: "absolute", zIndex: 30000, overflow: "visible" }}> - {InteractionUtils.CreatePolyline(l, b.left, b.top, InkingControl.Instance.selectedColor, InkingControl.Instance.selectedWidth, InkingControl.Instance.selectedBezier)} + {InteractionUtils.CreatePolyline(l, b.left, b.top, InkingStroke.InkColor, InkingStroke.InkWidth, InkingStroke.InkBezierApprox, 1, 1, InkingStroke.InkShape)} </svg>; }), this._points.length <= 1 ? (null) : <svg width={B.width} height={B.height} style={{ transform: `translate(${B.left}px, ${B.top}px)`, pointerEvents: "none", position: "absolute", zIndex: 30000, overflow: "visible" }}> - {InteractionUtils.CreatePolyline(this._points, B.left, B.top, InkingControl.Instance.selectedColor, InkingControl.Instance.selectedWidth, InkingControl.Instance.selectedBezier)} + {InteractionUtils.CreatePolyline(this._points, B.left, B.top, InkingStroke.InkColor, InkingStroke.InkWidth, InkingStroke.InkBezierApprox, 1, 1, InkingStroke.InkShape)} </svg>] ]; } @@ -921,15 +902,15 @@ Scripting.addGlobal(function setToolglass(tool: any) { }); Scripting.addGlobal(function setPen(width: any, color: any) { runInAction(() => { - GestureOverlay.Instance.SavedColor = InkingControl.Instance.selectedColor; - InkingControl.Instance.updateSelectedColor(color); - GestureOverlay.Instance.SavedWidth = InkingControl.Instance.selectedWidth; + GestureOverlay.Instance.SavedColor = InkingStroke.InkColor; + InkingControl.Instance.switchColor(color); + GestureOverlay.Instance.SavedWidth = InkingStroke.InkWidth; InkingControl.Instance.switchWidth(width); }); }); Scripting.addGlobal(function resetPen() { runInAction(() => { - InkingControl.Instance.updateSelectedColor(GestureOverlay.Instance.SavedColor ?? "rgb(0, 0, 0)"); + InkingControl.Instance.switchColor(GestureOverlay.Instance.SavedColor ?? "rgb(0, 0, 0)"); InkingControl.Instance.switchWidth(GestureOverlay.Instance.SavedWidth ?? "2"); }); }); diff --git a/src/client/views/InkingControl.tsx b/src/client/views/InkingControl.tsx index 83109db1c..349bc6ffc 100644 --- a/src/client/views/InkingControl.tsx +++ b/src/client/views/InkingControl.tsx @@ -1,22 +1,12 @@ -import { action, computed, observable } from "mobx"; -import { ColorState } from 'react-color'; +import { action, observable } from "mobx"; import { Doc } from "../../fields/Doc"; import { InkTool } from "../../fields/InkField"; -import { FieldValue, NumCast, StrCast } from "../../fields/Types"; import { CurrentUserUtils } from "../util/CurrentUserUtils"; import { Scripting } from "../util/Scripting"; -import { SelectionManager } from "../util/SelectionManager"; -import { undoBatch } from "../util/UndoManager"; -import GestureOverlay from "./GestureOverlay"; -import { FormattedTextBox } from "./nodes/formattedText/FormattedTextBox"; -import InkOptionsMenu from "./collections/collectionFreeForm/InkOptionsMenu"; +import { InkingStroke } from "./InkingStroke"; export class InkingControl { @observable static Instance: InkingControl; - @computed private get _selectedTool(): InkTool { return FieldValue(NumCast(Doc.UserDoc().inkTool)) ?? InkTool.None; } - @computed private get _selectedColor(): string { return CurrentUserUtils.ActivePen ? FieldValue(StrCast(CurrentUserUtils.ActivePen.backgroundColor)) ?? "rgb(0, 0, 0)" : "rgb(0, 0, 0)"; } - @computed private get _selectedWidth(): string { return FieldValue(StrCast(Doc.UserDoc().inkWidth)) ?? "2"; } - @computed private get _selectedBezier(): string { return FieldValue(StrCast(Doc.UserDoc().inkBezier)) ?? "2"; } @observable public _open: boolean = false; constructor() { InkingControl.Instance = this; @@ -26,110 +16,27 @@ export class InkingControl { // this._selectedTool = tool; Doc.UserDoc().inkTool = tool; }); - decimalToHexString(number: number) { - if (number < 0) { - number = 0xFFFFFFFF + number + 1; - } - return (number < 16 ? "0" : "") + number.toString(16).toUpperCase(); - } @action - inkOptionsMenuChangeColor = (color: string) => { - const col: ColorState = { - hex: color, hsl: { a: 0, h: 0, s: 0, l: 0, source: "" }, hsv: { a: 0, h: 0, s: 0, v: 0, source: "" }, - rgb: { a: 0, r: 0, b: 0, g: 0, source: "" }, oldHue: 0, source: "", - }; - this.switchColor(col); - InkOptionsMenu.Instance._colorBt = false; - } - - @undoBatch - switchColor = action((color: ColorState): void => { - Doc.UserDoc().backgroundColor = color.hex.startsWith("#") ? - color.hex + (color.rgb.a ? this.decimalToHexString(Math.round(color.rgb.a * 255)) : "ff") : color.hex; - InkOptionsMenu.Instance._color = StrCast(Doc.UserDoc().backgroundColor); - CurrentUserUtils.ActivePen && (CurrentUserUtils.ActivePen.backgroundColor = color.hex); - - if (InkingControl.Instance.selectedTool === InkTool.None) { - const selected = SelectionManager.SelectedDocuments(); - selected.map(view => { - const targetDoc = view.props.Document.dragFactory instanceof Doc ? view.props.Document.dragFactory : - view.props.Document.layout instanceof Doc ? view.props.Document.layout : - view.props.Document.isTemplateForField ? view.props.Document : Doc.GetProto(view.props.Document); - if (targetDoc) { - if (StrCast(Doc.Layout(view.props.Document).layout).indexOf("FormattedTextBox") !== -1 && FormattedTextBox.HadSelection) { - Doc.Layout(view.props.Document).color = Doc.UserDoc().bacgroundColor; - } else { - Doc.Layout(view.props.Document)._backgroundColor = Doc.UserDoc().backgroundColor; // '_backgroundColor' is template specific. 'backgroundColor' would apply to all templates, but has no UI at the moment - } - } - }); - } - }); - @action switchWidth = (width: string): void => { - // this._selectedWidth = width; if (!isNaN(parseInt(width))) { - Doc.UserDoc().inkWidth = width; + CurrentUserUtils.ActivePen && (CurrentUserUtils.ActivePen.inkWidth = InkingStroke.InkWidth = width); } - InkOptionsMenu.Instance._widthBt = false; } @action switchBezier = (bezier: string): void => { - if (!isNaN(parseInt(bezier))) { - Doc.UserDoc().inkBezier = bezier; - } + CurrentUserUtils.ActivePen && (CurrentUserUtils.ActivePen.inkBezier = InkingStroke.InkBezierApprox = isNaN(parseInt(bezier)) ? "" : bezier); } @action - inkOptionsMenuChangeBezier = (e: React.PointerEvent): void => { - if (InkOptionsMenu.Instance._bezierBt === true) { - Doc.UserDoc().inkBezier = "300"; - } else { - Doc.UserDoc().inkBezier = "0"; - } - } - - @computed - get selectedTool() { - return this._selectedTool; - } - - @computed - get selectedColor() { - return this._selectedColor; - } - - @action - updateSelectedColor(value: string) { - // this._selectedColor = value; - Doc.UserDoc().inkColor = value; - } - - @computed - get selectedWidth() { - return this._selectedWidth; - } - - @computed - get selectedBezier() { - return this._selectedBezier; + switchColor(value: string) { + CurrentUserUtils.ActivePen && (CurrentUserUtils.ActivePen.inkColor = InkingStroke.InkColor = value); } } -Scripting.addGlobal(function activatePen(pen: any, width: any, color: any) { - InkingControl.Instance.switchTool(pen ? InkTool.Pen : InkTool.None); InkingControl.Instance.switchWidth(width); InkingControl.Instance.updateSelectedColor(color); - //setup InkOptionsMenu(change jumpto value if necessary.Currenlty hardcoded to 300,300) - pen ? InkOptionsMenu.Instance.jumpTo(300, 300) : InkOptionsMenu.Instance.fadeOut(true); - InkOptionsMenu.Instance.changeColor = InkingControl.Instance.inkOptionsMenuChangeColor; - InkOptionsMenu.Instance.changeBezier = InkingControl.Instance.inkOptionsMenuChangeBezier; - InkOptionsMenu.Instance.changeWidth = InkingControl.Instance.switchWidth; - InkOptionsMenu.Instance._widthSelected = width; - InkOptionsMenu.Instance._color = color; -}); -Scripting.addGlobal(function activateBrush(pen: any, width: any, color: any) { InkingControl.Instance.switchTool(pen ? InkTool.Highlighter : InkTool.None); InkingControl.Instance.switchWidth(width); InkingControl.Instance.updateSelectedColor(color); }); +Scripting.addGlobal(function activateBrush(pen: any, width: any, color: any) { InkingControl.Instance.switchTool(pen ? InkTool.Highlighter : InkTool.None); InkingControl.Instance.switchWidth(width); InkingControl.Instance.switchColor(color); }); Scripting.addGlobal(function activateEraser(pen: any) { return InkingControl.Instance.switchTool(pen ? InkTool.Eraser : InkTool.None); }); Scripting.addGlobal(function activateStamp(pen: any) { return InkingControl.Instance.switchTool(pen ? InkTool.Stamp : InkTool.None); }); Scripting.addGlobal(function deactivateInk() { return InkingControl.Instance.switchTool(InkTool.None); }); Scripting.addGlobal(function setInkWidth(width: any) { return InkingControl.Instance.switchWidth(width); }); -Scripting.addGlobal(function setInkColor(color: any) { return InkingControl.Instance.updateSelectedColor(color); });
\ No newline at end of file +Scripting.addGlobal(function setInkColor(color: any) { return InkingControl.Instance.switchColor(color); });
\ No newline at end of file diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx index 3dc0a5b20..78d729eee 100644 --- a/src/client/views/InkingStroke.tsx +++ b/src/client/views/InkingStroke.tsx @@ -1,19 +1,19 @@ +import { library } from "@fortawesome/fontawesome-svg-core"; +import { faPaintBrush } from "@fortawesome/free-solid-svg-icons"; +import { observable, runInAction } from "mobx"; import { observer } from "mobx-react"; import { documentSchema } from "../../fields/documentSchemas"; import { InkData, InkField, InkTool } from "../../fields/InkField"; import { makeInterface } from "../../fields/Schema"; -import { Cast, StrCast, NumCast } from "../../fields/Types"; +import { Cast, StrCast } from "../../fields/Types"; +import { TraceMobx } from "../../fields/util"; +import { CognitiveServices } from "../cognitive_services/CognitiveServices"; +import { InteractionUtils } from "../util/InteractionUtils"; +import { ContextMenu } from "./ContextMenu"; import { ViewBoxBaseComponent } from "./DocComponent"; -import { InkingControl } from "./InkingControl"; import "./InkingStroke.scss"; import { FieldView, FieldViewProps } from "./nodes/FieldView"; import React = require("react"); -import { TraceMobx } from "../../fields/util"; -import { InteractionUtils } from "../util/InteractionUtils"; -import { ContextMenu } from "./ContextMenu"; -import { CognitiveServices } from "../cognitive_services/CognitiveServices"; -import { faPaintBrush } from "@fortawesome/free-solid-svg-icons"; -import { library } from "@fortawesome/fontawesome-svg-core"; library.add(faPaintBrush); @@ -23,6 +23,22 @@ const InkDocument = makeInterface(documentSchema); @observer export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocument>(InkDocument) { public static LayoutString(fieldStr: string) { return FieldView.LayoutString(InkingStroke, fieldStr); } + @observable public static InkColor: string; + @observable public static InkWidth: string; + @observable public static InkBezierApprox: string; + @observable public static InkShape: string; + + constructor(props: any) { + super(props); + if (InkingStroke.InkBezierApprox === undefined) { + runInAction(() => { + InkingStroke.InkBezierApprox = ""; + InkingStroke.InkWidth = "1"; + InkingStroke.InkColor = "black"; + InkingStroke.InkShape = ""; + }); + } + } private analyzeStrokes = () => { const data: InkData = Cast(this.dataDoc[this.fieldKey], InkField)?.inkData ?? []; @@ -38,22 +54,19 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume const top = Math.min(...ys); const right = Math.max(...xs); const bottom = Math.max(...ys); - const points = InteractionUtils.CreatePolyline(data, left, top, - StrCast(this.layoutDoc.color, InkingControl.Instance.selectedColor), - StrCast(this.layoutDoc.strokeWidth, InkingControl.Instance.selectedWidth), - StrCast(this.layoutDoc.strokeBezier, InkingControl.Instance.selectedBezier)); const width = right - left; const height = bottom - top; const scaleX = this.props.PanelWidth() / width; const scaleY = this.props.PanelHeight() / height; + const points = InteractionUtils.CreatePolyline(data, left, top, + StrCast(this.layoutDoc.color, InkingStroke.InkColor || "black"), + StrCast(this.layoutDoc.strokeWidth, InkingStroke.InkWidth || "1"), + StrCast(this.layoutDoc.strokeBezier, InkingStroke.InkBezierApprox || ""), scaleX, scaleY, ""); return ( <svg className="inkingStroke" width={width} height={height} - style={{ - transform: `scale(${scaleX}, ${scaleY})`, - mixBlendMode: this.layoutDoc.tool === InkTool.Highlighter ? "multiply" : "unset", - }} + style={{ mixBlendMode: this.layoutDoc.tool === InkTool.Highlighter ? "multiply" : "unset" }} onContextMenu={() => { ContextMenu.Instance.addItem({ description: "Analyze Stroke", diff --git a/src/client/views/PreviewCursor.tsx b/src/client/views/PreviewCursor.tsx index dd65681d4..33150ab7c 100644 --- a/src/client/views/PreviewCursor.tsx +++ b/src/client/views/PreviewCursor.tsx @@ -3,7 +3,7 @@ import { observer } from 'mobx-react'; import "normalize.css"; import * as React from 'react'; import "./PreviewCursor.scss"; -import { Docs } from '../documents/Documents'; +import { Docs, DocUtils } from '../documents/Documents'; import { Doc } from '../../fields/Doc'; import { Transform } from "../util/Transform"; import { DocServer } from '../DocServer'; @@ -65,7 +65,7 @@ export class PreviewCursor extends React.Component<{}> { count++; if (doc instanceof Doc) { i === 1 && (first = doc); - const alias = Doc.MakeClone(doc); + const alias = DocUtils.MakeClone(doc); const deltaX = NumCast(doc.x) - NumCast(first!.x) - ptx; const deltaY = NumCast(doc.y) - NumCast(first!.y) - pty; alias.x = newPoint[0] + deltaX; diff --git a/src/client/views/Touchable.tsx b/src/client/views/Touchable.tsx index 10d023d83..5e48d5ffb 100644 --- a/src/client/views/Touchable.tsx +++ b/src/client/views/Touchable.tsx @@ -1,8 +1,6 @@ import * as React from 'react'; import { action } from 'mobx'; import { InteractionUtils } from '../util/InteractionUtils'; -import { SelectionManager } from '../util/SelectionManager'; -import { RadialMenu } from './nodes/RadialMenu'; const HOLD_DURATION = 1000; diff --git a/src/client/views/collections/CollectionMapView.tsx b/src/client/views/collections/CollectionMapView.tsx index a0b7cd8a8..cfec3a6bc 100644 --- a/src/client/views/collections/CollectionMapView.tsx +++ b/src/client/views/collections/CollectionMapView.tsx @@ -42,7 +42,7 @@ const query = async (data: string | google.maps.LatLngLiteral) => { }; @observer -class CollectionMapView extends CollectionSubView<MapSchema, Partial<IMapProps> & { google: any }>(MapSchema) { +export class CollectionMapView extends CollectionSubView<MapSchema, Partial<IMapProps> & { google: any }>(MapSchema) { private _cancelAddrReq = new Map<string, boolean>(); private _cancelLocReq = new Map<string, boolean>(); diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 423eb1d90..b60419258 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -6,27 +6,19 @@ import { Id } from "../../../fields/FieldSymbols"; import { List } from "../../../fields/List"; import { listSpec } from "../../../fields/Schema"; import { ScriptField } from "../../../fields/ScriptField"; +import { WebField } from "../../../fields/URLField"; import { Cast, ScriptCast, NumCast } from "../../../fields/Types"; import { GestureUtils } from "../../../pen-gestures/GestureUtils"; -import { CurrentUserUtils } from "../../util/CurrentUserUtils"; import { Upload } from "../../../server/SharedMediaTypes"; import { Utils } from "../../../Utils"; -import { GooglePhotos } from "../../apis/google_docs/GooglePhotosClientUtils"; import { DocServer } from "../../DocServer"; -import { Docs, DocumentOptions, DocUtils } from "../../documents/Documents"; -import { DocumentType } from "../../documents/DocumentTypes"; import { Networking } from "../../Network"; -import { DragManager, dropActionType } from "../../util/DragManager"; import { ImageUtils } from "../../util/Import & Export/ImageUtils"; import { InteractionUtils } from "../../util/InteractionUtils"; import { undoBatch, UndoManager } from "../../util/UndoManager"; import { DocComponent } from "../DocComponent"; import { FieldViewProps } from "../nodes/FieldView"; -import { FormattedTextBox, GoogleRef } from "../nodes/formattedText/FormattedTextBox"; -import { CollectionView } from "./CollectionView"; import React = require("react"); -import { SelectionManager } from "../../util/SelectionManager"; -import { WebField } from "../../../fields/URLField"; export interface CollectionViewProps extends FieldViewProps { addDocument: (document: Doc | Doc[]) => boolean; @@ -405,7 +397,7 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: const stringContents = await new Promise<string>(resolve => item.getAsString(resolve)); const type = "html";// (await rp.head(Utils.CorsProxy(stringContents)))["content-type"]; if (type) { - const doc = await Docs.Get.DocumentFromType(type, stringContents, options); + const doc = await DocUtils.DocumentFromType(type, stringContents, options); doc && generatedDocuments.push(doc); } } @@ -435,7 +427,7 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: } const full = { ...options, _width: 400, title: name }; const pathname = Utils.prepend(result.accessPaths.agnostic.client); - const doc = await Docs.Get.DocumentFromType(type, pathname, full); + const doc = await DocUtils.DocumentFromType(type, pathname, full); if (!doc) { continue; } @@ -450,9 +442,9 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: generatedDocuments.push(doc); } if (generatedDocuments.length) { - const set = generatedDocuments.length > 1 && generatedDocuments.map(d => Doc.iconify(d)); + const set = generatedDocuments.length > 1 && generatedDocuments.map(d => DocUtils.iconify(d)); if (set) { - addDocument(Doc.pileup(generatedDocuments, options.x!, options.y!)!); + addDocument(DocUtils.pileup(generatedDocuments, options.x!, options.y!)!); } else { generatedDocuments.forEach(addDocument); } @@ -469,3 +461,10 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: return CollectionSubView; } +import { DragManager, dropActionType } from "../../util/DragManager"; +import { Docs, DocumentOptions, DocUtils } from "../../documents/Documents"; +import { CurrentUserUtils } from "../../util/CurrentUserUtils"; +import { DocumentType } from "../../documents/DocumentTypes"; +import { FormattedTextBox, GoogleRef } from "../nodes/formattedText/FormattedTextBox"; +import { CollectionView } from "./CollectionView"; +import { SelectionManager } from "../../util/SelectionManager"; diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index b2e1c0f73..e891c4274 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -757,7 +757,7 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll const existingOnClick = ContextMenu.Instance.findByDescription("OnClick..."); const onClicks: ContextMenuProps[] = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : []; onClicks.push({ - description: "Edit onChecked Script", event: () => UndoManager.RunInBatch(() => Doc.makeCustomViewClicked(this.props.Document, undefined, "onCheckedClick"), "edit onCheckedClick"), icon: "edit" + description: "Edit onChecked Script", event: () => UndoManager.RunInBatch(() => DocUtils.makeCustomViewClicked(this.props.Document, undefined, "onCheckedClick"), "edit onCheckedClick"), icon: "edit" }); !existingOnClick && ContextMenu.Instance.addItem({ description: "OnClick...", subitems: onClicks, icon: "hand-point-right" }); } diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index fecba32c5..873f61331 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -14,36 +14,16 @@ import { BoolCast, Cast, NumCast, StrCast, ScriptCast } from '../../../fields/Ty import { ImageField } from '../../../fields/URLField'; import { TraceMobx } from '../../../fields/util'; import { Utils, setupMoveUpEvents, returnFalse, returnZero, emptyPath, emptyFunction, returnOne } from '../../../Utils'; -import { DocumentType } from '../../documents/DocumentTypes'; import { ImageUtils } from '../../util/Import & Export/ImageUtils'; import { ContextMenu } from "../ContextMenu"; import { FieldView, FieldViewProps } from '../nodes/FieldView'; import { ScriptBox } from '../ScriptBox'; import { Touchable } from '../Touchable'; -import { CollectionCarouselView } from './CollectionCarouselView'; -import { CollectionDockingView } from "./CollectionDockingView"; -import { AddCustomFreeFormLayout } from './collectionFreeForm/CollectionFreeFormLayoutEngines'; -import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormView'; -import { CollectionLinearView } from './CollectionLinearView'; -import { CollectionMulticolumnView } from './collectionMulticolumn/CollectionMulticolumnView'; -import { CollectionMultirowView } from './collectionMulticolumn/CollectionMultirowView'; -import { CollectionSchemaView } from "./CollectionSchemaView"; -import { CollectionStackingView } from './CollectionStackingView'; -import { CollectionStaffView } from './CollectionStaffView'; -import { SubCollectionViewProps } from './CollectionSubView'; -import { CollectionTimeView } from './CollectionTimeView'; -import { CollectionTreeView } from "./CollectionTreeView"; -import './CollectionView.scss'; -import { CollectionViewBaseChrome } from './CollectionViewChromes'; -import { CurrentUserUtils } from '../../util/CurrentUserUtils'; import { Id } from '../../../fields/FieldSymbols'; import { listSpec } from '../../../fields/Schema'; -import { Docs } from '../../documents/Documents'; import { ScriptField, ComputedField } from '../../../fields/ScriptField'; import { InteractionUtils } from '../../util/InteractionUtils'; import { ObjectField } from '../../../fields/ObjectField'; -import CollectionMapView from './CollectionMapView'; -import { CollectionPileView } from './CollectionPileView'; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; @@ -519,4 +499,24 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus {this.facetWidth() < 10 ? (null) : this.filterView} </div>); } -}
\ No newline at end of file +} +import { SubCollectionViewProps } from './CollectionSubView'; +import { CollectionCarouselView } from './CollectionCarouselView'; +import { CollectionDockingView } from "./CollectionDockingView"; +import { AddCustomFreeFormLayout } from './collectionFreeForm/CollectionFreeFormLayoutEngines'; +import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormView'; +import { CollectionLinearView } from './CollectionLinearView'; +import { CollectionMulticolumnView } from './collectionMulticolumn/CollectionMulticolumnView'; +import { CollectionMultirowView } from './collectionMulticolumn/CollectionMultirowView'; +import { CollectionSchemaView } from "./CollectionSchemaView"; +import { CollectionStackingView } from './CollectionStackingView'; +import { CollectionStaffView } from './CollectionStaffView'; +import { CollectionTimeView } from './CollectionTimeView'; +import { CollectionTreeView } from "./CollectionTreeView"; +import { CollectionMapView } from './CollectionMapView'; +import { CollectionPileView } from './CollectionPileView'; +import './CollectionView.scss'; +import { CollectionViewBaseChrome } from './CollectionViewChromes'; +import { CurrentUserUtils } from '../../util/CurrentUserUtils'; +import { DocumentType } from '../../documents/DocumentTypes'; +import { Docs } from '../../documents/Documents';
\ No newline at end of file diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index fb7784b58..576c0c560 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -29,7 +29,6 @@ import { undoBatch, UndoManager } from "../../../util/UndoManager"; import { COLLECTION_BORDER_WIDTH } from "../../../views/globalCssVariables.scss"; import { ContextMenu } from "../../ContextMenu"; import { ContextMenuProps } from "../../ContextMenuItem"; -import { InkingControl } from "../../InkingControl"; import { CollectionFreeFormDocumentView } from "../../nodes/CollectionFreeFormDocumentView"; import { DocumentViewProps, DocumentView } from "../../nodes/DocumentView"; import { FormattedTextBox } from "../../nodes/formattedText/FormattedTextBox"; @@ -46,7 +45,7 @@ import React = require("react"); import { CollectionViewType } from "../CollectionView"; import { Timeline } from "../../animationtimeline/Timeline"; import { SnappingManager } from "../../../util/SnappingManager"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { InkingStroke } from "../../InkingStroke"; library.add(faEye as any, faTable, faPaintBrush, faExpandArrowsAlt, faCompressArrowsAlt, faCompass, faUpload, faBraille, faChalkboard, faFileUpload); @@ -391,7 +390,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P @action onPointerDown = (e: React.PointerEvent): void => { - if (e.nativeEvent.cancelBubble || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE) || InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || (InkingControl.Instance.selectedTool === InkTool.Highlighter || InkingControl.Instance.selectedTool === InkTool.Pen)) { + if (e.nativeEvent.cancelBubble || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE) || InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || (Doc.selectedTool === InkTool.Highlighter || Doc.selectedTool === InkTool.Pen)) { return; } this._hitCluster = this.props.Document.useClusters ? this.pickCluster(this.getTransform().transformPoint(e.clientX, e.clientY)) !== -1 : false; @@ -408,7 +407,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P document.addEventListener("pointermove", this.onPointerMove); document.addEventListener("pointerup", this.onPointerUp); // if not using a pen and in no ink mode - if (InkingControl.Instance.selectedTool === InkTool.None) { + if (Doc.selectedTool === InkTool.None) { this._downX = this._lastX = e.pageX; this._downY = this._lastY = e.pageY; } @@ -432,13 +431,13 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P this.addMoveListeners(); this.removeEndListeners(); this.addEndListeners(); - // if (InkingControl.Instance.selectedTool === InkTool.Highlighter || InkingControl.Instance.selectedTool === InkTool.Pen) { + // if (Doc.selectedTool === InkTool.Highlighter || Doc.selectedTool === InkTool.Pen) { // e.stopPropagation(); // e.preventDefault(); // const point = this.getTransform().transformPoint(pt.pageX, pt.pageY); // this._points.push({ X: point[0], Y: point[1] }); // } - if (InkingControl.Instance.selectedTool === InkTool.None) { + if (Doc.selectedTool === InkTool.None) { this._lastX = pt.pageX; this._lastY = pt.pageY; e.preventDefault(); @@ -458,7 +457,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P case GestureUtils.Gestures.Stroke: const points = ge.points; const B = this.getTransform().transformBounds(ge.bounds.left, ge.bounds.top, ge.bounds.width, ge.bounds.height); - const inkDoc = Docs.Create.InkDocument(InkingControl.Instance.selectedColor, InkingControl.Instance.selectedTool, InkingControl.Instance.selectedWidth, InkingControl.Instance.selectedBezier, points, { title: "ink stroke", x: B.x, y: B.y, _width: B.width, _height: B.height }); + const inkDoc = Docs.Create.InkDocument(InkingStroke.InkColor, Doc.selectedTool, InkingStroke.InkWidth, InkingStroke.InkBezierApprox, points, { title: "ink stroke", x: B.x, y: B.y, _width: B.width, _height: B.height }); this.addDocument(inkDoc); e.stopPropagation(); break; @@ -619,7 +618,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P return; } if (!e.cancelBubble) { - const selectedTool = InkingControl.Instance.selectedTool; + const selectedTool = Doc.selectedTool; if (selectedTool === InkTool.None) { if (this._hitCluster && this.tryDragCluster(e)) { e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers @@ -641,7 +640,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true); const pt = myTouches[0]; if (pt) { - if (InkingControl.Instance.selectedTool === InkTool.None) { + if (Doc.selectedTool === InkTool.None) { if (this._hitCluster && this.tryDragCluster(e)) { e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers e.preventDefault(); diff --git a/src/client/views/collections/collectionFreeForm/InkOptionsMenu.tsx b/src/client/views/collections/collectionFreeForm/InkOptionsMenu.tsx index 44488cbcf..b35c9682e 100644 --- a/src/client/views/collections/collectionFreeForm/InkOptionsMenu.tsx +++ b/src/client/views/collections/collectionFreeForm/InkOptionsMenu.tsx @@ -1,269 +1,117 @@ import React = require("react"); import AntimodeMenu from "../../AntimodeMenu"; import { observer } from "mobx-react"; -import { unimplementedFunction } from "../../../../Utils"; import { observable, action } from "mobx"; import "./InkOptionsMenu.scss"; - +import { InkingStroke } from "../../InkingStroke"; +import { Scripting } from "../../../util/Scripting"; +import { InkTool } from "../../../../fields/InkField"; +import { InkingControl } from "../../InkingControl"; +import { StrCast } from "../../../../fields/Types"; +import { ColorState } from "react-color"; +import { ColorBox } from "../../nodes/ColorBox"; @observer export default class InkOptionsMenu extends AntimodeMenu { static Instance: InkOptionsMenu; - public changeColor: (color: string) => void = unimplementedFunction; - public changeBezier: (e: React.PointerEvent) => void = unimplementedFunction; - public changeWidth: (color: string) => void = unimplementedFunction; - - private _palette: (string)[]; - private _width: (string)[]; - - - public _circle: boolean; - public _triangle: boolean; - public _rectangle: boolean; - public _arrow: boolean; - public _line: boolean; - public _widthSelected: string; - - @observable public _circleBt: boolean; - @observable public _triangleBt: boolean; - @observable public _rectangleBt: boolean; - @observable public _arrowBt: boolean; - @observable public _lineBt: boolean; - @observable public _colorBt: boolean; - @observable public _color: string; - @observable public _bezierBt: boolean; - @observable public _widthBt: boolean; + private _palette = ["D0021B", "F5A623", "F8E71C", "8B572A", "7ED321", "417505", "9013FE", "4A90E2", "50E3C2", "B8E986", "000000", "4A4A4A", "9B9B9B", "FFFFFF"]; + private _width = ["1", "5", "10", "100", "200", "300"]; + private _buttons = ["circle", "triangle", "rectangle", "arrow", "line"]; + private _icons = ["O", "∆", "ãƒ", "âžœ", "-"]; + @observable _colorBtn = false; + @observable _widthBtn = false; constructor(props: Readonly<{}>) { super(props); InkOptionsMenu.Instance = this; - this._canFade = false; - - this._circle = false; - this._triangle = false; - this._rectangle = false; - this._arrow = false; - this._line = false; - this._circleBt = false; - this._triangleBt = false; - this._rectangleBt = false; - this._arrowBt = false; - this._lineBt = false; - this._colorBt = false; - this._bezierBt = false; - this._widthBt = false; - - this._color = ""; - this._widthSelected = ""; - - - this._palette = [ - "D0021B", "F5A623", "F8E71C", "8B572A", "7ED321", "417505", "9013FE", "4A90E2", "50E3C2", "B8E986", "000000", "4A4A4A", "9B9B9B", "FFFFFF", - ]; - - this._width = [ - "1", "5", "10", "100", "200", "300" - ]; - - } - - - - drag = (e: React.PointerEvent) => { - this.dragStart(e); + this._canFade = false; // don't let the inking menu fade away } - - - - - @action - toggleCircle = (e: React.PointerEvent) => { - const curr = this._circle; - this.allFalse(); - curr ? this._circle = false : this._circle = true; - this._circleBt = this._circle; - } - @action - toggleTriangle = (e: React.PointerEvent) => { - const curr = this._triangle; - this.allFalse(); - curr ? this._triangle = false : this._triangle = true; - this._triangleBt = this._triangle; - } - @action - toggleRectangle = (e: React.PointerEvent) => { - const curr = this._rectangle; - this.allFalse(); - curr ? this._rectangle = false : this._rectangle = true; - this._rectangleBt = this._rectangle; - } - @action - toggleArrow = (e: React.PointerEvent) => { - const curr = this._arrow; - this.allFalse(); - curr ? this._arrow = false : this._arrow = true; - this._arrowBt = this._arrow; - } @action - toggleLine = (e: React.PointerEvent) => { - const curr = this._line; - this.allFalse(); - curr ? this._line = false : this._line = true; - this._lineBt = this._line; + changeColor = (color: string) => { + const col: ColorState = { + hex: color, hsl: { a: 0, h: 0, s: 0, l: 0, source: "" }, hsv: { a: 0, h: 0, s: 0, v: 0, source: "" }, + rgb: { a: 0, r: 0, b: 0, g: 0, source: "" }, oldHue: 0, source: "", + }; + ColorBox.switchColor(col); } @action - changeBezierClick = (e: React.PointerEvent) => { - const curr = this._bezierBt; - this.allFalse(); - curr ? this._bezierBt = false : this._bezierBt = true; - this.changeBezier(e); - } - - @action - changeWidthClick = (e: React.PointerEvent) => { - this._widthBt ? this._widthBt = false : this._widthBt = true; - } - @action - changeColorClick = (e: React.PointerEvent) => { - this._colorBt ? this._colorBt = false : this._colorBt = true; - } - - allFalse = () => { - this._circle = false; - this._triangle = false; - this._rectangle = false; - this._arrow = false; - this._line = false; - this._circleBt = false; - this._triangleBt = false; - this._rectangleBt = false; - this._arrowBt = false; - this._lineBt = false; - this._bezierBt = false; + changeBezier = (e: React.PointerEvent): void => { + InkingControl.Instance.switchBezier(!InkingStroke.InkBezierApprox ? "300" : ""); } render() { - var widthPicker; - if (this._widthBt) { + var widthPicker = <button + className="antimodeMenu-button" + key="width" + onPointerDown={action(e => this._widthBtn = !this._widthBtn)} + style={{ backgroundColor: this._widthBtn ? "121212" : "" }}> + W + </button>; + if (this._widthBtn) { widthPicker = <div className="btn2-group"> - <button - className="antimodeMenu-button" - key="width" - onPointerDown={this.changeWidthClick} - style={this._widthBt ? { backgroundColor: "121212" } : {}}> - W - </button> + {widthPicker} {this._width.map(wid => { return <button className="antimodeMenu-button" key={wid} - onPointerDown={() => this.changeWidth(wid)} - style={this._colorBt ? { backgroundColor: "121212" } : {}}> + onPointerDown={action(() => { InkingControl.Instance.switchWidth(wid); this._widthBtn = false; })} + style={{ backgroundColor: this._widthBtn ? "121212" : "" }}> {wid} </button>; - })} </div>; - } else { - widthPicker = <button - className="antimodeMenu-button" - key="width" - onPointerDown={this.changeWidthClick} - style={this._widthBt ? { backgroundColor: "121212" } : {}}> - W - </button>; } - var colorPicker; - if (this._colorBt) { + var colorPicker = <button + className="antimodeMenu-button" + key="color" + title="colorChanger" + onPointerDown={action(e => this._colorBtn = !this._colorBtn)} + style={{ backgroundColor: this._colorBtn ? "121212" : "" }}> + <div className="color-preview" style={{ backgroundColor: InkingStroke.InkColor ?? "121212" }}></div> + </button>; + if (this._colorBtn) { colorPicker = <div className="btn-group"> - <button - className="antimodeMenu-button" - key="color" - onPointerDown={this.changeColorClick} - style={this._colorBt ? { backgroundColor: "121212" } : {}}> - <div className="color-preview" style={this._color === "" ? { backgroundColor: "121212" } : { backgroundColor: this._color }}></div> - </button> + {colorPicker} {this._palette.map(color => { return <button className="antimodeMenu-button" key={color} - onPointerDown={() => this.changeColor(color)} - style={this._colorBt ? { backgroundColor: "121212" } : {}}> + onPointerDown={action(() => { this.changeColor(color); this._colorBtn = false; })} + style={{ backgroundColor: this._colorBtn ? "121212" : "" }}> <div className="color-preview" style={{ backgroundColor: color }}></div> </button>; })} </div>; - } else { - colorPicker = <button - className="antimodeMenu-button" - title="colorChanger" - key="color" - onPointerDown={this.changeColorClick} - style={this._colorBt ? { backgroundColor: "121212" } : {}}> - <div className="color-preview" style={this._color === "" ? { backgroundColor: "121212" } : { backgroundColor: this._color }}></div> - </button>; } - const buttons = [ - <button - className="antimodeMenu-button" + <button className="antimodeMenu-button" title="Drag" key="drag" - onPointerDown={this.drag}> + onPointerDown={e => this.dragStart(e)}> ✜ </button>, - <button - className="antimodeMenu-button" - title="Draw Circle" - key="circle" - onPointerDown={this.toggleCircle} - style={this._circleBt ? { backgroundColor: "121212" } : {}}> - O - </button>, - <button - className="antimodeMenu-button" - title="Draw Traingle" - key="triangle" - onPointerDown={this.toggleTriangle} - style={this._triangleBt ? { backgroundColor: "121212" } : {}}> - ∆ - </button>, - <button - className="antimodeMenu-button" - title="Draw Rectangle" - key="rectangle" - onPointerDown={this.toggleRectangle} - style={this._rectangleBt ? { backgroundColor: "121212" } : {}}> - ム- </button>, - <button - className="antimodeMenu-button" - title="Draw Arrow" - key="arrow" - onPointerDown={this.toggleArrow} - style={this._arrowBt ? { backgroundColor: "121212" } : {}}> - âžœ - </button>, - <button - className="antimodeMenu-button" - title="Draw Line" - key="line" - onPointerDown={this.toggleLine} - style={this._lineBt ? { backgroundColor: "121212" } : {}}> - – - </button>, + <> + {this._buttons.map((btn, i) => <button + className="antimodeMenu-button" + title={`Draw ${btn}`} + key={btn} + onPointerDown={action(e => InkingStroke.InkShape = btn)} + style={btn === InkingStroke.InkShape ? { backgroundColor: "121212" } : {}}> + {this._icons[i]} + </button>)}, + </>, <button className="antimodeMenu-button" title="Bezier changer" key="bezier" - onPointerDown={this.changeBezierClick} - style={this._bezierBt ? { backgroundColor: "121212" } : {}}> + onPointerDown={e => this.changeBezier(e)} + style={InkingStroke.InkBezierApprox ? { backgroundColor: "121212" } : {}}> B </button>, widthPicker, @@ -271,4 +119,15 @@ export default class InkOptionsMenu extends AntimodeMenu { ]; return this.getElement(buttons); } -}
\ No newline at end of file +} +Scripting.addGlobal(function activatePen(pen: any) { + InkingControl.Instance.switchTool(pen ? InkTool.Pen : InkTool.None); + if (pen) { + InkingControl.Instance.switchWidth(StrCast(pen.inkWidth, "1")); + InkingControl.Instance.switchColor(StrCast(pen.inkColor, "black")); + InkingControl.Instance.switchBezier(StrCast(pen.inkBezier, "")); + InkOptionsMenu.Instance.jumpTo(300, 300); + } else { + InkOptionsMenu.Instance.fadeOut(true); + } +});
\ No newline at end of file diff --git a/src/client/views/nodes/ColorBox.tsx b/src/client/views/nodes/ColorBox.tsx index 2ddf2c74a..762c57ae9 100644 --- a/src/client/views/nodes/ColorBox.tsx +++ b/src/client/views/nodes/ColorBox.tsx @@ -1,6 +1,6 @@ import React = require("react"); import { observer } from "mobx-react"; -import { SketchPicker } from 'react-color'; +import { SketchPicker, ColorState } from 'react-color'; import { documentSchema } from "../../../fields/documentSchemas"; import { makeInterface } from "../../../fields/Schema"; import { StrCast } from "../../../fields/Types"; @@ -10,6 +10,12 @@ import { ViewBoxBaseComponent } from "../DocComponent"; import { InkingControl } from "../InkingControl"; import "./ColorBox.scss"; import { FieldView, FieldViewProps } from './FieldView'; +import { InkingStroke } from "../InkingStroke"; +import { Doc } from "../../../fields/Doc"; +import { InkTool } from "../../../fields/InkField"; +import { undoBatch } from "../../util/UndoManager"; +import { action } from "mobx"; +import { FormattedTextBox } from "./formattedText/FormattedTextBox"; type ColorDocument = makeInterface<[typeof documentSchema]>; const ColorDocument = makeInterface(documentSchema); @@ -18,20 +24,52 @@ const ColorDocument = makeInterface(documentSchema); export class ColorBox extends ViewBoxBaseComponent<FieldViewProps, ColorDocument>(ColorDocument) { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(ColorBox, fieldKey); } + static decimalToHexString(number: number) { + if (number < 0) { + number = 0xFFFFFFFF + number + 1; + } + return (number < 16 ? "0" : "") + number.toString(16).toUpperCase(); + } + + @undoBatch + @action + static switchColor(color: ColorState) { + Doc.UserDoc().backgroundColor = color.hex.startsWith("#") ? + color.hex + (color.rgb.a ? ColorBox.decimalToHexString(Math.round(color.rgb.a * 255)) : "ff") : color.hex; + InkingStroke.InkColor = StrCast(Doc.UserDoc().backgroundColor); + CurrentUserUtils.ActivePen && (CurrentUserUtils.ActivePen.inkColor = color.hex); + + if (Doc.selectedTool === InkTool.None) { + const selected = SelectionManager.SelectedDocuments(); + selected.map(view => { + const targetDoc = view.props.Document.dragFactory instanceof Doc ? view.props.Document.dragFactory : + view.props.Document.layout instanceof Doc ? view.props.Document.layout : + view.props.Document.isTemplateForField ? view.props.Document : Doc.GetProto(view.props.Document); + if (targetDoc) { + if (StrCast(Doc.Layout(view.props.Document).layout).indexOf("FormattedTextBox") !== -1 && FormattedTextBox.HadSelection) { + Doc.Layout(view.props.Document).color = Doc.UserDoc().bacgroundColor; + } else { + Doc.Layout(view.props.Document)._backgroundColor = Doc.UserDoc().backgroundColor; // '_backgroundColor' is template specific. 'backgroundColor' would apply to all templates, but has no UI at the moment + } + } + }); + } + } + render() { const selDoc = SelectionManager.SelectedDocuments()?.[0]?.rootDoc; return <div className={`colorBox-container${this.active() ? "-interactive" : ""}`} onPointerDown={e => e.button === 0 && !e.ctrlKey && e.stopPropagation()} style={{ transform: `scale(${this.props.ContentScaling()})`, width: `${100 / this.props.ContentScaling()}%`, height: `${100 / this.props.ContentScaling()}%` }} > - <SketchPicker onChange={InkingControl.Instance.switchColor} presetColors={['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B', '#FFFFFF', '#f1efeb', 'transparent']} + <SketchPicker onChange={ColorBox.switchColor} presetColors={['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B', '#FFFFFF', '#f1efeb', 'transparent']} color={StrCast(CurrentUserUtils.ActivePen ? CurrentUserUtils.ActivePen.backgroundColor : undefined, StrCast(selDoc?._backgroundColor, StrCast(selDoc?.backgroundColor, "black")))} /> <div style={{ display: "grid", gridTemplateColumns: "20% 80%", paddingTop: "10px" }}> - <div> {InkingControl.Instance.selectedWidth ?? 2}</div> - <input type="range" value={InkingControl.Instance.selectedWidth ?? 2} defaultValue={2} min={1} max={100} onChange={(e: React.ChangeEvent<HTMLInputElement>) => InkingControl.Instance.switchWidth(e.target.value)} /> - <div> {InkingControl.Instance.selectedBezier ?? 2}</div> - <input type="range" value={InkingControl.Instance.selectedBezier ?? 2} defaultValue={2} min={0} max={300} onChange={(e: React.ChangeEvent<HTMLInputElement>) => InkingControl.Instance.switchBezier(e.target.value)} /> + <div> {InkingStroke.InkWidth ?? 2}</div> + <input type="range" value={InkingStroke.InkWidth ?? 2} defaultValue={2} min={1} max={100} onChange={(e: React.ChangeEvent<HTMLInputElement>) => InkingControl.Instance.switchWidth(e.target.value)} /> + <div> {InkingStroke.InkBezierApprox ?? 2}</div> + <input type="range" value={InkingStroke.InkBezierApprox ?? 2} defaultValue={2} min={0} max={300} onChange={(e: React.ChangeEvent<HTMLInputElement>) => InkingControl.Instance.switchBezier(e.target.value)} /> <br /> <br /> </div> diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index e245e045c..196e61a31 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -37,7 +37,6 @@ import { ContextMenu } from "../ContextMenu"; import { ContextMenuProps } from '../ContextMenuItem'; import { DocComponent } from "../DocComponent"; import { EditableView } from '../EditableView'; -import { InkingControl } from '../InkingControl'; import { KeyphraseQueryView } from '../KeyphraseQueryView'; import { DocumentContentsView } from "./DocumentContentsView"; import "./DocumentView.scss"; @@ -330,7 +329,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } else func(); } else if (this.Document["onClick-rawScript"] && !StrCast(Doc.LayoutField(this.layoutDoc))?.includes("ScriptingBox")) {// bcz: hack? don't edit a script if you're clicking on a scripting box itself const alias = Doc.MakeAlias(this.props.Document); - Doc.makeCustomViewClicked(alias, undefined, "onClick"); + DocUtils.makeCustomViewClicked(alias, undefined, "onClick"); this.props.addDocTab(alias, "onRight"); // UndoManager.RunInBatch(() => Doc.makeCustomViewClicked(this.props.Document, undefined, "onClick"), "edit onClick"); //ScriptBox.EditButtonScript("On Button Clicked ...", this.props.Document, "onClick", e.clientX, e.clientY), "on button click"); @@ -513,7 +512,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu // console.log(e.button) // console.log(e.nativeEvent) // continue if the event hasn't been canceled AND we are using a moues or this is has an onClick or onDragStart function (meaning it is a button document) - if (!(InteractionUtils.IsType(e, InteractionUtils.MOUSETYPE) || InkingControl.Instance.selectedTool === InkTool.Highlighter || InkingControl.Instance.selectedTool === InkTool.Pen)) { + if (!(InteractionUtils.IsType(e, InteractionUtils.MOUSETYPE) || Doc.selectedTool === InkTool.Highlighter || Doc.selectedTool === InkTool.Pen)) { if (!InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) { e.stopPropagation(); // TODO: check here for panning/inking @@ -544,7 +543,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu onPointerMove = (e: PointerEvent): void => { if ((e as any).formattedHandled) { e.stopPropagation(); return; } - if ((InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || InkingControl.Instance.selectedTool === InkTool.Highlighter || InkingControl.Instance.selectedTool === InkTool.Pen)) return; + if ((InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || Doc.selectedTool === InkTool.Highlighter || Doc.selectedTool === InkTool.Pen)) return; if (e.cancelBubble && this.active) { document.removeEventListener("pointermove", this.onPointerMove); // stop listening to pointerMove if something else has stopPropagated it (e.g., the MarqueeView) } @@ -728,7 +727,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu onClicks.push({ description: this.Document.isLinkButton ? "Remove Follow Behavior" : "Follow Link in Place", event: this.toggleFollowInPlace, icon: "concierge-bell" }); onClicks.push({ description: this.Document.isLinkButton ? "Remove Follow Behavior" : "Follow Link on Right", event: this.toggleFollowOnRight, icon: "concierge-bell" }); onClicks.push({ description: this.Document.isLinkButton || this.Document.onClick ? "Remove Click Behavior" : "Follow Link", event: this.toggleLinkButtonBehavior, icon: "concierge-bell" }); - onClicks.push({ description: "Edit onClick Script", event: () => UndoManager.RunInBatch(() => Doc.makeCustomViewClicked(this.props.Document, undefined, "onClick"), "edit onClick"), icon: "edit" }); + onClicks.push({ description: "Edit onClick Script", event: () => UndoManager.RunInBatch(() => DocUtils.makeCustomViewClicked(this.props.Document, undefined, "onClick"), "edit onClick"), icon: "edit" }); !existingOnClick && cm.addItem({ description: "OnClick...", subitems: onClicks, icon: "hand-point-right" }); @@ -1091,14 +1090,14 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu @computed get ignorePointerEvents() { return this.props.pointerEvents === false || (this.Document.isBackground && !this.isSelected() && !SnappingManager.GetIsDragging()) || - (this.Document.type === DocumentType.INK && InkingControl.Instance.selectedTool !== InkTool.None); + (this.Document.type === DocumentType.INK && Doc.selectedTool !== InkTool.None); } @undoBatch @action setCustomView = (custom: boolean, layout: string): void => { Doc.setNativeView(this.props.Document); if (custom) { - Doc.makeCustomViewClicked(this.props.Document, Docs.Create.StackingDocument, layout, undefined); + DocUtils.makeCustomViewClicked(this.props.Document, Docs.Create.StackingDocument, layout, undefined); } } @observable _animateScalingTo = 0; diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 6913dfbc7..cabf30c13 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -192,7 +192,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD extractFaces = () => { const converter = (results: any) => { - return results.map((face: CognitiveServices.Image.Face) => Docs.Get.FromJson({ data: face, title: `Face: ${face.faceId}` })!); + return results.map((face: CognitiveServices.Image.Face) => Doc.Get.FromJson({ data: face, title: `Face: ${face.faceId}` })!); }; this.url && CognitiveServices.Image.Appliers.ProcessImage(this.dataDoc, [this.fieldKey + "-faces"], this.url, Service.Face, converter); } diff --git a/src/client/views/nodes/PresBox.tsx b/src/client/views/nodes/PresBox.tsx index 9f1e99c77..81669dc2a 100644 --- a/src/client/views/nodes/PresBox.tsx +++ b/src/client/views/nodes/PresBox.tsx @@ -11,7 +11,6 @@ import { DocumentManager } from "../../util/DocumentManager"; import { undoBatch } from "../../util/UndoManager"; import { CollectionDockingView } from "../collections/CollectionDockingView"; import { CollectionView, CollectionViewType } from "../collections/CollectionView"; -import { InkingControl } from "../InkingControl"; import { FieldView, FieldViewProps } from './FieldView'; import "./PresBox.scss"; import { ViewBoxBaseComponent } from "../DocComponent"; @@ -20,6 +19,7 @@ import { Docs } from "../../documents/Documents"; import { PrefetchProxy } from "../../../fields/Proxy"; import { ScriptField } from "../../../fields/ScriptField"; import { Scripting } from "../../util/Scripting"; +import { InkingStroke } from "../InkingStroke"; type PresBoxSchema = makeInterface<[typeof documentSchema]>; const PresBoxDocument = makeInterface(documentSchema); @@ -293,7 +293,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> selectElement = (doc: Doc) => this.gotoDocument(this.childDocs.indexOf(doc), NumCast(this.itemIndex)); getTransform = () => this.props.ScreenToLocalTransform().translate(-5, -65);// listBox padding-left and pres-box-cont minHeight panelHeight = () => this.props.PanelHeight() - 20; - active = (outsideReaction?: boolean) => ((InkingControl.Instance.selectedTool === InkTool.None && !this.layoutDoc.isBackground) && + active = (outsideReaction?: boolean) => ((Doc.selectedTool === InkTool.None && !this.layoutDoc.isBackground) && (this.layoutDoc.forceActive || this.props.isSelected(outsideReaction) || this._isChildActive || this.props.renderDepth === 0) ? true : false) render() { diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx index 29e3c008a..1184f32f1 100644 --- a/src/client/views/nodes/ScreenshotBox.tsx +++ b/src/client/views/nodes/ScreenshotBox.tsx @@ -5,21 +5,19 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { action, computed, IReactionDisposer, observable, runInAction } from "mobx"; import { observer } from "mobx-react"; import * as rp from 'request-promise'; +import { Doc } from "../../../fields/Doc"; import { documentSchema } from "../../../fields/documentSchemas"; -import { makeInterface, listSpec } from "../../../fields/Schema"; +import { listSpec, makeInterface } from "../../../fields/Schema"; import { Cast, NumCast } from "../../../fields/Types"; import { VideoField } from "../../../fields/URLField"; -import { emptyFunction, returnFalse, returnOne, Utils, returnZero } from "../../../Utils"; -import { Docs, DocUtils } from "../../documents/Documents"; +import { emptyFunction, returnFalse, returnOne, returnZero, Utils } from "../../../Utils"; +import { Docs } from "../../documents/Documents"; import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView"; import { ContextMenu } from "../ContextMenu"; import { ContextMenuProps } from "../ContextMenuItem"; import { ViewBoxBaseComponent } from "../DocComponent"; -import { InkingControl } from "../InkingControl"; import { FieldView, FieldViewProps } from './FieldView'; import "./ScreenshotBox.scss"; -import { Doc, WidthSym, HeightSym } from "../../../fields/Doc"; -import { OverlayView } from "../OverlayView"; const path = require('path'); type ScreenshotDocument = makeInterface<[typeof documentSchema]>; @@ -134,7 +132,7 @@ export class ScreenshotBox extends ViewBoxBaseComponent<FieldViewProps, Screensh } @computed get content() { - const interactive = InkingControl.Instance.selectedTool || !this.props.isSelected() ? "" : "-interactive"; + const interactive = Doc.selectedTool || !this.props.isSelected() ? "" : "-interactive"; const style = "videoBox-content" + interactive; return <video className={`${style}`} key="video" autoPlay={this._screenCapture} ref={this.setVideoRef} style={{ width: this._screenCapture ? "100%" : undefined, height: this._screenCapture ? "100%" : undefined }} diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index 9d02239fc..8c8b7a622 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -15,7 +15,6 @@ import { ContextMenu } from "../ContextMenu"; import { ContextMenuProps } from "../ContextMenuItem"; import { ViewBoxAnnotatableComponent } from "../DocComponent"; import { DocumentDecorations } from "../DocumentDecorations"; -import { InkingControl } from "../InkingControl"; import { FieldView, FieldViewProps } from './FieldView'; import "./VideoBox.scss"; import { documentSchema } from "../../../fields/documentSchemas"; @@ -229,7 +228,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD @computed get content() { const field = Cast(this.dataDoc[this.fieldKey], VideoField); - const interactive = InkingControl.Instance.selectedTool || !this.props.isSelected() ? "" : "-interactive"; + const interactive = Doc.selectedTool || !this.props.isSelected() ? "" : "-interactive"; const style = "videoBox-content" + (this._fullScreen ? "-fullScreen" : "") + interactive; return !field ? <div>Loading</div> : <video className={`${style}`} key="video" autoPlay={this._screenCapture} ref={this.setVideoRef} @@ -273,8 +272,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD this._reactionDisposer && this._reactionDisposer(); this._youtubeReactionDisposer && this._youtubeReactionDisposer(); this._reactionDisposer = reaction(() => this.layoutDoc.currentTimecode, () => !this._playing && this.Seek((this.layoutDoc.currentTimecode || 0))); - this._youtubeReactionDisposer = reaction(() => [this.props.isSelected(), DocumentDecorations.Instance.Interacting, InkingControl.Instance.selectedTool], () => { - const interactive = InkingControl.Instance.selectedTool === InkTool.None && this.props.isSelected(true) && !DocumentDecorations.Instance.Interacting; + this._youtubeReactionDisposer = reaction(() => [this.props.isSelected(), DocumentDecorations.Instance.Interacting, Doc.selectedTool], () => { + const interactive = Doc.selectedTool === InkTool.None && this.props.isSelected(true) && !DocumentDecorations.Instance.Interacting; iframe.style.pointerEvents = interactive ? "all" : "none"; }, { fireImmediately: true }); }; diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index c4ab3c9e2..dcc87068b 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -15,7 +15,6 @@ import { DragManager } from "../../util/DragManager"; import { ImageUtils } from "../../util/Import & Export/ImageUtils"; import { ViewBoxAnnotatableComponent } from "../DocComponent"; import { DocumentDecorations } from "../DocumentDecorations"; -import { InkingControl } from "../InkingControl"; import { FieldView, FieldViewProps } from './FieldView'; import "./WebBox.scss"; import React = require("react"); @@ -425,7 +424,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum const frozen = !this.props.isSelected() || decInteracting; return (<> - <div className={"webBox-cont" + (this.props.isSelected() && InkingControl.Instance.selectedTool === InkTool.None && !decInteracting ? "-interactive" : "")} + <div className={"webBox-cont" + (this.props.isSelected() && Doc.selectedTool === InkTool.None && !decInteracting ? "-interactive" : "")} onWheel={this.onPostWheel} onPointerDown={this.onPostPointer} onPointerMove={this.onPostPointer} onPointerUp={this.onPostPointer}> {view} </div>; diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index fc131cd38..c0a8bec40 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -52,14 +52,12 @@ import { ContextMenu } from '../../ContextMenu'; import { ContextMenuProps } from '../../ContextMenuItem'; import { ViewBoxAnnotatableComponent } from "../../DocComponent"; import { DocumentButtonBar } from '../../DocumentButtonBar'; -import { InkingControl } from "../../InkingControl"; import { AudioBox } from '../AudioBox'; import { FieldView, FieldViewProps } from "../FieldView"; import "./FormattedTextBox.scss"; import { FormattedTextBoxComment, formattedTextBoxCommentPlugin } from './FormattedTextBoxComment'; import React = require("react"); -import { ScriptField } from '../../../../fields/ScriptField'; -import GoogleAuthenticationManager from '../../../apis/GoogleAuthenticationManager'; +import { InkingStroke } from '../../InkingStroke'; library.add(faEdit); library.add(faSmile, faTextHeight, faUpload); @@ -479,11 +477,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp changeItems.push({ description: StrCast(note.title), event: undoBatch(() => { Doc.setNativeView(this.rootDoc); - Doc.makeCustomViewClicked(this.rootDoc, Docs.Create.TreeDocument, StrCast(note.title), note); + DocUtils.makeCustomViewClicked(this.rootDoc, Docs.Create.TreeDocument, StrCast(note.title), note); }), icon: "eye" }); }); - changeItems.push({ description: "FreeForm", event: undoBatch(() => Doc.makeCustomViewClicked(this.rootDoc, Docs.Create.FreeformDocument, "freeform"), "change view"), icon: "eye" }); + changeItems.push({ description: "FreeForm", event: undoBatch(() => DocUtils.makeCustomViewClicked(this.rootDoc, Docs.Create.FreeformDocument, "freeform"), "change view"), icon: "eye" }); !change && cm.addItem({ description: "Change Perspective...", subitems: changeItems, icon: "external-link-alt" }); } @@ -1213,7 +1211,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp TraceMobx(); const scale = this.props.ContentScaling() * NumCast(this.layoutDoc.scale, 1); const rounded = StrCast(this.layoutDoc.borderRounding) === "100%" ? "-rounded" : ""; - const interactive = InkingControl.Instance.selectedTool || this.layoutDoc.isBackground; + const interactive = Doc.selectedTool || this.layoutDoc.isBackground; if (this.props.isSelected()) { this._editorView && RichTextMenu.Instance.updateFromDash(this._editorView, undefined, this.props); } else if (FormattedTextBoxComment.textBox === this) { @@ -1268,7 +1266,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp </div> {!this.layoutDoc._showSidebar ? (null) : this.sidebarWidthPercent === "0%" ? <div className="formattedTextBox-sidebar-handle" onPointerDown={this.sidebarDown} /> : - <div className={"formattedTextBox-sidebar" + (InkingControl.Instance.selectedTool !== InkTool.None ? "-inking" : "")} + <div className={"formattedTextBox-sidebar" + (Doc.selectedTool !== InkTool.None ? "-inking" : "")} style={{ width: `${this.sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}> <CollectionFreeFormView {...this.props} PanelHeight={this.props.PanelHeight} diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index 5bad248be..c18f69e1d 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -2,7 +2,6 @@ import { action, computed, IReactionDisposer, observable, reaction, runInAction import { observer } from "mobx-react"; import * as Pdfjs from "pdfjs-dist"; import "pdfjs-dist/web/pdf_viewer.css"; -import * as rp from "request-promise"; import { Dictionary } from "typescript-collections"; import { Doc, DocListCast, FieldResult, HeightSym, Opt, WidthSym } from "../../../fields/Doc"; import { documentSchema } from "../../../fields/documentSchemas"; @@ -26,7 +25,6 @@ import { CollectionFreeFormView } from "../collections/collectionFreeForm/Collec import { CollectionView } from "../collections/CollectionView"; import { ViewBoxAnnotatableComponent } from "../DocComponent"; import { DocumentDecorations } from "../DocumentDecorations"; -import { InkingControl } from "../InkingControl"; import Annotation from "./Annotation"; import PDFMenu from "./PDFMenu"; import "./PDFViewer.scss"; @@ -583,7 +581,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu const targetDoc = Docs.Create.TextDocument("", { _width: 200, _height: 200, title: "Note linked to " + this.props.Document.title }); Doc.GetProto(targetDoc).data = new List<Doc>([clipDoc]); clipDoc.rootDocument = targetDoc; - Doc.makeCustomViewClicked(targetDoc, Docs.Create.StackingDocument, "slideView", undefined); + DocUtils.makeCustomViewClicked(targetDoc, Docs.Create.StackingDocument, "slideView", undefined); targetDoc.layoutKey = "layout"; // const targetDoc = Docs.Create.TextDocument("", { _width: 200, _height: 200, title: "Note linked to " + this.props.Document.title }); // Doc.GetProto(targetDoc).snipped = this.dataDoc[this.props.fieldKey][Copy](); @@ -652,7 +650,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu panelWidth = () => (this.Document.scrollHeight || this.Document._nativeHeight || 0); panelHeight = () => this._pageSizes.length && this._pageSizes[0] ? this._pageSizes[0].width : (this.Document._nativeWidth || 0); @computed get overlayLayer() { - return <div className={`pdfViewer-overlay${InkingControl.Instance.selectedTool !== InkTool.None || SnappingManager.GetIsDragging() ? "-inking" : ""}`} id="overlay" + return <div className={`pdfViewer-overlay${Doc.selectedTool !== InkTool.None || SnappingManager.GetIsDragging() ? "-inking" : ""}`} id="overlay" style={{ transform: `scale(${this._zoomed})` }}> <CollectionFreeFormView {...this.props} LibraryPath={this.props.ContainingCollectionView?.props.LibraryPath ?? emptyPath} diff --git a/src/client/views/webcam/DashWebRTCVideo.tsx b/src/client/views/webcam/DashWebRTCVideo.tsx index 2ea011316..0b1e90b1d 100644 --- a/src/client/views/webcam/DashWebRTCVideo.tsx +++ b/src/client/views/webcam/DashWebRTCVideo.tsx @@ -4,10 +4,8 @@ import { CollectionFreeFormDocumentViewProps } from "../nodes/CollectionFreeForm import { FieldViewProps, FieldView } from "../nodes/FieldView"; import { observable, action } from "mobx"; import { DocumentDecorations } from "../DocumentDecorations"; -import { InkingControl } from "../InkingControl"; import "../../views/nodes/WebBox.scss"; import "./DashWebRTCVideo.scss"; -import adapter from 'webrtc-adapter'; import { initialize, hangup, refreshVideos } from "./WebCamLogic"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { IconProp, library } from '@fortawesome/fontawesome-svg-core'; @@ -73,7 +71,7 @@ export class DashWebRTCVideo extends React.Component<CollectionFreeFormDocumentV </div >; const frozen = !this.props.isSelected() || DocumentDecorations.Instance.Interacting; - const classname = "webBox-cont" + (this.props.isSelected() && !InkingControl.Instance.selectedTool && !DocumentDecorations.Instance.Interacting ? "-interactive" : ""); + const classname = "webBox-cont" + (this.props.isSelected() && !Doc.selectedTool && !DocumentDecorations.Instance.Interacting ? "-interactive" : ""); return ( diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index 6891bf652..d635836bb 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -6,20 +6,18 @@ import { DocumentType } from "../client/documents/DocumentTypes"; import { Scripting, scriptingGlobal } from "../client/util/Scripting"; import { afterDocDeserialize, autoObject, Deserializable, SerializationHelper } from "../client/util/SerializationHelper"; import { UndoManager } from "../client/util/UndoManager"; -import { intersectRect, Utils } from "../Utils"; -import { HandleUpdate, Id, OnUpdate, Parent, Self, SelfProxy, ToScriptString, ToString, Update, Copy } from "./FieldSymbols"; +import { intersectRect } from "../Utils"; +import { Copy, HandleUpdate, Id, OnUpdate, Parent, Self, SelfProxy, ToScriptString, ToString, Update } from "./FieldSymbols"; +import { InkTool } from "./InkField"; import { List } from "./List"; import { ObjectField } from "./ObjectField"; import { PrefetchProxy, ProxyField } from "./Proxy"; import { FieldId, RefField } from "./RefField"; import { RichTextField } from "./RichTextField"; import { listSpec } from "./Schema"; -import { ComputedField, ScriptField } from "./ScriptField"; -import { Cast, FieldValue, NumCast, StrCast, ToConstructor, ScriptCast } from "./Types"; +import { ComputedField } from "./ScriptField"; +import { Cast, FieldValue, NumCast, StrCast, ToConstructor } from "./Types"; import { deleteProperty, getField, getter, makeEditable, makeReadOnly, setter, updateFunction } from "./util"; -import { Docs, DocumentOptions } from "../client/documents/Documents"; -import { PdfField, VideoField, AudioField, ImageField } from "./URLField"; -import { LinkManager } from "../client/util/LinkManager"; export namespace Field { export function toKeyValueString(doc: Doc, key: string): string { @@ -134,6 +132,7 @@ function fetchProto(doc: Doc) { @scriptingGlobal @Deserializable("Doc", fetchProto).withFields(["id"]) export class Doc extends RefField { + @computed public static get selectedTool(): InkTool { return FieldValue(NumCast(Doc.UserDoc().inkTool)) ?? InkTool.None; } constructor(id?: FieldId, forceSave?: boolean) { super(id); const doc = new Proxy<this>(this, { @@ -618,76 +617,6 @@ export namespace Doc { return copy; } - export function MakeClone(doc: Doc): Doc { - const cloneMap = new Map<string, Doc>(); - const rtfMap: { copy: Doc, key: string, field: RichTextField }[] = []; - const copy = Doc.makeClone(doc, cloneMap, rtfMap); - rtfMap.map(({ copy, key, field }) => { - const replacer = (match: any, attr: string, id: string, offset: any, string: any) => { - const mapped = cloneMap.get(id); - return attr + "\"" + (mapped ? mapped[Id] : id) + "\""; - }; - const replacer2 = (match: any, href: string, id: string, offset: any, string: any) => { - const mapped = cloneMap.get(id); - return href + (mapped ? mapped[Id] : id); - }; - const regex = `(${Utils.prepend("/doc/")})([^"]*)`; - const re = new RegExp(regex, "g"); - copy[key] = new RichTextField(field.Data.replace(/("docid":|"targetId":|"linkId":)"([^"]+)"/g, replacer).replace(re, replacer2), field.Text); - }); - return copy; - } - - export function makeClone(doc: Doc, cloneMap: Map<string, Doc>, rtfs: { copy: Doc, key: string, field: RichTextField }[]): Doc { - if (Doc.IsBaseProto(doc)) return doc; - if (cloneMap.get(doc[Id])) return cloneMap.get(doc[Id])!; - const copy = new Doc(undefined, true); - cloneMap.set(doc[Id], copy); - if (LinkManager.Instance.getAllLinks().includes(doc) && LinkManager.Instance.getAllLinks().indexOf(copy) === -1) LinkManager.Instance.addLink(copy); - const exclude = Cast(doc.excludeFields, listSpec("string"), []); - Object.keys(doc).forEach(key => { - if (exclude.includes(key)) return; - const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key])); - const field = ProxyField.WithoutProxy(() => doc[key]); - const copyObjectField = (field: ObjectField) => { - const list = Cast(doc[key], listSpec(Doc)); - if (list !== undefined && !(list instanceof Promise)) { - copy[key] = new List<Doc>(list.filter(d => d instanceof Doc).map(d => Doc.makeClone(d as Doc, cloneMap, rtfs))); - } else if (doc[key] instanceof Doc) { - copy[key] = key.includes("layout[") ? undefined : Doc.makeClone(doc[key] as Doc, cloneMap, rtfs); // reference documents except copy documents that are expanded teplate fields - } else { - copy[key] = ObjectField.MakeCopy(field); - if (field instanceof RichTextField) { - if (field.Data.includes('"docid":') || field.Data.includes('"targetId":') || field.Data.includes('"linkId":')) { - rtfs.push({ copy, key, field }); - } - } - } - }; - if (key === "proto") { - if (doc[key] instanceof Doc) { - copy[key] = Doc.makeClone(doc[key]!, cloneMap, rtfs); - } - } else { - if (field instanceof RefField) { - copy[key] = field; - } else if (cfield instanceof ComputedField) { - copy[key] = ComputedField.MakeFunction(cfield.script.originalScript); - (key === "links" && field instanceof ObjectField) && copyObjectField(field); - } else if (field instanceof ObjectField) { - copyObjectField(field); - } else if (field instanceof Promise) { - debugger; //This shouldn't happend... - } else { - copy[key] = field; - } - } - }); - Doc.SetInPlace(copy, "title", "CLONE: " + doc.title, true); - copy.cloneOf = doc; - cloneMap.set(doc[Id], copy); - return copy; - } export function MakeDelegate(doc: Doc, id?: string, title?: string): Doc; export function MakeDelegate(doc: Opt<Doc>, id?: string, title?: string): Opt<Doc>; @@ -998,120 +927,147 @@ export namespace Doc { return false; } - // applies a custom template to a document. the template is identified by it's short name (e.g, slideView not layout_slideView) - export function makeCustomViewClicked(doc: Doc, creator: Opt<(documents: Array<Doc>, options: DocumentOptions, id?: string) => Doc>, templateSignature: string = "custom", docLayoutTemplate?: Doc) { - const batch = UndoManager.StartBatch("makeCustomViewClicked"); - runInAction(() => { - doc.layoutKey = "layout_" + templateSignature; - if (doc[doc.layoutKey] === undefined) { - createCustomView(doc, creator, templateSignature, docLayoutTemplate); - } - }); - batch.end(); - } - export function findTemplate(templateName: string, type: string, signature: string) { - let docLayoutTemplate: Opt<Doc>; - const iconViews = DocListCast(Cast(Doc.UserDoc()["template-icons"], Doc, null)?.data); - const templBtns = DocListCast(Cast(Doc.UserDoc()["template-buttons"], Doc, null)?.data); - const noteTypes = DocListCast(Cast(Doc.UserDoc()["template-notes"], Doc, null)?.data); - const clickFuncs = DocListCast(Cast(Doc.UserDoc().clickFuncs, Doc, null)?.data); - const allTemplates = iconViews.concat(templBtns).concat(noteTypes).concat(clickFuncs).map(btnDoc => (btnDoc.dragFactory as Doc) || btnDoc).filter(doc => doc.isTemplateDoc); - // bcz: this is hacky -- want to have different templates be applied depending on the "type" of a document. but type is not reliable and there could be other types of template searches so this should be generalized - // first try to find a template that matches the specific document type (<typeName>_<templateName>). otherwise, fallback to a general match on <templateName> - !docLayoutTemplate && allTemplates.forEach(tempDoc => StrCast(tempDoc.title) === templateName + "_" + type && (docLayoutTemplate = tempDoc)); - !docLayoutTemplate && allTemplates.forEach(tempDoc => StrCast(tempDoc.title) === templateName && (docLayoutTemplate = tempDoc)); - return docLayoutTemplate; - } - export function createCustomView(doc: Doc, creator: Opt<(documents: Array<Doc>, options: DocumentOptions, id?: string) => Doc>, templateSignature: string = "custom", docLayoutTemplate?: Doc) { - const templateName = templateSignature.replace(/\(.*\)/, ""); - docLayoutTemplate = docLayoutTemplate || findTemplate(templateName, StrCast(doc.type), templateSignature); - - const customName = "layout_" + templateSignature; - const _width = NumCast(doc._width); - const _height = NumCast(doc._height); - const options = { title: "data", backgroundColor: StrCast(doc.backgroundColor), _autoHeight: true, _width, x: -_width / 2, y: - _height / 2, _showSidebar: false }; - - let fieldTemplate: Opt<Doc>; - if (doc.data instanceof RichTextField || typeof (doc.data) === "string") { - fieldTemplate = Docs.Create.TextDocument("", options); - } else if (doc.data instanceof PdfField) { - fieldTemplate = Docs.Create.PdfDocument("http://www.msn.com", options); - } else if (doc.data instanceof VideoField) { - fieldTemplate = Docs.Create.VideoDocument("http://www.cs.brown.edu", options); - } else if (doc.data instanceof AudioField) { - fieldTemplate = Docs.Create.AudioDocument("http://www.cs.brown.edu", options); - } else if (doc.data instanceof ImageField) { - fieldTemplate = Docs.Create.ImageDocument("http://www.cs.brown.edu", options); - } - const docTemplate = docLayoutTemplate || creator?.(fieldTemplate ? [fieldTemplate] : [], { title: customName + "(" + doc.title + ")", isTemplateDoc: true, _width: _width + 20, _height: Math.max(100, _height + 45) }); - fieldTemplate && Doc.MakeMetadataFieldTemplate(fieldTemplate, docTemplate ? Doc.GetProto(docTemplate) : docTemplate); - docTemplate && Doc.ApplyTemplateTo(docTemplate, doc, customName, undefined); - } - export function makeCustomView(doc: Doc, custom: boolean, layout: string) { - Doc.setNativeView(doc); - if (custom) { - makeCustomViewClicked(doc, Docs.Create.StackingDocument, layout, undefined); + export namespace Get { + + const primitives = ["string", "number", "boolean"]; + + export interface JsonConversionOpts { + data: any; + title?: string; + appendToExisting?: { targetDoc: Doc, fieldKey?: string }; + excludeEmptyObjects?: boolean; } - } - export function iconify(doc: Doc) { - const layoutKey = Cast(doc.layoutKey, "string", null); - Doc.makeCustomViewClicked(doc, Docs.Create.StackingDocument, "icon", undefined); - if (layoutKey && layoutKey !== "layout" && layoutKey !== "layout_icon") doc.deiconifyLayout = layoutKey.replace("layout_", ""); - } - export function pileup(docList: Doc[], x?: number, y?: number) { - let w = 0, h = 0; - runInAction(() => { - docList.forEach(d => { - Doc.iconify(d); - w = Math.max(d[WidthSym](), w); - h = Math.max(d[HeightSym](), h); - }); - h = Math.max(h, w * 4 / 3); // converting to an icon does not update the height right away. so this is a fallback hack to try to do something reasonable - docList.forEach((d, i) => { - d.x = Math.cos(Math.PI * 2 * i / docList.length) * 10 - w / 2; - d.y = Math.sin(Math.PI * 2 * i / docList.length) * 10 - h / 2; - d.displayTimecode = undefined; // bcz: this should be automatic somehow.. along with any other properties that were logically associated with the original collection - }); - }); - if (x !== undefined && y !== undefined) { - const newCollection = Docs.Create.PileDocument(docList, { title: "pileup", x: x - 55, y: y - 55, _width: 110, _height: 100, _LODdisable: true }); - newCollection.x = NumCast(newCollection.x) + NumCast(newCollection._width) / 2 - 55; - newCollection.y = NumCast(newCollection.y) + NumCast(newCollection._height) / 2 - 55; - newCollection._width = newCollection._height = 110; - //newCollection.borderRounding = "40px"; - newCollection._jitterRotation = 10; - newCollection._backgroundColor = "gray"; - newCollection._overflow = "visible"; - return newCollection; + const defaultKey = "json"; + + /** + * This function takes any valid JSON(-like) data, i.e. parsed or unparsed, and at arbitrarily + * deep levels of nesting, converts the data and structure into nested documents with the appropriate fields. + * + * After building a hierarchy within / below a top-level document, it then returns that top-level parent. + * + * If we've received a string, treat it like valid JSON and try to parse it into an object. If this fails, the + * string is invalid JSON, so we should assume that the input is the result of a JSON.parse() + * call that returned a regular string value to be stored as a Field. + * + * If we've received something other than a string, since the caller might also pass in the results of a + * JSON.parse() call, valid input might be an object, an array (still typeof object), a boolean or a number. + * Anything else (like a function, etc. passed in naively as any) is meaningless for this operation. + * + * All TS/JS objects get converted directly to documents, directly preserving the key value structure. Everything else, + * lacking the key value structure, gets stored as a field in a wrapper document. + * + * @param data for convenience and flexibility, either a valid JSON string to be parsed, + * or the result of any JSON.parse() call. + * @param title an optional title to give to the highest parent document in the hierarchy. + * If whether this function creates a new document or appendToExisting is specified and that document already has a title, + * because this title field can be left undefined for the opposite behavior, including a title will overwrite the existing title. + * @param appendToExisting **if specified**, there are two cases, both of which return the target document: + * + * 1) the json to be converted can be represented as a document, in which case the target document will act as the root + * of the tree and receive all the conversion results as new fields on itself + * 2) the json can't be represented as a document, in which case the function will assign the field-level conversion + * results to either the specified key on the target document, or to its "json" key by default. + * + * If not specified, the function creates and returns a new entirely generic document (different from the Doc.Create calls) + * to act as the root of the tree. + * + * One might choose to specify this field if you want to write to a document returned from a Document.Create function call, + * say a TreeView document that will be rendered, not just an untyped, identityless doc that would otherwise be created + * from a default call to new Doc. + * + * @param excludeEmptyObjects whether non-primitive objects (TypeScript objects and arrays) should be converted even + * if they contain no data. By default, empty objects and arrays are ignored. + */ + export function FromJson({ data, title, appendToExisting, excludeEmptyObjects }: JsonConversionOpts): Opt<Doc> { + if (excludeEmptyObjects === undefined) { + excludeEmptyObjects = true; + } + if (data === undefined || data === null || ![...primitives, "object"].includes(typeof data)) { + return undefined; + } + let resolved: any; + try { + resolved = JSON.parse(typeof data === "string" ? data : JSON.stringify(data)); + } catch (e) { + return undefined; + } + let output: Opt<Doc>; + if (typeof resolved === "object" && !(resolved instanceof Array)) { + output = convertObject(resolved, excludeEmptyObjects, title, appendToExisting?.targetDoc); + } else { + const result = toField(resolved, excludeEmptyObjects); + if (appendToExisting) { + (output = appendToExisting.targetDoc)[appendToExisting.fieldKey || defaultKey] = result; + } else { + (output = new Doc).json = result; + } + } + title && output && (output.title = title); + return output; } - } + /** + * For each value of the object, recursively convert it to its appropriate field value + * and store the field at the appropriate key in the document if it is not undefined + * @param object the object to convert + * @returns the object mapped from JSON to field values, where each mapping + * might involve arbitrary recursion (since toField might itself call convertObject) + */ + const convertObject = (object: any, excludeEmptyObjects: boolean, title?: string, target?: Doc): Opt<Doc> => { + const hasEntries = Object.keys(object).length; + if (hasEntries || !excludeEmptyObjects) { + const resolved = target ?? new Doc; + if (hasEntries) { + let result: Opt<Field>; + Object.keys(object).map(key => { + // if excludeEmptyObjects is true, any qualifying conversions from toField will + // be undefined, and thus the results that would have + // otherwise been empty (List or Doc)s will just not be written + if (result = toField(object[key], excludeEmptyObjects, key)) { + resolved[key] = result; + } + }); + } + title && (resolved.title = title); + return resolved; + } + }; - export async function addFieldEnumerations(doc: Opt<Doc>, enumeratedFieldKey: string, enumerations: { title: string, _backgroundColor?: string, color?: string }[]) { - let optionsCollection = await DocServer.GetRefField(enumeratedFieldKey); - if (!(optionsCollection instanceof Doc)) { - optionsCollection = Docs.Create.StackingDocument([], { title: `${enumeratedFieldKey} field set` }, enumeratedFieldKey); - Doc.AddDocToList((Doc.UserDoc().fieldTypes as Doc), "data", optionsCollection as Doc); - } - const options = optionsCollection as Doc; - const targetDoc = doc && Doc.GetProto(Cast(doc.rootDocument, Doc, null) || doc); - const docFind = `options.data.find(doc => doc.title === (this.rootDocument||this)["${enumeratedFieldKey}"])?`; - targetDoc && (targetDoc.backgroundColor = ComputedField.MakeFunction(docFind + `._backgroundColor || "white"`, undefined, { options })); - targetDoc && (targetDoc.color = ComputedField.MakeFunction(docFind + `.color || "black"`, undefined, { options })); - targetDoc && (targetDoc.borderRounding = ComputedField.MakeFunction(docFind + `.borderRounding`, undefined, { options })); - enumerations.map(enumeration => { - const found = DocListCast(options.data).find(d => d.title === enumeration.title); - if (found) { - found._backgroundColor = enumeration._backgroundColor || found._backgroundColor; - found._color = enumeration.color || found._color; - } else { - Doc.AddDocToList(options, "data", Docs.Create.TextDocument(enumeration.title, enumeration)); + /** + * For each element in the list, recursively convert it to a document or other field + * and push the field to the list if it is not undefined + * @param list the list to convert + * @returns the list mapped from JSON to field values, where each mapping + * might involve arbitrary recursion (since toField might itself call convertList) + */ + const convertList = (list: Array<any>, excludeEmptyObjects: boolean): Opt<List<Field>> => { + const target = new List(); + let result: Opt<Field>; + // if excludeEmptyObjects is true, any qualifying conversions from toField will + // be undefined, and thus the results that would have + // otherwise been empty (List or Doc)s will just not be written + list.map(item => (result = toField(item, excludeEmptyObjects)) && target.push(result)); + if (target.length || !excludeEmptyObjects) { + return target; } - }); - return optionsCollection; + }; + + const toField = (data: any, excludeEmptyObjects: boolean, title?: string): Opt<Field> => { + if (data === null || data === undefined) { + return undefined; + } + if (primitives.includes(typeof data)) { + return data; + } + if (typeof data === "object") { + return data instanceof Array ? convertList(data, excludeEmptyObjects) : convertObject(data, excludeEmptyObjects, title, undefined); + } + throw new Error(`How did ${data} of type ${typeof data} end up in JSON?`); + }; } + } Scripting.addGlobal(function renameAlias(doc: any, n: any) { return StrCast(Doc.GetProto(doc).title).replace(/\([0-9]*\)/, "") + `(${n})`; }); diff --git a/src/pen-gestures/GestureUtils.ts b/src/pen-gestures/GestureUtils.ts index a50cca2b0..20e14a68d 100644 --- a/src/pen-gestures/GestureUtils.ts +++ b/src/pen-gestures/GestureUtils.ts @@ -1,12 +1,6 @@ -import { NDollarRecognizer } from "./ndollar"; -import { Type } from "typescript"; -import { InkField, PointData } from "../fields/InkField"; -import { Docs } from "../client/documents/Documents"; -import { Doc, WidthSym, HeightSym } from "../fields/Doc"; -import { NumCast } from "../fields/Types"; -import { CollectionFreeFormView } from "../client/views/collections/collectionFreeForm/CollectionFreeFormView"; import { Rect } from "react-measure"; -import { Scripting } from "../client/util/Scripting"; +import { PointData } from "../fields/InkField"; +import { NDollarRecognizer } from "./ndollar"; export namespace GestureUtils { export class GestureEvent { |