From c7a00c21fba02167a90921f6a96c20cc71e4f04f Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Thu, 12 Dec 2024 15:05:22 +0100 Subject: [PATCH] [Editor] Add the ability to resize an editor in using a pinch gesture --- src/display/editor/editor.js | 124 ++++++++++++++++++++++++++++++----- src/display/editor/tools.js | 4 +- src/display/touch_manager.js | 10 ++- web/app.js | 1 + web/pdf_viewer.js | 8 ++- 5 files changed, 128 insertions(+), 19 deletions(-) diff --git a/src/display/editor/editor.js b/src/display/editor/editor.js index aa5d6b175487b6..a5607dfa251694 100644 --- a/src/display/editor/editor.js +++ b/src/display/editor/editor.js @@ -26,6 +26,7 @@ import { FeatureTest, shadow, unreachable } from "../../shared/util.js"; import { noContextMenu, stopEvent } from "../display_utils.js"; import { AltText } from "./alt_text.js"; import { EditorToolbar } from "./toolbar.js"; +import { TouchManager } from "../touch_manager.js"; /** * @typedef {Object} AnnotationEditorParameters @@ -82,6 +83,8 @@ class AnnotationEditor { #telemetryTimeouts = null; + #touchManager = null; + _editToolbar = null; _initialOptions = Object.create(null); @@ -864,6 +867,13 @@ class AnnotationEditor { }); } + static _round(x) { + // 10000 because we multiply by 100 and use toFixed(2) in fixAndSetPosition. + // Without rounding, the positions of the corners other than the top left + // one can be slightly wrong. + return Math.round(x * 10000) / 10000; + } + #resizerPointermove(name, event) { const [parentWidth, parentHeight] = this.parentDimensions; const savedX = this.x; @@ -873,10 +883,6 @@ class AnnotationEditor { const minWidth = AnnotationEditor.MIN_SIZE / parentWidth; const minHeight = AnnotationEditor.MIN_SIZE / parentHeight; - // 10000 because we multiply by 100 and use toFixed(2) in fixAndSetPosition. - // Without rounding, the positions of the corners other than the top left - // one can be slightly wrong. - const round = x => Math.round(x * 10000) / 10000; const rotationMatrix = this.#getRotationMatrix(this.rotation); const transf = (x, y) => [ rotationMatrix[0] * x + rotationMatrix[2] * y, @@ -936,8 +942,8 @@ class AnnotationEditor { const point = getPoint(savedWidth, savedHeight); const oppositePoint = getOpposite(savedWidth, savedHeight); let transfOppositePoint = transf(...oppositePoint); - const oppositeX = round(savedX + transfOppositePoint[0]); - const oppositeY = round(savedY + transfOppositePoint[1]); + const oppositeX = AnnotationEditor._round(savedX + transfOppositePoint[0]); + const oppositeY = AnnotationEditor._round(savedY + transfOppositePoint[1]); let ratioX = 1; let ratioY = 1; @@ -990,8 +996,8 @@ class AnnotationEditor { ) / savedHeight; } - const newWidth = round(savedWidth * ratioX); - const newHeight = round(savedHeight * ratioY); + const newWidth = AnnotationEditor._round(savedWidth * ratioX); + const newHeight = AnnotationEditor._round(savedHeight * ratioY); transfOppositePoint = transf(...getOpposite(newWidth, newHeight)); const newX = oppositeX - transfOppositePoint[0]; const newY = oppositeY - transfOppositePoint[1]; @@ -1142,11 +1148,90 @@ class AnnotationEditor { bindEvents(this, this.div, ["pointerdown"]); + if (this.isResizable && this._uiManager._supportsPinchToZoom) { + this.#touchManager ||= new TouchManager({ + container: this.div, + isPinchingDisabled: () => false, // !this.isSelected, + isPinchingStopped: () => false, + onPinchStart: this.#touchPinchStartCallback.bind(this), + onPinching: this.#touchPinchCallback.bind(this), + onPinchEnd: this.#touchPinchEndCallback.bind(this), + signal: this._uiManager._signal, + }); + } + this._uiManager._editorUndoBar?.hide(); return this.div; } + #touchPinchStartCallback() { + this.#savedDimensions = { + savedX: this.x, + savedY: this.y, + savedWidth: this.width, + savedHeight: this.height, + }; + this.#altText?.toggle(false); + this.parent.togglePointerEvents(false); + } + + #touchPinchCallback(_origin, prevDistance, distance) { + const controlFactor = 0.7; + let factor = controlFactor * (distance / prevDistance) + 1 - controlFactor; + if (factor === 1) { + return; + } + + const rotationMatrix = this.#getRotationMatrix(this.rotation); + const transf = (x, y) => [ + rotationMatrix[0] * x + rotationMatrix[2] * y, + rotationMatrix[1] * x + rotationMatrix[3] * y, + ]; + + // The center of the editor is the fixed point. + const [parentWidth, parentHeight] = this.parentDimensions; + const savedX = this.x; + const savedY = this.y; + const savedWidth = this.width; + const savedHeight = this.height; + + const minWidth = AnnotationEditor.MIN_SIZE / parentWidth; + const minHeight = AnnotationEditor.MIN_SIZE / parentHeight; + factor = Math.max( + Math.min(factor, 1 / savedWidth, 1 / savedHeight), + minWidth / savedWidth, + minHeight / savedHeight + ); + const newWidth = AnnotationEditor._round(savedWidth * factor); + const newHeight = AnnotationEditor._round(savedHeight * factor); + if (newWidth === savedWidth && newHeight === savedHeight) { + return; + } + + this.#initialRect ||= [savedX, savedY, savedWidth, savedHeight]; + const transfCenterPoint = transf(savedWidth / 2, savedHeight / 2); + const centerX = AnnotationEditor._round(savedX + transfCenterPoint[0]); + const centerY = AnnotationEditor._round(savedY + transfCenterPoint[1]); + const newTransfCenterPoint = transf(newWidth / 2, newHeight / 2); + + this.x = centerX - newTransfCenterPoint[0]; + this.y = centerY - newTransfCenterPoint[1]; + this.width = newWidth; + this.height = newHeight; + + this.setDims(parentWidth * newWidth, parentHeight * newHeight); + this.fixAndSetPosition(); + + this._onResizing(); + } + + #touchPinchEndCallback() { + this.#altText?.toggle(true); + this.parent.togglePointerEvents(true); + this.#addResizeToUndoStack(); + } + /** * Onpointerdown callback. * @param {PointerEvent} event @@ -1158,7 +1243,6 @@ class AnnotationEditor { event.preventDefault(); return; } - this.#hasBeenClicked = true; if (this._isDraggable) { @@ -1189,6 +1273,7 @@ class AnnotationEditor { #setUpDragSession(event) { const { isSelected } = this; this._uiManager.setUpDragSession(); + let hasDraggingStarted = false; const ac = new AbortController(); const signal = this._uiManager.combinedSignal(ac); @@ -1198,9 +1283,12 @@ class AnnotationEditor { this.#dragPointerId = null; this.#hasBeenClicked = false; - if (!this._uiManager.endDragSession()) { + if (!this._uiManager.endDragSession() && !this.isSelected) { this.#selectOnPointerEvent(e); } + if (hasDraggingStarted) { + this._onStopDragging(); + } }; if (isSelected) { @@ -1211,6 +1299,10 @@ class AnnotationEditor { window.addEventListener( "pointermove", e => { + if (!hasDraggingStarted) { + hasDraggingStarted = true; + this._onStartDragging(); + } const { clientX: x, clientY: y, pointerId } = e; if (pointerId !== this.#dragPointerId) { stopEvent(e); @@ -1235,11 +1327,14 @@ class AnnotationEditor { "pointerdown", // If the user drags with one finger and then clicks with another. e => { - if (e.isPrimary && e.pointerType === this.#dragPointerType) { + if (e.pointerType === this.#dragPointerType) { + // We've a pinch to zoom session. // We cannot have two primaries at the same time. // It's possible to be in this state with Firefox and Gnome when // trying to drag with three fingers (see bug 1933716). - cancelDrag(e); + if (this.#touchManager || e.isPrimary) { + cancelDrag(e); + } } stopEvent(e); }, @@ -1247,12 +1342,9 @@ class AnnotationEditor { ); } - this._onStartDragging(); - const pointerUpCallback = e => { if (!this.#dragPointerId || this.#dragPointerId === e.pointerId) { cancelDrag(e); - this._onStopDragging(); return; } stopEvent(e); @@ -1557,6 +1649,8 @@ class AnnotationEditor { this.#telemetryTimeouts = null; } this.parent = null; + this.#touchManager?.destroy(); + this.#touchManager = null; } /** diff --git a/src/display/editor/tools.js b/src/display/editor/tools.js index f4a22e5604cb0e..e702e046d4c98e 100644 --- a/src/display/editor/tools.js +++ b/src/display/editor/tools.js @@ -834,7 +834,8 @@ class AnnotationEditorUIManager { enableUpdatedAddImage, enableNewAltTextWhenAddingImage, mlManager, - editorUndoBar + editorUndoBar, + supportsPinchToZoom ) { const signal = (this._signal = this.#abortController.signal); this.#container = container; @@ -870,6 +871,7 @@ class AnnotationEditorUIManager { }; this.isShiftKeyDown = false; this._editorUndoBar = editorUndoBar || null; + this._supportsPinchToZoom = supportsPinchToZoom || false; if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("TESTING")) { Object.defineProperty(this, "reset", { diff --git a/src/display/touch_manager.js b/src/display/touch_manager.js index 00b717c5bdd9f5..0442760a1d079b 100644 --- a/src/display/touch_manager.js +++ b/src/display/touch_manager.js @@ -14,6 +14,7 @@ */ import { shadow } from "../shared/util.js"; +import { stopEvent } from "./display_utils.js"; class TouchManager { #container; @@ -24,6 +25,8 @@ class TouchManager { #isPinchingDisabled; + #onPinchStart; + #onPinching; #onPinchEnd; @@ -40,6 +43,7 @@ class TouchManager { container, isPinchingDisabled = null, isPinchingStopped = null, + onPinchStart = null, onPinching = null, onPinchEnd = null, signal, @@ -47,6 +51,7 @@ class TouchManager { this.#container = container; this.#isPinchingStopped = isPinchingStopped; this.#isPinchingDisabled = isPinchingDisabled; + this.#onPinchStart = onPinchStart; this.#onPinching = onPinching; this.#onPinchEnd = onPinchEnd; this.#touchManagerAC = new AbortController(); @@ -93,9 +98,10 @@ class TouchManager { this.#onTouchEnd.bind(this), opt ); + this.#onPinchStart?.(); } - evt.preventDefault(); + stopEvent(evt); if (evt.touches.length !== 2 || this.#isPinchingStopped?.()) { this.#touchInfo = null; @@ -169,13 +175,13 @@ class TouchManager { #onTouchEnd(evt) { this.#touchMoveAC.abort(); this.#touchMoveAC = null; + this.#onPinchEnd?.(); if (!this.#touchInfo) { return; } if (this.#isPinching) { - this.#onPinchEnd?.(); this.#isPinching = false; } diff --git a/web/app.js b/web/app.js index d23a04f2cc5a22..8aa1efe3602eac 100644 --- a/web/app.js +++ b/web/app.js @@ -492,6 +492,7 @@ const PDFViewerApplication = { mlManager: this.mlManager, abortSignal: this._globalAbortController.signal, enableHWA, + supportsPinchToZoom: this.supportsPinchToZoom, }); this.pdfViewer = pdfViewer; diff --git a/web/pdf_viewer.js b/web/pdf_viewer.js index e968db3f14810f..5adc36bb590cad 100644 --- a/web/pdf_viewer.js +++ b/web/pdf_viewer.js @@ -126,6 +126,8 @@ function isValidAnnotationEditorMode(mode) { * mode. * @property {boolean} [enableHWA] - Enables hardware acceleration for * rendering. The default value is `false`. + * @property {boolean} [supportsPinchToZoom] - Enable zooming on pinch gesture. + * The default value is `false`. */ class PDFPageViewBuffer { @@ -248,6 +250,8 @@ class PDFViewer { #scaleTimeoutId = null; + #supportsPinchToZoom = false; + #textLayerMode = TextLayerMode.ENABLE; /** @@ -316,6 +320,7 @@ class PDFViewer { this.pageColors = options.pageColors || null; this.#mlManager = options.mlManager || null; this.#enableHWA = options.enableHWA || false; + this.#supportsPinchToZoom = options.supportsPinchToZoom || false; this.defaultRenderingQueue = !options.renderingQueue; if ( @@ -911,7 +916,8 @@ class PDFViewer { this.#enableUpdatedAddImage, this.#enableNewAltTextWhenAddingImage, this.#mlManager, - this.#editorUndoBar + this.#editorUndoBar, + this.#supportsPinchToZoom ); eventBus.dispatch("annotationeditoruimanager", { source: this,