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