]>
Commit | Line | Data |
---|---|---|
c1e2785f SM |
1 | /* |
2 | * noVNC: HTML5 VNC client | |
3 | * Copyright (C) 2012 Joel Martin | |
4 | * Copyright (C) 2013 Samuel Mannehed for Cendio AB | |
5 | * Licensed under MPL 2.0 or any later version (see LICENSE.txt) | |
6 | */ | |
7 | ||
8 | /*jslint browser: true, white: false */ | |
9 | /*global window, Util */ | |
10 | ||
11 | import * as Log from '../util/logging.js'; | |
12 | import { isTouchDevice } from '../util/browsers.js'; | |
13 | import { setCapture, stopEvent, getPointerEvent } from '../util/events.js'; | |
c1e2785f | 14 | |
28b004fd SM |
15 | var WHEEL_STEP = 10; // Delta threshold for a mouse wheel step |
16 | var WHEEL_STEP_TIMEOUT = 50; // ms | |
17 | var WHEEL_LINE_HEIGHT = 19; | |
18 | ||
747b4623 | 19 | export default function Mouse(target) { |
3d7bb020 | 20 | this._target = target || document; |
c1e2785f SM |
21 | |
22 | this._doubleClickTimer = null; | |
23 | this._lastTouchPos = null; | |
24 | ||
28b004fd SM |
25 | this._pos = null; |
26 | this._wheelStepXTimer = null; | |
27 | this._wheelStepYTimer = null; | |
28 | this._accumulatedWheelDeltaX = 0; | |
29 | this._accumulatedWheelDeltaY = 0; | |
30 | ||
c1e2785f SM |
31 | this._eventHandlers = { |
32 | 'mousedown': this._handleMouseDown.bind(this), | |
33 | 'mouseup': this._handleMouseUp.bind(this), | |
34 | 'mousemove': this._handleMouseMove.bind(this), | |
35 | 'mousewheel': this._handleMouseWheel.bind(this), | |
36 | 'mousedisable': this._handleMouseDisable.bind(this) | |
37 | }; | |
38 | }; | |
39 | ||
40 | Mouse.prototype = { | |
747b4623 PO |
41 | // ===== PROPERTIES ===== |
42 | ||
43 | touchButton: 1, // Button mask (1, 2, 4) for touch devices (0 means ignore clicks) | |
44 | ||
45 | // ===== EVENT HANDLERS ===== | |
46 | ||
47 | onmousebutton: function () {}, // Handler for mouse button click/release | |
48 | onmousemove: function () {}, // Handler for mouse movement | |
49 | ||
50 | // ===== PRIVATE METHODS ===== | |
c1e2785f SM |
51 | |
52 | _resetDoubleClickTimer: function () { | |
53 | this._doubleClickTimer = null; | |
54 | }, | |
55 | ||
56 | _handleMouseButton: function (e, down) { | |
28b004fd SM |
57 | this._updateMousePosition(e); |
58 | var pos = this._pos; | |
c1e2785f SM |
59 | |
60 | var bmask; | |
61 | if (e.touches || e.changedTouches) { | |
62 | // Touch device | |
63 | ||
64 | // When two touches occur within 500 ms of each other and are | |
65 | // close enough together a double click is triggered. | |
66 | if (down == 1) { | |
67 | if (this._doubleClickTimer === null) { | |
68 | this._lastTouchPos = pos; | |
69 | } else { | |
70 | clearTimeout(this._doubleClickTimer); | |
71 | ||
72 | // When the distance between the two touches is small enough | |
73 | // force the position of the latter touch to the position of | |
74 | // the first. | |
75 | ||
76 | var xs = this._lastTouchPos.x - pos.x; | |
77 | var ys = this._lastTouchPos.y - pos.y; | |
78 | var d = Math.sqrt((xs * xs) + (ys * ys)); | |
79 | ||
80 | // The goal is to trigger on a certain physical width, the | |
81 | // devicePixelRatio brings us a bit closer but is not optimal. | |
82 | var threshold = 20 * (window.devicePixelRatio || 1); | |
83 | if (d < threshold) { | |
84 | pos = this._lastTouchPos; | |
85 | } | |
86 | } | |
87 | this._doubleClickTimer = setTimeout(this._resetDoubleClickTimer.bind(this), 500); | |
88 | } | |
747b4623 | 89 | bmask = this.touchButton; |
c1e2785f SM |
90 | // If bmask is set |
91 | } else if (e.which) { | |
92 | /* everything except IE */ | |
93 | bmask = 1 << e.button; | |
94 | } else { | |
95 | /* IE including 9 */ | |
96 | bmask = (e.button & 0x1) + // Left | |
97 | (e.button & 0x2) * 2 + // Right | |
98 | (e.button & 0x4) / 2; // Middle | |
99 | } | |
100 | ||
747b4623 PO |
101 | Log.Debug("onmousebutton " + (down ? "down" : "up") + |
102 | ", x: " + pos.x + ", y: " + pos.y + ", bmask: " + bmask); | |
103 | this.onmousebutton(pos.x, pos.y, down, bmask); | |
104 | ||
c1e2785f SM |
105 | stopEvent(e); |
106 | }, | |
107 | ||
108 | _handleMouseDown: function (e) { | |
109 | // Touch events have implicit capture | |
110 | if (e.type === "mousedown") { | |
111 | setCapture(this._target); | |
112 | } | |
113 | ||
114 | this._handleMouseButton(e, 1); | |
115 | }, | |
116 | ||
117 | _handleMouseUp: function (e) { | |
118 | this._handleMouseButton(e, 0); | |
119 | }, | |
120 | ||
28b004fd SM |
121 | // Mouse wheel events are sent in steps over VNC. This means that the VNC |
122 | // protocol can't handle a wheel event with specific distance or speed. | |
123 | // Therefor, if we get a lot of small mouse wheel events we combine them. | |
124 | _generateWheelStepX: function () { | |
125 | ||
126 | if (this._accumulatedWheelDeltaX < 0) { | |
747b4623 PO |
127 | this.onmousebutton(this._pos.x, this._pos.y, 1, 1 << 5); |
128 | this.onmousebutton(this._pos.x, this._pos.y, 0, 1 << 5); | |
28b004fd | 129 | } else if (this._accumulatedWheelDeltaX > 0) { |
747b4623 PO |
130 | this.onmousebutton(this._pos.x, this._pos.y, 1, 1 << 6); |
131 | this.onmousebutton(this._pos.x, this._pos.y, 0, 1 << 6); | |
28b004fd SM |
132 | } |
133 | ||
134 | this._accumulatedWheelDeltaX = 0; | |
135 | }, | |
136 | ||
137 | _generateWheelStepY: function () { | |
138 | ||
139 | if (this._accumulatedWheelDeltaY < 0) { | |
747b4623 PO |
140 | this.onmousebutton(this._pos.x, this._pos.y, 1, 1 << 3); |
141 | this.onmousebutton(this._pos.x, this._pos.y, 0, 1 << 3); | |
28b004fd | 142 | } else if (this._accumulatedWheelDeltaY > 0) { |
747b4623 PO |
143 | this.onmousebutton(this._pos.x, this._pos.y, 1, 1 << 4); |
144 | this.onmousebutton(this._pos.x, this._pos.y, 0, 1 << 4); | |
28b004fd SM |
145 | } |
146 | ||
147 | this._accumulatedWheelDeltaY = 0; | |
148 | }, | |
149 | ||
150 | _resetWheelStepTimers: function () { | |
151 | window.clearTimeout(this._wheelStepXTimer); | |
152 | window.clearTimeout(this._wheelStepYTimer); | |
153 | this._wheelStepXTimer = null; | |
154 | this._wheelStepYTimer = null; | |
155 | }, | |
156 | ||
c1e2785f | 157 | _handleMouseWheel: function (e) { |
28b004fd | 158 | this._resetWheelStepTimers(); |
c1e2785f | 159 | |
28b004fd | 160 | this._updateMousePosition(e); |
c1e2785f | 161 | |
28b004fd SM |
162 | var dX = e.deltaX; |
163 | var dY = e.deltaY; | |
164 | ||
165 | // Pixel units unless it's non-zero. | |
166 | // Note that if deltamode is line or page won't matter since we aren't | |
167 | // sending the mouse wheel delta to the server anyway. | |
168 | // The difference between pixel and line can be important however since | |
169 | // we have a threshold that can be smaller than the line height. | |
170 | if (e.deltaMode !== 0) { | |
171 | dX *= WHEEL_LINE_HEIGHT; | |
172 | dY *= WHEEL_LINE_HEIGHT; | |
173 | } | |
174 | ||
175 | this._accumulatedWheelDeltaX += dX; | |
176 | this._accumulatedWheelDeltaY += dY; | |
177 | ||
178 | // Generate a mouse wheel step event when the accumulated delta | |
179 | // for one of the axes is large enough. | |
180 | // Small delta events that do not pass the threshold get sent | |
181 | // after a timeout. | |
182 | if (Math.abs(this._accumulatedWheelDeltaX) > WHEEL_STEP) { | |
183 | this._generateWheelStepX(); | |
184 | } else { | |
185 | this._wheelStepXTimer = | |
186 | window.setTimeout(this._generateWheelStepX.bind(this), | |
187 | WHEEL_STEP_TIMEOUT); | |
188 | } | |
189 | if (Math.abs(this._accumulatedWheelDeltaY) > WHEEL_STEP) { | |
190 | this._generateWheelStepY(); | |
191 | } else { | |
192 | this._wheelStepYTimer = | |
193 | window.setTimeout(this._generateWheelStepY.bind(this), | |
194 | WHEEL_STEP_TIMEOUT); | |
c1e2785f SM |
195 | } |
196 | ||
197 | stopEvent(e); | |
198 | }, | |
199 | ||
200 | _handleMouseMove: function (e) { | |
28b004fd | 201 | this._updateMousePosition(e); |
747b4623 | 202 | this.onmousemove(this._pos.x, this._pos.y); |
c1e2785f SM |
203 | stopEvent(e); |
204 | }, | |
205 | ||
206 | _handleMouseDisable: function (e) { | |
c1e2785f SM |
207 | /* |
208 | * Stop propagation if inside canvas area | |
209 | * Note: This is only needed for the 'click' event as it fails | |
210 | * to fire properly for the target element so we have | |
211 | * to listen on the document element instead. | |
212 | */ | |
213 | if (e.target == this._target) { | |
214 | stopEvent(e); | |
215 | } | |
216 | }, | |
217 | ||
28b004fd SM |
218 | // Update coordinates relative to target |
219 | _updateMousePosition: function(e) { | |
c1e2785f SM |
220 | e = getPointerEvent(e); |
221 | var bounds = this._target.getBoundingClientRect(); | |
222 | var x, y; | |
223 | // Clip to target bounds | |
224 | if (e.clientX < bounds.left) { | |
225 | x = 0; | |
226 | } else if (e.clientX >= bounds.right) { | |
227 | x = bounds.width - 1; | |
228 | } else { | |
229 | x = e.clientX - bounds.left; | |
230 | } | |
231 | if (e.clientY < bounds.top) { | |
232 | y = 0; | |
233 | } else if (e.clientY >= bounds.bottom) { | |
234 | y = bounds.height - 1; | |
235 | } else { | |
236 | y = e.clientY - bounds.top; | |
237 | } | |
28b004fd | 238 | this._pos = {x:x, y:y}; |
c1e2785f SM |
239 | }, |
240 | ||
747b4623 PO |
241 | // ===== PUBLIC METHODS ===== |
242 | ||
c1e2785f SM |
243 | grab: function () { |
244 | var c = this._target; | |
245 | ||
246 | if (isTouchDevice) { | |
247 | c.addEventListener('touchstart', this._eventHandlers.mousedown); | |
248 | c.addEventListener('touchend', this._eventHandlers.mouseup); | |
249 | c.addEventListener('touchmove', this._eventHandlers.mousemove); | |
250 | } | |
251 | c.addEventListener('mousedown', this._eventHandlers.mousedown); | |
252 | c.addEventListener('mouseup', this._eventHandlers.mouseup); | |
253 | c.addEventListener('mousemove', this._eventHandlers.mousemove); | |
254 | c.addEventListener('wheel', this._eventHandlers.mousewheel); | |
255 | ||
256 | /* Prevent middle-click pasting (see above for why we bind to document) */ | |
257 | document.addEventListener('click', this._eventHandlers.mousedisable); | |
258 | ||
259 | /* preventDefault() on mousedown doesn't stop this event for some | |
260 | reason so we have to explicitly block it */ | |
261 | c.addEventListener('contextmenu', this._eventHandlers.mousedisable); | |
262 | }, | |
263 | ||
264 | ungrab: function () { | |
265 | var c = this._target; | |
266 | ||
28b004fd SM |
267 | this._resetWheelStepTimers(); |
268 | ||
c1e2785f SM |
269 | if (isTouchDevice) { |
270 | c.removeEventListener('touchstart', this._eventHandlers.mousedown); | |
271 | c.removeEventListener('touchend', this._eventHandlers.mouseup); | |
272 | c.removeEventListener('touchmove', this._eventHandlers.mousemove); | |
273 | } | |
274 | c.removeEventListener('mousedown', this._eventHandlers.mousedown); | |
275 | c.removeEventListener('mouseup', this._eventHandlers.mouseup); | |
276 | c.removeEventListener('mousemove', this._eventHandlers.mousemove); | |
277 | c.removeEventListener('wheel', this._eventHandlers.mousewheel); | |
278 | ||
279 | document.removeEventListener('click', this._eventHandlers.mousedisable); | |
280 | ||
281 | c.removeEventListener('contextmenu', this._eventHandlers.mousedisable); | |
282 | } | |
283 | }; |