diff --git a/include/display.js b/include/display.js index db45d7b1d..d1278681f 100644 --- a/include/display.js +++ b/include/display.js @@ -111,54 +111,12 @@ var Display; Display.prototype = { // Public methods - viewportChange: function (deltaX, deltaY, width, height) { + viewportChangePos: function (deltaX, deltaY) { var vp = this._viewportLoc; - var cr = this._cleanRect; - var canvas = this._target; if (!this._viewport) { - Util.Debug("Setting viewport to full display region"); deltaX = -vp.w; // clamped later of out of bounds deltaY = -vp.h; - width = this._fb_width; - height = this._fb_height; - } - - if (typeof(deltaX) === "undefined") { deltaX = 0; } - if (typeof(deltaY) === "undefined") { deltaY = 0; } - if (typeof(width) === "undefined") { width = vp.w; } - if (typeof(height) === "undefined") { height = vp.h; } - - // Size change - if (width > this._fb_width) { width = this._fb_width; } - if (height > this._fb_height) { height = this._fb_height; } - - if (vp.w !== width || vp.h !== height) { - // Change width - if (width < vp.w && cr.x2 > vp.x + width - 1) { - cr.x2 = vp.x + width - 1; - } - vp.w = width; - - // Change height - if (height < vp.h && cr.y2 > vp.y + height - 1) { - cr.y2 = vp.y + height - 1; - } - vp.h = height; - - var saveImg = null; - if (vp.w > 0 && vp.h > 0 && canvas.width > 0 && canvas.height > 0) { - var img_width = canvas.width < vp.w ? canvas.width : vp.w; - var img_height = canvas.height < vp.h ? canvas.height : vp.h; - saveImg = this._drawCtx.getImageData(0, 0, img_width, img_height); - } - - canvas.width = vp.w; - canvas.height = vp.h; - - if (saveImg) { - this._drawCtx.putImageData(saveImg, 0, 0); - } } var vx2 = vp.x + vp.w - 1; @@ -191,6 +149,7 @@ var Display; vy2 += deltaY; // Update the clean rectangle + var cr = this._cleanRect; if (vp.x > cr.x1) { cr.x1 = vp.x; } @@ -228,6 +187,7 @@ var Display; // Copy the valid part of the viewport to the shifted location var saveStyle = this._drawCtx.fillStyle; + var canvas = this._target; this._drawCtx.fillStyle = "rgb(255,255,255)"; if (deltaX !== 0) { this._drawCtx.drawImage(canvas, 0, 0, vp.w, vp.h, -deltaX, 0, vp.w, vp.h); @@ -240,6 +200,58 @@ var Display; this._drawCtx.fillStyle = saveStyle; }, + viewportChangeSize: function(width, height) { + + if (!this._viewport || + typeof(width) === "undefined" || typeof(height) === "undefined") { + + Util.Debug("Setting viewport to full display region"); + width = this._fb_width; + height = this._fb_height; + } + + var vp = this._viewportLoc; + if (vp.w !== width || vp.h !== height) { + + var cr = this._cleanRect; + + if (width < vp.w && cr.x2 > vp.x + width - 1) { + cr.x2 = vp.x + width - 1; + } + + if (height < vp.h && cr.y2 > vp.y + height - 1) { + cr.y2 = vp.y + height - 1; + } + + if (this.fbuClip()) { + // clipping + vp.w = window.innerWidth; + var cb = document.getElementById('noVNC-control-bar'); + var controlbar_h = (cb !== null) ? cb.offsetHeight : 0; + vp.h = window.innerHeight - controlbar_h - 5; + } else { + // scrollbars + vp.w = width; + vp.h = height; + } + + var saveImg = null; + var canvas = this._target; + if (vp.w > 0 && vp.h > 0 && canvas.width > 0 && canvas.height > 0) { + var img_width = canvas.width < vp.w ? canvas.width : vp.w; + var img_height = canvas.height < vp.h ? canvas.height : vp.h; + saveImg = this._drawCtx.getImageData(0, 0, img_width, img_height); + } + + canvas.width = vp.w; + canvas.height = vp.h; + + if (saveImg) { + this._drawCtx.putImageData(saveImg, 0, 0); + } + } + }, + // Return a map of clean and dirty areas of the viewport and reset the // tracking of clean and dirty areas // @@ -305,7 +317,7 @@ var Display; this._rescale(this._scale); - this.viewportChange(); + this.viewportChangeSize(); }, clear: function () { @@ -475,6 +487,14 @@ var Display; this._target.style.cursor = "none"; }, + fbuClip: function () { + var cb = document.getElementById('noVNC-control-bar'); + var controlbar_h = (cb !== null) ? cb.offsetHeight : 0; + return (this._viewport && + (this._fb_width > window.innerWidth + || this._fb_height > window.innerHeight - controlbar_h - 5)); + }, + // Overridden getters/setters get_context: function () { return this._drawCtx; @@ -485,14 +505,14 @@ var Display; }, set_width: function (w) { - this.resize(w, this._fb_height); + this._fb_width = w; }, get_width: function () { return this._fb_width; }, set_height: function (h) { - this.resize(this._fb_width, h); + this._fb_height = h; }, get_height: function () { return this._fb_height; diff --git a/include/rfb.js b/include/rfb.js index 559eccbcd..03d4b8166 100644 --- a/include/rfb.js +++ b/include/rfb.js @@ -53,7 +53,8 @@ var RFB; //['compress_lo', -255 ], ['compress_hi', -247 ], ['last_rect', -224 ], - ['xvp', -309 ] + ['xvp', -309 ], + ['ext_desktop_size', -308 ] ]; this._encHandlers = {}; @@ -106,6 +107,10 @@ var RFB; pixels: 0 }; + this._supportsSetDesktopSize = false; + this._screen_id = 0; + this._screen_flags = 0; + // Mouse state this._mouse_buttonMask = 0; this._mouse_arr = []; @@ -305,6 +310,32 @@ var RFB; this._sock.send(RFB.messages.clientCutText(text)); }, + setDesktopSize: function (width, height) { + if (this._rfb_state !== "normal") { return; } + + if (this._supportsSetDesktopSize) { + + var arr = [251]; // msg-type + arr.push8(0); // padding + arr.push16(width); // width + arr.push16(height); // height + + arr.push8(1); // number-of-screens + arr.push8(0); // padding + + // screen array + arr.push32(this._screen_id); // id + arr.push16(0); // x-position + arr.push16(0); // y-position + arr.push16(width); // width + arr.push16(height); // height + arr.push32(this._screen_flags); // flags + + this._sock.send(arr); + } + }, + + // Private methods _connect: function () { @@ -585,7 +616,7 @@ var RFB; var deltaY = this._viewportDragPos.y - y; this._viewportDragPos = {'x': x, 'y': y}; - this._display.viewportChange(deltaX, deltaY); + this._display.viewportChangePos(deltaX, deltaY); // Skip sending mouse events return; @@ -944,8 +975,8 @@ var RFB; } this._display.set_true_color(this._true_color); - this._onFBResize(this, this._fb_width, this._fb_height); this._display.resize(this._fb_width, this._fb_height); + this._onFBResize(this, this._fb_width, this._fb_height); this._keyboard.grab(); this._mouse.grab(); @@ -1839,12 +1870,52 @@ var RFB; return true; }, + ext_desktop_size: function () { + this._FBU.bytes = 1; + if (this._sock.rQwait("ext_desktop_size", this._FBU.bytes)) { return false; } + + this._supportsSetDesktopSize = true; + var number_of_screens = this._sock.rQpeek8(); + + this._FBU.bytes = 4 + (number_of_screens * 16); + if (this._sock.rQwait("ext_desktop_size", this._FBU.bytes)) { return false; } + + this._sock.rQskipBytes(1); // number-of-screens + this._sock.rQskipBytes(3); // padding + + for (var i=0; i> set_desktopsize"); this._fb_width = this._FBU.width; this._fb_height = this._FBU.height; - this._onFBResize(this, this._fb_width, this._fb_height); this._display.resize(this._fb_width, this._fb_height); + this._onFBResize(this, this._fb_width, this._fb_height); this._timing.fbu_rt_start = (new Date()).getTime(); this._FBU.bytes = 0; diff --git a/include/ui.js b/include/ui.js index 4132d0506..8e13fee04 100644 --- a/include/ui.js +++ b/include/ui.js @@ -15,6 +15,8 @@ var UI; (function () { "use strict"; + var resizeTimeout; + // Load supporting scripts window.onscriptsload = function () { UI.load(); }; Util.load_scripts(["webutil.js", "base64.js", "websock.js", "des.js", @@ -43,6 +45,19 @@ var UI; WebUtil.initSettings(UI.start, callback); }, + onresize: function (callback) { + if (UI.getSetting('resize')) { + var innerW = window.innerWidth; + var innerH = window.innerHeight; + var controlbarH = $D('noVNC-control-bar').offsetHeight; + // For some unknown reason the container is higher than the canvas, + // 5px higher in Firefox and 4px higher in Chrome + var padding = 5; + if (innerW !== undefined && innerH !== undefined) + UI.rfb.setDesktopSize(innerW, innerH - controlbarH - padding); + } + }, + // Render default UI and initialize settings menu start: function(callback) { UI.isTouchDevice = 'ontouchstart' in document.documentElement; @@ -89,6 +104,7 @@ var UI; UI.initSetting('encrypt', (window.location.protocol === "https:")); UI.initSetting('true_color', true); UI.initSetting('cursor', !UI.isTouchDevice); + UI.initSetting('resize', false); UI.initSetting('shared', true); UI.initSetting('view_only', false); UI.initSetting('path', 'websockify'); @@ -98,6 +114,8 @@ var UI; 'onUpdateState': UI.updateState, 'onXvpInit': UI.updateXvpVisualState, 'onClipboard': UI.clipReceive, + 'onFBUComplete': UI.FBUComplete, + 'onFBResize': UI.updateViewDragButton, 'onDesktopName': UI.updateDocumentTitle}); var autoconnect = WebUtil.getQueryVar('autoconnect', false); @@ -118,7 +136,6 @@ var UI; // Remove the address bar setTimeout(function() { window.scrollTo(0, 1); }, 100); UI.forceSetting('clip', true); - $D('noVNC_clip').disabled = true; } else { UI.initSetting('clip', false); } @@ -136,7 +153,17 @@ var UI; $D('noVNC_host').focus(); UI.setViewClip(); - Util.addEvent(window, 'resize', UI.setViewClip); + + Util.addEvent(window, 'resize', function () { + UI.setViewClip(); + // When the window has been resized, wait until the size remains + // the same for 0.5 seconds before sending the request for changing + // the resolution of the session + clearTimeout(resizeTimeout); + resizeTimeout = setTimeout(function(){ + UI.onresize(); + }, 500); + } ); Util.addEvent(window, 'load', UI.keyboardinputReset); @@ -212,7 +239,7 @@ var UI; getSetting: function(name) { var ctrl = $D('noVNC_' + name); var val = WebUtil.readSetting(name); - if (val !== null && ctrl.type === 'checkbox') { + if (typeof val !== 'undefined' && val !== null && ctrl.type === 'checkbox') { if (val.toString().toLowerCase() in {'0':1, 'no':1, 'false':1}) { val = false; } else { @@ -427,6 +454,7 @@ var UI; $D('noVNC_cursor').disabled = true; } UI.updateSetting('clip'); + UI.updateSetting('resize'); UI.updateSetting('shared'); UI.updateSetting('view_only'); UI.updateSetting('path'); @@ -479,6 +507,7 @@ var UI; UI.saveSetting('cursor'); } UI.saveSetting('clip'); + UI.saveSetting('resize'); UI.saveSetting('shared'); UI.saveSetting('view_only'); UI.saveSetting('path'); @@ -595,6 +624,8 @@ var UI; UI.updateSetting('cursor', !UI.isTouchDevice); $D('noVNC_cursor').disabled = true; } + $D('noVNC_clip').disabled = connected || UI.isTouchDevice; + $D('noVNC_resize').disabled = connected; $D('noVNC_shared').disabled = connected; $D('noVNC_view_only').disabled = connected; $D('noVNC_path').disabled = connected; @@ -650,6 +681,16 @@ var UI; } }, + // This resize can not be done until we know from the first Frame Buffer Update + // if it is supported or not. + // The resize is needed to make sure the server desktop size is updated to the + // corresponding size of the current local window when reconnecting to an + // existing session. + FBUComplete: function(rfb, fbu) { + UI.onresize(); + UI.rfb.set_onFBUComplete(function() { }); + }, + // Display the desktop name in the document title updateDocumentTitle: function(rfb, name) { document.title = name + " - noVNC"; @@ -691,6 +732,9 @@ var UI; UI.closeSettingsMenu(); UI.rfb.disconnect(); + // Restore the callback used for initial resize + UI.rfb.set_onFBUComplete(UI.FBUComplete); + $D('noVNC_logo').style.display = "block"; UI.connSettingsOpen = false; UI.toggleConnectPanel(); @@ -742,7 +786,7 @@ var UI; UI.updateSetting('clip', false); display.set_viewport(false); $D('noVNC_canvas').style.position = 'static'; - display.viewportChange(); + display.viewportChangeSize(); } if (UI.getSetting('clip')) { // If clipping, update clipping settings @@ -751,27 +795,22 @@ var UI; var new_w = window.innerWidth - pos.x; var new_h = window.innerHeight - pos.y; display.set_viewport(true); - display.viewportChange(0, 0, new_w, new_h); + display.viewportChangeSize(new_w, new_h); } }, // Toggle/set/unset the viewport drag/move button setViewDrag: function(drag) { - var vmb = $D('noVNC_view_drag_button'); if (!UI.rfb) { return; } - if (UI.rfb_state === 'normal' && - UI.rfb.get_display().get_viewport()) { - vmb.style.display = "inline"; - } else { - vmb.style.display = "none"; - } + UI.updateViewDragButton(); if (typeof(drag) === "undefined" || typeof(drag) === "object") { // If not specified, then toggle drag = !UI.rfb.get_viewportDrag(); } + var vmb = $D('noVNC_view_drag_button'); if (drag) { vmb.className = "noVNC_status_button_selected"; UI.rfb.set_viewportDrag(true); @@ -781,6 +820,17 @@ var UI; } }, + updateViewDragButton: function() { + var vmb = $D('noVNC_view_drag_button'); + if (UI.rfb_state === 'normal' && + UI.rfb.get_display().get_viewport() && + UI.rfb.get_display().fbuClip()) { + vmb.style.display = "inline"; + } else { + vmb.style.display = "none"; + } + }, + // On touch devices, show the OS keyboard showKeyboard: function() { var kbi = $D('keyboardinput'); diff --git a/tests/test.display.js b/tests/test.display.js index 25adfbeac..949aca1e7 100644 --- a/tests/test.display.js +++ b/tests/test.display.js @@ -65,13 +65,15 @@ describe('Display/Canvas Helper', function () { beforeEach(function () { display = new Display({ target: document.createElement('canvas'), prefer_js: false, viewport: true }); display.resize(5, 5); - display.viewportChange(1, 1, 3, 3); + display.viewportChangeSize(3, 3); + display.viewportChangePos(1, 1); display.getCleanDirtyReset(); }); it('should take viewport location into consideration when drawing images', function () { - display.resize(4, 4); - display.viewportChange(0, 0, 2, 2); + display.set_width(4); + display.set_height(4); + display.viewportChangeSize(2, 2); display.drawImage(make_image_canvas(basic_data), 1, 1); var expected = new Uint8Array(16); @@ -82,7 +84,7 @@ describe('Display/Canvas Helper', function () { }); it('should redraw the left side when shifted left', function () { - display.viewportChange(-1, 0, 3, 3); + display.viewportChangePos(-1, 0); var cdr = display.getCleanDirtyReset(); expect(cdr.cleanBox).to.deep.equal({ x: 1, y: 1, w: 2, h: 3 }); expect(cdr.dirtyBoxes).to.have.length(1); @@ -90,7 +92,7 @@ describe('Display/Canvas Helper', function () { }); it('should redraw the right side when shifted right', function () { - display.viewportChange(1, 0, 3, 3); + display.viewportChangePos(1, 0); var cdr = display.getCleanDirtyReset(); expect(cdr.cleanBox).to.deep.equal({ x: 2, y: 1, w: 2, h: 3 }); expect(cdr.dirtyBoxes).to.have.length(1); @@ -98,7 +100,7 @@ describe('Display/Canvas Helper', function () { }); it('should redraw the top part when shifted up', function () { - display.viewportChange(0, -1, 3, 3); + display.viewportChangePos(0, -1); var cdr = display.getCleanDirtyReset(); expect(cdr.cleanBox).to.deep.equal({ x: 1, y: 1, w: 3, h: 2 }); expect(cdr.dirtyBoxes).to.have.length(1); @@ -106,7 +108,7 @@ describe('Display/Canvas Helper', function () { }); it('should redraw the bottom part when shifted down', function () { - display.viewportChange(0, 1, 3, 3); + display.viewportChangePos(0, 1); var cdr = display.getCleanDirtyReset(); expect(cdr.cleanBox).to.deep.equal({ x: 1, y: 2, w: 3, h: 2 }); expect(cdr.dirtyBoxes).to.have.length(1); @@ -114,7 +116,7 @@ describe('Display/Canvas Helper', function () { }); it('should reset the entire viewport to being clean after calculating the clean/dirty boxes', function () { - display.viewportChange(0, 1, 3, 3); + display.viewportChangePos(0, 1); var cdr1 = display.getCleanDirtyReset(); var cdr2 = display.getCleanDirtyReset(); expect(cdr1).to.not.deep.equal(cdr2); @@ -146,9 +148,9 @@ describe('Display/Canvas Helper', function () { }); it('should update the viewport dimensions', function () { - sinon.spy(display, 'viewportChange'); + sinon.spy(display, 'viewportChangeSize'); display.resize(2, 2); - expect(display.viewportChange).to.have.been.calledOnce; + expect(display.viewportChangeSize).to.have.been.calledOnce; }); }); diff --git a/tests/test.rfb.js b/tests/test.rfb.js index d777a8609..444e42c46 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -195,6 +195,48 @@ describe('Remote Frame Buffer Protocol Client', function() { }); }); + describe("#setDesktopSize", function () { + beforeEach(function() { + client._sock = new Websock(); + client._sock.open('ws://', 'binary'); + client._sock._websocket._open(); + sinon.spy(client._sock, 'send'); + client._rfb_state = "normal"; + client._view_only = false; + client._supportsSetDesktopSize = true; + }); + + it('should send the request with the given width and height', function () { + var expected = [251]; + expected.push8(0); // padding + expected.push16(1); // width + expected.push16(2); // height + expected.push8(1); // number-of-screens + expected.push8(0); // padding before screen array + expected.push32(0); // id + expected.push16(0); // x-position + expected.push16(0); // y-position + expected.push16(1); // width + expected.push16(2); // height + expected.push32(0); // flags + + client.setDesktopSize(1, 2); + expect(client._sock).to.have.sent(expected); + }); + + it('should not send the request if the client has not recieved a ExtendedDesktopSize rectangle', function () { + client._supportsSetDesktopSize = false; + client.setDesktopSize(1,2); + expect(client._sock.send).to.not.have.been.called; + }); + + it('should not send the request if we are not in a normal state', function () { + client._rfb_state = "broken"; + client.setDesktopSize(1,2); + expect(client._sock.send).to.not.have.been.called; + }); + }); + describe("XVP operations", function () { beforeEach(function () { client._sock = new Websock(); @@ -1443,6 +1485,122 @@ describe('Remote Frame Buffer Protocol Client', function() { expect(client._display.resize).to.have.been.calledWith(20, 50); }); + describe('the ExtendedDesktopSize pseudo-encoding handler', function () { + var client; + + beforeEach(function () { + client = make_rfb(); + client.connect('host', 8675); + client._sock._websocket._open(); + client._rfb_state = 'normal'; + client._fb_name = 'some device'; + client._supportsSetDesktopSize = false; + // a really small frame + client._fb_width = 4; + client._fb_height = 4; + client._display._fb_width = 4; + client._display._fb_height = 4; + client._display._viewportLoc.w = 4; + client._display._viewportLoc.h = 4; + client._fb_Bpp = 4; + sinon.spy(client._display, 'resize'); + client.set_onFBResize(sinon.spy()); + }); + + function make_screen_data (nr_of_screens) { + var data = []; + data.push8(nr_of_screens); // number-of-screens + data.push8(0); // padding + data.push16(0); // padding + for (var i=0; i True Color
  • Local Cursor
  • Clip to Window
  • +
  • Resize Remote to Window
  • Shared Mode
  • View Only
  • Path
  • diff --git a/vnc_auto.html b/vnc_auto.html index b05024e30..9fd2272a3 100644 --- a/vnc_auto.html +++ b/vnc_auto.html @@ -80,7 +80,23 @@ "jsunzip.js", "rfb.js", "keysym.js"]); var rfb; + var resizeTimeout; + + function UIresize() { + if (WebUtil.getQueryVar('resize', false)) { + var innerW = window.innerWidth; + var innerH = window.innerHeight; + var controlbarH = $D('noVNC_status_bar').offsetHeight; + var padding = 5; + if (innerW !== undefined && innerH !== undefined) + rfb.setDesktopSize(innerW, innerH - controlbarH - padding); + } + } + function FBUComplete(rfb, fbu) { + UIresize(); + rfb.set_onFBUComplete(function() { }); + } function passwordRequired(rfb) { var msg; msg = '