mirror of
https://github.com/gyurix1968/guacamole-client.git
synced 2025-09-08 06:01:22 +00:00
GUACAMOLE-1204: Merge addition of client-side support for multi-touch events.
This commit is contained in:
@@ -135,7 +135,29 @@
|
|||||||
</goals>
|
</goals>
|
||||||
</execution>
|
</execution>
|
||||||
</executions>
|
</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>
|
</plugin>
|
||||||
|
|
||||||
</plugins>
|
</plugins>
|
||||||
|
@@ -323,19 +323,33 @@ Guacamole.Client = function(tunnel) {
|
|||||||
* Sends a mouse event having the properties provided by the given mouse
|
* Sends a mouse event having the properties provided by the given mouse
|
||||||
* state.
|
* state.
|
||||||
*
|
*
|
||||||
* @param {Guacamole.Mouse.State} mouseState The state of the mouse to send
|
* @param {Guacamole.Mouse.State} mouseState
|
||||||
* in the mouse event.
|
* 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
|
// Do not send requests if not connected
|
||||||
if (!isConnected())
|
if (!isConnected())
|
||||||
return;
|
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
|
// Update client-side cursor
|
||||||
display.moveCursor(
|
display.moveCursor(
|
||||||
Math.floor(mouseState.x),
|
Math.floor(x),
|
||||||
Math.floor(mouseState.y)
|
Math.floor(y)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Build mask
|
// Build mask
|
||||||
@@ -347,7 +361,40 @@ Guacamole.Client = function(tunnel) {
|
|||||||
if (mouseState.down) buttonMask |= 16;
|
if (mouseState.down) buttonMask |= 16;
|
||||||
|
|
||||||
// Send message
|
// 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;
|
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
|
* Fired when the current value of a connection parameter is being exposed
|
||||||
* by the server.
|
* by the server.
|
||||||
@@ -825,6 +886,14 @@ Guacamole.Client = function(tunnel) {
|
|||||||
|
|
||||||
"miter-limit": function(layer, value) {
|
"miter-limit": function(layer, value) {
|
||||||
display.setMiterLimit(layer, parseFloat(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}
|
* @type {Guacamole.Mouse.State}
|
||||||
*/
|
*/
|
||||||
this.currentState = new Guacamole.Mouse.State(
|
this.currentState = new Guacamole.Mouse.State();
|
||||||
0, 0,
|
|
||||||
false, false, false, false, false
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fired whenever the user presses a mouse button down over the element
|
* 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
|
* @constructor
|
||||||
* @param {Number} x The X position of the mouse pointer in pixels.
|
* @augments Guacamole.Position
|
||||||
* @param {Number} y The Y position of the mouse pointer in pixels.
|
* @param {Guacamole.Mouse.State|Object} [template={}]
|
||||||
* @param {Boolean} left Whether the left mouse button is pressed.
|
* The object whose properties should be copied within the new
|
||||||
* @param {Boolean} middle Whether the middle mouse button is pressed.
|
* Guacamole.Mouse.State.
|
||||||
* @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).
|
|
||||||
*/
|
*/
|
||||||
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
|
* @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
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
// Accept old-style constructor, as well
|
||||||
* The current X position of the mouse pointer.
|
if (arguments.length > 1)
|
||||||
* @type {Number}
|
template = legacyConstructor.apply(this, arguments);
|
||||||
*/
|
else
|
||||||
this.x = x;
|
template = template || {};
|
||||||
|
|
||||||
/**
|
Guacamole.Position.call(this, template);
|
||||||
* The current Y position of the mouse pointer.
|
|
||||||
* @type {Number}
|
|
||||||
*/
|
|
||||||
this.y = y;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether the left mouse button is currently pressed.
|
* Whether the left mouse button is currently pressed.
|
||||||
|
*
|
||||||
* @type {Boolean}
|
* @type {Boolean}
|
||||||
|
* @default false
|
||||||
*/
|
*/
|
||||||
this.left = left;
|
this.left = template.left || false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether the middle mouse button is currently pressed.
|
* Whether the middle mouse button is currently pressed.
|
||||||
|
*
|
||||||
* @type {Boolean}
|
* @type {Boolean}
|
||||||
|
* @default false
|
||||||
*/
|
*/
|
||||||
this.middle = middle;
|
this.middle = template.middle || false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether the right mouse button is currently pressed.
|
* Whether the right mouse button is currently pressed.
|
||||||
|
*
|
||||||
* @type {Boolean}
|
* @type {Boolean}
|
||||||
|
* @default false
|
||||||
*/
|
*/
|
||||||
this.right = right;
|
this.right = template.right || false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether the up mouse button is currently pressed. This is the fourth
|
* Whether the up mouse button is currently pressed. This is the fourth
|
||||||
* mouse button, associated with upward scrolling of the mouse scroll
|
* mouse button, associated with upward scrolling of the mouse scroll
|
||||||
* wheel.
|
* wheel.
|
||||||
|
*
|
||||||
* @type {Boolean}
|
* @type {Boolean}
|
||||||
|
* @default false
|
||||||
*/
|
*/
|
||||||
this.up = up;
|
this.up = template.up || false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether the down mouse button is currently pressed. This is the fifth
|
* Whether the down mouse button is currently pressed. This is the fifth
|
||||||
* mouse button, associated with downward scrolling of the mouse scroll
|
* mouse button, associated with downward scrolling of the mouse scroll
|
||||||
* wheel.
|
* wheel.
|
||||||
|
*
|
||||||
* @type {Boolean}
|
* @type {Boolean}
|
||||||
|
* @default false
|
||||||
*/
|
*/
|
||||||
this.down = down;
|
this.down = template.down || false;
|
||||||
|
|
||||||
/**
|
|
||||||
* 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -543,10 +545,7 @@ Guacamole.Mouse.Touchpad = function(element) {
|
|||||||
*
|
*
|
||||||
* @type {Guacamole.Mouse.State}
|
* @type {Guacamole.Mouse.State}
|
||||||
*/
|
*/
|
||||||
this.currentState = new Guacamole.Mouse.State(
|
this.currentState = new Guacamole.Mouse.State();
|
||||||
0, 0,
|
|
||||||
false, false, false, false, false
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fired whenever a mouse button is effectively pressed. This can happen
|
* Fired whenever a mouse button is effectively pressed. This can happen
|
||||||
@@ -850,10 +849,7 @@ Guacamole.Mouse.Touchscreen = function(element) {
|
|||||||
*
|
*
|
||||||
* @type {Guacamole.Mouse.State}
|
* @type {Guacamole.Mouse.State}
|
||||||
*/
|
*/
|
||||||
this.currentState = new Guacamole.Mouse.State(
|
this.currentState = new Guacamole.Mouse.State();
|
||||||
0, 0,
|
|
||||||
false, false, false, false, false
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fired whenever a mouse button is effectively pressed. This can happen
|
* 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",
|
"name" : "timezone",
|
||||||
"type" : "TIMEZONE"
|
"type" : "TIMEZONE"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name" : "enable-touch",
|
||||||
|
"type" : "BOOLEAN",
|
||||||
|
"options" : [ "true" ]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name" : "console",
|
"name" : "console",
|
||||||
"type" : "BOOLEAN",
|
"type" : "BOOLEAN",
|
||||||
@@ -347,6 +352,11 @@
|
|||||||
"type" : "BOOLEAN",
|
"type" : "BOOLEAN",
|
||||||
"options" : [ "true" ]
|
"options" : [ "true" ]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name" : "recording-exclude-touch",
|
||||||
|
"type" : "BOOLEAN",
|
||||||
|
"options" : [ "true" ]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name" : "recording-include-keys",
|
"name" : "recording-include-keys",
|
||||||
"type" : "BOOLEAN",
|
"type" : "BOOLEAN",
|
||||||
|
@@ -463,6 +463,11 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
|
|||||||
// Zoom and pan client via pinch gestures
|
// Zoom and pan client via pinch gestures
|
||||||
$scope.clientPinch = function clientPinch(inProgress, startLength, currentLength, centerX, centerY) {
|
$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
|
// Do not handle pinch gestures while relative mouse is in use
|
||||||
if (!$scope.client.clientProperties.emulateAbsoluteMouse)
|
if (!$scope.client.clientProperties.emulateAbsoluteMouse)
|
||||||
return false;
|
return false;
|
||||||
|
@@ -119,6 +119,14 @@ angular.module('client').directive('guacClient', [function guacClient() {
|
|||||||
*/
|
*/
|
||||||
var touchPad = new Guacamole.Mouse.Touchpad(displayContainer);
|
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
|
* Updates the scale of the attached Guacamole.Client based on current window
|
||||||
* size and "auto-fit" setting.
|
* 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.
|
* Handles a mouse event originating from the user's actual mouse.
|
||||||
* This differs from handleEmulatedMouseState() in that the
|
* 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
|
// Send mouse state, show cursor if necessary
|
||||||
display.showCursor(!localCursor);
|
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
|
// Send mouse state, ensure cursor is visible
|
||||||
scrollToMouse(mouseState);
|
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);
|
localCursor = mouse.setCursor(cursor.canvas, cursor.x, cursor.y);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Swap mouse emulation modes depending on absolute mode flag
|
// Update touch event handling depending on remote multi-touch
|
||||||
$scope.$watch('client.clientProperties.emulateAbsoluteMouse',
|
// support and mouse emulation mode
|
||||||
function mouseEmulationModeChanged(emulateAbsoluteMouse) {
|
$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
|
touchScreen.onmousedown =
|
||||||
if (emulateAbsoluteMouse) {
|
touchScreen.onmouseup =
|
||||||
newMode = touchScreen;
|
touchScreen.onmousemove = null;
|
||||||
oldMode = touchPad;
|
|
||||||
|
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 {
|
else {
|
||||||
newMode = touchPad;
|
touchPad.onmousedown =
|
||||||
oldMode = touchScreen;
|
touchPad.onmouseup =
|
||||||
}
|
touchPad.onmousemove = handleEmulatedMouseState;
|
||||||
|
|
||||||
// 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;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@@ -155,7 +155,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Mouse mode -->
|
<!-- 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>
|
<h3>{{'CLIENT.SECTION_HEADER_MOUSE_MODE' | translate}}</h3>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<p class="description">{{'CLIENT.HELP_MOUSE_MODE' | translate}}</p>
|
<p class="description">{{'CLIENT.HELP_MOUSE_MODE' | translate}}</p>
|
||||||
|
@@ -203,6 +203,15 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector',
|
|||||||
*/
|
*/
|
||||||
this.shareLinks = template.shareLinks || {};
|
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,
|
* The current state of the Guacamole client (idle, connecting,
|
||||||
* connected, terminated with error, etc.).
|
* 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
|
// Update title when a "name" instruction is received
|
||||||
client.onname = function clientNameReceived(name) {
|
client.onname = function clientNameReceived(name) {
|
||||||
$rootScope.$apply(function updateClientTitle() {
|
$rootScope.$apply(function updateClientTitle() {
|
||||||
|
@@ -124,8 +124,6 @@ angular.module('touch').directive('guacTouchDrag', [function guacTouchDrag() {
|
|||||||
element.addEventListener("touchmove", function dragTouchMove(e) {
|
element.addEventListener("touchmove", function dragTouchMove(e) {
|
||||||
if (e.touches.length === 1) {
|
if (e.touches.length === 1) {
|
||||||
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
// Get touch location
|
// Get touch location
|
||||||
var x = e.touches[0].clientX;
|
var x = e.touches[0].clientX;
|
||||||
var y = e.touches[0].clientY;
|
var y = e.touches[0].clientY;
|
||||||
@@ -163,8 +161,6 @@ angular.module('touch').directive('guacTouchDrag', [function guacTouchDrag() {
|
|||||||
|
|
||||||
if (startX && startY && e.touches.length === 0) {
|
if (startX && startY && e.touches.length === 0) {
|
||||||
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
// Signal end of drag gesture
|
// Signal end of drag gesture
|
||||||
if (inProgress && guacTouchDrag) {
|
if (inProgress && guacTouchDrag) {
|
||||||
$scope.$apply(function dragComplete() {
|
$scope.$apply(function dragComplete() {
|
||||||
|
@@ -159,8 +159,6 @@ angular.module('touch').directive('guacTouchPinch', [function guacTouchPinch() {
|
|||||||
element.addEventListener("touchmove", function pinchTouchMove(e) {
|
element.addEventListener("touchmove", function pinchTouchMove(e) {
|
||||||
if (e.touches.length === 2) {
|
if (e.touches.length === 2) {
|
||||||
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
// Calculate current zoom level
|
// Calculate current zoom level
|
||||||
currentLength = pinchDistance(e);
|
currentLength = pinchDistance(e);
|
||||||
|
|
||||||
@@ -188,8 +186,6 @@ angular.module('touch').directive('guacTouchPinch', [function guacTouchPinch() {
|
|||||||
|
|
||||||
if (startLength && e.touches.length < 2) {
|
if (startLength && e.touches.length < 2) {
|
||||||
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
// Notify of pinch end
|
// Notify of pinch end
|
||||||
if (guacTouchPinch) {
|
if (guacTouchPinch) {
|
||||||
$scope.$apply(function pinchComplete() {
|
$scope.$apply(function pinchComplete() {
|
||||||
|
@@ -480,6 +480,7 @@
|
|||||||
"FIELD_HEADER_ENABLE_PRINTING" : "Enable printing:",
|
"FIELD_HEADER_ENABLE_PRINTING" : "Enable printing:",
|
||||||
"FIELD_HEADER_ENABLE_SFTP" : "Enable SFTP:",
|
"FIELD_HEADER_ENABLE_SFTP" : "Enable SFTP:",
|
||||||
"FIELD_HEADER_ENABLE_THEMING" : "Enable theming:",
|
"FIELD_HEADER_ENABLE_THEMING" : "Enable theming:",
|
||||||
|
"FIELD_HEADER_ENABLE_TOUCH" : "Enable multi-touch:",
|
||||||
"FIELD_HEADER_ENABLE_WALLPAPER" : "Enable wallpaper:",
|
"FIELD_HEADER_ENABLE_WALLPAPER" : "Enable wallpaper:",
|
||||||
"FIELD_HEADER_GATEWAY_DOMAIN" : "Domain:",
|
"FIELD_HEADER_GATEWAY_DOMAIN" : "Domain:",
|
||||||
"FIELD_HEADER_GATEWAY_HOSTNAME" : "Hostname:",
|
"FIELD_HEADER_GATEWAY_HOSTNAME" : "Hostname:",
|
||||||
@@ -499,6 +500,7 @@
|
|||||||
"FIELD_HEADER_READ_ONLY" : "Read-only:",
|
"FIELD_HEADER_READ_ONLY" : "Read-only:",
|
||||||
"FIELD_HEADER_RECORDING_EXCLUDE_MOUSE" : "Exclude mouse:",
|
"FIELD_HEADER_RECORDING_EXCLUDE_MOUSE" : "Exclude mouse:",
|
||||||
"FIELD_HEADER_RECORDING_EXCLUDE_OUTPUT" : "Exclude graphics/streams:",
|
"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_INCLUDE_KEYS" : "Include key events:",
|
||||||
"FIELD_HEADER_RECORDING_NAME" : "Recording name:",
|
"FIELD_HEADER_RECORDING_NAME" : "Recording name:",
|
||||||
"FIELD_HEADER_RECORDING_PATH" : "Recording path:",
|
"FIELD_HEADER_RECORDING_PATH" : "Recording path:",
|
||||||
|
Reference in New Issue
Block a user