Merge pull request #19219 from calixteman/pinch_editor

[Editor] Add the ability to resize an editor in using a pinch gesture
This commit is contained in:
calixteman 2024-12-14 21:50:51 +01:00 committed by GitHub
commit a8c35a9e0c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 130 additions and 22 deletions

View File

@ -26,6 +26,7 @@ import { FeatureTest, shadow, unreachable } from "../../shared/util.js";
import { noContextMenu, stopEvent } from "../display_utils.js"; import { noContextMenu, stopEvent } from "../display_utils.js";
import { AltText } from "./alt_text.js"; import { AltText } from "./alt_text.js";
import { EditorToolbar } from "./toolbar.js"; import { EditorToolbar } from "./toolbar.js";
import { TouchManager } from "../touch_manager.js";
/** /**
* @typedef {Object} AnnotationEditorParameters * @typedef {Object} AnnotationEditorParameters
@ -82,6 +83,8 @@ class AnnotationEditor {
#telemetryTimeouts = null; #telemetryTimeouts = null;
#touchManager = null;
_editToolbar = null; _editToolbar = null;
_initialOptions = Object.create(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) { #resizerPointermove(name, event) {
const [parentWidth, parentHeight] = this.parentDimensions; const [parentWidth, parentHeight] = this.parentDimensions;
const savedX = this.x; const savedX = this.x;
@ -873,10 +883,6 @@ class AnnotationEditor {
const minWidth = AnnotationEditor.MIN_SIZE / parentWidth; const minWidth = AnnotationEditor.MIN_SIZE / parentWidth;
const minHeight = AnnotationEditor.MIN_SIZE / parentHeight; 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 rotationMatrix = this.#getRotationMatrix(this.rotation);
const transf = (x, y) => [ const transf = (x, y) => [
rotationMatrix[0] * x + rotationMatrix[2] * y, rotationMatrix[0] * x + rotationMatrix[2] * y,
@ -936,8 +942,8 @@ class AnnotationEditor {
const point = getPoint(savedWidth, savedHeight); const point = getPoint(savedWidth, savedHeight);
const oppositePoint = getOpposite(savedWidth, savedHeight); const oppositePoint = getOpposite(savedWidth, savedHeight);
let transfOppositePoint = transf(...oppositePoint); let transfOppositePoint = transf(...oppositePoint);
const oppositeX = round(savedX + transfOppositePoint[0]); const oppositeX = AnnotationEditor._round(savedX + transfOppositePoint[0]);
const oppositeY = round(savedY + transfOppositePoint[1]); const oppositeY = AnnotationEditor._round(savedY + transfOppositePoint[1]);
let ratioX = 1; let ratioX = 1;
let ratioY = 1; let ratioY = 1;
@ -990,8 +996,8 @@ class AnnotationEditor {
) / savedHeight; ) / savedHeight;
} }
const newWidth = round(savedWidth * ratioX); const newWidth = AnnotationEditor._round(savedWidth * ratioX);
const newHeight = round(savedHeight * ratioY); const newHeight = AnnotationEditor._round(savedHeight * ratioY);
transfOppositePoint = transf(...getOpposite(newWidth, newHeight)); transfOppositePoint = transf(...getOpposite(newWidth, newHeight));
const newX = oppositeX - transfOppositePoint[0]; const newX = oppositeX - transfOppositePoint[0];
const newY = oppositeY - transfOppositePoint[1]; const newY = oppositeY - transfOppositePoint[1];
@ -1142,11 +1148,92 @@ class AnnotationEditor {
bindEvents(this, this.div, ["pointerdown"]); bindEvents(this, this.div, ["pointerdown"]);
if (this.isResizable && this._uiManager._supportsPinchToZoom) {
this.#touchManager ||= new TouchManager({
container: this.div,
isPinchingDisabled: () => !this.isSelected,
onPinchStart: this.#touchPinchStartCallback.bind(this),
onPinching: this.#touchPinchCallback.bind(this),
onPinchEnd: this.#touchPinchEndCallback.bind(this),
signal: this._uiManager._signal,
});
}
this._uiManager._editorUndoBar?.hide(); this._uiManager._editorUndoBar?.hide();
return this.div; 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) {
// Slightly slow down the zooming because the editor could be small and the
// user could have difficulties to rescale it as they want.
const slowDownFactor = 0.7;
let factor =
slowDownFactor * (distance / prevDistance) + 1 - slowDownFactor;
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. * Onpointerdown callback.
* @param {PointerEvent} event * @param {PointerEvent} event
@ -1158,7 +1245,6 @@ class AnnotationEditor {
event.preventDefault(); event.preventDefault();
return; return;
} }
this.#hasBeenClicked = true; this.#hasBeenClicked = true;
if (this._isDraggable) { if (this._isDraggable) {
@ -1189,6 +1275,7 @@ class AnnotationEditor {
#setUpDragSession(event) { #setUpDragSession(event) {
const { isSelected } = this; const { isSelected } = this;
this._uiManager.setUpDragSession(); this._uiManager.setUpDragSession();
let hasDraggingStarted = false;
const ac = new AbortController(); const ac = new AbortController();
const signal = this._uiManager.combinedSignal(ac); const signal = this._uiManager.combinedSignal(ac);
@ -1201,6 +1288,9 @@ class AnnotationEditor {
if (!this._uiManager.endDragSession()) { if (!this._uiManager.endDragSession()) {
this.#selectOnPointerEvent(e); this.#selectOnPointerEvent(e);
} }
if (hasDraggingStarted) {
this._onStopDragging();
}
}; };
if (isSelected) { if (isSelected) {
@ -1211,6 +1301,10 @@ class AnnotationEditor {
window.addEventListener( window.addEventListener(
"pointermove", "pointermove",
e => { e => {
if (!hasDraggingStarted) {
hasDraggingStarted = true;
this._onStartDragging();
}
const { clientX: x, clientY: y, pointerId } = e; const { clientX: x, clientY: y, pointerId } = e;
if (pointerId !== this.#dragPointerId) { if (pointerId !== this.#dragPointerId) {
stopEvent(e); stopEvent(e);
@ -1235,11 +1329,14 @@ class AnnotationEditor {
"pointerdown", "pointerdown",
// If the user drags with one finger and then clicks with another. // If the user drags with one finger and then clicks with another.
e => { 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. // We cannot have two primaries at the same time.
// It's possible to be in this state with Firefox and Gnome when // It's possible to be in this state with Firefox and Gnome when
// trying to drag with three fingers (see bug 1933716). // trying to drag with three fingers (see bug 1933716).
cancelDrag(e); if (this.#touchManager || e.isPrimary) {
cancelDrag(e);
}
} }
stopEvent(e); stopEvent(e);
}, },
@ -1247,12 +1344,9 @@ class AnnotationEditor {
); );
} }
this._onStartDragging();
const pointerUpCallback = e => { const pointerUpCallback = e => {
if (!this.#dragPointerId || this.#dragPointerId === e.pointerId) { if (!this.#dragPointerId || this.#dragPointerId === e.pointerId) {
cancelDrag(e); cancelDrag(e);
this._onStopDragging();
return; return;
} }
stopEvent(e); stopEvent(e);
@ -1557,6 +1651,8 @@ class AnnotationEditor {
this.#telemetryTimeouts = null; this.#telemetryTimeouts = null;
} }
this.parent = null; this.parent = null;
this.#touchManager?.destroy();
this.#touchManager = null;
} }
/** /**

View File

@ -834,7 +834,8 @@ class AnnotationEditorUIManager {
enableUpdatedAddImage, enableUpdatedAddImage,
enableNewAltTextWhenAddingImage, enableNewAltTextWhenAddingImage,
mlManager, mlManager,
editorUndoBar editorUndoBar,
supportsPinchToZoom
) { ) {
const signal = (this._signal = this.#abortController.signal); const signal = (this._signal = this.#abortController.signal);
this.#container = container; this.#container = container;
@ -870,6 +871,7 @@ class AnnotationEditorUIManager {
}; };
this.isShiftKeyDown = false; this.isShiftKeyDown = false;
this._editorUndoBar = editorUndoBar || null; this._editorUndoBar = editorUndoBar || null;
this._supportsPinchToZoom = supportsPinchToZoom !== false;
if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("TESTING")) { if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("TESTING")) {
Object.defineProperty(this, "reset", { Object.defineProperty(this, "reset", {

View File

@ -14,6 +14,7 @@
*/ */
import { shadow } from "../shared/util.js"; import { shadow } from "../shared/util.js";
import { stopEvent } from "./display_utils.js";
class TouchManager { class TouchManager {
#container; #container;
@ -24,6 +25,8 @@ class TouchManager {
#isPinchingDisabled; #isPinchingDisabled;
#onPinchStart;
#onPinching; #onPinching;
#onPinchEnd; #onPinchEnd;
@ -40,6 +43,7 @@ class TouchManager {
container, container,
isPinchingDisabled = null, isPinchingDisabled = null,
isPinchingStopped = null, isPinchingStopped = null,
onPinchStart = null,
onPinching = null, onPinching = null,
onPinchEnd = null, onPinchEnd = null,
signal, signal,
@ -47,6 +51,7 @@ class TouchManager {
this.#container = container; this.#container = container;
this.#isPinchingStopped = isPinchingStopped; this.#isPinchingStopped = isPinchingStopped;
this.#isPinchingDisabled = isPinchingDisabled; this.#isPinchingDisabled = isPinchingDisabled;
this.#onPinchStart = onPinchStart;
this.#onPinching = onPinching; this.#onPinching = onPinching;
this.#onPinchEnd = onPinchEnd; this.#onPinchEnd = onPinchEnd;
this.#touchManagerAC = new AbortController(); this.#touchManagerAC = new AbortController();
@ -93,9 +98,10 @@ class TouchManager {
this.#onTouchEnd.bind(this), this.#onTouchEnd.bind(this),
opt opt
); );
this.#onPinchStart?.();
} }
evt.preventDefault(); stopEvent(evt);
if (evt.touches.length !== 2 || this.#isPinchingStopped?.()) { if (evt.touches.length !== 2 || this.#isPinchingStopped?.()) {
this.#touchInfo = null; this.#touchInfo = null;
@ -169,18 +175,15 @@ class TouchManager {
#onTouchEnd(evt) { #onTouchEnd(evt) {
this.#touchMoveAC.abort(); this.#touchMoveAC.abort();
this.#touchMoveAC = null; this.#touchMoveAC = null;
this.#onPinchEnd?.();
if (!this.#touchInfo) { if (!this.#touchInfo) {
return; return;
} }
if (this.#isPinching) {
this.#onPinchEnd?.();
this.#isPinching = false;
}
evt.preventDefault(); evt.preventDefault();
this.#touchInfo = null; this.#touchInfo = null;
this.#isPinching = false;
} }
destroy() { destroy() {

View File

@ -492,6 +492,7 @@ const PDFViewerApplication = {
mlManager: this.mlManager, mlManager: this.mlManager,
abortSignal: this._globalAbortController.signal, abortSignal: this._globalAbortController.signal,
enableHWA, enableHWA,
supportsPinchToZoom: this.supportsPinchToZoom,
}); });
this.pdfViewer = pdfViewer; this.pdfViewer = pdfViewer;

View File

@ -126,6 +126,8 @@ function isValidAnnotationEditorMode(mode) {
* mode. * mode.
* @property {boolean} [enableHWA] - Enables hardware acceleration for * @property {boolean} [enableHWA] - Enables hardware acceleration for
* rendering. The default value is `false`. * rendering. The default value is `false`.
* @property {boolean} [supportsPinchToZoom] - Enable zooming on pinch gesture.
* The default value is `true`.
*/ */
class PDFPageViewBuffer { class PDFPageViewBuffer {
@ -248,6 +250,8 @@ class PDFViewer {
#scaleTimeoutId = null; #scaleTimeoutId = null;
#supportsPinchToZoom = true;
#textLayerMode = TextLayerMode.ENABLE; #textLayerMode = TextLayerMode.ENABLE;
/** /**
@ -316,6 +320,7 @@ class PDFViewer {
this.pageColors = options.pageColors || null; this.pageColors = options.pageColors || null;
this.#mlManager = options.mlManager || null; this.#mlManager = options.mlManager || null;
this.#enableHWA = options.enableHWA || false; this.#enableHWA = options.enableHWA || false;
this.#supportsPinchToZoom = options.supportsPinchToZoom !== false;
this.defaultRenderingQueue = !options.renderingQueue; this.defaultRenderingQueue = !options.renderingQueue;
if ( if (
@ -911,7 +916,8 @@ class PDFViewer {
this.#enableUpdatedAddImage, this.#enableUpdatedAddImage,
this.#enableNewAltTextWhenAddingImage, this.#enableNewAltTextWhenAddingImage,
this.#mlManager, this.#mlManager,
this.#editorUndoBar this.#editorUndoBar,
this.#supportsPinchToZoom
); );
eventBus.dispatch("annotationeditoruimanager", { eventBus.dispatch("annotationeditoruimanager", {
source: this, source: this,