diff --git a/app/ui.js b/app/ui.js index 21c2bd80..206018ab 100644 --- a/app/ui.js +++ b/app/ui.js @@ -41,6 +41,7 @@ import KeyTable from "../core/input/keysym.js"; import keysyms from "../core/input/keysymdef.js"; import Keyboard from "../core/input/keyboard.js"; import RFB from "../core/rfb.js"; +import { MouseButtonMapper, XVNC_BUTTONS } from "../core/mousebuttonmapper.js"; import * as WebUtil from "./webutil.js"; const PAGE_TITLE = "KasmVNC"; @@ -264,6 +265,24 @@ const UI = { UI.setupSettingLabels(); UI.updateQuality(); }, + initMouseButtonMapper() { + const mouseButtonMapper = new MouseButtonMapper(); + + const settings = WebUtil.readSetting("mouseButtonMapper"); + if (settings) { + mouseButtonMapper.load(settings); + return mouseButtonMapper; + } + + mouseButtonMapper.set(0, XVNC_BUTTONS.LEFT_BUTTON); + mouseButtonMapper.set(1, XVNC_BUTTONS.MIDDLE_BUTTON); + mouseButtonMapper.set(2, XVNC_BUTTONS.RIGHT_BUTTON); + mouseButtonMapper.set(3, XVNC_BUTTONS.BACK_BUTTON); + mouseButtonMapper.set(4, XVNC_BUTTONS.FORWARD_BUTTON); + WebUtil.writeSetting("mouseButtonMapper", mouseButtonMapper.dump()); + + return mouseButtonMapper; + }, // Adds a link to the label elements on the corresponding input elements setupSettingLabels() { const labels = document.getElementsByTagName('LABEL'); @@ -1381,6 +1400,7 @@ const UI = { UI.rfb.keyboard.enableIME = UI.getSetting('enable_ime'); UI.rfb.clipboardBinary = supportsBinaryClipboard() && UI.rfb.clipboardSeamless; UI.rfb.enableWebRTC = UI.getSetting('enable_webrtc'); + UI.rfb.mouseButtonMapper = UI.initMouseButtonMapper(); //Only explicitly request permission to clipboard on browsers that support binary clipboard access if (supportsBinaryClipboard()) { diff --git a/core/mousebuttonmapper.js b/core/mousebuttonmapper.js new file mode 100644 index 00000000..b69475a4 --- /dev/null +++ b/core/mousebuttonmapper.js @@ -0,0 +1,67 @@ +export const XVNC_BUTTONS = { + LEFT_BUTTON: 1, + MIDDLE_BUTTON: 2, + RIGHT_BUTTON: 3, + TURN_SCROLL_WHEEL_UP: 4, + TURN_SCROLL_WHEEL_DOWN: 5, + PUSH_SCROLL_WHEEL_LEFT: 6, + PUSH_SCROLL_WHEEL_RIGHT: 7, + BACK_BUTTON: 8, + FORWARD_BUTTON: 9 +}; + +export function xvncButtonToMask(xvncButton) { + return 1 << (xvncButton - 1); +} + +export default class MouseButtonMapper { + constructor() { + this.map = new Map(); + } + + get(mouseButton) { + if (!this.map.has(mouseButton)) { + return mouseButton; + } + + return this.map.get(mouseButton); + } + + set(mouseButton, xorgMouseButton) { + return this.map.set(mouseButton, xorgMouseButton); + } + + delete(mouseButton) { + return this.map.delete(mouseButton); + } + + dump() { + return JSON.stringify(this.map, this._replacer); + } + + load(json) { + this.map = JSON.parse(json, this._reviver); + } + + _replacer(key, value) { + if (!(value instanceof Map)) { + return value; + } + + return { + dataType: 'Map', + value: Array.from(value.entries()) + }; + } + + _reviver(key, value) { + if (typeof value === 'object' && value !== null) { + if (value.dataType === 'Map') { + return new Map(value.value); + } + } + return value; + } +} + +export { MouseButtonMapper }; diff --git a/core/rfb.js b/core/rfb.js index cec38580..18f788eb 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -26,6 +26,7 @@ import DES from "./des.js"; import KeyTable from "./input/keysym.js"; import XtScancode from "./input/xtscancodes.js"; import { encodings } from "./encodings.js"; +import { MouseButtonMapper, xvncButtonToMask } from "./mousebuttonmapper.js"; import RawDecoder from "./decoders/raw.js"; import CopyRectDecoder from "./decoders/copyrect.js"; @@ -192,6 +193,7 @@ export default class RFB extends EventTargetMixin { this._viewportHasMoved = false; this._accumulatedWheelDeltaX = 0; this._accumulatedWheelDeltaY = 0; + this.mouseButtonMapper = null; // Gesture state this._gestureLastTapTime = null; @@ -1558,6 +1560,7 @@ export default class RFB extends EventTargetMixin { this._canvas); } + const mappedButton = this.mouseButtonMapper.get(ev.button); switch (ev.type) { case 'mousedown': setCapture(this._canvas); @@ -1575,11 +1578,11 @@ export default class RFB extends EventTargetMixin { this.checkLocalClipboard(); this._handleMouseButton(pos.x, pos.y, - true, 1 << ev.button); + true, xvncButtonToMask(mappedButton)); break; case 'mouseup': this._handleMouseButton(pos.x, pos.y, - false, 1 << ev.button); + false, xvncButtonToMask(mappedButton)); break; case 'mousemove': this._handleMouseMove(pos.x, pos.y); @@ -3659,21 +3662,22 @@ RFB.messages = { buff[offset] = 5; // msg-type - buff[offset + 1] = mask; + buff[offset + 1] = mask >> 8; + buff[offset + 2] = mask; - buff[offset + 2] = x >> 8; - buff[offset + 3] = x; + buff[offset + 3] = x >> 8; + buff[offset + 4] = x; - buff[offset + 4] = y >> 8; - buff[offset + 5] = y; + buff[offset + 5] = y >> 8; + buff[offset + 6] = y; - buff[offset + 6] = dX >> 8; - buff[offset + 7] = dX; + buff[offset + 7] = dX >> 8; + buff[offset + 8] = dX; - buff[offset + 8] = dY >> 8; - buff[offset + 9] = dY; + buff[offset + 9] = dY >> 8; + buff[offset + 10] = dY; - sock._sQlen += 10; + sock._sQlen += 11; sock.flush(); },