diff options
| author | bobzel <zzzman@gmail.com> | 2023-04-05 22:44:03 -0400 |
|---|---|---|
| committer | bobzel <zzzman@gmail.com> | 2023-04-05 22:44:03 -0400 |
| commit | 9b41da1af16b982ee8ac2fc09f2f8b5d67eac9fb (patch) | |
| tree | bc3f57cd5b31fd453d272c925f6d5b728ab63bae /src/client/views/DocumentDecorations.tsx | |
| parent | 9dae453967183b294bf4f7444b948023a1d52d39 (diff) | |
| parent | 8f7e99641f84ad15f34ba9e4a60b664ac93d2e5d (diff) | |
Merge branch 'master' into data-visualization-view-naafi
Diffstat (limited to 'src/client/views/DocumentDecorations.tsx')
| -rw-r--r-- | src/client/views/DocumentDecorations.tsx | 407 |
1 files changed, 284 insertions, 123 deletions
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 3544f74b4..9bc583ce5 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -1,8 +1,10 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@material-ui/core'; -import { action, computed, observable, reaction } from 'mobx'; +import { IconButton } from 'browndash-components'; +import { action, computed, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; +import { FaUndo } from 'react-icons/fa'; import { DateField } from '../../fields/DateField'; import { AclAdmin, AclEdit, DataSym, Doc, DocListCast, Field, HeightSym, WidthSym } from '../../fields/Doc'; import { Document } from '../../fields/documentSchemas'; @@ -10,7 +12,7 @@ import { InkField } from '../../fields/InkField'; import { ScriptField } from '../../fields/ScriptField'; import { Cast, NumCast, StrCast } from '../../fields/Types'; import { GetEffectiveAcl } from '../../fields/util'; -import { emptyFunction, numberValue, returnFalse, setupMoveUpEvents } from '../../Utils'; +import { emptyFunction, numberValue, returnFalse, setupMoveUpEvents, Utils } from '../../Utils'; import { Docs } from '../documents/Documents'; import { DocumentType } from '../documents/DocumentTypes'; import { DragManager } from '../util/DragManager'; @@ -25,10 +27,11 @@ import { Colors } from './global/globalEnums'; import { InkingStroke } from './InkingStroke'; import { InkStrokeProperties } from './InkStrokeProperties'; import { LightboxView } from './LightboxView'; -import { DocumentView } from './nodes/DocumentView'; +import { DocumentView, OpenWhere, OpenWhereMod } from './nodes/DocumentView'; import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox'; import { ImageBox } from './nodes/ImageBox'; import React = require('react'); +import { RichTextField } from '../../fields/RichTextField'; @observer export class DocumentDecorations extends React.Component<{ PanelWidth: number; PanelHeight: number; boundsLeft: number; boundsTop: number }, { value: string }> { @@ -64,12 +67,26 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P DocumentDecorations.Instance = this; reaction( () => SelectionManager.Views().slice(), - action(docs => (this._editingTitle = false)) + action(docs => { + this._showNothing = !DocumentView.LongPress && docs.length === 1; // show decorations if multiple docs are selected or we're long pressing + this._editingTitle = false; + }) + ); + document.addEventListener( + // show decorations whenever pointer moves outside of selection bounds. + 'pointermove', + action(e => { + if (this.Bounds.x !== Number.MAX_VALUE && (this.Bounds.x > e.clientX || this.Bounds.r < e.clientX || this.Bounds.y > e.clientY || this.Bounds.b < e.clientY)) { + this._showNothing = false; + } + }) ); } + @observable overrideBounds = false; @computed get Bounds() { + if (this.overrideBounds) return { x: 0, y: 0, r: 0, b: 0 }; const views = SelectionManager.Views(); return views .filter(dv => dv.props.renderDepth > 0) @@ -110,20 +127,24 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P } } //@ts-ignore - const titleField = +this._accumulatedTitle === this._accumulatedTitle ? +this._accumulatedTitle : this._accumulatedTitle; - Doc.SetInPlace(d.rootDoc, titleFieldKey, titleField, true); + const titleField = +this._accumulatedTitle == this._accumulatedTitle ? +this._accumulatedTitle : this._accumulatedTitle; - if (d.rootDoc.syncLayoutFieldWithTitle) { - const title = titleField.toString(); + if (titleField.toString().startsWith('<this>')) { + const title = titleField.toString().replace(/<this>\.?/, ''); const curKey = Doc.LayoutFieldKey(d.rootDoc); - if (curKey !== title && d.dataDoc[title] === undefined) { - d.rootDoc.layout = FormattedTextBox.LayoutString(title); - setTimeout(() => { - const val = d.dataDoc[curKey]; - d.dataDoc[curKey] = undefined; - d.dataDoc[title] = val; - }); + if (curKey !== title) { + if (title) { + if (d.dataDoc[title] === undefined || d.dataDoc[title] instanceof RichTextField || typeof d.dataDoc[title] === 'string') { + d.rootDoc.layoutKey = `layout_${title}`; + d.rootDoc[`layout_${title}`] = FormattedTextBox.LayoutString(title); + d.rootDoc[`${title}-nativeWidth`] = d.rootDoc[`${title}-nativeHeight`] = 0; + } + } else { + d.rootDoc.layoutKey = undefined; + } } + } else { + Doc.SetInPlace(d.rootDoc, titleFieldKey, titleField, true); } }), 'title blur' @@ -138,6 +159,16 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P } }; + @action onContainerDown = (e: React.PointerEvent): void => { + setupMoveUpEvents( + this, + e, + e => this.onBackgroundMove(true, e), + e => {}, + emptyFunction + ); + }; + @action onTitleDown = (e: React.PointerEvent): void => { setupMoveUpEvents( this, @@ -165,9 +196,10 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P ); dragData.offset = dragDocView.props.ScreenToLocalTransform().transformDirection(e.x - left, e.y - top); dragData.moveDocument = dragDocView.props.moveDocument; + dragData.removeDocument = dragDocView.props.removeDocument; dragData.isDocDecorationMove = true; dragData.canEmbed = dragTitle; - this._hidden = this.Interacting = true; + this._hidden = true; DragManager.StartDocumentDrag( SelectionManager.Views().map(dv => dv.ContentDiv!), dragData, @@ -176,7 +208,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P { dragComplete: action(e => { dragData.canEmbed && SelectionManager.DeselectAll(); - this._hidden = this.Interacting = false; + this._hidden = false; }), hideSource: true, } @@ -189,7 +221,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P onCloseClick = (forceDeleteOrIconify: boolean | undefined) => { const views = SelectionManager.Views() .slice() - .filter(v => v); + .filter(v => v && v.props.renderDepth > 0); if (forceDeleteOrIconify === false && this._iconifyBatch) return; this._deleteAfterIconify = forceDeleteOrIconify || this._iconifyBatch ? true : false; if (!this._iconifyBatch) { @@ -203,7 +235,11 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P if (this._deleteAfterIconify) { views.forEach(iconView => { Doc.setNativeView(iconView.props.Document); - iconView.props.removeDocument?.(iconView.props.Document); + if (iconView.props.Document.activeFrame) { + iconView.props.Document.opacity = 0; // bcz: hacky ... allows inkMasks and other documents to be "turned off" without removing them from the animated collection which allows them to function properly in a presenation. + } else { + iconView.props.removeDocument?.(iconView.props.Document); + } }); SelectionManager.DeselectAll(); } @@ -234,29 +270,29 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P if (selectedDocs.length) { if (e.ctrlKey) { // open an alias in a new tab with Ctrl Key - const bestAlias = DocListCast(selectedDocs[0].props.Document.aliases).find(doc => !doc.context && doc.author === Doc.CurrentUserEmail); - CollectionDockingView.AddSplit(bestAlias ?? Doc.MakeAlias(selectedDocs[0].props.Document), 'right'); + CollectionDockingView.AddSplit(Doc.BestAlias(selectedDocs[0].props.Document), OpenWhereMod.right); } else if (e.shiftKey) { // open centered in a new workspace with Shift Key const alias = Doc.MakeAlias(selectedDocs[0].props.Document); alias.context = undefined; alias.x = -alias[WidthSym]() / 2; alias.y = -alias[HeightSym]() / 2; - CollectionDockingView.AddSplit(Docs.Create.FreeformDocument([alias], { title: 'Tab for ' + alias.title }), 'right'); + CollectionDockingView.AddSplit(Docs.Create.FreeformDocument([alias], { title: 'Tab for ' + alias.title }), OpenWhereMod.right); } else if (e.altKey) { // open same document in new tab - CollectionDockingView.ToggleSplit(selectedDocs[0].props.Document, 'right'); + CollectionDockingView.ToggleSplit(selectedDocs[0].props.Document, OpenWhereMod.right); } else { var openDoc = selectedDocs[0].props.Document; if (openDoc.layoutKey === 'layout_icon') { openDoc = DocListCast(openDoc.aliases).find(alias => !alias.context) ?? Doc.MakeAlias(openDoc); Doc.deiconifyView(openDoc); } - LightboxView.SetLightboxDoc( - openDoc, - undefined, - selectedDocs.slice(1).map(view => view.props.Document) - ); + selectedDocs[0].props.addDocTab(openDoc, OpenWhere.lightbox); + // LightboxView.SetLightboxDoc( + // openDoc, + // undefined, + // selectedDocs.slice(1).map(view => view.props.Document) + // ); } } SelectionManager.DeselectAll(); @@ -275,7 +311,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P */ @action onRadiusDown = (e: React.PointerEvent): void => { - this._isRounding = true; + this._isRounding = DocumentDecorations.Instance.Interacting = true; this._resizeUndo = UndoManager.StartBatch('DocDecs set radius'); // Call util move event function setupMoveUpEvents( @@ -298,73 +334,130 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P return false; }, // moveEvent action(e => { - this._isRounding = false; + DocumentDecorations.Instance.Interacting = this._isRounding = false; this._resizeUndo?.end(); }), // upEvent - e => {} // clickEvent + e => {}, // clickEvent, + true + ); + }; + + @action + onLockDown = (e: React.PointerEvent): void => { + // Call util move event function + setupMoveUpEvents( + this, // target + e, // pointerEvent + returnFalse, // moveEvent + emptyFunction, // upEvent + e => { + UndoManager.RunInBatch( + () => + SelectionManager.Views().map(dv => { + dv.rootDoc._lockedPosition = !dv.rootDoc._lockedPosition; + dv.rootDoc._pointerEvents = dv.rootDoc._lockedPosition ? 'none' : undefined; + }), + 'toggleBackground' + ); + } // clickEvent + ); + }; + + setRotateCenter = (seldocview: DocumentView, rotCenter: number[]) => { + const newloccentern = seldocview.props.ScreenToLocalTransform().transformPoint(rotCenter[0], rotCenter[1]); + const newlocenter = [newloccentern[0] - NumCast(seldocview.layoutDoc._width) / 2, newloccentern[1] - NumCast(seldocview.layoutDoc._height) / 2]; + const final = Utils.rotPt(newlocenter[0], newlocenter[1], -(NumCast(seldocview.rootDoc._rotation) / 180) * Math.PI); + seldocview.rootDoc.rotateCenterX = final.x / NumCast(seldocview.layoutDoc._width); + seldocview.rootDoc.rotateCenterY = final.y / NumCast(seldocview.layoutDoc._height); + }; + + @action + onRotateCenterDown = (e: React.PointerEvent): void => { + this._isRotating = true; + const seldocview = SelectionManager.Views()[0]; + setupMoveUpEvents( + this, + e, + action((e: PointerEvent, down: number[], delta: number[]) => { + this.setRotateCenter(seldocview, [this.rotCenter[0] + delta[0], this.rotCenter[1] + delta[1]]); + return false; + }), // moveEvent + action(action(() => (this._isRotating = false))), // upEvent + action((e, doubleTap) => { + if (doubleTap) { + seldocview.rootDoc.rotateCenterX = 0.5; + seldocview.rootDoc.rotateCenterY = 0.5; + } + }) ); }; @action onRotateDown = (e: React.PointerEvent): void => { this._isRotating = true; + const rcScreen = { X: this.rotCenter[0], Y: this.rotCenter[1] }; const rotateUndo = UndoManager.StartBatch('rotatedown'); const selectedInk = SelectionManager.Views().filter(i => i.ComponentView instanceof InkingStroke); - const centerPoint = { X: (this.Bounds.x + this.Bounds.r) / 2, Y: (this.Bounds.y + this.Bounds.b) / 2 }; + const centerPoint = this.rotCenter.slice(); + const infos = new Map<Doc, { unrotatedDocPos: { x: number; y: number }; startRotCtr: { x: number; y: number }; accumRot: number }>(); + const seldocview = SelectionManager.Views()[0]; + SelectionManager.Views().forEach(dv => { + const accumRot = (NumCast(dv.rootDoc._rotation) / 180) * Math.PI; + const localRotCtr = dv.props.ScreenToLocalTransform().transformPoint(rcScreen.X, rcScreen.Y); + const localRotCtrOffset = [localRotCtr[0] - NumCast(dv.rootDoc.width) / 2, localRotCtr[1] - NumCast(dv.rootDoc.height) / 2]; + const startRotCtr = Utils.rotPt(localRotCtrOffset[0], localRotCtrOffset[1], -accumRot); + const unrotatedDocPos = { x: NumCast(dv.rootDoc.x) + localRotCtrOffset[0] - startRotCtr.x, y: NumCast(dv.rootDoc.y) + localRotCtrOffset[1] - startRotCtr.y }; + infos.set(dv.rootDoc, { unrotatedDocPos, startRotCtr, accumRot }); + }); + const infoRot = (angle: number, isAbs = false) => { + SelectionManager.Views().forEach( + action(dv => { + const { unrotatedDocPos, startRotCtr, accumRot } = infos.get(dv.rootDoc)!; + const endRotCtr = Utils.rotPt(startRotCtr.x, startRotCtr.y, isAbs ? angle : accumRot + angle); + infos.set(dv.rootDoc, { unrotatedDocPos, startRotCtr, accumRot: isAbs ? angle : accumRot + angle }); + dv.rootDoc.x = infos.get(dv.rootDoc)!.unrotatedDocPos.x - (endRotCtr.x - startRotCtr.x); + dv.rootDoc.y = infos.get(dv.rootDoc)!.unrotatedDocPos.y - (endRotCtr.y - startRotCtr.y); + dv.rootDoc._rotation = ((isAbs ? 0 : NumCast(dv.rootDoc._rotation)) + (angle * 180) / Math.PI) % 360; // Rotation between -360 and 360 + }) + ); + }; setupMoveUpEvents( this, e, (e: PointerEvent, down: number[], delta: number[]) => { const previousPoint = { X: e.clientX, Y: e.clientY }; const movedPoint = { X: e.clientX - delta[0], Y: e.clientY - delta[1] }; - const angle = InkStrokeProperties.angleChange(previousPoint, movedPoint, centerPoint); + const deltaAng = InkStrokeProperties.angleChange(movedPoint, previousPoint, rcScreen); if (selectedInk.length) { - angle && InkStrokeProperties.Instance.rotateInk(selectedInk, -angle, centerPoint); + deltaAng && InkStrokeProperties.Instance.rotateInk(selectedInk, deltaAng, rcScreen); + this.setRotateCenter(seldocview, centerPoint); } else { - SelectionManager.Views().forEach(dv => { - const oldRotation = NumCast(dv.rootDoc._jitterRotation); - // Rotation between -360 and 360 - let newRotation = (oldRotation - (angle * 180) / Math.PI) % 360; - - const diff = Math.round(newRotation / 45) - newRotation / 45; - if (diff < 0.05) { - console.log('show lines'); - } - dv.rootDoc._jitterRotation = newRotation; - }); + infoRot(deltaAng); } return false; }, // moveEvent action(() => { - SelectionManager.Views().forEach(dv => { - const oldRotation = NumCast(dv.rootDoc._jitterRotation); - const diff = Math.round(oldRotation / 45) - oldRotation / 45; - if (diff < 0.05) { - let newRotation = Math.round(oldRotation / 45) * 45; - dv.rootDoc._jitterRotation = newRotation; + const oldRotation = NumCast(seldocview.rootDoc._rotation); + const diff = oldRotation - Math.round(oldRotation / 45) * 45; + if (Math.abs(diff) < 5) { + if (selectedInk.length) { + InkStrokeProperties.Instance.rotateInk(selectedInk, ((Math.round(oldRotation / 45) * 45 - oldRotation) / 180) * Math.PI, rcScreen); + } else { + infoRot(((Math.round(oldRotation / 45) * 45) / 180) * Math.PI, true); } - }); + } + if (selectedInk.length) { + this.setRotateCenter(seldocview, centerPoint); + } this._isRotating = false; rotateUndo?.end(); - UndoManager.FilterBatches(['data', 'x', 'y', 'width', 'height']); }), // upEvent - emptyFunction + action(e => (this._showRotCenter = !this._showRotCenter)) // clickEvent ); }; @action onPointerDown = (e: React.PointerEvent): void => { - DragManager.docsBeingDragged.push(...SelectionManager.Views().map(dv => dv.rootDoc)); - this._inkDragDocs = DragManager.docsBeingDragged - .filter(doc => doc.type === DocumentType.INK) - .map(doc => { - if (InkStrokeProperties.Instance._lock) { - Doc.SetNativeHeight(doc, NumCast(doc._height)); - Doc.SetNativeWidth(doc, NumCast(doc._width)); - } - return { doc, x: NumCast(doc.x), y: NumCast(doc.y), width: NumCast(doc._width), height: NumCast(doc._height) }; - }); - setupMoveUpEvents(this, e, this.onPointerMove, this.onPointerUp, emptyFunction); this.Interacting = true; // turns off pointer events on things like youtube videos and web pages so that dragging doesn't get "stuck" when cursor moves over them this._resizeHdlId = e.currentTarget.className; @@ -388,10 +481,6 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P if (!first) return false; let thisPt = { x: e.clientX - this._offX, y: e.clientY - this._offY }; var fixedAspect = Doc.NativeAspect(first.layoutDoc); - InkStrokeProperties.Instance._lock && - SelectionManager.Views() - .filter(dv => dv.rootDoc.type === DocumentType.INK) - .forEach(dv => (fixedAspect = Doc.NativeAspect(dv.rootDoc))); const resizeHdl = this._resizeHdlId.split(' ')[0]; if (fixedAspect && (resizeHdl === 'documentDecorations-bottomRightResizer' || resizeHdl === 'documentDecorations-topLeftResizer')) { @@ -472,6 +561,9 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P if (e.ctrlKey && !Doc.NativeHeight(docView.props.Document)) docView.toggleNativeDimensions(); if (dX !== 0 || dY !== 0 || dW !== 0 || dH !== 0) { const doc = Document(docView.rootDoc); + if (doc.nativeHeightUnfrozen && !NumCast(doc.nativeHeight)) { + doc._nativeHeight = (NumCast(doc._height) / NumCast(doc._width, 1)) * docView.nativeWidth; + } const nwidth = docView.nativeWidth; const nheight = docView.nativeHeight; let docheight = doc._height || 0; @@ -493,7 +585,8 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P } let actualdW = Math.max(width + dW * scale, 20); let actualdH = Math.max(height + dH * scale, 20); - const fixedAspect = nwidth && nheight && (!doc._fitWidth || e.ctrlKey || doc.nativeHeightUnfrozen); + const preserveNativeDim = doc._nativeHeightUnfrozen === false && doc._nativeDimModifiable === false; + const fixedAspect = nwidth && nheight && (!doc._fitWidth || preserveNativeDim || e.ctrlKey || doc.nativeHeightUnfrozen || doc.nativeDimModifiable); if (fixedAspect) { if ((Math.abs(dW) > Math.abs(dH) && ((!dragBottom && !dragTop) || !modifyNativeDim)) || dragRight) { if (dragRight && modifyNativeDim) { @@ -501,7 +594,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P doc._nativeWidth = (actualdW / (doc._width || 1)) * Doc.NativeWidth(doc); } } else { - if (!doc._fitWidth) { + if (!doc._fitWidth || preserveNativeDim) { actualdH = (nheight / nwidth) * actualdW; doc._height = actualdH; } else if (!modifyNativeDim || dragBotRight) doc._height = actualdH; @@ -509,11 +602,13 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P doc._width = actualdW; } else { if ((dragBottom || dragTop) && (modifyNativeDim || (docView.layoutDoc.nativeHeightUnfrozen && docView.layoutDoc._fitWidth))) { - // frozen web pages, PDFs, and some RTFS have frozen nativewidth/height. But they are marked to allow their nativeHeight to be explicitly modified with fitWidth and vertical resizing. (ie, with fitWidth they can't grow horizontally to match a vertical resize so it makes more sense to change their nativeheight even if the ctrl key isn't used) + // frozen web pages, PDFs, and some RTFS have frozen nativewidth/height. But they are marked to allow their nativeHeight + // to be explicitly modified with fitWidth and vertical resizing. (ie, with fitWidth they can't grow horizontally to match + // a vertical resize so it makes more sense to change their nativeheight even if the ctrl key isn't used) doc._nativeHeight = (actualdH / (doc._height || 1)) * Doc.NativeHeight(doc); doc._autoHeight = false; } else { - if (!doc._fitWidth) { + if (!doc._fitWidth || preserveNativeDim) { actualdW = (nwidth / nheight) * actualdH; doc._width = actualdW; } else if (!modifyNativeDim || dragBotRight) doc._width = actualdW; @@ -524,10 +619,18 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P } else doc._height = actualdH; } } else { - const maxHeight = 0; //Math.max(nheight, NumCast(doc.scrollHeight, NumCast(doc[docView.LayoutFieldKey + '-scrollHeight']))) * docView.NativeDimScaling(); + const rotCtr = [NumCast(doc._width) / 2, NumCast(doc._height) / 2]; + const tlRotated = Utils.rotPt(-rotCtr[0], -rotCtr[1], (NumCast(doc._rotation) / 180) * Math.PI); + + const maxHeight = doc.nativeHeightUnfrozen || !nheight ? 0 : Math.max(nheight, NumCast(doc.scrollHeight, NumCast(doc[docView.LayoutFieldKey + '-scrollHeight']))) * docView.NativeDimScaling(); dH && (doc._height = actualdH > maxHeight && maxHeight ? maxHeight : actualdH); dW && (doc._width = actualdW); dH && (doc._autoHeight = false); + + const rotCtr2 = [NumCast(doc._width) / 2, NumCast(doc._height) / 2]; + const tlRotated2 = Utils.rotPt(-rotCtr2[0], -rotCtr2[1], (NumCast(doc._rotation) / 180) * Math.PI); + doc.x = NumCast(doc.x) + tlRotated.x + rotCtr[0] - (tlRotated2.x + rotCtr2[0]); // doc shifts by amount topleft moves because rotation is about center of doc + doc.y = NumCast(doc.y) + tlRotated.y + rotCtr[1] - (tlRotated2.y + rotCtr2[1]); } doc.x = (doc.x || 0) + dX * (actualdW - docwidth); doc.y = (doc.y || 0) + (dragBottom ? 0 : dY * (actualdH - docheight)); @@ -593,28 +696,53 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P return SelectionManager.Views().some(docView => docView.rootDoc.layoutKey === 'layout_icon'); } + @observable _showRotCenter = false; + @observable _rotCenter = [0, 0]; + @computed get rotCenter() { + if (SelectionManager.Views().length) { + const seldocview = SelectionManager.Views()[0]; + const loccenter = Utils.rotPt( + NumCast(seldocview.rootDoc.rotateCenterX) * NumCast(seldocview.layoutDoc._width), + NumCast(seldocview.rootDoc.rotateCenterY) * NumCast(seldocview.layoutDoc._height), + (NumCast(seldocview.rootDoc._rotation) / 180) * Math.PI + ); + return seldocview.props + .ScreenToLocalTransform() + .inverse() + .transformPoint(loccenter.x + NumCast(seldocview.layoutDoc._width) / 2, loccenter.y + NumCast(seldocview.layoutDoc._height) / 2); + } + return this._rotCenter; + } + + @observable _showNothing = true; + render() { - const bounds = this.Bounds; - const seldoc = SelectionManager.Views().slice(-1)[0]; - if (SnappingManager.GetIsDragging() || bounds.r - bounds.x < 1 || bounds.x === Number.MAX_VALUE || !seldoc || this._hidden || isNaN(bounds.r) || isNaN(bounds.b) || isNaN(bounds.x) || isNaN(bounds.y)) { + const { b, r, x, y } = this.Bounds; + const bounds = { b, r, x, y }; + const seldocview = SelectionManager.Views().lastElement(); + if (SnappingManager.GetIsDragging() || bounds.r - bounds.x < 1 || bounds.x === Number.MAX_VALUE || !seldocview || this._hidden || isNaN(bounds.r) || isNaN(bounds.b) || isNaN(bounds.x) || isNaN(bounds.y)) { + setTimeout(action(() => (this._showNothing = true))); return null; } // hide the decorations if the parent chooses to hide it or if the document itself hides it - const hideResizers = seldoc.props.hideResizeHandles || seldoc.rootDoc.hideResizeHandles || seldoc.rootDoc._isGroup || this._isRounding || this._isRotating; - const hideTitle = seldoc.props.hideDecorationTitle || seldoc.rootDoc.hideDecorationTitle || this._isRounding || this._isRotating; - const hideDocumentButtonBar = seldoc.props.hideDocumentButtonBar || seldoc.rootDoc.hideDocumentButtonBar || this._isRounding || this._isRotating; + const hideDecorations = seldocview.props.hideDecorations || seldocview.rootDoc.hideDecorations; + const hideResizers = hideDecorations || seldocview.props.hideResizeHandles || seldocview.rootDoc.hideResizeHandles || seldocview.rootDoc._isGroup || this._isRounding || this._isRotating; + const hideTitle = hideDecorations || seldocview.props.hideDecorationTitle || seldocview.rootDoc.hideDecorationTitle || this._isRounding || this._isRotating; + const hideDocumentButtonBar = hideDecorations || seldocview.props.hideDocumentButtonBar || seldocview.rootDoc.hideDocumentButtonBar || this._isRounding || this._isRotating; // if multiple documents have been opened at the same time, then don't show open button const hideOpenButton = - seldoc.props.hideOpenButton || - seldoc.rootDoc.hideOpenButton || + hideDecorations || + seldocview.props.hideOpenButton || + seldocview.rootDoc.hideOpenButton || SelectionManager.Views().some(docView => docView.props.Document._stayInCollection || docView.props.Document.isGroup || docView.props.Document.hideOpenButton) || this._isRounding || this._isRotating; const hideDeleteButton = + hideDecorations || this._isRounding || this._isRotating || - seldoc.props.hideDeleteButton || - seldoc.rootDoc.hideDeleteButton || + seldocview.props.hideDeleteButton || + seldocview.rootDoc.hideDeleteButton || SelectionManager.Views().some(docView => { const collectionAcl = docView.props.ContainingCollectionView ? GetEffectiveAcl(docView.props.ContainingCollectionDoc?.[DataSym]) : AclEdit; return docView.rootDoc.stayInCollection || (collectionAcl !== AclAdmin && collectionAcl !== AclEdit && GetEffectiveAcl(docView.rootDoc) !== AclAdmin); @@ -642,6 +770,29 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P ); const colorScheme = StrCast(Doc.ActiveDashboard?.colorScheme); + + const leftBounds = this.props.boundsLeft; + const topBounds = LightboxView.LightboxDoc ? 0 : this.props.boundsTop; + bounds.x = Math.max(leftBounds, bounds.x - this._resizeBorderWidth / 2) + this._resizeBorderWidth / 2; + bounds.y = Math.max(topBounds, bounds.y - this._resizeBorderWidth / 2 - this._titleHeight) + this._resizeBorderWidth / 2 + this._titleHeight; + const borderRadiusDraggerWidth = 15; + bounds.r = Math.max(bounds.x, Math.max(leftBounds, Math.min(window.innerWidth, bounds.r + borderRadiusDraggerWidth + this._resizeBorderWidth / 2) - this._resizeBorderWidth / 2 - borderRadiusDraggerWidth)); + bounds.b = Math.max(bounds.y, Math.max(topBounds, Math.min(window.innerHeight, bounds.b + this._resizeBorderWidth / 2 + this._linkBoxHeight) - this._resizeBorderWidth / 2 - this._linkBoxHeight)); + + const useLock = bounds.r - bounds.x > 135 && seldocview.props.CollectionFreeFormDocumentView; + const useRotation = !hideResizers && seldocview.rootDoc.type !== DocumentType.EQUATION && seldocview.props.CollectionFreeFormDocumentView; // when do we want an object to not rotate? + const rotation = NumCast(seldocview.rootDoc._rotation); + + const resizerScheme = colorScheme ? 'documentDecorations-resizer' + colorScheme : ''; + + // Radius constants + const useRounding = seldocview.ComponentView instanceof ImageBox || seldocview.ComponentView instanceof FormattedTextBox || seldocview.ComponentView instanceof CollectionFreeFormView; + const borderRadius = numberValue(StrCast(seldocview.rootDoc.borderRounding)); + const docMax = Math.min(NumCast(seldocview.rootDoc.width) / 2, NumCast(seldocview.rootDoc.height) / 2); + const maxDist = Math.min((this.Bounds.r - this.Bounds.x) / 2, (this.Bounds.b - this.Bounds.y) / 2); + const radiusHandle = (borderRadius / docMax) * maxDist; + const radiusHandleLocation = Math.min(radiusHandle, maxDist); + const titleArea = this._editingTitle ? ( <input ref={this._keyinput} @@ -653,41 +804,25 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P onBlur={e => !hideTitle && this.titleBlur()} onChange={action(e => !hideTitle && (this._accumulatedTitle = e.target.value))} onKeyDown={hideTitle ? emptyFunction : this.titleEntered} + onPointerDown={e => e.stopPropagation()} /> ) : ( <div className="documentDecorations-title" key="title" onPointerDown={this.onTitleDown}> <span className={`documentDecorations-titleSpan${colorScheme}`}>{`${hideTitle ? '' : this.selectionTitle}`}</span> + {!useLock ? null : ( + <Tooltip key="lock" title={<div className="dash-tooltip">toggle ability to interact with document</div>} placement="top"> + <div className="documentDecorations-lock" style={{ color: seldocview.rootDoc._lockedPosition ? 'red' : undefined }} onPointerDown={this.onLockDown} onContextMenu={e => e.preventDefault()}> + <FontAwesomeIcon size="sm" icon="lock" /> + </div> + </Tooltip> + )} </div> ); - - const leftBounds = this.props.boundsLeft; - const topBounds = LightboxView.LightboxDoc ? 0 : this.props.boundsTop; - bounds.x = Math.max(leftBounds, bounds.x - this._resizeBorderWidth / 2) + this._resizeBorderWidth / 2; - bounds.y = Math.max(topBounds, bounds.y - this._resizeBorderWidth / 2 - this._titleHeight) + this._resizeBorderWidth / 2 + this._titleHeight; - const borderRadiusDraggerWidth = 15; - bounds.r = Math.max(bounds.x, Math.max(leftBounds, Math.min(window.innerWidth, bounds.r + borderRadiusDraggerWidth + this._resizeBorderWidth / 2) - this._resizeBorderWidth / 2 - borderRadiusDraggerWidth)); - bounds.b = Math.max(bounds.y, Math.max(topBounds, Math.min(window.innerHeight, bounds.b + this._resizeBorderWidth / 2 + this._linkBoxHeight) - this._resizeBorderWidth / 2 - this._linkBoxHeight)); - - // Rotation constants: Only allow rotation on ink and images - const useRotation = seldoc.ComponentView instanceof InkingStroke || seldoc.ComponentView instanceof ImageBox; - const rotation = NumCast(seldoc.rootDoc._jitterRotation); - - const resizerScheme = colorScheme ? 'documentDecorations-resizer' + colorScheme : ''; - - // Radius constants - const useRounding = seldoc.ComponentView instanceof ImageBox || seldoc.ComponentView instanceof FormattedTextBox; - const borderRadius = numberValue(StrCast(seldoc.rootDoc.borderRounding)); - const docMax = Math.min(NumCast(seldoc.rootDoc.width) / 2, NumCast(seldoc.rootDoc.height) / 2); - const maxDist = Math.min((this.Bounds.r - this.Bounds.x) / 2, (this.Bounds.b - this.Bounds.y) / 2); - const radiusHandle = (borderRadius / docMax) * maxDist; - const radiusHandleLocation = Math.min(radiusHandle, maxDist); return ( - <div className={`documentDecorations${colorScheme}`}> + <div className={`documentDecorations${colorScheme}`} style={{ opacity: this._showNothing ? 0.1 : undefined }}> <div className="documentDecorations-background" style={{ - transform: `rotate(${rotation}deg)`, - transformOrigin: 'top left', width: bounds.r - bounds.x + this._resizeBorderWidth + 'px', height: bounds.b - bounds.y + this._resizeBorderWidth + 'px', left: bounds.x - this._resizeBorderWidth / 2, @@ -712,9 +847,12 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P width: bounds.r - bounds.x + this._resizeBorderWidth + 'px', height: bounds.b - bounds.y + this._resizeBorderWidth + this._titleHeight + 'px', }}> - {hideDeleteButton ? <div /> : topBtn('close', this.hasIcons ? 'times' : 'window-maximize', undefined, e => this.onCloseClick(this.hasIcons ? true : undefined), 'Close')} - {titleArea} - {hideOpenButton ? null : topBtn('open', 'external-link-alt', this.onMaximizeDown, undefined, 'Open in Tab (ctrl: as alias, shift: in new collection)')} + <div className="documentDecorations-topbar" onPointerDown={this.onContainerDown}> + {hideDeleteButton ? <div /> : topBtn('close', 'times', undefined, e => this.onCloseClick(true), 'Close')} + {hideResizers || hideDeleteButton ? <div /> : topBtn('minimize', 'window-maximize', undefined, e => this.onCloseClick(undefined), 'Minimize')} + {hideTitle ? null : titleArea} + {hideOpenButton ? <div /> : topBtn('open', 'external-link-alt', this.onMaximizeDown, undefined, 'Open in Lightbox (ctrl: as alias, shift: in new collection)')} + </div> {hideResizers ? null : ( <> <div key="tl" className={`documentDecorations-topLeftResizer ${resizerScheme}`} onPointerDown={this.onPointerDown} onContextMenu={e => e.preventDefault()} /> @@ -727,23 +865,15 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P <div key="b" className={`documentDecorations-bottomResizer ${resizerScheme}`} onPointerDown={this.onPointerDown} onContextMenu={e => e.preventDefault()} /> <div key="br" className={`documentDecorations-bottomRightResizer ${resizerScheme}`} onPointerDown={this.onPointerDown} onContextMenu={e => e.preventDefault()} /> - {seldoc.props.renderDepth <= 1 || !seldoc.props.ContainingCollectionView ? null : topBtn('selector', 'arrow-alt-circle-up', undefined, this.onSelectorClick, 'tap to select containing document')} + {seldocview.props.renderDepth <= 1 || !seldocview.props.ContainingCollectionView ? null : topBtn('selector', 'arrow-alt-circle-up', undefined, this.onSelectorClick, 'tap to select containing document')} </> )} - - {useRotation && ( - <div key="rot" className={`documentDecorations-rotation`} onPointerDown={this.onRotateDown} onContextMenu={e => e.preventDefault()}> - {'⟲'} - </div> - )} - {useRounding && ( <div key="rad" style={{ background: `${this._isRounding ? Colors.MEDIUM_BLUE : undefined}`, - left: `${radiusHandleLocation + 3}`, - top: `${radiusHandleLocation + 23}`, + transform: `translate(${radiusHandleLocation}px, ${radiusHandleLocation}px)`, }} className={`documentDecorations-borderRadius`} onPointerDown={this.onRadiusDown} @@ -756,12 +886,43 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P className="link-button-container" key="links" style={{ - transform: ` translate(${-this._resizeBorderWidth / 2 + 10}px, ${this._resizeBorderWidth + bounds.b - bounds.y + this._titleHeight}px) `, + transform: `translate(${-this._resizeBorderWidth / 2 + 10}px, ${this._resizeBorderWidth + bounds.b - bounds.y + this._titleHeight}px) `, }}> <DocumentButtonBar views={SelectionManager.Views} /> </div> )} </div> + + {useRotation && ( + <> + <div + style={{ + position: 'absolute', + transform: `rotate(${rotation}deg)`, + width: this.Bounds.r - this.Bounds.x + 'px', + height: this.Bounds.b - this.Bounds.y + 'px', + left: this.Bounds.x, + top: this.Bounds.y, + pointerEvents: 'none', + }}> + {this._isRotating ? null : ( + <Tooltip enterDelay={750} title={<div className="dash-tooltip">tap to set rotate center, drag to rotate</div>}> + <div className="documentDecorations-rotation" style={{ pointerEvents: 'all' }} onPointerDown={this.onRotateDown} onContextMenu={e => e.preventDefault()}> + <IconButton icon={<FaUndo />} isCircle={true} hoverStyle={'lighten'} backgroundColor={Colors.DARK_GRAY} color={Colors.LIGHT_GRAY} /> + </div> + </Tooltip> + )} + </div> + {!this._showRotCenter ? null : ( + <div + className="documentDecorations-rotationCenter" + style={{ transform: `translate(${this.rotCenter[0] - 3}px, ${this.rotCenter[1] - 3}px)` }} + onPointerDown={this.onRotateCenterDown} + onContextMenu={e => e.preventDefault()} + /> + )} + </> + )} </div> )} </div> |
