diff --git a/ext/bg/data/options-schema.json b/ext/bg/data/options-schema.json index 9cc8837d..240305b5 100644 --- a/ext/bg/data/options-schema.json +++ b/ext/bg/data/options-schema.json @@ -320,6 +320,7 @@ "required": [ "inputs", "touchInputEnabled", + "pointerEventsEnabled", "selectText", "alphanumeric", "autoHideResults", @@ -430,6 +431,10 @@ "type": "boolean", "default": true }, + "pointerEventsEnabled": { + "type": "boolean", + "default": false + }, "selectText": { "type": "boolean", "default": true diff --git a/ext/bg/js/options.js b/ext/bg/js/options.js index 3470da58..89538b3e 100644 --- a/ext/bg/js/options.js +++ b/ext/bg/js/options.js @@ -505,6 +505,7 @@ class OptionsUtil { // Updated handlebars templates to include "clipboard-image" definition. // Added hideDelay. // Added inputs to profileOptions.scanning. + // Added pointerEventsEnabled to profileOptions.scanning. for (const {conditionGroups} of options.profiles) { for (const {conditions} of conditionGroups) { for (const condition of conditions) { @@ -526,6 +527,7 @@ class OptionsUtil { for (const {options: profileOptions} of options.profiles) { profileOptions.general.usePopupWindow = false; profileOptions.scanning.hideDelay = 0; + profileOptions.scanning.pointerEventsEnabled = false; const {modifier, middleMouse, touchInputEnabled} = profileOptions.scanning; const scanningInputs = []; diff --git a/ext/bg/settings.html b/ext/bg/settings.html index cc6f93e0..cc209c8a 100644 --- a/ext/bg/settings.html +++ b/ext/bg/settings.html @@ -406,6 +406,10 @@ +
+ +
+
diff --git a/ext/fg/js/frontend.js b/ext/fg/js/frontend.js index 6b0b50a6..da620136 100644 --- a/ext/fg/js/frontend.js +++ b/ext/fg/js/frontend.js @@ -326,6 +326,7 @@ class Frontend { selectText: scanningOptions.selectText, delay: scanningOptions.delay, touchInputEnabled: scanningOptions.touchInputEnabled, + pointerEventsEnabled: scanningOptions.pointerEventsEnabled, scanLength: scanningOptions.length, sentenceExtent: options.anki.sentenceExt, layoutAwareScan: scanningOptions.layoutAwareScan diff --git a/ext/mixed/js/display.js b/ext/mixed/js/display.js index 92e27faa..ef56f4aa 100644 --- a/ext/mixed/js/display.js +++ b/ext/mixed/js/display.js @@ -252,6 +252,7 @@ class Display extends EventDispatcher { selectText: scanning.selectText, delay: scanning.delay, touchInputEnabled: scanning.touchInputEnabled, + pointerEventsEnabled: scanning.pointerEventsEnabled, scanLength: scanning.length, sentenceExtent: options.anki.sentenceExt, layoutAwareScan: scanning.layoutAwareScan diff --git a/ext/mixed/js/text-scanner.js b/ext/mixed/js/text-scanner.js index d2383af4..c2016807 100644 --- a/ext/mixed/js/text-scanner.js +++ b/ext/mixed/js/text-scanner.js @@ -45,6 +45,7 @@ class TextScanner extends EventDispatcher { this._selectText = false; this._delay = 0; this._touchInputEnabled = false; + this._pointerEventsEnabled = false; this._scanLength = 1; this._sentenceExtent = 1; this._layoutAwareScan = false; @@ -59,6 +60,8 @@ class TextScanner extends EventDispatcher { this._preventNextMouseDown = false; this._preventNextClick = false; this._preventScroll = false; + this._penPointerPressed = false; + this._penPointerReleased = false; this._canClearSelection = true; } @@ -96,6 +99,8 @@ class TextScanner extends EventDispatcher { this._preventNextMouseDown = false; this._preventNextClick = false; this._preventScroll = false; + this._penPointerPressed = false; + this._penPointerReleased = false; this._enabledValue = value; @@ -106,12 +111,18 @@ class TextScanner extends EventDispatcher { } } - setOptions({inputs, deepContentScan, selectText, delay, touchInputEnabled, scanLength, sentenceExtent, layoutAwareScan}) { + setOptions({inputs, deepContentScan, selectText, delay, touchInputEnabled, pointerEventsEnabled, scanLength, sentenceExtent, layoutAwareScan}) { if (Array.isArray(inputs)) { - this._inputs = inputs.map(({include, exclude, types}) => ({ + this._inputs = inputs.map(({ + include, + exclude, + types, + options: {scanOnPenHover, scanOnPenPress, scanOnPenRelease} + }) => ({ include: this._getInputArray(include), exclude: this._getInputArray(exclude), - types: this._getInputTypeSet(types) + types: this._getInputTypeSet(types), + options: {scanOnPenHover, scanOnPenPress, scanOnPenRelease} })); } if (typeof deepContentScan === 'boolean') { @@ -126,6 +137,9 @@ class TextScanner extends EventDispatcher { if (typeof touchInputEnabled === 'boolean') { this._touchInputEnabled = touchInputEnabled; } + if (typeof pointerEventsEnabled === 'boolean') { + this._pointerEventsEnabled = pointerEventsEnabled; + } if (typeof scanLength === 'number') { this._scanLength = scanLength; } @@ -374,6 +388,162 @@ class TextScanner extends EventDispatcher { e.preventDefault(); // Disable scroll } + _onPointerOver(e) { + if (!e.isPrimary) { return; } + switch (e.pointerType) { + case 'mouse': return this._onMousePointerOver(e); + case 'touch': return this._onTouchPointerOver(e); + case 'pen': return this._onPenPointerOver(e); + } + } + + _onPointerDown(e) { + if (!e.isPrimary) { return; } + switch (e.pointerType) { + case 'mouse': return this._onMousePointerDown(e); + case 'touch': return this._onTouchPointerDown(e); + case 'pen': return this._onPenPointerDown(e); + } + } + + _onPointerMove(e) { + if (!e.isPrimary) { return; } + switch (e.pointerType) { + case 'mouse': return this._onMousePointerMove(e); + case 'touch': return this._onTouchPointerMove(e); + case 'pen': return this._onPenPointerMove(e); + } + } + + _onPointerUp(e) { + if (!e.isPrimary) { return; } + switch (e.pointerType) { + case 'mouse': return this._onMousePointerUp(e); + case 'touch': return this._onTouchPointerUp(e); + case 'pen': return this._onPenPointerUp(e); + } + } + + _onPointerCancel(e) { + if (!e.isPrimary) { return; } + switch (e.pointerType) { + case 'mouse': return this._onMousePointerCancel(e); + case 'touch': return this._onTouchPointerCancel(e); + case 'pen': return this._onPenPointerCancel(e); + } + } + + _onPointerOut(e) { + if (!e.isPrimary) { return; } + switch (e.pointerType) { + case 'mouse': return this._onMousePointerOut(e); + case 'touch': return this._onTouchPointerOut(e); + case 'pen': return this._onPenPointerOut(e); + } + } + + _onMousePointerOver(e) { + return this._onMouseOver(e); + } + + _onMousePointerDown(e) { + return this._onMouseDown(e); + } + + _onMousePointerMove(e) { + return this._onMouseMove(e); + } + + _onMousePointerUp() { + // NOP + } + + _onMousePointerCancel(e) { + return this._onMouseOut(e); + } + + _onMousePointerOut(e) { + return this._onMouseOut(e); + } + + _onTouchPointerOver() { + // NOP + } + + _onTouchPointerDown(e) { + const {clientX, clientY, pointerId} = e; + return this._onPrimaryTouchStart(e, clientX, clientY, pointerId); + } + + _onTouchPointerMove(e) { + if (!this._preventScroll || !e.cancelable) { + return; + } + + const inputInfo = this._getMatchingInputGroupFromEvent(e, 'touch'); + if (inputInfo === null) { return; } + + const {index, empty} = inputInfo; + this._searchAt(e.clientX, e.clientY, {type: 'touch', cause: 'touchMove', index, empty}); + } + + _onTouchPointerUp() { + return this._onPrimaryTouchEnd(); + } + + _onTouchPointerCancel() { + return this._onPrimaryTouchEnd(); + } + + _onTouchPointerOut() { + // NOP + } + + _onTouchMovePreventScroll(e) { + if (!this._preventScroll) { return; } + + if (e.cancelable) { + e.preventDefault(); + } else { + this._preventScroll = false; + } + } + + _onPenPointerOver(e) { + this._penPointerPressed = false; + this._penPointerReleased = false; + this._searchAtFromPen(e, e.clientX, e.clientY, 'pointerOver', false); + } + + _onPenPointerDown(e) { + this._penPointerPressed = true; + this._searchAtFromPen(e, e.clientX, e.clientY, 'pointerDown', true); + } + + _onPenPointerMove(e) { + if (this._penPointerPressed && (!this._preventScroll || !e.cancelable)) { return; } + this._searchAtFromPen(e, e.clientX, e.clientY, 'pointerMove', true); + } + + _onPenPointerUp() { + this._penPointerPressed = false; + this._penPointerReleased = true; + this._preventScroll = false; + } + + _onPenPointerCancel(e) { + this._onPenPointerOut(e); + } + + _onPenPointerOut() { + this._penPointerPressed = false; + this._penPointerReleased = false; + this._preventScroll = false; + this._preventNextContextMenu = false; + this._preventNextMouseDown = false; + this._preventNextClick = false; + } + async _scanTimerWait() { const delay = this._delay; const promise = promiseTimeout(delay, true); @@ -394,10 +564,19 @@ class TextScanner extends EventDispatcher { } } + _arePointerEventsSupported() { + return (this._pointerEventsEnabled && typeof PointerEvent !== 'undefined'); + } + _hookEvents() { - const eventListenerInfos = this._getMouseEventListeners(); - if (this._touchInputEnabled) { - eventListenerInfos.push(...this._getTouchEventListeners()); + let eventListenerInfos; + if (this._arePointerEventsSupported()) { + eventListenerInfos = this._getPointerEventListeners(); + } else { + eventListenerInfos = this._getMouseEventListeners(); + if (this._touchInputEnabled) { + eventListenerInfos.push(...this._getTouchEventListeners()); + } } for (const [node, type, listener, options] of eventListenerInfos) { @@ -405,6 +584,21 @@ class TextScanner extends EventDispatcher { } } + _getPointerEventListeners() { + return [ + [this._node, 'pointerover', this._onPointerOver.bind(this)], + [this._node, 'pointerdown', this._onPointerDown.bind(this)], + [this._node, 'pointermove', this._onPointerMove.bind(this)], + [this._node, 'pointerup', this._onPointerUp.bind(this)], + [this._node, 'pointercancel', this._onPointerCancel.bind(this)], + [this._node, 'pointerout', this._onPointerOut.bind(this)], + [this._node, 'touchmove', this._onTouchMovePreventScroll.bind(this), {passive: false}], + [this._node, 'mousedown', this._onMouseDown.bind(this)], + [this._node, 'click', this._onClick.bind(this)], + [this._node, 'auxclick', this._onAuxClick.bind(this)] + ]; + } + _getMouseEventListeners() { return [ [this._node, 'mousedown', this._onMouseDown.bind(this)], @@ -543,6 +737,34 @@ class TextScanner extends EventDispatcher { } } + async _searchAtFromPen(e, x, y, cause, prevent) { + if (this._pendingLookup) { return; } + + const type = 'pen'; + const inputInfo = this._getMatchingInputGroupFromEvent(e, type); + if (inputInfo === null) { return; } + + const {index, empty, input: {options}} = inputInfo; + if ( + (!options.scanOnPenRelease && this._penPointerReleased) || + !(this._penPointerPressed ? options.scanOnPenPress : options.scanOnPenHover) + ) { + return; + } + + await this._searchAt(x, y, {type, cause, index, empty}); + + if ( + prevent && + this._textSourceCurrent !== null + ) { + this._preventScroll = true; + this._preventNextContextMenu = true; + this._preventNextMouseDown = true; + this._preventNextClick = true; + } + } + _getMatchingInputGroupFromEvent(event, type) { const modifiers = DocumentUtil.getActiveModifiersAndButtons(event); this.trigger('activeModifiersChanged', {modifiers});