Skip to content

Commit

Permalink
workaround for minecraft pointerevent bug on chromeos
Browse files Browse the repository at this point in the history
  • Loading branch information
riknoll committed Nov 13, 2024
1 parent 1e4e5e8 commit debdfb9
Show file tree
Hide file tree
Showing 3 changed files with 120 additions and 0 deletions.
114 changes: 114 additions & 0 deletions pxtblocks/monkeyPatches/gesture.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import * as Blockly from "blockly";

interface PatchedGesture extends Blockly.Gesture {
id: number | undefined
boundEvents_: Blockly.browserEvents.Data[];
}

export function monkeyPatchGesture() {
// This monkey patch is only required for the in-game experience of Minecraft on ChromeOS.
// For some reason, events are occasionally dropped by the Android webview when multitouch
// is being used which can cause Blockly to get stuck thinking it's in a gesture when it isn't.
// This patch effectively removes multitouch by ignoring all pointer events except for ones
// generated by the first pointer that is encountered. All pointerup events are still allowed
// through (see wrapHandler below)
if (!pxt.BrowserUtils.isChromeOS() || !pxt.BrowserUtils.isInGame()) {
return;
}

const oldDoStart = Blockly.Gesture.prototype.doStart;
Blockly.Gesture.prototype.doStart = function(this: PatchedGesture, e: PointerEvent) {
if (this.id) {
e.stopPropagation();
e.preventDefault();
return;
}

this.id = e.pointerId;
oldDoStart.call(this, e);
}

Blockly.Gesture.prototype.bindMouseEvents = function (this: PatchedGesture, e: PointerEvent) {
if (!this.boundEvents_) this.boundEvents_ = [];

const createSyntheticEvent = (e: PointerEvent) => {
const syntheticEvent = new PointerEvent(e.type, {
...e,
clientX: e.clientX,
clientY: e.clientY,
pointerId: this.id,
});
syntheticEvent.stopPropagation = () => e.stopPropagation();
syntheticEvent.stopImmediatePropagation = () => e.stopImmediatePropagation();
syntheticEvent.preventDefault = () => e.preventDefault();
return syntheticEvent;
}

const wrapHandler = (handler: (e: PointerEvent) => void) => {
return (e: PointerEvent) => {
// Always let pointerup events through, just remap them to the
// correct pointerId. This lets us recover if we end up getting
// stuck; the next click or drag on the workspace should clear the
// current gesture.
if (e.type === "pointerup") {
e = createSyntheticEvent(e);
}
else if (e.pointerId !== this.id) {
return;
}

try {
handler.call(this, e);
}
catch (e) {
pxt.error("Uncaught error while executing gesture handler", e);
this.cancel();
this.dispose();
}
}
}

this.boundEvents_.push(
Blockly.browserEvents.conditionalBind(
document,
'pointerdown',
null,
wrapHandler(this.handleStart),
/* opt_noCaptureIdentifier */ true,
),
);
this.boundEvents_.push(
Blockly.browserEvents.conditionalBind(
document,
'pointermove',
null,
wrapHandler(this.handleMove),
/* opt_noCaptureIdentifier */ true,
),
);
this.boundEvents_.push(
Blockly.browserEvents.conditionalBind(
document,
'pointerup',
null,
wrapHandler(this.handleUp),
/* opt_noCaptureIdentifier */ true,
),
);

e.preventDefault();
e.stopPropagation();
}

const oldDispose = Blockly.Gesture.prototype.dispose;

Blockly.Gesture.prototype.dispose = function (this: PatchedGesture) {
oldDispose.call(this);
if (this.boundEvents_) {
for (const event of this.boundEvents_) {
Blockly.browserEvents.unbind(event);
}
this.boundEvents_.length = 0;
}
}
}
2 changes: 2 additions & 0 deletions pxtblocks/monkeyPatches/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { monkeyPatchBlockSvg } from "./blockSvg";
import { monkeyPatchGesture } from "./gesture";
import { monkeyPatchGrid } from "./grid";

export function applyMonkeyPatches() {
monkeyPatchBlockSvg();
monkeyPatchGrid();
monkeyPatchGesture();
}
4 changes: 4 additions & 0 deletions pxtlib/browserutils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,10 @@ namespace pxt.BrowserUtils {
return hasNavigator() && /Epiphany/i.test(navigator.userAgent);
}

export function isChromeOS(): boolean {
return hasNavigator() && /cros/i.test(navigator.userAgent);
}

export function isTouchEnabled(): boolean {
return typeof window !== "undefined" &&
('ontouchstart' in window // works on most browsers
Expand Down

0 comments on commit debdfb9

Please sign in to comment.