mirror of
https://github.com/gyurix1968/guacamole-client.git
synced 2025-09-05 20:57:40 +00:00
GUACAMOLE-1820: Refactor key event interpretation, moving display-specific code to the webapp.
This commit is contained in:
@@ -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));
|
||||
|
||||
};
|
||||
|
||||
|
@@ -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);
|
||||
|
@@ -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.<Number, Boolean>}
|
||||
*/
|
||||
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.<Number, Boolean>}
|
||||
*/
|
||||
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.<Number, Boolean>}
|
||||
*/
|
||||
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;
|
||||
|
||||
}]);
|
@@ -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;
|
||||
|
||||
}]);
|
Reference in New Issue
Block a user