Source: main/webapp/modules/Mouse.js

  1. /*
  2. * Licensed to the Apache Software Foundation (ASF) under one
  3. * or more contributor license agreements. See the NOTICE file
  4. * distributed with this work for additional information
  5. * regarding copyright ownership. The ASF licenses this file
  6. * to you under the Apache License, Version 2.0 (the
  7. * "License"); you may not use this file except in compliance
  8. * with the License. You may obtain a copy of the License at
  9. *
  10. * http://www.apache.org/licenses/LICENSE-2.0
  11. *
  12. * Unless required by applicable law or agreed to in writing,
  13. * software distributed under the License is distributed on an
  14. * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  15. * KIND, either express or implied. See the License for the
  16. * specific language governing permissions and limitations
  17. * under the License.
  18. */
  19. var Guacamole = Guacamole || {};
  20. /**
  21. * Provides cross-browser mouse events for a given element. The events of
  22. * the given element are automatically populated with handlers that translate
  23. * mouse events into a non-browser-specific event provided by the
  24. * Guacamole.Mouse instance.
  25. *
  26. * @example
  27. * var mouse = new Guacamole.Mouse(client.getDisplay().getElement());
  28. *
  29. * // Forward all mouse interaction over Guacamole connection
  30. * mouse.onEach(['mousedown', 'mousemove', 'mouseup'], function sendMouseEvent(e) {
  31. * client.sendMouseState(e.state, true);
  32. * });
  33. *
  34. * @example
  35. * // Hide software cursor when mouse leaves display
  36. * mouse.on('mouseout', function hideCursor() {
  37. * client.getDisplay().showCursor(false);
  38. * });
  39. *
  40. * @constructor
  41. * @augments Guacamole.Mouse.Event.Target
  42. * @param {!Element} element
  43. * The Element to use to provide mouse events.
  44. */
  45. Guacamole.Mouse = function Mouse(element) {
  46. Guacamole.Mouse.Event.Target.call(this);
  47. /**
  48. * Reference to this Guacamole.Mouse.
  49. *
  50. * @private
  51. * @type {!Guacamole.Mouse}
  52. */
  53. var guac_mouse = this;
  54. /**
  55. * The number of mousemove events to require before re-enabling mouse
  56. * event handling after receiving a touch event.
  57. *
  58. * @type {!number}
  59. */
  60. this.touchMouseThreshold = 3;
  61. /**
  62. * The minimum amount of pixels scrolled required for a single scroll button
  63. * click.
  64. *
  65. * @type {!number}
  66. */
  67. this.scrollThreshold = 53;
  68. /**
  69. * The number of pixels to scroll per line.
  70. *
  71. * @type {!number}
  72. */
  73. this.PIXELS_PER_LINE = 18;
  74. /**
  75. * The number of pixels to scroll per page.
  76. *
  77. * @type {!number}
  78. */
  79. this.PIXELS_PER_PAGE = this.PIXELS_PER_LINE * 16;
  80. /**
  81. * Array of {@link Guacamole.Mouse.State} button names corresponding to the
  82. * mouse button indices used by DOM mouse events.
  83. *
  84. * @private
  85. * @type {!string[]}
  86. */
  87. var MOUSE_BUTTONS = [
  88. Guacamole.Mouse.State.Buttons.LEFT,
  89. Guacamole.Mouse.State.Buttons.MIDDLE,
  90. Guacamole.Mouse.State.Buttons.RIGHT
  91. ];
  92. /**
  93. * Counter of mouse events to ignore. This decremented by mousemove, and
  94. * while non-zero, mouse events will have no effect.
  95. *
  96. * @private
  97. * @type {!number}
  98. */
  99. var ignore_mouse = 0;
  100. /**
  101. * Cumulative scroll delta amount. This value is accumulated through scroll
  102. * events and results in scroll button clicks if it exceeds a certain
  103. * threshold.
  104. *
  105. * @private
  106. * @type {!number}
  107. */
  108. var scroll_delta = 0;
  109. // Block context menu so right-click gets sent properly
  110. element.addEventListener("contextmenu", function(e) {
  111. Guacamole.Event.DOMEvent.cancelEvent(e);
  112. }, false);
  113. element.addEventListener("mousemove", function(e) {
  114. // If ignoring events, decrement counter
  115. if (ignore_mouse) {
  116. Guacamole.Event.DOMEvent.cancelEvent(e);
  117. ignore_mouse--;
  118. return;
  119. }
  120. guac_mouse.move(Guacamole.Position.fromClientPosition(element, e.clientX, e.clientY), e);
  121. }, false);
  122. element.addEventListener("mousedown", function(e) {
  123. // Do not handle if ignoring events
  124. if (ignore_mouse) {
  125. Guacamole.Event.DOMEvent.cancelEvent(e);
  126. return;
  127. }
  128. var button = MOUSE_BUTTONS[e.button];
  129. if (button)
  130. guac_mouse.press(button, e);
  131. }, false);
  132. element.addEventListener("mouseup", function(e) {
  133. // Do not handle if ignoring events
  134. if (ignore_mouse) {
  135. Guacamole.Event.DOMEvent.cancelEvent(e);
  136. return;
  137. }
  138. var button = MOUSE_BUTTONS[e.button];
  139. if (button)
  140. guac_mouse.release(button, e);
  141. }, false);
  142. element.addEventListener("mouseout", function(e) {
  143. // Get parent of the element the mouse pointer is leaving
  144. if (!e) e = window.event;
  145. // Check that mouseout is due to actually LEAVING the element
  146. var target = e.relatedTarget || e.toElement;
  147. while (target) {
  148. if (target === element)
  149. return;
  150. target = target.parentNode;
  151. }
  152. // Release all buttons and fire mouseout
  153. guac_mouse.reset(e);
  154. guac_mouse.out(e);
  155. }, false);
  156. // Override selection on mouse event element.
  157. element.addEventListener("selectstart", function(e) {
  158. Guacamole.Event.DOMEvent.cancelEvent(e);
  159. }, false);
  160. // Ignore all pending mouse events when touch events are the apparent source
  161. function ignorePendingMouseEvents() { ignore_mouse = guac_mouse.touchMouseThreshold; }
  162. element.addEventListener("touchmove", ignorePendingMouseEvents, false);
  163. element.addEventListener("touchstart", ignorePendingMouseEvents, false);
  164. element.addEventListener("touchend", ignorePendingMouseEvents, false);
  165. // Scroll wheel support
  166. function mousewheel_handler(e) {
  167. // Determine approximate scroll amount (in pixels)
  168. var delta = e.deltaY || -e.wheelDeltaY || -e.wheelDelta;
  169. // If successfully retrieved scroll amount, convert to pixels if not
  170. // already in pixels
  171. if (delta) {
  172. // Convert to pixels if delta was lines
  173. if (e.deltaMode === 1)
  174. delta = e.deltaY * guac_mouse.PIXELS_PER_LINE;
  175. // Convert to pixels if delta was pages
  176. else if (e.deltaMode === 2)
  177. delta = e.deltaY * guac_mouse.PIXELS_PER_PAGE;
  178. }
  179. // Otherwise, assume legacy mousewheel event and line scrolling
  180. else
  181. delta = e.detail * guac_mouse.PIXELS_PER_LINE;
  182. // Update overall delta
  183. scroll_delta += delta;
  184. // Up
  185. if (scroll_delta <= -guac_mouse.scrollThreshold) {
  186. // Repeatedly click the up button until insufficient delta remains
  187. do {
  188. guac_mouse.click(Guacamole.Mouse.State.Buttons.UP);
  189. scroll_delta += guac_mouse.scrollThreshold;
  190. } while (scroll_delta <= -guac_mouse.scrollThreshold);
  191. // Reset delta
  192. scroll_delta = 0;
  193. }
  194. // Down
  195. if (scroll_delta >= guac_mouse.scrollThreshold) {
  196. // Repeatedly click the down button until insufficient delta remains
  197. do {
  198. guac_mouse.click(Guacamole.Mouse.State.Buttons.DOWN);
  199. scroll_delta -= guac_mouse.scrollThreshold;
  200. } while (scroll_delta >= guac_mouse.scrollThreshold);
  201. // Reset delta
  202. scroll_delta = 0;
  203. }
  204. // All scroll/wheel events must currently be cancelled regardless of
  205. // whether the dispatched event is cancelled, as there is no Guacamole
  206. // scroll event and thus no way to cancel scroll events that are
  207. // smaller than required to produce an up/down click
  208. Guacamole.Event.DOMEvent.cancelEvent(e);
  209. }
  210. element.addEventListener('DOMMouseScroll', mousewheel_handler, false);
  211. element.addEventListener('mousewheel', mousewheel_handler, false);
  212. element.addEventListener('wheel', mousewheel_handler, false);
  213. /**
  214. * Whether the browser supports CSS3 cursor styling, including hotspot
  215. * coordinates.
  216. *
  217. * @private
  218. * @type {!boolean}
  219. */
  220. var CSS3_CURSOR_SUPPORTED = (function() {
  221. var div = document.createElement("div");
  222. // If no cursor property at all, then no support
  223. if (!("cursor" in div.style))
  224. return false;
  225. try {
  226. // Apply simple 1x1 PNG
  227. div.style.cursor = "url(data:image/png;base64,"
  228. + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB"
  229. + "AQMAAAAl21bKAAAAA1BMVEX///+nxBvI"
  230. + "AAAACklEQVQI12NgAAAAAgAB4iG8MwAA"
  231. + "AABJRU5ErkJggg==) 0 0, auto";
  232. }
  233. catch (e) {
  234. return false;
  235. }
  236. // Verify cursor property is set to URL with hotspot
  237. return /\burl\([^()]*\)\s+0\s+0\b/.test(div.style.cursor || "");
  238. })();
  239. /**
  240. * Changes the local mouse cursor to the given canvas, having the given
  241. * hotspot coordinates. This affects styling of the element backing this
  242. * Guacamole.Mouse only, and may fail depending on browser support for
  243. * setting the mouse cursor.
  244. *
  245. * If setting the local cursor is desired, it is up to the implementation
  246. * to do something else, such as use the software cursor built into
  247. * Guacamole.Display, if the local cursor cannot be set.
  248. *
  249. * @param {!HTMLCanvasElement} canvas
  250. * The cursor image.
  251. *
  252. * @param {!number} x
  253. * The X-coordinate of the cursor hotspot.
  254. *
  255. * @param {!number} y
  256. * The Y-coordinate of the cursor hotspot.
  257. *
  258. * @return {!boolean}
  259. * true if the cursor was successfully set, false if the cursor could
  260. * not be set for any reason.
  261. */
  262. this.setCursor = function(canvas, x, y) {
  263. // Attempt to set via CSS3 cursor styling
  264. if (CSS3_CURSOR_SUPPORTED) {
  265. var dataURL = canvas.toDataURL('image/png');
  266. element.style.cursor = "url(" + dataURL + ") " + x + " " + y + ", auto";
  267. return true;
  268. }
  269. // Otherwise, setting cursor failed
  270. return false;
  271. };
  272. };
  273. /**
  274. * The current state of a mouse, including position and buttons.
  275. *
  276. * @constructor
  277. * @augments Guacamole.Position
  278. * @param {Guacamole.Mouse.State|object} [template={}]
  279. * The object whose properties should be copied within the new
  280. * Guacamole.Mouse.State.
  281. */
  282. Guacamole.Mouse.State = function State(template) {
  283. /**
  284. * Returns the template object that would be provided to the
  285. * Guacamole.Mouse.State constructor to produce a new Guacamole.Mouse.State
  286. * object with the properties specified. The order and type of arguments
  287. * used by this function are identical to those accepted by the
  288. * Guacamole.Mouse.State constructor of Apache Guacamole 1.3.0 and older.
  289. *
  290. * @private
  291. * @param {!number} x
  292. * The X position of the mouse pointer in pixels.
  293. *
  294. * @param {!number} y
  295. * The Y position of the mouse pointer in pixels.
  296. *
  297. * @param {!boolean} left
  298. * Whether the left mouse button is pressed.
  299. *
  300. * @param {!boolean} middle
  301. * Whether the middle mouse button is pressed.
  302. *
  303. * @param {!boolean} right
  304. * Whether the right mouse button is pressed.
  305. *
  306. * @param {!boolean} up
  307. * Whether the up mouse button is pressed (the fourth button, usually
  308. * part of a scroll wheel).
  309. *
  310. * @param {!boolean} down
  311. * Whether the down mouse button is pressed (the fifth button, usually
  312. * part of a scroll wheel).
  313. *
  314. * @return {!object}
  315. * The equivalent template object that would be passed to the new
  316. * Guacamole.Mouse.State constructor.
  317. */
  318. var legacyConstructor = function legacyConstructor(x, y, left, middle, right, up, down) {
  319. return {
  320. x : x,
  321. y : y,
  322. left : left,
  323. middle : middle,
  324. right : right,
  325. up : up,
  326. down : down
  327. };
  328. };
  329. // Accept old-style constructor, as well
  330. if (arguments.length > 1)
  331. template = legacyConstructor.apply(this, arguments);
  332. else
  333. template = template || {};
  334. Guacamole.Position.call(this, template);
  335. /**
  336. * Whether the left mouse button is currently pressed.
  337. *
  338. * @type {!boolean}
  339. * @default false
  340. */
  341. this.left = template.left || false;
  342. /**
  343. * Whether the middle mouse button is currently pressed.
  344. *
  345. * @type {!boolean}
  346. * @default false
  347. */
  348. this.middle = template.middle || false;
  349. /**
  350. * Whether the right mouse button is currently pressed.
  351. *
  352. * @type {!boolean}
  353. * @default false
  354. */
  355. this.right = template.right || false;
  356. /**
  357. * Whether the up mouse button is currently pressed. This is the fourth
  358. * mouse button, associated with upward scrolling of the mouse scroll
  359. * wheel.
  360. *
  361. * @type {!boolean}
  362. * @default false
  363. */
  364. this.up = template.up || false;
  365. /**
  366. * Whether the down mouse button is currently pressed. This is the fifth
  367. * mouse button, associated with downward scrolling of the mouse scroll
  368. * wheel.
  369. *
  370. * @type {!boolean}
  371. * @default false
  372. */
  373. this.down = template.down || false;
  374. };
  375. /**
  376. * All mouse buttons that may be represented by a
  377. * {@link Guacamole.Mouse.State}.
  378. *
  379. * @readonly
  380. * @enum
  381. */
  382. Guacamole.Mouse.State.Buttons = {
  383. /**
  384. * The name of the {@link Guacamole.Mouse.State} property representing the
  385. * left mouse button.
  386. *
  387. * @constant
  388. * @type {!string}
  389. */
  390. LEFT : 'left',
  391. /**
  392. * The name of the {@link Guacamole.Mouse.State} property representing the
  393. * middle mouse button.
  394. *
  395. * @constant
  396. * @type {!string}
  397. */
  398. MIDDLE : 'middle',
  399. /**
  400. * The name of the {@link Guacamole.Mouse.State} property representing the
  401. * right mouse button.
  402. *
  403. * @constant
  404. * @type {!string}
  405. */
  406. RIGHT : 'right',
  407. /**
  408. * The name of the {@link Guacamole.Mouse.State} property representing the
  409. * up mouse button (the fourth mouse button, clicked when the mouse scroll
  410. * wheel is scrolled up).
  411. *
  412. * @constant
  413. * @type {!string}
  414. */
  415. UP : 'up',
  416. /**
  417. * The name of the {@link Guacamole.Mouse.State} property representing the
  418. * down mouse button (the fifth mouse button, clicked when the mouse scroll
  419. * wheel is scrolled up).
  420. *
  421. * @constant
  422. * @type {!string}
  423. */
  424. DOWN : 'down'
  425. };
  426. /**
  427. * Base event type for all mouse events. The mouse producing the event may be
  428. * the user's local mouse (as with {@link Guacamole.Mouse}) or an emulated
  429. * mouse (as with {@link Guacamole.Mouse.Touchpad}).
  430. *
  431. * @constructor
  432. * @augments Guacamole.Event.DOMEvent
  433. * @param {!string} type
  434. * The type name of the event ("mousedown", "mouseup", etc.)
  435. *
  436. * @param {!Guacamole.Mouse.State} state
  437. * The current mouse state.
  438. *
  439. * @param {Event|Event[]} [events=[]]
  440. * The DOM events that are related to this event, if any.
  441. */
  442. Guacamole.Mouse.Event = function MouseEvent(type, state, events) {
  443. Guacamole.Event.DOMEvent.call(this, type, events);
  444. /**
  445. * The name of the event handler used by the Guacamole JavaScript API for
  446. * this event prior to the migration to Guacamole.Event.Target.
  447. *
  448. * @private
  449. * @constant
  450. * @type {!string}
  451. */
  452. var legacyHandlerName = 'on' + this.type;
  453. /**
  454. * The current mouse state at the time this event was fired.
  455. *
  456. * @type {!Guacamole.Mouse.State}
  457. */
  458. this.state = state;
  459. /**
  460. * @inheritdoc
  461. */
  462. this.invokeLegacyHandler = function invokeLegacyHandler(target) {
  463. if (target[legacyHandlerName]) {
  464. this.preventDefault();
  465. this.stopPropagation();
  466. target[legacyHandlerName](this.state);
  467. }
  468. };
  469. };
  470. /**
  471. * An object which can dispatch {@link Guacamole.Mouse.Event} objects
  472. * representing mouse events. These mouse events may be produced from an actual
  473. * mouse device (as with {@link Guacamole.Mouse}), from an emulated mouse
  474. * device (as with {@link Guacamole.Mouse.Touchpad}, or may be programmatically
  475. * generated (using functions like [dispatch()]{@link Guacamole.Mouse.Event.Target#dispatch},
  476. * [press()]{@link Guacamole.Mouse.Event.Target#press}, and
  477. * [release()]{@link Guacamole.Mouse.Event.Target#release}).
  478. *
  479. * @constructor
  480. * @augments Guacamole.Event.Target
  481. */
  482. Guacamole.Mouse.Event.Target = function MouseEventTarget() {
  483. Guacamole.Event.Target.call(this);
  484. /**
  485. * The current mouse state. The properties of this state are updated when
  486. * mouse events fire. This state object is also passed in as a parameter to
  487. * the handler of any mouse events.
  488. *
  489. * @type {!Guacamole.Mouse.State}
  490. */
  491. this.currentState = new Guacamole.Mouse.State();
  492. /**
  493. * Fired whenever a mouse button is effectively pressed. Depending on the
  494. * object dispatching the event, this can be due to a true mouse button
  495. * press ({@link Guacamole.Mouse}), an emulated mouse button press from a
  496. * touch gesture ({@link Guacamole.Mouse.Touchpad} and
  497. * {@link Guacamole.Mouse.Touchscreen}), or may be programmatically
  498. * generated through [dispatch()]{@link Guacamole.Mouse.Event.Target#dispatch},
  499. * [press()]{@link Guacamole.Mouse.Event.Target#press}, or
  500. * [click()]{@link Guacamole.Mouse.Event.Target#click}.
  501. *
  502. * @event Guacamole.Mouse.Event.Target#mousedown
  503. * @param {!Guacamole.Mouse.Event} event
  504. * The mousedown event that was fired.
  505. */
  506. /**
  507. * Fired whenever a mouse button is effectively released. Depending on the
  508. * object dispatching the event, this can be due to a true mouse button
  509. * release ({@link Guacamole.Mouse}), an emulated mouse button release from
  510. * a touch gesture ({@link Guacamole.Mouse.Touchpad} and
  511. * {@link Guacamole.Mouse.Touchscreen}), or may be programmatically
  512. * generated through [dispatch()]{@link Guacamole.Mouse.Event.Target#dispatch},
  513. * [release()]{@link Guacamole.Mouse.Event.Target#release}, or
  514. * [click()]{@link Guacamole.Mouse.Event.Target#click}.
  515. *
  516. * @event Guacamole.Mouse.Event.Target#mouseup
  517. * @param {!Guacamole.Mouse.Event} event
  518. * The mouseup event that was fired.
  519. */
  520. /**
  521. * Fired whenever the mouse pointer is effectively moved. Depending on the
  522. * object dispatching the event, this can be due to true mouse movement
  523. * ({@link Guacamole.Mouse}), emulated mouse movement from
  524. * a touch gesture ({@link Guacamole.Mouse.Touchpad} and
  525. * {@link Guacamole.Mouse.Touchscreen}), or may be programmatically
  526. * generated through [dispatch()]{@link Guacamole.Mouse.Event.Target#dispatch},
  527. * or [move()]{@link Guacamole.Mouse.Event.Target#move}.
  528. *
  529. * @event Guacamole.Mouse.Event.Target#mousemove
  530. * @param {!Guacamole.Mouse.Event} event
  531. * The mousemove event that was fired.
  532. */
  533. /**
  534. * Fired whenever the mouse pointer leaves the boundaries of the element
  535. * being monitored for interaction. This will only ever be automatically
  536. * fired due to movement of an actual mouse device via
  537. * {@link Guacamole.Mouse} unless programmatically generated through
  538. * [dispatch()]{@link Guacamole.Mouse.Event.Target#dispatch},
  539. * or [out()]{@link Guacamole.Mouse.Event.Target#out}.
  540. *
  541. * @event Guacamole.Mouse.Event.Target#mouseout
  542. * @param {!Guacamole.Mouse.Event} event
  543. * The mouseout event that was fired.
  544. */
  545. /**
  546. * Presses the given mouse button, if it isn't already pressed. Valid
  547. * button names are defined by {@link Guacamole.Mouse.State.Buttons} and
  548. * correspond to the button-related properties of
  549. * {@link Guacamole.Mouse.State}.
  550. *
  551. * @fires Guacamole.Mouse.Event.Target#mousedown
  552. *
  553. * @param {!string} button
  554. * The name of the mouse button to press, as defined by
  555. * {@link Guacamole.Mouse.State.Buttons}.
  556. *
  557. * @param {Event|Event[]} [events=[]]
  558. * The DOM events that are related to the mouse button press, if any.
  559. */
  560. this.press = function press(button, events) {
  561. if (!this.currentState[button]) {
  562. this.currentState[button] = true;
  563. this.dispatch(new Guacamole.Mouse.Event('mousedown', this.currentState, events));
  564. }
  565. };
  566. /**
  567. * Releases the given mouse button, if it isn't already released. Valid
  568. * button names are defined by {@link Guacamole.Mouse.State.Buttons} and
  569. * correspond to the button-related properties of
  570. * {@link Guacamole.Mouse.State}.
  571. *
  572. * @fires Guacamole.Mouse.Event.Target#mouseup
  573. *
  574. * @param {!string} button
  575. * The name of the mouse button to release, as defined by
  576. * {@link Guacamole.Mouse.State.Buttons}.
  577. *
  578. * @param {Event|Event[]} [events=[]]
  579. * The DOM events related to the mouse button release, if any.
  580. */
  581. this.release = function release(button, events) {
  582. if (this.currentState[button]) {
  583. this.currentState[button] = false;
  584. this.dispatch(new Guacamole.Mouse.Event('mouseup', this.currentState, events));
  585. }
  586. };
  587. /**
  588. * Clicks (presses and releases) the given mouse button. Valid button
  589. * names are defined by {@link Guacamole.Mouse.State.Buttons} and
  590. * correspond to the button-related properties of
  591. * {@link Guacamole.Mouse.State}.
  592. *
  593. * @fires Guacamole.Mouse.Event.Target#mousedown
  594. * @fires Guacamole.Mouse.Event.Target#mouseup
  595. *
  596. * @param {!string} button
  597. * The name of the mouse button to click, as defined by
  598. * {@link Guacamole.Mouse.State.Buttons}.
  599. *
  600. * @param {Event|Event[]} [events=[]]
  601. * The DOM events related to the click, if any.
  602. */
  603. this.click = function click(button, events) {
  604. this.press(button, events);
  605. this.release(button, events);
  606. };
  607. /**
  608. * Moves the mouse to the given coordinates.
  609. *
  610. * @fires Guacamole.Mouse.Event.Target#mousemove
  611. *
  612. * @param {!(Guacamole.Position|object)} position
  613. * The new coordinates of the mouse pointer. This object may be a
  614. * {@link Guacamole.Position} or any object with "x" and "y"
  615. * properties.
  616. *
  617. * @param {Event|Event[]} [events=[]]
  618. * The DOM events related to the mouse movement, if any.
  619. */
  620. this.move = function move(position, events) {
  621. if (this.currentState.x !== position.x || this.currentState.y !== position.y) {
  622. this.currentState.x = position.x;
  623. this.currentState.y = position.y;
  624. this.dispatch(new Guacamole.Mouse.Event('mousemove', this.currentState, events));
  625. }
  626. };
  627. /**
  628. * Notifies event listeners that the mouse pointer has left the boundaries
  629. * of the area being monitored for mouse events.
  630. *
  631. * @fires Guacamole.Mouse.Event.Target#mouseout
  632. *
  633. * @param {Event|Event[]} [events=[]]
  634. * The DOM events related to the mouse leaving the boundaries of the
  635. * monitored object, if any.
  636. */
  637. this.out = function out(events) {
  638. this.dispatch(new Guacamole.Mouse.Event('mouseout', this.currentState, events));
  639. };
  640. /**
  641. * Releases all mouse buttons that are currently pressed. If all mouse
  642. * buttons have already been released, this function has no effect.
  643. *
  644. * @fires Guacamole.Mouse.Event.Target#mouseup
  645. *
  646. * @param {Event|Event[]} [events=[]]
  647. * The DOM event related to all mouse buttons being released, if any.
  648. */
  649. this.reset = function reset(events) {
  650. for (var button in Guacamole.Mouse.State.Buttons) {
  651. this.release(Guacamole.Mouse.State.Buttons[button], events);
  652. }
  653. };
  654. };
  655. /**
  656. * Provides cross-browser relative touch event translation for a given element.
  657. *
  658. * Touch events are translated into mouse events as if the touches occurred
  659. * on a touchpad (drag to push the mouse pointer, tap to click).
  660. *
  661. * @example
  662. * var touchpad = new Guacamole.Mouse.Touchpad(client.getDisplay().getElement());
  663. *
  664. * // Emulate a mouse using touchpad-style gestures, forwarding all mouse
  665. * // interaction over Guacamole connection
  666. * touchpad.onEach(['mousedown', 'mousemove', 'mouseup'], function sendMouseEvent(e) {
  667. *
  668. * // Re-show software mouse cursor if possibly hidden by a prior call to
  669. * // showCursor(), such as a "mouseout" event handler that hides the
  670. * // cursor
  671. * client.getDisplay().showCursor(true);
  672. *
  673. * client.sendMouseState(e.state, true);
  674. *
  675. * });
  676. *
  677. * @constructor
  678. * @augments Guacamole.Mouse.Event.Target
  679. * @param {!Element} element
  680. * The Element to use to provide touch events.
  681. */
  682. Guacamole.Mouse.Touchpad = function Touchpad(element) {
  683. Guacamole.Mouse.Event.Target.call(this);
  684. /**
  685. * The "mouseout" event will never be fired by Guacamole.Mouse.Touchpad.
  686. *
  687. * @ignore
  688. * @event Guacamole.Mouse.Touchpad#mouseout
  689. */
  690. /**
  691. * Reference to this Guacamole.Mouse.Touchpad.
  692. *
  693. * @private
  694. * @type {!Guacamole.Mouse.Touchpad}
  695. */
  696. var guac_touchpad = this;
  697. /**
  698. * The distance a two-finger touch must move per scrollwheel event, in
  699. * pixels.
  700. *
  701. * @type {!number}
  702. */
  703. this.scrollThreshold = 20 * (window.devicePixelRatio || 1);
  704. /**
  705. * The maximum number of milliseconds to wait for a touch to end for the
  706. * gesture to be considered a click.
  707. *
  708. * @type {!number}
  709. */
  710. this.clickTimingThreshold = 250;
  711. /**
  712. * The maximum number of pixels to allow a touch to move for the gesture to
  713. * be considered a click.
  714. *
  715. * @type {!number}
  716. */
  717. this.clickMoveThreshold = 10 * (window.devicePixelRatio || 1);
  718. /**
  719. * The current mouse state. The properties of this state are updated when
  720. * mouse events fire. This state object is also passed in as a parameter to
  721. * the handler of any mouse events.
  722. *
  723. * @type {!Guacamole.Mouse.State}
  724. */
  725. this.currentState = new Guacamole.Mouse.State();
  726. var touch_count = 0;
  727. var last_touch_x = 0;
  728. var last_touch_y = 0;
  729. var last_touch_time = 0;
  730. var pixels_moved = 0;
  731. var touch_buttons = {
  732. 1: "left",
  733. 2: "right",
  734. 3: "middle"
  735. };
  736. var gesture_in_progress = false;
  737. var click_release_timeout = null;
  738. element.addEventListener("touchend", function(e) {
  739. e.preventDefault();
  740. // If we're handling a gesture AND this is the last touch
  741. if (gesture_in_progress && e.touches.length === 0) {
  742. var time = new Date().getTime();
  743. // Get corresponding mouse button
  744. var button = touch_buttons[touch_count];
  745. // If mouse already down, release anad clear timeout
  746. if (guac_touchpad.currentState[button]) {
  747. // Fire button up event
  748. guac_touchpad.release(button, e);
  749. // Clear timeout, if set
  750. if (click_release_timeout) {
  751. window.clearTimeout(click_release_timeout);
  752. click_release_timeout = null;
  753. }
  754. }
  755. // If single tap detected (based on time and distance)
  756. if (time - last_touch_time <= guac_touchpad.clickTimingThreshold
  757. && pixels_moved < guac_touchpad.clickMoveThreshold) {
  758. // Fire button down event
  759. guac_touchpad.press(button, e);
  760. // Delay mouse up - mouse up should be canceled if
  761. // touchstart within timeout.
  762. click_release_timeout = window.setTimeout(function() {
  763. // Fire button up event
  764. guac_touchpad.release(button, e);
  765. // Gesture now over
  766. gesture_in_progress = false;
  767. }, guac_touchpad.clickTimingThreshold);
  768. }
  769. // If we're not waiting to see if this is a click, stop gesture
  770. if (!click_release_timeout)
  771. gesture_in_progress = false;
  772. }
  773. }, false);
  774. element.addEventListener("touchstart", function(e) {
  775. e.preventDefault();
  776. // Track number of touches, but no more than three
  777. touch_count = Math.min(e.touches.length, 3);
  778. // Clear timeout, if set
  779. if (click_release_timeout) {
  780. window.clearTimeout(click_release_timeout);
  781. click_release_timeout = null;
  782. }
  783. // Record initial touch location and time for touch movement
  784. // and tap gestures
  785. if (!gesture_in_progress) {
  786. // Stop mouse events while touching
  787. gesture_in_progress = true;
  788. // Record touch location and time
  789. var starting_touch = e.touches[0];
  790. last_touch_x = starting_touch.clientX;
  791. last_touch_y = starting_touch.clientY;
  792. last_touch_time = new Date().getTime();
  793. pixels_moved = 0;
  794. }
  795. }, false);
  796. element.addEventListener("touchmove", function(e) {
  797. e.preventDefault();
  798. // Get change in touch location
  799. var touch = e.touches[0];
  800. var delta_x = touch.clientX - last_touch_x;
  801. var delta_y = touch.clientY - last_touch_y;
  802. // Track pixels moved
  803. pixels_moved += Math.abs(delta_x) + Math.abs(delta_y);
  804. // If only one touch involved, this is mouse move
  805. if (touch_count === 1) {
  806. // Calculate average velocity in Manhatten pixels per millisecond
  807. var velocity = pixels_moved / (new Date().getTime() - last_touch_time);
  808. // Scale mouse movement relative to velocity
  809. var scale = 1 + velocity;
  810. // Update mouse location
  811. var position = new Guacamole.Position(guac_touchpad.currentState);
  812. position.x += delta_x*scale;
  813. position.y += delta_y*scale;
  814. // Prevent mouse from leaving screen
  815. position.x = Math.min(Math.max(0, position.x), element.offsetWidth - 1);
  816. position.y = Math.min(Math.max(0, position.y), element.offsetHeight - 1);
  817. // Fire movement event, if defined
  818. guac_touchpad.move(position, e);
  819. // Update touch location
  820. last_touch_x = touch.clientX;
  821. last_touch_y = touch.clientY;
  822. }
  823. // Interpret two-finger swipe as scrollwheel
  824. else if (touch_count === 2) {
  825. // If change in location passes threshold for scroll
  826. if (Math.abs(delta_y) >= guac_touchpad.scrollThreshold) {
  827. // Decide button based on Y movement direction
  828. var button;
  829. if (delta_y > 0) button = "down";
  830. else button = "up";
  831. guac_touchpad.click(button, e);
  832. // Only update touch location after a scroll has been
  833. // detected
  834. last_touch_x = touch.clientX;
  835. last_touch_y = touch.clientY;
  836. }
  837. }
  838. }, false);
  839. };
  840. /**
  841. * Provides cross-browser absolute touch event translation for a given element.
  842. *
  843. * Touch events are translated into mouse events as if the touches occurred
  844. * on a touchscreen (tapping anywhere on the screen clicks at that point,
  845. * long-press to right-click).
  846. *
  847. * @example
  848. * var touchscreen = new Guacamole.Mouse.Touchscreen(client.getDisplay().getElement());
  849. *
  850. * // Emulate a mouse using touchscreen-style gestures, forwarding all mouse
  851. * // interaction over Guacamole connection
  852. * touchscreen.onEach(['mousedown', 'mousemove', 'mouseup'], function sendMouseEvent(e) {
  853. *
  854. * // Re-show software mouse cursor if possibly hidden by a prior call to
  855. * // showCursor(), such as a "mouseout" event handler that hides the
  856. * // cursor
  857. * client.getDisplay().showCursor(true);
  858. *
  859. * client.sendMouseState(e.state, true);
  860. *
  861. * });
  862. *
  863. * @constructor
  864. * @augments Guacamole.Mouse.Event.Target
  865. * @param {!Element} element
  866. * The Element to use to provide touch events.
  867. */
  868. Guacamole.Mouse.Touchscreen = function Touchscreen(element) {
  869. Guacamole.Mouse.Event.Target.call(this);
  870. /**
  871. * The "mouseout" event will never be fired by Guacamole.Mouse.Touchscreen.
  872. *
  873. * @ignore
  874. * @event Guacamole.Mouse.Touchscreen#mouseout
  875. */
  876. /**
  877. * Reference to this Guacamole.Mouse.Touchscreen.
  878. *
  879. * @private
  880. * @type {!Guacamole.Mouse.Touchscreen}
  881. */
  882. var guac_touchscreen = this;
  883. /**
  884. * Whether a gesture is known to be in progress. If false, touch events
  885. * will be ignored.
  886. *
  887. * @private
  888. * @type {!boolean}
  889. */
  890. var gesture_in_progress = false;
  891. /**
  892. * The start X location of a gesture.
  893. *
  894. * @private
  895. * @type {number}
  896. */
  897. var gesture_start_x = null;
  898. /**
  899. * The start Y location of a gesture.
  900. *
  901. * @private
  902. * @type {number}
  903. */
  904. var gesture_start_y = null;
  905. /**
  906. * The timeout associated with the delayed, cancellable click release.
  907. *
  908. * @private
  909. * @type {number}
  910. */
  911. var click_release_timeout = null;
  912. /**
  913. * The timeout associated with long-press for right click.
  914. *
  915. * @private
  916. * @type {number}
  917. */
  918. var long_press_timeout = null;
  919. /**
  920. * The distance a two-finger touch must move per scrollwheel event, in
  921. * pixels.
  922. *
  923. * @type {!number}
  924. */
  925. this.scrollThreshold = 20 * (window.devicePixelRatio || 1);
  926. /**
  927. * The maximum number of milliseconds to wait for a touch to end for the
  928. * gesture to be considered a click.
  929. *
  930. * @type {!number}
  931. */
  932. this.clickTimingThreshold = 250;
  933. /**
  934. * The maximum number of pixels to allow a touch to move for the gesture to
  935. * be considered a click.
  936. *
  937. * @type {!number}
  938. */
  939. this.clickMoveThreshold = 16 * (window.devicePixelRatio || 1);
  940. /**
  941. * The amount of time a press must be held for long press to be
  942. * detected.
  943. */
  944. this.longPressThreshold = 500;
  945. /**
  946. * Returns whether the given touch event exceeds the movement threshold for
  947. * clicking, based on where the touch gesture began.
  948. *
  949. * @private
  950. * @param {!TouchEvent} e
  951. * The touch event to check.
  952. *
  953. * @return {!boolean}
  954. * true if the movement threshold is exceeded, false otherwise.
  955. */
  956. function finger_moved(e) {
  957. var touch = e.touches[0] || e.changedTouches[0];
  958. var delta_x = touch.clientX - gesture_start_x;
  959. var delta_y = touch.clientY - gesture_start_y;
  960. return Math.sqrt(delta_x*delta_x + delta_y*delta_y) >= guac_touchscreen.clickMoveThreshold;
  961. }
  962. /**
  963. * Begins a new gesture at the location of the first touch in the given
  964. * touch event.
  965. *
  966. * @private
  967. * @param {!TouchEvent} e
  968. * The touch event beginning this new gesture.
  969. */
  970. function begin_gesture(e) {
  971. var touch = e.touches[0];
  972. gesture_in_progress = true;
  973. gesture_start_x = touch.clientX;
  974. gesture_start_y = touch.clientY;
  975. }
  976. /**
  977. * End the current gesture entirely. Wait for all touches to be done before
  978. * resuming gesture detection.
  979. *
  980. * @private
  981. */
  982. function end_gesture() {
  983. window.clearTimeout(click_release_timeout);
  984. window.clearTimeout(long_press_timeout);
  985. gesture_in_progress = false;
  986. }
  987. element.addEventListener("touchend", function(e) {
  988. // Do not handle if no gesture
  989. if (!gesture_in_progress)
  990. return;
  991. // Ignore if more than one touch
  992. if (e.touches.length !== 0 || e.changedTouches.length !== 1) {
  993. end_gesture();
  994. return;
  995. }
  996. // Long-press, if any, is over
  997. window.clearTimeout(long_press_timeout);
  998. // Always release mouse button if pressed
  999. guac_touchscreen.release(Guacamole.Mouse.State.Buttons.LEFT, e);
  1000. // If finger hasn't moved enough to cancel the click
  1001. if (!finger_moved(e)) {
  1002. e.preventDefault();
  1003. // If not yet pressed, press and start delay release
  1004. if (!guac_touchscreen.currentState.left) {
  1005. var touch = e.changedTouches[0];
  1006. guac_touchscreen.move(Guacamole.Position.fromClientPosition(element, touch.clientX, touch.clientY));
  1007. guac_touchscreen.press(Guacamole.Mouse.State.Buttons.LEFT, e);
  1008. // Release button after a delay, if not canceled
  1009. click_release_timeout = window.setTimeout(function() {
  1010. guac_touchscreen.release(Guacamole.Mouse.State.Buttons.LEFT, e);
  1011. end_gesture();
  1012. }, guac_touchscreen.clickTimingThreshold);
  1013. }
  1014. } // end if finger not moved
  1015. }, false);
  1016. element.addEventListener("touchstart", function(e) {
  1017. // Ignore if more than one touch
  1018. if (e.touches.length !== 1) {
  1019. end_gesture();
  1020. return;
  1021. }
  1022. e.preventDefault();
  1023. // New touch begins a new gesture
  1024. begin_gesture(e);
  1025. // Keep button pressed if tap after left click
  1026. window.clearTimeout(click_release_timeout);
  1027. // Click right button if this turns into a long-press
  1028. long_press_timeout = window.setTimeout(function() {
  1029. var touch = e.touches[0];
  1030. guac_touchscreen.move(Guacamole.Position.fromClientPosition(element, touch.clientX, touch.clientY));
  1031. guac_touchscreen.click(Guacamole.Mouse.State.Buttons.RIGHT, e);
  1032. end_gesture();
  1033. }, guac_touchscreen.longPressThreshold);
  1034. }, false);
  1035. element.addEventListener("touchmove", function(e) {
  1036. // Do not handle if no gesture
  1037. if (!gesture_in_progress)
  1038. return;
  1039. // Cancel long press if finger moved
  1040. if (finger_moved(e))
  1041. window.clearTimeout(long_press_timeout);
  1042. // Ignore if more than one touch
  1043. if (e.touches.length !== 1) {
  1044. end_gesture();
  1045. return;
  1046. }
  1047. // Update mouse position if dragging
  1048. if (guac_touchscreen.currentState.left) {
  1049. e.preventDefault();
  1050. // Update state
  1051. var touch = e.touches[0];
  1052. guac_touchscreen.move(Guacamole.Position.fromClientPosition(element, touch.clientX, touch.clientY), e);
  1053. }
  1054. }, false);
  1055. };