From 23e909d2fc7904eefeafb533b2eb63f0eaf06ce5 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Sun, 7 Feb 2021 00:40:44 -0800 Subject: [PATCH] GUACAMOLE-1204: Add support for multi-touch events. --- .../src/main/webapp/modules/Client.js | 55 ++++ .../src/main/webapp/modules/Touch.js | 280 ++++++++++++++++++ 2 files changed, 335 insertions(+) create mode 100644 guacamole-common-js/src/main/webapp/modules/Touch.js diff --git a/guacamole-common-js/src/main/webapp/modules/Client.js b/guacamole-common-js/src/main/webapp/modules/Client.js index 3c1c239df..9ce54e329 100644 --- a/guacamole-common-js/src/main/webapp/modules/Client.js +++ b/guacamole-common-js/src/main/webapp/modules/Client.js @@ -364,6 +364,39 @@ Guacamole.Client = function(tunnel) { tunnel.sendMessage("mouse", Math.floor(x), Math.floor(y), buttonMask); }; + /** + * Sends a touch event having the properties provided by the given touch + * state. + * + * @param {Guacamole.Touch.State} touchState + * The state of the touch contact to send in the touch event. + * + * @param {Boolean} [applyDisplayScale=false] + * Whether the provided touch state uses local display units, rather + * than remote display units, and should be scaled to match the + * {@link Guacamole.Display}. + */ + this.sendTouchState = function sendTouchState(touchState, applyDisplayScale) { + + // Do not send requests if not connected + if (!isConnected()) + return; + + var x = touchState.x; + var y = touchState.y; + + // Translate for display units if requested + if (applyDisplayScale) { + x /= display.getScale(); + y /= display.getScale(); + } + + tunnel.sendMessage('touch', touchState.id, Math.floor(x), Math.floor(y), + Math.floor(touchState.radiusX), Math.floor(touchState.radiusY), + touchState.angle, touchState.force); + + }; + /** * Allocates an available stream index and creates a new * Guacamole.OutputStream using that index, associating the resulting @@ -663,6 +696,20 @@ Guacamole.Client = function(tunnel) { */ this.onvideo = null; + /** + * Fired when the remote client is explicitly declaring the level of + * multi-touch support provided by a particular display layer. + * + * @event + * @param {Guacamole.Display.VisibleLayer} layer + * The layer whose multi-touch support level is being declared. + * + * @param {Number} touches + * The maximum number of simultaneous touches supported by the given + * layer, where 0 indicates that touch events are not supported at all. + */ + this.onmultitouch = null; + /** * Fired when the current value of a connection parameter is being exposed * by the server. @@ -839,6 +886,14 @@ Guacamole.Client = function(tunnel) { "miter-limit": function(layer, value) { display.setMiterLimit(layer, parseFloat(value)); + }, + + "multi-touch" : function layerSupportsMultiTouch(layer, value) { + + // Process "multi-touch" property only for true visible layers (not off-screen buffers) + if (guac_client.onmultitouch && layer instanceof Guacamole.Display.VisibleLayer) + guac_client.onmultitouch(layer, parseInt(value)); + } }; diff --git a/guacamole-common-js/src/main/webapp/modules/Touch.js b/guacamole-common-js/src/main/webapp/modules/Touch.js new file mode 100644 index 000000000..dd57789c3 --- /dev/null +++ b/guacamole-common-js/src/main/webapp/modules/Touch.js @@ -0,0 +1,280 @@ +/* + * 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. + */ + +var Guacamole = Guacamole || {}; + +/** + * Provides cross-browser multi-touch events for a given element. The events of + * the given element are automatically populated with handlers that translate + * touch events into a non-browser-specific event provided by the + * Guacamole.Touch instance. + * + * @constructor + * @augments Guacamole.Event.Target + * @param {Element} element + * The Element to use to provide touch events. + */ +Guacamole.Touch = function Touch(element) { + + Guacamole.Event.Target.call(this); + + /** + * Reference to this Guacamole.Touch. + * + * @private + * @type {Guacamole.Touch} + */ + var guacTouch = this; + + /** + * The default X/Y radius of each touch if the device or browser does not + * expose the size of the contact area. + * + * @private + * @constant + * @type {Number} + */ + var DEFAULT_CONTACT_RADIUS = Math.floor(16 * window.devicePixelRatio); + + /** + * The set of all active touches, stored by their unique identifiers. + * + * @type {Object.} + */ + this.touches = {}; + + /** + * The number of active touches currently stored within + * {@link Guacamole.Touch#touches touches}. + */ + this.activeTouches = 0; + + /** + * Fired whenever a new touch contact is initiated on the element + * associated with this Guacamole.Touch. + * + * @event Guacamole.Touch#touchstart + * @param {Guacamole.Touch.Event} event + * A {@link Guacamole.Touch.Event} object representing the "touchstart" + * event. + */ + + /** + * Fired whenever an established touch contact moves within the element + * associated with this Guacamole.Touch. + * + * @event Guacamole.Touch#touchmove + * @param {Guacamole.Touch.Event} event + * A {@link Guacamole.Touch.Event} object representing the "touchmove" + * event. + */ + + /** + * Fired whenever an established touch contact is lifted from the element + * associated with this Guacamole.Touch. + * + * @event Guacamole.Touch#touchend + * @param {Guacamole.Touch.Event} event + * A {@link Guacamole.Touch.Event} object representing the "touchend" + * event. + */ + + element.addEventListener('touchstart', function touchstart(e) { + + // Fire "ontouchstart" events for all new touches + for (var i = 0; i < e.changedTouches.length; i++) { + + var changedTouch = e.changedTouches[i]; + var identifier = changedTouch.identifier; + + // Ignore duplicated touches + if (guacTouch.touches[identifier]) + continue; + + var touch = guacTouch.touches[identifier] = new Guacamole.Touch.State({ + id : identifier, + radiusX : changedTouch.radiusX || DEFAULT_CONTACT_RADIUS, + radiusY : changedTouch.radiusY || DEFAULT_CONTACT_RADIUS, + angle : changedTouch.angle || 0.0, + force : changedTouch.force || 1.0 /* Within JavaScript changedTouch events, a force of 0.0 indicates the device does not support reporting changedTouch force */ + }); + + guacTouch.activeTouches++; + + touch.fromClientPosition(element, changedTouch.clientX, changedTouch.clientY); + guacTouch.dispatch(new Guacamole.Touch.Event('touchmove', e, touch)); + + } + + }, false); + + element.addEventListener('touchmove', function touchstart(e) { + + // Fire "ontouchmove" events for all updated touches + for (var i = 0; i < e.changedTouches.length; i++) { + + var changedTouch = e.changedTouches[i]; + var identifier = changedTouch.identifier; + + // Ignore any unrecognized touches + var touch = guacTouch.touches[identifier]; + if (!touch) + continue; + + // Update force only if supported by browser (otherwise, assume + // force is unchanged) + if (changedTouch.force) + touch.force = changedTouch.force; + + // Update touch area, if supported by browser and device + touch.angle = changedTouch.angle || 0.0; + touch.radiusX = changedTouch.radiusX || DEFAULT_CONTACT_RADIUS; + touch.radiusY = changedTouch.radiusY || DEFAULT_CONTACT_RADIUS; + + // Update with any change in position + touch.fromClientPosition(element, changedTouch.clientX, changedTouch.clientY); + guacTouch.dispatch(new Guacamole.Touch.Event('touchmove', e, touch)); + + } + + }, false); + + element.addEventListener('touchend', function touchstart(e) { + + // Fire "ontouchend" events for all updated touches + for (var i = 0; i < e.changedTouches.length; i++) { + + var changedTouch = e.changedTouches[i]; + var identifier = changedTouch.identifier; + + // Ignore any unrecognized touches + var touch = guacTouch.touches[identifier]; + if (!touch) + continue; + + // Stop tracking this particular touch + delete guacTouch.touches[identifier]; + guacTouch.activeTouches--; + + // Touch has ended + touch.force = 0.0; + + // Update with final position + touch.fromClientPosition(element, changedTouch.clientX, changedTouch.clientY); + guacTouch.dispatch(new Guacamole.Touch.Event('touchend', e, touch)); + + } + + }, false); + +}; + +/** + * The current state of a touch contact. + * + * @constructor + * @augments Guacamole.Position + * @param {Guacamole.Touch.State|Object} [template={}] + * The object whose properties should be copied within the new + * Guacamole.Touch.State. + */ +Guacamole.Touch.State = function State(template) { + + template = template || {}; + + Guacamole.Position.call(this, template); + + /** + * An arbitrary integer ID which uniquely identifies this contact relative + * to other active contacts. + * + * @type {Number} + * @default 0 + */ + this.id = template.id || 0; + + /** + * The Y radius of the ellipse covering the general area of the touch + * contact, in pixels. + * + * @type {Number} + * @default 0 + */ + this.radiusX = template.radiusX || 0; + + /** + * The X radius of the ellipse covering the general area of the touch + * contact, in pixels. + * + * @type {Number} + * @default 0 + */ + this.radiusY = template.radiusY || 0; + + /** + * The rough angle of clockwise rotation of the general area of the touch + * contact, in degrees. + * + * @type {Number} + * @default 0.0 + */ + this.angle = template.angle || 0.0; + + /** + * The relative force exerted by the touch contact, where 0 is no force + * (the touch has been lifted) and 1 is maximum force (the maximum amount + * of force representable by the device). + * + * @type {Number} + * @default 1.0 + */ + this.force = template.force || 1.0; + +}; + +/** + * An event which represents a change in state of a single touch contact, + * including the creation or removal of that contact. If multiple contacts are + * involved in a touch interaction, each contact will be associated with its + * own event. + * + * @constructor + * @augments Guacamole.Event.DOMEvent + * @param {String} type + * The name of the touch event type. Possible values are "touchstart", + * "touchmove", and "touchend". + * + * @param {TouchEvent} event + * The DOM touch event that produced this Guacamole.Touch.Event. + * + * @param {Guacamole.Touch.State} state + * The state of the touch contact associated with this event. + */ +Guacamole.Touch.Event = function TouchEvent(type, event, state) { + + Guacamole.Event.DOMEvent.call(this, type, [ event ]); + + /** + * The state of the touch contact associated with this event. + * + * @type{Guacamole.Touch.State} + */ + this.state = state; + +};