Merge pull request #26 from glyptodon/guac-input

GUAC-810: Complete implementation of text input
This commit is contained in:
Mike Jumper
2014-12-20 21:26:28 -08:00
12 changed files with 255 additions and 134 deletions

View File

@@ -23,4 +23,4 @@
/** /**
* The module for code used to connect to a connection or balancing group. * The module for code used to connect to a connection or balancing group.
*/ */
angular.module('client', ['auth', 'history', 'osk', 'rest']); angular.module('client', ['auth', 'history', 'osk', 'rest', 'textInput']);

View File

@@ -204,8 +204,9 @@ angular.module('home').controller('clientController', ['$scope', '$routeParams',
// Show/hide UI elements depending on input method // Show/hide UI elements depending on input method
$scope.$watch('inputMethod', function setInputMethod(inputMethod) { $scope.$watch('inputMethod', function setInputMethod(inputMethod) {
// Show on-screen keyboard only if selected // Show input methods only if selected
$scope.showOSK = (inputMethod === 'osk'); $scope.showOSK = (inputMethod === 'osk');
$scope.showTextInput = (inputMethod === 'text');
}); });

View File

@@ -115,6 +115,13 @@ angular.module('client').directive('guacClient', [function guacClient() {
*/ */
var main = $element[0]; 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 * Guacamole mouse event object, wrapped around the main client
* display. * display.
@@ -491,8 +498,8 @@ angular.module('client').directive('guacClient', [function guacClient() {
$scope.clientProperties.scale = $scope.clientProperties.minScale; $scope.clientProperties.scale = $scope.clientProperties.minScale;
}); });
// If the window is resized, attempt to resize client // If the element is resized, attempt to resize client
$window.addEventListener('resize', function onResizeWindow() { resizeSensor.contentWindow.addEventListener('resize', function mainElementResized() {
// Send new display size, if changed // Send new display size, if changed
if (client && display) { if (client && display) {
@@ -530,6 +537,16 @@ angular.module('client').directive('guacClient', [function guacClient() {
} }
}); });
// 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);
});
/** /**
* Converts the given bytes to a base64-encoded string. * Converts the given bytes to a base64-encoded string.
* *

View File

@@ -27,18 +27,6 @@ body.client {
overflow: hidden; overflow: hidden;
} }
/* Viewport Clone */
#viewportClone {
display: table;
height: 100%;
width: 100%;
position: fixed;
left: 0;
top: 0;
visibility: hidden;
}
#preload { #preload {
visibility: hidden; visibility: hidden;
position: absolute; position: absolute;
@@ -48,3 +36,21 @@ body.client {
height: 0; height: 0;
overflow: hidden; overflow: hidden;
} }
.client-view {
display: table;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.client-view .client-body {
display: table-row;
}
.client-view .client-bottom {
display: table-row;
height: 0;
}

View File

@@ -32,11 +32,21 @@
div.main { div.main {
overflow: auto; overflow: auto;
position: fixed; width: 100%;
top: 0; height: 100%;
bottom: 0; position: relative;
right: 0; }
.resize-sensor {
height: 100%;
width: 100%;
position: absolute;
left: 0; left: 0;
top: 0;
overflow: hidden;
border: none;
opacity: 0;
z-index: -1;
} }
div.displayOuter { div.displayOuter {

View File

@@ -19,17 +19,4 @@
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
--> -->
<html><body></body></html>
<div ng-show="errorPresent" class="dialogOuter guac-error">
<div class="dialogMiddle">
<div class="dialog">
<p class="title">{{'client.error.connectionErrorTitle' | translate}}</p>
<p class="status">{{errorStatus}}</p>
<div class="reconnect">
<button ng-click="reconnect()">{{'client.error.reconnect' | translate}}</button>
</div>
</div>
</div>
</div>

View File

@@ -20,99 +20,107 @@
THE SOFTWARE. THE SOFTWARE.
--> -->
<div id="clientContainer"> <!-- Client view -->
<div class="client-view">
<!-- Client --> <!-- Central portion of view -->
<guac-client <div class="client-body">
client-properties="clientProperties"
id="id"
connection-parameters="connectionParameters"
></guac-client>
<!-- Text input target --> <!-- Client -->
<div id="text-input"><div id="text-input-field"><div id="sent-history"></div><textarea rows="1" id="target"></textarea></div><div id="text-input-buttons"><button class="key" data-keysym="0xFFE3" data-sticky="true">{{'client.ctrl' | translate}}</button><button class="key" data-keysym="0xFFE9" data-sticky="true">{{'client.alt' | translate}}</button><button class="key" data-keysym="0xFF1B">{{'client.esc' | translate}}</button><button class="key" data-keysym="0xFF09">{{'client.tab' | translate}}</button></div></div> <guac-client
client-properties="clientProperties"
id="id"
connection-parameters="connectionParameters"/></guac-client>
<!-- Dimensional clone of viewport --> </div>
<div id="viewportClone"></div>
<!-- Menu --> <!-- Bottom portion of view -->
<div ng-class="{open: menuShown}" id="menu"> <div class="client-bottom">
<h2>{{'client.clipboard' | translate}}</h2>
<div class="content" id="clipboard-settings">
<p class="description">{{'client.copiedText' | translate}}</p>
<textarea ng-model="clipboardData" rows="10" cols="40" id="clipboard"></textarea>
</div>
<h2>{{'client.inputMethod' | translate}}</h2> <!-- Text input -->
<div class="content" id="keyboard-settings"> <div class="text-input-container" ng-show="showTextInput">
<guac-text-input needs-focus="showTextInput"></guac-text-input>
<!-- No IME -->
<div class="choice">
<label><input name="input-method" ng-change="closeMenu()" ng-model="inputMethod" type="radio" value="none"/> {{'client.none' | translate}}</label>
<p class="caption"><label for="ime-none">{{'client.noneDesc' | translate}}</label></p>
</div>
<!-- Text input -->
<div class="choice">
<div class="figure"><label for="ime-text"><img src="images/settings/tablet-keys.png" alt=""/></label></div>
<label><input name="input-method" ng-change="closeMenu()" ng-model="inputMethod" type="radio" value="text"/> {{'client.textInput' | translate}}</label>
<p class="caption"><label for="ime-text">{{'client.textInputDesc' | translate}} </label></p>
</div>
<!-- Guac OSK -->
<div class="choice">
<label><input name="input-method" ng-change="closeMenu()" ng-model="inputMethod" type="radio" value="osk"/> {{'client.osk' | translate}}</label>
<p class="caption"><label for="ime-osk">{{'client.oskDesc' | translate}}</label></p>
</div>
</div>
<h2>{{'client.mouseMode' | translate}}</h2>
<div class="content" id="mouse-settings">
<p class="description">{{'client.mouseModeDesc' | translate}}</p>
<!-- Touchscreen -->
<div class="choice">
<input name="mouse-mode" ng-change="closeMenu()" ng-model="clientProperties.emulateAbsoluteMouse" type="radio" ng-value="true" checked="checked" id="absolute"/>
<div class="figure">
<label for="absolute"><img src="images/settings/touchscreen.png" alt="{{'client.touchscreen' | translate}}"/></label>
<p class="caption"><label for="absolute">{{'client.touchscreenDesc' | translate}}</label></p>
</div>
</div>
<!-- Touchpad -->
<div class="choice">
<input name="mouse-mode" ng-change="closeMenu()" ng-model="clientProperties.emulateAbsoluteMouse" type="radio" ng-value="false" id="relative"/>
<div class="figure">
<label for="relative"><img src="images/settings/touchpad.png" alt="{{'client.touchpad' | translate}}"/></label>
<p class="caption"><label for="relative">{{'client.touchpadDesc' | translate}}</label></p>
</div>
</div>
</div>
<h2>{{'client.display' | translate}}</h2>
<div class="content">
<div id="zoom-settings">
<div ng-click="zoomOut()" id="zoom-out"><img src="images/settings/zoom-out.png" alt="-"/></div>
<div id="zoom-state">{{formattedScale()}}%</div>
<div ng-click="zoomIn()" id="zoom-in"><img src="images/settings/zoom-in.png" alt="+"/></div>
</div>
<div><label><input ng-model="autoFit" ng-change="changeAutoFit()" ng-disabled="autoFitDisabled()" type="checkbox" id="auto-fit"/> {{'client.autoFit' | translate}}</label></div>
</div> </div>
</div> </div>
<!-- On-screen keyboard --> </div>
<div class="keyboard-container" ng-class="{shown: showOSK}">
<guac-osk layout="'client.oskLayout' | translate"/> <!-- On-screen keyboard -->
</div> <div class="keyboard-container" ng-class="{shown: showOSK}">
<guac-osk layout="'client.oskLayout' | translate"/>
<!-- Images which should be preloaded --> </div>
<div id="preload">
<img src="images/action-icons/guac-close.png" alt=""/> <!-- Menu -->
<img src="images/progress.png" alt=""/> <div ng-class="{open: menuShown}" id="menu">
</div> <h2>{{'client.clipboard' | translate}}</h2>
<div class="content" id="clipboard-settings">
<ng-include src="app/client/template/clientError.html"/> <p class="description">{{'client.copiedText' | translate}}</p>
<textarea ng-model="clipboardData" rows="10" cols="40" id="clipboard"></textarea>
</div>
<h2>{{'client.inputMethod' | translate}}</h2>
<div class="content" id="keyboard-settings">
<!-- No IME -->
<div class="choice">
<label><input name="input-method" ng-change="closeMenu()" ng-model="inputMethod" type="radio" value="none"/> {{'client.none' | translate}}</label>
<p class="caption"><label for="ime-none">{{'client.noneDesc' | translate}}</label></p>
</div>
<!-- Text input -->
<div class="choice">
<div class="figure"><label for="ime-text"><img src="images/settings/tablet-keys.png" alt=""/></label></div>
<label><input name="input-method" ng-change="closeMenu()" ng-model="inputMethod" type="radio" value="text"/> {{'client.textInput' | translate}}</label>
<p class="caption"><label for="ime-text">{{'client.textInputDesc' | translate}} </label></p>
</div>
<!-- Guac OSK -->
<div class="choice">
<label><input name="input-method" ng-change="closeMenu()" ng-model="inputMethod" type="radio" value="osk"/> {{'client.osk' | translate}}</label>
<p class="caption"><label for="ime-osk">{{'client.oskDesc' | translate}}</label></p>
</div>
</div>
<h2>{{'client.mouseMode' | translate}}</h2>
<div class="content" id="mouse-settings">
<p class="description">{{'client.mouseModeDesc' | translate}}</p>
<!-- Touchscreen -->
<div class="choice">
<input name="mouse-mode" ng-change="closeMenu()" ng-model="clientProperties.emulateAbsoluteMouse" type="radio" ng-value="true" checked="checked" id="absolute"/>
<div class="figure">
<label for="absolute"><img src="images/settings/touchscreen.png" alt="{{'client.touchscreen' | translate}}"/></label>
<p class="caption"><label for="absolute">{{'client.touchscreenDesc' | translate}}</label></p>
</div>
</div>
<!-- Touchpad -->
<div class="choice">
<input name="mouse-mode" ng-change="closeMenu()" ng-model="clientProperties.emulateAbsoluteMouse" type="radio" ng-value="false" id="relative"/>
<div class="figure">
<label for="relative"><img src="images/settings/touchpad.png" alt="{{'client.touchpad' | translate}}"/></label>
<p class="caption"><label for="relative">{{'client.touchpadDesc' | translate}}</label></p>
</div>
</div>
</div>
<h2>{{'client.display' | translate}}</h2>
<div class="content">
<div id="zoom-settings">
<div ng-click="zoomOut()" id="zoom-out"><img src="images/settings/zoom-out.png" alt="-"/></div>
<div id="zoom-state">{{formattedScale()}}%</div>
<div ng-click="zoomIn()" id="zoom-in"><img src="images/settings/zoom-in.png" alt="+"/></div>
</div>
<div><label><input ng-model="autoFit" ng-change="changeAutoFit()" ng-disabled="autoFitDisabled()" type="checkbox" id="auto-fit"/> {{'client.autoFit' | translate}}</label></div>
</div>
</div>
<!-- Images which should be preloaded -->
<div id="preload">
<img src="images/action-icons/guac-close.png" alt=""/>
<img src="images/progress.png" alt=""/>
</div> </div>

View File

@@ -21,6 +21,9 @@
THE SOFTWARE. THE SOFTWARE.
--> -->
<!-- Resize sensor -->
<iframe class="resize-sensor" src="app/client/templates/blank.html"></iframe>
<!-- Display --> <!-- Display -->
<div class="displayOuter"> <div class="displayOuter">
<div class="displayMiddle"> <div class="displayMiddle">

View File

@@ -197,15 +197,31 @@ angular.module('index').controller('indexController', ['$scope', '$injector',
// Create event listeners at the global level // Create event listeners at the global level
var keyboard = new Guacamole.Keyboard($document[0]); var keyboard = new Guacamole.Keyboard($document[0]);
// Broadcast keydown events down the scope heirarchy // Broadcast keydown events
keyboard.onkeydown = function onkeydown(keysym) { keyboard.onkeydown = function onkeydown(keysym) {
// Warn of pending keydown
var guacBeforeKeydownEvent = $scope.$broadcast('guacBeforeKeydown', keysym, keyboard);
if (guacBeforeKeydownEvent.defaultPrevented)
return true;
// If not prevented via guacBeforeKeydown, fire corresponding keydown event
var guacKeydownEvent = $scope.$broadcast('guacKeydown', keysym, keyboard); var guacKeydownEvent = $scope.$broadcast('guacKeydown', keysym, keyboard);
return !guacKeydownEvent.defaultPrevented; return !guacKeydownEvent.defaultPrevented;
}; };
// Broadcast keyup events down the scope heirarchy // Broadcast keyup events
keyboard.onkeyup = function onkeyup(keysym) { keyboard.onkeyup = function onkeyup(keysym) {
// Warn of pending keyup
var guacBeforeKeydownEvent = $scope.$broadcast('guacBeforeKeyup', keysym, keyboard);
if (guacBeforeKeydownEvent.defaultPrevented)
return;
// If not prevented via guacBeforeKeyup, fire corresponding keydown event
$scope.$broadcast('guacKeyup', keysym, keyboard); $scope.$broadcast('guacKeyup', keysym, keyboard);
}; };
// Release all keys when window loses focus // Release all keys when window loses focus

View File

@@ -88,12 +88,12 @@ angular.module('osk').directive('guacOsk', [function guacOsk() {
// Broadcast keydown for each key pressed // Broadcast keydown for each key pressed
keyboard.onkeydown = function(keysym) { keyboard.onkeydown = function(keysym) {
$rootScope.$broadcast('guacKeydown', keysym); $rootScope.$broadcast('guacSyntheticKeydown', keysym);
}; };
// Broadcast keydown for each key released // Broadcast keydown for each key released
keyboard.onkeyup = function(keysym) { keyboard.onkeyup = function(keysym) {
$rootScope.$broadcast('guacKeyup', keysym); $rootScope.$broadcast('guacSyntheticKeyup', keysym);
}; };
// Resize keyboard whenever window changes size // Resize keyboard whenever window changes size

View File

@@ -90,8 +90,8 @@ angular.module('textInput').directive('guacKey', [function guacKey() {
// For all non-sticky keys, press and release key immediately // For all non-sticky keys, press and release key immediately
else { else {
$rootScope.$broadcast('guacKeydown', $scope.keysym); $rootScope.$broadcast('guacSyntheticKeydown', $scope.keysym);
$rootScope.$broadcast('guacKeyup', $scope.keysym); $rootScope.$broadcast('guacSyntheticKeyup', $scope.keysym);
} }
}; };
@@ -101,11 +101,11 @@ angular.module('textInput').directive('guacKey', [function guacKey() {
// If the key is pressed now, send keydown // If the key is pressed now, send keydown
if (isPressed) if (isPressed)
$rootScope.$broadcast('guacKeydown', $scope.keysym); $rootScope.$broadcast('guacSyntheticKeydown', $scope.keysym);
// If the key was pressed, but is not pressed any longer, send keyup // If the key was pressed, but is not pressed any longer, send keyup
else if (wasPressed) else if (wasPressed)
$rootScope.$broadcast('guacKeyup', $scope.keysym); $rootScope.$broadcast('guacSyntheticKeyup', $scope.keysym);
}); });

View File

@@ -28,7 +28,17 @@ angular.module('textInput').directive('guacTextInput', [function guacTextInput()
return { return {
restrict: 'E', restrict: 'E',
replace: true, replace: true,
scope: {}, scope: {
/**
* Whether the text input UI should have focus. Setting this value
* is not guaranteed to work, due to browser limitations.
*
* @type Boolean
*/
needsFocus : '=?'
},
templateUrl: 'app/textInput/templates/guacTextInput.html', templateUrl: 'app/textInput/templates/guacTextInput.html',
controller: ['$scope', '$rootScope', '$element', '$timeout', controller: ['$scope', '$rootScope', '$element', '$timeout',
@@ -50,6 +60,47 @@ angular.module('textInput').directive('guacTextInput', [function guacTextInput()
*/ */
var TEXT_INPUT_PADDING_CODEPOINT = 0x200B; var TEXT_INPUT_PADDING_CODEPOINT = 0x200B;
/**
* Keys which should be allowed through to the client when in text
* input mode, providing corresponding key events are received.
* Keys in this set will be allowed through to the server.
*
* @type Object.<Number, Boolean>
*/
var ALLOWED_KEYS = {
0xFE03: true, /* AltGr */
0xFF08: true, /* Backspace */
0xFF09: true, /* Tab */
0xFF0D: true, /* Enter */
0xFF1B: true, /* Escape */
0xFF50: true, /* Home */
0xFF51: true, /* Left */
0xFF52: true, /* Up */
0xFF53: true, /* Right */
0xFF54: true, /* Down */
0xFF57: true, /* End */
0xFF64: true, /* Insert */
0xFFBE: true, /* F1 */
0xFFBF: true, /* F2 */
0xFFC0: true, /* F3 */
0xFFC1: true, /* F4 */
0xFFC2: true, /* F5 */
0xFFC3: true, /* F6 */
0xFFC4: true, /* F7 */
0xFFC5: true, /* F8 */
0xFFC6: true, /* F9 */
0xFFC7: true, /* F10 */
0xFFC8: true, /* F11 */
0xFFC9: true, /* F12 */
0xFFE1: true, /* Left shift */
0xFFE2: true, /* Right shift */
0xFFE3: true, /* Left ctrl */
0xFFE4: true, /* Right ctrl */
0xFFE9: true, /* Left alt */
0xFFEA: true, /* Right alt */
0xFFFF: true /* Delete */
};
/** /**
* Recently-sent text, ordered from oldest to most recent. * Recently-sent text, ordered from oldest to most recent.
* *
@@ -151,8 +202,8 @@ angular.module('textInput').directive('guacTextInput', [function guacTextInput()
* @param {Number} keysym The keysym of the key to send. * @param {Number} keysym The keysym of the key to send.
*/ */
var sendKeysym = function sendKeysym(keysym) { var sendKeysym = function sendKeysym(keysym) {
$rootScope.$broadcast('guacKeydown', keysym); $rootScope.$broadcast('guacSyntheticKeydown', keysym);
$rootScope.$broadcast('guacKeyup', keysym); $rootScope.$broadcast('guacSyntheticKeyup', keysym);
}; };
/** /**
@@ -283,6 +334,28 @@ angular.module('textInput').directive('guacTextInput', [function guacTextInput()
e.preventDefault(); e.preventDefault();
}, false); }, false);
// Attempt to change focus depending on need
$scope.$watch('needsFocus', function focusDesireChanged(focusNeeded) {
if (focusNeeded)
target.focus();
else
target.blur();
});
// If the text input UI has focus, prevent keydown events
$scope.$on('guacBeforeKeydown', function filterKeydown(event, keysym) {
if (hasFocus && !ALLOWED_KEYS[keysym])
event.preventDefault();
});
// If the text input UI has focus, prevent keyup events
$scope.$on('guacBeforeKeyup', function filterKeyup(event, keysym) {
if (hasFocus && !ALLOWED_KEYS[keysym])
event.preventDefault();
});
}] }]
}; };