From e3a249d39a3f9455a377d1edf168c48f6371317c Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Wed, 30 Aug 2023 23:22:13 +0000 Subject: [PATCH] GUACAMOLE-1820: Refactor key event interpretation, moving display-specific code to the webapp. --- .../webapp/modules/KeyEventInterpreter.js | 383 ++++-------------- .../main/webapp/modules/SessionRecording.js | 32 +- .../src/app/player/directives/player.js | 15 +- .../src/app/player/directives/textView.js | 8 +- .../player/services/keyEventDisplayService.js | 371 +++++++++++++++++ .../src/app/player/types/TextBatch.js | 117 ------ 6 files changed, 477 insertions(+), 449 deletions(-) create mode 100644 guacamole/src/main/frontend/src/app/player/services/keyEventDisplayService.js delete mode 100644 guacamole/src/main/frontend/src/app/player/types/TextBatch.js diff --git a/guacamole-common-js/src/main/webapp/modules/KeyEventInterpreter.js b/guacamole-common-js/src/main/webapp/modules/KeyEventInterpreter.js index 44354fb6e7..47b090243b 100644 --- a/guacamole-common-js/src/main/webapp/modules/KeyEventInterpreter.js +++ b/guacamole-common-js/src/main/webapp/modules/KeyEventInterpreter.js @@ -20,84 +20,22 @@ var Guacamole = Guacamole || {}; /** - * An object that will accept raw key events and produce human readable text - * batches, seperated by at least `batchSeperation` milliseconds, which can be - * retrieved through the onbatch callback or by calling getCurrentBatch(). - * - * NOTE: The event processing logic and output format is based on the `guaclog` - * tool, with the addition of batching support. + * An object that will accept raw key events and produce a chronologically + * ordered array of key event objects. These events can be obtained by + * calling getEvents(). * * @constructor - * - * @param {number} [batchSeperation=5000] - * The minimum number of milliseconds that must elapse between subsequent - * batches of key-event-generated text. If 0 or negative, no splitting will - * occur, resulting in a single batch for all provided key events. - * * @param {number} [startTimestamp=0] * The starting timestamp for the recording being intepreted. If provided, * the timestamp of each intepreted event will be relative to this timestamp. * If not provided, the raw recording timestamp will be used. */ -Guacamole.KeyEventInterpreter = function KeyEventInterpreter(batchSeperation, startTimestamp) { - - /** - * Reference to this Guacamole.KeyEventInterpreter. - * - * @private - * @type {!Guacamole.SessionRecording} - */ - var interpreter = this; - - // Default to 5 seconds if the batch seperation was not provided - if (batchSeperation === undefined || batchSeperation === null) - batchSeperation = 5000; +Guacamole.KeyEventInterpreter = function KeyEventInterpreter(startTimestamp) { // Default to 0 seconds to keep the raw timestamps if (startTimestamp === undefined || startTimestamp === null) startTimestamp = 0; - /** - * A definition for a known key. - * - * @constructor - * @private - * @param {KeyDefinition|object} [template={}] - * The object whose properties should be copied within the new - * KeyDefinition. - */ - var KeyDefinition = function KeyDefinition(template) { - - /** - * The X11 keysym of the key. - * @type {!number} - */ - this.keysym = parseInt(template.keysym); - - /** - * A human-readable name for the key. - * @type {!String} - */ - this.name = template.name; - - /** - * The value which would be typed in a typical text editor, if any. If the - * key is not associated with any typable value, or if the typable value is - * not generally useful in an auditing context, this will be undefined. - * @type {String} - */ - this.value = template.value; - - /** - * Whether this key is a modifier which may affect the interpretation of - * other keys, and thus should be tracked as it is held down. - * @type {!boolean} - * @default false - */ - this.modifier = template.modifier || false; - - }; - /** * A precursor array to the KNOWN_KEYS map. The objects contained within * will be constructed into full KeyDefinition objects. @@ -107,7 +45,7 @@ Guacamole.KeyEventInterpreter = function KeyEventInterpreter(batchSeperation, st * @type {Object[]} */ var _KNOWN_KEYS = [ - {keysym: 0xFE03, name: 'AltGr', value: "", modifier: true }, + {keysym: 0xFE03, name: 'AltGr' }, {keysym: 0xFF08, name: 'Backspace' }, {keysym: 0xFF09, name: 'Tab' }, {keysym: 0xFF0B, name: 'Clear' }, @@ -178,19 +116,19 @@ Guacamole.KeyEventInterpreter = function KeyEventInterpreter(batchSeperation, st {keysym: 0xFFD3, name: 'F22' }, {keysym: 0xFFD4, name: 'F23' }, {keysym: 0xFFD5, name: 'F24' }, - {keysym: 0xFFE1, name: 'Shift', value: "", modifier: true }, - {keysym: 0xFFE2, name: 'Shift', value: "", modifier: true }, - {keysym: 0xFFE3, name: 'Ctrl', value: null, modifier: true }, - {keysym: 0xFFE4, name: 'Ctrl', value: null, modifier: true }, + {keysym: 0xFFE1, name: 'Shift' }, + {keysym: 0xFFE2, name: 'Shift' }, + {keysym: 0xFFE3, name: 'Ctrl' }, + {keysym: 0xFFE4, name: 'Ctrl' }, {keysym: 0xFFE5, name: 'Caps' }, - {keysym: 0xFFE7, name: 'Meta', value: null, modifier: true }, - {keysym: 0xFFE8, name: 'Meta', value: null, modifier: true }, - {keysym: 0xFFE9, name: 'Alt', value: null, modifier: true }, - {keysym: 0xFFEA, name: 'Alt', value: null, modifier: true }, - {keysym: 0xFFEB, name: 'Super', value: null, modifier: true }, - {keysym: 0xFFEC, name: 'Super', value: null, modifier: true }, - {keysym: 0xFFED, name: 'Hyper', value: null, modifier: true }, - {keysym: 0xFFEE, name: 'Hyper', value: null, modifier: true }, + {keysym: 0xFFE7, name: 'Meta' }, + {keysym: 0xFFE8, name: 'Meta' }, + {keysym: 0xFFE9, name: 'Alt' }, + {keysym: 0xFFEA, name: 'Alt' }, + {keysym: 0xFFEB, name: 'Super' }, + {keysym: 0xFFEC, name: 'Super' }, + {keysym: 0xFFED, name: 'Hyper' }, + {keysym: 0xFFEE, name: 'Hyper' }, {keysym: 0xFFFF, name: 'Delete' } ]; @@ -205,60 +143,18 @@ Guacamole.KeyEventInterpreter = function KeyEventInterpreter(batchSeperation, st _KNOWN_KEYS.forEach(function createKeyDefinitionMap(keyDefinition) { // Construct a map of keysym to KeyDefinition object - KNOWN_KEYS[keyDefinition.keysym] = new KeyDefinition(keyDefinition) + KNOWN_KEYS[keyDefinition.keysym] = ( + new Guacamole.KeyEventInterpreter.KeyDefinition(keyDefinition)); }); /** - * A map of X11 keysyms to a KeyDefinition object, if the corresponding - * key is currently pressed. If a keysym has no entry in this map at all, - * it means that the key is not being pressed. Note that not all keysyms - * are necessarily tracked within this map - only those that are explicitly - * tracked. - * - * @private - * @type {Object. } - */ - var pressedKeys = {}; - - /** - * The current key event batch, containing a representation of all key - * events processed since the end of the last batch passed to onbatch. - * Null if no key events have been processed yet. + * All key events parsed as of the most recent handleKeyEvent() invocation. * * @private - * @type {!KeyEventBatch} - */ - var currentBatch = null; - - /** - * The timestamp of the most recent key event processed. - * - * @private - * @type {Number} - */ - var lastKeyEvent = 0; - - /** - * Returns true if the currently-pressed keys are part of a shortcut, or - * false otherwise. - * - * @private - * @returns {!boolean} - * True if the currently-pressed keys are part of a shortcut, or false - * otherwise. + * @type {!Guacamole.KeyEventInterpreter.KeyEvent[]} */ - function isShortcut() { - - // If one of the currently-pressed keys is non-printable, a shortcut - // is being typed - for (var keysym in pressedKeys) { - if (pressedKeys[keysym].value === null) - return true; - } - - return false; - } + var parsedEvents = []; /** * If the provided keysym corresponds to a valid UTF-8 character, return @@ -268,7 +164,7 @@ Guacamole.KeyEventInterpreter = function KeyEventInterpreter(batchSeperation, st * @param {Number} keysym * The keysym to produce a UTF-8 KeyDefinition for, if valid. * - * @returns {KeyDefinition} + * @returns {Guacamole.KeyEventInterpreter.KeyDefinition} * A KeyDefinition for the provided keysym, if it's a valid UTF-8 * keysym, or null otherwise. */ @@ -283,7 +179,8 @@ Guacamole.KeyEventInterpreter = function KeyEventInterpreter(batchSeperation, st var name = String.fromCharCode(codepoint); // Create and return the definition - return new KeyDefinition({keysym: keysym, name: name, value: name, modifier: false}); + return new Guacamole.KeyEventInterpreter.KeyDefinition({ + keysym: keysym, name: name, value: name}); } @@ -310,7 +207,7 @@ Guacamole.KeyEventInterpreter = function KeyEventInterpreter(batchSeperation, st // If it's not UTF-8, return an unknown definition, with the name // just set to the hex value of the keysym - return new KeyDefinition({ + return new Guacamole.KeyEventInterpreter.KeyDefinition({ keysym: keysym, name: '0x' + String(keysym.toString(16)) }) @@ -318,20 +215,8 @@ Guacamole.KeyEventInterpreter = function KeyEventInterpreter(batchSeperation, st } /** - * Fired whenever a new batch of typed text extracted from key events - * is available. A new batch will be provided every time a new key event - * is processed after more than batchSeperation milliseconds after the - * previous key event. - * - * @event - * @param {!Guacamole.KeyEventInterpreter.KeyEventBatch} - */ - this.onbatch = null; - - /** - * Handles a raw key event, potentially appending typed text to the - * current batch, and calling onbatch with the current batch, if the - * callback is set and a new batch is about to be started. + * Handles a raw key event, appending a new key event object for every + * handled raw event. * * @param {!string[]} args * The arguments of the key event. @@ -347,111 +232,18 @@ Guacamole.KeyEventInterpreter = function KeyEventInterpreter(batchSeperation, st // The timestamp when this key event occured var timestamp = parseInt(args[2]); - // If no current batch exists, start a new one now - if (!currentBatch) - currentBatch = new Guacamole.KeyEventInterpreter.KeyEventBatch(); - - // Only switch to a new batch of text if sufficient time has passed - // since the last key event - var newBatch = (batchSeperation >= 0 - && (timestamp - lastKeyEvent) >= batchSeperation); - lastKeyEvent = timestamp; - - if (newBatch) { - - // Call the handler with the current batch of text and the timestamp - // at which the current batch started - if (currentBatch.events.length && interpreter.onbatch) - interpreter.onbatch(currentBatch); - - // Move on to the next batch of text - currentBatch = new Guacamole.KeyEventInterpreter.KeyEventBatch(); - - } - - var keyDefinition = getKeyDefinitionByKeysym(keysym); - - // Mark down whether the key was pressed or released - if (keyDefinition.modifier) { - if (pressed) - pressedKeys[keysym] = keyDefinition; - else - delete pressedKeys[keysym]; - } - - // Append to the current typed value when a printable - // (non-modifier) key is pressed - else if (pressed) { - - var relativeTimestap = timestamp - startTimestamp; + // The timestamp relative to the provided initial timestamp + var relativeTimestap = timestamp - startTimestamp; - if (isShortcut()) { + // Known information about the parsed key + var definition = getKeyDefinitionByKeysym(keysym); - var shortcutText = '<'; - - var firstKey = true; - - // Compose entry by inspecting the state of each tracked key. - // At least one key must be pressed when in a shortcut. - for (var keysym in pressedKeys) { - - var pressedKeyDefinition = pressedKeys[keysym]; - - // Print name of key - if (firstKey) { - shortcutText += pressedKeyDefinition.name; - firstKey = false; - } - - else - shortcutText += ('+' + pressedKeyDefinition.name); - - } - - // Finally, append the printable key to close the shortcut - shortcutText += ('+' + keyDefinition.name + '>') - - // Add the shortcut to the current batch - currentBatch.simpleValue += shortcutText; - currentBatch.events.push(new Guacamole.KeyEventInterpreter.KeyEvent( - shortcutText, false, relativeTimestap)); - - } - - // Print the key itself - else { - - var keyText; - var typed; - - // Print the value if explicitly defined - if (keyDefinition.value != null) { - - keyText = keyDefinition.value; - typed = true; - - } - - // Otherwise print the name - else { - - keyText = ('<' + keyDefinition.name + '>'); - - // While this is a representation for a single character, - // the key text is the name of the key, not the actual - // character itself - typed = false; - - } - - // Add the key to the current batch - currentBatch.simpleValue += keyText; - currentBatch.events.push(new Guacamole.KeyEventInterpreter.KeyEvent( - keyText, typed, relativeTimestap)); - - } - - } + // Push the latest parsed event into the list + parsedEvents.push(new Guacamole.KeyEventInterpreter.KeyEvent({ + definition: definition, + pressed: pressed, + timestamp: relativeTimestap + })); }; @@ -460,91 +252,84 @@ Guacamole.KeyEventInterpreter = function KeyEventInterpreter(batchSeperation, st * incomplete, as more key events might be processed before the next * batch starts. * - * @returns {Guacamole.KeyEventInterpreter.KeyEventBatch} + * @returns {Guacamole.KeyEventInterpreter.KeyEvent[]} * The current batch of text. */ - this.getCurrentBatch = function getCurrentBatch() { - return currentBatch; + this.getEvents = function getEvents() { + return parsedEvents; }; + }; /** - * A granular description of an extracted key event, including a human-readable - * text representation of the event, whether the event is directly typed or not, - * and the timestamp when the event occured. + * A definition for a known key. * * @constructor - * @param {!String} text - * A human-readable representation of the event. - * - * @param {!boolean} typed - * True if this event represents a directly-typed character, or false - * otherwise. - * - * @param {!Number} timestamp - * The timestamp from the recording when this event occured. + * @param {Guacamole.KeyEventInterpreter.KeyDefinition|object} [template={}] + * The object whose properties should be copied within the new + * KeyDefinition. */ -Guacamole.KeyEventInterpreter.KeyEvent = function KeyEvent(text, typed, timestamp) { +Guacamole.KeyEventInterpreter.KeyDefinition = function KeyDefinition(template) { + + // Use empty object by default + template = template || {}; /** - * A human-readable representation of the event. If a printable character - * was directly typed, this will just be that character. Otherwise it will - * be a string describing the event. - * - * @type {!String} + * The X11 keysym of the key. + * @type {!number} */ - this.text = text; + this.keysym = parseInt(template.keysym); /** - * True if this text of this event is exactly a typed character, or false - * otherwise. - * - * @type {!boolean} + * A human-readable name for the key. + * @type {!String} */ - this.typed = typed; + this.name = template.name; /** - * The timestamp from the recording when this event occured. If a - * `startTimestamp` value was provided to the interpreter constructor, this - * will be relative to start of the recording. If not, it will be the raw - * timestamp from the key event. - * - * @type {!Number} + * The value which would be typed in a typical text editor, if any. If the + * key is not associated with any typeable value, this will be undefined. + * @type {String} */ - this.timestamp = timestamp; + this.value = template.value; }; /** - * A series of intepreted key events, seperated by at least the configured - * batchSeperation value from any other key events in the recording corresponding - * to the interpreted key events. A batch will always consist of at least one key - * event, and an associated simplified representation of the event(s). + * A granular description of an extracted key event, including a human-readable + * text representation of the event, whether the event is directly typed or not, + * and the timestamp when the event occured. * * @constructor - * @param {!Guacamole.KeyEventInterpreter.KeyEvent[]} events - * The interpreted key events for this batch. - * - * @param {!String} simpleValue - * The simplified, human-readable value representing the key events for - * this batch. + * @param {Guacamole.KeyEventInterpreter.KeyEvent|object} [template={}] + * The object whose properties should be copied within the new + * KeyEvent. */ -Guacamole.KeyEventInterpreter.KeyEventBatch = function KeyEventBatch(events, simpleValue) { +Guacamole.KeyEventInterpreter.KeyEvent = function KeyEvent(template) { + + // Use empty object by default + template = template || {}; /** - * All key events for this batch. + * The key definition for the pressed key. * - * @type {!Guacamole.KeyEventInterpreter.KeyEvent[]} + * @type {!Guacamole.KeyEventInterpreter.KeyDefinition} */ - this.events = events || []; + this.definition = template.definition; /** - * The simplified, human-readable value representing the key events for - * this batch, equivalent to concatenating the `text` field of all key - * events in the batch. + * True if the key was pressed to create this event, or false if it was + * released. * - * @type {!String} + * @type {!boolean} + */ + this.pressed = !!template.pressed; + + /** + * The timestamp from the recording when this event occured. + * + * @type {!Number} */ - this.simpleValue = simpleValue || ''; + this.timestamp = template.timestamp; }; diff --git a/guacamole-common-js/src/main/webapp/modules/SessionRecording.js b/guacamole-common-js/src/main/webapp/modules/SessionRecording.js index 619f78fbaa..95f881a084 100644 --- a/guacamole-common-js/src/main/webapp/modules/SessionRecording.js +++ b/guacamole-common-js/src/main/webapp/modules/SessionRecording.js @@ -394,17 +394,7 @@ Guacamole.SessionRecording = function SessionRecording(source, refreshInterval) * the recording. */ function initializeKeyInterpreter(startTimestamp) { - - keyEventInterpreter = new Guacamole.KeyEventInterpreter(null, startTimestamp); - - // Pass through any received batches to the recording ontext handler - keyEventInterpreter.onbatch = function onbatch(batch) { - - // Pass the batch through if a handler is set - if (recording.ontext) - recording.ontext(batch); - - }; + keyEventInterpreter = new Guacamole.KeyEventInterpreter(startTimestamp); } /** @@ -527,11 +517,10 @@ Guacamole.SessionRecording = function SessionRecording(source, refreshInterval) instructionBuffer = ''; } - // If there's any typed text that's yet to be sent to the ontext - // handler, send it now - var batch = keyEventInterpreter.getCurrentBatch(); - if (batch && recording.ontext) - recording.ontext(batch); + // Now that the recording is fully processed, and all key events + // have been extracted, call the onkeyevents handler if defined + if (recording.onkeyevents) + recording.onkeyevents(keyEventInterpreter.getEvents()); // Consider recording loaded if tunnel has closed without errors if (!errorEncountered) @@ -919,14 +908,15 @@ Guacamole.SessionRecording = function SessionRecording(source, refreshInterval) this.onpause = null; /** - * Fired whenever a new batch of typed text extracted from key events - * is available. + * Fired with all extracted key events when the recording is fully + * processed. The callback will be invoked with an empty list + * if no key events were extracted. * * @event - * @param {!Guacamole.KeyEventInterpreter.KeyEventBatch} batch - * The batch of extracted text. + * @param {!Guacamole.KeyEventInterpreter.KeyEvent[]} batch + * The extracted key events. */ - this.ontext = null; + this.onkeyevents = null; /** * Fired whenever the playback position within the recording changes. diff --git a/guacamole/src/main/frontend/src/app/player/directives/player.js b/guacamole/src/main/frontend/src/app/player/directives/player.js index ae300bcbfd..a239e0d2bf 100644 --- a/guacamole/src/main/frontend/src/app/player/directives/player.js +++ b/guacamole/src/main/frontend/src/app/player/directives/player.js @@ -78,11 +78,9 @@ angular.module('player').directive('guacPlayer', ['$injector', function guacPlayer($injector) { // Required services + const keyEventDisplayService = $injector.get('keyEventDisplayService'); const playerTimeService = $injector.get('playerTimeService'); - // Required types - const TextBatch = $injector.get('TextBatch'); - const config = { restrict : 'E', templateUrl : 'app/player/templates/player.html' @@ -151,7 +149,7 @@ angular.module('player').directive('guacPlayer', ['$injector', function guacPlay /** * Any batches of text typed during the recording. * - * @type {TextBatch[]} + * @type {keyEventDisplayService.TextBatch[]} */ $scope.textBatches = []; @@ -357,11 +355,12 @@ angular.module('player').directive('guacPlayer', ['$injector', function guacPlay $scope.$evalAsync(); }; - // Append any extracted batches of typed text - $scope.recording.ontext = function appendTextBatch(batch) { + // Extract key events from the recording + $scope.recording.onkeyevents = function keyEventsReceived(events) { - // Convert to the display-optimized TextBatch type - $scope.textBatches.push(new TextBatch(batch)); + // Convert to a display-optimized format + $scope.textBatches = ( + keyEventDisplayService.parseEvents(events)); }; diff --git a/guacamole/src/main/frontend/src/app/player/directives/textView.js b/guacamole/src/main/frontend/src/app/player/directives/textView.js index 979cd947d1..6e0ea060c9 100644 --- a/guacamole/src/main/frontend/src/app/player/directives/textView.js +++ b/guacamole/src/main/frontend/src/app/player/directives/textView.js @@ -38,7 +38,7 @@ angular.module('player').directive('guacPlayerTextView', /** * All the batches of text extracted from this recording. * - * @type {!TextBatch[]} + * @type {!keyEventDisplayService.TextBatch[]} */ textBatches : '=', @@ -74,7 +74,7 @@ angular.module('player').directive('guacPlayerTextView', * The text batches that match the current search phrase, or all * batches if no search phrase is set. * - * @type {!TextBatch[]} + * @type {!keyEventDisplayService.TextBatch[]} */ $scope.filteredBatches = $scope.textBatches; @@ -117,8 +117,8 @@ angular.module('player').directive('guacPlayerTextView', }; - // Reapply the filter to the updated text batches - $scope.$watch('textBatches', applyFilter); + // Reapply the current filter to the updated text batches + $scope.$watch('textBatches', () => applyFilter($scope.searchPhrase)); // Reapply the filter whenever the search phrase is updated $scope.$watch('searchPhrase', applyFilter); diff --git a/guacamole/src/main/frontend/src/app/player/services/keyEventDisplayService.js b/guacamole/src/main/frontend/src/app/player/services/keyEventDisplayService.js new file mode 100644 index 0000000000..cbfce14d76 --- /dev/null +++ b/guacamole/src/main/frontend/src/app/player/services/keyEventDisplayService.js @@ -0,0 +1,371 @@ +/* + * 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. + */ + + /* + * NOTE: This session recording player implementation is based on the Session + * Recording Player for Glyptodon Enterprise which is available at + * https://github.com/glyptodon/glyptodon-enterprise-player under the + * following license: + * + * Copyright (C) 2019 Glyptodon, Inc. + * + * 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. + */ + +/* global _ */ + +/** + * A service for translating parsed key events in the format produced by + * KeyEventInterpreter into display-optimized text batches. + */ +angular.module('player').factory('keyEventDisplayService', + ['$injector', function keyEventDisplayService($injector) { + + /** + * A set of all keysyms corresponding to modifier keys. + * @type{Object.} + */ + const MODIFIER_KEYS = { + 0xFE03: true, // AltGr + 0xFFE1: true, // Left Shift + 0xFFE2: true, // Right Shift + 0xFFE3: true, // Left Control + 0xFFE4: true, // Right Control, + 0xFFE7: true, // Left Meta + 0xFFE8: true, // Right Meta + 0xFFE9: true, // Left Alt + 0xFFEA: true, // Right Alt + 0xFFEB: true, // Left Super + 0xFFEC: true, // Right Super + 0xFFED: true, // Left Hyper + 0xFFEE: true // Right Super + }; + + /** + * A set of all keysyms for which the name should be printed alongside the + * value of the key itself. + * @type{Object.} + */ + const PRINT_NAME_TOO_KEYS = { + 0xFF09: true, // Tab + 0xFF0D: true, // Return + 0xFF8D: true, // Enter + }; + + /** + * A set of all keysyms corresponding to keys commonly used in shortcuts. + * @type{Object.} + */ + const SHORTCUT_KEYS = { + 0xFFE3: true, // Left Control + 0xFFE4: true, // Right Control, + 0xFFE7: true, // Left Meta + 0xFFE8: true, // Right Meta + 0xFFE9: true, // Left Alt + 0xFFEA: true, // Right Alt + 0xFFEB: true, // Left Super + 0xFFEC: true, // Right Super + 0xFFED: true, // Left Hyper + 0xFFEE: true // Right Super + } + + /** + * Format and return a key name for display. + * + * @param {*} name + * The name of the key + * + * @returns + * The formatted key name. + */ + const formatKeyName = name => ('<' + name + '>'); + + const service = {}; + + /** + * A batch of text associated with a recording. The batch consists of a + * string representation of the text that would be typed based on the key + * events in the recording, as well as a timestamp when the batch started. + * + * @constructor + * @param {TextBatch|Object} [template={}] + * The object whose properties should be copied within the new TextBatch. + */ + service.TextBatch = function TextBatch(template) { + + // Use empty object by default + template = template || {}; + + /** + * All key events for this batch, some of which may be conslidated, + * representing multiple raw events. + * + * @type {ConsolidatedKeyEvent[]} + */ + this.events = template.events || []; + + /** + * The simplified, human-readable value representing the key events for + * this batch, equivalent to concatenating the `text` field of all key + * events in the batch. + * + * @type {!String} + */ + this.simpleValue = template.simpleValue || ''; + + }; + + /** + * A granular description of an extracted key event or sequence of events. + * It may contain multiple contiguous events of the same type, meaning that all + * event(s) that were combined into this event must have had the same `typed` + * field value. A single timestamp for the first combined event will be used + * for the whole batch if consolidated. + * + * @constructor + * @param {ConsolidatedKeyEvent|Object} [template={}] + * The object whose properties should be copied within the new KeyEventBatch. + */ + service.ConsolidatedKeyEvent = function ConsolidatedKeyEvent(template) { + + // Use empty object by default + template = template || {}; + + /** + * A human-readable representation of the event(s). If a series of printable + * characters was directly typed, this will just be those character(s). + * Otherwise it will be a string describing the event(s). + * + * @type {!String} + */ + this.text = template.text; + + /** + * True if this text of this event is exactly a typed character, or false + * otherwise. + * + * @type {!boolean} + */ + this.typed = template.typed; + + /** + * The timestamp from the recording when this event occured. + * + * @type {!Number} + */ + this.timestamp = template.timestamp; + + }; + + /** + * Accepts key events in the format produced by KeyEventInterpreter and returns + * human readable text batches, seperated by at least `batchSeperation` milliseconds + * if provided. + * + * NOTE: The event processing logic and output format is based on the `guaclog` + * tool, with the addition of batching support. + * + * @param {Guacamole.KeyEventInterpreter.KeyEvent[]} [rawEvents] + * The raw key events to prepare for display. + * + * @param {number} [batchSeperation=5000] + * The minimum number of milliseconds that must elapse between subsequent + * batches of key-event-generated text. If 0 or negative, no splitting will + * occur, resulting in a single batch for all provided key events. + * + * @param {boolean} [consolidateEvents=false] + * Whether consecutive sequences of events with similar properties + * should be consolidated into a single ConsolidatedKeyEvent object for + * display performance reasons. + */ + service.parseEvents = function parseEvents( + rawEvents, batchSeperation, consolidateEvents) { + + // Default to 5 seconds if the batch seperation was not provided + if (batchSeperation === undefined || batchSeperation === null) + batchSeperation = 5000; + /** + * A map of X11 keysyms to a KeyDefinition object, if the corresponding + * key is currently pressed. If a keysym has no entry in this map at all + * it means that the key is not being pressed. Note that not all keysyms + * are necessarily tracked within this map - only those that are + * explicitly tracked. + */ + const pressedKeys = {}; + + // The timestamp of the most recent key event processed + let lastKeyEvent = 0; + + // All text batches produced from the provided raw key events + const batches = [new service.TextBatch()]; + + // Process every provided raw + _.forEach(rawEvents, event => { + + // Extract all fields from the raw event + const { definition, pressed, timestamp } = event; + const { keysym, name, value } = definition; + + // Only switch to a new batch of text if sufficient time has passed + // since the last key event + const newBatch = (batchSeperation >= 0 + && (timestamp - lastKeyEvent) >= batchSeperation); + lastKeyEvent = timestamp; + + if (newBatch) + batches.push(new service.TextBatch()); + + const currentBatch = _.last(batches); + + /** + * Either push the a new event constructed using the provided fields + * into the latest batch, or consolidate into the latest event as + * appropriate given the consolidation configuration and event type. + * + * @param {!String} text + * The text representation of the event. + * + * @param {!Boolean} typed + * Whether the text value would be literally produced by typing + * the key that produced the event. + */ + const pushEvent = (text, typed) => { + const latestEvent = _.last(currentBatch.events); + + // Only consolidate the event if configured to do so and it + // matches the type of the previous event + if (consolidateEvents && latestEvent && latestEvent.typed === typed) { + latestEvent.text += text; + currentBatch.simpleValue += text; + } + + // Otherwise, push a new event + else { + currentBatch.events.push(new service.ConsolidatedKeyEvent({ + text, typed, timestamp})); + currentBatch.simpleValue += text; + } + } + + // Track modifier state + if (MODIFIER_KEYS[keysym]) { + if (pressed) + pressedKeys[keysym] = definition; + else + delete pressedKeys[keysym]; + } + + // Append to the current typed value when a printable + // (non-modifier) key is pressed + else if (pressed) { + + // If any shorcut keys are currently pressed + if (_.some(pressedKeys, (def, key) => SHORTCUT_KEYS[key])) { + + var shortcutText = '<'; + + var firstKey = true; + + // Compose entry by inspecting the state of each tracked key. + // At least one key must be pressed when in a shortcut. + for (let pressedKeysym in pressedKeys) { + + var pressedKeyDefinition = pressedKeys[pressedKeysym]; + + // Print name of key + if (firstKey) { + shortcutText += pressedKeyDefinition.name; + firstKey = false; + } + + else + shortcutText += ('+' + pressedKeyDefinition.name); + + } + + // Finally, append the printable key to close the shortcut + shortcutText += ('+' + name + '>') + + // Add the shortcut to the current batch + pushEvent(shortcutText, false); + } + } + + // Print the key itself + else { + + var keyText; + var typed; + + // Print the value if explicitly defined + if (value !== undefined) { + + keyText = value; + typed = true; + + // If the name should be printed in addition, add it as a + // seperate event before the actual character value + if (PRINT_NAME_TOO_KEYS[keysym]) + pushEvent(formatKeyName(name), false); + + } + + // Otherwise print the name + else { + + keyText = formatKeyName(name); + + // While this is a representation for a single character, + // the key text is the name of the key, not the actual + // character itself + typed = false; + + } + + // Add the key to the current batch + pushEvent(keyText, typed); + + } + + }); + + // All processed batches + return batches; + + }; + + return service; + +}]); diff --git a/guacamole/src/main/frontend/src/app/player/types/TextBatch.js b/guacamole/src/main/frontend/src/app/player/types/TextBatch.js deleted file mode 100644 index 58575e4c13..0000000000 --- a/guacamole/src/main/frontend/src/app/player/types/TextBatch.js +++ /dev/null @@ -1,117 +0,0 @@ -/* - * 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. - */ - -/* global _ */ - -/** - * Service which defines the TextBatch class. - */ -angular.module('player').factory('TextBatch', [function defineTextBatch() { - - /** - * A batch of text associated with a recording. The batch consists of a - * string representation of the text that would be typed based on the key - * events in the recording, as well as a timestamp when the batch started. - * - * @constructor - * @param {Guacamole.KeyEventInterpreter.KeyEvent|TextBatch|Object} [template={}] - * The object whose properties should be copied within the new TextBatch. - */ - const TextBatch = function TextBatch(template) { - - /** - * All key events for this batch, with sequences of key events having - * the same `typed` field value combined. - * - * @type {!KeyEventBatch[]} - */ - this.events = _.reduce(template.events, (consolidatedEvents, rawEvent) => { - - const currentEvent = _.last(consolidatedEvents); - - // If a current event exists with the same `typed` value, conslidate - // the raw text event into it - if (currentEvent && currentEvent.typed === rawEvent.typed) - currentEvent.text += rawEvent.text; - - // Otherwise, create a new conslidated event starting now - else - consolidatedEvents.push(new TextBatch.ConsolidatedKeyEvent(rawEvent)); - - return consolidatedEvents; - - }, []); - - /** - * The simplified, human-readable value representing the key events for - * this batch, equivalent to concatenating the `text` field of all key - * events in the batch. - * - * @type {!String} - */ - this.simpleValue = template.simpleValue || ''; - - }; - - /** - * A granular description of an extracted key event or sequence of events. - * Similar to the Guacamole.KeyEventInterpreter.KeyEvent type, except that - * this KeyEventBatch may contain multiple contiguous events of the same type, - * meaning that all event(s) that were combined into this event must have - * had the same `typed` field value. A single timestamp for the first combined - * event will be used for the whole batch. - * - * @constructor - * @param {Guacamole.KeyEventInterpreter.KeyEventBatch|ConsolidatedKeyEvent|Object} [template={}] - * The object whose properties should be copied within the new KeyEventBatch. - */ - TextBatch.ConsolidatedKeyEvent = function ConsolidatedKeyEvent(template) { - - /** - * A human-readable representation of the event(s). If a series of printable - * characters was directly typed, this will just be those character(s). - * Otherwise it will be a string describing the event(s). - * - * @type {!String} - */ - this.text = template.text; - - /** - * True if this text of this event is exactly a typed character, or false - * otherwise. - * - * @type {!boolean} - */ - this.typed = template.typed; - - /** - * The timestamp from the recording when this event occured. If a - * `startTimestamp` value was provided to the interpreter constructor, this - * will be relative to start of the recording. If not, it will be the raw - * timestamp from the key event. - * - * @type {!Number} - */ - this.timestamp = template.timestamp; - - }; - - return TextBatch; - -}]);