devices.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497
  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. /*jslint browser: true, white: false */
  8. /*global window, Util */
  9. import * as Log from '../util/logging.js';
  10. import { isTouchDevice } from '../util/browsers.js';
  11. import { setCapture, stopEvent, getPointerEvent } from '../util/events.js';
  12. import { set_defaults, make_properties } from '../util/properties.js';
  13. import * as KeyboardUtil from "./util.js";
  14. import KeyTable from "./keysym.js";
  15. //
  16. // Keyboard event handler
  17. //
  18. function Keyboard(defaults) {
  19. this._keyDownList = {}; // List of depressed keys
  20. // (even if they are happy)
  21. this._pendingKey = null; // Key waiting for keypress
  22. set_defaults(this, defaults, {
  23. 'target': document,
  24. 'focused': true
  25. });
  26. // keep these here so we can refer to them later
  27. this._eventHandlers = {
  28. 'keyup': this._handleKeyUp.bind(this),
  29. 'keydown': this._handleKeyDown.bind(this),
  30. 'keypress': this._handleKeyPress.bind(this),
  31. 'blur': this._allKeysUp.bind(this)
  32. };
  33. };
  34. function isMac() {
  35. return navigator && !!(/mac/i).exec(navigator.platform);
  36. }
  37. function isWindows() {
  38. return navigator && !!(/win/i).exec(navigator.platform);
  39. }
  40. Keyboard.prototype = {
  41. // private methods
  42. _sendKeyEvent: function (keysym, code, down) {
  43. if (!this._onKeyEvent) {
  44. return;
  45. }
  46. Log.Debug("onKeyEvent " + (down ? "down" : "up") +
  47. ", keysym: " + keysym, ", code: " + code);
  48. // Windows sends CtrlLeft+AltRight when you press
  49. // AltGraph, which tends to confuse the hell out of
  50. // remote systems. Fake a release of these keys until
  51. // there is a way to detect AltGraph properly.
  52. var fakeAltGraph = false;
  53. if (down && isWindows()) {
  54. if ((code !== 'ControlLeft') &&
  55. (code !== 'AltRight') &&
  56. ('ControlLeft' in this._keyDownList) &&
  57. ('AltRight' in this._keyDownList)) {
  58. fakeAltGraph = true;
  59. this._onKeyEvent(this._keyDownList['AltRight'],
  60. 'AltRight', false);
  61. this._onKeyEvent(this._keyDownList['ControlLeft'],
  62. 'ControlLeft', false);
  63. }
  64. }
  65. this._onKeyEvent(keysym, code, down);
  66. if (fakeAltGraph) {
  67. this._onKeyEvent(this._keyDownList['ControlLeft'],
  68. 'ControlLeft', true);
  69. this._onKeyEvent(this._keyDownList['AltRight'],
  70. 'AltRight', true);
  71. }
  72. },
  73. _getKeyCode: function (e) {
  74. var code = KeyboardUtil.getKeycode(e);
  75. if (code === 'Unidentified') {
  76. // Unstable, but we don't have anything else to go on
  77. // (don't use it for 'keypress' events thought since
  78. // WebKit sets it to the same as charCode)
  79. if (e.keyCode && (e.type !== 'keypress')) {
  80. code = 'Platform' + e.keyCode;
  81. }
  82. }
  83. return code;
  84. },
  85. _handleKeyDown: function (e) {
  86. if (!this._focused) { return; }
  87. var code = this._getKeyCode(e);
  88. var keysym = KeyboardUtil.getKeysym(e);
  89. // We cannot handle keys we cannot track, but we also need
  90. // to deal with virtual keyboards which omit key info
  91. if (code === 'Unidentified') {
  92. if (keysym) {
  93. // If it's a virtual keyboard then it should be
  94. // sufficient to just send press and release right
  95. // after each other
  96. this._sendKeyEvent(keysym, 'Unidentified', true);
  97. this._sendKeyEvent(keysym, 'Unidentified', false);
  98. }
  99. stopEvent(e);
  100. return;
  101. }
  102. // Alt behaves more like AltGraph on macOS, so shuffle the
  103. // keys around a bit to make things more sane for the remote
  104. // server. This method is used by RealVNC and TigerVNC (and
  105. // possibly others).
  106. if (isMac()) {
  107. switch (keysym) {
  108. case KeyTable.XK_Super_L:
  109. keysym = KeyTable.XK_Alt_L;
  110. break;
  111. case KeyTable.XK_Super_R:
  112. keysym = KeyTable.XK_Super_L;
  113. break;
  114. case KeyTable.XK_Alt_L:
  115. keysym = KeyTable.XK_Mode_switch;
  116. break;
  117. case KeyTable.XK_Alt_R:
  118. keysym = KeyTable.XK_ISO_Level3_Shift;
  119. break;
  120. }
  121. }
  122. // Is this key already pressed? If so, then we must use the
  123. // same keysym or we'll confuse the server
  124. if (code in this._keyDownList) {
  125. keysym = this._keyDownList[code];
  126. }
  127. // macOS doesn't send proper key events for modifiers, only
  128. // state change events. That gets extra confusing for CapsLock
  129. // which toggles on each press, but not on release. So pretend
  130. // it was a quick press and release of the button.
  131. if (isMac() && (code === 'CapsLock')) {
  132. this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', true);
  133. this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', false);
  134. stopEvent(e);
  135. return;
  136. }
  137. // If this is a legacy browser then we'll need to wait for
  138. // a keypress event as well
  139. if (!keysym) {
  140. this._pendingKey = code;
  141. return;
  142. }
  143. this._pendingKey = null;
  144. stopEvent(e);
  145. this._keyDownList[code] = keysym;
  146. this._sendKeyEvent(keysym, code, true);
  147. },
  148. // Legacy event for browsers without code/key
  149. _handleKeyPress: function (e) {
  150. if (!this._focused) { return; }
  151. stopEvent(e);
  152. // Are we expecting a keypress?
  153. if (this._pendingKey === null) {
  154. return;
  155. }
  156. var code = this._getKeyCode(e);
  157. var keysym = KeyboardUtil.getKeysym(e);
  158. // The key we were waiting for?
  159. if ((code !== 'Unidentified') && (code != this._pendingKey)) {
  160. return;
  161. }
  162. code = this._pendingKey;
  163. this._pendingKey = null;
  164. if (!keysym) {
  165. console.log('keypress with no keysym:', e);
  166. return;
  167. }
  168. this._keyDownList[code] = keysym;
  169. this._sendKeyEvent(keysym, code, true);
  170. },
  171. _handleKeyUp: function (e) {
  172. if (!this._focused) { return; }
  173. stopEvent(e);
  174. var code = this._getKeyCode(e);
  175. // See comment in _handleKeyDown()
  176. if (isMac() && (code === 'CapsLock')) {
  177. this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', true);
  178. this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', false);
  179. return;
  180. }
  181. // Do we really think this key is down?
  182. if (!(code in this._keyDownList)) {
  183. return;
  184. }
  185. this._sendKeyEvent(this._keyDownList[code], code, false);
  186. delete this._keyDownList[code];
  187. },
  188. _allKeysUp: function () {
  189. Log.Debug(">> Keyboard.allKeysUp");
  190. for (var code in this._keyDownList) {
  191. this._sendKeyEvent(this._keyDownList[code], code, false);
  192. };
  193. this._keyDownList = {};
  194. Log.Debug("<< Keyboard.allKeysUp");
  195. },
  196. // Public methods
  197. grab: function () {
  198. //Log.Debug(">> Keyboard.grab");
  199. var c = this._target;
  200. c.addEventListener('keydown', this._eventHandlers.keydown);
  201. c.addEventListener('keyup', this._eventHandlers.keyup);
  202. c.addEventListener('keypress', this._eventHandlers.keypress);
  203. // Release (key up) if window loses focus
  204. window.addEventListener('blur', this._eventHandlers.blur);
  205. //Log.Debug("<< Keyboard.grab");
  206. },
  207. ungrab: function () {
  208. //Log.Debug(">> Keyboard.ungrab");
  209. var c = this._target;
  210. c.removeEventListener('keydown', this._eventHandlers.keydown);
  211. c.removeEventListener('keyup', this._eventHandlers.keyup);
  212. c.removeEventListener('keypress', this._eventHandlers.keypress);
  213. window.removeEventListener('blur', this._eventHandlers.blur);
  214. // Release (key up) all keys that are in a down state
  215. this._allKeysUp();
  216. //Log.Debug(">> Keyboard.ungrab");
  217. },
  218. };
  219. make_properties(Keyboard, [
  220. ['target', 'wo', 'dom'], // DOM element that captures keyboard input
  221. ['focused', 'rw', 'bool'], // Capture and send key events
  222. ['onKeyEvent', 'rw', 'func'] // Handler for key press/release
  223. ]);
  224. function Mouse(defaults) {
  225. this._doubleClickTimer = null;
  226. this._lastTouchPos = null;
  227. // Configuration attributes
  228. set_defaults(this, defaults, {
  229. 'target': document,
  230. 'focused': true,
  231. 'touchButton': 1
  232. });
  233. this._eventHandlers = {
  234. 'mousedown': this._handleMouseDown.bind(this),
  235. 'mouseup': this._handleMouseUp.bind(this),
  236. 'mousemove': this._handleMouseMove.bind(this),
  237. 'mousewheel': this._handleMouseWheel.bind(this),
  238. 'mousedisable': this._handleMouseDisable.bind(this)
  239. };
  240. };
  241. Mouse.prototype = {
  242. // private methods
  243. _resetDoubleClickTimer: function () {
  244. this._doubleClickTimer = null;
  245. },
  246. _handleMouseButton: function (e, down) {
  247. if (!this._focused) { return; }
  248. var pos = this._getMousePosition(e);
  249. var bmask;
  250. if (e.touches || e.changedTouches) {
  251. // Touch device
  252. // When two touches occur within 500 ms of each other and are
  253. // close enough together a double click is triggered.
  254. if (down == 1) {
  255. if (this._doubleClickTimer === null) {
  256. this._lastTouchPos = pos;
  257. } else {
  258. clearTimeout(this._doubleClickTimer);
  259. // When the distance between the two touches is small enough
  260. // force the position of the latter touch to the position of
  261. // the first.
  262. var xs = this._lastTouchPos.x - pos.x;
  263. var ys = this._lastTouchPos.y - pos.y;
  264. var d = Math.sqrt((xs * xs) + (ys * ys));
  265. // The goal is to trigger on a certain physical width, the
  266. // devicePixelRatio brings us a bit closer but is not optimal.
  267. var threshold = 20 * (window.devicePixelRatio || 1);
  268. if (d < threshold) {
  269. pos = this._lastTouchPos;
  270. }
  271. }
  272. this._doubleClickTimer = setTimeout(this._resetDoubleClickTimer.bind(this), 500);
  273. }
  274. bmask = this._touchButton;
  275. // If bmask is set
  276. } else if (e.which) {
  277. /* everything except IE */
  278. bmask = 1 << e.button;
  279. } else {
  280. /* IE including 9 */
  281. bmask = (e.button & 0x1) + // Left
  282. (e.button & 0x2) * 2 + // Right
  283. (e.button & 0x4) / 2; // Middle
  284. }
  285. if (this._onMouseButton) {
  286. Log.Debug("onMouseButton " + (down ? "down" : "up") +
  287. ", x: " + pos.x + ", y: " + pos.y + ", bmask: " + bmask);
  288. this._onMouseButton(pos.x, pos.y, down, bmask);
  289. }
  290. stopEvent(e);
  291. },
  292. _handleMouseDown: function (e) {
  293. // Touch events have implicit capture
  294. if (e.type === "mousedown") {
  295. setCapture(this._target);
  296. }
  297. this._handleMouseButton(e, 1);
  298. },
  299. _handleMouseUp: function (e) {
  300. this._handleMouseButton(e, 0);
  301. },
  302. _handleMouseWheel: function (e) {
  303. if (!this._focused) { return; }
  304. var pos = this._getMousePosition(e);
  305. if (this._onMouseButton) {
  306. if (e.deltaX < 0) {
  307. this._onMouseButton(pos.x, pos.y, 1, 1 << 5);
  308. this._onMouseButton(pos.x, pos.y, 0, 1 << 5);
  309. } else if (e.deltaX > 0) {
  310. this._onMouseButton(pos.x, pos.y, 1, 1 << 6);
  311. this._onMouseButton(pos.x, pos.y, 0, 1 << 6);
  312. }
  313. if (e.deltaY < 0) {
  314. this._onMouseButton(pos.x, pos.y, 1, 1 << 3);
  315. this._onMouseButton(pos.x, pos.y, 0, 1 << 3);
  316. } else if (e.deltaY > 0) {
  317. this._onMouseButton(pos.x, pos.y, 1, 1 << 4);
  318. this._onMouseButton(pos.x, pos.y, 0, 1 << 4);
  319. }
  320. }
  321. stopEvent(e);
  322. },
  323. _handleMouseMove: function (e) {
  324. if (! this._focused) { return; }
  325. var pos = this._getMousePosition(e);
  326. if (this._onMouseMove) {
  327. this._onMouseMove(pos.x, pos.y);
  328. }
  329. stopEvent(e);
  330. },
  331. _handleMouseDisable: function (e) {
  332. if (!this._focused) { return; }
  333. /*
  334. * Stop propagation if inside canvas area
  335. * Note: This is only needed for the 'click' event as it fails
  336. * to fire properly for the target element so we have
  337. * to listen on the document element instead.
  338. */
  339. if (e.target == this._target) {
  340. stopEvent(e);
  341. }
  342. },
  343. // Return coordinates relative to target
  344. _getMousePosition: function(e) {
  345. e = getPointerEvent(e);
  346. var bounds = this._target.getBoundingClientRect();
  347. var x, y;
  348. // Clip to target bounds
  349. if (e.clientX < bounds.left) {
  350. x = 0;
  351. } else if (e.clientX >= bounds.right) {
  352. x = bounds.width - 1;
  353. } else {
  354. x = e.clientX - bounds.left;
  355. }
  356. if (e.clientY < bounds.top) {
  357. y = 0;
  358. } else if (e.clientY >= bounds.bottom) {
  359. y = bounds.height - 1;
  360. } else {
  361. y = e.clientY - bounds.top;
  362. }
  363. return {x:x, y:y};
  364. },
  365. // Public methods
  366. grab: function () {
  367. var c = this._target;
  368. if (isTouchDevice) {
  369. c.addEventListener('touchstart', this._eventHandlers.mousedown);
  370. c.addEventListener('touchend', this._eventHandlers.mouseup);
  371. c.addEventListener('touchmove', this._eventHandlers.mousemove);
  372. }
  373. c.addEventListener('mousedown', this._eventHandlers.mousedown);
  374. c.addEventListener('mouseup', this._eventHandlers.mouseup);
  375. c.addEventListener('mousemove', this._eventHandlers.mousemove);
  376. c.addEventListener('wheel', this._eventHandlers.mousewheel);
  377. /* Prevent middle-click pasting (see above for why we bind to document) */
  378. document.addEventListener('click', this._eventHandlers.mousedisable);
  379. /* preventDefault() on mousedown doesn't stop this event for some
  380. reason so we have to explicitly block it */
  381. c.addEventListener('contextmenu', this._eventHandlers.mousedisable);
  382. },
  383. ungrab: function () {
  384. var c = this._target;
  385. if (isTouchDevice) {
  386. c.removeEventListener('touchstart', this._eventHandlers.mousedown);
  387. c.removeEventListener('touchend', this._eventHandlers.mouseup);
  388. c.removeEventListener('touchmove', this._eventHandlers.mousemove);
  389. }
  390. c.removeEventListener('mousedown', this._eventHandlers.mousedown);
  391. c.removeEventListener('mouseup', this._eventHandlers.mouseup);
  392. c.removeEventListener('mousemove', this._eventHandlers.mousemove);
  393. c.removeEventListener('wheel', this._eventHandlers.mousewheel);
  394. document.removeEventListener('click', this._eventHandlers.mousedisable);
  395. c.removeEventListener('contextmenu', this._eventHandlers.mousedisable);
  396. }
  397. };
  398. make_properties(Mouse, [
  399. ['target', 'ro', 'dom'], // DOM element that captures mouse input
  400. ['focused', 'rw', 'bool'], // Capture and send mouse clicks/movement
  401. ['onMouseButton', 'rw', 'func'], // Handler for mouse button click/release
  402. ['onMouseMove', 'rw', 'func'], // Handler for mouse movement
  403. ['touchButton', 'rw', 'int'] // Button mask (1, 2, 4) for touch devices (0 means ignore clicks)
  404. ]);
  405. export { Keyboard, Mouse };