| /* |
| * 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 |
| */ |
| |
| }] |
| }; |
| }]); |