Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for forward and back mouse buttons #1919

Merged
merged 2 commits into from
Jan 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions core/encodings.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export const encodings = {
pseudoEncodingXvp: -309,
pseudoEncodingFence: -312,
pseudoEncodingContinuousUpdates: -313,
pseudoEncodingExtendedMouseButtons: -316,
pseudoEncodingCompressLevel9: -247,
pseudoEncodingCompressLevel0: -256,
pseudoEncodingVMwareCursor: 0x574d5664,
Expand Down
51 changes: 48 additions & 3 deletions core/rfb.js
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,8 @@ export default class RFB extends EventTargetMixin {

this._qemuExtKeyEventSupported = false;

this._extendedPointerEventSupported = false;

this._clipboardText = null;
this._clipboardServerCapabilitiesActions = {};
this._clipboardServerCapabilitiesFormats = {};
Expand Down Expand Up @@ -1051,10 +1053,11 @@ export default class RFB extends EventTargetMixin {
1: 1 << 2, // Right
2: 1 << 1, // Middle
3: 1 << 7, // Back
4: 1 << 8, // Forward
};

let bmask = 0;
for (let i = 0; i < 4; i++) {
for (let i = 0; i < 5; i++) {
if (buttons & (1 << i)) {
bmask |= buttonMaskMap[i];
}
Expand Down Expand Up @@ -1189,8 +1192,20 @@ export default class RFB extends EventTargetMixin {
if (this._rfbConnectionState !== 'connected') { return; }
if (this._viewOnly) { return; } // View only, skip mouse events

RFB.messages.pointerEvent(this._sock, this._display.absX(x),
this._display.absY(y), mask);
// Highest bit in mask is never sent to the server
if (mask & 0x8000) {
throw new Error("Illegal mouse button mask (mask: " + mask + ")");
}

let extendedMouseButtons = mask & 0x7f80;

if (this._extendedPointerEventSupported && extendedMouseButtons) {
RFB.messages.extendedPointerEvent(this._sock, this._display.absX(x),
this._display.absY(y), mask);
} else {
RFB.messages.pointerEvent(this._sock, this._display.absX(x),
this._display.absY(y), mask);
}
}

_handleWheel(ev) {
Expand Down Expand Up @@ -2229,6 +2244,7 @@ export default class RFB extends EventTargetMixin {
encs.push(encodings.pseudoEncodingContinuousUpdates);
encs.push(encodings.pseudoEncodingDesktopName);
encs.push(encodings.pseudoEncodingExtendedClipboard);
encs.push(encodings.pseudoEncodingExtendedMouseButtons);

if (this._fbDepth == 24) {
encs.push(encodings.pseudoEncodingVMwareCursor);
Expand Down Expand Up @@ -2658,6 +2674,10 @@ export default class RFB extends EventTargetMixin {
case encodings.pseudoEncodingExtendedDesktopSize:
return this._handleExtendedDesktopSize();

case encodings.pseudoEncodingExtendedMouseButtons:
this._extendedPointerEventSupported = true;
return true;

case encodings.pseudoEncodingQEMULedEvent:
return this._handleLedEvent();

Expand Down Expand Up @@ -3067,6 +3087,10 @@ RFB.messages = {
pointerEvent(sock, x, y, mask) {
sock.sQpush8(5); // msg-type

// Marker bit must be set to 0, otherwise the server might
// confuse the marker bit with the highest bit in a normal
// PointerEvent message.
mask = mask & 0x7f;
sock.sQpush8(mask);

sock.sQpush16(x);
Expand All @@ -3075,6 +3099,27 @@ RFB.messages = {
sock.flush();
},

extendedPointerEvent(sock, x, y, mask) {
sock.sQpush8(5); // msg-type

let higherBits = (mask >> 7) & 0xff;

// Bits 2-7 are reserved
if (higherBits & 0xfc) {
throw new Error("Invalid mouse button mask: " + mask);
}

let lowerBits = mask & 0x7f;
lowerBits |= 0x80; // Set marker bit to 1

sock.sQpush8(lowerBits);
sock.sQpush16(x);
sock.sQpush16(y);
sock.sQpush8(higherBits);

sock.flush();
},

// Used to build Notify and Request data.
_buildExtendedClipboardFlags(actions, formats) {
let data = new Uint8Array(4);
Expand Down
96 changes: 71 additions & 25 deletions tests/test.rfb.js
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,30 @@ describe('Remote Frame Buffer protocol client', function () {
client._canvas.dispatchEvent(ev);
}

function sendFbuMsg(rectInfo, rectData, client, rectCnt) {
let data = [];

if (!rectCnt || rectCnt > -1) {
// header
data.push(0); // msg type
data.push(0); // padding
push16(data, rectCnt || rectData.length);
}

for (let i = 0; i < rectData.length; i++) {
if (rectInfo[i]) {
push16(data, rectInfo[i].x);
push16(data, rectInfo[i].y);
push16(data, rectInfo[i].width);
push16(data, rectInfo[i].height);
push32(data, rectInfo[i].encoding);
}
data = data.concat(rectData[i]);
}

client._sock._websocket._receiveData(new Uint8Array(data));
}

describe('Connecting/Disconnecting', function () {
describe('#RFB (constructor)', function () {
let open, attach;
Expand Down Expand Up @@ -2757,30 +2781,6 @@ describe('Remote Frame Buffer protocol client', function () {
});

describe('Framebuffer update handling', function () {
function sendFbuMsg(rectInfo, rectData, client, rectCnt) {
let data = [];

if (!rectCnt || rectCnt > -1) {
// header
data.push(0); // msg type
data.push(0); // padding
push16(data, rectCnt || rectData.length);
}

for (let i = 0; i < rectData.length; i++) {
if (rectInfo[i]) {
push16(data, rectInfo[i].x);
push16(data, rectInfo[i].y);
push16(data, rectInfo[i].width);
push16(data, rectInfo[i].height);
push32(data, rectInfo[i].encoding);
}
data = data.concat(rectData[i]);
}

client._sock._websocket._receiveData(new Uint8Array(data));
}

it('should send an update request if there is sufficient data', function () {
let esock = new Websock();
let ews = new FakeWebSocket();
Expand Down Expand Up @@ -3265,6 +3265,7 @@ describe('Remote Frame Buffer protocol client', function () {
expect(spy).to.have.been.calledOnce;
expect(spy.args[0][0].detail.name).to.equal('som€ nam€');
});

});

describe('Caps Lock and Num Lock remote fixup', function () {
Expand Down Expand Up @@ -3757,6 +3758,7 @@ describe('Remote Frame Buffer protocol client', function () {
describe('Asynchronous events', function () {
let client;
let pointerEvent;
let extendedPointerEvent;
let keyEvent;
let qemuKeyEvent;

Expand All @@ -3770,12 +3772,14 @@ describe('Remote Frame Buffer protocol client', function () {
client.focusOnClick = false;

pointerEvent = sinon.spy(RFB.messages, 'pointerEvent');
extendedPointerEvent = sinon.spy(RFB.messages, 'extendedPointerEvent');
keyEvent = sinon.spy(RFB.messages, 'keyEvent');
qemuKeyEvent = sinon.spy(RFB.messages, 'QEMUExtendedKeyEvent');
});

afterEach(function () {
pointerEvent.restore();
extendedPointerEvent.restore();
keyEvent.restore();
qemuKeyEvent.restore();
});
Expand Down Expand Up @@ -3884,6 +3888,23 @@ describe('Remote Frame Buffer protocol client', function () {
50, 70, 0x0);
});

it('should send extended pointer event when server supports extended pointer events', function () {
// Enable extended pointer events
sendFbuMsg([{ x: 0, y: 0, width: 0, height: 0, encoding: -316 }], [[]], client);

sendMouseButtonEvent(50, 70, true, 0x10, client);

expect(extendedPointerEvent).to.have.been.calledOnceWith(client._sock,
50, 70, 0x100);
});

it('should send normal pointer event when server does not support extended pointer events', function () {
sendMouseButtonEvent(50, 70, true, 0x10, client);

expect(pointerEvent).to.have.been.calledOnceWith(client._sock,
50, 70, 0x100);
});

describe('Event aggregation', function () {
it('should send a single pointer event on mouse movement', function () {
sendMouseMoveEvent(50, 70, 0x0, client);
Expand Down Expand Up @@ -5135,11 +5156,36 @@ describe('RFB messages', function () {
});

it('should send correct data for pointer events', function () {
RFB.messages.pointerEvent(sock, 12345, 54321, 0x2b);
let expected =
[ 5, 0x2b, 0x30, 0x39, 0xd4, 0x31];
expect(sock).to.have.sent(new Uint8Array(expected));
});

it('should send correct data for pointer events with marker bit set', function () {
RFB.messages.pointerEvent(sock, 12345, 54321, 0xab);
let expected =
[ 5, 0xab, 0x30, 0x39, 0xd4, 0x31];
[ 5, 0x2b, 0x30, 0x39, 0xd4, 0x31];
expect(sock).to.have.sent(new Uint8Array(expected));
});
CendioHalim marked this conversation as resolved.
Show resolved Hide resolved

it('should send correct data for pointer events with extended button bits set', function () {
RFB.messages.pointerEvent(sock, 12345, 54321, 0x3ab);
let expected =
[ 5, 0x2b, 0x30, 0x39, 0xd4, 0x31];
expect(sock).to.have.sent(new Uint8Array(expected));
});

it('should send correct data for extended pointer events', function () {
RFB.messages.extendedPointerEvent(sock, 12345, 54321, 0xab);
let expected =
[ 5, 0xab, 0x30, 0x39, 0xd4, 0x31, 0x1];
expect(sock).to.have.sent(new Uint8Array(expected));
});

it('should not send invalid data for extended pointer events', function () {
expect(() => RFB.messages.extendedPointerEvent(sock, 12345, 54321, 0x3ab)).to.throw(Error);
});
});
CendioHalim marked this conversation as resolved.
Show resolved Hide resolved

describe('Clipboard events', function () {
Expand Down
Loading