diff --git a/guacamole-common-js/pom.xml b/guacamole-common-js/pom.xml
index e38ea1c39..9025e9a95 100644
--- a/guacamole-common-js/pom.xml
+++ b/guacamole-common-js/pom.xml
@@ -135,7 +135,29 @@
+
+
+
+ com.github.searls
+ jasmine-maven-plugin
+ 2.2
+
+
+
+ test
+
+
+
+
+
+ 2.1.1
+
+
+ **/*.min.js
+
+ ${project.build.directory}/${project.build.finalName}
+
diff --git a/guacamole-common-js/src/main/webapp/modules/Client.js b/guacamole-common-js/src/main/webapp/modules/Client.js
index 18d8412a6..9ce54e329 100644
--- a/guacamole-common-js/src/main/webapp/modules/Client.js
+++ b/guacamole-common-js/src/main/webapp/modules/Client.js
@@ -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));
+
}
};
diff --git a/guacamole-common-js/src/main/webapp/modules/Event.js b/guacamole-common-js/src/main/webapp/modules/Event.js
new file mode 100644
index 000000000..778d90736
--- /dev/null
+++ b/guacamole-common-js/src/main/webapp/modules/Event.js
@@ -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.
+ *
+ * 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.
+ *
+ * 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.}
+ */
+ 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()}.
+ *
+ * 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.
+ *
+ * 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;
+
+ };
+
+};
diff --git a/guacamole-common-js/src/main/webapp/modules/Mouse.js b/guacamole-common-js/src/main/webapp/modules/Mouse.js
index 48f81f010..b714ee69d 100644
--- a/guacamole-common-js/src/main/webapp/modules/Mouse.js
+++ b/guacamole-common-js/src/main/webapp/modules/Mouse.js
@@ -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}
+ * @default false
*/
- 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.
- */
- 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
diff --git a/guacamole-common-js/src/main/webapp/modules/Position.js b/guacamole-common-js/src/main/webapp/modules/Position.js
new file mode 100644
index 000000000..faaeca343
--- /dev/null
+++ b/guacamole-common-js/src/main/webapp/modules/Position.js
@@ -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;
+ }
+
+ };
+
+};
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;
+
+};
diff --git a/guacamole-common-js/src/test/javascript/EventSpec.js b/guacamole-common-js/src/test/javascript/EventSpec.js
new file mode 100644
index 000000000..9f94d6fa1
--- /dev/null
+++ b/guacamole-common-js/src/test/javascript/EventSpec.js
@@ -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);
+
+ });
+
+ });
+});
diff --git a/guacamole-ext/src/main/resources/org/apache/guacamole/protocols/rdp.json b/guacamole-ext/src/main/resources/org/apache/guacamole/protocols/rdp.json
index 048122741..915b9c8c1 100644
--- a/guacamole-ext/src/main/resources/org/apache/guacamole/protocols/rdp.json
+++ b/guacamole-ext/src/main/resources/org/apache/guacamole/protocols/rdp.json
@@ -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",
diff --git a/guacamole/src/main/webapp/app/client/controllers/clientController.js b/guacamole/src/main/webapp/app/client/controllers/clientController.js
index 451adeeb9..782167614 100644
--- a/guacamole/src/main/webapp/app/client/controllers/clientController.js
+++ b/guacamole/src/main/webapp/app/client/controllers/clientController.js
@@ -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;
diff --git a/guacamole/src/main/webapp/app/client/directives/guacClient.js b/guacamole/src/main/webapp/app/client/directives/guacClient.js
index cc8829657..6911609dd 100644
--- a/guacamole/src/main/webapp/app/client/directives/guacClient.js
+++ b/guacamole/src/main/webapp/app/client/directives/guacClient.js
@@ -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;
}
});
diff --git a/guacamole/src/main/webapp/app/client/templates/client.html b/guacamole/src/main/webapp/app/client/templates/client.html
index 1270efc38..20d711ee6 100644
--- a/guacamole/src/main/webapp/app/client/templates/client.html
+++ b/guacamole/src/main/webapp/app/client/templates/client.html
@@ -155,7 +155,7 @@
-