diff --git a/app/ui.js b/app/ui.js index adab59a8..87649258 100644 --- a/app/ui.js +++ b/app/ui.js @@ -185,7 +185,7 @@ const UI = { UI.initSetting('bell', 'on'); UI.initSetting('view_only', false); UI.initSetting('show_dot', false); - UI.initSetting('ultravnc_gestures', false); + UI.initSetting('gestures_mode', 'novnc'); UI.initSetting('path', 'websockify'); UI.initSetting('repeaterID', ''); UI.initSetting('reconnect', false); @@ -372,7 +372,7 @@ const UI = { UI.addSettingChangeHandler('view_only', UI.updateViewOnly); UI.addSettingChangeHandler('show_dot'); UI.addSettingChangeHandler('show_dot', UI.updateShowDotCursor); - UI.addSettingChangeHandler('ultravnc_gestures'); + UI.addSettingChangeHandler('gestures_mode'); UI.addSettingChangeHandler('host'); UI.addSettingChangeHandler('port'); UI.addSettingChangeHandler('path'); @@ -443,7 +443,7 @@ const UI = { UI.disableSetting('port'); UI.disableSetting('path'); UI.disableSetting('repeaterID'); - UI.disableSetting('ultravnc_gestures'); + UI.disableSetting('gestures_mode'); // Hide the controlbar after 2 seconds UI.closeControlbarTimeout = setTimeout(UI.closeControlbar, 2000); @@ -454,7 +454,7 @@ const UI = { UI.enableSetting('port'); UI.enableSetting('path'); UI.enableSetting('repeaterID'); - UI.enableSetting('ultravnc_gestures'); + UI.enableSetting('gestures_mode'); UI.updatePowerButton(); UI.keepControlbar(); } @@ -1077,7 +1077,7 @@ const UI = { { shared: UI.getSetting('shared'), repeaterID: UI.getSetting('repeaterID'), credentials: { password: password }, - useUltraVNCGestures: UI.getSetting('ultravnc_gestures') }); + gesturesMode: UI.getSetting('gestures_mode') }); } catch (exc) { Log.Error("Failed to connect to server: " + exc); UI.updateVisualState('disconnected'); diff --git a/core/input/touchhandlerultravnc.js b/core/input/touchhandlerultravnc.js index 48bb0015..f44a660e 100644 --- a/core/input/touchhandlerultravnc.js +++ b/core/input/touchhandlerultravnc.js @@ -15,6 +15,48 @@ export default class TouchHandlerUltraVNC { static IDFORMAT_32 = 0x1; // 32 bits ID static IDFORMAT_CLEAR = 0xF; // No more touch points + // GII + static giiMsgType = 253; + static giiEventInjectionMsgType = 128; + static giiDeviceVersionMsgType = 129; + static giiDeviceCreationMsgType = 130; + + static giiDeviceCreationMsgSize = 172; + static giiDeviceVersion = 1; + static giiDeviceVersionMsgSize = 2; + + static giiEventInjectionHeaderSize = 4; + static giiEventInjectionSize = this.giiEventInjectionHeaderSize + 16; + static giiEventInjectionTouchSize = 12; + static giiEventInjectionEventType = 12; + + static giiDeviceName = "NOVNC-MT"; + static giiDeviceNameSize = 31; + static giiDeviceLongName = "noVNC Multitouch Device"; + static giiDeviceLongNameSize = 74; + static giiDeviceShortName = "NMD"; + static giiDeviceShortNameSize = 4; + + static giiDNTerm = 0; + static giiVendorID = 0x0908; + static giiProductID = 0x000b; + static giiEventMask = 0x00002000; + static giiNumRegisters = 0; + static giiNumValuators = 1; + static giiNumButtons = 5; + static giiNumTouches = 6; + static giiIndex = 0; + static giiLNTerm = 0; + static giiSNTerm = 0; + static giiRangeMin = 0; + static giiRangeCenter = 0; + static giiRangeMax = 0; + static giiSIUnit = 0; + static giiSIAdd = 0; + static giiSIMul = 0; + static giiSIDiv = 0; + static giiSIShift = 0; + constructor() { this._target = null; diff --git a/core/rfb.js b/core/rfb.js index 3ee10522..cc38f32b 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -120,7 +120,7 @@ export default class RFB extends EventTargetMixin { this._shared = 'shared' in options ? !!options.shared : true; this._repeaterID = options.repeaterID || ''; this._wsProtocols = options.wsProtocols || []; - this._useUltraVNCGestures = options.useUltraVNCGestures || false; + this._gesturesMode = options.gesturesMode || '' // Internal state this._rfbConnectionState = ''; @@ -270,10 +270,12 @@ export default class RFB extends EventTargetMixin { this._remoteCapsLock = null; // Null indicates unknown or irrelevant this._remoteNumLock = null; - if (this._useUltraVNCGestures) { - this._gestures = new TouchHandlerUltraVNC(); - } else { - this._gestures = new GestureHandler(); + switch (this._gesturesMode) { + case 'ultravnc': + this._gestures = new TouchHandlerUltraVNC(); + break; + default: + this._gestures = new GestureHandler(); } this._sock = new Websock(); @@ -605,12 +607,14 @@ export default class RFB extends EventTargetMixin { this._canvas.addEventListener("wheel", this._eventHandlers.handleWheel); // Gesture events - if (this._useUltraVNCGestures) { - this._canvas.addEventListener('ultravnctouch', this._eventHandlers.handleUltraVNCTouch); - } else { - this._canvas.addEventListener('gesturestart', this._eventHandlers.handleGesture); - this._canvas.addEventListener('gesturemove', this._eventHandlers.handleGesture); - this._canvas.addEventListener('gestureend', this._eventHandlers.handleGesture); + switch (this._gesturesMode) { + case 'ultravnc': + this._canvas.addEventListener('ultravnctouch', this._eventHandlers.handleUltraVNCTouch); + break; + default: + this._canvas.addEventListener('gesturestart', this._eventHandlers.handleGesture); + this._canvas.addEventListener('gesturemove', this._eventHandlers.handleGesture); + this._canvas.addEventListener('gestureend', this._eventHandlers.handleGesture); } Log.Debug("<< RFB.connect"); @@ -619,13 +623,17 @@ export default class RFB extends EventTargetMixin { _disconnect() { Log.Debug(">> RFB.disconnect"); this._cursor.detach(); - if (this._useUltraVNCGestures) { - this._canvas.removeEventListener('ultravnctouch', this._eventHandlers.handleUltraVNCTouch); - } else { - this._canvas.removeEventListener('gesturestart', this._eventHandlers.handleGesture); - this._canvas.removeEventListener('gesturemove', this._eventHandlers.handleGesture); - this._canvas.removeEventListener('gestureend', this._eventHandlers.handleGesture); + + switch (this._gesturesMode) { + case 'ultravnc': + this._canvas.removeEventListener('ultravnctouch', this._eventHandlers.handleUltraVNCTouch); + break; + default: + this._canvas.removeEventListener('gesturestart', this._eventHandlers.handleGesture); + this._canvas.removeEventListener('gesturemove', this._eventHandlers.handleGesture); + this._canvas.removeEventListener('gestureend', this._eventHandlers.handleGesture); } + this._canvas.removeEventListener("wheel", this._eventHandlers.handleWheel); this._canvas.removeEventListener('mousedown', this._eventHandlers.handleMouse); this._canvas.removeEventListener('mouseup', this._eventHandlers.handleMouse); @@ -1394,18 +1402,18 @@ export default class RFB extends EventTargetMixin { } _handleUltraVNCTouch(ev) { - this._sock.sQpush8(253); // GII message type - this._sock.sQpush8(128); // GII event - this._sock.sQpush16(4 + 16 + (6 * 2 * ev.detail.currentTouches.length)); // Length - this._sock.sQpush8(4 + 16 + (6 * 2 * ev.detail.currentTouches.length)); // eventSize - this._sock.sQpush8(12); // eventType + this._sock.sQpush8(TouchHandlerUltraVNC.giiMsgType); // GII message type + this._sock.sQpush8(TouchHandlerUltraVNC.giiEventInjectionMsgType); // GII event + this._sock.sQpush16(TouchHandlerUltraVNC.giiEventInjectionSize + (TouchHandlerUltraVNC.giiEventInjectionTouchSize * ev.detail.currentTouches.length)); // length, not used + this._sock.sQpush8(TouchHandlerUltraVNC.giiEventInjectionSize + (TouchHandlerUltraVNC.giiEventInjectionTouchSize * ev.detail.currentTouches.length)); // eventSize, not used + this._sock.sQpush8(TouchHandlerUltraVNC.giiEventInjectionEventType); // eventType, not used this._sock.sQpush16(0); // padding - this._sock.sQpush32(ev.detail.giiDeviceOrigin); // deviceOrigin - this._sock.sQpush32(ev.detail.currentTouches.length); // first - this._sock.sQpush32(6 * ev.detail.currentTouches.length); // count - + this._sock.sQpush32(ev.detail.giiDeviceOrigin); + this._sock.sQpush32(ev.detail.currentTouches.length); // nb of touch events + this._sock.sQpush32(TouchHandlerUltraVNC.giiNumTouches * ev.detail.currentTouches.length); // count + let pointerUpIds = []; - + // Send all current touches for (let i = 0; i < ev.detail.currentTouches.length; i++) { let valuatorFlag = 0x00000000; @@ -1413,28 +1421,28 @@ export default class RFB extends EventTargetMixin { valuatorFlag |= TouchHandlerUltraVNC.IDFORMAT_32; if (ev.detail.currentTouches[i].status !== "POINTER_UP") valuatorFlag |= TouchHandlerUltraVNC.PF_flag; if (ev.detail.currentTouches[i].event.identifier === 0) valuatorFlag |= TouchHandlerUltraVNC.IF_flag; // IF_flag - + this._sock.sQpush32(valuatorFlag); this._sock.sQpush32(ev.detail.currentTouches[i].event.touchIdentifier); - + let scaledPosition = clientToElement(ev.detail.currentTouches[i].event.clientX, ev.detail.currentTouches[i].event.clientY, this._canvas); - + if ((valuatorFlag & TouchHandlerUltraVNC.LENGTH_16_flag) !== 0) { let scaledX16 = this._display.absX(scaledPosition.x) & 0xFFFF; let scaledY16 = this._display.absY(scaledPosition.y) & 0xFFFF; let coordinates = (scaledX16 << 16) | scaledY16; this._sock.sQpush32(coordinates); } - + // Keep track of last released touches if (ev.detail.currentTouches[i].status === "POINTER_UP") { pointerUpIds.push(ev.detail.currentTouches[i].event.identifier); } } - + this._sock.flush(); - + // Remove released touches from current touches in handler for (let i = 0; i < pointerUpIds.length; i++) { const index = ev.detail.currentTouches.findIndex(t => t.event.identifier === pointerUpIds[i]); @@ -1442,10 +1450,9 @@ export default class RFB extends EventTargetMixin { this._gestures._removeTouch(index); } } - + // Interrupt touch sending interval if (ev.detail.currentTouches.length === 0 && this._sendTouchesIntervalId !== -1) { - Log.Debug("NO MORE TOUCHES\n"); this._gestures._interruptTouches(); this._sendEmptyTouch(ev.detail.giiDeviceOrigin); return; @@ -1457,176 +1464,18 @@ export default class RFB extends EventTargetMixin { valuatorFlag |= TouchHandlerUltraVNC.LENGTH_16_flag; valuatorFlag |= TouchHandlerUltraVNC.IDFORMAT_CLEAR; - this._sock.sQpush8(253); // GII message type - this._sock.sQpush8(128); // GII event - this._sock.sQpush16(24); // Header length - this._sock.sQpush8(24); // Event size - this._sock.sQpush8(12); // eventType + this._sock.sQpush8(TouchHandlerUltraVNC.giiMsgType); // GII message type + this._sock.sQpush8(TouchHandlerUltraVNC.giiEventInjectionMsgType); // GII event + this._sock.sQpush16(TouchHandlerUltraVNC.giiEventInjectionHeaderSize + TouchHandlerUltraVNC.giiEventInjectionSize); + this._sock.sQpush8(TouchHandlerUltraVNC.giiEventInjectionHeaderSize + TouchHandlerUltraVNC.giiEventInjectionSize); + this._sock.sQpush8(TouchHandlerUltraVNC.giiEventInjectionEventType); this._sock.sQpush16(0); // padding this._sock.sQpush32(giiDeviceOrigin); // deviceOrigin - this._sock.sQpush32(1); // first - this._sock.sQpush32(4); // Count - this._sock.sQpush32(valuatorFlag); // Flag - this._sock.sQpush32(0); // Empty Id - this._sock.sQpush32(0); // Empty coordinates - this._sock.flush(); - } - - _handleUltraVNCTouch(ev) { - this._sock.sQpush8(253); // GII message type - this._sock.sQpush8(128); // GII event - this._sock.sQpush16(4 + 16 + (6 * 2 * ev.detail.currentTouches.length)); // Length - this._sock.sQpush8(4 + 16 + (6 * 2 * ev.detail.currentTouches.length)); // eventSize - this._sock.sQpush8(12); // eventType - this._sock.sQpush16(0); // padding - this._sock.sQpush32(ev.detail.giiDeviceOrigin); // deviceOrigin - this._sock.sQpush32(ev.detail.currentTouches.length); // first - this._sock.sQpush32(6 * ev.detail.currentTouches.length); // count - - let pointerUpIds = []; - - // Send all current touches - for (let i = 0; i < ev.detail.currentTouches.length; i++) { - let valuatorFlag = 0x00000000; - valuatorFlag |= TouchHandlerUltraVNC.LENGTH_16_flag; - valuatorFlag |= TouchHandlerUltraVNC.IDFORMAT_32; - if (ev.detail.currentTouches[i].status !== "POINTER_UP") valuatorFlag |= TouchHandlerUltraVNC.PF_flag; - if (ev.detail.currentTouches[i].event.identifier === 0) valuatorFlag |= TouchHandlerUltraVNC.IF_flag; // IF_flag - - this._sock.sQpush32(valuatorFlag); - this._sock.sQpush32(ev.detail.currentTouches[i].event.touchIdentifier); - - let scaledPosition = clientToElement(ev.detail.currentTouches[i].event.clientX, ev.detail.currentTouches[i].event.clientY, - this._canvas); - - if ((valuatorFlag & TouchHandlerUltraVNC.LENGTH_16_flag) !== 0) { - let scaledX16 = this._display.absX(scaledPosition.x) & 0xFFFF; - let scaledY16 = this._display.absY(scaledPosition.y) & 0xFFFF; - let coordinates = (scaledX16 << 16) | scaledY16; - this._sock.sQpush32(coordinates); - } - - // Keep track of last released touches - if (ev.detail.currentTouches[i].status === "POINTER_UP") { - pointerUpIds.push(ev.detail.currentTouches[i].event.identifier); - } - } - - this._sock.flush(); - - // Remove released touches from current touches in handler - for (let i = 0; i < pointerUpIds.length; i++) { - const index = ev.detail.currentTouches.findIndex(t => t.event.identifier === pointerUpIds[i]); - if (index !== -1) { - this._gestures._removeTouch(index); - } - } - - // Interrupt touch sending interval - if (ev.detail.currentTouches.length === 0 && this._sendTouchesIntervalId !== -1) { - Log.Debug("NO MORE TOUCHES\n"); - this._gestures._interruptTouches(); - this._sendEmptyTouch(ev.detail.giiDeviceOrigin); - return; - } - } - - _sendEmptyTouch(giiDeviceOrigin) { - let valuatorFlag = 0x00000000; - valuatorFlag |= TouchHandlerUltraVNC.LENGTH_16_flag; - valuatorFlag |= TouchHandlerUltraVNC.IDFORMAT_CLEAR; - - this._sock.sQpush8(253); // GII message type - this._sock.sQpush8(128); // GII event - this._sock.sQpush16(24); // Header length - this._sock.sQpush8(24); // Event size - this._sock.sQpush8(12); // eventType - this._sock.sQpush16(0); // padding - this._sock.sQpush32(giiDeviceOrigin); // deviceOrigin - this._sock.sQpush32(1); // first - this._sock.sQpush32(4); // Count - this._sock.sQpush32(valuatorFlag); // Flag - this._sock.sQpush32(0); // Empty Id - this._sock.sQpush32(0); // Empty coordinates - this._sock.flush(); - } - - _handleUltraVNCTouch(ev) { - this._sock.sQpush8(253); // GII message type - this._sock.sQpush8(128); // GII event - this._sock.sQpush16(4 + 16 + (6 * 2 * ev.detail.currentTouches.length)); // Length - this._sock.sQpush8(4 + 16 + (6 * 2 * ev.detail.currentTouches.length)); // eventSize - this._sock.sQpush8(12); // eventType - this._sock.sQpush16(0); // padding - this._sock.sQpush32(ev.detail.giiDeviceOrigin); // deviceOrigin - this._sock.sQpush32(ev.detail.currentTouches.length); // first - this._sock.sQpush32(6 * ev.detail.currentTouches.length); // count - - let pointerUpIds = []; - - // Send all current touches - for (let i = 0; i < ev.detail.currentTouches.length; i++) { - let valuatorFlag = 0x00000000; - valuatorFlag |= TouchHandlerUltraVNC.LENGTH_16_flag; - valuatorFlag |= TouchHandlerUltraVNC.IDFORMAT_32; - if (ev.detail.currentTouches[i].status !== "POINTER_UP") valuatorFlag |= TouchHandlerUltraVNC.PF_flag; - if (ev.detail.currentTouches[i].event.identifier === 0) valuatorFlag |= TouchHandlerUltraVNC.IF_flag; // IF_flag - - this._sock.sQpush32(valuatorFlag); - this._sock.sQpush32(ev.detail.currentTouches[i].event.touchIdentifier); - - let scaledPosition = clientToElement(ev.detail.currentTouches[i].event.clientX, ev.detail.currentTouches[i].event.clientY, - this._canvas); - - if ((valuatorFlag & TouchHandlerUltraVNC.LENGTH_16_flag) !== 0) { - let scaledX16 = this._display.absX(scaledPosition.x) & 0xFFFF; - let scaledY16 = this._display.absY(scaledPosition.y) & 0xFFFF; - let coordinates = (scaledX16 << 16) | scaledY16; - this._sock.sQpush32(coordinates); - } - - // Keep track of last released touches - if (ev.detail.currentTouches[i].status === "POINTER_UP") { - pointerUpIds.push(ev.detail.currentTouches[i].event.identifier); - } - } - - this._sock.flush(); - - // Remove released touches from current touches in handler - for (let i = 0; i < pointerUpIds.length; i++) { - const index = ev.detail.currentTouches.findIndex(t => t.event.identifier === pointerUpIds[i]); - if (index !== -1) { - this._gestures._removeTouch(index); - } - } - - // Interrupt touch sending interval - if (ev.detail.currentTouches.length === 0 && this._sendTouchesIntervalId !== -1) { - Log.Debug("NO MORE TOUCHES\n"); - this._gestures._interruptTouches(); - this._sendEmptyTouch(ev.detail.giiDeviceOrigin); - return; - } - } - - _sendEmptyTouch(giiDeviceOrigin) { - let valuatorFlag = 0x00000000; - valuatorFlag |= TouchHandlerUltraVNC.LENGTH_16_flag; - valuatorFlag |= TouchHandlerUltraVNC.IDFORMAT_CLEAR; - - this._sock.sQpush8(253); // GII message type - this._sock.sQpush8(128); // GII event - this._sock.sQpush16(24); // Header length - this._sock.sQpush8(24); // Event size - this._sock.sQpush8(12); // eventType - this._sock.sQpush16(0); // padding - this._sock.sQpush32(giiDeviceOrigin); // deviceOrigin - this._sock.sQpush32(1); // first - this._sock.sQpush32(4); // Count - this._sock.sQpush32(valuatorFlag); // Flag - this._sock.sQpush32(0); // Empty Id - this._sock.sQpush32(0); // Empty coordinates + this._sock.sQpush32(1); // nb of touchevents + this._sock.sQpush32(4); // nb of values, not used + this._sock.sQpush32(valuatorFlag); + this._sock.sQpush32(0); // empty Id + this._sock.sQpush32(0); // empty coordinates this._sock.flush(); } @@ -2399,7 +2248,7 @@ export default class RFB extends EventTargetMixin { encs.push(encodings.pseudoEncodingDesktopName); encs.push(encodings.pseudoEncodingExtendedClipboard); - if (this._useUltraVNCGestures) { + if (this._gesturesMode === 'ultravnc') { encs.push(encodings.pseudoEncodingGii); } @@ -3529,39 +3378,39 @@ RFB.messages = { }, giiVersionMessage(sock) { - sock.sQpush8(253); // gii msg-type - sock.sQpush8(129); // gii version sub-msg-type - sock.sQpush16(2); // length - sock.sQpush16(1); // version + sock.sQpush8(TouchHandlerUltraVNC.giiMsgType); + sock.sQpush8(TouchHandlerUltraVNC.giiDeviceVersionMsgType); + sock.sQpush16(TouchHandlerUltraVNC.giiDeviceVersionMsgSize); + sock.sQpush16(TouchHandlerUltraVNC.giiDeviceVersion); sock.flush(); }, giiDeviceCreation(sock) { - sock.sQpush8(253); // gii msg-type - sock.sQpush8(130); // gii device creation sub-msg-type - sock.sQpush16(172); // length - sock.sQpushBytes(RFB.stringAsByteArrayWithPadding("NOVNC-MT", 31)); // device name - sock.sQpush8(0); // DNTerm - sock.sQpush32(0x0908); // vendorID - sock.sQpush32(0x000b); // productID - sock.sQpush32(0x00002000); // eventMask - sock.sQpush32(0); // numRegisters - sock.sQpush32(1); // numValuators - sock.sQpush32(5); // numButtons - sock.sQpush32(0); // index - sock.sQpushBytes(RFB.stringAsByteArrayWithPadding("NOVNC Multitouch Device", 74)); // longName - sock.sQpush8(0); // LNTerm - sock.sQpushBytes(RFB.stringAsByteArrayWithPadding("NMD", 4)); // shortName - sock.sQpush8(0); // SNTerm - sock.sQpush32(0); // rangeMin - sock.sQpush32(0); // rangeCenter - sock.sQpush32(0); // rangeMax - sock.sQpush32(0); // SIUnit - sock.sQpush32(0); // SIAdd - sock.sQpush32(0); // SIMul - sock.sQpush32(0); // SIDiv - sock.sQpush32(0); // SIShift + sock.sQpush8(TouchHandlerUltraVNC.giiMsgType); + sock.sQpush8(TouchHandlerUltraVNC.giiDeviceCreationMsgType); + sock.sQpush16(TouchHandlerUltraVNC.giiDeviceCreationMsgSize); + sock.sQpushBytes(RFB.stringAsByteArrayWithPadding(TouchHandlerUltraVNC.giiDeviceName, TouchHandlerUltraVNC.giiDeviceNameSize)); + sock.sQpush8(TouchHandlerUltraVNC.giiDNTerm); + sock.sQpush32(TouchHandlerUltraVNC.giiVendorID); + sock.sQpush32(TouchHandlerUltraVNC.giiProductID); + sock.sQpush32(TouchHandlerUltraVNC.giiEventMask); + sock.sQpush32(TouchHandlerUltraVNC.giiNumRegisters); + sock.sQpush32(TouchHandlerUltraVNC.giiNumValuators); + sock.sQpush32(TouchHandlerUltraVNC.giiNumButtons); + sock.sQpush32(TouchHandlerUltraVNC.giiIndex); + sock.sQpushBytes(RFB.stringAsByteArrayWithPadding(TouchHandlerUltraVNC.giiDeviceLongName, TouchHandlerUltraVNC.giiDeviceLongNameSize)); + sock.sQpush8(TouchHandlerUltraVNC.giiLNTerm); + sock.sQpushBytes(RFB.stringAsByteArrayWithPadding(TouchHandlerUltraVNC.giiDeviceShortName, TouchHandlerUltraVNC.giiDeviceShortNameSize)); + sock.sQpush8(TouchHandlerUltraVNC.giiSNTerm); + sock.sQpush32(TouchHandlerUltraVNC.giiRangeMin); + sock.sQpush32(TouchHandlerUltraVNC.giiRangeCenter); + sock.sQpush32(TouchHandlerUltraVNC.giiRangeMax); + sock.sQpush32(TouchHandlerUltraVNC.giiSIUnit); + sock.sQpush32(TouchHandlerUltraVNC.giiSIAdd); + sock.sQpush32(TouchHandlerUltraVNC.giiSIMul); + sock.sQpush32(TouchHandlerUltraVNC.giiSIDiv); + sock.sQpush32(TouchHandlerUltraVNC.giiSIShift); sock.flush(); } diff --git a/vnc.html b/vnc.html index 44c43718..e4c9a601 100644 --- a/vnc.html +++ b/vnc.html @@ -270,7 +270,11 @@