diff options
Diffstat (limited to 'src/client/views/nodes/ImageBox.tsx')
-rw-r--r-- | src/client/views/nodes/ImageBox.tsx | 153 |
1 files changed, 118 insertions, 35 deletions
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 9590bcb15..f38ebba27 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -8,15 +8,16 @@ import { List } from '../../../fields/List'; import { ObjectField } from '../../../fields/ObjectField'; import { createSchema } from '../../../fields/Schema'; import { ComputedField } from '../../../fields/ScriptField'; -import { Cast, NumCast } from '../../../fields/Types'; +import { BoolCast, Cast, NumCast, StrCast } from '../../../fields/Types'; import { ImageField } from '../../../fields/URLField'; import { TraceMobx } from '../../../fields/util'; -import { emptyFunction, OmitKeys, returnFalse, returnOne, setupMoveUpEvents, Utils } from '../../../Utils'; +import { DashColor, emptyFunction, returnEmptyString, returnFalse, returnOne, returnZero, setupMoveUpEvents, Utils } from '../../../Utils'; import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils'; import { CognitiveServices, Confidence, Service, Tag } from '../../cognitive_services/CognitiveServices'; -import { DocUtils } from '../../documents/Documents'; +import { Docs, DocUtils } from '../../documents/Documents'; import { DocumentType } from '../../documents/DocumentTypes'; import { Networking } from '../../Network'; +import { DocumentManager } from '../../util/DocumentManager'; import { DragManager } from '../../util/DragManager'; import { undoBatch } from '../../util/UndoManager'; import { ContextMenu } from '../../views/ContextMenu'; @@ -26,10 +27,13 @@ import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../DocComp import { MarqueeAnnotator } from '../MarqueeAnnotator'; import { AnchorMenu } from '../pdf/AnchorMenu'; import { StyleProp } from '../StyleProvider'; +import { DocFocusOptions, OpenWhere } from './DocumentView'; import { FaceRectangles } from './FaceRectangles'; import { FieldView, FieldViewProps } from './FieldView'; import './ImageBox.scss'; +import { PinProps, PresBox } from './trails'; import React = require('react'); +import Color = require('color'); export const pageSchema = createSchema({ googlePhotosUrl: 'string', @@ -48,44 +52,75 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp public static LayoutString(fieldKey: string) { return FieldView.LayoutString(ImageBox, fieldKey); } + private _ignoreScroll = false; + private _forcedScroll = false; private _dropDisposer?: DragManager.DragDropDisposer; private _disposers: { [name: string]: IReactionDisposer } = {}; - private _getAnchor: (savedAnnotations?: ObservableMap<number, HTMLDivElement[]>) => Opt<Doc> = () => undefined; + private _getAnchor: (savedAnnotations: Opt<ObservableMap<number, HTMLDivElement[]>>, addAsAnnotation: boolean) => Opt<Doc> = () => undefined; @observable _curSuffix = ''; @observable _uploadIcon = uploadIcons.idle; + constructor(props: any) { + super(props); + this.props.setContentView?.(this); + } + protected createDropTarget = (ele: HTMLDivElement) => { this._dropDisposer?.(); ele && (this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this), this.props.Document)); }; - setViewSpec = (anchor: Doc, preview: boolean) => {}; // sets viewing information for a componentview, typically when following a link. 'preview' tells the view to use the values without writing to the document - getAnchor = () => { - const anchor = this._getAnchor?.(this._savedAnnotations); - anchor && this.addDocument(anchor); - return anchor ?? this.rootDoc; + getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => { + const anchor = + this._getAnchor?.(this._savedAnnotations, false) ?? // use marquee anchor, otherwise, save zoom/pan as anchor + Docs.Create.ImageanchorDocument({ + title: 'ImgAnchor:' + this.rootDoc.title, + presPanX: NumCast(this.layoutDoc._panX), + presPanY: NumCast(this.layoutDoc._panY), + presViewScale: Cast(this.layoutDoc._viewScale, 'number', null), + presTransition: 1000, + unrendered: true, + annotationOn: this.rootDoc, + }); + if (anchor) { + if (!addAsAnnotation) anchor.backgroundColor = 'transparent'; + /* addAsAnnotation &&*/ this.addDocument(anchor); + PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), pannable: true } }, this.rootDoc); + return anchor; + } + return this.rootDoc; }; componentDidMount() { - this.props.setContentView?.(this); // bcz: do not remove this. without it, stepping into an image in the lightbox causes an infinite loop.... this._disposers.sizer = reaction( () => ({ forceFull: this.props.renderDepth < 1 || this.layoutDoc._showFullRes, - scrSize: this.props.ScreenToLocalTransform().inverse().transformDirection(this.nativeSize.nativeWidth, this.nativeSize.nativeHeight)[0] / this.nativeSize.nativeWidth, + scrSize: (this.props.ScreenToLocalTransform().inverse().transformDirection(this.nativeSize.nativeWidth, this.nativeSize.nativeHeight)[0] / this.nativeSize.nativeWidth) * NumCast(this.rootDoc._viewScale, 1), selected: this.props.isSelected(), }), - ({ forceFull, scrSize, selected }) => (this._curSuffix = this.fieldKey === 'icon' ? '_m' : forceFull ? '_o' : scrSize < 0.25 ? '_s' : scrSize < 0.5 ? '_m' : scrSize < 0.8 || !selected ? '_l' : '_o'), + ({ forceFull, scrSize, selected }) => (this._curSuffix = selected ? '_o' : this.fieldKey === 'icon' ? '_m' : forceFull ? '_o' : scrSize < 0.25 ? '_s' : scrSize < 0.5 ? '_m' : scrSize < 0.8 ? '_l' : '_o'), { fireImmediately: true, delay: 1000 } ); + const layoutDoc = this.layoutDoc; this._disposers.path = reaction( () => ({ nativeSize: this.nativeSize, width: this.layoutDoc[WidthSym]() }), ({ nativeSize, width }) => { - if (true || !this.layoutDoc._height) { + if (layoutDoc === this.layoutDoc || !this.layoutDoc._height) { this.layoutDoc._height = (width * nativeSize.nativeHeight) / nativeSize.nativeWidth; } }, { fireImmediately: true } ); + this._disposers.scroll = reaction( + () => this.layoutDoc._scrollTop, + s_top => { + this._forcedScroll = true; + !this._ignoreScroll && this._mainCont.current && (this._mainCont.current.scrollTop = NumCast(s_top)); + this._mainCont.current?.scrollTo({ top: NumCast(s_top) }); + this._forcedScroll = false; + }, + { fireImmediately: true } + ); } componentWillUnmount() { @@ -119,8 +154,25 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp @undoBatch resolution = () => (this.layoutDoc._showFullRes = !this.layoutDoc._showFullRes); + @undoBatch + setUseAlt = () => (this.layoutDoc[this.fieldKey + '-useAlt'] = !this.layoutDoc[this.fieldKey + '-useAlt']); @undoBatch + setNativeSize = action(() => { + const scaling = (this.props.DocumentView?.().props.ScreenToLocalTransform().Scale || 1) / NumCast(this.rootDoc._viewScale, 1); + const nscale = NumCast(this.props.PanelWidth()) / scaling; + const nh = nscale / NumCast(this.dataDoc[this.fieldKey + '-nativeHeight']); + const nw = nscale / NumCast(this.dataDoc[this.fieldKey + '-nativeWidth']); + this.dataDoc[this.fieldKey + '-nativeHeight'] = NumCast(this.dataDoc[this.fieldKey + '-nativeHeight']) * nh; + this.dataDoc[this.fieldKey + '-nativeWidth'] = NumCast(this.dataDoc[this.fieldKey + '-nativeWidth']) * nh; + this.rootDoc._panX = nh * NumCast(this.rootDoc._panX); + this.rootDoc._panY = nh * NumCast(this.rootDoc._panY); + this.dataDoc._panXMax = this.dataDoc._panXMax ? nh * NumCast(this.dataDoc._panXMax) : undefined; + this.dataDoc._panXMin = this.dataDoc._panXMin ? nh * NumCast(this.dataDoc._panXMin) : undefined; + this.dataDoc._panYMax = this.dataDoc._panYMax ? nw * NumCast(this.dataDoc._panYMax) : undefined; + this.dataDoc._panYMin = this.dataDoc._panYMin ? nw * NumCast(this.dataDoc._panYMin) : undefined; + }); + @undoBatch rotate = action(() => { const nw = NumCast(this.dataDoc[this.fieldKey + '-nativeWidth']); const nh = NumCast(this.dataDoc[this.fieldKey + '-nativeHeight']); @@ -138,7 +190,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp const cropping = Doc.MakeCopy(region, true); Doc.GetProto(region).lockedPosition = true; Doc.GetProto(region).title = 'region:' + this.rootDoc.title; - Doc.GetProto(region).isPushpin = true; + Doc.GetProto(region).followLinkToggle = true; this.addDocument(region); const anchx = NumCast(cropping.x); const anchy = NumCast(cropping.y); @@ -154,6 +206,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp const croppingProto = Doc.GetProto(cropping); croppingProto.annotationOn = undefined; croppingProto.isPrototype = true; + croppingProto.backgroundColor = undefined; croppingProto.proto = Cast(this.rootDoc.proto, Doc, null)?.proto; // set proto of cropping's data doc to be IMAGE_PROTO croppingProto.type = DocumentType.IMG; croppingProto.layout = ImageBox.LayoutString('data'); @@ -169,8 +222,12 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp croppingProto.panYMin = anchy / viewScale; croppingProto.panYMax = anchh / viewScale; if (addCrop) { - DocUtils.MakeLink({ doc: region }, { doc: cropping }, 'cropped image', ''); + DocUtils.MakeLink(region, cropping, { linkRelationship: 'cropped image' }); + cropping.x = NumCast(this.rootDoc.x) + this.rootDoc[WidthSym](); + cropping.y = NumCast(this.rootDoc.y); + this.props.addDocTab(cropping, OpenWhere.inParent); } + DocumentManager.Instance.AddViewRenderedCb(cropping, dv => setTimeout(() => (dv.ComponentView as ImageBox).setNativeSize(), 200)); this.props.bringToFront(cropping); return cropping; }; @@ -179,9 +236,11 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp const field = Cast(this.dataDoc[this.fieldKey], ImageField); if (field) { const funcs: ContextMenuProps[] = []; - funcs.push({ description: 'Rotate Clockwise 90', event: this.rotate, icon: 'expand-arrows-alt' }); - funcs.push({ description: `Show ${this.layoutDoc._showFullRes ? 'Dynamic Res' : 'Full Res'}`, event: this.resolution, icon: 'expand-arrows-alt' }); - funcs.push({ description: 'Copy path', event: () => Utils.CopyText(this.choosePath(field.url)), icon: 'expand-arrows-alt' }); + funcs.push({ description: 'Rotate Clockwise 90', event: this.rotate, icon: 'redo-alt' }); + funcs.push({ description: `Show ${this.layoutDoc._showFullRes ? 'Dynamic Res' : 'Full Res'}`, event: this.resolution, icon: 'expand' }); + funcs.push({ description: 'Set Native Pixel Size', event: this.setNativeSize, icon: 'expand-arrows-alt' }); + funcs.push({ description: `${this.layoutDoc[this.fieldKey + '-useAlt'] ? 'Show Alternate' : 'Show Primary'}`, event: this.setUseAlt, icon: 'image' }); + funcs.push({ description: 'Copy path', event: () => Utils.CopyText(this.choosePath(field.url)), icon: 'copy' }); if (!Doc.noviceMode) { funcs.push({ description: 'Export to Google Photos', event: () => GooglePhotos.Transactions.UploadImages([this.props.Document]), icon: 'caret-square-right' }); @@ -230,7 +289,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp const lower = url.href.toLowerCase(); if (url.protocol === 'data') return url.href; if (url.href.indexOf(window.location.origin) === -1) return Utils.CorsProxy(url.href); - if (!/\.(png|jpg|jpeg|gif|webp)$/.test(lower)) return url.href; //Why is this here + if (!/\.(png|jpg|jpeg|gif|webp)$/.test(lower)) return `/assets/unknown-file-icon-hi.png`; const ext = extname(url.href); return url.href.replace(ext, this._curSuffix + ext); @@ -246,6 +305,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp return !tags ? null : <img id={'google-tags'} src={'/assets/google_tags.png'} />; }; + getScrollHeight = () => (this.props.fitWidth?.(this.rootDoc) !== false && NumCast(this.rootDoc._viewScale, 1) === NumCast(this.rootDoc._viewScaleMin, 1) ? this.nativeSize.nativeHeight : undefined); + @computed private get considerDownloadIcon() { const data = this.dataDoc[this.fieldKey]; @@ -279,9 +340,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp setTimeout( action(() => { this._uploadIcon = idle; - if (data) { - dataDoc[this.fieldKey] = data; - } + data && (dataDoc[this.fieldKey] = data); }), 2000 ); @@ -309,11 +368,13 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp return paths.length ? paths : [Utils.CorsProxy('http://www.cs.brown.edu/~bcz/noImage.png')]; } + @observable _isHovering = false; // flag to switch between primary and alternate images on hover @computed get content() { TraceMobx(); - const srcpath = this.paths[0]; - const fadepath = this.paths[Math.min(1, this.paths.length - 1)]; + const backAlpha = DashColor(this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor)).alpha(); + const srcpath = this.layoutDoc.hideImage ? '' : this.paths[0]; + const fadepath = this.layoutDoc.hideImage ? '' : this.paths.lastElement(); const { nativeWidth, nativeHeight, nativeOrientation } = this.nativeSize; const rotation = NumCast(this.dataDoc[this.fieldKey + '-rotation']); const aspect = rotation % 180 ? nativeHeight / nativeWidth : 1; @@ -330,25 +391,24 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp } return ( - <div className="imageBox-cont" key={this.layoutDoc[Id]} ref={this.createDropTarget} onPointerDown={this.marqueeDown}> - <div className="imageBox-fader" style={{ overflow: Array.from(this.props.docViewPath?.()).slice(-1)[0].fitWidth ? 'auto' : undefined }}> + <div className="imageBox-cont" onPointerEnter={action(() => (this._isHovering = true))} onPointerLeave={action(() => (this._isHovering = false))} key={this.layoutDoc[Id]} ref={this.createDropTarget} onPointerDown={this.marqueeDown}> + <div className="imageBox-fader" style={{ opacity: backAlpha }}> <img key="paths" src={srcpath} style={{ transform, transformOrigin }} draggable={false} width={nativeWidth} /> {fadepath === srcpath ? null : ( - <div className={`imageBox-fadeBlocker${this.props.isHovering?.() ? '-hover' : ''}`}> - <img className="imageBox-fadeaway" key={'fadeaway'} src={fadepath} style={{ transform, transformOrigin }} draggable={false} width={nativeWidth} /> + <div + className={`imageBox-fadeBlocker${(this._isHovering && this.layoutDoc[this.fieldKey + '-useAlt'] === undefined) || BoolCast(this.layoutDoc[this.fieldKey + '-useAlt']) ? '-hover' : ''}`} + style={{ transition: StrCast(this.layoutDoc.viewTransition, 'opacity 1000ms') }}> + <img className="imageBox-fadeaway" key="fadeaway" src={fadepath} style={{ transform, transformOrigin }} draggable={false} width={nativeWidth} /> </div> )} </div> - {this.considerDownloadIcon} + {!Doc.noviceMode && this.considerDownloadIcon} {this.considerGooglePhotosLink()} <FaceRectangles document={this.dataDoc} color={'#0000FF'} backgroundColor={'#0000FF'} /> </div> ); } - screenToLocalTransform = this.props.ScreenToLocalTransform; - contentFunc = () => [this.content]; - private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef(); @observable _marqueeing: number[] | undefined; @@ -357,6 +417,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp TraceMobx(); return <div className="imageBox-annotationLayer" style={{ height: this.props.PanelHeight() }} ref={this._annotationLayer} />; } + screenToLocalTransform = () => this.props.ScreenToLocalTransform().translate(0, NumCast(this.layoutDoc._scrollTop) * this.props.ScreenToLocalTransform().Scale); marqueeDown = (e: React.PointerEvent) => { if (!e.altKey && e.button === 0 && NumCast(this.rootDoc._viewScale, 1) <= NumCast(this.rootDoc.viewScaleMin, 1) && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) { setupMoveUpEvents( @@ -379,8 +440,10 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp this._marqueeing = undefined; this.props.select(false); }; - savedAnnotations = () => this._savedAnnotations; + focus = (anchor: Doc, options: DocFocusOptions) => this._ffref.current?.focus(anchor, options); + _ffref = React.createRef<CollectionFreeFormView>(); + savedAnnotations = () => this._savedAnnotations; render() { TraceMobx(); const borderRad = this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BorderRounding); @@ -390,16 +453,31 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp className="imageBox" onContextMenu={this.specificContextMenu} ref={this._mainCont} + onScroll={action(e => { + if (!this._forcedScroll) { + if (this.layoutDoc._scrollTop || this._mainCont.current?.scrollTop) { + this._ignoreScroll = true; + this.layoutDoc._scrollTop = this._mainCont.current?.scrollTop; + this._ignoreScroll = false; + } + } + })} style={{ width: this.props.PanelWidth() ? undefined : `100%`, height: this.props.PanelWidth() ? undefined : `100%`, pointerEvents: this.layoutDoc._lockedPosition ? 'none' : undefined, borderRadius, + overflow: this.layoutDoc.fitWidth || this.props.fitWidth?.(this.rootDoc) ? 'auto' : undefined, }}> <CollectionFreeFormView - {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit} + ref={this._ffref} + {...this.props} + setContentView={emptyFunction} + NativeWidth={returnZero} + NativeHeight={returnZero} renderDepth={this.props.renderDepth + 1} fieldKey={this.annotationKey} + styleProvider={this.props.styleProvider} CollectionView={undefined} isAnnotationOverlay={true} annotationLayerHostsContent={true} @@ -407,12 +485,15 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp PanelHeight={this.props.PanelHeight} ScreenToLocalTransform={this.screenToLocalTransform} select={emptyFunction} + focus={this.focus} + getScrollHeight={this.getScrollHeight} NativeDimScaling={returnOne} + isAnyChildContentActive={returnFalse} whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} removeDocument={this.removeDocument} moveDocument={this.moveDocument} addDocument={this.addDocument}> - {this.contentFunc} + {this.content} </CollectionFreeFormView> {this.annotationLayer} {!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? null : ( @@ -425,8 +506,10 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp addDocument={this.addDocument} finishMarquee={this.finishMarquee} savedAnnotations={this.savedAnnotations} + selectionText={returnEmptyString} annotationLayer={this._annotationLayer.current} mainCont={this._mainCont.current} + highlightDragSrcColor={''} anchorMenuCrop={this.crop} /> )} |