Эх сурвалжийг харах

Merge pull request #444 from samhed/extdesktop

Support ExtendedDesktopSize and SetDesktopSize
Solly 10 жил өмнө
parent
commit
f675e03ccc
8 өөрчлөгдсөн 410 нэмэгдсэн , 82 устгасан
  1. 66 46
      include/display.js
  2. 75 4
      include/rfb.js
  3. 62 12
      include/ui.js
  4. 12 10
      tests/test.display.js
  5. 162 4
      tests/test.rfb.js
  6. 4 5
      tests/viewport.html
  7. 1 0
      vnc.html
  8. 28 1
      vnc_auto.html

+ 66 - 46
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;

+ 75 - 4
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<number_of_screens; i += 1) {
+                // Save the id and flags of the first screen
+                if (i == 0) {
+                    this._screen_id = this._sock.rQshiftBytes(4);    // id
+                    this._sock.rQskipBytes(2);                       // x-position
+                    this._sock.rQskipBytes(2);                       // y-position
+                    this._sock.rQskipBytes(2);                       // width
+                    this._sock.rQskipBytes(2);                       // height
+                    this._screen_flags = this._sock.rQshiftBytes(4); // flags
+                } else {
+                    this._sock.rQskipBytes(16);
+                }
+            }
+
+            if (this._FBU.x == 0 && this._FBU.y != 0) { return true; }
+
+            this._fb_width = this._FBU.width;
+            this._fb_height = this._FBU.height;
+            this._display.resize(this._fb_width, this._fb_height);
+            this._onFBResize(this, this._fb_width, this._fb_height);
+
+            this._FBU.bytes = 0;
+            this._FBU.rects -= 1;
+
+            return true;
+        },
+
         DesktopSize: function () {
             Util.Debug(">> 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;

+ 62 - 12
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');

+ 12 - 10
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;
         });
     });
 

+ 162 - 4
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<nr_of_screens; i += 1) {
+                            data.push32(0);  // id
+                            data.push16(0);  // x-position
+                            data.push16(0);  // y-position
+                            data.push16(20); // width
+                            data.push16(50); // height
+                            data.push32(0);  // flags
+                        }
+                        return data;
+                    }
+
+                    it('should handle a resize requested by this client', function () {
+                        var reason_for_change = 1; // requested by this client
+                        var status_code       = 0; // No error
+
+                        send_fbu_msg([{ x: reason_for_change, y: status_code,
+                                        width: 20, height: 50, encoding: -308 }],
+                                     make_screen_data(1), client);
+
+                        expect(client._supportsSetDesktopSize).to.be.true;
+                        expect(client._fb_width).to.equal(20);
+                        expect(client._fb_height).to.equal(50);
+
+                        expect(client._display.resize).to.have.been.calledOnce;
+                        expect(client._display.resize).to.have.been.calledWith(20, 50);
+
+                        var spy = client.get_onFBResize();
+                        expect(spy).to.have.been.calledOnce;
+                        expect(spy).to.have.been.calledWith(sinon.match.any, 20, 50);
+                    });
+
+                    it('should handle a resize requested by another client', function () {
+                        var reason_for_change = 2; // requested by another client
+                        var status_code       = 0; // No error
+
+                        send_fbu_msg([{ x: reason_for_change, y: status_code,
+                                        width: 20, height: 50, encoding: -308 }],
+                                     make_screen_data(1), client);
+
+                        expect(client._supportsSetDesktopSize).to.be.true;
+                        expect(client._fb_width).to.equal(20);
+                        expect(client._fb_height).to.equal(50);
+
+                        expect(client._display.resize).to.have.been.calledOnce;
+                        expect(client._display.resize).to.have.been.calledWith(20, 50);
+
+                        var spy = client.get_onFBResize();
+                        expect(spy).to.have.been.calledOnce;
+                        expect(spy).to.have.been.calledWith(sinon.match.any, 20, 50);
+                    });
+
+                    it('should be able to recieve requests which contain data for multiple screens', function () {
+                        var reason_for_change = 2; // requested by another client
+                        var status_code       = 0; // No error
+
+                        send_fbu_msg([{ x: reason_for_change, y: status_code,
+                                        width: 60, height: 50, encoding: -308 }],
+                                     make_screen_data(3), client);
+
+                        expect(client._supportsSetDesktopSize).to.be.true;
+                        expect(client._fb_width).to.equal(60);
+                        expect(client._fb_height).to.equal(50);
+
+                        expect(client._display.resize).to.have.been.calledOnce;
+                        expect(client._display.resize).to.have.been.calledWith(60, 50);
+
+                        var spy = client.get_onFBResize();
+                        expect(spy).to.have.been.calledOnce;
+                        expect(spy).to.have.been.calledWith(sinon.match.any, 60, 50);
+                    });
+
+                    it('should not handle a failed request', function () {
+                        var reason_for_change = 0; // non-incremental
+                        var status_code       = 1; // Resize is administratively prohibited
+
+                        send_fbu_msg([{ x: reason_for_change, y: status_code,
+                                        width: 20, height: 50, encoding: -308 }],
+                                     make_screen_data(1), client);
+
+                        expect(client._fb_width).to.equal(4);
+                        expect(client._fb_height).to.equal(4);
+
+                        expect(client._display.resize).to.not.have.been.called;
+
+                        var spy = client.get_onFBResize();
+                        expect(spy).to.not.have.been.called;
+                    });
+                });
+
                 it.skip('should handle the Cursor pseudo-encoding', function () {
                     // TODO(directxman12): test
                 });
@@ -1593,7 +1751,7 @@ describe('Remote Frame Buffer Protocol Client', function() {
 
             it('should not send movement messages when viewport dragging', function () {
                 client._viewportDragging = true;
-                client._display.viewportChange = sinon.spy();
+                client._display.viewportChangePos = sinon.spy();
                 client._mouse._onMouseMove(13, 9);
                 expect(client._sock.send).to.not.have.been.called;
             });
@@ -1622,14 +1780,14 @@ describe('Remote Frame Buffer Protocol Client', function() {
                 client._viewportDrag = true;
                 client._viewportDragging = true;
                 client._viewportDragPos = { x: 13, y: 9 };
-                client._display.viewportChange = sinon.spy();
+                client._display.viewportChangePos = sinon.spy();
 
                 client._mouse._onMouseMove(10, 4);
 
                 expect(client._viewportDragging).to.be.true;
                 expect(client._viewportDragPos).to.deep.equal({ x: 10, y: 4 });
-                expect(client._display.viewportChange).to.have.been.calledOnce;
-                expect(client._display.viewportChange).to.have.been.calledWith(3, 5);
+                expect(client._display.viewportChangePos).to.have.been.calledOnce;
+                expect(client._display.viewportChangePos).to.have.been.calledWith(3, 5);
             });
         });
 

+ 4 - 5
tests/viewport.html

@@ -97,7 +97,7 @@
                 deltaY = lastPos.y - y; // drag frame buffer
                 lastPos = {'x': x, 'y': y};
 
-                display.viewportChange(deltaX, deltaY);
+                display.viewportChangePos(deltaX, deltaY);
                 return;
             }
 
@@ -166,14 +166,13 @@
             var p = $D('canvas').parentNode;
             message("doResize1: [" + (p.offsetWidth - padW) +
                     "," + (p.offsetHeight - padH) + "]");
-            display.viewportChange(0, 0,
-                p.offsetWidth - padW, p.offsetHeight - padH);
+            display.viewportChangeSize(p.offsetWidth - padW, p.offsetHeight - padH);
             /*
             var pos, new_w, new_h;pos
             pos = Util.getPosition($D('canvas').parentNode);
             new_w = window.innerWidth - pos.x;
             new_h = window.innerHeight - pos.y;
-            display.viewportChange(0, 0, new_w, new_h);
+            display.viewportChangeSize(new_w, new_h);
             */
         }
 
@@ -194,7 +193,7 @@
             Util.addEvent(window, 'resize', doResize);
             // Shrink viewport for first resize call so that the
             // scrollbars are disabled
-            display.viewportChange(0, 0, 10, 10);
+            display.viewportChangeSize(10, 10);
             setTimeout(doResize, 1);
             setInterval(dirtyRedraw, 50);
 

+ 1 - 0
vnc.html

@@ -157,6 +157,7 @@
                     <li><input id="noVNC_true_color" type="checkbox" checked> True Color</li>
                     <li><input id="noVNC_cursor" type="checkbox"> Local Cursor</li>
                     <li><input id="noVNC_clip" type="checkbox"> Clip to Window</li>
+                    <li><input id="noVNC_resize" type="checkbox"> Resize Remote to Window</li>
                     <li><input id="noVNC_shared" type="checkbox"> Shared Mode</li>
                     <li><input id="noVNC_view_only" type="checkbox"> View Only</li>
                     <li><input id="noVNC_path" type="input" value="websockify"> Path</li>

+ 28 - 1
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 = '<form onsubmit="return setPassword();"';
@@ -138,6 +154,16 @@
             }
         }
 
+        window.onresize = function () {
+            // 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(){
+                UIresize();
+            }, 500);
+        };
+
         function xvpInit(ver) {
             var xvpbuttons;
             xvpbuttons = $D('noVNC_xvp_buttons');
@@ -200,7 +226,8 @@
                            'view_only':    WebUtil.getQueryVar('view_only', false),
                            'onUpdateState':  updateState,
                            'onXvpInit':    xvpInit,
-                           'onPasswordRequired':  passwordRequired});
+                           'onPasswordRequired':  passwordRequired,
+                           'onFBUComplete': FBUComplete});
             rfb.connect(host, port, password, path);
         };
         </script>