Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
fe63a4f
GUACAMOLE-288: Add client-side settings for limiting usage of multi-m…
corentin-soriano Oct 29, 2024
5a4f6ad
GUACAMOLE-288: Add UI button to open new window on RDP connections.
corentin-soriano Nov 16, 2024
3a0120e
GUACAMOLE-288: Add service to handle monitor open/close.
corentin-soriano Nov 16, 2024
9317a17
GUACAMOLE-288: Forward instructions between guacd and secondary monit…
corentin-soriano Nov 16, 2024
c6b18d9
GUACAMOLE-288: Adjust the display according to the number of monitors.
corentin-soriano Nov 16, 2024
645ecce
GUACAMOLE-288: Propagate full screen mode on additional monitors.
corentin-soriano Nov 16, 2024
9b55beb
GUACAMOLE-288: Offset draw instructions on X depending on monitor pos…
corentin-soriano Nov 16, 2024
abe6013
GUACAMOLE-288: Dedicated controller for all secondary monitors that c…
corentin-soriano Nov 16, 2024
1f75409
GUACAMOLE-288: Add directive that handle client display element.
corentin-soriano Nov 30, 2024
548cf58
GUACAMOLE-288: Allow drag and drop from a browser window to another.
corentin-soriano Jan 16, 2025
6a0c47a
GUACAMOLE-288: Allow windows to have different dimensions.
corentin-soriano May 23, 2025
d488331
GUACAMOLE-288: Add Y offset on monitor position.
corentin-soriano Jun 3, 2025
934efe5
WIP - GUACAMOLE-288: Receive monitor layout via layer set parameter.
corentin-soriano Jun 7, 2025
573dfc8
GUACAMOLE-288: Force disconnect on error to avoid dead browser windows.
corentin-soriano Nov 3, 2025
18d3783
GUACAMOLE-288: Close multi-screen mode on change in the active client…
corentin-soriano Nov 27, 2025
7c2447a
GUACAMOLE-288: Namespace the monitor BroadcastChannel per connection.
ciroiriarte Jul 20, 2026
8a10ae4
GUACAMOLE-288: Use absolute left offset and tolerate transient layout…
ciroiriarte Jul 20, 2026
bc87ff8
GUACAMOLE-288: Crop the display when the monitor size changes.
ciroiriarte Jul 20, 2026
1fd930f
GUACAMOLE-288: Debounce resize requests and suppress remote resize ec…
ciroiriarte Jul 20, 2026
bb9d538
GUACAMOLE-288: Validate the multimon-layout parameter before use.
ciroiriarte Jul 20, 2026
cc15bee
GUACAMOLE-288: Order monitors by screen position rather than opening …
ciroiriarte Jul 20, 2026
19a33b1
GUACAMOLE-288: Address multi-monitor PR feedback
ciroiriarte Jul 21, 2026
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
290 changes: 212 additions & 78 deletions guacamole-common-js/src/main/webapp/modules/Client.js

Large diffs are not rendered by default.

43 changes: 37 additions & 6 deletions guacamole-common-js/src/main/webapp/modules/Display.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,16 @@ Guacamole.Display = function() {
* Reference to this Guacamole.Display.
* @private
*/
var guac_display = this;
const guac_display = this;

var displayWidth = 0;
var displayHeight = 0;
var displayScale = 1;
let displayWidth = 0;
let displayHeight = 0;
let monitorWidth = null;
let monitorHeight = null;
let displayScale = 1;

// Create display
var display = document.createElement("div");
const display = document.createElement("div");
display.style.position = "relative";
display.style.width = displayWidth + "px";
display.style.height = displayHeight + "px";
Expand Down Expand Up @@ -740,7 +742,7 @@ Guacamole.Display = function() {
* @param {!number} y
* The Y coordinate to move the cursor to.
*/
this.moveCursor = function(x, y) {
this.moveCursor = function moveCursor(x, y) {

// Move cursor layer
cursor.translate(x - guac_display.cursorHotspotX,
Expand All @@ -752,6 +754,27 @@ Guacamole.Display = function() {

};

/**
* Set the current monitor size.
*
* @param {!number} width
* The width of the monitor, in pixels.
* @param {!number} height
* The height of the monitor, in pixels.
*/
this.setMonitorSize = function setMonitorSize(width, height) {
monitorWidth = width;
monitorHeight = height;

// A monitor layout update must take effect immediately. The Guacamole
// server may have already resized the default layer to the size of the
// overall desktop, and may not send another resize after the per-window
// monitor rectangle is known. Re-run the resize path to authoritatively
// clamp the visible display to the current monitor bounds.
if (displayWidth !== monitorWidth || displayHeight !== monitorHeight)
guac_display.resize(default_layer, displayWidth, displayHeight);
};

/**
* Changes the size of the given Layer to the given width and height.
* Resizing is only attempted if the new size provided is actually different
Expand All @@ -769,6 +792,14 @@ Guacamole.Display = function() {
this.resize = function(layer, width, height) {
scheduleTask(function __display_resize() {

// Adjust width when using multiple monitors
if (monitorWidth)
width = monitorWidth;

// Adjust height when using multiple monitors
if (monitorHeight)
height = monitorHeight;

layer.resize(width, height);

// Resize display if default layer is resized
Expand Down
11 changes: 11 additions & 0 deletions guacamole-common-js/src/main/webapp/modules/Mouse.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,17 @@ Guacamole.Mouse = function Mouse(element) {
Guacamole.Event.DOMEvent.cancelEvent(e);
}, false);

// Capture mouse events outside the display element when a button is
// pressed to allow drag and drop between multiple windows.
element.addEventListener("pointerdown", function(e) {
element.setPointerCapture(e.pointerId);
}, false);

// Stop capture when mouse button is released
element.addEventListener("pointerup", function(e) {
element.releasePointerCapture(e.pointerId);
}, false);

element.addEventListener("mousemove", function(e) {

// If ignoring events, decrement counter
Expand Down
2 changes: 1 addition & 1 deletion guacamole-common-js/src/main/webapp/modules/Tunnel.js
Original file line number Diff line number Diff line change
Expand Up @@ -429,7 +429,7 @@ Guacamole.HTTPTunnel = function(tunnelURL, crossDomain, extraTunnelHeaders) {
}


this.sendMessage = function() {
this.sendMessage = function sendMessage() {

// Do not attempt to send messages if not connected
if (!tunnel.isConnected())
Expand Down
158 changes: 158 additions & 0 deletions guacamole-common-js/src/test/javascript/ClientSpec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

/**
* Tests the handling of the "multimon-layout" layer parameter. The value of
* this parameter is supplied by the server and is parsed directly, so a
* malformed payload must be absorbed rather than allowed to propagate out of
* the instruction handler and tear down the client.
*/
describe("Guacamole.Client multimon-layout handling", function ClientSpec() {

/**
* The client being tested.
*
* @type Guacamole.Client
*/
var client;

/**
* A minimal tunnel stub. Guacamole.Client assigns its instruction handler
* to tunnel.oninstruction, which is how instructions are injected below.
*
* @type Object
*/
var tunnel;

/**
* Sends a "set" instruction assigning the given raw value to the
* "multimon-layout" parameter of the default layer.
*
* @param {!string} value
* The raw parameter value, as it would arrive from the server.
*/
function sendLayout(value) {
tunnel.oninstruction('set', [ '0', 'multimon-layout', value ]);
}

beforeEach(function() {

tunnel = {
sendMessage : function() {},
connect : function() {},
disconnect : function() {},
isConnected : function() { return true; },
oninstruction : null,
onerror : null,
onstatechange : null
};

client = new Guacamole.Client(tunnel);

});

it("should pass a well-formed layout through to the handler", function() {

var received = null;
client.onmultimonlayout = function(layout) {
received = layout;
};

sendLayout('{"0":{"left":0,"top":0,"width":2560,"height":1422}}');

expect(received).not.toBeNull();
expect(received['0'].width).toBe(2560);
expect(received['0'].height).toBe(1422);

});

it("should not throw when the layout is not valid JSON", function() {

client.onmultimonlayout = function() {};

expect(function() {
sendLayout('{"0":{"left":0,');
}).not.toThrow();

});

it("should not invoke the handler when the layout is not valid JSON", function() {

var called = false;
client.onmultimonlayout = function() {
called = true;
};

sendLayout('not json at all');

expect(called).toBe(false);

});

it("should not invoke the handler for a JSON null", function() {

var called = false;
client.onmultimonlayout = function() {
called = true;
};

sendLayout('null');

expect(called).toBe(false);

});

it("should not invoke the handler for a non-object JSON value", function() {

var calls = 0;
client.onmultimonlayout = function() {
calls++;
};

sendLayout('42');
sendLayout('"a string"');
sendLayout('true');

expect(calls).toBe(0);

});

it("should absorb an exception thrown by the handler", function() {

client.onmultimonlayout = function() {
throw new Error('consumer failure');
};

expect(function() {
sendLayout('{"0":{"left":0,"top":0,"width":2560,"height":1422}}');
}).not.toThrow();

});

it("should not require a handler to be assigned", function() {

client.onmultimonlayout = null;

expect(function() {
sendLayout('{"0":{"left":0,"top":0,"width":2560,"height":1422}}');
}).not.toThrow();

});

});
66 changes: 66 additions & 0 deletions guacamole-common-js/src/test/javascript/DisplaySpec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

/* global Guacamole, expect */

describe("Guacamole.Display", function DisplaySpec() {

/**
* The display under test.
*
* @type {!Guacamole.Display}
*/
var display;

beforeEach(function() {
display = new Guacamole.Display();
});

it("should re-crop the default layer when monitor size changes", function() {

display.resize(display.getDefaultLayer(), 5120, 1598);
display.flush();

expect(display.getWidth()).toBe(5120);
expect(display.getHeight()).toBe(1598);

display.setMonitorSize(2560, 1422);
display.flush();

expect(display.getDefaultLayer().width).toBe(2560);
expect(display.getDefaultLayer().height).toBe(1422);
expect(display.getWidth()).toBe(2560);
expect(display.getHeight()).toBe(1422);

});

it("should constrain later default layer resizes to current monitor size", function() {

display.setMonitorSize(2560, 1422);
display.resize(display.getDefaultLayer(), 5120, 1598);
display.flush();

expect(display.getDefaultLayer().width).toBe(2560);
expect(display.getDefaultLayer().height).toBe(1422);
expect(display.getWidth()).toBe(2560);
expect(display.getHeight()).toBe(1422);

});

});
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,10 @@
"type" : "ENUM",
"options" : [ "", "display-update", "reconnect" ]
},
{
"name" : "secondary-monitors",
"type" : "NUMERIC"
},
{
"name" : "read-only",
"type" : "BOOLEAN",
Expand Down
32 changes: 31 additions & 1 deletion guacamole/src/main/frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading