Merge pull request #25 from glyptodon/guac-input

GUAC-810: Implement guacTextInput directive.
This commit is contained in:
James Muehlner
2014-12-19 22:52:38 -08:00
6 changed files with 496 additions and 17 deletions

View File

@@ -0,0 +1,115 @@
/*
* 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.
*/
/**
* A directive which displays a button that controls the pressed state of a
* single keyboard key.
*/
angular.module('textInput').directive('guacKey', [function guacKey() {
return {
restrict: 'E',
replace: true,
scope: {
/**
* The text to display within the key. This will be run through the
* translation filter prior to display.
*
* @type String
*/
text : '=',
/**
* The keysym to send within keyup and keydown events when this key
* is pressed or released.
*
* @type Number
*/
keysym : '=',
/**
* Whether this key is sticky. Sticky keys toggle their pressed
* state with each click.
*
* @type Boolean
* @default false
*/
sticky : '=?',
/**
* Whether this key is currently pressed.
*
* @type Boolean
* @default false
*/
pressed : '=?'
},
templateUrl: 'app/textInput/templates/guacKey.html',
controller: ['$scope', '$rootScope',
function guacKey($scope, $rootScope) {
// Not sticky by default
$scope.sticky = $scope.sticky || false;
// Unpressed by default
$scope.pressed = $scope.pressed || false;
/**
* Presses and releases this key, sending the corresponding keydown
* and keyup events. In the case of sticky keys, the pressed state
* is toggled, and only a single keydown/keyup event will be sent,
* depending on the current state.
*/
$scope.updateKey = function updateKey() {
// If sticky, toggle pressed state
if ($scope.sticky)
$scope.pressed = !$scope.pressed;
// For all non-sticky keys, press and release key immediately
else {
$rootScope.$broadcast('guacKeydown', $scope.keysym);
$rootScope.$broadcast('guacKeyup', $scope.keysym);
}
};
// Send keyup/keydown when pressed state is altered
$scope.$watch('pressed', function updatePressedState(isPressed, wasPressed) {
// If the key is pressed now, send keydown
if (isPressed)
$rootScope.$broadcast('guacKeydown', $scope.keysym);
// If the key was pressed, but is not pressed any longer, send keyup
else if (wasPressed)
$rootScope.$broadcast('guacKeyup', $scope.keysym);
});
}]
};
}]);

View File

@@ -0,0 +1,289 @@
/*
* 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.
*/
/**
* A directive which displays the Guacamole text input method.
*/
angular.module('textInput').directive('guacTextInput', [function guacTextInput() {
return {
restrict: 'E',
replace: true,
scope: {},
templateUrl: 'app/textInput/templates/guacTextInput.html',
controller: ['$scope', '$rootScope', '$element', '$timeout',
function guacTextInput($scope, $rootScope, $element, $timeout) {
/**
* The number of characters to include on either side of text input
* content, to allow the user room to use backspace and delete.
*
* @type Number
*/
var TEXT_INPUT_PADDING = 128;
/**
* The Unicode codepoint of the character to use for padding on
* either side of text input content.
*
* @type Number
*/
var TEXT_INPUT_PADDING_CODEPOINT = 0x200B;
/**
* Recently-sent text, ordered from oldest to most recent.
*
* @type String[]
*/
$scope.sentText = [];
/**
* Whether the "Alt" key is currently pressed within the text input
* interface.
*
* @type Boolean
*/
$scope.altPressed = false;
/**
* Whether the "Ctrl" key is currently pressed within the text
* input interface.
*
* @type Boolean
*/
$scope.ctrlPressed = false;
/**
* The text area input target.
*
* @type Element
*/
var target = $element.find('.target')[0];
/**
* Whether the text input target currently has focus. Setting this
* attribute has no effect, but any bound property will be updated
* as focus is gained or lost.
*
* @type Boolean
*/
var hasFocus = false;
target.onfocus = function targetFocusGained() {
hasFocus = true;
resetTextInputTarget(TEXT_INPUT_PADDING);
};
target.onblur = function targetFocusLost() {
hasFocus = false;
target.focus();
};
/**
* Whether composition is currently active within the text input
* target element, such as when an IME is in use.
*
* @type Boolean
*/
var composingText = false;
target.addEventListener("compositionstart", function targetComposeStart(e) {
composingText = true;
}, false);
target.addEventListener("compositionend", function targetComposeEnd(e) {
composingText = false;
}, false);
/**
* Translates a given Unicode codepoint into the corresponding X11
* keysym.
*
* @param {Number} codepoint
* The Unicode codepoint to translate.
*
* @returns {Number}
* The X11 keysym that corresponds to the given Unicode
* codepoint, or null if no such keysym exists.
*/
var keysymFromCodepoint = function keysymFromCodepoint(codepoint) {
// Keysyms for control characters
if (codepoint <= 0x1F || (codepoint >= 0x7F && codepoint <= 0x9F))
return 0xFF00 | codepoint;
// Keysyms for ASCII chars
if (codepoint >= 0x0000 && codepoint <= 0x00FF)
return codepoint;
// Keysyms for Unicode
if (codepoint >= 0x0100 && codepoint <= 0x10FFFF)
return 0x01000000 | codepoint;
return null;
};
/**
* Presses and releases the key corresponding to the given keysym,
* as if typed by the user.
*
* @param {Number} keysym The keysym of the key to send.
*/
var sendKeysym = function sendKeysym(keysym) {
$rootScope.$broadcast('guacKeydown', keysym);
$rootScope.$broadcast('guacKeyup', keysym);
};
/**
* Presses and releases the key having the keysym corresponding to
* the Unicode codepoint given, as if typed by the user.
*
* @param {Number} codepoint
* The Unicode codepoint of the key to send.
*/
var sendCodepoint = function sendCodepoint(codepoint) {
if (codepoint === 10) {
sendKeysym(0xFF0D);
releaseStickyKeys();
return;
}
var keysym = keysymFromCodepoint(codepoint);
if (keysym) {
sendKeysym(keysym);
releaseStickyKeys();
}
};
/**
* Translates each character within the given string to keysyms and
* sends each, in order, as if typed by the user.
*
* @param {String} content
* The string to send.
*/
var sendString = function sendString(content) {
var sentText = "";
// Send each codepoint within the string
for (var i=0; i<content.length; i++) {
var codepoint = content.charCodeAt(i);
if (codepoint !== TEXT_INPUT_PADDING_CODEPOINT) {
sentText += String.fromCharCode(codepoint);
sendCodepoint(codepoint);
}
}
// Display the text that was sent
$scope.$apply(function addSentText() {
$scope.sentText.push(sentText);
});
// Remove text after one second
$timeout(function removeSentText() {
$scope.sentText.shift();
}, 1000);
};
/**
* Releases all currently-held sticky keys within the text input UI.
*/
var releaseStickyKeys = function releaseStickyKeys() {
// Reset all sticky keys
$scope.$apply(function clearAllStickyKeys() {
$scope.altPressed = false;
$scope.ctrlPressed = false;
});
};
/**
* Removes all content from the text input target, replacing it
* with the given number of padding characters. Padding of the
* requested size is added on both sides of the cursor, thus the
* overall number of characters added will be twice the number
* specified.
*
* @param {Number} padding
* The number of characters to pad the text area with.
*/
var resetTextInputTarget = function resetTextInputTarget(padding) {
var paddingChar = String.fromCharCode(TEXT_INPUT_PADDING_CODEPOINT);
// Pad text area with an arbitrary, non-typable character (so there is something
// to delete with backspace or del), and position cursor in middle.
target.value = new Array(padding*2 + 1).join(paddingChar);
target.setSelectionRange(padding, padding);
};
target.addEventListener("input", function(e) {
// Ignore input events during text composition
if (composingText)
return;
var i;
var content = target.value;
var expectedLength = TEXT_INPUT_PADDING*2;
// If content removed, update
if (content.length < expectedLength) {
// Calculate number of backspaces and send
var backspaceCount = TEXT_INPUT_PADDING - target.selectionStart;
for (i = 0; i < backspaceCount; i++)
sendKeysym(0xFF08);
// Calculate number of deletes and send
var deleteCount = expectedLength - content.length - backspaceCount;
for (i = 0; i < deleteCount; i++)
sendKeysym(0xFFFF);
}
else
sendString(content);
// Reset content
resetTextInputTarget(TEXT_INPUT_PADDING);
e.preventDefault();
}, false);
// Do not allow event target contents to be selected during input
target.addEventListener("selectstart", function(e) {
e.preventDefault();
}, false);
}]
};
}]);

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2013 Glyptodon LLC
* 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
@@ -20,33 +20,30 @@
* THE SOFTWARE.
*/
#text-input {
display: none;
position: absolute;
.text-input {
width: 100%;
border-top: 1px solid rgba(0, 0, 0, 0.5);
background: #CDA;
}
#text-input #text-input-field,
#text-input #text-input-buttons {
.text-input .text-input-field,
.text-input .text-input-buttons {
display: inline-block;
vertical-align: middle;
}
#text-input #text-input-field {
.text-input .text-input-field {
width: 30%;
overflow: hidden;
white-space: nowrap;
}
#text-input #text-input-buttons {
.text-input .text-input-buttons {
width: 70%;
text-align: right;
}
#text-input #target {
.text-input .target {
border: none;
border-radius: 0;
@@ -67,18 +64,18 @@
}
#text-input.open {
.text-input.open {
display: block;
}
#text-input #sent-history {
.text-input .sent-history {
display: inline-block;
vertical-align: middle;
padding: 0.25em;
padding-right: 0;
}
#text-input #sent-history .sent-text {
.text-input .sent-history .sent-text {
display: inline-block;
vertical-align: baseline;
white-space: pre;
@@ -89,7 +86,7 @@
opacity: 0;
}
#text-input #text-input-buttons button {
.text-input .text-input-buttons button {
border: 1px solid rgba(0, 0, 0, 0.5);
background: none;
color: black;
@@ -101,8 +98,8 @@
min-width: 3em;
}
#text-input #text-input-buttons button:active,
#text-input #text-input-buttons button.pressed {
.text-input .text-input-buttons button:active,
.text-input .text-input-buttons button.pressed {
border: 1px solid rgba(255, 255, 255, 0.5);
background: rgba(0, 0, 0, 0.75);
color: white;

View File

@@ -0,0 +1,25 @@
<button class="key" ng-click="updateKey()" ng-class="{pressed: pressed, sticky: sticky}">
<!--
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.
-->
{{text | translate}}
</button>

View File

@@ -0,0 +1,27 @@
<div class="text-input">
<!--
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.
-->
<!-- Text input target -->
<div class="text-input-field"><div class="sent-history"><div class="sent-text" ng-repeat="text in sentText track by $index">{{text}}</div></div><textarea rows="1" class="target"></textarea></div><div class="text-input-buttons"><guac-key keysym="65507" sticky="true" text="'client.ctrl'" pressed="ctrlPressed"></guac-key><guac-key keysym="65513" sticky="true" text="'client.alt'" pressed="altPressed"></guac-key><guac-key keysym="65307" text="'client.esc'"></guac-key><guac-key keysym="65289" text="'client.tab'"></guac-key></div>
</div>

View File

@@ -0,0 +1,26 @@
/*
* 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.
*/
/**
* Module for displaying the Guacamole text input method.
*/
angular.module('textInput', []);