GUACAMOLE-1204: Merge addition of client-side support for multi-touch events.

This commit is contained in:
James Muehlner
2021-02-11 20:54:37 -08:00
committed by GitHub
15 changed files with 1081 additions and 146 deletions

View File

@@ -135,7 +135,29 @@
</goals>
</execution>
</executions>
</plugin>
<!-- Unit test using Jasmin and PhantomJS -->
<plugin>
<groupId>com.github.searls</groupId>
<artifactId>jasmine-maven-plugin</artifactId>
<version>2.2</version>
<executions>
<execution>
<goals>
<goal>test</goal>
</goals>
</execution>
</executions>
<configuration>
<phantomjs>
<version>2.1.1</version>
</phantomjs>
<sourceIncludes>
<sourceInclude>**/*.min.js</sourceInclude>
</sourceIncludes>
<jsSrcDir>${project.build.directory}/${project.build.finalName}</jsSrcDir>
</configuration>
</plugin>
</plugins>

View File

@@ -323,19 +323,33 @@ Guacamole.Client = function(tunnel) {
* Sends a mouse event having the properties provided by the given mouse
* state.
*
* @param {Guacamole.Mouse.State} mouseState The state of the mouse to send
* in the mouse event.
* @param {Guacamole.Mouse.State} mouseState
* The state of the mouse to send in the mouse event.
*
* @param {Boolean} [applyDisplayScale=false]
* Whether the provided mouse state uses local display units, rather
* than remote display units, and should be scaled to match the
* {@link Guacamole.Display}.
*/
this.sendMouseState = function(mouseState) {
this.sendMouseState = function sendMouseState(mouseState, applyDisplayScale) {
// Do not send requests if not connected
if (!isConnected())
return;
var x = mouseState.x;
var y = mouseState.y;
// Translate for display units if requested
if (applyDisplayScale) {
x /= display.getScale();
y /= display.getScale();
}
// Update client-side cursor
display.moveCursor(
Math.floor(mouseState.x),
Math.floor(mouseState.y)
Math.floor(x),
Math.floor(y)
);
// Build mask
@@ -347,7 +361,40 @@ Guacamole.Client = function(tunnel) {
if (mouseState.down) buttonMask |= 16;
// Send message
tunnel.sendMessage("mouse", Math.floor(mouseState.x), Math.floor(mouseState.y), buttonMask);
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);
};
/**
@@ -649,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.
@@ -825,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));
}
};

View File

@@ -0,0 +1,305 @@
/*
* 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 || {};
/**
* An arbitrary event, emitted by a {@link Guacamole.Event.Target}. This object
* should normally serve as the base class for a different object that is more
* specific to the event type.
*
* @constructor
* @param {String} type
* The unique name of this event type.
*/
Guacamole.Event = function Event(type) {
/**
* The unique name of this event type.
*
* @type {String}
*/
this.type = type;
/**
* An arbitrary timestamp in milliseconds, indicating this event's
* position in time relative to other events.
*
* @type {Number}
*/
this.timestamp = new Date().getTime();
/**
* Returns the number of milliseconds elapsed since this event was created.
*
* @return {Number}
* The number of milliseconds elapsed since this event was created.
*/
this.getAge = function getAge() {
return new Date().getTime() - this.timestamp;
};
/**
* Requests that the legacy event handler associated with this event be
* invoked on the given event target. This function will be invoked
* automatically by implementations of {@link Guacamole.Event.Target}
* whenever {@link Guacamole.Event.Target#emit emit()} is invoked.
* <p>
* Older versions of Guacamole relied on single event handlers with the
* prefix "on", such as "onmousedown" or "onkeyup". If a Guacamole.Event
* implementation is replacing the event previously represented by one of
* these handlers, this function gives the implementation the opportunity
* to provide backward compatibility with the old handler.
* <p>
* Unless overridden, this function does nothing.
*
* @param {Guacamole.Event.Target} eventTarget
* The {@link Guacamole.Event.Target} that emitted this event.
*/
this.invokeLegacyHandler = function invokeLegacyHandler(eventTarget) {
// Do nothing
};
};
/**
* A {@link Guacamole.Event} that relates to one or more DOM events. Continued
* propagation and default behavior of the related DOM events may be prevented
* with {@link Guacamole.Event.DOMEvent#stopPropagation stopPropagation()} and
* {@link Guacamole.Event.DOMEvent#preventDefault preventDefault()}
* respectively.
*
* @constructor
* @augments Guacamole.Event
*
* @param {String} type
* The unique name of this event type.
*
* @param {Event[]} events
* The DOM events that are related to this event. Future calls to
* {@link Guacamole.Event.DOMEvent#preventDefault preventDefault()} and
* {@link Guacamole.Event.DOMEvent#stopPropagation stopPropagation()} will
* affect these events.
*/
Guacamole.Event.DOMEvent = function DOMEvent(type, events) {
Guacamole.Event.call(this, type);
/**
* Requests that the default behavior of related DOM events be prevented.
* Whether this request will be honored by the browser depends on the
* nature of those events and the timing of the request.
*/
this.preventDefault = function preventDefault() {
events.forEach(function applyPreventDefault(event) {
if (event.preventDefault) event.preventDefault();
event.returnValue = false;
});
};
/**
* Stops further propagation of related events through the DOM. Only events
* that are directly related to this event will be stopped.
*/
this.stopPropagation = function stopPropagation() {
events.forEach(function applyStopPropagation(event) {
event.stopPropagation();
});
};
};
/**
* An object which can dispatch {@link Guacamole.Event} objects. Listeners
* registered with {@link Guacamole.Event.Target#on on()} will automatically
* be invoked based on the type of {@link Guacamole.Event} passed to
* {@link Guacamole.Event.Target#dispatch dispatch()}. It is normally
* subclasses of Guacamole.Event.Target that will dispatch events, and usages
* of those subclasses that will catch dispatched events with on().
*
* @constructor
*/
Guacamole.Event.Target = function Target() {
/**
* A callback function which handles an event dispatched by an event
* target.
*
* @callback Guacamole.Event.Target~listener
* @param {Guacamole.Event} event
* The event that was dispatched.
*
* @param {Guacamole.Event.Target} target
* The object that dispatched the event.
*/
/**
* All listeners (callback functions) registered for each event type passed
* to {@link Guacamole.Event.Targer#on on()}.
*
* @private
* @type {Object.<String, Guacamole.Event.Target~listener[]>}
*/
var listeners = {};
/**
* Registers a listener for events having the given type, as dictated by
* the {@link Guacamole.Event#type type} property of {@link Guacamole.Event}
* provided to {@link Guacamole.Event.Target#dispatch dispatch()}.
*
* @param {String} type
* The unique name of this event type.
*
* @param {Guacamole.Event.Target~listener} listener
* The function to invoke when an event having the given type is
* dispatched. The {@link Guacamole.Event} object provided to
* {@link Guacamole.Event.Target#dispatch dispatch()} will be passed to
* this function, along with the dispatching Guacamole.Event.Target.
*/
this.on = function on(type, listener) {
var relevantListeners = listeners[type];
if (!relevantListeners)
listeners[type] = relevantListeners = [];
relevantListeners.push(listener);
};
/**
* Registers a listener for events having the given types, as dictated by
* the {@link Guacamole.Event#type type} property of {@link Guacamole.Event}
* provided to {@link Guacamole.Event.Target#dispatch dispatch()}.
* <p>
* Invoking this function is equivalent to manually invoking
* {@link Guacamole.Event.Target#on on()} for each of the provided types.
*
* @param {String[]} types
* The unique names of the event types to associate with the given
* listener.
*
* @param {Guacamole.Event.Target~listener} listener
* The function to invoke when an event having any of the given types
* is dispatched. The {@link Guacamole.Event} object provided to
* {@link Guacamole.Event.Target#dispatch dispatch()} will be passed to
* this function, along with the dispatching Guacamole.Event.Target.
*/
this.onEach = function onEach(types, listener) {
types.forEach(function addListener(type) {
this.on(type, listener);
}, this);
};
/**
* Dispatches the given event, invoking all event handlers registered with
* this Guacamole.Event.Target for that event's
* {@link Guacamole.Event#type type}.
*
* @param {Guacamole.Event} event
* The event to dispatch.
*/
this.dispatch = function dispatch(event) {
// Invoke any relevant legacy handler for the event
event.invokeLegacyHandler(this);
// Invoke all registered listeners
var relevantListeners = listeners[event.type];
if (relevantListeners) {
for (var i = 0; i < relevantListeners.length; i++) {
relevantListeners[i](event, this);
}
}
};
/**
* Unregisters a listener that was previously registered with
* {@link Guacamole.Event.Target#on on()} or
* {@link Guacamole.Event.Target#onEach onEach()}. If no such listener was
* registered, this function has no effect. If multiple copies of the same
* listener were registered, the first listener still registered will be
* removed.
*
* @param {String} type
* The unique name of the event type handled by the listener being
* removed.
*
* @param {Guacamole.Event.Target~listener} listener
* The listener function previously provided to
* {@link Guacamole.Event.Target#on on()}or
* {@link Guacamole.Event.Target#onEach onEach()}.
*
* @returns {Boolean}
* true if the specified listener was removed, false otherwise.
*/
this.off = function off(type, listener) {
var relevantListeners = listeners[type];
if (!relevantListeners)
return false;
for (var i = 0; i < relevantListeners.length; i++) {
if (relevantListeners[i] === listener) {
relevantListeners.splice(i, 1);
return true;
}
}
return false;
};
/**
* Unregisters listeners that were previously registered with
* {@link Guacamole.Event.Target#on on()} or
* {@link Guacamole.Event.Target#onEach onEach()}. If no such listeners
* were registered, this function has no effect. If multiple copies of the
* same listener were registered for the same event type, the first
* listener still registered will be removed.
* <p>
* Invoking this function is equivalent to manually invoking
* {@link Guacamole.Event.Target#off off()} for each of the provided types.
*
* @param {String[]} types
* The unique names of the event types handled by the listeners being
* removed.
*
* @param {Guacamole.Event.Target~listener} listener
* The listener function previously provided to
* {@link Guacamole.Event.Target#on on()} or
* {@link Guacamole.Event.Target#onEach onEach()}.
*
* @returns {Boolean}
* true if any of the specified listeners were removed, false
* otherwise.
*/
this.offEach = function offEach(types, listener) {
var changed = false;
types.forEach(function removeListener(type) {
changed |= this.off(type, listener);
}, this);
return changed;
};
};

View File

@@ -65,10 +65,7 @@ Guacamole.Mouse = function(element) {
*
* @type {Guacamole.Mouse.State}
*/
this.currentState = new Guacamole.Mouse.State(
0, 0,
false, false, false, false, false
);
this.currentState = new Guacamole.Mouse.State();
/**
* Fired whenever the user presses a mouse button down over the element
@@ -395,109 +392,114 @@ Guacamole.Mouse = function(element) {
};
/**
* Simple container for properties describing the state of a mouse.
* The current state of a mouse, including position and buttons.
*
* @constructor
* @param {Number} x The X position of the mouse pointer in pixels.
* @param {Number} y The Y position of the mouse pointer in pixels.
* @param {Boolean} left Whether the left mouse button is pressed.
* @param {Boolean} middle Whether the middle mouse button is pressed.
* @param {Boolean} right Whether the right mouse button is pressed.
* @param {Boolean} up Whether the up mouse button is pressed (the fourth
* button, usually part of a scroll wheel).
* @param {Boolean} down Whether the down mouse button is pressed (the fifth
* button, usually part of a scroll wheel).
* @augments Guacamole.Position
* @param {Guacamole.Mouse.State|Object} [template={}]
* The object whose properties should be copied within the new
* Guacamole.Mouse.State.
*/
Guacamole.Mouse.State = function(x, y, left, middle, right, up, down) {
Guacamole.Mouse.State = function State(template) {
/**
* Reference to this Guacamole.Mouse.State.
* Returns the template object that would be provided to the
* Guacamole.Mouse.State constructor to produce a new Guacamole.Mouse.State
* object with the properties specified. The order and type of arguments
* used by this function are identical to those accepted by the
* Guacamole.Mouse.State constructor of Apache Guacamole 1.3.0 and older.
*
* @private
* @param {Number} x
* The X position of the mouse pointer in pixels.
*
* @param {Number} y
* The Y position of the mouse pointer in pixels.
*
* @param {Boolean} left
* Whether the left mouse button is pressed.
*
* @param {Boolean} middle
* Whether the middle mouse button is pressed.
*
* @param {Boolean} right
* Whether the right mouse button is pressed.
*
* @param {Boolean} up
* Whether the up mouse button is pressed (the fourth button, usually
* part of a scroll wheel).
*
* @param {Boolean} down
* Whether the down mouse button is pressed (the fifth button, usually
* part of a scroll wheel).
*
* @return {Object}
* The equivalent template object that would be passed to the new
* Guacamole.Mouse.State constructor.
*/
var guac_state = this;
var legacyConstructor = function legacyConstructor(x, y, left, middle, right, up, down) {
return {
x : x,
y : y,
left : left,
middle : middle,
right : right,
up : up,
down : down
};
};
/**
* The current X position of the mouse pointer.
* @type {Number}
*/
this.x = x;
// Accept old-style constructor, as well
if (arguments.length > 1)
template = legacyConstructor.apply(this, arguments);
else
template = template || {};
/**
* The current Y position of the mouse pointer.
* @type {Number}
*/
this.y = y;
Guacamole.Position.call(this, template);
/**
* Whether the left mouse button is currently pressed.
*
* @type {Boolean}
* @default false
*/
this.left = left;
this.left = template.left || false;
/**
* Whether the middle mouse button is currently pressed.
*
* @type {Boolean}
* @default false
*/
this.middle = middle;
this.middle = template.middle || false;
/**
* Whether the right mouse button is currently pressed.
*
* @type {Boolean}
* @default false
*/
this.right = right;
this.right = template.right || false;
/**
* Whether the up mouse button is currently pressed. This is the fourth
* mouse button, associated with upward scrolling of the mouse scroll
* wheel.
*
* @type {Boolean}
* @default false
*/
this.up = up;
this.up = template.up || false;
/**
* Whether the down mouse button is currently pressed. This is the fifth
* mouse button, associated with downward scrolling of the mouse scroll
* wheel.
* @type {Boolean}
*/
this.down = down;
/**
* Updates the position represented within this state object by the given
* element and clientX/clientY coordinates (commonly available within event
* objects). Position is translated from clientX/clientY (relative to
* viewport) to element-relative coordinates.
*
* @param {Element} element The element the coordinates should be relative
* to.
* @param {Number} clientX The X coordinate to translate, viewport-relative.
* @param {Number} clientY The Y coordinate to translate, viewport-relative.
* @type {Boolean}
* @default false
*/
this.fromClientPosition = function(element, clientX, clientY) {
guac_state.x = clientX - element.offsetLeft;
guac_state.y = clientY - element.offsetTop;
// This is all JUST so we can get the mouse position within the element
var parent = element.offsetParent;
while (parent && !(parent === document.body)) {
guac_state.x -= parent.offsetLeft - parent.scrollLeft;
guac_state.y -= parent.offsetTop - parent.scrollTop;
parent = parent.offsetParent;
}
// Element ultimately depends on positioning within document body,
// take document scroll into account.
if (parent) {
var documentScrollLeft = document.body.scrollLeft || document.documentElement.scrollLeft;
var documentScrollTop = document.body.scrollTop || document.documentElement.scrollTop;
guac_state.x -= parent.offsetLeft - documentScrollLeft;
guac_state.y -= parent.offsetTop - documentScrollTop;
}
};
this.down = template.down || false;
};
@@ -543,10 +545,7 @@ Guacamole.Mouse.Touchpad = function(element) {
*
* @type {Guacamole.Mouse.State}
*/
this.currentState = new Guacamole.Mouse.State(
0, 0,
false, false, false, false, false
);
this.currentState = new Guacamole.Mouse.State();
/**
* Fired whenever a mouse button is effectively pressed. This can happen
@@ -850,10 +849,7 @@ Guacamole.Mouse.Touchscreen = function(element) {
*
* @type {Guacamole.Mouse.State}
*/
this.currentState = new Guacamole.Mouse.State(
0, 0,
false, false, false, false, false
);
this.currentState = new Guacamole.Mouse.State();
/**
* Fired whenever a mouse button is effectively pressed. This can happen

View File

@@ -0,0 +1,92 @@
/*
* 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 || {};
/**
* A position in 2-D space.
*
* @constructor
* @param {Guacamole.Position|Object} [template={}]
* The object whose properties should be copied within the new
* Guacamole.Position.
*/
Guacamole.Position = function Position(template) {
template = template || {};
/**
* The current X position, in pixels.
*
* @type {Number}
* @default 0
*/
this.x = template.x || 0;
/**
* The current Y position, in pixels.
*
* @type {Number}
* @default 0
*/
this.y = template.y || 0;
/**
* Assigns the position represented by the given element and
* clientX/clientY coordinates. The clientX and clientY coordinates are
* relative to the browser viewport and are commonly available within
* JavaScript event objects. The final position is translated to
* coordinates that are relative the given element.
*
* @param {Element} element
* The element the coordinates should be relative to.
*
* @param {Number} clientX
* The viewport-relative X coordinate to translate.
*
* @param {Number} clientY
* The viewport-relative Y coordinate to translate.
*/
this.fromClientPosition = function fromClientPosition(element, clientX, clientY) {
this.x = clientX - element.offsetLeft;
this.y = clientY - element.offsetTop;
// This is all JUST so we can get the position within the element
var parent = element.offsetParent;
while (parent && !(parent === document.body)) {
this.x -= parent.offsetLeft - parent.scrollLeft;
this.y -= parent.offsetTop - parent.scrollTop;
parent = parent.offsetParent;
}
// Element ultimately depends on positioning within document body,
// take document scroll into account.
if (parent) {
var documentScrollLeft = document.body.scrollLeft || document.documentElement.scrollLeft;
var documentScrollTop = document.body.scrollTop || document.documentElement.scrollTop;
this.x -= parent.offsetLeft - documentScrollLeft;
this.y -= parent.offsetTop - documentScrollTop;
}
};
};

View File

@@ -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.<Number, Guacamole.Touch.State>}
*/
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;
};

View File

@@ -0,0 +1,139 @@
/*
* 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 Guacamole, jasmine, expect */
describe("Guacamole.Event", function EventSpec() {
/**
* Test subclass of {@link Guacamole.Event} which provides a single
* "value" property supports an "ontest" legacy event handler.
*
* @constructor
* @augments Guacamole.Event
*/
var TestEvent = function TestEvent(value) {
Guacamole.Event.apply(this, [ 'test' ]);
/**
* An arbitrary value to expose to the handler of this event.
*
* @type {Object}
*/
this.value = value;
/**
* @inheritdoc
*/
this.invokeLegacyHandler = function invokeLegacyHandler(target) {
if (target.ontest)
target.ontest(value);
};
};
/**
* Event target instance which will receive each fired {@link TestEvent}.
*
* @type {Guacamole.Event.Target}
*/
var eventTarget;
beforeEach(function() {
eventTarget = new Guacamole.Event.Target();
});
describe("when an event is dispatched", function(){
it("should invoke the legacy handler for matching events", function() {
eventTarget.ontest = jasmine.createSpy('ontest');
eventTarget.dispatch(new TestEvent('event1'));
expect(eventTarget.ontest).toHaveBeenCalledWith('event1');
});
it("should invoke all listeners for matching events", function() {
var listener1 = jasmine.createSpy('listener1');
var listener2 = jasmine.createSpy('listener2');
eventTarget.on('test', listener1);
eventTarget.on('test', listener2);
eventTarget.dispatch(new TestEvent('event2'));
expect(listener1).toHaveBeenCalledWith(jasmine.objectContaining({ type : 'test', value : 'event2' }), eventTarget);
expect(listener2).toHaveBeenCalledWith(jasmine.objectContaining({ type : 'test', value : 'event2' }), eventTarget);
});
it("should not invoke any listeners for non-matching events", function() {
var listener1 = jasmine.createSpy('listener1');
var listener2 = jasmine.createSpy('listener2');
eventTarget.on('test2', listener1);
eventTarget.on('test2', listener2);
eventTarget.dispatch(new TestEvent('event3'));
expect(listener1).not.toHaveBeenCalled();
expect(listener2).not.toHaveBeenCalled();
});
it("should not invoke any listeners that have been removed", function() {
var listener1 = jasmine.createSpy('listener1');
var listener2 = jasmine.createSpy('listener2');
eventTarget.on('test', listener1);
eventTarget.on('test', listener2);
eventTarget.off('test', listener1);
eventTarget.dispatch(new TestEvent('event4'));
expect(listener1).not.toHaveBeenCalled();
expect(listener2).toHaveBeenCalledWith(jasmine.objectContaining({ type : 'test', value : 'event4' }), eventTarget);
});
});
describe("when listeners are removed", function(){
it("should return whether a listener is successfully removed", function() {
var listener1 = jasmine.createSpy('listener1');
var listener2 = jasmine.createSpy('listener2');
eventTarget.on('test', listener1);
eventTarget.on('test', listener2);
expect(eventTarget.off('test', listener1)).toBe(true);
expect(eventTarget.off('test', listener1)).toBe(false);
expect(eventTarget.off('test', listener2)).toBe(true);
expect(eventTarget.off('test', listener2)).toBe(false);
});
});
});

View File

@@ -114,6 +114,11 @@
"name" : "timezone",
"type" : "TIMEZONE"
},
{
"name" : "enable-touch",
"type" : "BOOLEAN",
"options" : [ "true" ]
},
{
"name" : "console",
"type" : "BOOLEAN",
@@ -347,6 +352,11 @@
"type" : "BOOLEAN",
"options" : [ "true" ]
},
{
"name" : "recording-exclude-touch",
"type" : "BOOLEAN",
"options" : [ "true" ]
},
{
"name" : "recording-include-keys",
"type" : "BOOLEAN",

View File

@@ -463,6 +463,11 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
// Zoom and pan client via pinch gestures
$scope.clientPinch = function clientPinch(inProgress, startLength, currentLength, centerX, centerY) {
// Do not handle pinch gestures if they would conflict with remote
// handling of similar gestures
if ($scope.client.multiTouchSupport > 1)
return false;
// Do not handle pinch gestures while relative mouse is in use
if (!$scope.client.clientProperties.emulateAbsoluteMouse)
return false;

View File

@@ -119,6 +119,14 @@ angular.module('client').directive('guacClient', [function guacClient() {
*/
var touchPad = new Guacamole.Mouse.Touchpad(displayContainer);
/**
* Guacamole touch event handling object, wrapped around the main
* client dislay.
*
* @type Guacamole.Touch
*/
var touch = new Guacamole.Touch(displayContainer);
/**
* Updates the scale of the attached Guacamole.Client based on current window
* size and "auto-fit" setting.
@@ -185,29 +193,6 @@ angular.module('client').directive('guacClient', [function guacClient() {
};
/**
* Sends the given mouse state to the current client.
*
* @param {Guacamole.Mouse.State} mouseState The mouse state to
* send.
*/
var sendScaledMouseState = function sendScaledMouseState(mouseState) {
// Scale event by current scale
var scaledState = new Guacamole.Mouse.State(
mouseState.x / display.getScale(),
mouseState.y / display.getScale(),
mouseState.left,
mouseState.middle,
mouseState.right,
mouseState.up,
mouseState.down);
// Send mouse event
client.sendMouseState(scaledState);
};
/**
* Handles a mouse event originating from the user's actual mouse.
* This differs from handleEmulatedMouseState() in that the
@@ -226,7 +211,7 @@ angular.module('client').directive('guacClient', [function guacClient() {
// Send mouse state, show cursor if necessary
display.showCursor(!localCursor);
sendScaledMouseState(mouseState);
client.sendMouseState(mouseState, true);
};
@@ -251,7 +236,28 @@ angular.module('client').directive('guacClient', [function guacClient() {
// Send mouse state, ensure cursor is visible
scrollToMouse(mouseState);
sendScaledMouseState(mouseState);
client.sendMouseState(mouseState, true);
};
/**
* Handles a touch event originating from the user's device.
*
* @param {Guacamole.Touch.Event} touchEvent
* The touch event.
*/
var handleTouchEvent = function handleTouchEvent(event) {
// Do not attempt to handle touch state changes if the client
// or display are not yet available
if (!client || !display)
return;
event.preventDefault();
// Send touch state, hiding local cursor
display.showCursor(false);
client.sendTouchState(event.state, true);
};
@@ -310,39 +316,42 @@ angular.module('client').directive('guacClient', [function guacClient() {
localCursor = mouse.setCursor(cursor.canvas, cursor.x, cursor.y);
});
// Swap mouse emulation modes depending on absolute mode flag
$scope.$watch('client.clientProperties.emulateAbsoluteMouse',
function mouseEmulationModeChanged(emulateAbsoluteMouse) {
// Update touch event handling depending on remote multi-touch
// support and mouse emulation mode
$scope.$watchGroup([
'client.multiTouchSupport',
'client.clientProperties.emulateAbsoluteMouse'
], function touchBehaviorChanged(emulateAbsoluteMouse) {
var newMode, oldMode;
// Clear existing event handling
touch.offEach(['touchstart', 'touchmove', 'touchend'], handleTouchEvent);
// Switch to touchscreen if absolute
if (emulateAbsoluteMouse) {
newMode = touchScreen;
oldMode = touchPad;
touchScreen.onmousedown =
touchScreen.onmouseup =
touchScreen.onmousemove = null;
touchPad.onmousedown =
touchPad.onmouseup =
touchPad.onmousemove = null;
// Directly forward local touch events
if ($scope.client.multiTouchSupport)
touch.onEach(['touchstart', 'touchmove', 'touchend'], handleTouchEvent);
// Switch to touchscreen if mouse emulation is required and
// absolute mouse emulation is preferred
else if ($scope.client.clientProperties.emulateAbsoluteMouse) {
touchScreen.onmousedown =
touchScreen.onmouseup =
touchScreen.onmousemove = handleEmulatedMouseState;
}
// Switch to touchpad if not absolute (relative)
// Use touchpad for mouse emulation if absolute mouse emulation
// is not preferred
else {
newMode = touchPad;
oldMode = touchScreen;
}
// Set applicable mouse emulation object, unset the old one
if (newMode) {
// Clear old handlers and copy state to new emulation mode
if (oldMode) {
oldMode.onmousedown = oldMode.onmouseup = oldMode.onmousemove = null;
newMode.currentState.x = oldMode.currentState.x;
newMode.currentState.y = oldMode.currentState.y;
}
// Handle emulated events only from the new emulation mode
newMode.onmousedown =
newMode.onmouseup =
newMode.onmousemove = handleEmulatedMouseState;
touchPad.onmousedown =
touchPad.onmouseup =
touchPad.onmousemove = handleEmulatedMouseState;
}
});

View File

@@ -155,7 +155,7 @@
</div>
<!-- Mouse mode -->
<div class="menu-section" id="mouse-settings">
<div class="menu-section" id="mouse-settings" ng-hide="client.multiTouchSupport">
<h3>{{'CLIENT.SECTION_HEADER_MOUSE_MODE' | translate}}</h3>
<div class="content">
<p class="description">{{'CLIENT.HELP_MOUSE_MODE' | translate}}</p>

View File

@@ -203,6 +203,15 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector',
*/
this.shareLinks = template.shareLinks || {};
/**
* The number of simultaneous touch contacts supported by the remote
* desktop. Unless explicitly declared otherwise by the remote desktop
* after connecting, this will be 0 (multi-touch unsupported).
*
* @type Number
*/
this.multiTouchSupport = template.multiTouchSupport || 0;
/**
* The current state of the Guacamole client (idle, connecting,
* connected, terminated with error, etc.).
@@ -578,6 +587,11 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector',
};
// Update level of multi-touch support when known
client.onmultitouch = function multiTouchSupportDeclared(layer, touches) {
managedClient.multiTouchSupport = touches;
};
// Update title when a "name" instruction is received
client.onname = function clientNameReceived(name) {
$rootScope.$apply(function updateClientTitle() {

View File

@@ -124,8 +124,6 @@ angular.module('touch').directive('guacTouchDrag', [function guacTouchDrag() {
element.addEventListener("touchmove", function dragTouchMove(e) {
if (e.touches.length === 1) {
e.stopPropagation();
// Get touch location
var x = e.touches[0].clientX;
var y = e.touches[0].clientY;
@@ -163,8 +161,6 @@ angular.module('touch').directive('guacTouchDrag', [function guacTouchDrag() {
if (startX && startY && e.touches.length === 0) {
e.stopPropagation();
// Signal end of drag gesture
if (inProgress && guacTouchDrag) {
$scope.$apply(function dragComplete() {

View File

@@ -159,8 +159,6 @@ angular.module('touch').directive('guacTouchPinch', [function guacTouchPinch() {
element.addEventListener("touchmove", function pinchTouchMove(e) {
if (e.touches.length === 2) {
e.stopPropagation();
// Calculate current zoom level
currentLength = pinchDistance(e);
@@ -188,8 +186,6 @@ angular.module('touch').directive('guacTouchPinch', [function guacTouchPinch() {
if (startLength && e.touches.length < 2) {
e.stopPropagation();
// Notify of pinch end
if (guacTouchPinch) {
$scope.$apply(function pinchComplete() {

View File

@@ -480,6 +480,7 @@
"FIELD_HEADER_ENABLE_PRINTING" : "Enable printing:",
"FIELD_HEADER_ENABLE_SFTP" : "Enable SFTP:",
"FIELD_HEADER_ENABLE_THEMING" : "Enable theming:",
"FIELD_HEADER_ENABLE_TOUCH" : "Enable multi-touch:",
"FIELD_HEADER_ENABLE_WALLPAPER" : "Enable wallpaper:",
"FIELD_HEADER_GATEWAY_DOMAIN" : "Domain:",
"FIELD_HEADER_GATEWAY_HOSTNAME" : "Hostname:",
@@ -499,6 +500,7 @@
"FIELD_HEADER_READ_ONLY" : "Read-only:",
"FIELD_HEADER_RECORDING_EXCLUDE_MOUSE" : "Exclude mouse:",
"FIELD_HEADER_RECORDING_EXCLUDE_OUTPUT" : "Exclude graphics/streams:",
"FIELD_HEADER_RECORDING_EXCLUDE_TOUCH" : "Exclude touch events:",
"FIELD_HEADER_RECORDING_INCLUDE_KEYS" : "Include key events:",
"FIELD_HEADER_RECORDING_NAME" : "Recording name:",
"FIELD_HEADER_RECORDING_PATH" : "Recording path:",