blob: 769edd703347478a268e7e8bf0b92b0db8b54b23 [file] [log] [blame]
/*
* 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.
*/
/**
* A directive for the guacamole client.
*/
angular.module('client').directive('guacClient', [function guacClient() {
return {
// Element only
restrict: 'E',
replace: true,
scope: {
/**
* The client to display within this guacClient directive.
*
* @type ManagedClient
*/
client : '='
},
templateUrl: 'app/client/templates/guacClient.html',
controller: ['$scope', '$injector', '$element', function guacClientController($scope, $injector, $element) {
// Required types
var ManagedClient = $injector.get('ManagedClient');
// Required services
var $window = $injector.get('$window');
/**
* Whether the local, hardware mouse cursor is in use.
*
* @type Boolean
*/
var localCursor = false;
/**
* The current Guacamole client instance.
*
* @type Guacamole.Client
*/
var client = null;
/**
* The display of the current Guacamole client instance.
*
* @type Guacamole.Display
*/
var display = null;
/**
* The element associated with the display of the current
* Guacamole client instance.
*
* @type Element
*/
var displayElement = null;
/**
* The element which must contain the Guacamole display element.
*
* @type Element
*/
var displayContainer = $element.find('.display')[0];
/**
* The main containing element for the entire directive.
*
* @type Element
*/
var main = $element[0];
/**
* The element which functions as a detector for size changes.
*
* @type Element
*/
var resizeSensor = $element.find('.resize-sensor')[0];
/**
* Guacamole mouse event object, wrapped around the main client
* display.
*
* @type Guacamole.Mouse
*/
var mouse = new Guacamole.Mouse(displayContainer);
/**
* Guacamole absolute mouse emulation object, wrapped around the
* main client display.
*
* @type Guacamole.Mouse.Touchscreen
*/
var touchScreen = new Guacamole.Mouse.Touchscreen(displayContainer);
/**
* Guacamole relative mouse emulation object, wrapped around the
* main client display.
*
* @type Guacamole.Mouse.Touchpad
*/
var touchPad = new Guacamole.Mouse.Touchpad(displayContainer);
/**
* Updates the scale of the attached Guacamole.Client based on current window
* size and "auto-fit" setting.
*/
var updateDisplayScale = function updateDisplayScale() {
if (!display) return;
// Calculate scale to fit screen
$scope.client.clientProperties.minScale = Math.min(
main.offsetWidth / Math.max(display.getWidth(), 1),
main.offsetHeight / Math.max(display.getHeight(), 1)
);
// Calculate appropriate maximum zoom level
$scope.client.clientProperties.maxScale = Math.max($scope.client.clientProperties.minScale, 3);
// Clamp zoom level, maintain auto-fit
if (display.getScale() < $scope.client.clientProperties.minScale || $scope.client.clientProperties.autoFit)
$scope.client.clientProperties.scale = $scope.client.clientProperties.minScale;
else if (display.getScale() > $scope.client.clientProperties.maxScale)
$scope.client.clientProperties.scale = $scope.client.clientProperties.maxScale;
};
/**
* Scrolls the client view such that the mouse cursor is visible.
*
* @param {Guacamole.Mouse.State} mouseState The current mouse
* state.
*/
var scrollToMouse = function scrollToMouse(mouseState) {
// Determine mouse position within view
var mouse_view_x = mouseState.x + displayContainer.offsetLeft - main.scrollLeft;
var mouse_view_y = mouseState.y + displayContainer.offsetTop - main.scrollTop;
// Determine viewport dimensions
var view_width = main.offsetWidth;
var view_height = main.offsetHeight;
// Determine scroll amounts based on mouse position relative to document
var scroll_amount_x;
if (mouse_view_x > view_width)
scroll_amount_x = mouse_view_x - view_width;
else if (mouse_view_x < 0)
scroll_amount_x = mouse_view_x;
else
scroll_amount_x = 0;
var scroll_amount_y;
if (mouse_view_y > view_height)
scroll_amount_y = mouse_view_y - view_height;
else if (mouse_view_y < 0)
scroll_amount_y = mouse_view_y;
else
scroll_amount_y = 0;
// Scroll (if necessary) to keep mouse on screen.
main.scrollLeft += scroll_amount_x;
main.scrollTop += scroll_amount_y;
};
/**
* Sends the given mouse state to the current client.
*
* @param {Guacamole.Mouse.State} mouseState The mouse state to
* send.
*/
var sendScaledMouseState = function sendScaledMouseState(mouseState) {
// Scale event by current scale
var scaledState = new Guacamole.Mouse.State(
mouseState.x / display.getScale(),
mouseState.y / display.getScale(),
mouseState.left,
mouseState.middle,
mouseState.right,
mouseState.up,
mouseState.down);
// Send mouse event
client.sendMouseState(scaledState);
};
/**
* Handles a mouse event originating from the user's actual mouse.
* This differs from handleEmulatedMouseState() in that the
* software mouse cursor must be shown only if the user's browser
* does not support explicitly setting the hardware mouse cursor.
*
* @param {Guacamole.Mouse.State} mouseState
* The current state of the user's hardware mouse.
*/
var handleMouseState = function handleMouseState(mouseState) {
// Do not attempt to handle mouse state changes if the client
// or display are not yet available
if (!client || !display)
return;
// Send mouse state, show cursor if necessary
display.showCursor(!localCursor);
sendScaledMouseState(mouseState);
};
/**
* Handles a mouse event originating from one of Guacamole's mouse
* emulation objects. This differs from handleMouseState() in that
* the software mouse cursor must always be shown (as the emulated
* mouse device will not have its own cursor).
*
* @param {Guacamole.Mouse.State} mouseState
* The current state of the user's emulated (touch) mouse.
*/
var handleEmulatedMouseState = function handleEmulatedMouseState(mouseState) {
// Do not attempt to handle mouse state changes if the client
// or display are not yet available
if (!client || !display)
return;
// Ensure software cursor is shown
display.showCursor(true);
// Send mouse state, ensure cursor is visible
scrollToMouse(mouseState);
sendScaledMouseState(mouseState);
};
// Attach any given managed client
$scope.$watch('client', function attachManagedClient(managedClient) {
// Remove any existing display
displayContainer.innerHTML = "";
// Only proceed if a client is given
if (!managedClient)
return;
// Get Guacamole client instance
client = managedClient.client;
// Attach possibly new display
display = client.getDisplay();
display.scale($scope.client.clientProperties.scale);
// Add display element
displayElement = display.getElement();
displayContainer.appendChild(displayElement);
// Do nothing when the display element is clicked on
display.getElement().onclick = function(e) {
e.preventDefault();
return false;
};
});
// Update actual view scrollLeft when scroll properties change
$scope.$watch('client.clientProperties.scrollLeft', function scrollLeftChanged(scrollLeft) {
main.scrollLeft = scrollLeft;
$scope.client.clientProperties.scrollLeft = main.scrollLeft;
});
// Update actual view scrollTop when scroll properties change
$scope.$watch('client.clientProperties.scrollTop', function scrollTopChanged(scrollTop) {
main.scrollTop = scrollTop;
$scope.client.clientProperties.scrollTop = main.scrollTop;
});
// Update scale when display is resized
$scope.$watch('client.managedDisplay.size', function setDisplaySize() {
$scope.$evalAsync(updateDisplayScale);
});
// Keep local cursor up-to-date
$scope.$watch('client.managedDisplay.cursor', function setCursor(cursor) {
if (cursor)
localCursor = mouse.setCursor(cursor.canvas, cursor.x, cursor.y);
});
// Swap mouse emulation modes depending on absolute mode flag
$scope.$watch('client.clientProperties.emulateAbsoluteMouse',
function mouseEmulationModeChanged(emulateAbsoluteMouse) {
var newMode, oldMode;
// Switch to touchscreen if absolute
if (emulateAbsoluteMouse) {
newMode = touchScreen;
oldMode = touchPad;
}
// Switch to touchpad if not absolute (relative)
else {
newMode = touchPad;
oldMode = touchScreen;
}
// Set applicable mouse emulation object, unset the old one
if (newMode) {
// Clear old handlers and copy state to new emulation mode
if (oldMode) {
oldMode.onmousedown = oldMode.onmouseup = oldMode.onmousemove = null;
newMode.currentState.x = oldMode.currentState.x;
newMode.currentState.y = oldMode.currentState.y;
}
// Handle emulated events only from the new emulation mode
newMode.onmousedown =
newMode.onmouseup =
newMode.onmousemove = handleEmulatedMouseState;
}
});
// Adjust scale if modified externally
$scope.$watch('client.clientProperties.scale', function changeScale(scale) {
// Fix scale within limits
scale = Math.max(scale, $scope.client.clientProperties.minScale);
scale = Math.min(scale, $scope.client.clientProperties.maxScale);
// If at minimum zoom level, hide scroll bars
if (scale === $scope.client.clientProperties.minScale)
main.style.overflow = "hidden";
// If not at minimum zoom level, show scroll bars
else
main.style.overflow = "auto";
// Apply scale if client attached
if (display)
display.scale(scale);
if (scale !== $scope.client.clientProperties.scale)
$scope.client.clientProperties.scale = scale;
});
// If autofit is set, the scale should be set to the minimum scale, filling the screen
$scope.$watch('client.clientProperties.autoFit', function changeAutoFit(autoFit) {
if(autoFit)
$scope.client.clientProperties.scale = $scope.client.clientProperties.minScale;
});
// If the element is resized, attempt to resize client
$scope.mainElementResized = function mainElementResized() {
// Send new display size, if changed
if (client && display) {
var pixelDensity = $window.devicePixelRatio || 1;
var width = main.offsetWidth * pixelDensity;
var height = main.offsetHeight * pixelDensity;
if (display.getWidth() !== width || display.getHeight() !== height)
client.sendSize(width, height);
}
$scope.$evalAsync(updateDisplayScale);
};
// Ensure focus is regained via mousedown before forwarding event
mouse.onmousedown = function(mouseState) {
document.body.focus();
handleMouseState(mouseState);
};
// Forward mouseup / mousemove events untouched
mouse.onmouseup =
mouse.onmousemove = handleMouseState;
// Hide software cursor when mouse leaves display
mouse.onmouseout = function() {
if (!display) return;
display.showCursor(false);
};
// Update remote clipboard if local clipboard changes
$scope.$on('guacClipboard', function onClipboard(event, data) {
if (client) {
ManagedClient.setClipboard($scope.client, data);
$scope.client.clipboardData = data;
}
});
// Translate local keydown events to remote keydown events if keyboard is enabled
$scope.$on('guacKeydown', function keydownListener(event, keysym, keyboard) {
if ($scope.client.clientProperties.keyboardEnabled && !event.defaultPrevented) {
client.sendKeyEvent(1, keysym);
event.preventDefault();
}
});
// Translate local keyup events to remote keyup events if keyboard is enabled
$scope.$on('guacKeyup', function keyupListener(event, keysym, keyboard) {
if ($scope.client.clientProperties.keyboardEnabled && !event.defaultPrevented) {
client.sendKeyEvent(0, keysym);
event.preventDefault();
}
});
// Universally handle all synthetic keydown events
$scope.$on('guacSyntheticKeydown', function syntheticKeydownListener(event, keysym) {
client.sendKeyEvent(1, keysym);
});
// Universally handle all synthetic keyup events
$scope.$on('guacSyntheticKeyup', function syntheticKeyupListener(event, keysym) {
client.sendKeyEvent(0, keysym);
});
/**
* Ignores the given event.
*
* @param {Event} e The event to ignore.
*/
function ignoreEvent(e) {
e.preventDefault();
e.stopPropagation();
}
// Handle and ignore dragenter/dragover
displayContainer.addEventListener("dragenter", ignoreEvent, false);
displayContainer.addEventListener("dragover", ignoreEvent, false);
// File drop event handler
displayContainer.addEventListener("drop", function(e) {
e.preventDefault();
e.stopPropagation();
// Ignore file drops if no attached client
if (!$scope.client)
return;
// Upload each file
var files = e.dataTransfer.files;
for (var i=0; i<files.length; i++)
ManagedClient.uploadFile($scope.client, files[i]);
}, false);
/*
* END CLIENT DIRECTIVE
*/
}]
};
}]);