diff options
Diffstat (limited to 'src')
45 files changed, 537 insertions, 966 deletions
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 49d1820f5..53671707e 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -22,7 +22,6 @@ import { ImageField, VideoField, AudioField, PdfField, WebField, YoutubeField } import { HtmlField } from "../../new_fields/HtmlField"; import { List } from "../../new_fields/List"; import { Cast, NumCast } from "../../new_fields/Types"; -import { IconField } from "../../new_fields/IconField"; import { listSpec } from "../../new_fields/Schema"; import { DocServer } from "../DocServer"; import { dropActionType } from "../util/DragManager"; @@ -53,8 +52,8 @@ import { InkingStroke } from "../views/InkingStroke"; import { InkField } from "../../new_fields/InkField"; import { InkingControl } from "../views/InkingControl"; import { RichTextField } from "../../new_fields/RichTextField"; -import { Networking } from "../Network"; import { extname } from "path"; +import { MessageStore } from "../../server/Message"; const requestImageSize = require('../util/request-image-size'); const path = require('path'); @@ -69,7 +68,9 @@ export interface DocumentOptions { _fitWidth?: boolean; _fitToBox?: boolean; // whether a freeformview should zoom/scale to create a shrinkwrapped view of its contents _LODdisable?: boolean; - dropAction?: dropActionType; + _showTitleHover?: string; // + _showTitle?: string; // which field to display in the title area. leave empty to have no title + _showCaption?: string; // which field to display in the caption area. leave empty to have no caption _chromeStatus?: string; _viewType?: number; _gridGap?: number; // gap between items in masonry view @@ -81,6 +82,7 @@ export interface DocumentOptions { x?: number; y?: number; z?: number; + dropAction?: dropActionType; layoutKey?: string; type?: string; title?: string; @@ -88,7 +90,6 @@ export interface DocumentOptions { scale?: number; isDisplayPanel?: boolean; // whether the panel functions as GoldenLayout "stack" used to display documents forceActive?: boolean; - preventTreeViewOpen?: boolean; // ignores the treeViewOpen Doc flag which allows a treeViewItem's expande/collapse state to be independent of other views of the same document in the tree view layout?: string | Doc; hideHeadings?: boolean; // whether stacking view column headings should be hidden isTemplateForField?: string; // the field key for which the containing document is a rendering template @@ -105,10 +106,8 @@ export interface DocumentOptions { curPage?: number; currentTimecode?: number; // the current timecode of a time-based document (e.g., current time of a video) value is in seconds displayTimecode?: number; // the time that a document should be displayed (e.g., time an annotation should be displayed on a video) - documentText?: string; borderRounding?: string; boxShadow?: string; - showTitle?: string; sectionFilter?: string; // field key used to determine headings for sections in stacking and masonry views schemaColumns?: List<SchemaHeaderField>; dockingConfig?: string; @@ -121,15 +120,16 @@ export interface DocumentOptions { onChildClick?: ScriptField; // script given to children of a collection to execute when they are clicked onPointerDown?: ScriptField; onPointerUp?: ScriptField; + dropConverter?: ScriptField; // script to run when documents are dropped on this Document. dragFactory?: Doc; // document to create when dragging with a suitable onDragStart script onDragStart?: ScriptField; //script to execute at start of drag operation -- e.g., when a "creator" button is dragged this script generates a different document to drop - clipboard?: Doc; //script to execute at start of drag operation -- e.g., when a "creator" button is dragged this script generates a different document to drop + clipboard?: Doc; icon?: string; sourcePanel?: Doc; // panel to display in 'targetContainer' as the result of a button onClick script targetContainer?: Doc; // document whose proto will be set to 'panel' as the result of a onClick click script - dropConverter?: ScriptField; // script to run when documents are dropped on this Document. strokeWidth?: number; color?: string; + treeViewPreventOpen?: boolean; // ignores the treeViewOpen Doc flag which allows a treeViewItem's expand/collapse state to be independent of other views of the same document in the tree view treeViewHideTitle?: boolean; // whether to hide the title of a tree view treeViewHideHeaderFields?: boolean; // whether to hide the drop down options for tree view items. treeViewOpen?: boolean; // whether this document is expanded in a tree view @@ -138,9 +138,9 @@ export interface DocumentOptions { limitHeight?: number; // maximum height for newly created (eg, from pasting) text documents // [key: string]: Opt<Field>; pointerHack?: boolean; // for buttons, allows onClick handler to fire onPointerDown - isExpanded?: boolean; // is linear view expanded - textTransform?: string; // is linear view expanded - letterSpacing?: string; // is linear view expanded + linearViewIsExpanded?: boolean; // is linear view expanded + textTransform?: string; + letterSpacing?: string; } class EmptyBox { @@ -168,7 +168,7 @@ export namespace Docs { const TemplateMap: TemplateMap = new Map([ [DocumentType.TEXT, { layout: { view: FormattedTextBox, dataField: data }, - options: { _height: 150, backgroundColor: "#f1efeb", defaultBackgroundColor: "#f1efeb" } + options: { _height: 150 } }], [DocumentType.HIST, { layout: { view: HistogramBox, dataField: data }, @@ -346,6 +346,7 @@ export namespace Docs { export namespace Create { export function Buxton() { + let responded = false; const loading = new Doc; loading.title = "Please wait for the import script..."; const parent = TreeDocument([loading], { @@ -354,41 +355,48 @@ export namespace Docs { _height: 400, _LODdisable: true }); - Networking.FetchFromServer("/buxton").then(response => { - const devices = JSON.parse(response); - if (!Array.isArray(devices)) { - if ("error" in devices) { - loading.title = devices.error; - } else { - console.log(devices); - alert("The importer returned an unexpected import format. Check the console."); - } - return; + const parentProto = Doc.GetProto(parent); + const { _socket } = DocServer; + Utils.AddServerHandler(_socket, MessageStore.BuxtonDocumentResult, ({ device, errors }) => { + if (!responded) { + responded = true; + parentProto.data = new List<Doc>(); } - const parentProto = Doc.GetProto(parent); - parentProto.data = new List<Doc>(); - devices.forEach(device => { + if (device) { const { __images } = device; delete device.__images; const { ImageDocument, StackingDocument } = Docs.Create; - if (Array.isArray(__images)) { - const constructed = __images.map(relative => Utils.prepend(relative)); - const deviceImages = constructed.map((url, i) => ImageDocument(url, { title: `image${i}.${extname(url)}` })); - const doc = StackingDocument(deviceImages, { title: device.title, _LODdisable: true }); - const deviceProto = Doc.GetProto(doc); - deviceProto.hero = new ImageField(constructed[0]); - Docs.Get.DocumentHierarchyFromJson(device, undefined, deviceProto); - Doc.AddDocToList(parentProto, "data", doc); - } - }); + const constructed = __images.map(({ url, nativeWidth, nativeHeight }) => ({ url: Utils.prepend(url), nativeWidth, nativeHeight })); + const deviceImages = constructed.map(({ url, nativeWidth, nativeHeight }, i) => ImageDocument(url, { + title: `image${i}.${extname(url)}`, + _nativeWidth: nativeWidth, + _nativeHeight: nativeHeight + })); + const doc = StackingDocument(deviceImages, { title: device.title, _LODdisable: true }); + const deviceProto = Doc.GetProto(doc); + deviceProto.hero = new ImageField(constructed[0].url); + Docs.Get.DocumentHierarchyFromJson(device, undefined, deviceProto); + Doc.AddDocToList(parentProto, "data", doc); + } else if (errors) { + console.log(errors); + } else { + alert("A Buxton document import was completely empty (??)"); + } + }); + Utils.AddServerHandler(_socket, MessageStore.BuxtonImportComplete, ({ deviceCount, errorCount }) => { + _socket.off(MessageStore.BuxtonDocumentResult.Message); + _socket.off(MessageStore.BuxtonImportComplete.Message); + alert(`Successfully imported ${deviceCount} device${deviceCount === 1 ? "" : "s"}, with ${errorCount} error${errorCount === 1 ? "" : "s"}, in ${(Date.now() - startTime) / 1000} seconds.`); }); + const startTime = Date.now(); + Utils.Emit(_socket, MessageStore.BeginBuxtonImport, ""); return parent; } Scripting.addGlobal(Buxton); const delegateKeys = ["x", "y", "layoutKey", "_width", "_height", "_panX", "_panY", "_viewType", "_nativeWidth", "_nativeHeight", "dropAction", "_annotationOn", - "_chromeStatus", "_forceActive", "_autoHeight", "_fitWidth", "_LODdisable", "_itemIndex", "_showSidebar", "showTitle"]; + "_chromeStatus", "_forceActive", "_autoHeight", "_fitWidth", "_LODdisable", "_itemIndex", "_showSidebar", "_showTitle", "_showCaption", "_showTitleHover"]; /** * This function receives the relevant document prototype and uses diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index e572f0fcb..2877d5fd7 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -134,7 +134,6 @@ export namespace DragManager { embedDoc?: boolean; moveDocument?: MoveFunction; isSelectionMove?: boolean; // indicates that an explicitly selected Document is being dragged. this will suppress onDragStart scripts - applyAsTemplate?: boolean; } export class LinkDragData { constructor(linkSourceDoc: Doc) { diff --git a/src/client/util/RichTextRules.ts b/src/client/util/RichTextRules.ts index 6b8762a5e..de0f46202 100644 --- a/src/client/util/RichTextRules.ts +++ b/src/client/util/RichTextRules.ts @@ -121,8 +121,7 @@ export class RichTextRules { new InputRule( new RegExp(/##$/), (state, match, start, end) => { - const schemaDoc = Doc.GetDataDoc(this.Document); - const textDoc = Doc.GetProto(Cast(schemaDoc[DataSym], Doc, null)!); + const textDoc = this.Document[DataSym]; const numInlines = NumCast(textDoc.inlineTextCount); textDoc.inlineTextCount = numInlines + 1; const inlineFieldKey = "inline" + numInlines; // which field on the text document this annotation will write to diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx index 3cf0561dc..80bd75771 100644 --- a/src/client/util/RichTextSchema.tsx +++ b/src/client/util/RichTextSchema.tsx @@ -1136,7 +1136,7 @@ const fromJson = schema.nodeFromJSON; schema.nodeFromJSON = (json: any) => { const node = fromJson(json); - if (json.type === schema.marks.summarize.name) { + if (json.type === schema.nodes.summary.name) { node.attrs.text = Slice.fromJSON(schema, node.attrs.textslice); } return node; diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx index ee850922a..2201fe710 100644 --- a/src/client/views/DocumentButtonBar.tsx +++ b/src/client/views/DocumentButtonBar.tsx @@ -301,7 +301,7 @@ export class DocumentButtonBar extends React.Component<{ views: (DocumentView | const view0 = this.view0; const templates: Map<Template, boolean> = new Map(); Array.from(Object.values(Templates.TemplateList)).map(template => - templates.set(template, this.props.views.reduce((checked, doc) => checked || doc?.getLayoutPropStr("show" + template.Name) ? true : false, false as boolean))); + templates.set(template, this.props.views.reduce((checked, doc) => checked || doc?.props.Document["_show" + template.Name] ? true : false, false as boolean))); return !view0 ? (null) : <div title="Customize layout" className="documentButtonBar-linkFlyout" ref={this._dragRef}> <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={<TemplateMenu docViews={this.props.views.filter(v => v).map(v => v as DocumentView)} templates={templates} />}> diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss index d57b1456a..455e53a79 100644 --- a/src/client/views/DocumentDecorations.scss +++ b/src/client/views/DocumentDecorations.scss @@ -165,11 +165,12 @@ $linkGap : 3px; .link-button-container { margin-top: $linkGap; - grid-column: 1/4; width: max-content; height: auto; display: flex; flex-direction: row; + z-index: 5; + position: absolute; } .linkButtonWrapper { diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 3dfe34234..65c02591c 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -266,11 +266,11 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> const layoutKey = Cast(dv.props.Document.layoutKey, "string", null); const collapse = layoutKey !== "layout_icon"; if (collapse) { - dv.setCustomView(collapse, "icon"); + dv.switchViews(collapse, "icon"); if (layoutKey && layoutKey !== "layout") dv.props.Document.deiconifyLayout = layoutKey.replace("layout_", ""); } else { const deiconifyLayout = Cast(dv.props.Document.deiconifyLayout, "string", null); - dv.setCustomView(deiconifyLayout ? true : false, deiconifyLayout); + dv.switchViews(deiconifyLayout ? true : false, deiconifyLayout); dv.props.Document.deiconifyLayout = undefined; } }); @@ -509,11 +509,11 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> top: bounds.y - this._resizeBorderWidth / 2, pointerEvents: this.Interacting ? "none" : "all", zIndex: SelectionManager.SelectedDocuments().length > 1 ? 900 : 0, - }} onPointerDown={this.onBackgroundDown} onContextMenu={(e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); }} > + }} onPointerDown={this.onBackgroundDown} onContextMenu={e => { e.preventDefault(); e.stopPropagation(); }} > </div> <div className="documentDecorations-container" ref={this.setTextBar} style={{ width: (bounds.r - bounds.x + this._resizeBorderWidth) + "px", - height: (bounds.b - bounds.y + this._resizeBorderWidth + this._linkBoxHeight + this._titleHeight + 3) + "px", + height: (bounds.b - bounds.y + this._resizeBorderWidth + this._titleHeight) + "px", left: bounds.x - this._resizeBorderWidth / 2, top: bounds.y - this._resizeBorderWidth / 2 - this._titleHeight, opacity: this._opacity @@ -545,10 +545,11 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> <div id="documentDecorations-bottomResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div> <div id="documentDecorations-bottomRightResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div> <div id="documentDecorations-borderRadius" className="documentDecorations-radius" onPointerDown={this.onRadiusDown} onContextMenu={(e) => e.preventDefault()}><span className="borderRadiusTooltip" title="Drag Corner Radius"></span></div> - <div className="link-button-container"> - <DocumentButtonBar views={SelectionManager.SelectedDocuments()} /> - </div> + </div > + <div className="link-button-container" style={{ left: bounds.x - this._resizeBorderWidth / 2, top: bounds.b + this._resizeBorderWidth / 2 }}> + <DocumentButtonBar views={SelectionManager.SelectedDocuments()} /> + </div> </div> ); } diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx index 84c6b0dfd..4a27425e8 100644 --- a/src/client/views/EditableView.tsx +++ b/src/client/views/EditableView.tsx @@ -7,6 +7,7 @@ import { SchemaHeaderField } from '../../new_fields/SchemaHeaderField'; import { ContextMenu } from './ContextMenu'; import { ContextMenuProps } from './ContextMenuItem'; import "./EditableView.scss"; +import { CollectionTreeView } from './collections/CollectionTreeView'; export interface EditableProps { /** @@ -60,12 +61,14 @@ export interface EditableProps { */ @observer export class EditableView extends React.Component<EditableProps> { + public static loadId = ""; @observable _editing: boolean = false; @observable _headingsHack: number = 1; constructor(props: EditableProps) { super(props); this._editing = this.props.editing ? true : false; + EditableView.loadId = ""; } @action @@ -75,6 +78,7 @@ export class EditableView extends React.Component<EditableProps> { // to false. this will no longer do so -syip if (nextProps.editing && nextProps.editing !== this._editing) { this._editing = nextProps.editing; + EditableView.loadId = ""; } } diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 401a4b15c..ba49a2b53 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -13,7 +13,7 @@ import { Doc, DocListCast, Field, FieldResult, Opt } from '../../new_fields/Doc' import { Id } from '../../new_fields/FieldSymbols'; import { List } from '../../new_fields/List'; import { listSpec } from '../../new_fields/Schema'; -import { Cast, FieldValue, StrCast } from '../../new_fields/Types'; +import { Cast, FieldValue, StrCast, BoolCast } from '../../new_fields/Types'; import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils'; import { emptyFunction, returnEmptyString, returnFalse, returnOne, returnTrue, Utils, emptyPath } from '../../Utils'; import GoogleAuthenticationManager from '../apis/GoogleAuthenticationManager'; @@ -43,6 +43,7 @@ import SettingsManager from '../util/SettingsManager'; import { TraceMobx } from '../../new_fields/util'; import { RadialMenu } from './nodes/RadialMenu'; import RichTextMenu from '../util/RichTextMenu'; +import { DocumentType } from '../documents/DocumentTypes'; @observer export class MainView extends React.Component { @@ -56,8 +57,9 @@ export class MainView extends React.Component { @observable private _panelHeight: number = 0; @observable private _flyoutTranslate: boolean = true; @observable public flyoutWidth: number = 250; + private get darkScheme() { return BoolCast(Cast(this.userDoc.activeWorkspace, Doc, null)?.darkScheme); } - @computed private get userDoc() { return CurrentUserUtils.UserDocument; } + @computed private get userDoc() { return Doc.UserDoc(); } @computed private get mainContainer() { return this.userDoc ? FieldValue(Cast(this.userDoc.activeWorkspace, Doc)) : CurrentUserUtils.GuestWorkspace; } @computed public get mainFreeform(): Opt<Doc> { return (docs => (docs && docs.length > 1) ? docs[1] : undefined)(DocListCast(this.mainContainer!.data)); } @computed public get sidebarButtonsDoc() { return Cast(CurrentUserUtils.UserDocument.sidebarButtons, Doc) as Doc; } @@ -207,7 +209,6 @@ export class MainView extends React.Component { _width: this._panelWidth * .7, _height: this._panelHeight, title: "Collection " + workspaceCount, - backgroundColor: "white" }; const freeformDoc = CurrentUserUtils.GuestTarget || Docs.Create.FreeformDocument([], freeformOptions); Doc.AddDocToList(Doc.GetProto(CurrentUserUtils.UserDocument.documents as Doc), "data", freeformDoc); @@ -273,6 +274,16 @@ export class MainView extends React.Component { getPHeight = () => this._panelHeight; getContentsHeight = () => this._panelHeight - this._buttonBarHeight; + childBackgroundColor = (doc: Doc) => { + if (this.darkScheme) { + return doc.type === DocumentType.TEXT ? "#112423" : "black"; + } + return doc.type === DocumentType.TEXT ? "#f1efeb" : + doc.type === DocumentType.COL && doc._viewType === CollectionViewType.Tree ? "lightgray" : "white"; + } + sidebarBackgroundColor = (doc: Doc) => { + return this.childBackgroundColor(doc); + } @computed get mainDocView() { return <DocumentView Document={this.mainContainer!} DataDoc={undefined} @@ -281,13 +292,13 @@ export class MainView extends React.Component { addDocTab={this.addDocTabFunc} pinToPres={emptyFunction} onClick={undefined} + backgroundColor={this.childBackgroundColor} removeDocument={undefined} ScreenToLocalTransform={Transform.Identity} ContentScaling={returnOne} PanelWidth={this.getPWidth} PanelHeight={this.getPHeight} renderDepth={0} - backgroundColor={returnEmptyString} focus={emptyFunction} parentActive={returnTrue} whenActiveChanged={emptyFunction} @@ -366,7 +377,7 @@ export class MainView extends React.Component { mainContainerXf = () => new Transform(0, -this._buttonBarHeight, 1); @computed get flyout() { - const sidebarContent = this.userDoc && this.userDoc.sidebarContainer; + const sidebarContent = this.userDoc?.sidebarContainer; if (!(sidebarContent instanceof Doc)) { return (null); } @@ -388,7 +399,7 @@ export class MainView extends React.Component { PanelHeight={this.getPHeight} renderDepth={0} focus={emptyFunction} - backgroundColor={returnEmptyString} + backgroundColor={this.sidebarBackgroundColor} parentActive={returnTrue} whenActiveChanged={emptyFunction} bringToFront={emptyFunction} @@ -414,7 +425,7 @@ export class MainView extends React.Component { PanelHeight={this.getContentsHeight} renderDepth={0} focus={emptyFunction} - backgroundColor={returnEmptyString} + backgroundColor={this.sidebarBackgroundColor} parentActive={returnTrue} whenActiveChanged={emptyFunction} bringToFront={emptyFunction} @@ -437,7 +448,7 @@ export class MainView extends React.Component { @computed get mainContent() { const sidebar = this.userDoc && this.userDoc.sidebarContainer; return !this.userDoc || !(sidebar instanceof Doc) ? (null) : ( - <div className="mainView-mainContent" > + <div className="mainView-mainContent" style={{ color: this.darkScheme ? "lightGray" : "black" }} > <div className="mainView-flyoutContainer" onPointerLeave={this.pointerLeaveDragger} style={{ width: this.flyoutWidth }}> <div className="mainView-libraryHandle" onPointerDown={this.onPointerDown} onPointerOver={this.pointerOverDragger} style={{ backgroundColor: `${StrCast(sidebar.backgroundColor, "lightGray")}` }} > @@ -482,12 +493,13 @@ export class MainView extends React.Component { return new Transform(-translateX, -translateY, 1 / scale); } @computed get docButtons() { - if (CurrentUserUtils.UserDocument?.expandingButtons instanceof Doc) { + const expandingBtns = Doc.UserDoc()?.expandingButtons; + if (expandingBtns instanceof Doc) { return <div className="mainView-docButtons" ref={this._docBtnRef} - style={{ height: !CurrentUserUtils.UserDocument.expandingButtons.isExpanded ? "42px" : undefined }} > + style={{ height: !expandingBtns.linearViewIsExpanded ? "42px" : undefined }} > <MainViewNotifs /> <CollectionLinearView - Document={CurrentUserUtils.UserDocument.expandingButtons} + Document={expandingBtns} DataDoc={undefined} LibraryPath={emptyPath} fieldKey={"data"} diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx index c9c6af054..595c3817e 100644 --- a/src/client/views/TemplateMenu.tsx +++ b/src/client/views/TemplateMenu.tsx @@ -50,7 +50,7 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> { @observable private _hidden: boolean = true; toggleLayout = (e: React.ChangeEvent<HTMLInputElement>, layout: string): void => { - this.props.docViews.map(dv => dv.setCustomView(e.target.checked, layout)); + this.props.docViews.map(dv => dv.switchViews(e.target.checked, layout));//.setCustomView(e.target.checked, layout)); } toggleFloat = (e: React.ChangeEvent<HTMLInputElement>): void => { @@ -64,11 +64,7 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> { @undoBatch @action toggleTemplate = (event: React.ChangeEvent<HTMLInputElement>, template: Template): void => { - if (event.target.checked) { - this.props.docViews.map(d => d.Document["show" + template.Name] = template.Name.toLowerCase()); - } else { - this.props.docViews.map(d => d.Document["show" + template.Name] = ""); - } + this.props.docViews.forEach(d => Doc.Layout(d.Document)["_show" + template.Name] = event.target.checked ? template.Name.toLowerCase() : ""); } @action @@ -79,10 +75,8 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> { @undoBatch @action toggleChrome = (): void => { - this.props.docViews.map(dv => { - const layout = Doc.Layout(dv.Document); - layout._chromeStatus = (layout._chromeStatus !== "disabled" ? "disabled" : StrCast(layout._replacedChrome, "enabled")); - }); + this.props.docViews.map(dv => Doc.Layout(dv.Document)).forEach(layout => + layout._chromeStatus = (layout._chromeStatus !== "disabled" ? "disabled" : StrCast(layout._replacedChrome, "enabled"))); } // todo: add brushes to brushMap to save with a style name diff --git a/src/client/views/collections/CollectionCarouselView.scss b/src/client/views/collections/CollectionCarouselView.scss index ad369bbff..fd1296286 100644 --- a/src/client/views/collections/CollectionCarouselView.scss +++ b/src/client/views/collections/CollectionCarouselView.scss @@ -1,6 +1,7 @@ .collectionCarouselView-outer { background: gray; + height : 100%; .collectionCarouselView-caption { margin-left: 10%; margin-right: 10%; diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx index 00edf71dd..226a1c813 100644 --- a/src/client/views/collections/CollectionCarouselView.tsx +++ b/src/client/views/collections/CollectionCarouselView.tsx @@ -12,6 +12,8 @@ import { CollectionSubView } from './CollectionSubView'; import { faCaretLeft, faCaretRight } from '@fortawesome/free-solid-svg-icons'; import { Doc } from '../../../new_fields/Doc'; import { FormattedTextBox } from '../nodes/FormattedTextBox'; +import { ContextMenu } from '../ContextMenu'; +import { ObjectField } from '../../../new_fields/ObjectField'; type CarouselDocument = makeInterface<[typeof documentSchema,]>; const CarouselDocument = makeInterface(documentSchema); @@ -50,6 +52,7 @@ export class CollectionCarouselView extends CollectionSubView(CarouselDocument) <div> <div className="collectionCarouselView-image"> <ContentFittingDocumentView {...this.props} + backgroundColor={this.props.backgroundColor} Document={this.childLayoutPairs[index].layout} DataDocument={this.childLayoutPairs[index].data} PanelHeight={this.panelHeight} @@ -70,8 +73,21 @@ export class CollectionCarouselView extends CollectionSubView(CarouselDocument) </div> </>; } + + + onContextMenu = (e: React.MouseEvent): void => { + // need to test if propagation has stopped because GoldenLayout forces a parallel react hierarchy to be created for its top-level layout + if (!e.isPropagationStopped()) { + ContextMenu.Instance.addItem({ + description: "Make Hero Image", event: () => { + const index = NumCast(this.layoutDoc._itemIndex); + (this.dataDoc || Doc.GetProto(this.props.Document)).hero = ObjectField.MakeCopy(this.childLayoutPairs[index].layout.data as ObjectField); + }, icon: "plus" + }); + } + } render() { - return <div className="collectionCarouselView-outer" ref={this.createDashEventsTarget}> + return <div className="collectionCarouselView-outer" ref={this.createDashEventsTarget} onContextMenu={this.onContextMenu}> {this.content} {this.buttons} </div>; diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index cb413b3e3..0b7dbea7c 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -599,6 +599,7 @@ interface DockedFrameProps { dataDocumentId: FieldId; glContainer: any; libraryPath: (FieldId[]); + backgroundColor?: (doc:Doc) => string| undefined; //collectionDockingView: CollectionDockingView } @observer @@ -756,7 +757,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { parentActive={returnTrue} whenActiveChanged={emptyFunction} focus={emptyFunction} - backgroundColor={returnEmptyString} + backgroundColor={CollectionDockingView.Instance.props.backgroundColor} addDocTab={this.addDocTab} pinToPres={DockedFrameRenderer.PinDoc} ContainingCollectionView={undefined} diff --git a/src/client/views/collections/CollectionLinearView.tsx b/src/client/views/collections/CollectionLinearView.tsx index 67062ae41..7eb316cf0 100644 --- a/src/client/views/collections/CollectionLinearView.tsx +++ b/src/client/views/collections/CollectionLinearView.tsx @@ -38,8 +38,8 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) { componentDidMount() { // is there any reason this needs to exist? -syip. yes, it handles autoHeight for stacking views (masonry isn't yet supported). - this._widthDisposer = reaction(() => this.props.Document[HeightSym]() + this.childDocs.length + (this.props.Document.isExpanded ? 1 : 0), - () => this.props.Document._width = 5 + (this.props.Document.isExpanded ? this.childDocs.length * (this.props.Document[HeightSym]()) : 10), + this._widthDisposer = reaction(() => this.props.Document[HeightSym]() + this.childDocs.length + (this.props.Document.linearViewIsExpanded ? 1 : 0), + () => this.props.Document._width = 5 + (this.props.Document.linearViewIsExpanded ? this.childDocs.length * (this.props.Document[HeightSym]()) : 10), { fireImmediately: true } ); @@ -84,8 +84,8 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) { const guid = Utils.GenerateGuid(); return <div className="collectionLinearView-outer"> <div className="collectionLinearView" ref={this.createDashEventsTarget} > - <input id={`${guid}`} type="checkbox" checked={BoolCast(this.props.Document.isExpanded)} ref={this.addMenuToggle} - onChange={action((e: any) => this.props.Document.isExpanded = this.addMenuToggle.current!.checked)} /> + <input id={`${guid}`} type="checkbox" checked={BoolCast(this.props.Document.linearViewIsExpanded)} ref={this.addMenuToggle} + onChange={action((e: any) => this.props.Document.linearViewIsExpanded = this.addMenuToggle.current!.checked)} /> <label htmlFor={`${guid}`} style={{ marginTop: "auto", marginBottom: "auto", background: StrCast(this.props.Document.backgroundColor, "black") === StrCast(this.props.Document.color, "white") ? "black" : StrCast(this.props.Document.backgroundColor, "black") }} title="Close Menu"><p>+</p></label> <div className="collectionLinearView-content" style={{ height: this.dimension(), width: NumCast(this.props.Document._width, 25) }}> @@ -97,7 +97,7 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) { return <div className={`collectionLinearView-docBtn` + (pair.layout.onClick || pair.layout.onDragStart ? "-scalable" : "")} key={pair.layout[Id]} ref={dref} style={{ width: nested ? pair.layout[WidthSym]() : this.dimension() - deltaSize, - height: nested && pair.layout.isExpanded ? pair.layout[HeightSym]() : this.dimension() - deltaSize, + height: nested && pair.layout.linearViewIsExpanded ? pair.layout[HeightSym]() : this.dimension() - deltaSize, }} > <DocumentView Document={pair.layout} diff --git a/src/client/views/collections/CollectionSchemaCells.tsx b/src/client/views/collections/CollectionSchemaCells.tsx index caffa7eb1..a3b1b5ec0 100644 --- a/src/client/views/collections/CollectionSchemaCells.tsx +++ b/src/client/views/collections/CollectionSchemaCells.tsx @@ -37,7 +37,8 @@ export interface CellProps { renderDepth: number; addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => boolean; pinToPres: (document: Doc) => void; - moveDocument: (document: Doc, targetCollection: Doc | undefined, addDocument: (document: Doc) => boolean) => boolean; + moveDocument: (document: Doc, targetCollection: Doc | undefined, + addDocument: (document: Doc) => boolean) => boolean; isFocused: boolean; changeFocusedCellByIndex: (row: number, col: number) => void; setIsEditing: (isEditing: boolean) => void; diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index d21ae32bc..055035b3e 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -40,7 +40,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { @computed get sectionFilter() { return StrCast(this.props.Document.sectionFilter); } @computed get filteredChildren() { return this.childLayoutPairs.filter(pair => pair.layout instanceof Doc && !pair.layout.isMinimized).map(pair => pair.layout); } @computed get xMargin() { return NumCast(this.props.Document._xMargin, 2 * Math.min(this.gridGap, .05 * this.props.PanelWidth())); } - @computed get yMargin() { return Math.max(this.props.Document.showTitle && !this.props.Document.showTitleHover ? 30 : 0, NumCast(this.props.Document._yMargin, 0)); } // 2 * this.gridGap)); } + @computed get yMargin() { return Math.max(this.props.Document._showTitle && !this.props.Document._showTitleHover ? 30 : 0, NumCast(this.props.Document._yMargin, 0)); } // 2 * this.gridGap)); } @computed get gridGap() { return NumCast(this.props.Document._gridGap, 10); } @computed get isStackingView() { return BoolCast(this.props.Document.singleColumn, true); } @computed get numGroupColumns() { return this.isStackingView ? Math.max(1, this.Sections.size + (this.showAddAGroup ? 1 : 0)) : 1; } @@ -159,6 +159,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { return <ContentFittingDocumentView Document={doc} DataDocument={dataDoc} + backgroundColor={this.props.backgroundColor} LayoutDoc={this.props.childLayoutTemplate} LibraryPath={this.props.LibraryPath} renderDepth={this.props.renderDepth + 1} @@ -369,8 +370,6 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { if (!e.isPropagationStopped()) { const subItems: ContextMenuProps[] = []; subItems.push({ description: `${this.props.Document.fillColumn ? "Variable Size" : "Autosize"} Column`, event: () => this.props.Document.fillColumn = !this.props.Document.fillColumn, icon: "plus" }); - subItems.push({ description: `${this.props.Document.showTitles ? "Hide Titles" : "Show Titles"}`, event: () => this.props.Document.showTitles = !this.props.Document.showTitles ? "title" : "", icon: "plus" }); - subItems.push({ description: `${this.props.Document.showCaptions ? "Hide Captions" : "Show Captions"}`, event: () => this.props.Document.showCaptions = !this.props.Document.showCaptions ? "caption" : "", icon: "plus" }); ContextMenu.Instance.addItem({ description: "Stacking Options ...", subitems: subItems, icon: "eye" }); } } diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 32480ad4e..0963e1ea6 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -159,7 +159,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { const docDragData = de.complete.docDragData; (this.props.Document.dropConverter instanceof ScriptField) && this.props.Document.dropConverter.script.run({ dragData: docDragData }); /// bcz: check this - if (docDragData && !docDragData.applyAsTemplate) { + if (docDragData) { if (de.altKey && docDragData.draggedDocuments.length) { this.childDocs.map(doc => { doc.layout_fromParent = docDragData.draggedDocuments[0]; @@ -253,7 +253,8 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { } }); } else { - const htmlDoc = Docs.Create.HtmlDocument(html, { ...options, title: "-web page-", _width: 300, _height: 300, documentText: text }); + const htmlDoc = Docs.Create.HtmlDocument(html, { ...options, title: "-web page-", _width: 300, _height: 300 }); + Doc.GetProto(htmlDoc)["data-text"] = text; this.props.addDocument(htmlDoc); } return; @@ -283,63 +284,74 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { const albumId = matches[3]; const mediaItems = await GooglePhotos.Query.AlbumSearch(albumId); console.log(mediaItems); + return; } - const batch = UndoManager.StartBatch("collection view drop"); - const promises: Promise<void>[] = []; - // tslint:disable-next-line:prefer-for-of - for (let i = 0; i < e.dataTransfer.items.length; i++) { - const item = e.dataTransfer.items[i]; - if (item.kind === "string" && item.type.indexOf("uri") !== -1) { - let str: string; - const prom = new Promise<string>(resolve => e.dataTransfer.items[i].getAsString(resolve)) - .then(action((s: string) => rp.head(Utils.CorsProxy(str = s)))) - .then(result => { - const type = result["content-type"]; - if (type) { - Docs.Get.DocumentFromType(type, str, options) - .then(doc => doc && this.props.addDocument(doc)); - } - }); - promises.push(prom); - } - const type = item.type; - if (item.kind === "file") { - const file = item.getAsFile(); - const formData = new FormData(); - - if (!file || !file.type) { - continue; + const { items } = e.dataTransfer; + const { length } = items; + if (length) { + const batch = UndoManager.StartBatch("collection view drop"); + const promises: Promise<void>[] = []; + // tslint:disable-next-line:prefer-for-of + for (let i = 0; i < length; i++) { + const item = e.dataTransfer.items[i]; + if (item.kind === "string" && item.type.indexOf("uri") !== -1) { + let str: string; + const prom = new Promise<string>(resolve => item.getAsString(resolve)) + .then(action((s: string) => rp.head(Utils.CorsProxy(str = s)))) + .then(result => { + const type = result["content-type"]; + if (type) { + Docs.Get.DocumentFromType(type, str, options) + .then(doc => doc && this.props.addDocument(doc)); + } + }); + promises.push(prom); } + const type = item.type; + if (item.kind === "file") { + const file = item.getAsFile(); + const formData = new FormData(); + + if (!file || !file.type) { + continue; + } - formData.append('file', file); - const dropFileName = file ? file.name : "-empty-"; - promises.push(Networking.PostFormDataToServer("/uploadFormData", formData).then(results => { - results.map(action((result: any) => { - const { accessPaths, nativeWidth, nativeHeight, contentSize } = result; - const full = { ...options, _width: 300, title: dropFileName }; - const pathname = Utils.prepend(accessPaths.agnostic.client); - Docs.Get.DocumentFromType(type, pathname, full).then(doc => { - if (doc) { - const proto = Doc.GetProto(doc); - proto.fileUpload = basename(pathname).replace("upload_", "").replace(/\.[a-z0-9]*$/, ""); - nativeWidth && (proto["data-nativeWidth"] = nativeWidth); - nativeHeight && (proto["data-nativeHeight"] = nativeHeight); - contentSize && (proto.contentSize = contentSize); - this.props?.addDocument(doc); + formData.append('file', file); + const dropFileName = file ? file.name : "-empty-"; + promises.push(Networking.PostFormDataToServer("/uploadFormData", formData).then(results => { + results.map(action((result: any) => { + const { accessPaths, nativeWidth, nativeHeight, contentSize } = result; + if (Object.keys(accessPaths).length) { + const full = { ...options, _width: 300, title: dropFileName }; + const pathname = Utils.prepend(accessPaths.agnostic.client); + Docs.Get.DocumentFromType(type, pathname, full).then(doc => { + if (doc) { + const proto = Doc.GetProto(doc); + proto.fileUpload = basename(pathname).replace("upload_", "").replace(/\.[a-z0-9]*$/, ""); + nativeWidth && (proto["data-nativeWidth"] = nativeWidth); + nativeHeight && (proto["data-nativeHeight"] = nativeHeight); + contentSize && (proto.contentSize = contentSize); + this.props?.addDocument(doc); + } + }); + } else { + alert("Upload failed..."); } - }); + })); })); - })); + } } - } - if (promises.length) { - Promise.all(promises).finally(() => { completed && completed(); batch.end(); }); - } else { - if (text && !text.includes("https://")) { - this.props.addDocument(Docs.Create.TextDocument(text, { ...options, _width: 400, _height: 315 })); + if (promises.length) { + Promise.all(promises).finally(() => { completed && completed(); batch.end(); }); + } else { + if (text && !text.includes("https://")) { + this.props.addDocument(Docs.Create.TextDocument(text, { ...options, _width: 400, _height: 315 })); + } + batch.end(); } - batch.end(); + } else { + alert("No uploadable content found."); } } } diff --git a/src/client/views/collections/CollectionTimeView.scss b/src/client/views/collections/CollectionTimeView.scss index 2dffb3ea0..6ea5e6908 100644 --- a/src/client/views/collections/CollectionTimeView.scss +++ b/src/client/views/collections/CollectionTimeView.scss @@ -67,6 +67,7 @@ pointer-events: all; padding: 5px; border: 1px solid black; + display:none; } .collectionTimeView-treeView { @@ -131,4 +132,10 @@ .collectionFreeform-customText { text-align: center; } +} + +.collectionTimeView:hover, .collectionTimeView-pivot:hover { + .pivotKeyEntry { + display:unset; + } }
\ No newline at end of file diff --git a/src/client/views/collections/CollectionTreeView.scss b/src/client/views/collections/CollectionTreeView.scss index 2fa6813d7..6ebe81545 100644 --- a/src/client/views/collections/CollectionTreeView.scss +++ b/src/client/views/collections/CollectionTreeView.scss @@ -63,7 +63,9 @@ font-size: 8pt; margin-left: 3px; display: none; - background: lightgray; +} +.collectionTreeView-keyHeader:hover { + background: #797777; } .collectionTreeView-subtitle { @@ -84,9 +86,11 @@ .treeViewItem-openRight { display: none; height: 17px; - background: gray; width: 15px; } +.treeViewItem-openRight:hover { + background: #797777; +} .treeViewItem-border { display: inherit; @@ -101,7 +105,6 @@ .treeViewItem-openRight { display: inline-block; height: 17px; - background: #a8a7a7; width: 15px; // display: inline; diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index be2947dff..e2b3cc425 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -3,7 +3,7 @@ import { faAngleRight, faArrowsAltH, faBell, faCamera, faCaretDown, faCaretRight import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, observable, runInAction, untracked } from "mobx"; import { observer } from "mobx-react"; -import { Doc, DocListCast, Field, HeightSym, WidthSym } from '../../../new_fields/Doc'; +import { Doc, DocListCast, Field, HeightSym, WidthSym, DataSym, Opt } from '../../../new_fields/Doc'; import { Id } from '../../../new_fields/FieldSymbols'; import { List } from '../../../new_fields/List'; import { Document, listSpec } from '../../../new_fields/Schema'; @@ -56,12 +56,13 @@ export interface TreeViewProps { indentDocument?: () => void; outdentDocument?: () => void; ScreenToLocalTransform: () => Transform; + backgroundColor?: (doc: Doc) => string | undefined; outerXf: () => { translateX: number, translateY: number }; treeViewId: Doc; parentKey: string; active: (outsideReaction?: boolean) => boolean; treeViewHideHeaderFields: () => boolean; - preventTreeViewOpen: boolean; + treeViewPreventOpen: boolean; renderedIds: string[]; onCheckedClick?: ScriptField; } @@ -84,11 +85,10 @@ library.add(faPlus, faMinus); * * special fields: * treeViewOpen : flag denoting whether the documents sub-tree (contents) is visible or hidden - * preventTreeViewOpen : ignores the treeViewOpen flag (for allowing a view to not be slaved to other views of the document) + * treeViewPreventOpen : ignores the treeViewOpen flag (for allowing a view to not be slaved to other views of the document) * treeViewExpandedView : name of field whose contents are being displayed as the document's subtree */ class TreeView extends React.Component<TreeViewProps> { - static loadId = ""; private _header?: React.RefObject<HTMLDivElement> = React.createRef(); private _treedropDisposer?: DragManager.DragDropDisposer; private _dref = React.createRef<HTMLDivElement>(); @@ -97,8 +97,8 @@ class TreeView extends React.Component<TreeViewProps> { get defaultExpandedView() { return this.childDocs ? this.fieldKey : StrCast(this.props.document.defaultExpandedView, "fields"); } @observable _overrideTreeViewOpen = false; // override of the treeViewOpen field allowing the display state to be independent of the document's state - set treeViewOpen(c: boolean) { if (this.props.preventTreeViewOpen) this._overrideTreeViewOpen = c; else this.props.document.treeViewOpen = this._overrideTreeViewOpen = c; } - @computed get treeViewOpen() { return (!this.props.preventTreeViewOpen && BoolCast(this.props.document.treeViewOpen)) || this._overrideTreeViewOpen; } + set treeViewOpen(c: boolean) { if (this.props.treeViewPreventOpen) this._overrideTreeViewOpen = c; else this.props.document.treeViewOpen = this._overrideTreeViewOpen = c; } + @computed get treeViewOpen() { return (!this.props.treeViewPreventOpen && BoolCast(this.props.document.treeViewOpen)) || this._overrideTreeViewOpen; } @computed get treeViewExpandedView() { return StrCast(this.props.document.treeViewExpandedView, this.defaultExpandedView); } @computed get MAX_EMBED_HEIGHT() { return NumCast(this.props.document.maxEmbedHeight, 300); } @computed get dataDoc() { return this.templateDataDoc ? this.templateDataDoc : this.props.document; } @@ -171,7 +171,7 @@ class TreeView extends React.Component<TreeViewProps> { editableView = (key: string, style?: string) => (<EditableView oneLine={true} display={"inline-block"} - editing={this.dataDoc[Id] === TreeView.loadId} + editing={this.dataDoc[Id] === EditableView.loadId} contents={StrCast(this.props.document[key])} height={12} fontStyle={style} @@ -180,18 +180,17 @@ class TreeView extends React.Component<TreeViewProps> { SetValue={undoBatch((value: string) => Doc.SetInPlace(this.props.document, key, value, false) || true)} OnFillDown={undoBatch((value: string) => { Doc.SetInPlace(this.props.document, key, value, false); - const layoutDoc = this.props.document.layout_custom instanceof Doc ? Doc.ApplyTemplate(Doc.GetProto(this.props.document.layout_custom)) : undefined; - const doc = layoutDoc || Docs.Create.FreeformDocument([], { title: "", x: 0, y: 0, _width: 100, _height: 25, templates: new List<string>([Templates.Title.Layout]) }); - TreeView.loadId = doc[Id]; + const doc = Docs.Create.FreeformDocument([], { title: "", x: 0, y: 0, _width: 100, _height: 25, templates: new List<string>([Templates.Title.Layout]) }); + EditableView.loadId = doc[Id]; return this.props.addDocument(doc); })} OnTab={undoBatch((shift?: boolean) => { - TreeView.loadId = this.dataDoc[Id]; + EditableView.loadId = this.dataDoc[Id]; shift ? this.props.outdentDocument?.() : this.props.indentDocument?.(); setTimeout(() => { // unsetting/setting brushing for this doc will recreate & refocus this editableView after all other treeview changes have been made to the Dom (which may remove focus from this document). Doc.UnBrushDoc(this.props.document); Doc.BrushDoc(this.props.document); - TreeView.loadId = ""; + EditableView.loadId = ""; }, 0); })} />) @@ -291,8 +290,8 @@ class TreeView extends React.Component<TreeViewProps> { const addDoc = (doc: Doc, addBefore?: Doc, before?: boolean) => Doc.AddDocToList(this.dataDoc, key, doc, addBefore, before, false, true); contentElement = TreeView.GetChildElements(contents instanceof Doc ? [contents] : DocListCast(contents), this.props.treeViewId, doc, undefined, key, this.props.containingCollection, this.props.prevSibling, addDoc, remDoc, this.move, - this.props.dropAction, this.props.addDocTab, this.props.pinToPres, this.props.ScreenToLocalTransform, this.props.outerXf, this.props.active, - this.props.panelWidth, this.props.ChromeHeight, this.props.renderDepth, this.props.treeViewHideHeaderFields, this.props.preventTreeViewOpen, + this.props.dropAction, this.props.addDocTab, this.props.pinToPres, this.props.backgroundColor, this.props.ScreenToLocalTransform, this.props.outerXf, this.props.active, + this.props.panelWidth, this.props.ChromeHeight, this.props.renderDepth, this.props.treeViewHideHeaderFields, this.props.treeViewPreventOpen, [...this.props.renderedIds, doc[Id]], this.props.libraryPath, this.props.onCheckedClick); } else { contentElement = <EditableView @@ -334,8 +333,8 @@ class TreeView extends React.Component<TreeViewProps> { {!docs ? (null) : TreeView.GetChildElements(docs, this.props.treeViewId, Doc.Layout(this.props.document), this.templateDataDoc, expandKey, this.props.containingCollection, this.props.prevSibling, addDoc, remDoc, this.move, - this.props.dropAction, this.props.addDocTab, this.props.pinToPres, this.props.ScreenToLocalTransform, - this.props.outerXf, this.props.active, this.props.panelWidth, this.props.ChromeHeight, this.props.renderDepth, this.props.treeViewHideHeaderFields, this.props.preventTreeViewOpen, + this.props.dropAction, this.props.addDocTab, this.props.pinToPres, this.props.backgroundColor, this.props.ScreenToLocalTransform, + this.props.outerXf, this.props.active, this.props.panelWidth, this.props.ChromeHeight, this.props.renderDepth, this.props.treeViewHideHeaderFields, this.props.treeViewPreventOpen, [...this.props.renderedIds, this.props.document[Id]], this.props.libraryPath, this.props.onCheckedClick)} </ul >; } else if (this.treeViewExpandedView === "fields") { @@ -350,6 +349,7 @@ class TreeView extends React.Component<TreeViewProps> { DataDocument={this.templateDataDoc} LibraryPath={emptyPath} renderDepth={this.props.renderDepth + 1} + backgroundColor={this.props.backgroundColor} fitToBox={this.boundsOfCollectionDocument !== undefined} PanelWidth={this.docWidth} PanelHeight={this.docHeight} @@ -386,7 +386,7 @@ class TreeView extends React.Component<TreeViewProps> { @computed get renderBullet() { const checked = this.props.document.type === DocumentType.COL ? undefined : this.props.onCheckedClick ? (this.props.document.treeViewChecked ? this.props.document.treeViewChecked : "unchecked") : undefined; - return <div className="bullet" title="view inline" onClick={this.bulletClick} style={{ color: StrCast(this.props.document.color, checked === "unchecked" ? "white" : "black"), opacity: 0.4 }}> + return <div className="bullet" title="view inline" onClick={this.bulletClick} style={{ color: StrCast(this.props.document.color, checked === "unchecked" ? "white" : "inherit"), opacity: 0.4 }}> {<FontAwesomeIcon icon={checked === "check" ? "check" : (checked === "x" ? "times" : checked === "unchecked" ? "square" : !this.treeViewOpen ? (this.childDocs ? "caret-square-right" : "caret-right") : (this.childDocs ? "caret-square-down" : "caret-down"))} />} </div>; } @@ -417,7 +417,7 @@ class TreeView extends React.Component<TreeViewProps> { return <> <div className="docContainer" title="click to edit title" id={`docContainer-${this.props.parentKey}`} ref={reference} onPointerDown={onItemDown} style={{ - color: this.props.document.isMinimized ? "red" : "black", + color: this.props.document.isMinimized ? "red" : "inherit", background: Doc.IsHighlighted(this.props.document) ? "orange" : Doc.IsBrushed(this.props.document) ? "#06121212" : "0", fontWeight: this.props.document.searchMatch ? "bold" : undefined, outline: BoolCast(this.props.document.workspaceBrush) ? "dashed 1px #06123232" : undefined, @@ -458,6 +458,7 @@ class TreeView extends React.Component<TreeViewProps> { dropAction: dropActionType, addDocTab: (doc: Doc, dataDoc: Doc | undefined, where: string) => boolean, pinToPres: (document: Doc) => void, + backgroundColor: undefined | ((document: Doc) => string | undefined), screenToLocalXf: () => Transform, outerXf: () => { translateX: number, translateY: number }, active: (outsideReaction?: boolean) => boolean, @@ -465,7 +466,7 @@ class TreeView extends React.Component<TreeViewProps> { ChromeHeight: undefined | (() => number), renderDepth: number, treeViewHideHeaderFields: () => boolean, - preventTreeViewOpen: boolean, + treeViewPreventOpen: boolean, renderedIds: string[], libraryPath: Doc[] | undefined, onCheckedClick: ScriptField | undefined @@ -563,6 +564,7 @@ class TreeView extends React.Component<TreeViewProps> { renderDepth={renderDepth} deleteDoc={remove} addDocument={addDocument} + backgroundColor={backgroundColor} panelWidth={rowWidth} panelHeight={rowHeight} ChromeHeight={ChromeHeight} @@ -575,7 +577,7 @@ class TreeView extends React.Component<TreeViewProps> { parentKey={key} active={active} treeViewHideHeaderFields={treeViewHideHeaderFields} - preventTreeViewOpen={preventTreeViewOpen} + treeViewPreventOpen={treeViewPreventOpen} renderedIds={renderedIds} />; }); } @@ -602,13 +604,24 @@ export class CollectionTreeView extends CollectionSubView(Document) { @action remove = (document: Document): boolean => { - const children = Cast(this.props.Document[this.props.fieldKey], listSpec(Doc), []); + const children = Cast(this.props.Document[DataSym][this.props.fieldKey], listSpec(Doc), []); if (children.indexOf(document) !== -1) { children.splice(children.indexOf(document), 1); return true; } return false; } + @action + addDoc = (doc: Document, relativeTo: Opt<Doc>, before?: boolean): boolean => { + const doAddDoc = () => + Doc.AddDocToList(this.props.Document[DataSym], this.props.fieldKey, doc, relativeTo, before, false, false, false); + if (this.props.Document.resolvedDataDoc instanceof Promise) { + this.props.Document.resolvedDataDoc.then(resolved => doAddDoc()); + } else { + doAddDoc(); + } + return true; + } onContextMenu = (e: React.MouseEvent): void => { // need to test if propagation has stopped because GoldenLayout forces a parallel react hierarchy to be created for its top-level layout if (!e.isPropagationStopped() && this.props.Document === CurrentUserUtils.UserDocument.workspaces) { @@ -624,7 +637,7 @@ export class CollectionTreeView extends CollectionSubView(Document) { ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15); } else { const layoutItems: ContextMenuProps[] = []; - layoutItems.push({ description: (this.props.Document.preventTreeViewOpen ? "Persist" : "Abandon") + "Treeview State", event: () => this.props.Document.preventTreeViewOpen = !this.props.Document.preventTreeViewOpen, icon: "paint-brush" }); + layoutItems.push({ description: (this.props.Document.treeViewPreventOpen ? "Persist" : "Abandon") + "Treeview State", event: () => this.props.Document.treeViewPreventOpen = !this.props.Document.treeViewPreventOpen, icon: "paint-brush" }); layoutItems.push({ description: (this.props.Document.treeViewHideHeaderFields ? "Show" : "Hide") + " Header Fields", event: () => this.props.Document.treeViewHideHeaderFields = !this.props.Document.treeViewHideHeaderFields, icon: "paint-brush" }); layoutItems.push({ description: (this.props.Document.treeViewHideTitle ? "Show" : "Hide") + " Title", event: () => this.props.Document.treeViewHideTitle = !this.props.Document.treeViewHideTitle, icon: "paint-brush" }); ContextMenu.Instance.addItem({ description: "Treeview Options ...", subitems: layoutItems, icon: "eye" }); @@ -656,8 +669,8 @@ export class CollectionTreeView extends CollectionSubView(Document) { const heroView = ImageDocument(fallbackImg, { title: "heroView", isTemplateDoc: true, isTemplateForField: "hero", }); // this acts like a template doc and a template field ... a little weird, but seems to work? heroView.proto!.layout = ImageBox.LayoutString("hero"); - heroView.showTitle = "title"; - heroView.showTitleHover = "titlehover"; + heroView._showTitle = "title"; + heroView._showTitleHover = "titlehover"; Doc.AddDocToList(CurrentUserUtils.UserDocument.expandingButtons as Doc, "data", Docs.Create.FontIconDocument({ @@ -701,17 +714,18 @@ export class CollectionTreeView extends CollectionSubView(Document) { render() { const dropAction = StrCast(this.props.Document.dropAction) as dropActionType; - const addDoc = (doc: Doc, relativeTo?: Doc, before?: boolean) => Doc.AddDocToList(this.props.Document, this.props.fieldKey, doc, relativeTo, before, false, false, false); + const addDoc = (doc: Doc, relativeTo?: Doc, before?: boolean) => this.addDoc(doc, relativeTo, before); const moveDoc = (d: Doc, target: Doc | undefined, addDoc: (doc: Doc) => boolean) => this.props.moveDocument(d, target, addDoc); return !this.childDocs ? (null) : ( <div className="collectionTreeView-dropTarget" id="body" - style={{ background: StrCast(this.props.Document.backgroundColor, "lightgray"), paddingTop: `${NumCast(this.props.Document._yMargin, 20)}px` }} + style={{ background: this.props.backgroundColor?.(this.props.Document), paddingTop: `${NumCast(this.props.Document._yMargin, 20)}px` }} onContextMenu={this.onContextMenu} onWheel={(e: React.WheelEvent) => this._mainEle && this._mainEle.scrollHeight > this._mainEle.clientHeight && e.stopPropagation()} onDrop={this.onTreeDrop} ref={this.createTreeDropTarget}> {(this.props.Document.treeViewHideTitle ? (null) : <EditableView contents={this.dataDoc.title} + editing={false} display={"block"} maxHeight={72} height={"auto"} @@ -719,18 +733,17 @@ export class CollectionTreeView extends CollectionSubView(Document) { SetValue={undoBatch((value: string) => Doc.SetInPlace(this.dataDoc, "title", value, false) || true)} OnFillDown={undoBatch((value: string) => { Doc.SetInPlace(this.dataDoc, "title", value, false); - const layoutDoc = this.props.Document.layout_custom instanceof Doc ? Doc.ApplyTemplate(Doc.GetProto(this.props.Document.layout_custom)) : undefined; - const doc = layoutDoc || Docs.Create.FreeformDocument([], { title: "", x: 0, y: 0, _width: 100, _height: 25, templates: new List<string>([Templates.Title.Layout]) }); - TreeView.loadId = doc[Id]; - Doc.AddDocToList(this.props.Document, this.props.fieldKey, doc, this.childDocs.length ? this.childDocs[0] : undefined, true, false, false, false); + const doc = Docs.Create.FreeformDocument([], { title: "", x: 0, y: 0, _width: 100, _height: 25, templates: new List<string>([Templates.Title.Layout]) }); + EditableView.loadId = doc[Id]; + this.addDoc(doc, this.childDocs.length ? this.childDocs[0] : undefined, true); })} />)} {this.props.Document.allowClear ? this.renderClearButton : (null)} <ul className="no-indent" style={{ width: "max-content" }} > { TreeView.GetChildElements(this.childDocs, this.props.Document, this.props.Document, this.props.DataDoc, this.props.fieldKey, this.props.ContainingCollectionDoc, undefined, addDoc, this.remove, - moveDoc, dropAction, this.props.addDocTab, this.props.pinToPres, this.props.ScreenToLocalTransform, + moveDoc, dropAction, this.props.addDocTab, this.props.pinToPres, this.props.backgroundColor, this.props.ScreenToLocalTransform, this.outerXf, this.props.active, this.props.PanelWidth, this.props.ChromeHeight, this.props.renderDepth, () => BoolCast(this.props.Document.treeViewHideHeaderFields), - BoolCast(this.props.Document.preventTreeViewOpen), [], this.props.LibraryPath, ScriptCast(this.props.Document.onCheckedClick)) + BoolCast(this.props.Document.treeViewPreventOpen), [], this.props.LibraryPath, ScriptCast(this.props.Document.onCheckedClick)) } </ul> </div > diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index de3e9737f..be971eda6 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -38,6 +38,7 @@ import { CollectionViewBaseChrome } from './CollectionViewChromes'; import { CollectionTimeView } from './CollectionTimeView'; import { CollectionMultirowView } from './collectionMulticolumn/CollectionMultirowView'; import { List } from '../../../new_fields/List'; +import { SubCollectionViewProps } from './CollectionSubView'; export const COLLECTION_BORDER_WIDTH = 2; const path = require('path'); library.add(faTh, faTree, faSquare, faProjectDiagram, faSignature, faThList, faFingerprint, faColumns, faEllipsisV, faImage, faEye as any, faCopy); @@ -133,7 +134,7 @@ export class CollectionView extends Touchable<FieldViewProps> { @action.bound addDocument(doc: Doc): boolean { - const targetDataDoc = this.props.Document.resolvedDataDoc && !this.props.Document.isTemplateForField ? this.props.Document : Doc.GetProto(this.props.Document[DataSym]); + const targetDataDoc = this.props.Document[DataSym]; targetDataDoc[this.props.fieldKey] = new List<Doc>([...DocListCast(targetDataDoc[this.props.fieldKey]), doc]); // DocAddToList may write to targetdataDoc's parent ... we don't want this. should really change GetProto to GetDataDoc and test for resolvedDataDoc there // Doc.AddDocToList(targetDataDoc, this.props.fieldKey, doc); targetDataDoc[this.props.fieldKey + "-lastModified"] = new DateField(new Date(Date.now())); @@ -145,8 +146,7 @@ export class CollectionView extends Touchable<FieldViewProps> { removeDocument(doc: Doc): boolean { const docView = DocumentManager.Instance.getDocumentView(doc, this.props.ContainingCollectionView); docView && SelectionManager.DeselectDoc(docView); - const targetDataDoc = this.props.Document.resolvedDataDoc ? this.props.Document : this.props.Document[DataSym]; - const value = Cast(targetDataDoc[this.props.fieldKey], listSpec(Doc), []); + const value = Cast(this.props.Document[DataSym][this.props.fieldKey], listSpec(Doc), []); let index = value.reduce((p, v, i) => (v instanceof Doc && v === doc) ? i : p, -1); index = index !== -1 ? index : value.reduce((p, v, i) => (v instanceof Doc && Doc.AreProtosEqual(v, doc)) ? i : p, -1); @@ -178,7 +178,7 @@ export class CollectionView extends Touchable<FieldViewProps> { } private SubViewHelper = (type: CollectionViewType, renderProps: CollectionRenderProps) => { - const props = { ...this.props, ...renderProps, ChromeHeight: this.chromeHeight, CollectionView: this, annotationsKey: "" }; + const props: SubCollectionViewProps = { ...this.props, ...renderProps, ChromeHeight: this.chromeHeight, CollectionView: this, annotationsKey: "" }; switch (type) { case CollectionViewType.Schema: return (<CollectionSchemaView key="collview" {...props} />); case CollectionViewType.Docking: return (<CollectionDockingView key="collview" {...props} />); diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx index 050ca8347..d363770bf 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx @@ -44,7 +44,9 @@ export interface ViewDefResult { } function toLabel(target: FieldResult<Field>) { if (typeof target === "number" || Number(target)) { - return Number(target).toFixed(2).toString(); + const truncated = Number(Number(target).toFixed(0)); + const precise = Number(Number(target).toFixed(2)); + return truncated === precise ? Number(target).toFixed(0) : Number(target).toFixed(2); } if (target instanceof ObjectField || target instanceof RefField) { return target[ToString](); @@ -287,7 +289,7 @@ export function computeTimelineLayout( groupNames.push({ type: "text", text: Math.ceil(maxTime).toString(), x: Math.ceil(maxTime - minTime) * scaling, y: 0, height: fontHeight, fontSize, payload: undefined }); } - const divider = { type: "div", color: "black", x: 0, y: 0, width: panelDim[0], height: 1, payload: undefined }; + const divider = { type: "div", color: "black", x: 0, y: 0, width: panelDim[0], height: -1, payload: undefined }; return normalizeResults(panelDim, fontHeight, childPairs, docMap, poolData, viewDefsToJSX, groupNames, (maxTime - minTime) * scaling, [divider], childDocs.filter(c => !filterDocs.includes(c))); function layoutDocsAtTime(keyDocs: Doc[], key: number) { @@ -303,7 +305,7 @@ export function computeTimelineLayout( docMap.set(doc, { type: "doc", x: x, y: -Math.sqrt(stack) * pivotAxisWidth / 2 - pivotAxisWidth + (pivotAxisWidth - hgt) / 2, - zIndex: (curTime === key ? 1000 : zind++), highlight: curTime === key, width: wid / (Math.max(stack, 1)), height: hgt, payload: undefined + zIndex: (curTime === key ? 1000 : zind++), highlight: curTime === key, width: wid / (Math.max(stack, 1)), height: hgt / (Math.max(stack, 1)), payload: undefined }); stacking[stack] = x + pivotAxisWidth; }); @@ -340,17 +342,17 @@ function normalizeResults(panelDim: number[], fontHeight: number, childPairs: { extraDocs.map(ed => poolData.set(ed[Id], { x: 0, y: 0, zIndex: -99 })); return { - elements: viewDefsToJSX(extras.concat(groupNames.map(gname => ({ + elements: viewDefsToJSX(extras.concat(groupNames).map(gname => ({ type: gname.type, text: gname.text, x: gname.x * scale, y: gname.y * scale, color: gname.color, width: gname.width === undefined ? undefined : gname.width * scale, - height: Math.max(fontHeight, (gname.height || 0) * scale), + height: gname.height === -1 ? 1 : Math.max(fontHeight, (gname.height || 0) * scale), fontSize: gname.fontSize, payload: gname.payload - })))) + }))) }; } diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 07a5a2c7b..969d6b3c8 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -4,13 +4,13 @@ import { faBraille, faChalkboard, faCompass, faCompressArrowsAlt, faExpandArrows import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; import { computedFn } from "mobx-utils"; -import { Doc, DocListCast, HeightSym, Opt, WidthSym } from "../../../../new_fields/Doc"; +import { Doc, DocListCast, HeightSym, Opt, WidthSym, DocCastAsync } from "../../../../new_fields/Doc"; import { documentSchema, positionSchema } from "../../../../new_fields/documentSchemas"; import { Id } from "../../../../new_fields/FieldSymbols"; import { InkTool } from "../../../../new_fields/InkField"; import { createSchema, listSpec, makeInterface } from "../../../../new_fields/Schema"; import { ScriptField } from "../../../../new_fields/ScriptField"; -import { Cast, NumCast, ScriptCast, StrCast } from "../../../../new_fields/Types"; +import { Cast, NumCast, ScriptCast, BoolCast, StrCast } from "../../../../new_fields/Types"; import { TraceMobx } from "../../../../new_fields/util"; import { GestureUtils } from "../../../../pen-gestures/GestureUtils"; import { CurrentUserUtils } from "../../../../server/authentication/models/current_user_utils"; @@ -206,6 +206,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { } @undoBatch + @action updateClusters(useClusters: boolean) { this.props.Document.useClusters = useClusters; this._clusterSets.length = 0; @@ -243,7 +244,6 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { docs.map(doc => this._clusterSets[doc.cluster = NumCast(docFirst.cluster)].push(doc)); } childLayouts.map(child => !this._clusterSets.some((set, i) => Doc.IndexOf(child, set) !== -1 && child.cluster === i) && this.updateCluster(child)); - childLayouts.map(child => Doc.GetProto(child).clusterStr = child.cluster?.toString()); } } @@ -279,16 +279,16 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { } getClusterColor = (doc: Doc) => { - let clusterColor = ""; + let clusterColor = this.props.backgroundColor?.(doc); const cluster = NumCast(doc.cluster); if (this.Document.useClusters) { if (this._clusterSets.length <= cluster) { setTimeout(() => this.updateCluster(doc), 0); } else { // choose a cluster color from a palette - const colors = ["#da42429e", "#31ea318c", "#8c4000", "#4a7ae2c4", "#d809ff", "#ff7601", "#1dffff", "yellow", "#1b8231f2", "#000000ad"]; + const colors = ["#da42429e", "#31ea318c", "rgba(197, 87, 20, 0.55)", "#4a7ae2c4", "rgba(216, 9, 255, 0.5)", "#ff7601", "#1dffff", "yellow", "rgba(27, 130, 49, 0.55)", "rgba(0, 0, 0, 0.268)"]; clusterColor = colors[cluster % colors.length]; - const set = this._clusterSets[cluster] && this._clusterSets[cluster].filter(s => s.backgroundColor && (s.backgroundColor !== s.defaultBackgroundColor)); + const set = this._clusterSets[cluster]?.filter(s => s.backgroundColor); // override the cluster color with an explicitly set color on a non-background document. then override that with an explicitly set color on a background document set && set.filter(s => !s.isBackground).map(s => clusterColor = StrCast(s.backgroundColor)); set && set.filter(s => s.isBackground).map(s => clusterColor = StrCast(s.backgroundColor)); @@ -697,7 +697,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { } - setScaleToZoom = (doc: Doc, scale: number = 0.5) => { + setScaleToZoom = (doc: Doc, scale: number = 0.75) => { this.Document.scale = scale * Math.min(this.props.PanelWidth() / NumCast(doc._width), this.props.PanelHeight() / NumCast(doc._height)); } @@ -709,6 +709,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { @computed get libraryPath() { return this.props.LibraryPath ? [...this.props.LibraryPath, this.props.Document] : []; } @computed get onChildClickHandler() { return ScriptCast(this.Document.onChildClick); } + backgroundHalo = () => BoolCast(this.Document.useClusters); getChildDocumentViewProps(childLayout: Doc, childData?: Doc): DocumentViewProps { return { @@ -728,6 +729,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { ContainingCollectionDoc: this.props.Document, focus: this.focusDocument, backgroundColor: this.getClusterColor, + backgroundHalo: this.backgroundHalo, parentActive: this.props.active, bringToFront: this.bringToFront, zoomToScale: this.zoomToScale, @@ -867,6 +869,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { }); return rangeFilteredDocs; } + childLayoutDocFunc = () => this.props.childLayoutTemplate?.() || Cast(this.props.Document.childLayoutTemplate, Doc, null) as Doc; get doLayoutComputation() { const { newPool, computedElementData } = this.doInternalLayoutComputation; runInAction(() => @@ -883,6 +886,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { computedElementData.elements.push({ ele: <CollectionFreeFormDocumentView key={pair.layout[Id]} {...this.getChildDocumentViewProps(pair.layout, pair.data)} dataProvider={this.childDataProvider} + LayoutDoc={this.childLayoutDocFunc} jitterRotation={NumCast(this.props.Document.jitterRotation)} fitToBox={this.props.fitToBox || this.props.layoutEngine !== undefined} />, bounds: this.childDataProvider(pair.layout) @@ -1072,7 +1076,6 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { // this.Document.fitH = this.contentBounds && (this.contentBounds.b - this.contentBounds.y); // if isAnnotationOverlay is set, then children will be stored in the extension document for the fieldKey. // otherwise, they are stored in fieldKey. All annotations to this document are stored in the extension document - // let lodarea = this.Document[WidthSym]() * this.Document[HeightSym]() / this.props.ScreenToLocalTransform().Scale / this.props.ScreenToLocalTransform().Scale; return <div className={"collectionfreeformview-container"} ref={this.createDashEventsTarget} onWheel={this.onPointerWheel}//pointerEvents: SelectionManager.GetIsDragging() ? "all" : undefined, @@ -1084,7 +1087,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { width: this.contentScaling ? `${100 / this.contentScaling}%` : "", height: this.contentScaling ? `${100 / this.contentScaling}%` : this.isAnnotationOverlay ? (this.props.Document.scrollHeight ? this.Document.scrollHeight : "100%") : this.props.PanelHeight() }}> - {!this.Document._LODdisable && !this.props.active() && !this.props.isAnnotationOverlay && !this.props.annotationsKey && this.props.renderDepth > 0 ? // && this.props.CollectionView && lodarea < NumCast(this.Document.LODarea, 100000) ? + {!this.Document._LODdisable && !this.props.active() && !this.props.isAnnotationOverlay && !this.props.annotationsKey && this.props.renderDepth > 0 ? this.placeholder : this.marqueeView} <CollectionFreeFormOverlayView elements={this.elementFunc} /> </div>; diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index 4b0855635..85cda4ecb 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -308,8 +308,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque y: bounds.top, _panX: 0, _panY: 0, - backgroundColor: this.props.isAnnotationOverlay ? "#00000015" : "white", - defaultBackgroundColor: this.props.isAnnotationOverlay ? "#00000015" : "white", + backgroundColor: this.props.isAnnotationOverlay ? "#00000015" : undefined, _width: bounds.width, _height: bounds.height, _LODdisable: true, diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx index 7d8de0db4..db5673573 100644 --- a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx +++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx @@ -207,6 +207,7 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu {...this.props} Document={layout} DataDocument={layout.resolvedDataDoc as Doc} + backgroundColor={this.props.backgroundColor} CollectionDoc={this.props.Document} PanelWidth={width} PanelHeight={height} diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx index ec05443e5..630a178cf 100644 --- a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx +++ b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx @@ -208,6 +208,7 @@ export class CollectionMultirowView extends CollectionSubView(MultirowDocument) {...this.props} Document={layout} DataDocument={layout.resolvedDataDoc as Doc} + backgroundColor={this.props.backgroundColor} CollectionDoc={this.props.Document} PanelWidth={width} PanelHeight={height} diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index 3bceec45f..53d17b580 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -61,15 +61,12 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF } contentScaling = () => this.nativeWidth > 0 && !this.props.Document.ignoreAspect && !this.props.fitToBox ? this.width / this.nativeWidth : 1; - clusterColorFunc = (doc: Doc) => this.clusterColor; panelWidth = () => (this.dataProvider?.width || this.props.PanelWidth()); panelHeight = () => (this.dataProvider?.height || this.props.PanelHeight()); getTransform = (): Transform => this.props.ScreenToLocalTransform() .translate(-this.X, -this.Y) .scale(1 / this.contentScaling()) - @computed - get clusterColor() { return this.props.backgroundColor(this.props.Document); } focusDoc = (doc: Doc) => this.props.focus(doc, false); render() { TraceMobx(); @@ -78,13 +75,13 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF boxShadow: this.layoutDoc.opacity === 0 ? undefined : // if it's not visible, then no shadow this.layoutDoc.z ? `#9c9396 ${StrCast(this.layoutDoc.boxShadow, "10px 10px 0.9vw")}` : // if it's a floating doc, give it a big shadow - this.clusterColor ? (`${this.clusterColor} ${StrCast(this.layoutDoc.boxShadow, `0vw 0vw ${(this.layoutDoc.isBackground ? 100 : 50) / this.props.ContentScaling()}px`)}`) : // if it's just in a cluster, make the shadown roughly match the cluster border extent + this.props.backgroundHalo?.() ? (`${this.props.backgroundColor?.(this.props.Document)} ${StrCast(this.layoutDoc.boxShadow, `0vw 0vw ${(this.layoutDoc.isBackground ? 100 : 50) / this.props.ContentScaling()}px`)}`) : // if it's just in a cluster, make the shadown roughly match the cluster border extent this.layoutDoc.isBackground ? undefined : // if it's a background & has a cluster color, make the shadow spread really big StrCast(this.layoutDoc.boxShadow, ""), borderRadius: StrCast(Doc.Layout(this.layoutDoc).borderRounding), outline: this.Highlight ? "orange solid 2px" : "", transform: this.transform, - transition: this.Document.isAnimating ? ".5s ease-in" : this.props.transition ? this.props.transition : this.dataProvider ? this.dataProvider.transition : StrCast(this.layoutDoc.transition), + transition: this.props.transition ? this.props.transition : this.dataProvider ? this.dataProvider.transition : StrCast(this.layoutDoc.transition), width: this.width, height: this.height, zIndex: this.ZInd, @@ -96,7 +93,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF dragDivName={"collectionFreeFormDocumentView-container"} ContentScaling={this.contentScaling} ScreenToLocalTransform={this.getTransform} - backgroundColor={this.clusterColorFunc} + backgroundColor={this.props.backgroundColor} PanelWidth={this.panelWidth} PanelHeight={this.panelHeight} /> : <ContentFittingDocumentView {...this.props} diff --git a/src/client/views/nodes/ContentFittingDocumentView.tsx b/src/client/views/nodes/ContentFittingDocumentView.tsx index 671f5b96e..387da88f5 100644 --- a/src/client/views/nodes/ContentFittingDocumentView.tsx +++ b/src/client/views/nodes/ContentFittingDocumentView.tsx @@ -1,19 +1,17 @@ import React = require("react"); -import { action, computed } from "mobx"; +import { computed } from "mobx"; import { observer } from "mobx-react"; import "react-table/react-table.css"; import { Doc, Opt } from "../../../new_fields/Doc"; -import { ComputedField, ScriptField } from "../../../new_fields/ScriptField"; +import { ScriptField } from "../../../new_fields/ScriptField"; import { NumCast, StrCast } from "../../../new_fields/Types"; -import { emptyFunction, returnEmptyString, returnOne } from "../../../Utils"; -import { DragManager } from "../../util/DragManager"; +import { TraceMobx } from "../../../new_fields/util"; +import { emptyFunction, returnOne } from "../../../Utils"; import { Transform } from "../../util/Transform"; -import { undoBatch } from "../../util/UndoManager"; +import { CollectionView } from "../collections/CollectionView"; import '../DocumentDecorations.scss'; import { DocumentView } from "../nodes/DocumentView"; import "./ContentFittingDocumentView.scss"; -import { CollectionView } from "../collections/CollectionView"; -import { TraceMobx } from "../../../new_fields/util"; interface ContentFittingDocumentViewProps { Document?: Doc; @@ -29,6 +27,7 @@ interface ContentFittingDocumentViewProps { CollectionView?: CollectionView; CollectionDoc?: Doc; onClick?: ScriptField; + backgroundColor?: (doc: Doc) => string | undefined; getTransform: () => Transform; addDocument?: (document: Doc) => boolean; moveDocument?: (document: Doc, target: Doc | undefined, addDoc: ((doc: Doc) => boolean)) => boolean; @@ -55,20 +54,6 @@ export class ContentFittingDocumentView extends React.Component<ContentFittingDo } private contentScaling = () => this.scaling; - @undoBatch - @action - drop = (e: Event, de: DragManager.DropEvent) => { - const docDragData = de.complete.docDragData; - if (docDragData) { - this.props.childDocs && this.props.childDocs.map(otherdoc => { - const target = Doc.GetProto(otherdoc); - target.layout = ComputedField.MakeFunction("this.image_data[0]"); - target.layout_custom = Doc.MakeDelegate(docDragData.draggedDocuments[0]); - }); - e.stopPropagation(); - } - return true; - } private PanelWidth = () => this.panelWidth; private PanelHeight = () => this.panelHeight;; @@ -102,6 +87,7 @@ export class ContentFittingDocumentView extends React.Component<ContentFittingDo LibraryPath={this.props.LibraryPath} fitToBox={this.props.fitToBox} onClick={this.props.onClick} + backgroundColor={this.props.backgroundColor} addDocument={this.props.addDocument} removeDocument={this.props.removeDocument} moveDocument={this.props.moveDocument} @@ -117,7 +103,6 @@ export class ContentFittingDocumentView extends React.Component<ContentFittingDo PanelWidth={this.PanelWidth} PanelHeight={this.PanelHeight} focus={this.props.focus || emptyFunction} - backgroundColor={returnEmptyString} bringToFront={emptyFunction} dontRegisterView={this.props.dontRegisterView} zoomToScale={emptyFunction} diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 95858af4c..52928e8cb 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -1,6 +1,6 @@ import { library } from '@fortawesome/fontawesome-svg-core'; import * as fa from '@fortawesome/free-solid-svg-icons'; -import { action, computed, runInAction } from "mobx"; +import { action, computed, runInAction, observable } from "mobx"; import { observer } from "mobx-react"; import * as rp from "request-promise"; import { Doc, DocListCast, DocListCastAsync, Opt } from "../../../new_fields/Doc"; @@ -77,7 +77,8 @@ export interface DocumentViewProps { addDocTab: (doc: Doc, dataDoc: Doc | undefined, where: string, libraryPath?: Doc[]) => boolean; pinToPres: (document: Doc) => void; zoomToScale: (scale: number) => void; - backgroundColor: (doc: Doc) => string | undefined; + backgroundHalo?: () => boolean; + backgroundColor?: (doc: Doc) => string | undefined; getScale: () => number; animateBetweenIcon?: (maximize: boolean, target: number[]) => void; ChromeHeight?: () => number; @@ -92,7 +93,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu private _downY: number = 0; private _lastTap: number = 0; private _doubleTap = false; - private _hitTemplateDrag = false; private _mainCont = React.createRef<HTMLDivElement>(); private _dropDisposer?: DragManager.DragDropDisposer; private _gestureEventDisposer?: GestureUtils.GestureEventDisposer; @@ -196,14 +196,13 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu !this.props.dontRegisterView && DocumentManager.Instance.DocumentViews.splice(DocumentManager.Instance.DocumentViews.indexOf(this), 1); } - startDragging(x: number, y: number, dropAction: dropActionType, applyAsTemplate?: boolean) { + startDragging(x: number, y: number, dropAction: dropActionType) { if (this._mainCont.current) { const dragData = new DragManager.DocumentDragData([this.props.Document]); const [left, top] = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).inverse().transformPoint(0, 0); dragData.offset = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).transformDirection(x - left, y - top); dragData.dropAction = dropAction; dragData.moveDocument = this.props.moveDocument;// this.Document.onDragStart ? undefined : this.props.moveDocument; - dragData.applyAsTemplate = applyAsTemplate; dragData.dragDivName = this.props.dragDivName; this.props.Document.sourceContext = this.props.ContainingCollectionDoc; // bcz: !! shouldn't need this ... use search find the document's context dynamically DragManager.StartDocumentDrag([this._mainCont.current], dragData, x, y, { hideSource: !dropAction && !this.Document.onDragStart }); @@ -232,7 +231,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu e.stopPropagation(); e.preventDefault(); if (e.key === "†" || e.key === "t") { - if (!StrCast(this.layoutDoc.showTitle)) this.layoutDoc.showTitle = "title"; + if (!StrCast(this.layoutDoc._showTitle)) this.layoutDoc._showTitle = "title"; if (!this._titleRef.current) setTimeout(() => this._titleRef.current?.setIsFocused(true), 0); else if (!this._titleRef.current.setIsFocused(true)) { // if focus didn't change, focus on interior text... { @@ -257,8 +256,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu let preventDefault = true; if (this._doubleTap && this.props.renderDepth && !this.onClickHandler?.script) { // disable double-click to show full screen for things that have an on click behavior since clicking them twice can be misinterpreted as a double click const fullScreenAlias = Doc.MakeAlias(this.props.Document); - if (StrCast(fullScreenAlias.layoutKey) !== "layout_custom" && fullScreenAlias.layout_custom !== undefined) { - fullScreenAlias.layoutKey = "layout_custom"; + if (StrCast(fullScreenAlias.layoutKey) !== "layout_fullScreen" && fullScreenAlias.layout_fullScreen) { + fullScreenAlias.layoutKey = "layout_fullScreen"; } this.props.addDocTab(fullScreenAlias, undefined, "inTab"); SelectionManager.DeselectAll(); @@ -297,12 +296,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu this._downX = touch.clientX; this._downY = touch.clientY; if (!e.nativeEvent.cancelBubble) { - this._hitTemplateDrag = false; - for (let element = (e.target as any); element && !this._hitTemplateDrag; element = element.parentElement) { - if (element.className && element.className.toString() === "collectionViewBaseChrome-collapse") { - this._hitTemplateDrag = true; - } - } if ((this.active || this.Document.onDragStart || this.Document.onClick) && !e.ctrlKey && !this.Document.lockedPosition && !this.Document.inOverlay) e.stopPropagation(); this.removeMoveListeners(); this.addMoveListeners(); @@ -323,7 +316,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu if (Math.abs(this._downX - touch.clientX) > 3 || Math.abs(this._downY - touch.clientY) > 3) { if (!e.altKey && (!this.topMost || this.Document.onDragStart || this.Document.onClick)) { this.cleanUpInteractions(); - this.startDragging(this._downX, this._downY, this.Document.dropAction ? this.Document.dropAction as any : e.ctrlKey || e.altKey ? "alias" : undefined, this._hitTemplateDrag); + this.startDragging(this._downX, this._downY, this.Document.dropAction ? this.Document.dropAction as any : e.ctrlKey || e.altKey ? "alias" : undefined); } } 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 @@ -434,14 +427,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu if (!e.nativeEvent.cancelBubble || this.onClickHandler || this.Document.onDragStart) { this._downX = e.clientX; this._downY = e.clientY; - this._hitTemplateDrag = false; - // this whole section needs to move somewhere else. We're trying to initiate a special "template" drag where - // this document is the template and we apply it to whatever we drop it on. - for (let element = (e.target as any); element && !this._hitTemplateDrag; element = element.parentElement) { - if (element.className && element.className.toString() === "collectionViewBaseChrome-collapse") { - this._hitTemplateDrag = true; - } - } if ((this.active || this.Document.onDragStart || this.onClickHandler) && !e.ctrlKey && (e.button === 0 || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) && @@ -470,7 +455,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu if (!e.altKey && (!this.topMost || this.Document.onDragStart || this.onClickHandler) && (e.buttons === 1 || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE))) { document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); - this.startDragging(this._downX, this._downY, this.props.ContainingCollectionDoc?.childDropAction ? this.props.ContainingCollectionDoc?.childDropAction : this.Document.dropAction ? this.Document.dropAction as any : e.ctrlKey || e.altKey ? "alias" : undefined, this._hitTemplateDrag); + this.startDragging(this._downX, this._downY, this.props.ContainingCollectionDoc?.childDropAction ? this.props.ContainingCollectionDoc?.childDropAction : this.Document.dropAction ? this.Document.dropAction as any : e.ctrlKey || e.altKey ? "alias" : undefined); } } 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 @@ -557,17 +542,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } @undoBatch - makeSelBtnClicked = (): void => { - if (this.Document.isButton || this.Document.onClick || this.Document.ignoreClick) { - this.Document.isButton = false; - this.Document.ignoreClick = false; - this.Document.onClick = undefined; - } else { - this.props.Document.isButton = "Selector"; - } - } - - @undoBatch @action drop = async (e: Event, de: DragManager.DropEvent) => { if (de.complete.annoDragData) { @@ -578,10 +552,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu DocUtils.MakeLink({ doc: de.complete.annoDragData.annotationDocument }, { doc: this.props.Document, ctx: this.props.ContainingCollectionDoc }, `Link from ${StrCast(de.complete.annoDragData.annotationDocument.title)}`); } - if (de.complete.docDragData && de.complete.docDragData.applyAsTemplate) { - Doc.ApplyTemplateTo(de.complete.docDragData.draggedDocuments[0], this.props.Document, "layout_custom", undefined); - e.stopPropagation(); - } if (de.complete.linkDragData) { e.stopPropagation(); // const docs = await SearchUtil.Search(`data_l:"${destDoc[Id]}"`, true); @@ -694,10 +664,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu const existingOnClick = ContextMenu.Instance.findByDescription("OnClick..."); const onClicks: ContextMenuProps[] = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : []; onClicks.push({ description: "Enter Portal", event: this.makeIntoPortal, icon: "window-restore" }); - onClicks.push({ description: "Toggle Detail", event: () => this.Document.onClick = ScriptField.MakeScript("toggleDetail(this)"), icon: "window-restore" }); + onClicks.push({ description: "Toggle Detail", event: () => this.Document.onClick = ScriptField.MakeScript(`toggleDetail(this, "${this.props.Document.layoutKey}")`), icon: "window-restore" }); onClicks.push({ description: this.Document.ignoreClick ? "Select" : "Do Nothing", event: () => this.Document.ignoreClick = !this.Document.ignoreClick, icon: this.Document.ignoreClick ? "unlock" : "lock" }); onClicks.push({ description: this.Document.isButton || this.Document.onClick ? "Remove Click Behavior" : "Follow Link", event: this.makeBtnClicked, icon: "concierge-bell" }); - onClicks.push({ description: this.props.Document.isButton ? "Remove Select Link Behavior" : "Select Link", event: this.makeSelBtnClicked, icon: "concierge-bell" }); onClicks.push({ description: "Edit onClick Script", icon: "edit", event: (obj: any) => ScriptBox.EditButtonScript("On Button Clicked ...", this.props.Document, "onClick", obj.x, obj.y) }); !existingOnClick && cm.addItem({ description: "OnClick...", subitems: onClicks, icon: "hand-point-right" }); @@ -804,19 +773,11 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu }); } - // does Document set a layout prop - setsLayoutProp = (prop: string) => this.props.Document[prop] !== this.props.Document["default" + prop[0].toUpperCase() + prop.slice(1)] && this.props.Document["default" + prop[0].toUpperCase() + prop.slice(1)]; - // get the a layout prop by first choosing the prop from Document, then falling back to the layout doc otherwise. - getLayoutPropStr = (prop: string) => StrCast(this.setsLayoutProp(prop) ? this.props.Document[prop] : this.layoutDoc[prop]); - getLayoutPropNum = (prop: string) => NumCast(this.setsLayoutProp(prop) ? this.props.Document[prop] : this.layoutDoc[prop]); - isSelected = (outsideReaction?: boolean) => SelectionManager.IsSelected(this, outsideReaction); select = (ctrlPressed: boolean) => { SelectionManager.SelectDoc(this, ctrlPressed); }; chromeHeight = () => { - const showTitle = StrCast(this.layoutDoc.showTitle); - const showTitleHover = StrCast(this.layoutDoc.showTitleHover); - return (showTitle && !showTitleHover ? 0 : 0) + 1; + return 1; } @computed get finalLayoutKey() { @@ -874,9 +835,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu @computed get innards() { TraceMobx(); - const showTitle = StrCast(this.getLayoutPropStr("showTitle")); - const showTitleHover = StrCast(this.getLayoutPropStr("showTitleHover")); - const showCaption = this.getLayoutPropStr("showCaption"); + const showTitle = StrCast(this.layoutDoc._showTitle); + const showTitleHover = StrCast(this.layoutDoc._showTitleHover); + const showCaption = StrCast(this.layoutDoc._showCaption); const showTextTitle = showTitle && (StrCast(this.layoutDoc.layout).indexOf("PresBox") !== -1 || StrCast(this.layoutDoc.layout).indexOf("FormattedTextBox") !== -1) ? showTitle : undefined; const searchHighlight = (!this.Document.searchFields ? (null) : <div className="documentView-searchHighlight"> @@ -931,16 +892,25 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu return (this.Document.isBackground && !this.isSelected()) || (this.Document.type === DocumentType.INK && InkingControl.Instance.selectedTool !== InkTool.None); } + @observable _animate = 0; + switchViews = action((custom: boolean, view: string) => { + SelectionManager.SetIsDragging(true); + this._animate = 0.1; + setTimeout(action(() => { + this.setCustomView(custom, view); + this._animate = 1; + setTimeout(action(() => { + this._animate = 0; + SelectionManager.SetIsDragging(false); + }), 400); + }), 400); + }); + render() { if (!(this.props.Document instanceof Doc)) return (null); - const colorSet = this.setsLayoutProp("backgroundColor"); - const clusterCol = this.props.ContainingCollectionDoc && this.props.ContainingCollectionDoc.clusterOverridesDefaultBackground; - const backgroundColor = (clusterCol && !colorSet) ? - this.props.backgroundColor(this.Document) || StrCast(this.layoutDoc.backgroundColor) : - StrCast(this.layoutDoc.backgroundColor) || this.props.backgroundColor(this.Document); - + const backgroundColor = StrCast(this.layoutDoc.backgroundColor) || this.props.backgroundColor?.(this.Document); const fullDegree = Doc.isBrushedHighlightedDegree(this.props.Document); - const borderRounding = this.getLayoutPropStr("borderRounding"); + const borderRounding = this.layoutDoc.borderRounding; const localScale = fullDegree; const highlightColors = ["transparent", "maroon", "maroon", "yellow", "magenta", "cyan", "orange"]; @@ -951,9 +921,11 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} onClick={this.onClick} onPointerEnter={e => Doc.BrushDoc(this.props.Document)} onPointerLeave={e => Doc.UnBrushDoc(this.props.Document)} style={{ - transition: this.Document.isAnimating ? ".5s linear" : StrCast(this.Document.transition), + transformOrigin: this._animate ? "center center" : undefined, + transform: this._animate ? `scale(${this._animate})` : undefined, + transition: !this._animate ? StrCast(this.Document.transition) : this._animate < 1 ? "transform 0.5s ease-in" : "transform 0.5s ease-out", pointerEvents: this.ignorePointerEvents ? "none" : "all", - color: StrCast(this.Document.color), + color: StrCast(this.Document.color, "inherit"), outline: highlighting && !borderRounding ? `${highlightColors[fullDegree]} ${highlightStyles[fullDegree]} ${localScale}px` : "solid 0px", border: highlighting && borderRounding ? `${highlightStyles[fullDegree]} ${highlightColors[fullDegree]} ${localScale}px` : undefined, boxShadow: this.props.Document.isTemplateForField ? "black 0.2vw 0.2vw 0.8vw" : undefined, @@ -971,4 +943,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } } -Scripting.addGlobal(function toggleDetail(doc: any) { doc.layoutKey = StrCast(doc.layoutKey, "layout") === "layout" ? "layout_custom" : "layout"; });
\ No newline at end of file +Scripting.addGlobal(function toggleDetail(doc: any, layoutKey: string) { + const dv = DocumentManager.Instance.getDocumentView(doc); + if (dv?.props.Document.layoutKey === layoutKey) dv?.switchViews(false, ""); + else dv?.switchViews(true, layoutKey.replace("layout_", "")); +});
\ No newline at end of file diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index 8250f41f3..00f00dd52 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -3,14 +3,13 @@ import { computed } from "mobx"; import { observer } from "mobx-react"; import { DateField } from "../../../new_fields/DateField"; import { Doc, FieldResult, Opt } from "../../../new_fields/Doc"; -import { IconField } from "../../../new_fields/IconField"; import { List } from "../../../new_fields/List"; -import { AudioField, ImageField, VideoField } from "../../../new_fields/URLField"; +import { ScriptField } from "../../../new_fields/ScriptField"; +import { AudioField, VideoField } from "../../../new_fields/URLField"; import { Transform } from "../../util/Transform"; import { CollectionView } from "../collections/CollectionView"; import { AudioBox } from "./AudioBox"; import { VideoBox } from "./VideoBox"; -import { ScriptField } from "../../../new_fields/ScriptField"; // // these properties get assigned through the render() method of the DocumentView when it creates this node. @@ -34,6 +33,7 @@ export interface FieldViewProps { pinToPres: (document: Doc) => void; removeDocument?: (document: Doc) => boolean; moveDocument?: (document: Doc, targetCollection: Doc | undefined, addDocument: (document: Doc) => boolean) => boolean; + backgroundColor?: (document: Doc) => string | undefined; ScreenToLocalTransform: () => Transform; active: (outsideReaction?: boolean) => boolean; whenActiveChanged: (isActive: boolean) => void; diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index 7fbee8881..3c64b3974 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -186,7 +186,6 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & tsel.marks().filter(m => m.type === this._editorView!.state.schema.marks.user_mark).map(m => AudioBox.SetScrubTime(Math.max(0, m.attrs.modified * 5000 - 1000))); this._applyingChange = true; if (!this.props.Document._textTemplate || Doc.GetProto(this.props.Document) === this.dataDoc) { - this.dataDoc[this.props.fieldKey + "-lastModified"] && (this.dataDoc[this.props.fieldKey + "-backgroundColor"] = "lightGray"); this.dataDoc[this.props.fieldKey + "-lastModified"] = new DateField(new Date(Date.now())); this.dataDoc[this.props.fieldKey] = new RichTextField(JSON.stringify(state.toJSON()), state.doc.textBetween(0, state.doc.content.size, "\n\n")); } @@ -250,17 +249,6 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & Doc.GetProto(this.dataDoc)[this.props.fieldKey] = new RichTextField(draggedDoc.data.Data, draggedDoc.data.Text); e.stopPropagation(); } - // apply as template when dragging with Meta - } else if (draggedDoc && draggedDoc.type === DocumentType.TEXT && !Doc.AreProtosEqual(draggedDoc, this.props.Document) && de.metaKey) { - draggedDoc.isTemplateDoc = true; - let newLayout = Doc.Layout(draggedDoc); - if (typeof (draggedDoc.layout) === "string") { - newLayout = Doc.MakeDelegate(draggedDoc); - newLayout.layout = StrCast(newLayout.layout).replace(/fieldKey={'[^']*'}/, `fieldKey={'${this.props.fieldKey}'}`); - } - this.Document.layout_custom = newLayout; - this.Document.layoutKey = "layout_custom"; - e.stopPropagation(); // embed document when dragging with a userDropAction or an embedDoc flag set } else if (de.complete.docDragData.userDropAction || de.complete.docDragData.embedDoc) { const target = de.complete.docDragData.droppedDocuments[0]; @@ -1086,7 +1074,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & <div className={`formattedTextBox-cont`} ref={this._ref} style={{ height: this.layoutDoc._autoHeight ? "max-content" : undefined, - background: this.props.hideOnLeave ? "rgba(0,0,0 ,0.4)" : undefined, + background: this.props.hideOnLeave ? "rgba(0,0,0 ,0.4)" : StrCast(this.layoutDoc[this.props.fieldKey + "-backgroundColor"]), opacity: this.props.hideOnLeave ? (this._entered ? 1 : 0.1) : 1, color: this.props.hideOnLeave ? "white" : "inherit", pointerEvents: interactive ? "none" : "all", diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index c0e102195..364bce7a8 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -92,10 +92,11 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum } else if (de.altKey || !this.dataDoc[this.props.fieldKey]) { const layoutDoc = de.complete.docDragData?.draggedDocuments[0]; const targetField = Doc.LayoutFieldKey(layoutDoc); - if (layoutDoc?.[DataSym][targetField] instanceof ImageField) { - this.dataDoc[this.props.fieldKey] = ObjectField.MakeCopy(layoutDoc[DataSym][targetField] as ImageField); - this.dataDoc[this.props.fieldKey + "-nativeWidth"] = NumCast(layoutDoc[DataSym][targetField + "-nativeWidth"]); - this.dataDoc[this.props.fieldKey + "-nativeHeight"] = NumCast(layoutDoc[DataSym][targetField + "-nativeHeight"]); + const targetDoc = layoutDoc[DataSym]; + if (targetDoc[targetField] instanceof ImageField) { + this.dataDoc[this.props.fieldKey] = ObjectField.MakeCopy(targetDoc[targetField] as ImageField); + this.dataDoc[this.props.fieldKey + "-nativeWidth"] = NumCast(targetDoc[targetField + "-nativeWidth"]); + this.dataDoc[this.props.fieldKey + "-nativeHeight"] = NumCast(targeDoc[targetField + "-nativeHeight"]); e.stopPropagation(); } } diff --git a/src/client/views/webcam/DashWebRTCVideo.scss b/src/client/views/webcam/DashWebRTCVideo.scss index 2f35eeca2..41307a808 100644 --- a/src/client/views/webcam/DashWebRTCVideo.scss +++ b/src/client/views/webcam/DashWebRTCVideo.scss @@ -4,7 +4,7 @@ background: whitesmoke; color: grey; border-radius: 15px; - box-shadow: $intermediate-color 0.2vw 0.2vw 0.4vw; + box-shadow: #9c9396 0.2vw 0.2vw 0.4vw; border: solid #BBBBBBBB 5px; pointer-events: all; display: flex; @@ -18,29 +18,66 @@ letter-spacing: 2px; font-size: 16px; width: 100%; + margin-top: 20px; + } + + .videoContainer { + position: relative; + width: calc(100% - 20px); + height: 100%; + /* border: 10px solid red; */ + margin-left: 10px; + } + + .buttonContainer { + display: flex; + width: calc(100% - 20px); + height: 50px; + justify-content: center; + text-align: center; + /* border: 1px solid black; */ + margin-left: 10px; + margin-top: 0; + margin-bottom: 15px; } #roomName { outline: none; border-radius: inherit; border: 1px solid #BBBBBBBB; + margin: 10px; + padding: 10px; } .side { width: 25%; height: 20%; position: absolute; - top: 65%; + /* top: 65%; */ z-index: 2; - right: 5%; + right: 0px; + bottom: 18px; } .main { position: absolute; - width: 95%; - height: 75%; - top: 20%; + width: 100%; + height: 100%; + /* top: 20%; */ + align-self: center; + } + + .videoButtons { + border-radius: 50%; + height: 30px; + width: 30px; + display: flex; + justify-content: center; + align-items: center; + justify-self: center; align-self: center; + margin: 5px; + border: 1px solid black; } }
\ No newline at end of file diff --git a/src/client/views/webcam/DashWebRTCVideo.tsx b/src/client/views/webcam/DashWebRTCVideo.tsx index cbf75f708..9c339e986 100644 --- a/src/client/views/webcam/DashWebRTCVideo.tsx +++ b/src/client/views/webcam/DashWebRTCVideo.tsx @@ -8,7 +8,14 @@ import { InkingControl } from "../InkingControl"; import "../../views/nodes/WebBox.scss"; import "./DashWebRTCVideo.scss"; import adapter from 'webrtc-adapter'; -import { initialize, hangup } from "./WebCamLogic"; +import { initialize, hangup, refreshVideos } from "./WebCamLogic"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { IconProp, library } from '@fortawesome/fontawesome-svg-core'; +import { faSync, faPhoneSlash } from "@fortawesome/free-solid-svg-icons"; + +library.add(faSync); +library.add(faPhoneSlash); + /** * This models the component that will be rendered, that can be used as a doc that will reflect the video cams. @@ -19,14 +26,6 @@ export class DashWebRTCVideo extends React.Component<CollectionFreeFormDocumentV private roomText: HTMLInputElement | undefined; @observable remoteVideoAdded: boolean = false; - componentDidMount() { - DocumentDecorations.Instance.addCloseCall(this.closeConnection); - } - - closeConnection: CloseCall = () => { - hangup(); - } - @action changeUILook = () => { this.remoteVideoAdded = true; @@ -47,34 +46,30 @@ export class DashWebRTCVideo extends React.Component<CollectionFreeFormDocumentV public static LayoutString(fieldKey: string) { return FieldView.LayoutString(DashWebRTCVideo, fieldKey); } - _ignore = 0; - onPreWheel = (e: React.WheelEvent) => { - this._ignore = e.timeStamp; - } - onPrePointer = (e: React.PointerEvent) => { - this._ignore = e.timeStamp; - } - onPostPointer = (e: React.PointerEvent) => { - if (this._ignore !== e.timeStamp) { - e.stopPropagation(); - } + @action + onClickRefresh = () => { + refreshVideos(); } - onPostWheel = (e: React.WheelEvent) => { - if (this._ignore !== e.timeStamp) { - e.stopPropagation(); - } + + onClickHangUp = () => { + hangup(); } render() { let content = - <div className="webcam-cont" style={{ width: "100%", height: "100%" }} onWheel={this.onPostWheel} onPointerDown={this.onPostPointer} onPointerMove={this.onPostPointer} onPointerUp={this.onPostPointer}> + <div className="webcam-cont" style={{ width: "100%", height: "100%" }}> <div className="webcam-header">DashWebRTC</div> <input id="roomName" type="text" placeholder="Enter room name" ref={(e) => this.roomText = e!} onKeyDown={this.onEnterKeyDown} /> - <video id="localVideo" className={"RTCVideo" + (this.remoteVideoAdded ? " side" : " main")} autoPlay playsInline muted ref={(e) => { - }}></video> - <video id="remoteVideo" className="RTCVideo main" autoPlay playsInline ref={(e) => { - }}></video> - + <div className="videoContainer"> + <video id="localVideo" className={"RTCVideo" + (this.remoteVideoAdded ? " side" : " main")} autoPlay playsInline muted ref={(e) => { + }}></video> + <video id="remoteVideo" className="RTCVideo main" autoPlay playsInline ref={(e) => { + }}></video> + </div> + <div className="buttonContainer"> + <div className="videoButtons" style={{ background: "red" }} onClick={this.onClickHangUp}><FontAwesomeIcon icon={faPhoneSlash} color="white" /></div> + <div className="videoButtons" style={{ background: "green" }} onClick={this.onClickRefresh}><FontAwesomeIcon icon={faSync} color="white" /></div> + </div> </div >; let frozen = !this.props.isSelected() || DocumentDecorations.Instance.Interacting; @@ -86,7 +81,7 @@ export class DashWebRTCVideo extends React.Component<CollectionFreeFormDocumentV <div className={classname} > {content} </div> - {!frozen ? (null) : <div className="webBox-overlay" onWheel={this.onPreWheel} onPointerDown={this.onPrePointer} onPointerMove={this.onPrePointer} onPointerUp={this.onPrePointer} />} + {!frozen ? (null) : <div className="webBox-overlay" />} </>); } diff --git a/src/client/views/webcam/WebCamLogic.js b/src/client/views/webcam/WebCamLogic.js index 3dfb82465..f542fb983 100644 --- a/src/client/views/webcam/WebCamLogic.js +++ b/src/client/views/webcam/WebCamLogic.js @@ -277,4 +277,16 @@ function handleRemoteHangup() { function sendMessage(message) { console.log('Client sending message: ', message); socket.emit('message', message, room); -};
\ No newline at end of file +}; + +export function refreshVideos() { + var localVideo = document.querySelector('#localVideo'); + var remoteVideo = document.querySelector('#remoteVideo'); + if (localVideo) { + localVideo.srcObject = localStream; + } + if (remoteVideo) { + remoteVideo.srcObject = remoteStream; + } + +}
\ No newline at end of file diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts index 447dbe3b0..55c0660c0 100644 --- a/src/new_fields/Doc.ts +++ b/src/new_fields/Doc.ts @@ -17,7 +17,6 @@ import { intersectRect } from "../Utils"; import { UndoManager, undoBatch } from "../client/util/UndoManager"; import { computedFn } from "mobx-utils"; import { RichTextField } from "./RichTextField"; -import { Script } from "vm"; export namespace Field { export function toKeyValueString(doc: Doc, key: string): string { @@ -112,7 +111,12 @@ export class Doc extends RefField { get: getter, // getPrototypeOf: (target) => Cast(target[SelfProxy].proto, Doc) || null, // TODO this might be able to replace the proto logic in getter has: (target, key) => key in target.__fields, - ownKeys: target => Object.keys(target.__allfields), + ownKeys: target => { + let obj = {} as any; + Object.assign(obj, target.___fields); + runInAction(() => obj.__LAYOUT__ = target.__LAYOUT__); + return Object.keys(obj) + }, getOwnPropertyDescriptor: (target, prop) => { if (prop.toString() === "__LAYOUT__") { return Reflect.getOwnPropertyDescriptor(target, prop); @@ -140,17 +144,7 @@ export class Doc extends RefField { [key: string]: FieldResult; @serializable(alias("fields", map(autoObject(), { afterDeserialize: afterDocDeserialize }))) - private get __fields() { - return this.___fields; - } - private get __allfields() { - let obj = {} as any; - Object.assign(obj, this.___fields); - runInAction(() => obj.__LAYOUT__ = this.__LAYOUT__); - return obj; - } - - + private get __fields() { return this.___fields; } private set __fields(value) { this.___fields = value; for (const key in value) { @@ -168,18 +162,19 @@ export class Doc extends RefField { private [UpdatingFromServer]: boolean = false; private [Update] = (diff: any) => { - if (this[UpdatingFromServer]) { - return; - } - DocServer.UpdateField(this[Id], diff); + !this[UpdatingFromServer] && DocServer.UpdateField(this[Id], diff); } private [Self] = this; private [SelfProxy]: any; public [WidthSym] = () => NumCast(this[SelfProxy]._width); public [HeightSym] = () => NumCast(this[SelfProxy]._height); - public get [DataSym]() { return Cast(Doc.Layout(this[SelfProxy]).resolvedDataDoc, Doc, null) || this[SelfProxy]; } public get [LayoutSym]() { return this[SelfProxy].__LAYOUT__; } + public get [DataSym]() { + const self = this[SelfProxy]; + return self.resolvedDataDoc && !self.isTemplateForField ? self : + Doc.GetProto(Cast(Doc.Layout(self).resolvedDataDoc, Doc, null) || self); + } @computed get __LAYOUT__() { const templateLayoutDoc = Cast(Doc.LayoutField(this[SelfProxy]), Doc, null); if (templateLayoutDoc) { @@ -195,12 +190,8 @@ export class Doc extends RefField { return undefined; } - [ToScriptString]() { - return "invalid"; - } - [ToString]() { - return "Doc"; - } + [ToScriptString]() { return "invalid"; } + [ToString]() { return "Doc"; } private [CachedUpdates]: { [key: string]: () => void | Promise<any> } = {}; public static CurrentUserEmail: string = ""; @@ -287,8 +278,7 @@ export namespace Doc { export function Get(doc: Doc, key: string, ignoreProto: boolean = false): FieldResult { try { - const self = doc[Self]; - return getField(self, key, ignoreProto); + return getField(doc[Self], key, ignoreProto); } catch { return doc; } @@ -357,13 +347,12 @@ export namespace Doc { return r || r2 || r3 || r4; } - // gets the document's prototype or returns the document if it is a prototype - export function GetProto(doc: Doc) { - return doc && (Doc.GetT(doc, "isPrototype", "boolean", true) ? doc : (doc.proto || doc)); - } - export function GetDataDoc(doc: Doc): Doc { - const proto = Doc.GetProto(doc); - return proto === doc ? proto : Doc.GetDataDoc(proto); + // Gets the data document for the document. Note: this is mis-named -- it does not specifically + // return the doc's proto, but rather recursively searches through the proto inheritance chain + // and returns the document who's proto is undefined or whose proto is marked as a base prototype ('isPrototype'). + export function GetProto(doc: Doc): Doc { + const proto = doc && (Doc.GetT(doc, "isPrototype", "boolean", true) ? doc : (doc.proto || doc)); + return proto === doc ? proto : Doc.GetProto(proto); } export function allKeys(doc: Doc): string[] { @@ -443,11 +432,6 @@ export namespace Doc { return bounds; } - export function MakeTitled(title: string) { - const doc = new Doc(); - doc.title = title; - return doc; - } export function MakeAlias(doc: Doc, id?: string) { const alias = !GetT(doc, "isPrototype", "boolean", true) ? Doc.MakeCopy(doc, undefined, id) : Doc.MakeDelegate(doc, id); const layout = Doc.LayoutField(alias); @@ -488,7 +472,7 @@ export namespace Doc { if (templateLayoutDoc.resolvedDataDoc instanceof Promise) { expandedTemplateLayout = undefined; - } else if (templateLayoutDoc.resolvedDataDoc === Doc.GetDataDoc(targetDoc)) { + } else if (templateLayoutDoc.resolvedDataDoc === Doc.GetProto(targetDoc)) { expandedTemplateLayout = templateLayoutDoc; } else if (expandedTemplateLayout === undefined) { setTimeout(action(() => { @@ -497,7 +481,7 @@ export namespace Doc { newLayoutDoc.lockedPosition = true; newLayoutDoc.expandedTemplate = targetDoc; targetDoc[expandedLayoutFieldKey] = newLayoutDoc; - const dataDoc = Doc.GetDataDoc(targetDoc); + const dataDoc = Doc.GetProto(targetDoc); newLayoutDoc.resolvedDataDoc = dataDoc; if (dataDoc[templateField] === undefined && templateLayoutDoc[templateField] instanceof List && Cast(templateLayoutDoc[templateField], listSpec(Doc), []).length) { dataDoc[templateField] = ComputedField.MakeFunction(`ObjectField.MakeCopy(templateLayoutDoc["${templateField}"] as List)`, { templateLayoutDoc: Doc.name }, { templateLayoutDoc: templateLayoutDoc }); @@ -660,10 +644,6 @@ export namespace Doc { // assign the template field doc a delegate of any extension document that was previously used to render the template field (since extension doc's carry rendering informatino) Doc.Layout(templateField)[metadataFieldKey + "_ext"] = Doc.MakeDelegate(templateField[templateFieldLayoutString?.split("'")[1] + "_ext"] as Doc); - if (templateField.backgroundColor !== templateDoc?.defaultBackgroundColor) { - templateField.defaultBackgroundColor = templateField.backgroundColor; - } - return true; } @@ -716,26 +696,26 @@ export namespace Doc { export function SetUserDoc(doc: Doc) { manager._user_doc = doc; } export function IsBrushed(doc: Doc) { return computedFn(function IsBrushed(doc: Doc) { - return brushManager.BrushedDoc.has(doc) || brushManager.BrushedDoc.has(Doc.GetDataDoc(doc)); + return brushManager.BrushedDoc.has(doc) || brushManager.BrushedDoc.has(Doc.GetProto(doc)); })(doc); } // don't bother memoizing (caching) the result if called from a non-reactive context. (plus this avoids a warning message) export function IsBrushedDegreeUnmemoized(doc: Doc) { - return brushManager.BrushedDoc.has(doc) ? 2 : brushManager.BrushedDoc.has(Doc.GetDataDoc(doc)) ? 1 : 0; + return brushManager.BrushedDoc.has(doc) ? 2 : brushManager.BrushedDoc.has(Doc.GetProto(doc)) ? 1 : 0; } export function IsBrushedDegree(doc: Doc) { return computedFn(function IsBrushDegree(doc: Doc) { - return brushManager.BrushedDoc.has(doc) ? 2 : brushManager.BrushedDoc.has(Doc.GetDataDoc(doc)) ? 1 : 0; + return Doc.IsBrushedDegreeUnmemoized(doc); })(doc); } export function BrushDoc(doc: Doc) { brushManager.BrushedDoc.set(doc, true); - brushManager.BrushedDoc.set(Doc.GetDataDoc(doc), true); + brushManager.BrushedDoc.set(Doc.GetProto(doc), true); return doc; } export function UnBrushDoc(doc: Doc) { brushManager.BrushedDoc.delete(doc); - brushManager.BrushedDoc.delete(Doc.GetDataDoc(doc)); + brushManager.BrushedDoc.delete(Doc.GetProto(doc)); return doc; } @@ -748,14 +728,14 @@ export namespace Doc { document.removeEventListener("pointerdown", linkFollowUnhighlight); } - let dt = 0; + let _lastDate = 0; export function linkFollowHighlight(destDoc: Doc, dataAndDisplayDocs = true) { linkFollowUnhighlight(); Doc.HighlightDoc(destDoc, dataAndDisplayDocs); document.removeEventListener("pointerdown", linkFollowUnhighlight); document.addEventListener("pointerdown", linkFollowUnhighlight); - const x = dt = Date.now(); - window.setTimeout(() => dt === x && linkFollowUnhighlight(), 5000); + const lastDate = _lastDate = Date.now(); + window.setTimeout(() => _lastDate === lastDate && linkFollowUnhighlight(), 5000); } export class HighlightBrush { @@ -763,18 +743,18 @@ export namespace Doc { } const highlightManager = new HighlightBrush(); export function IsHighlighted(doc: Doc) { - return highlightManager.HighlightedDoc.get(doc) || highlightManager.HighlightedDoc.get(Doc.GetDataDoc(doc)); + return highlightManager.HighlightedDoc.get(doc) || highlightManager.HighlightedDoc.get(Doc.GetProto(doc)); } export function HighlightDoc(doc: Doc, dataAndDisplayDocs = true) { runInAction(() => { highlightManager.HighlightedDoc.set(doc, true); - dataAndDisplayDocs && highlightManager.HighlightedDoc.set(Doc.GetDataDoc(doc), true); + dataAndDisplayDocs && highlightManager.HighlightedDoc.set(Doc.GetProto(doc), true); }); } export function UnHighlightDoc(doc: Doc) { runInAction(() => { highlightManager.HighlightedDoc.set(doc, false); - highlightManager.HighlightedDoc.set(Doc.GetDataDoc(doc), false); + highlightManager.HighlightedDoc.set(Doc.GetProto(doc), false); }); } export function UnhighlightAll() { @@ -858,8 +838,6 @@ export namespace Doc { } } - @undoBatch - @action export function freezeNativeDimensions(layoutDoc: Doc, width: number, height: number): void { layoutDoc._autoHeight = false; layoutDoc.ignoreAspect = false; diff --git a/src/new_fields/documentSchemas.ts b/src/new_fields/documentSchemas.ts index aac6d4e3e..3cc05d3d5 100644 --- a/src/new_fields/documentSchemas.ts +++ b/src/new_fields/documentSchemas.ts @@ -4,16 +4,20 @@ import { Doc } from "./Doc"; import { DateField } from "./DateField"; export const documentSchema = createSchema({ - layout: "string", // this is the native layout string for the document. templates can be added using other fields and setting layoutKey below (see layout_custom as an example) + type: "string", // enumerated type of document -- should be template-specific (ie, start with an '_') + layout: "string", // this is the native layout string for the document. templates can be added using other fields and setting layoutKey below layoutKey: "string", // holds the field key for the field that actually holds the current lyoat - layout_custom: Doc, // used to hold a custom layout (there's nothing special about this field .. any field could hold a custom layout that can be selected by setting 'layoutKey') title: "string", // document title (can be on either data document or layout) dropAction: "string", // override specifying what should happen when this document is dropped (can be "alias" or "copy") childDropAction: "string", // specify the override for what should happen when the child of a collection is dragged from it and dropped (can be "alias" or "copy") + _autoHeight: "boolean", // whether the height of the document should be computed automatically based on its contents _nativeWidth: "number", // native width of document which determines how much document contents are scaled when the document's width is set _nativeHeight: "number", // " _width: "number", // width of document in its container's coordinate system _height: "number", // " + _showCaption: "string", // whether editable caption text is overlayed at the bottom of the document + _showTitle: "string", // the fieldkey whose contents should be displayed at the top of the document + _showTitleHover: "string", // the showTitle should be shown only on hover _freeformLayoutEngine: "string",// the string ID for the layout engine to use to layout freeform view documents _LODdisable: "boolean", // whether to disbale LOD switching for CollectionFreeFormViews _pivotField: "string", // specifies which field should be used as the timeline/pivot axis @@ -24,20 +28,18 @@ export const documentSchema = createSchema({ opacity: "number", // opacity of document creationDate: DateField, // when the document was created links: listSpec(Doc), // computed (readonly) list of links associated with this document - removeDropProperties: listSpec("string"), // properties that should be removed from the alias/copy/etc of this document when it is dropped onClick: ScriptField, // script to run when document is clicked (can be overriden by an onClick prop) onPointerDown: ScriptField, // script to run when document is clicked (can be overriden by an onClick prop) onPointerUp: ScriptField, // script to run when document is clicked (can be overriden by an onClick prop) onDragStart: ScriptField, // script to run when document is dragged (without being selected). the script should return the Doc to be dropped. dragFactory: Doc, // the document that serves as the "template" for the onDragStart script. ie, to drag out copies of the dragFactory document. + removeDropProperties: listSpec("string"), // properties that should be removed from the alias/copy/etc of this document when it is dropped ignoreAspect: "boolean", // whether aspect ratio should be ignored when laying out or manipulating the document - autoHeight: "boolean", // whether the height of the document should be computed automatically based on its contents isTemplateForField: "string",// when specifies a field key, then the containing document is a template that renders the specified field isBackground: "boolean", // whether document is a background element and ignores input events (can only selet with marquee) - type: "string", // enumerated type of document treeViewOpen: "boolean", // flag denoting whether the documents sub-tree (contents) is visible or hidden treeViewExpandedView: "string", // name of field whose contents are being displayed as the document's subtree - preventTreeViewOpen: "boolean", // ignores the treeViewOpen flag (for allowing a view to not be slaved to other views of the document) + treeViewPreventOpen: "boolean", // ignores the treeViewOpen flag (for allowing a view to not be slaved to other views of the document) currentTimecode: "number", // current play back time of a temporal document (video / audio) maximizeLocation: "string", // flag for where to place content when following a click interaction (e.g., onRight, inPlace, inTab) lockedPosition: "boolean", // whether the document can be moved (dragged) @@ -46,19 +48,14 @@ export const documentSchema = createSchema({ borderRounding: "string", // border radius rounding of document searchFields: "string", // the search fields to display when this document matches a search in its metadata heading: "number", // the logical layout 'heading' of this document (used by rule provider to stylize h1 header elements, from h2, etc) - showCaption: "string", // whether editable caption text is overlayed at the bottom of the document - showTitle: "string", // the fieldkey whose contents should be displayed at the top of the document - showTitleHover: "string", // the showTitle should be shown only on hover isButton: "boolean", // whether document functions as a button (overiding native interactions of its content) ignoreClick: "boolean", // whether documents ignores input clicks (but does not ignore manipulation and other events) - isAnimating: "string", // whether the document is in the midst of animating between two layouts (used by icons to de/iconify documents). value is undefined|"min"|"max" scrollToLinkID: "string", // id of link being traversed. allows this doc to scroll/highlight/etc its link anchor. scrollToLinkID should be set to undefined by this doc after it sets up its scroll,etc. strokeWidth: "number", fontSize: "string", fitToBox: "boolean", // whether freeform view contents should be zoomed/panned to fill the area of the document view xPadding: "number", // pixels of padding on left/right of collectionfreeformview contents when fitToBox is set yPadding: "number", // pixels of padding on left/right of collectionfreeformview contents when fitToBox is set - LODarea: "number", // area (width*height) where CollectionFreeFormViews switch from a label to rendering contents letterSpacing: "string", textTransform: "string" }); diff --git a/src/scraping/buxton/final/BuxtonImporter.ts b/src/scraping/buxton/final/BuxtonImporter.ts index 6e3c66690..098671942 100644 --- a/src/scraping/buxton/final/BuxtonImporter.ts +++ b/src/scraping/buxton/final/BuxtonImporter.ts @@ -1,11 +1,9 @@ -import { readdirSync, writeFile, mkdirSync, createWriteStream } from "fs"; +import { readdirSync, writeFile, mkdirSync } from "fs"; import * as path from "path"; import { red, cyan, yellow } from "colors"; import { Utils } from "../../../Utils"; import rimraf = require("rimraf"); -import * as sharp from 'sharp'; -import { SizeSuffix, DashUploadUtils, InjectSize } from "../../../server/DashUploadUtils"; -import { AcceptibleMedia } from "../../../server/SharedMediaTypes"; +import { DashUploadUtils } from "../../../server/DashUploadUtils"; const StreamZip = require('node-stream-zip'); const createImageSizeStream = require("image-size-stream"); import { parseXml } from "libxmljs"; @@ -13,7 +11,7 @@ import { strictEqual } from "assert"; interface DocumentContents { body: string; - imageUrls: string[]; + imageData: ImageData[]; hyperlinks: string[]; captions: string[]; embeddedFileNames: string[]; @@ -25,21 +23,33 @@ export interface DeviceDocument { longDescription: string; company: string; year: number; - originalPrice: number; - degreesOfFreedom: number; + originalPrice?: number; + degreesOfFreedom?: number; dimensions?: string; primaryKey: string; secondaryKey: string; attribute: string; + __images: ImageData[]; + hyperlinks: string[]; + captions: string[]; + embeddedFileNames: string[]; } -interface AnalysisResult { +export interface AnalysisResult { device?: DeviceDocument; - errors?: any; + errors?: { [key: string]: string }; } type Transformer<T> = (raw: string) => { transformed?: T, error?: string }; +export interface ImportResults { + deviceCount: number, + errorCount: number +} + +type ResultCallback = (result: AnalysisResult) => void; +type TerminatorCallback = (result: ImportResults) => void; + interface Processor<T> { exp: RegExp; matchIndex?: number; @@ -47,6 +57,12 @@ interface Processor<T> { required?: boolean; } +interface ImageData { + url: string; + nativeWidth: number; + nativeHeight: number; +} + namespace Utilities { export function numberValue(raw: string) { @@ -78,6 +94,7 @@ namespace Utilities { } export async function readAndParseXml(zip: any, relativePath: string) { + console.log(`Text streaming ${relativePath}`); const contents = await new Promise<string>((resolve, reject) => { let body = ""; zip.stream(relativePath, (error: any, stream: any) => { @@ -104,7 +121,7 @@ const RegexMap = new Map<keyof DeviceDocument, Processor<any>>([ }], ["year", { exp: /Year:\s+([^\|]*)\s+\|/, - transformer: Utilities.numberValue + transformer: (raw: string) => Utilities.numberValue(/[0-9]{4}/.exec(raw)![0]) }], ["primaryKey", { exp: /Primary:\s+(.*)(Secondary|Additional):/, @@ -120,17 +137,20 @@ const RegexMap = new Map<keyof DeviceDocument, Processor<any>>([ transformer: raw => ({ transformed: Utilities.collectUniqueTokens(raw).transformed[0] }), }], ["originalPrice", { - exp: /Original Price \(USD\)\:\s+(\$[0-9]+\.[0-9]+|NFS)/, + exp: /Original Price \(USD\)\:\s+(\$[0-9\,]+\.[0-9]+|NFS)/, transformer: (raw: string) => { + raw = raw.replace(/\,/g, ""); if (raw === "NFS") { return { transformed: -1 }; } return Utilities.numberValue(raw.slice(1)); - } + }, + required: false }], ["degreesOfFreedom", { exp: /Degrees of Freedom:\s+([0-9]+)/, - transformer: Utilities.numberValue + transformer: Utilities.numberValue, + required: false }], ["dimensions", { exp: /Dimensions\s+\(L x W x H\):\s+([0-9\.]+\s+x\s+[0-9\.]+\s+x\s+[0-9\.]+\s\([A-Za-z]+\))/, @@ -165,7 +185,7 @@ const successOut = "buxton.json"; const failOut = "incomplete.json"; const deviceKeys = Array.from(RegexMap.keys()); -export default async function executeImport() { +export default async function executeImport(emitter: ResultCallback, terminator: TerminatorCallback) { try { const contents = readdirSync(sourceDir); const wordDocuments = contents.filter(file => /.*\.docx?$/.test(file)).map(file => `${sourceDir}/${file}`); @@ -173,7 +193,7 @@ export default async function executeImport() { rimraf.sync(dir); mkdirSync(dir); }); - return parseFiles(wordDocuments); + return parseFiles(wordDocuments, emitter, terminator); } catch (e) { const message = [ "Unable to find a source directory.", @@ -185,23 +205,22 @@ export default async function executeImport() { } } -async function parseFiles(wordDocuments: string[]): Promise<DeviceDocument[]> { - const imported: { fileName: string, contents: DocumentContents }[] = []; +async function parseFiles(wordDocuments: string[], emitter: ResultCallback, terminator: TerminatorCallback): Promise<DeviceDocument[]> { + const results: AnalysisResult[] = []; for (const filePath of wordDocuments) { const fileName = path.basename(filePath).replace("Bill_Notes_", ""); console.log(cyan(`\nExtracting contents from ${fileName}...`)); - imported.push({ fileName, contents: await extractFileContents(filePath) }); + const result = analyze(fileName, await extractFileContents(filePath)); + emitter(result); + results.push(result); } - console.log(yellow("\nAnalyzing the extracted document text...\n")); - const results = imported.map(({ fileName, contents }) => analyze(fileName, contents)); - const masterDevices: DeviceDocument[] = []; - const masterErrors: any[] = []; + const masterErrors: { [key: string]: string }[] = []; results.forEach(({ device, errors }) => { if (device) { masterDevices.push(device); - } else { + } else if (errors) { masterErrors.push(errors); } }); @@ -216,6 +235,8 @@ async function parseFiles(wordDocuments: string[]): Promise<DeviceDocument[]> { await writeOutputFile(failOut, masterErrors, total, false); console.log(); + terminator({ deviceCount: masterDevices.length, errorCount: masterErrors.length }); + return masterDevices; } @@ -239,10 +260,9 @@ async function extractFileContents(pathToDocument: string): Promise<DocumentCont strictEqual(length % 3 === 0, true, "Improper caption formatting."); for (let i = 3; i < captionTargets.length; i += 3) { - const [image, fileName, caption] = captionTargets.slice(i, i + 3); - strictEqual(image, "", `The image cell in one row was not the empty string: ${image}`); - captions.push(caption); - embeddedFileNames.push(fileName); + const row = captionTargets.slice(i, i + 3); + captions.push(row[1]); + embeddedFileNames.push(row[2]); } // extract all hyperlinks embedded in the document @@ -251,12 +271,12 @@ async function extractFileContents(pathToDocument: string): Promise<DocumentCont console.log("Text extracted."); console.log("Beginning image extraction..."); - const imageUrls = await writeImages(zip); - console.log(`Extracted ${imageUrls.length} images.`); + const imageData = await writeImages(zip); + console.log(`Extracted ${imageData.length} images.`); zip.close(); - return { body, imageUrls, captions, embeddedFileNames, hyperlinks }; + return { body, imageData, captions, embeddedFileNames, hyperlinks }; } const imageEntry = /^word\/media\/\w+\.(jpeg|jpg|png|gif)/; @@ -267,19 +287,23 @@ interface Dimensions { type: string; } -async function writeImages(zip: any): Promise<string[]> { +async function writeImages(zip: any): Promise<ImageData[]> { const allEntries = Object.values<any>(zip.entries()).map(({ name }) => name); const imageEntries = allEntries.filter(name => imageEntry.test(name)); - const imageUrls: string[] = []; + const imageUrls: ImageData[] = []; for (const mediaPath of imageEntries) { const streamImage = () => new Promise<any>((resolve, reject) => { zip.stream(mediaPath, (error: any, stream: any) => error ? reject(error) : resolve(stream)); }); const { width, height, type } = await new Promise<Dimensions>(async resolve => { - const sizeStream = createImageSizeStream().on('size', resolve); - (await streamImage()).pipe(sizeStream); + const sizeStream = createImageSizeStream().on('size', (dimensions: Dimensions) => { + readStream.destroy(); + resolve(dimensions) + }); + const readStream = await streamImage(); + readStream.pipe(sizeStream); }); if (Math.abs(width - height) < 10) { continue; @@ -290,21 +314,25 @@ async function writeImages(zip: any): Promise<string[]> { await DashUploadUtils.outputResizedImages(streamImage, imageDir, generatedFileName, ext); - imageUrls.push(`/files/images/buxton/${generatedFileName}`); + imageUrls.push({ + url: `/files/images/buxton/${generatedFileName}`, + nativeWidth: width, + nativeHeight: height + }); } return imageUrls; } function analyze(fileName: string, contents: DocumentContents): AnalysisResult { - const { body, imageUrls, captions, hyperlinks, embeddedFileNames } = contents; + const { body, imageData, captions, hyperlinks, embeddedFileNames } = contents; const device: any = { hyperlinks, captions, embeddedFileNames, - __images: imageUrls + __images: imageData }; - const errors: any = { fileName }; + const errors: { [key: string]: string } = { fileName }; for (const key of deviceKeys) { const { exp, transformer, matchIndex, required } = RegexMap.get(key)!; diff --git a/src/scraping/buxton/final/json/buxton.json b/src/scraping/buxton/final/json/buxton.json deleted file mode 100644 index 329194633..000000000 --- a/src/scraping/buxton/final/json/buxton.json +++ /dev/null @@ -1,504 +0,0 @@ -[ - { - "hyperlinks": [ - "http://research.microsoft.com/en-us/um/people/bibuxton/buxtoncollection/default.aspx", - "http://spacemice.org/index.php?title=Cadman", - "http://research.microsoft.com/en-us/um/people/bibuxton/buxtoncollection/default.aspx", - "http://research.microsoft.com/en-us/um/people/bibuxton/buxtoncollection/contact.aspx", - "http://research.microsoft.com/en-us/um/people/bibuxton/buxtoncollection/acknowledgements.aspx", - "http://research.microsoft.com/en-us/um/people/bibuxton/buxtoncollection/browse.aspx", - "3DCad_Brochure.pdf" - ], - "captions": [ - "The 3Dconnexion CadMan 3D Motion Controller, a 6DOF joystick.", - "Brochure for the CadMan 3D Motion Controller.(Click on image to access full document.)" - ], - "embeddedFileNames": [ - "3DCad_0410.JPG", - "3DCad_Brochure.jpg" - ], - "__images": [ - "/files/images/buxton/upload_8396a3a2-9ec1-4707-af14-b61c08f40388.png", - "/files/images/buxton/upload_1c03cd29-0114-42f6-8016-467f708809e1.png", - "/files/images/buxton/upload_552b2a04-4c7c-453c-8153-4976f6dae47e.png" - ], - "title": "3Dconnexion CadMan 3D Motion Controller", - "company": "3Dconnexion", - "year": 2003, - "primaryKey": "Joystick", - "secondaryKey": "Blank", - "attribute": "Isometric", - "originalPrice": 399, - "degreesOfFreedom": 6, - "dimensions": { - "dim_length": 175, - "dim_width": 122, - "dim_height": 43, - "dim_unit": "mm" - }, - "shortDescription": "The CadMan is a 6 degree of freedom (DOF) joystick controller. It represented a significant step towards making this class of is controller affordable. It was mainly directed at 3D modelling and animation and was a “next generation” of the Magellan controller, which is also in the collection.", - "longDescription": "The CadMan is a 6 degree of freedom (DOF) joystick controller. It represented a significant step towards making this class of is controller more affordable. It was mainly directed at 3D modelling and animation and was a “next generation” of the Magellan/SpaceMouse controller, which is also in the collection. Like the Magellan, this is an isometric rate-control joystick. That is, it rests in a neutral central position, not sending and signal. When a force is applied to it, it emits a signal indicating the direction and strength of that force. This signal can then be mapped to a parameter of a selected object, such as a sphere, and – for example – cause that sphere to rotate for as long as, and as fast as, and in the direction determined by, the duration, force, and direction of the applied force. When released, it springs back to neutral position. Note that the force does not need to be directed along a single DOF. In fact, a core feature of the device is that one can simultaneously and independently apply force that asserts control over more than one DOF, and furthermore, vary those forces dynamically. As an aid to understanding, let me walk through some of the underlying concepts at play here by using a more familiar device: a computer mouse. If you move a mouse in a forward/backward direction, the mouse pointer on the screen moves between the screen’s top and bottom. If you think of the screen as a piece of graph paper, that corresponds to moving along the “Y” axis. That is one degree of freedom. On the other hand, you could move the mouse left and right, which causes the mouse to move between the left and right side of the screen. That would correspond to moving along the graph paper’s “X” axis – a second degree of freedom. Yet, you can also move the mouse diagonally. This is an example of independently controlling two degrees of freedom. Now imagine that if you lifted your mouse off your desktop, that your computer could dynamically sense its height as you did so. This would constitute a “flying mouse” (the literal translation of the German word for a “Bat”, which Canadian colleague, Colin Ware, applied to just such a mouse which he built in 1988). If you moved your Bat vertically up and down, perpendicular to the desktop, you would be controlling movement along the “Z” axis - a third degree of freedom. Having already seen that we can move a mouse diagonally, we have established that we need not be constrained to only moving along a single axis. That extends to the movement of our Bat and movement along the “Z” axis. We can control our hand movement in dependently in any or all directions in 3D space. But how does one reconcile the fact that we call the CadMan a “3D controller, and yet also describe it as having 6 degrees of freedom? Yes, as described, our bat can fly in 3D, but on the other hand, its range of movement within those 3 dimensions is much richer. To demonstrate this, move your hand in 3D space on and above your desktop. However, do so keeping your palm flat, parallel to the desktop with your fingers pointing directly forward. In so doing, you are still moving in 3D. Now, while moving, twist your wrist, while moving the hand, such that your palm is alternatively exposed to the left and right side. This constitutes rotation around the “Y” axis. A fourth DOF. Now add a waving motion to your hand, as if it were a paper airplane diving up and down, while also rocking left and right. But keep your fingers pointing forward. You have now added a fifth DOF, rotation around the “X” axis. Finally, add a twist to your wrist so that your fingers are no longer constrained to pointing forward. This is the sixth degree of freedom, rotation around the “Z” axis. Now don’t be fooled, this exercise could continue. We are not restricted to even six DOF. Imagine doing the above, but where the movement and rotations are measured relative to the Bat’s position and orientation, rather than to the holding/controlling hand, per se. One could imagine the Bat having a scroll wheel, like the one on most mice today. Furthermore, while flying your Bat around in 3D, that wheel could easily be rolled in either forward or backward, and thereby control the size of whatever was being controlled. Hence, with one hand we could assert simultaneous and independent control over 7 DOF in 3D space. This exercise has two intended take-aways. The first is a better working understanding between the notion of Degree of Freedom (DOF) and Dimension in space. Hopefully, the confusion frequently encountered when 3D and 6DOF are used in close context, can now be eliminated. Second, is that, with appropriate sensing, the human hand is capable of exercising control over far more degrees of freedom that six. And if we use the two hands together, the potential number of DOF that one can control goes even further. Finally, it is important to add one more take-away – one which both emerges from, and is frequently encountered when discussing, the previous two. That is, do not equate exercising simultaneous control over a high number of DOF with consciously doing the same number of different things all at once. The example that used to be thrown at me when I started talking about coordinated simultaneously bi-manual action went along the lines of, “Psychology tells us that we cannot do multiple things at once, for example, simultaneously tapping your head and rubbing your stomach. ”Well, first, I can tap my head with one hand while rubbing my stomach with the other. But that is not the point. The whole essence of skill – motor-sensory and cognitive – is “chunking” or task integration. When one appears to be doing many different things at once, if they are skilled, they are consciously doing only one thing. Playing a chord on the piano, for example, or skiing down the hill. Likewise, in flying your imaginary BAT in the previous exercise with the scroll wheel, were you doing 7 things at once, or one thing with 7 DOF? And if you had a Bat in each hand, does that mean you are now doing 14 things at once, or are you doing one thing with 14 DOF? Let me provide a different way of answering this question: if you have ever played air guitar, or “conducted” the orchestra that you are listening to on the radio, you are exercising control over more than 14 DOF. And you are doing exactly what I just said, “playing air guitar” or “conducting an orchestra”. One thing – at the conscious level, which is what matters – despite almost any one thing being able to be deconstructed into hundreds of sub-tasks. As I said the essence of skill: aggregation, or chunking. What is most important for both tool designers and users to be mindful of, is the overwhelming influence that our choice and design of tools impacts the degree to which such integration or chunking can take place. The degree to which the tool matches both the skills that we have already acquired through a lifetime of living in the everyday world, and the demands of the intended task, the more seamless that task can be performed, the more “natural” it will feel, and the less learning will be required. In my experience, it brought particular value when used bimanually, in combination with a mouse, where the preferred hand performed conventional pointing, selection and dragging tasks, while the non-preferred hand could manipulate the parameters of the thing being selected. First variation of the since the 2001 formation of 3Dconnextion. The CadMan came in 5 colours: smoke, orange, red, blue and green. See the notes for the LogiCad3D Magellan for more details on this class of device. It is the “parent” of the CadMan, and despite the change in company name, it comes from the same team." - }, - { - "hyperlinks": [ - "http://research.microsoft.com/en-us/um/people/bibuxton/buxtoncollection/acknowledgements.aspx", - "SpaceNavigator_Press_Release.pdf", - "http://research.microsoft.com/en-us/um/people/bibuxton/buxtoncollection/browse.aspx", - "https://web.archive.org/web/20061205222533/http://www.3dconnexion.com:80/products/3a1d.php", - "3DConnexion_SpaceNavigator/SpaceNavigator_Launch_Web_Page.pdf", - "http://research.microsoft.com/en-us/um/people/bibuxton/buxtoncollection/default.aspx", - "https://www.pcmag.com/article2/0,2817,2082798,00.asp", - "http://research.microsoft.com/en-us/um/people/bibuxton/buxtoncollection/default.aspx", - "SpaceNavigator_Launch_Web_Page.pdf", - "http://research.microsoft.com/en-us/um/people/bibuxton/buxtoncollection/contact.aspx", - "Navigator_Data_Sheet_2006.pdf", - "3DConnexion_SpaceNavigator/SpaceNavigator_Press_Release.pdf" - ], - "captions": [ - "The 3Dconnexion SpaceNavigator 6DOF Joystick.", - "The 3Dconnexion SpaceNavigator adjacent to ruler in order to show scale.", - "Diagram from SpaceNavigator Data Sheet illustrating the directions of force which control each of the 6DOF.", - "Page 1 of the SpaceNavigarot Data Sheet.(Click on image to access full document.)", - "A page from the launch web site of the SpaceNavigator.(Click on image to access full document.)", - "Front page of the press release announcing the launch of the SpaceNavigator.(Click on image to access full document.)" - ], - "embeddedFileNames": [ - "SpaceNavigator_01.JPG", - "SpaceNavigator_02.JPG", - "SpaceNavigator_Control_Axes.jpg", - "Navigator_Data_Sheet_2006.jpg", - "SpaceNavigator_Launch_Web_Page.jpg", - "SpaceNavigator_Press_Release.jpg" - ], - "__images": [ - "/files/images/buxton/upload_11debe1a-f991-4c2f-be98-0d42e80fd1e4.jpg", - "/files/images/buxton/upload_40359203-b8fc-4b9f-8ce6-a55fed65441e.png", - "/files/images/buxton/upload_579c375e-ebbe-46dc-bb75-9f9238b6f3ff.png", - "/files/images/buxton/upload_f2188d31-5397-4f21-a20e-36944eaacb89.jpg", - "/files/images/buxton/upload_a149da03-e294-46ab-8054-fb730f3cb8f6.png", - "/files/images/buxton/upload_1c77aeb0-a08c-413a-91d7-002de76a7a20.png", - "/files/images/buxton/upload_a0beeeb8-f7f4-41f6-bd99-be01beaae63e.png" - ], - "title": "3Dconnexion SpaceNavigator ", - "company": "3Dconnexion", - "year": 2006, - "primaryKey": "Joystick", - "secondaryKey": "Dial", - "attribute": "Isometric", - "originalPrice": 59, - "degreesOfFreedom": 6, - "dimensions": { - "dim_length": 78, - "dim_width": 78, - "dim_height": 53, - "dim_unit": "mm" - }, - "shortDescription": "The SpaceNavigator is an entry level 6DOF joystick for the interactive 3D market. It came in a “Personal” and “Standard” edition, at $59. 00 and $99. 00 USD, respectively. These were break-through prices which opened up this technology (which cost $1, 595. 00 in 1991) to gamers and consumers. Doing so was necessary, since the high-end professional 3D graphics market was relatively small, and not growing anywhere near as fast as the consumer and gaming market.", - "longDescription": "The SpaceNavigator is an entry level 6DOF joystick for the interactive 3D market. It came in a “Personal” and “Standard” edition, at $59. 00 and $99. 00 USD, respectively. These were break-through prices which opened up this technology (which cost $1, 595. 00 in 1991) to gamers and consumers. Doing so was necessary, since the high-end professional 3D graphics market was relatively small, and not growing anywhere near as fast as the consumer and gaming market. As illustrated in an accompanying image, the direction of the force which controls each of the 6 degrees of freedom of the SpaceNavigator are: Move Left-Right: Push/Pull left-right parallel to the desktop. Move Forward-Backward: Push/Pull forward-backward parallel to the desktop. Move Vertically, Up-Down: Push down vertically into the table or pull up vertically away from the tableTilt Left-Right: Tilt the joystick left-rightTilt Forward-Backward: Tilt joystick forward-backwardRotate around vertical axis: Twist the joystick clockwise or counter clockwise. Control of these 6 DOF can be combined. For example, you can rotate/roll right while spinning. Besides gaming, one of the hopes was that this device would be used in interacting with 3D programs like Google Earth. The problem was, however, that there were few such programs then, just as now, relatively speaking, and even Google Earth, while remarkable, is not used anywhere as frequently of 2D Google Maps, for example. For those of us in the 3D graphics market, it was fantastic with respect to animation, games and industrial design. But it never took off, no matter how seductive it was. And, the interesting question is, will VR and AR change that? And if so, how will this class of 6DOF device play in that market?" - }, - { - "hyperlinks": [ - "http://research.microsoft.com/en-us/um/people/bibuxton/buxtoncollection/default.aspx", - "SpaceMouse_Plus_Data_Sheet.pdf", - "http://research.microsoft.com/en-us/um/people/bibuxton/buxtoncollection/default.aspx", - "http://research.microsoft.com/en-us/um/people/bibuxton/buxtoncollection/contact.aspx", - "http://youtu.be/w5OH3EfeL64", - "http://research.microsoft.com/en-us/um/people/bibuxton/buxtoncollection/acknowledgements.aspx", - "http://research.microsoft.com/en-us/um/people/bibuxton/buxtoncollection/browse.aspx", - "SpaceMouse_Plus_Info_Sheet.pdf" - ], - "captions": [ - "Overview of the Magellan Plus.", - "Overview of Magellan Plus in different colour.", - "Page one of the Magellan Plus / SpaceMouse Plus Data Sheet.(Click on image to access full document.)", - "Page one of the Magellan Plus / SpaceMouse Plus Information Sheet.(Click on image to access full document.)" - ], - "embeddedFileNames": [ - "3DPlus_0396.JPG", - "3DPlus_0391.JPG", - "SpaceMouse_Plus_Data_Sheet.jpg", - "SpaceMouse_Plus_Info_Sheet.jpg" - ], - "__images": [ - "/files/images/buxton/upload_30f4892d-deb7-40f0-8cf6-f350aa4eb7a0.jpg", - "/files/images/buxton/upload_441f5294-f3fb-4d57-a58b-b6235de7a117.png", - "/files/images/buxton/upload_4bda82c6-00d0-4b34-8654-36461db9cea5.png", - "/files/images/buxton/upload_5becb1a5-aaff-4137-86c2-3e3de3250d3b.png", - "/files/images/buxton/upload_8411549d-d401-414c-aa37-a3d01d39101f.png" - ], - "title": "3Dconnexion Magellan/SpaceMouse Plus", - "company": "3Dconnexion", - "year": 1998, - "primaryKey": "Joystick", - "secondaryKey": "Blank", - "attribute": "Isometric", - "originalPrice": 745, - "degreesOfFreedom": 6, - "dimensions": { - "dim_length": 188, - "dim_width": 120, - "dim_height": 44, - "dim_unit": "mm" - }, - "shortDescription": "The Magellan/SpaceMouse Plus is a refinement of the original LogiCad3D Magellan. From the industrial design perspective, the main difference is the switch from the original round “hockey puck” shaped handle to this asymmetric one.", - "longDescription": "The Magellan Plus (also known as the Spacemouse Plus) is a refinement of the original LogiCad3D Magellan (LogiCad3D evolved into 3DConnexion). From the industrial design perspective, the main difference between the two is the switch from the round “hockey puck” shaped handle to this asymmetric one. Despite this rather small change, both are included in the collection since that small change is a good example of my axiom that “everything being best for something and worst for something else. ” One of the things which most attracted me to the original Magellan was its hockey-puck shaped handle. The reason is that in my mind, it shouted out, “jog-shuttle wheel”. That is, the kind of controller that I was familiar with from editing audio and video. Since we were building software for 3D animation, the shape had value as a physical icon, or “phycon”, whose affordances suggested how this new control could employ existing skills. That is the good side. On the other hand, for 3D manipulation, when gripped, that same symmetry lacked any immediate tactile feed-forward as to orientation. That is, what axis of the 3D model would be affected by tilting the handle in any direction. On the other hand, the new asymmetric handle told the user, through touch, how the orientation of the handle aligned with that of the 3D model being controlled. One of the key habits leading to design literacy is to constantly prospect for patterns, rather than just individual examples. The reg the pattern means that one can separate the superficial features of the example, and see the underlying issue. For example, compare the Magellan Plus and the LogiCad3D Magellan, respectfully, with any Apple mouse and the 1988 Apple iMac G3 “hockey puck” mouse. While mice and 3D joysticks are very different devices, the two pairings reflect the same pattern. The suggested lesson is that patterns suggest that certain things are not mere exceptions – they are something which will likely reoccur, and therefore something that one can learn from – as long as one can recognize the deep pattern hidden beneath the superficial exterior." - }, - { - "hyperlinks": [ - "http://research.microsoft.com/en-us/um/people/bibuxton/buxtoncollection/default.aspx", - "http://www.evermotion.org/tutorials/show/7916/3dconnexion-s-spaceball-5000-review", - "http://research.microsoft.com/en-us/um/people/bibuxton/buxtoncollection/default.aspx", - "http://research.microsoft.com/en-us/um/people/bibuxton/buxtoncollection/contact.aspx", - "SpaceBall_5000_Data_Sheet.pdf", - "http://research.microsoft.com/en-us/um/people/bibuxton/buxtoncollection/acknowledgements.aspx", - "http://research.microsoft.com/en-us/um/people/bibuxton/buxtoncollection/browse.aspx", - "SP_SB_SM_Feature_Comparison_001.pdf" - ], - "captions": [ - "Side view of the Spaceball 5000.", - "Upper-left view of the Spaceball 5000, showing the position of 9 of the 12 programable buttons.", - "Back view of the Spaceball 5000, showing the position of buttons on either side of the ball.", - "View showing bimanual use, with the SpaceBall in the left hand, mouse in right. Note position of buttons relative to thumb and fingers.", - "Caption to come.(Click on image to access full document.)", - "Page 1 of SpaceBall 5000 Date Sheet.(Click on image to access full document.)" - ], - "embeddedFileNames": [ - "3DSpace_5000_01.JPG", - "3DSpace_5000_02.JPG", - "3DSpace_5000_05.JPG", - "3DSpace_5000_06.JPG", - "SP_SB_SM_Feature_Comparison.jpg", - "SpaceBall_5000_Data_Sheet.jpg" - ], - "__images": [ - "/files/images/buxton/upload_aedc76dd-7be5-4320-9938-7a01411024a2.jpg", - "/files/images/buxton/upload_d22b6f52-b5b2-47f3-88d2-6da98ae4a7f5.png", - "/files/images/buxton/upload_c4a9404f-05d4-44d4-bca0-00aad6fa4698.png", - "/files/images/buxton/upload_2a42083d-dc57-4534-aa4f-74e775fb0041.png", - "/files/images/buxton/upload_0743bb48-4c98-4a80-847e-3e33ea6ec939.jpg", - "/files/images/buxton/upload_4bae0ed1-e854-40ec-bc5f-91522bfb3ee9.jpg", - "/files/images/buxton/upload_cd885d1b-1efc-4732-97de-1258bc1bce0d.jpg" - ], - "title": "3Dconnexion Spaceball 5000", - "company": "3Dconnexion", - "year": 2003, - "primaryKey": "Joystick", - "secondaryKey": "Blank", - "attribute": "Isometric", - "originalPrice": 499, - "degreesOfFreedom": 6, - "dimensions": { - "dim_length": 213, - "dim_width": 76, - "dim_height": 152, - "dim_unit": "mm" - }, - "shortDescription": "This is an improved version of the original 1991 SpaceBall, manufactured by SpaceBall Technologies. It is a good example of how products improve as the market grows, while the price goes down. The original model sold for $1, 595. 00 USD, while this for $499. 00.", - "longDescription": "This is an improved version of the original 1991 SpaceBall, manufactured by SpaceBall Technologies. It is a good example of how products improve as the market grows, while the price goes down. The original model sold for $1, 595. 00 USD, while this for $499. 00. This version of the SpaceBall illustrates how the form-factor of the original version has changed over time. There are now 12 programmable function keys, 9 to be operated by the fingers on one side, and 3 to be operated by the thumb on the other. Note how the button placement indicates that the device is intended to be used by the left hand, with an accompanying mouse by the right – that is, by being meant for the left hand, it is intended for a right handed person." - }, - { - "hyperlinks": [ - "http://research.microsoft.com/en-us/um/people/bibuxton/buxtoncollection/default.aspx", - "https://web.archive.org/web/20070104202529/http://solutions.3m.com:80/wps/portal/3M/en_US/ergonomics/home/products/ergonomicmouse/", - "http://research.microsoft.com/en-us/um/people/bibuxton/buxtoncollection/default.aspx", - "http://research.microsoft.com/en-us/um/people/bibuxton/buxtoncollection/contact.aspx", - "http://research.microsoft.com/en-us/um/people/bibuxton/buxtoncollection/acknowledgements.aspx", - "3M_2006_Catalogue.pdf", - "http://research.microsoft.com/en-us/um/people/bibuxton/buxtoncollection/browse.aspx", - "https://web.archive.org/web/20070106100452/http://solutions.3m.com/wps/portal/3M/en_US/ergonomics/home/products/ergonomicmouse/factsheet/" - ], - "captions": [ - "A view of the 3M Ergonomic Mouse in the grasp of the right hand, with the thumb activating the button at the top of the stem.", - "A view of the 3M Ergonomic Mouse standing alone.", - "EM500 Ergonomic web page, Jan, 2007", - "EM500 Ergonomic Mouse Fact Sheet from 3M web site, Jan, 2007", - "Entry for the EM500 Ergonomic Mouse in the 2006 3M catalogue, p. 18.(To access full document, click on image.)" - ], - "embeddedFileNames": [ - "3MErgo_01.JPG", - "3MErgo_02.JPG", - "3MErgo_Web_Jan_2007.JPG", - "3MErgo_Web_Fact Sheet_Jan_2007.jpg", - "3M_2006_Catalogue_p18.jpg" - ], - "__images": [ - "/files/images/buxton/upload_4e56b4a5-8324-47ca-9454-a8802021b652.jpg", - "/files/images/buxton/upload_743fe56a-d682-4ab5-b73e-f6abba2d8d3b.jpg", - "/files/images/buxton/upload_a8745808-f9df-4157-8cbe-e6e8ec346004.jpg", - "/files/images/buxton/upload_769673c7-5968-47a9-b75d-1ee047aa4c49.jpg", - "/files/images/buxton/upload_a08b85cf-d3ee-47f0-82ec-0527cec63e01.jpg", - "/files/images/buxton/upload_cf9b360a-9716-4cb1-930f-9c467a6fcd47.jpg" - ], - "title": "3M EM500 Ergonomic Mouse", - "company": "3M", - "year": 2006, - "primaryKey": "Mouse", - "secondaryKey": "Blank", - "attribute": "Blank", - "originalPrice": 72.5, - "degreesOfFreedom": 2, - "shortDescription": "Despite its form-factor suggesting that this is a joystick, it is actually a mouse. Ergonomic concerns drove this design. It forces the hand to assume a “thumb up” posture of the hand. This in turn reduces constriction of blood-flow through the relatively narrow channel of the wrist and reduces tension in the forearm. The compromise of this, however, is that one loses some fine-motion control which might otherwise have been possible using the fingers, rather than relying more on the wrist and forearm.", - "longDescription": "Despite its form-factor suggesting that this is a joystick, it is actually a mouse. Ergonomic concerns drove this design. It forces the hand to assume a “thumb up” posture of the hand. This in turn reduces constriction of blood-flow through the relatively narrow channel of the wrist and reduces tension in the forearm. The joystick also came in two sizes so as to better accommodate different hand sizes. There almost always trade-offs in design. Often more than one. In this case, one compromise is a loss of some fine-motion control which might otherwise have been possible using the fingers, rather than relying more on the wrist and forearm. Another is the increased time/attention required when moving from the keyboard to the mouse. To do a simple test, move your hand to a conventional mouse. Now place a water glass which is taller than it is wide in the same position as the mouse, and compare how fast you can get it “in hand” enough to control its movement as well as you could the mouse. Try the same thing while not looking – using motor memory. Then consider how often you make that change. And, by the same token, consider how many different ways you can hold your mouse while still using it, versus a joystick shaped mouse with the button on top. In none of this am I complaining about, nor criticizing this mouse design. Rather, I am trying to illustrate that there is a lot to consider, and as either a designer or consumer, these are things to train oneself to notice and question. From such experience emerges the basis for better choices in both design and purchase. ." - }, - { - "hyperlinks": [ - "http://research.microsoft.com/en-us/um/people/bibuxton/buxtoncollection/default.aspx", - "Abaton_ProPoint_Brochure.pdf", - "http://research.microsoft.com/en-us/um/people/bibuxton/buxtoncollection/default.aspx", - "http://research.microsoft.com/en-us/um/people/bibuxton/buxtoncollection/contact.aspx", - "http://research.microsoft.com/en-us/um/people/bibuxton/buxtoncollection/acknowledgements.aspx", - "http://research.microsoft.com/en-us/um/people/bibuxton/buxtoncollection/browse.aspx" - ], - "captions": [ - "An upper-left view of the Abaton ProPoint trackball.", - "Left side view of the Abaton ProPoint trackball showing the Apple Desktop Bus (ADB) socket.", - "Front-side view of the Abaton ProPoint trackball..", - "Back-side view of the Abaton ProPoint trackball.", - "Bottom view of the Abaton ProPoint trackball showing the serial number, etc.", - "View of the Abaton ProPoint with the trackball removed. The two shaft encoders that sense rotation in X and Y, respectively can be seen.", - "First page of the Abatron ProPoint Trackball product sheet.(Click on image to access full document.)" - ], - "embeddedFileNames": [ - "Abaton_0105.JPG", - "Abaton_0098.JPG", - "Abaton_0099.JPG", - "Abaton_0100.JPG", - "Abaton_0103.JPG", - "Abaton_0106.JPG", - "Abaton_ProPoint_Brochure.jpg" - ], - "__images": [ - "/files/images/buxton/upload_b038a3ce-6ab6-40fc-8ac9-802e8e0be599.jpg", - "/files/images/buxton/upload_84a23406-fc2c-4536-9894-d846c7970931.jpg", - "/files/images/buxton/upload_9b7b420e-ddfb-4d9d-ae63-f54b27862457.jpg", - "/files/images/buxton/upload_2e3fb2af-f3b9-45c5-90a9-f7986ce791d4.jpg", - "/files/images/buxton/upload_d4bde3ed-1712-4e47-8221-49fd2053c07a.jpg", - "/files/images/buxton/upload_a858df0c-e87c-4a23-a397-5fae47ff2ef2.jpg", - "/files/images/buxton/upload_b0b9fdcd-cd0f-4bcb-9687-bb24b9224ec9.jpg" - ], - "title": "Abaton ProPoint Optical Trackball", - "company": "Abaton", - "year": 1989, - "primaryKey": "Trackball", - "secondaryKey": "Blank", - "attribute": "Blank", - "originalPrice": 140, - "degreesOfFreedom": 2, - "shortDescription": "A relatively early trackball for the Apple Macintosh computer. Note the positioning of the buttons, which bias the device for right-handed use, and using the thumb for the buttons and fingers for manipulating the ball.", - "longDescription": "A relatively early trackball for the Apple Macintosh computer. The larger button was functionally equivalent to the mouse button, and the smaller one was a ‘lock’ button. The lock button is analogous to the SHIFT LOCK or CAPS LOCK on a QWERTY keyboard: With the keyboard, it means that one does not have to hold the SHIFT key down while typing a string of upper-case characters. With the trackball, it means that you don’t have to hold the primary “mouse” button down while rolling the trackball. The need that this meets can be easily be seen if one compares the relative difficulty of drawing a line by moving a mouse while holding down its button, compared to doing the same task with a trackball. With the mouse, the wrist and forearm mainly move the mouse, and the fingers are used to hold it, as well as the button. With the trackball, the fingers are engaged in rolling the ball as well as holding the button. Hence, the probability of task interference is high, just as with typing a string of upper-case characters on a keyboard without a SHIFT LOCK key. Next, in looking at trackballs, pay attention to the position of the buttons relative to the trackball itself. How this relationship varies across devices and says a lot about how the designers envisioned the device being used. For example, the relationship may indicate the design intends for the thumb or fingers to operate the ball. How does the relationship impact the device’s ability to accommodate left and right hand usage equally well? This latter point is especially important when the trackball is used simultaneously with a mouse. An example would be if the trackball was used to scroll a spreadsheet up-down / left-right, while the mouse was used to point, select, and/or drag. In this case, for example, the trackball would usually be operated by the non-dominant hand and the mouse by the dominant one. Yet, when used alone, the same user would typically operate the trackball with the dominant hand. The lesson from this example is the recognition that handedness is a factor of use (one vs two handed), not just a factor of whether the user’s left or right hand is dominant." - }, - { - "hyperlinks": [ - "http://research.microsoft.com/en-us/um/people/bibuxton/buxtoncollection/browse.aspx", - "Active_Book_Brochure.pdf", - "http://research.microsoft.com/en-us/um/people/bibuxton/buxtoncollection/default.aspx", - "http://www.computinghistory.org.uk/det/21617/Active-Book-Prototype-Circuit-Boards/", - "http://research.microsoft.com/en-us/um/people/bibuxton/buxtoncollection/contact.aspx", - "http://research.microsoft.com/en-us/um/people/bibuxton/buxtoncollection/acknowledgements.aspx" - ], - "captions": [ - "View of the Active Book prototype.", - "Front page of the Active Book brochure.(Click on image to access full document.)" - ], - "embeddedFileNames": [ - "Active_1206.JPG", - "Active_Book_Brochure_p1.jpg" - ], - "__images": [ - "/files/images/buxton/upload_27da6de1-8275-4b87-add8-5d6cf20dc353.jpg", - "/files/images/buxton/upload_a8df63c9-acb7-429f-9a86-8dfd53eac317.png" - ], - "title": "Active Book Company Active Book Prototype", - "company": "Active Book Company", - "year": 1991, - "primaryKey": "Computer", - "secondaryKey": "Blank", - "attribute": "Prototype", - "originalPrice": -1, - "degreesOfFreedom": 2, - "shortDescription": "This device is a prototype pen computer, The Active Book, which was developed in Cambridge, UK, by The Active Book Company. It had a strong focus on user experience, and like the equally ill-fated Momenta pen computer, was implemented using the pioneering object-oriented language Smalltalk. At about the time that this working prototype was built, near going into production, the company was bought and merged with EO, and the Active Book never went into production. This is an exceptionally rare piece of the history of pen computing.", - "longDescription": "This device is a prototype pen computer, The Active Book, which was developed in Cambridge, UK, by The Active Book Company. It had a strong focus on user experience, and like the equally ill-fated Momenta pen computer, was implemented using the pioneering object-oriented language Smalltalk. At about the time that this working prototype was built, near going into production, the company was bought and merged with EO, and the Active Book never went into production. This is an exceptionally rare piece of the history of pen computing. ." - }, - { - "hyperlinks": [ - "http://research.microsoft.com/en-us/um/people/bibuxton/buxtoncollection/default.aspx", - "http://research.microsoft.com/en-us/um/people/bibuxton/buxtoncollection/contact.aspx", - "http://research.microsoft.com/en-us/um/people/bibuxton/buxtoncollection/acknowledgements.aspx", - "https://web.archive.org/web/20030621224236/http://www.adesso.us:80/product_details.asp?dept_id=106&pf_id=KA33ACK-540", - "http://research.microsoft.com/en-us/um/people/bibuxton/buxtoncollection/browse.aspx", - "http://research.microsoft.com/en-us/um/people/bibuxton/buxtoncollection/default.aspx", - "https://web.archive.org/web/20030507032656/http://www.adesso.us:80/product_details.asp?dept_id=106&pf_id=KA33ACK-540PB" - ], - "captions": [ - "Top view of the Adesso Mini-Touch Keyboard with Touchpad.", - "Side view of the Adesso Mini-Touch Keyboard with Touchpad.", - "Adesso 2003 product web page for the black 540_PB model of the Mini Touchpad Keyboard.", - "Adesso 2003 product web page for the white 540_PW model of the Mini Touchpad Keyboard" - ], - "embeddedFileNames": [ - "Adesso_540_Top.JPG", - "Adesso_540_Side.JPG", - "Adesso_ACK-540PB_May_7_2003.jpg", - "Adesso_ACK-540PW_June 21_2003.jpg" - ], - "__images": [ - "/files/images/buxton/upload_2d07ac10-f9cd-4c51-a6d6-9367699a661e.jpg", - "/files/images/buxton/upload_8e8a8720-ec0a-44e2-8d38-bc280cf55b08.jpg", - "/files/images/buxton/upload_341a5e96-59b5-4f3a-9572-5693b2f7f4fa.jpg", - "/files/images/buxton/upload_c6177050-e805-4ad4-ba69-f58047bde8e7.jpg", - "/files/images/buxton/upload_234bd02a-a0c3-4c07-933a-4457b115362c.jpg" - ], - "title": "Adesso ACK-540PB PS/2 Mini PS/2 Touchpad Keyboard", - "company": "Adesso", - "year": 2003, - "primaryKey": "Keyboard", - "secondaryKey": "Touchpad", - "attribute": "Blank", - "originalPrice": 69.95, - "degreesOfFreedom": 2, - "dimensions": { - "dim_length": 287, - "dim_width": 140, - "dim_height": 35.5, - "dim_unit": "mm" - }, - "shortDescription": "The Mini-Touch Keyboard is a small-footprint keyboard with a centrally mounted touchpad. It initially was released with a PS/2 connector, and then in 2006 the connector was updated to USB. While keyboards with integrated touchpads had been available since the mid-1980s, small-footprint ones with centrally mounted touch pads were far less common.", - "longDescription": "Released in 2003, this is a small add-on keyboard with an integrated touchpad. While there had been keyboards released with touchpads earlier – see the 1985 KeyTronic LT Touchpad Keyboard in the collection, for example – these were full-sized keyboard, typically with the touchpad mounted at the side, rather than the middle. Keyboards such as the Adesso ACK-540PB, were styled after the smaller foot-print keyboards then becoming standard on laptops, in terms of the central placement of the touchpad, as well as size. This central placement was significant, since it gave equal access to either right or left hand. The touchpad used was a Glidepoint, a 1994 stand-alone version of which is in the collection, the 1994 Cirque Glidepoint. This first model of the ACK-540 was released in both black (ACK-540PB) and white (ACK-540PW) and came with a PS/2 connector. In 2006, black and white versions updated with a USB connectors were released (the ACK-540UB and ACK-540UW) were released – an indication that the product had sustained a place in the market. These same keyboards were also marketed under different brand names, including SolidTek and Daltaco." - }, - { - "hyperlinks": [ - "https://web.archive.org/web/20070902035057/http://www.adesso.com/new_arrival.asp", - "Adesso_KP_Mouse_11_Product.pdf", - "http://research.microsoft.com/en-us/um/people/bibuxton/buxtoncollection/default.aspx", - "http://research.microsoft.com/en-us/um/people/bibuxton/buxtoncollection/contact.aspx", - "Adesso_KP_Mouse_10.pdf", - "http://www.notebookreview.com/review/adesso-usb-numeric-keypad-and-optical-mouse-review/", - "http://research.microsoft.com/en-us/um/people/bibuxton/buxtoncollection/acknowledgements.aspx", - "http://research.microsoft.com/en-us/um/people/bibuxton/buxtoncollection/browse.aspx", - "https://web.archive.org/web/20071025131401/http://www.adesso.com/products_detail.asp?productid=363", - "http://research.microsoft.com/en-us/um/people/bibuxton/buxtoncollection/default.aspx" - ], - "captions": [ - "An overview of the Adesso AKP-170 Mouse with the transparent hinged keypad cover open. Note that the mouse controls can still be accessed.", - "The Adesso AKP-170 Mouse with the transparent hinged keypad cover in its default closed position.", - "The 1990 Sony RMT-V5A Space Control. Note how the hinged cover of the Adesso mouse echoes this design. Stepping outside of one’s product class can provide a rich source of relevant ideas.", - "A view of the Adesso AKP-170 Mouse from the upper left side.", - "A front-end view of the Adesso AKP-170 Mouse.", - "A view of the bottom of the Adesso AKP-170 Mouse, showing serial number, etc.", - "A front view of the Adesso AKP-170 Mouse in its original box.", - "A back-side view of the original box for the Adesso AKP-170 Mouse.", - "1-page user manual for the Adesso AKP-170 Mouse.", - "Page 1 of a review of the Adesso AKP-170 from the Dec. 2007 Notebook Review web page. (Click on image to access full document.)", - "First page of Adesso Oct. 25, 2007 Product Web Page for the AKP-170 Mouse.(Click on image to access full document.)" - ], - "embeddedFileNames": [ - "Adesso_KP_Mouse_01.JPG", - "Adesso_KP_Mouse_02.JPG", - "Adesso_KP_Mouse_03.JPG", - "Adesso_KP_Mouse_04.JPG", - "Adesso_KP_Mouse_05.JPG", - "Adesso_KP_Mouse_06.JPG", - "Adesso_KP_Mouse_07.JPG", - "Adesso_KP_Mouse_08.JPG", - "Adesso_KP_Mouse_09.JPG", - "Adesso_KP_Mouse_10.JPG", - "Adesso_KP_Mouse_11_Product.JPG" - ], - "__images": [ - "/files/images/buxton/upload_dc960b02-0ffb-4d43-be53-e44e79ca29d2.jpg", - "/files/images/buxton/upload_0d60cf7f-5bb6-4a33-b219-d04a5d6fb85e.jpg", - "/files/images/buxton/upload_0d3fd05f-71ad-405b-8d3b-bf66c38db405.jpg", - "/files/images/buxton/upload_89334d96-d365-4a11-9e88-cc93abeaf729.jpg", - "/files/images/buxton/upload_48d36841-0270-4697-9222-6ef53f9650b3.jpg", - "/files/images/buxton/upload_dd63a33a-c85d-4fd9-a54e-4dfcbdc9af92.jpg", - "/files/images/buxton/upload_f34c3e06-32ee-4baf-8f6d-864b3e3ed97a.jpg", - "/files/images/buxton/upload_98216eac-1c56-47ee-99d9-530dfb2efa2d.jpg", - "/files/images/buxton/upload_87dee921-aa24-4598-8d21-b16929d4fd5d.jpg", - "/files/images/buxton/upload_f4f73b8f-8c12-4f35-947b-258d8c88f133.jpg", - "/files/images/buxton/upload_f325d49e-8ebb-4428-bdbc-db89010d8e27.jpg" - ], - "title": "Adesso 2-in-1 Optical Keypad Calculator Mouse AKP-170", - "company": "Adesso Inc", - "year": 2007, - "primaryKey": "Mouse", - "secondaryKey": "Keypad", - "attribute": "Blank", - "originalPrice": 29.99, - "degreesOfFreedom": 3, - "dimensions": { - "dim_length": 120.7, - "dim_width": 57.15, - "dim_height": 38.1, - "dim_unit": "mm" - }, - "shortDescription": "This is a mouse / keypad hybrid. With the transparent hinged cover down, it functions like a conventional scroll-wheel optical mouse. All the while, its additional capability as a numerical keypad / calculator is visible, and physically accessed by flipping up the cover. Since the design affords access to the mouse buttons and scroll wheel with the lid open, the opportunity to select cells in a spreadsheet, for example, and enter numbers without moving between the traditional keyboard and mouse is provided.", - "longDescription": "This is a mouse / numerical keypad hybrid. With the transparent hinged cover down, it functions like a conventional scroll-wheel optical mouse. All the while, its additional capability as a numerical keypad / calculator is visible, and physically accessed by flipping up the cover. Since the design affords access to the mouse buttons and scroll wheel with the lid open, the opportunity to select cells in a spreadsheet, for example, and enter numbers without moving between the traditional keyboard and mouse is provided. There are a few mice with integrated keypads included in the collection. Each takes a different approach in terms of intent as well as industrial design. Comparing them is a worthwhile exercise. By the same token, the approach taken by this example echoes that taken by a very different product, but with the same intent: layer complexity. In this case the president from the collection is a TV/VCR remote control: the 1990 Sony RMT-V5A. For comparison, see the accompanying photo, and for more information, look at the Sony’s detailed device description. Finally, in drawing attention to this specific example rather than one of the hybrid mouse/keypads referred to earlier, the intent is to show that inspiration for design solutions for one class of device can come from those of very different categories. For sure, look at previous in-class solutions. But that just puts you on par with most other designers. The best exercise their creativity by looking in far less explored places. Note to the sharp eyed: If you look at the bottom of the mouse, you will see it marked model KM-1411, while I have been referring to it, as well as the Adesso Web Site, as model AKP-170. Rest assured, these are the same thing, as can be seen on the bottom right corner of the back of the box, where both numbers appear. I too am confused as to why, but also reassured. ." - }, - { - "hyperlinks": [ - "https://web.archive.org/web/20051210101548/http://www.a4tech.com/en/product1.asp?CID=90&SCID=92", - "http://a4tech.com/product.asp?cid=142&scid=122&id=342", - "http://research.microsoft.com/en-us/um/people/bibuxton/buxtoncollection/default.aspx", - "https://microsoft-my.sharepoint.com/personal/bibuxton_microsoft_com/Documents/Buxton%20Collection/Collection/To%20Shoot/A4_Tech_NB-75D%20Mouse/NB75D_Mouse_Manual.pdf", - "http://www.ubergizmo.com/2007/11/a4tech-battery-free-wireless-mouse/", - "BATTERYfree_Mouse_Data_Sheet.pdf", - "http://research.microsoft.com/en-us/um/people/bibuxton/buxtoncollection/contact.aspx", - "https://web.archive.org/web/20060707175129/http://ergoshops.com:80/store.asp?CID=200209271427192&SCID=200410281216151", - "http://research.microsoft.com/en-us/um/people/bibuxton/buxtoncollection/acknowledgements.aspx", - "NB75D_Mouse_Manual.pdf", - "http://research.microsoft.com/en-us/um/people/bibuxton/buxtoncollection/browse.aspx", - "https://web.archive.org/web/20060211161019/http://www.a4tech.com/en/product2.asp?CID=90&SCID=92&MNO=NB-75", - "http://research.microsoft.com/en-us/um/people/bibuxton/buxtoncollection/default.aspx" - ], - "captions": [ - "The A4 Tech Model NB-75D optical mouse on its charging mouse pad.", - "Front view of the A4 Tech NB-75D mouse beside its A4 Tech IRW-5 4D companion. Note the difference in the second scroll-wheel. See text for discussion.", - "Top view of the A4 Tech NB-75D mouse beside its A4 Tech IRW-5 4D companion.", - "Upper front quarter view of the A4 Tech Model NB-75D optical mouse.", - "Front view of the A4 Tech Model NB-75D optical mouse.", - "Upper rear-quarter view of the A4 Tech Model NB-75D optical mouse.", - "Top view of the A4 Tech Model NB-75D optical mouse.", - "The A4 Tech Model NB-75D optical mouse on its charging mouse pad, showing the mouse pad USB connector.", - "The A4 Tech Model NB-75D product web page.", - "Front page of Jandata sheet for the A4 BATTERYfree optical mouse product line. From A4 Tech web site.", - "The A4 Tech Model NB-75D User’s Guide.(Click on image to access full document.)" - ], - "embeddedFileNames": [ - "NB75D_IMG_0682.JPG", - "NB75D_IMG_0666.JPG", - "NB75D_IMG_0676.JPG", - "NB75D_IMG_0671.JPG", - "NB75D_IMG_0672.JPG", - "NB75D_IMG_0673.JPG", - "NB75D_IMG_0679.JPG", - "NB75D_IMG_2016.JPG", - "NB75D_Product_Page.jpg", - "BATTERYfree_Mouse_Data_Sheet.jpg", - "NB75D_Mouse_Manual.jpg" - ], - "__images": [ - "/files/images/buxton/upload_7b16bc25-7d85-4da9-abc4-52ed356d2209.jpg", - "/files/images/buxton/upload_426c9b53-77f1-42d9-b75b-3eff2367ade3.jpg", - "/files/images/buxton/upload_d5d6ba18-9cf1-44d3-9301-bf37b0b1743f.jpg", - "/files/images/buxton/upload_de812753-cf90-4564-aad4-dd4ab798b383.jpg", - "/files/images/buxton/upload_1b253c1d-0bd2-42d4-a152-0ba74f854fc2.jpg", - "/files/images/buxton/upload_e1989f54-14e3-42b1-bff2-473c08826d56.jpg", - "/files/images/buxton/upload_9a1baab0-ae19-4780-b86b-9944ceda75cc.jpg", - "/files/images/buxton/upload_ca64659c-3df7-4686-b920-e508980e7c4a.jpg", - "/files/images/buxton/upload_df243abf-5e27-4f9c-93f1-9a68e81248b0.jpg", - "/files/images/buxton/upload_257c0440-2912-4df8-af80-7543789f4f80.jpg", - "/files/images/buxton/upload_1810191c-c527-4989-85ab-c83fa91439aa.jpg", - "/files/images/buxton/upload_101c36d0-e88e-491a-bbd6-706dd7e99261.jpg" - ], - "title": "A4 Tech BatteryFREE Wireless Optical Mouse Model NB-75D", - "company": "A4Tech", - "year": 2005, - "primaryKey": "Mouse", - "secondaryKey": "Scroll-wheel", - "attribute": "Blank", - "originalPrice": 35.99, - "degreesOfFreedom": 4, - "shortDescription": "This is a little-known innovative mouse worth study. It is one of the first to have wireless charging – by sitting on its mouse pad. It has two scroll wheels which are mounted at right angles – like an Etch-a-Sketch, to conform to the direction to be scrolled (up/down vs left/right. It also has a dedicated button which “double clicks” with one push.", - "longDescription": "This mouse fell below the radar, and yet it had remarkable innovations in its design, especially given its price. Like any mouse, it provided 2 degrees of freedom for pointing. Unlike most scroll-wheel mouse, it had two separate scroll wheels mounted at right angles. Hence, the orientation of each wheel provided a cue as to which direction the affected document would scroll – up/down or left/right. It is interesting to consider this scroll-wheel layout with that on a companion mouse, the A4 Tech Model IRW-5 4D Wireless Mouse, also in the collection, and shown side-by-side in one of the accompanying photographs. While the scroll wheels of the NF-75 are at right angles, those of the IRW-5 are parallel. Such differences should always provoke the question “Why? ” – for would-be designer and consumer alike. As a memory aid, and the conform to what psychologists call “stimulus-response (S: -R) compatibility”, the right-angle arrangement seems to be far better mapping of action to effect. On the other hand, try an experiment. First, place your hand, palm facing down, on a desktop as if you were holding a mouse. All fingers and thumb should be touching the desktop, but not your palm. Maintaining that position, left your index (scrolling) finger, and repeatedly “fold” and “unfold” it between a pointing posture, and touching your palm. Next, keeping your hand in the same position, this time, again extend your index finger. But this time, move its tip back-and-forth in a left-right motion. What I hope you quickly perceive is that the finger was “optimized” for the folding rather than lateral motion. You “feel” that from the difference in tension in the hand in the two cases of the “study”. The two conditions of our “quick” study roughly mimic the motor actions required to operate the scroll wheels. Since the brief test suggests the possibility that repeated lateral movement may accelerate the onset of repetitive stress injury, it would therefore also suggest that further studies should be undertaken before using a left-right scroll wheel – despite the acknowledged advantage with respect to S: R compatibility. In addition to the select button on virtually all mice, there was an additional button which issued a double-click on a single push. Hence, clicking one button would select, while pushing the other would select and open. While for many, the cost of an extra button may not seem worth saving the effort of a double click, those with some motor impairment may disagree. This is an example where knowing about such things may trigger useful insights to improve future designs. However, perhaps the biggest innovation with this line of A4 mice is that they were powered wirelessly. That is, there were no batteries in the mouse that might die at the wrong moment. Power was provided wirelessly from the mouse pad. Yet, one again we see that design is full of trade-offs. The cost of providing this feature is that of requiring not just the mouse pad, but a special one – something not overly attractive to road-warriors whose brief-cases were already weighted down by cables and other paraphernalia which took up more space than the laptop itself. ." - } -]
\ No newline at end of file diff --git a/src/scraping/buxton/final/json/incomplete.json b/src/scraping/buxton/final/json/incomplete.json deleted file mode 100644 index 0637a088a..000000000 --- a/src/scraping/buxton/final/json/incomplete.json +++ /dev/null @@ -1 +0,0 @@ -[]
\ No newline at end of file diff --git a/src/server/ApiManagers/UtilManager.ts b/src/server/ApiManagers/UtilManager.ts index 4cb57a4e7..8adc3da81 100644 --- a/src/server/ApiManagers/UtilManager.ts +++ b/src/server/ApiManagers/UtilManager.ts @@ -41,12 +41,6 @@ export default class UtilManager extends ApiManager { register({ method: Method.GET, - subscription: "/buxton", - secureHandler: async ({ res }) => res.send(await executeImport()) - }); - - register({ - method: Method.GET, subscription: "/version", secureHandler: ({ res }) => { return new Promise<void>(resolve => { diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts index 39e5538af..913ddc1c3 100644 --- a/src/server/DashUploadUtils.ts +++ b/src/server/DashUploadUtils.ts @@ -97,7 +97,7 @@ export namespace DashUploadUtils { } console.log(red(`Ignoring unsupported file (${name}) with upload type (${type}).`)); - return { accessPaths: undefined }; + return { accessPaths: {} }; } async function UploadPdf(absolutePath: string) { @@ -216,13 +216,15 @@ export namespace DashUploadUtils { }; }; - export async function MoveParsedFile(absolutePath: string, destination: Directory): Promise<Opt<AccessPathInfo>> { - return new Promise<Opt<AccessPathInfo>>(resolve => { + export async function MoveParsedFile(absolutePath: string, destination: Directory): Promise<Opt<{ accessPaths: AccessPathInfo }>> { + return new Promise(resolve => { const filename = basename(absolutePath); const destinationPath = serverPathToFile(destination, filename); rename(absolutePath, destinationPath, error => { resolve(error ? undefined : { - agnostic: getAccessPaths(destination, filename) + accessPaths: { + agnostic: getAccessPaths(destination, filename) + } }); }); }); diff --git a/src/server/Message.ts b/src/server/Message.ts index 79b6fa1e0..2a03e2311 100644 --- a/src/server/Message.ts +++ b/src/server/Message.ts @@ -1,4 +1,5 @@ import { Utils } from "../Utils"; +import { AnalysisResult, ImportResults } from "../scraping/buxton/final/BuxtonImporter"; export class Message<T> { private _name: string; @@ -56,6 +57,9 @@ export namespace MessageStore { export const GetDocument = new Message<string>("Get Document"); export const DeleteAll = new Message<any>("Delete All"); export const ConnectionTerminated = new Message<string>("Connection Terminated"); + export const BeginBuxtonImport = new Message<string>("Begin Buxton Import"); + export const BuxtonDocumentResult = new Message<AnalysisResult>("Buxton Document Result"); + export const BuxtonImportComplete = new Message<ImportResults>("Buxton Import Complete"); export const GetRefField = new Message<string>("Get Ref Field"); export const GetRefFields = new Message<string[]>("Get Ref Fields"); @@ -65,5 +69,4 @@ export namespace MessageStore { export const DeleteField = new Message<string>("Delete field"); export const DeleteFields = new Message<string[]>("Delete fields"); - } diff --git a/src/server/Websocket/Websocket.ts b/src/server/Websocket/Websocket.ts index ba7ca8f35..6ee571395 100644 --- a/src/server/Websocket/Websocket.ts +++ b/src/server/Websocket/Websocket.ts @@ -11,7 +11,7 @@ import { logPort } from "../ActionUtilities"; import { timeMap } from "../ApiManagers/UserManager"; import { green } from "colors"; import { networkInterfaces, type } from "os"; -import { object } from "serializr"; +import executeImport from "../../scraping/buxton/final/BuxtonImporter"; export namespace WebSocket { @@ -106,6 +106,12 @@ export namespace WebSocket { Utils.AddServerHandler(socket, MessageStore.DeleteFields, ids => DeleteFields(socket, ids)); Utils.AddServerHandlerCallback(socket, MessageStore.GetRefField, GetRefField); Utils.AddServerHandlerCallback(socket, MessageStore.GetRefFields, GetRefFields); + Utils.AddServerHandler(socket, MessageStore.BeginBuxtonImport, () => { + executeImport( + deviceOrError => Utils.Emit(socket, MessageStore.BuxtonDocumentResult, deviceOrError), + results => Utils.Emit(socket, MessageStore.BuxtonImportComplete, results) + ); + }); disconnect = () => { socket.broadcast.emit("connection_terminated", Date.now()); diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts index ce4f94d83..32c375bd7 100644 --- a/src/server/authentication/models/current_user_utils.ts +++ b/src/server/authentication/models/current_user_utils.ts @@ -48,7 +48,7 @@ export class CurrentUserUtils { // setup the "creator" buttons for the sidebar-- eg. the default set of draggable document creation tools static setupCreatorButtons(doc: Doc, buttons?: string[]) { const notes = CurrentUserUtils.setupNoteTypes(doc); - const emptyPresentation = Docs.Create.PresDocument(new List<Doc>(), { title: "Presentation", _viewType: CollectionViewType.Stacking, _LODdisable: true, _chromeStatus: "replaced", showTitle: "title", boxShadow: "0 0" }); + const emptyPresentation = Docs.Create.PresDocument(new List<Doc>(), { title: "Presentation", _viewType: CollectionViewType.Stacking, _LODdisable: true, _chromeStatus: "replaced", _showTitle: "title", boxShadow: "0 0" }); const emptyCollection = Docs.Create.FreeformDocument([], { _nativeWidth: undefined, _nativeHeight: undefined, _LODdisable: true, _width: 150, _height: 100, title: "freeform" }); doc.noteTypes = Docs.Create.TreeDocument(notes, { title: "Note Types", _height: 75 }); doc.activePen = doc; @@ -139,7 +139,7 @@ export class CurrentUserUtils { static setupThumbDoc(userDoc: Doc) { if (!userDoc.thumbDoc) { userDoc.thumbDoc = Docs.Create.LinearDocument(CurrentUserUtils.setupThumbButtons(userDoc), { - _width: 100, _height: 50, ignoreClick: true, lockedPosition: true, _chromeStatus: "disabled", title: "buttons", _autoHeight: true, _yMargin: 5, isExpanded: true, backgroundColor: "white" + _width: 100, _height: 50, ignoreClick: true, lockedPosition: true, _chromeStatus: "disabled", title: "buttons", _autoHeight: true, _yMargin: 5, linearViewIsExpanded: true, backgroundColor: "white" }); } return userDoc.thumbDoc; @@ -181,12 +181,12 @@ export class CurrentUserUtils { }); doc.documents = Docs.Create.TreeDocument([], { - title: "DOCUMENTS", _height: 42, forceActive: true, boxShadow: "0 0", preventTreeViewOpen: true, lockedPosition: true, backgroundColor: "#eeeeee" + title: "DOCUMENTS", _height: 42, forceActive: true, boxShadow: "0 0", treeViewPreventOpen: true, lockedPosition: true, backgroundColor: "#eeeeee" }); // setup Recently Closed library item doc.recentlyClosed = Docs.Create.TreeDocument([], { - title: "RECENTLY CLOSED", _height: 75, forceActive: true, boxShadow: "0 0", preventTreeViewOpen: true, lockedPosition: true, backgroundColor: "#eeeeee" + title: "RECENTLY CLOSED", _height: 75, forceActive: true, boxShadow: "0 0", treeViewPreventOpen: true, lockedPosition: true, backgroundColor: "#eeeeee" }); return Docs.Create.ButtonDocument({ @@ -255,7 +255,7 @@ export class CurrentUserUtils { { _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100, dropAction: "alias", onDragStart: ScriptField.MakeFunction('getCopy(this.dragFactory, true)'), dragFactory: slideTemplate, removeDropProperties: new List<string>(["dropAction"]), title: "presentation slide", icon: "sticky-note" }); doc.expandingButtons = Docs.Create.LinearDocument([doc.undoBtn as Doc, doc.redoBtn as Doc, doc.slidesBtn as Doc], { title: "expanding buttons", _gridGap: 5, _xMargin: 5, _yMargin: 5, _height: 42, _width: 100, boxShadow: "0 0", - backgroundColor: "black", preventTreeViewOpen: true, forceActive: true, lockedPosition: true, + backgroundColor: "black", treeViewPreventOpen: true, forceActive: true, lockedPosition: true, dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name }) }); @@ -271,7 +271,7 @@ export class CurrentUserUtils { // the initial presentation Doc to use static setupDefaultPresentation(doc: Doc) { doc.presentationTemplate = new PrefetchProxy(Docs.Create.PresElementBoxDocument({ backgroundColor: "transparent", _xMargin: 5, _height: 46, isTemplateDoc: true, isTemplateForField: "data" })); - doc.curPresentation = Docs.Create.PresDocument(new List<Doc>(), { title: "Presentation", _viewType: CollectionViewType.Stacking, _LODdisable: true, _chromeStatus: "replaced", showTitle: "title", boxShadow: "0 0" }); + doc.curPresentation = Docs.Create.PresDocument(new List<Doc>(), { title: "Presentation", _viewType: CollectionViewType.Stacking, _LODdisable: true, _chromeStatus: "replaced", _showTitle: "title", boxShadow: "0 0" }); } static setupMobileUploads(doc: Doc) { |