mirror of
https://github.com/gyurix1968/guacamole-client.git
synced 2025-09-07 21:51:23 +00:00
GUACAMOLE-1204: Merge addition of client-side support for multi-touch events.
This commit is contained in:
@@ -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>
|
||||
|
@@ -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));
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
305
guacamole-common-js/src/main/webapp/modules/Event.js
Normal file
305
guacamole-common-js/src/main/webapp/modules/Event.js
Normal 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;
|
||||
|
||||
};
|
||||
|
||||
};
|
@@ -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
|
||||
|
92
guacamole-common-js/src/main/webapp/modules/Position.js
Normal file
92
guacamole-common-js/src/main/webapp/modules/Position.js
Normal 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;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
};
|
280
guacamole-common-js/src/main/webapp/modules/Touch.js
Normal file
280
guacamole-common-js/src/main/webapp/modules/Touch.js
Normal 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;
|
||||
|
||||
};
|
139
guacamole-common-js/src/test/javascript/EventSpec.js
Normal file
139
guacamole-common-js/src/test/javascript/EventSpec.js
Normal 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);
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
});
|
@@ -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",
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
});
|
||||
|
@@ -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>
|
||||
|
@@ -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() {
|
||||
|
@@ -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() {
|
||||
|
@@ -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() {
|
||||
|
@@ -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:",
|
||||
|
Reference in New Issue
Block a user