Skip to content

Commit

Permalink
WIP - GUACAMOLE-288: Add directive that handle client display element.
Browse files Browse the repository at this point in the history
  • Loading branch information
corentin-soriano committed Nov 30, 2024
1 parent c9e8c3e commit 51bc430
Show file tree
Hide file tree
Showing 4 changed files with 428 additions and 310 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,30 +23,18 @@
angular.module('client').controller('secondaryMonitorController', ['$scope', '$injector', '$routeParams',
function clientController($scope, $injector, $routeParams) {

// Required types
const ClipboardData = $injector.get('ClipboardData');

// Required services
const $window = $injector.get('$window');
const clipboardService = $injector.get('clipboardService');
const guacFullscreen = $injector.get('guacFullscreen');
const $window = $injector.get('$window');
const guacFullscreen = $injector.get('guacFullscreen');
const guacManageMonitor = $injector.get('guacManageMonitor');

// ID of this monitor
/**
* ID of this monitor.
*
* @type {!String}
*/
const monitorId = $routeParams.id;

// Broadcast channel
const broadcast = new BroadcastChannel('guac_monitors');

// Latest mouse state
const mouseState = {};

// Display size in pixels and position of the monitor
let displayWidth = 0;
let displayHeight = 0;
let monitorPosition = 0;
let monitorsCount = 0;
let currentScaling = 1;

/**
* In order to open the guacamole menu, we need to hit ctrl-alt-shift. There are
* several possible keysysms for each key.
Expand All @@ -57,255 +45,10 @@ angular.module('client').controller('secondaryMonitorController', ['$scope', '$i
CTRL_KEYS = {0xFFE3 : true, 0xFFE4 : true},
MENU_KEYS = angular.extend({}, SHIFT_KEYS, ALT_KEYS, CTRL_KEYS);

// Instantiate client, using an HTTP tunnel for communications.
const client = new Guacamole.Client(
new Guacamole.HTTPTunnel("tunnel")
);

const display = client.getDisplay();
let displayContainer;

setTimeout(function() {
displayContainer = document.querySelector('.display')

// Remove any existing display
displayContainer.innerHTML = "";

// Add display element
displayContainer.appendChild(display.getElement());

// Ready for resize
pushBroadcastMessage('resize', true);
}, 1000);

/**
* Adjust the display scaling according to the window size.
*/
$scope.scaleDisplay = function scaleDisplay() {

// Calculate required scaling factor
const scaleX = $window.innerWidth / displayWidth;
const scaleY = $window.innerHeight / displayHeight;

// Use the lowest scaling to avoid acreen overflow
if (scaleX <= scaleY)
currentScaling = scaleX;
else
currentScaling = scaleY;

display.scale(currentScaling);
};

// Send monitor-close event to broadcast channel on window unload
$window.addEventListener('unload', function unloadWindow() {
pushBroadcastMessage('monitorClose', monitorId);
});

// Mouse and keyboard
const mouse = new Guacamole.Mouse(client.getDisplay().getElement());
const keyboard = new Guacamole.Keyboard(document);

// Move mouse on screen and send mouse events to main window
mouse.onEach(['mousedown', 'mouseup', 'mousemove'], function sendMouseEvent(e) {

// Ensure software cursor is shown
display.showCursor(true);

// Update client-side cursor
display.moveCursor(
Math.floor(e.state.x / currentScaling),
Math.floor(e.state.y / currentScaling)
);

// Limit mouse move events to reduce latency
if (mouseState.lastPush && Date.now() - mouseState.lastPush < 100
&& mouseState.down === e.state.down
&& mouseState.up === e.state.up
&& mouseState.left === e.state.left
&& mouseState.middle === e.state.middle
&& mouseState.right === e.state.right)
return;

// Click on actual display instead of the first
const displayOffset = displayWidth * monitorPosition;

// Convert mouse state to serializable object
mouseState.down = e.state.down;
mouseState.up = e.state.up;
mouseState.left = e.state.left;
mouseState.middle = e.state.middle;
mouseState.right = e.state.right;
mouseState.x = e.state.x / currentScaling + displayOffset;
mouseState.y = e.state.y / currentScaling;
mouseState.lastPush = Date.now();

// Send mouse state to main window
pushBroadcastMessage('mouseState', mouseState);
});

// Hide software cursor when mouse leaves display
mouse.on('mouseout', function() {
if (!display) return;
display.showCursor(false);
});

// Handle any received clipboard data
client.onclipboard = function clientClipboardReceived(stream, mimetype) {

let reader;

// If the received data is text, read it as a simple string
if (/^text\//.exec(mimetype)) {

reader = new Guacamole.StringReader(stream);

// Assemble received data into a single string
let data = '';
reader.ontext = function textReceived(text) {
data += text;
};

// Set clipboard contents once stream is finished
reader.onend = function textComplete() {
clipboardService.setClipboard(new ClipboardData({
source : 'secondaryMonitor',
type : mimetype,
data : data
}))['catch'](angular.noop);
};

}

// Otherwise read the clipboard data as a Blob
else {
reader = new Guacamole.BlobReader(stream, mimetype);
reader.onend = function blobComplete() {
clipboardService.setClipboard(new ClipboardData({
source : 'secondaryMonitor',
type : mimetype,
data : reader.getBlob()
}))['catch'](angular.noop);
};
}

};

// Send keydown events to main window
keyboard.onkeydown = function (keysym) {
pushBroadcastMessage('keydown', keysym);
};

// Send keyup events to main window
keyboard.onkeyup = function (keysym) {
pushBroadcastMessage('keyup', keysym);
};

/**
* Push broadcast message containing instructions that allows additional
* monitor windows to draw display, resize window and more.
*
* @param {!string} type
* The type of message (ex: handler, fullscreen, resize)
*
* @param {*} content
* The content of the message, can contain any type of serializable
* content.
*/
function pushBroadcastMessage(type, content) {
const message = {
[type]: content
};

broadcast.postMessage(message);
};

/**
* Handle messages sent by main window in guac_monitors channel. These
* messages contain instructions to draw the screen, resize window, or
* request full screen mode.
*
* @param {Event} e
* Received message event from guac_monitors channel.
*/
broadcast.onmessage = function broadcastMessage(message) {

// Run the client handler to draw display
if (message.data.handler)
client.runHandler(message.data.handler.opcode,
message.data.handler.parameters);

if (message.data.monitorsInfos) {

const monitorsInfos = message.data.monitorsInfos;

// Store new monitor count and position
monitorsCount = monitorsInfos.count;
monitorPosition = monitorsInfos.map[monitorId];

// Set the monitor count in display
display.updateMonitors(monitorsCount);

}

// Resize display and window with parameters sent by guacd in the size handler
if (message.data.handler && message.data.handler.opcode === 'size') {
guacManageMonitor.init();
guacManageMonitor.monitorAttributes.monitorId = monitorId;

const parameters = message.data.handler.parameters;
const default_layer = 0;
const layer = parseInt(parameters[0]);

// Ignore other layers (ex: mouse) that can have other size
if (layer !== default_layer)
return;

// Set the new display size
displayWidth = parseInt(parameters[1]) / monitorsCount;
displayHeight = parseInt(parameters[2]);

// Translate all draw actions on X to draw the current display
// instead of the first
client.offsetX = displayWidth * monitorPosition;

// Get unusable window height and width (ex: titlebar)
const windowUnusableHeight = $window.outerHeight - $window.innerHeight;
const windowUnusableWidth = $window.outerWidth - $window.innerWidth;

// Remove scrollbars
document.querySelector('.client-main').style.overflow = 'hidden';

// Resize window to the display size
$window.resizeTo(
displayWidth + windowUnusableWidth,
displayHeight + windowUnusableHeight
);

// Adjust scaling to new size
$scope.scaleDisplay();

}

// Full screen mode instructions
if (message.data.fullscreen) {

// setFullscreenMode require explicit user action
if (message.data.fullscreen !== false)
openConsentButton();

// Close fullscreen mode instantly
else
guacFullscreen.setFullscreenMode(message.data.fullscreen);

}

};

/**
* Add button to request user consent before enabling fullscreen mode to
* comply with the setFullscreenMode requirements that require explicit
* user action. The button is removed after a few seconds if the user does
* not click on it.
*/
function openConsentButton() {
guacManageMonitor.openConsentButton = function openConsentButton() {

// Show button
$scope.showFullscreenConsent = true;
Expand Down Expand Up @@ -367,11 +110,16 @@ angular.module('client').controller('secondaryMonitorController', ['$scope', '$i

// Toggle the menu
$scope.$apply(function() {
pushBroadcastMessage('guacMenu', true);
guacManageMonitor.pushBroadcastMessage('guacMenu', true);
});

}

});

// Send monitor-close event to broadcast channel on window unload
$window.addEventListener('unload', function unloadWindow() {
guacManageMonitor.pushBroadcastMessage('monitorClose', monitorId);
});

}]);
Loading

0 comments on commit 51bc430

Please sign in to comment.