mirror of
https://github.com/gyurix1968/guacamole-client.git
synced 2025-09-07 13:41:21 +00:00
526 lines
17 KiB
JavaScript
526 lines
17 KiB
JavaScript
/*
|
|
* Copyright (C) 2014 Glyptodon LLC
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
* of this software and associated documentation files (the "Software"), to deal
|
|
* in the Software without restriction, including without limitation the rights
|
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
* copies of the Software, and to permit persons to whom the Software is
|
|
* furnished to do so, subject to the following conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be included in
|
|
* all copies or substantial portions of the Software.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
* THE SOFTWARE.
|
|
*/
|
|
|
|
/**
|
|
* The controller for the page used to connect to a connection or balancing group.
|
|
*/
|
|
angular.module('client').controller('clientController', ['$scope', '$routeParams', '$injector',
|
|
function clientController($scope, $routeParams, $injector) {
|
|
|
|
// Required types
|
|
var ManagedClientState = $injector.get('ManagedClientState');
|
|
var ScrollState = $injector.get('ScrollState');
|
|
|
|
// Required services
|
|
var $location = $injector.get('$location');
|
|
var guacClientManager = $injector.get('guacClientManager');
|
|
|
|
/**
|
|
* The minimum number of pixels a drag gesture must move to result in the
|
|
* menu being shown or hidden.
|
|
*
|
|
* @type Number
|
|
*/
|
|
var MENU_DRAG_DELTA = 64;
|
|
|
|
/**
|
|
* The maximum X location of the start of a drag gesture for that gesture
|
|
* to potentially show the menu.
|
|
*
|
|
* @type Number
|
|
*/
|
|
var MENU_DRAG_MARGIN = 64;
|
|
|
|
/**
|
|
* When showing or hiding the menu via a drag gesture, the maximum number
|
|
* of pixels the touch can move vertically and still affect the menu.
|
|
*
|
|
* @type Number
|
|
*/
|
|
var MENU_DRAG_VERTICAL_TOLERANCE = 10;
|
|
|
|
/*
|
|
* In order to open the guacamole menu, we need to hit ctrl-alt-shift. There are
|
|
* several possible keysysms for each key.
|
|
*/
|
|
var SHIFT_KEYS = {0xFFE1 : true, 0xFFE2: true},
|
|
ALT_KEYS = {0xFFE9 : true, 0xFFEA : true, 0xFE03: true},
|
|
CTRL_KEYS = {0xFFE3 : true, 0xFFE4: true},
|
|
MENU_KEYS = angular.extend({}, SHIFT_KEYS, ALT_KEYS, CTRL_KEYS);
|
|
|
|
/**
|
|
* All client error codes handled and passed off for translation. Any error
|
|
* code not present in this list will be represented by the "DEFAULT"
|
|
* translation.
|
|
*/
|
|
var CLIENT_ERRORS = {
|
|
0x0201: true,
|
|
0x0202: true,
|
|
0x0203: true,
|
|
0x0205: true,
|
|
0x0301: true,
|
|
0x0303: true,
|
|
0x0308: true,
|
|
0x031D: true
|
|
};
|
|
|
|
/**
|
|
* All error codes for which automatic reconnection is appropriate when a
|
|
* client error occurs.
|
|
*/
|
|
var CLIENT_AUTO_RECONNECT = {
|
|
0x0200: true,
|
|
0x0202: true,
|
|
0x0203: true,
|
|
0x0301: true,
|
|
0x0308: true
|
|
};
|
|
|
|
/**
|
|
* All tunnel error codes handled and passed off for translation. Any error
|
|
* code not present in this list will be represented by the "DEFAULT"
|
|
* translation.
|
|
*/
|
|
var TUNNEL_ERRORS = {
|
|
0x0201: true,
|
|
0x0202: true,
|
|
0x0203: true,
|
|
0x0204: true,
|
|
0x0205: true,
|
|
0x0301: true,
|
|
0x0303: true,
|
|
0x0308: true,
|
|
0x031D: true
|
|
};
|
|
|
|
/**
|
|
* All error codes for which automatic reconnection is appropriate when a
|
|
* tunnel error occurs.
|
|
*/
|
|
var TUNNEL_AUTO_RECONNECT = {
|
|
0x0200: true,
|
|
0x0202: true,
|
|
0x0203: true,
|
|
0x0308: true
|
|
};
|
|
|
|
/**
|
|
* Action which returns the user to the home screen.
|
|
*/
|
|
var NAVIGATE_BACK_ACTION = {
|
|
name : "CLIENT.ACTION_NAVIGATE_BACK",
|
|
className : "back button",
|
|
callback : function navigateBackCallback() {
|
|
$location.path('/');
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Action which replaces the current client with a newly-connected client.
|
|
*/
|
|
var RECONNECT_ACTION = {
|
|
name : "CLIENT.ACTION_RECONNECT",
|
|
callback : function reconnectCallback() {
|
|
$scope.client = guacClientManager.replaceManagedClient(uniqueId, $routeParams.params);
|
|
$scope.showStatus(false);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* The reconnect countdown to display if an error or status warrants an
|
|
* automatic, timed reconnect.
|
|
*/
|
|
var RECONNECT_COUNTDOWN = {
|
|
text: "CLIENT.TEXT_RECONNECT_COUNTDOWN",
|
|
callback: RECONNECT_ACTION.callback,
|
|
remaining: 15
|
|
};
|
|
|
|
// Hide menu by default
|
|
$scope.menuShown = false;
|
|
|
|
// Use physical keyboard by default
|
|
$scope.inputMethod = 'none';
|
|
|
|
// Convenience method for closing the menu
|
|
$scope.closeMenu = function closeMenu() {
|
|
$scope.menuShown = false;
|
|
};
|
|
|
|
/**
|
|
* The current scroll state of the menu.
|
|
*
|
|
* @type ScrollState
|
|
*/
|
|
$scope.menuScrollState = new ScrollState();
|
|
|
|
// Update the model when clipboard data received from client
|
|
$scope.$on('guacClientClipboard', function clientClipboardListener(event, client, mimetype, clipboardData) {
|
|
$scope.clipboardData = clipboardData;
|
|
});
|
|
|
|
/*
|
|
* Parse the type, name, and id out of the url paramteres,
|
|
* as well as any extra parameters if set.
|
|
*/
|
|
var uniqueId = $routeParams.type + '/' + $routeParams.id;
|
|
$scope.client = guacClientManager.getManagedClient(uniqueId, $routeParams.params);
|
|
|
|
var keysCurrentlyPressed = {};
|
|
|
|
/*
|
|
* Check to see if all currently pressed keys are in the set of menu keys.
|
|
*/
|
|
function checkMenuModeActive() {
|
|
for(var keysym in keysCurrentlyPressed) {
|
|
if(!MENU_KEYS[keysym]) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// Hide menu when the user swipes from the right
|
|
$scope.menuDrag = function menuDrag(inProgress, startX, startY, currentX, currentY, deltaX, deltaY) {
|
|
|
|
// Hide menu if swipe gesture is detected
|
|
if (Math.abs(currentY - startY) < MENU_DRAG_VERTICAL_TOLERANCE
|
|
&& startX - currentX >= MENU_DRAG_DELTA)
|
|
$scope.menuShown = false;
|
|
|
|
// Scroll menu by default
|
|
else {
|
|
$scope.menuScrollState.left -= deltaX;
|
|
$scope.menuScrollState.top -= deltaY;
|
|
}
|
|
|
|
return false;
|
|
|
|
};
|
|
|
|
// Update menu or client based on dragging gestures
|
|
$scope.clientDrag = function clientDrag(inProgress, startX, startY, currentX, currentY, deltaX, deltaY) {
|
|
|
|
// Show menu if the user swipes from the left
|
|
if (startX <= MENU_DRAG_MARGIN) {
|
|
|
|
if (Math.abs(currentY - startY) < MENU_DRAG_VERTICAL_TOLERANCE
|
|
&& currentX - startX >= MENU_DRAG_DELTA)
|
|
$scope.menuShown = true;
|
|
|
|
}
|
|
|
|
// Scroll display if absolute mouse is in use
|
|
else if ($scope.client.clientProperties.emulateAbsoluteMouse) {
|
|
$scope.client.clientProperties.scrollLeft -= deltaX;
|
|
$scope.client.clientProperties.scrollTop -= deltaY;
|
|
}
|
|
|
|
return false;
|
|
|
|
};
|
|
|
|
/**
|
|
* If a pinch gesture is in progress, the scale of the client display when
|
|
* the pinch gesture began.
|
|
*
|
|
* @type Number
|
|
*/
|
|
var initialScale = null;
|
|
|
|
/**
|
|
* If a pinch gesture is in progress, the X coordinate of the point on the
|
|
* client display that was centered within the pinch at the time the
|
|
* gesture began.
|
|
*
|
|
* @type Number
|
|
*/
|
|
var initialCenterX = 0;
|
|
|
|
/**
|
|
* If a pinch gesture is in progress, the Y coordinate of the point on the
|
|
* client display that was centered within the pinch at the time the
|
|
* gesture began.
|
|
*
|
|
* @type Number
|
|
*/
|
|
var initialCenterY = 0;
|
|
|
|
// Zoom and pan client via pinch gestures
|
|
$scope.clientPinch = function clientPinch(inProgress, startLength, currentLength, centerX, centerY) {
|
|
|
|
// Do not handle pinch gestures while relative mouse is in use
|
|
if (!$scope.client.clientProperties.emulateAbsoluteMouse)
|
|
return false;
|
|
|
|
// Stop gesture if not in progress
|
|
if (!inProgress) {
|
|
initialScale = null;
|
|
return false;
|
|
}
|
|
|
|
// Set initial scale if gesture has just started
|
|
if (!initialScale) {
|
|
initialScale = $scope.client.clientProperties.scale;
|
|
initialCenterX = (centerX + $scope.client.clientProperties.scrollLeft) / initialScale;
|
|
initialCenterY = (centerY + $scope.client.clientProperties.scrollTop) / initialScale;
|
|
}
|
|
|
|
// Determine new scale absolutely
|
|
var currentScale = initialScale * currentLength / startLength;
|
|
|
|
// Fix scale within limits - scroll will be miscalculated otherwise
|
|
currentScale = Math.max(currentScale, $scope.client.clientProperties.minScale);
|
|
currentScale = Math.min(currentScale, $scope.client.clientProperties.maxScale);
|
|
|
|
// Update scale based on pinch distance
|
|
$scope.autoFit = false;
|
|
$scope.client.clientProperties.autoFit = false;
|
|
$scope.client.clientProperties.scale = currentScale;
|
|
|
|
// Scroll display to keep original pinch location centered within current pinch
|
|
$scope.client.clientProperties.scrollLeft = initialCenterX * currentScale - centerX;
|
|
$scope.client.clientProperties.scrollTop = initialCenterY * currentScale - centerY;
|
|
|
|
return false;
|
|
|
|
};
|
|
|
|
// Show/hide UI elements depending on input method
|
|
$scope.$watch('inputMethod', function setInputMethod(inputMethod) {
|
|
|
|
// Show input methods only if selected
|
|
$scope.showOSK = (inputMethod === 'osk');
|
|
$scope.showTextInput = (inputMethod === 'text');
|
|
|
|
});
|
|
|
|
$scope.$watch('menuShown', function menuVisibilityChanged(menuShown, menuShownPreviousState) {
|
|
|
|
// Send clipboard data if menu is hidden
|
|
if (!menuShown && menuShownPreviousState)
|
|
$scope.$broadcast('guacClipboard', 'text/plain', $scope.client.clipboardData);
|
|
|
|
// Disable client keyboard if the menu is shown
|
|
$scope.client.clientProperties.keyboardEnabled = !menuShown;
|
|
|
|
});
|
|
|
|
$scope.$on('guacKeydown', function keydownListener(event, keysym, keyboard) {
|
|
keysCurrentlyPressed[keysym] = true;
|
|
|
|
/*
|
|
* If only menu keys are pressed, and we have one keysym from each group,
|
|
* and one of the keys is being released, show the menu.
|
|
*/
|
|
if(checkMenuModeActive()) {
|
|
var currentKeysPressedKeys = Object.keys(keysCurrentlyPressed);
|
|
|
|
// Check that there is a key pressed for each of the required key classes
|
|
if(!_.isEmpty(_.pick(SHIFT_KEYS, currentKeysPressedKeys)) &&
|
|
!_.isEmpty(_.pick(ALT_KEYS, currentKeysPressedKeys)) &&
|
|
!_.isEmpty(_.pick(CTRL_KEYS, currentKeysPressedKeys))
|
|
) {
|
|
|
|
// Don't send this key event through to the client
|
|
event.preventDefault();
|
|
|
|
// Reset the keys pressed
|
|
keysCurrentlyPressed = {};
|
|
keyboard.reset();
|
|
|
|
// Toggle the menu
|
|
$scope.$apply(function() {
|
|
$scope.menuShown = !$scope.menuShown;
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
// Listen for broadcasted keyup events and fire the appropriate listeners
|
|
$scope.$on('guacKeyup', function keyupListener(event, keysym, keyboard) {
|
|
delete keysCurrentlyPressed[keysym];
|
|
});
|
|
|
|
// Update page title when client name is received
|
|
$scope.$watch('client.name', function clientNameChanged(name) {
|
|
$scope.page.title = name;
|
|
});
|
|
|
|
// Show file transfer section of menu if new file transfers have started
|
|
$scope.$watch('client.uploads.length + client.downloads.length', function transfersChanged(count, oldCount) {
|
|
|
|
// Show menu and scroll file transfer into view
|
|
if (count > oldCount) {
|
|
$scope.menuShown = true;
|
|
$scope.fileTransferMarker.scrollIntoView();
|
|
}
|
|
|
|
});
|
|
|
|
// Show status dialog when connection status changes
|
|
$scope.$watch('client.clientState.connectionState', function clientStateChanged(connectionState) {
|
|
|
|
// Hide any existing status
|
|
$scope.showStatus(false);
|
|
|
|
// Do not display status if status not known
|
|
if (!connectionState)
|
|
return;
|
|
|
|
// Get any associated status code
|
|
var status = $scope.client.clientState.statusCode;
|
|
|
|
// Connecting
|
|
if (connectionState === ManagedClientState.ConnectionState.CONNECTING
|
|
|| connectionState === ManagedClientState.ConnectionState.WAITING) {
|
|
$scope.showStatus({
|
|
title: "CLIENT.DIALOG_HEADER_CONNECTING",
|
|
text: "CLIENT.TEXT_CLIENT_STATUS_" + connectionState.toUpperCase()
|
|
});
|
|
}
|
|
|
|
// Client error
|
|
else if (connectionState === ManagedClientState.ConnectionState.CLIENT_ERROR) {
|
|
|
|
// Determine translation name of error
|
|
var errorName = (status in CLIENT_ERRORS) ? status.toString(16).toUpperCase() : "DEFAULT";
|
|
|
|
// Determine whether the reconnect countdown applies
|
|
var countdown = (status in CLIENT_AUTO_RECONNECT) ? RECONNECT_COUNTDOWN : null;
|
|
|
|
// Show error status
|
|
$scope.showStatus({
|
|
className: "error",
|
|
title: "CLIENT.DIALOG_HEADER_CONNECTION_ERROR",
|
|
text: "CLIENT.ERROR_CLIENT_" + errorName,
|
|
countdown: countdown,
|
|
actions: [ NAVIGATE_BACK_ACTION, RECONNECT_ACTION ]
|
|
});
|
|
|
|
}
|
|
|
|
// Tunnel error
|
|
else if (connectionState === ManagedClientState.ConnectionState.TUNNEL_ERROR) {
|
|
|
|
// Determine translation name of error
|
|
var errorName = (status in TUNNEL_ERRORS) ? status.toString(16).toUpperCase() : "DEFAULT";
|
|
|
|
// Determine whether the reconnect countdown applies
|
|
var countdown = (status in TUNNEL_AUTO_RECONNECT) ? RECONNECT_COUNTDOWN : null;
|
|
|
|
// Show error status
|
|
$scope.showStatus({
|
|
className: "error",
|
|
title: "CLIENT.DIALOG_HEADER_CONNECTION_ERROR",
|
|
text: "CLIENT.ERROR_TUNNEL_" + errorName,
|
|
countdown: countdown,
|
|
actions: [ NAVIGATE_BACK_ACTION, RECONNECT_ACTION ]
|
|
});
|
|
|
|
}
|
|
|
|
// Disconnected
|
|
else if (connectionState === ManagedClientState.ConnectionState.DISCONNECTED) {
|
|
$scope.showStatus({
|
|
title: "CLIENT.DIALOG_HEADER_DISCONNECTED",
|
|
text: "CLIENT.TEXT_CLIENT_STATUS_" + connectionState.toUpperCase(),
|
|
actions: [ NAVIGATE_BACK_ACTION, RECONNECT_ACTION ]
|
|
});
|
|
}
|
|
|
|
// Hide status for all other states
|
|
else
|
|
$scope.showStatus(false);
|
|
|
|
});
|
|
|
|
$scope.formattedScale = function formattedScale() {
|
|
return Math.round($scope.client.clientProperties.scale * 100);
|
|
};
|
|
|
|
$scope.zoomIn = function zoomIn() {
|
|
$scope.autoFit = false;
|
|
$scope.client.clientProperties.autoFit = false;
|
|
$scope.client.clientProperties.scale += 0.1;
|
|
};
|
|
|
|
$scope.zoomOut = function zoomOut() {
|
|
$scope.client.clientProperties.autoFit = false;
|
|
$scope.client.clientProperties.scale -= 0.1;
|
|
};
|
|
|
|
$scope.autoFit = true;
|
|
|
|
$scope.changeAutoFit = function changeAutoFit() {
|
|
if ($scope.autoFit && $scope.client.clientProperties.minScale) {
|
|
$scope.client.clientProperties.autoFit = true;
|
|
} else {
|
|
$scope.client.clientProperties.autoFit = false;
|
|
$scope.client.clientProperties.scale = 1;
|
|
}
|
|
};
|
|
|
|
$scope.autoFitDisabled = function() {
|
|
return $scope.client.clientProperties.minZoom >= 1;
|
|
};
|
|
|
|
/**
|
|
* Immediately disconnects the currently-connected client, if any.
|
|
*/
|
|
$scope.disconnect = function disconnect() {
|
|
|
|
// Disconnect if client is available
|
|
if ($scope.client)
|
|
$scope.client.client.disconnect();
|
|
|
|
// Hide menu
|
|
$scope.menuShown = false;
|
|
|
|
};
|
|
|
|
// Clean up when view destroyed
|
|
$scope.$on('$destroy', function clientViewDestroyed() {
|
|
|
|
// Remove client from client manager if no longer connected
|
|
var managedClient = $scope.client;
|
|
if (managedClient) {
|
|
|
|
// Get current connection state
|
|
var connectionState = managedClient.clientState.connectionState;
|
|
|
|
// If disconnected, remove from management
|
|
if (connectionState === ManagedClientState.ConnectionState.DISCONNECTED
|
|
|| connectionState === ManagedClientState.ConnectionState.TUNNEL_ERROR
|
|
|| connectionState === ManagedClientState.ConnectionState.CLIENT_ERROR)
|
|
guacClientManager.removeManagedClient(managedClient.id);
|
|
|
|
}
|
|
|
|
// Hide any status dialog
|
|
$scope.showStatus(false);
|
|
|
|
});
|
|
|
|
}]);
|