mouseHandler.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465
  1. /*---------------------------------------------------------------------------------------------
  2. * Copyright (c) Microsoft Corporation. All rights reserved.
  3. * Licensed under the MIT License. See License.txt in the project root for license information.
  4. *--------------------------------------------------------------------------------------------*/
  5. import * as dom from '../../../base/browser/dom.js';
  6. import { StandardWheelEvent } from '../../../base/browser/mouseEvent.js';
  7. import { TimeoutTimer } from '../../../base/common/async.js';
  8. import { Disposable } from '../../../base/common/lifecycle.js';
  9. import * as platform from '../../../base/common/platform.js';
  10. import { HitTestContext, MouseTarget, MouseTargetFactory } from './mouseTarget.js';
  11. import { ClientCoordinates, EditorMouseEventFactory, GlobalEditorMouseMoveMonitor, createEditorPagePosition } from '../editorDom.js';
  12. import { EditorZoom } from '../../common/config/editorZoom.js';
  13. import { Position } from '../../common/core/position.js';
  14. import { Selection } from '../../common/core/selection.js';
  15. import { ViewEventHandler } from '../../common/viewModel/viewEventHandler.js';
  16. /**
  17. * Merges mouse events when mouse move events are throttled
  18. */
  19. export function createMouseMoveEventMerger(mouseTargetFactory) {
  20. return function (lastEvent, currentEvent) {
  21. let targetIsWidget = false;
  22. if (mouseTargetFactory) {
  23. targetIsWidget = mouseTargetFactory.mouseTargetIsWidget(currentEvent);
  24. }
  25. if (!targetIsWidget) {
  26. currentEvent.preventDefault();
  27. }
  28. return currentEvent;
  29. };
  30. }
  31. export class MouseHandler extends ViewEventHandler {
  32. constructor(context, viewController, viewHelper) {
  33. super();
  34. this._context = context;
  35. this.viewController = viewController;
  36. this.viewHelper = viewHelper;
  37. this.mouseTargetFactory = new MouseTargetFactory(this._context, viewHelper);
  38. this._mouseDownOperation = this._register(new MouseDownOperation(this._context, this.viewController, this.viewHelper, (e, testEventTarget) => this._createMouseTarget(e, testEventTarget), (e) => this._getMouseColumn(e)));
  39. this.lastMouseLeaveTime = -1;
  40. this._height = this._context.configuration.options.get(130 /* layoutInfo */).height;
  41. const mouseEvents = new EditorMouseEventFactory(this.viewHelper.viewDomNode);
  42. this._register(mouseEvents.onContextMenu(this.viewHelper.viewDomNode, (e) => this._onContextMenu(e, true)));
  43. this._register(mouseEvents.onMouseMoveThrottled(this.viewHelper.viewDomNode, (e) => this._onMouseMove(e), createMouseMoveEventMerger(this.mouseTargetFactory), MouseHandler.MOUSE_MOVE_MINIMUM_TIME));
  44. this._register(mouseEvents.onMouseUp(this.viewHelper.viewDomNode, (e) => this._onMouseUp(e)));
  45. this._register(mouseEvents.onMouseLeave(this.viewHelper.viewDomNode, (e) => this._onMouseLeave(e)));
  46. this._register(mouseEvents.onMouseDown(this.viewHelper.viewDomNode, (e) => this._onMouseDown(e)));
  47. const onMouseWheel = (browserEvent) => {
  48. this.viewController.emitMouseWheel(browserEvent);
  49. if (!this._context.configuration.options.get(67 /* mouseWheelZoom */)) {
  50. return;
  51. }
  52. const e = new StandardWheelEvent(browserEvent);
  53. const doMouseWheelZoom = (platform.isMacintosh
  54. // on macOS we support cmd + two fingers scroll (`metaKey` set)
  55. // and also the two fingers pinch gesture (`ctrKey` set)
  56. ? ((browserEvent.metaKey || browserEvent.ctrlKey) && !browserEvent.shiftKey && !browserEvent.altKey)
  57. : (browserEvent.ctrlKey && !browserEvent.metaKey && !browserEvent.shiftKey && !browserEvent.altKey));
  58. if (doMouseWheelZoom) {
  59. const zoomLevel = EditorZoom.getZoomLevel();
  60. const delta = e.deltaY > 0 ? 1 : -1;
  61. EditorZoom.setZoomLevel(zoomLevel + delta);
  62. e.preventDefault();
  63. e.stopPropagation();
  64. }
  65. };
  66. this._register(dom.addDisposableListener(this.viewHelper.viewDomNode, dom.EventType.MOUSE_WHEEL, onMouseWheel, { capture: true, passive: false }));
  67. this._context.addEventHandler(this);
  68. }
  69. dispose() {
  70. this._context.removeEventHandler(this);
  71. super.dispose();
  72. }
  73. // --- begin event handlers
  74. onConfigurationChanged(e) {
  75. if (e.hasChanged(130 /* layoutInfo */)) {
  76. // layout change
  77. const height = this._context.configuration.options.get(130 /* layoutInfo */).height;
  78. if (this._height !== height) {
  79. this._height = height;
  80. this._mouseDownOperation.onHeightChanged();
  81. }
  82. }
  83. return false;
  84. }
  85. onCursorStateChanged(e) {
  86. this._mouseDownOperation.onCursorStateChanged(e);
  87. return false;
  88. }
  89. onFocusChanged(e) {
  90. return false;
  91. }
  92. onScrollChanged(e) {
  93. this._mouseDownOperation.onScrollChanged();
  94. return false;
  95. }
  96. // --- end event handlers
  97. getTargetAtClientPoint(clientX, clientY) {
  98. const clientPos = new ClientCoordinates(clientX, clientY);
  99. const pos = clientPos.toPageCoordinates();
  100. const editorPos = createEditorPagePosition(this.viewHelper.viewDomNode);
  101. if (pos.y < editorPos.y || pos.y > editorPos.y + editorPos.height || pos.x < editorPos.x || pos.x > editorPos.x + editorPos.width) {
  102. return null;
  103. }
  104. return this.mouseTargetFactory.createMouseTarget(this.viewHelper.getLastRenderData(), editorPos, pos, null);
  105. }
  106. _createMouseTarget(e, testEventTarget) {
  107. let target = e.target;
  108. if (!this.viewHelper.viewDomNode.contains(target)) {
  109. const shadowRoot = dom.getShadowRoot(this.viewHelper.viewDomNode);
  110. if (shadowRoot) {
  111. target = shadowRoot.elementsFromPoint(e.posx, e.posy).find((el) => this.viewHelper.viewDomNode.contains(el));
  112. }
  113. }
  114. return this.mouseTargetFactory.createMouseTarget(this.viewHelper.getLastRenderData(), e.editorPos, e.pos, testEventTarget ? target : null);
  115. }
  116. _getMouseColumn(e) {
  117. return this.mouseTargetFactory.getMouseColumn(e.editorPos, e.pos);
  118. }
  119. _onContextMenu(e, testEventTarget) {
  120. this.viewController.emitContextMenu({
  121. event: e,
  122. target: this._createMouseTarget(e, testEventTarget)
  123. });
  124. }
  125. _onMouseMove(e) {
  126. if (this._mouseDownOperation.isActive()) {
  127. // In selection/drag operation
  128. return;
  129. }
  130. const actualMouseMoveTime = e.timestamp;
  131. if (actualMouseMoveTime < this.lastMouseLeaveTime) {
  132. // Due to throttling, this event occurred before the mouse left the editor, therefore ignore it.
  133. return;
  134. }
  135. this.viewController.emitMouseMove({
  136. event: e,
  137. target: this._createMouseTarget(e, true)
  138. });
  139. }
  140. _onMouseLeave(e) {
  141. this.lastMouseLeaveTime = (new Date()).getTime();
  142. this.viewController.emitMouseLeave({
  143. event: e,
  144. target: null
  145. });
  146. }
  147. _onMouseUp(e) {
  148. this.viewController.emitMouseUp({
  149. event: e,
  150. target: this._createMouseTarget(e, true)
  151. });
  152. }
  153. _onMouseDown(e) {
  154. const t = this._createMouseTarget(e, true);
  155. const targetIsContent = (t.type === 6 /* CONTENT_TEXT */ || t.type === 7 /* CONTENT_EMPTY */);
  156. const targetIsGutter = (t.type === 2 /* GUTTER_GLYPH_MARGIN */ || t.type === 3 /* GUTTER_LINE_NUMBERS */ || t.type === 4 /* GUTTER_LINE_DECORATIONS */);
  157. const targetIsLineNumbers = (t.type === 3 /* GUTTER_LINE_NUMBERS */);
  158. const selectOnLineNumbers = this._context.configuration.options.get(97 /* selectOnLineNumbers */);
  159. const targetIsViewZone = (t.type === 8 /* CONTENT_VIEW_ZONE */ || t.type === 5 /* GUTTER_VIEW_ZONE */);
  160. const targetIsWidget = (t.type === 9 /* CONTENT_WIDGET */);
  161. let shouldHandle = e.leftButton || e.middleButton;
  162. if (platform.isMacintosh && e.leftButton && e.ctrlKey) {
  163. shouldHandle = false;
  164. }
  165. const focus = () => {
  166. e.preventDefault();
  167. this.viewHelper.focusTextArea();
  168. };
  169. if (shouldHandle && (targetIsContent || (targetIsLineNumbers && selectOnLineNumbers))) {
  170. focus();
  171. this._mouseDownOperation.start(t.type, e);
  172. }
  173. else if (targetIsGutter) {
  174. // Do not steal focus
  175. e.preventDefault();
  176. }
  177. else if (targetIsViewZone) {
  178. const viewZoneData = t.detail;
  179. if (this.viewHelper.shouldSuppressMouseDownOnViewZone(viewZoneData.viewZoneId)) {
  180. focus();
  181. this._mouseDownOperation.start(t.type, e);
  182. e.preventDefault();
  183. }
  184. }
  185. else if (targetIsWidget && this.viewHelper.shouldSuppressMouseDownOnWidget(t.detail)) {
  186. focus();
  187. e.preventDefault();
  188. }
  189. this.viewController.emitMouseDown({
  190. event: e,
  191. target: t
  192. });
  193. }
  194. }
  195. MouseHandler.MOUSE_MOVE_MINIMUM_TIME = 100; // ms
  196. class MouseDownOperation extends Disposable {
  197. constructor(context, viewController, viewHelper, createMouseTarget, getMouseColumn) {
  198. super();
  199. this._context = context;
  200. this._viewController = viewController;
  201. this._viewHelper = viewHelper;
  202. this._createMouseTarget = createMouseTarget;
  203. this._getMouseColumn = getMouseColumn;
  204. this._mouseMoveMonitor = this._register(new GlobalEditorMouseMoveMonitor(this._viewHelper.viewDomNode));
  205. this._onScrollTimeout = this._register(new TimeoutTimer());
  206. this._mouseState = new MouseDownState();
  207. this._currentSelection = new Selection(1, 1, 1, 1);
  208. this._isActive = false;
  209. this._lastMouseEvent = null;
  210. }
  211. dispose() {
  212. super.dispose();
  213. }
  214. isActive() {
  215. return this._isActive;
  216. }
  217. _onMouseDownThenMove(e) {
  218. this._lastMouseEvent = e;
  219. this._mouseState.setModifiers(e);
  220. const position = this._findMousePosition(e, true);
  221. if (!position) {
  222. // Ignoring because position is unknown
  223. return;
  224. }
  225. if (this._mouseState.isDragAndDrop) {
  226. this._viewController.emitMouseDrag({
  227. event: e,
  228. target: position
  229. });
  230. }
  231. else {
  232. this._dispatchMouse(position, true);
  233. }
  234. }
  235. start(targetType, e) {
  236. this._lastMouseEvent = e;
  237. this._mouseState.setStartedOnLineNumbers(targetType === 3 /* GUTTER_LINE_NUMBERS */);
  238. this._mouseState.setStartButtons(e);
  239. this._mouseState.setModifiers(e);
  240. const position = this._findMousePosition(e, true);
  241. if (!position || !position.position) {
  242. // Ignoring because position is unknown
  243. return;
  244. }
  245. this._mouseState.trySetCount(e.detail, position.position);
  246. // Overwrite the detail of the MouseEvent, as it will be sent out in an event and contributions might rely on it.
  247. e.detail = this._mouseState.count;
  248. const options = this._context.configuration.options;
  249. if (!options.get(80 /* readOnly */)
  250. && options.get(31 /* dragAndDrop */)
  251. && !options.get(18 /* columnSelection */)
  252. && !this._mouseState.altKey // we don't support multiple mouse
  253. && e.detail < 2 // only single click on a selection can work
  254. && !this._isActive // the mouse is not down yet
  255. && !this._currentSelection.isEmpty() // we don't drag single cursor
  256. && (position.type === 6 /* CONTENT_TEXT */) // single click on text
  257. && position.position && this._currentSelection.containsPosition(position.position) // single click on a selection
  258. ) {
  259. this._mouseState.isDragAndDrop = true;
  260. this._isActive = true;
  261. this._mouseMoveMonitor.startMonitoring(e.target, e.buttons, createMouseMoveEventMerger(null), (e) => this._onMouseDownThenMove(e), (browserEvent) => {
  262. const position = this._findMousePosition(this._lastMouseEvent, true);
  263. if (browserEvent && browserEvent instanceof KeyboardEvent) {
  264. // cancel
  265. this._viewController.emitMouseDropCanceled();
  266. }
  267. else {
  268. this._viewController.emitMouseDrop({
  269. event: this._lastMouseEvent,
  270. target: (position ? this._createMouseTarget(this._lastMouseEvent, true) : null) // Ignoring because position is unknown, e.g., Content View Zone
  271. });
  272. }
  273. this._stop();
  274. });
  275. return;
  276. }
  277. this._mouseState.isDragAndDrop = false;
  278. this._dispatchMouse(position, e.shiftKey);
  279. if (!this._isActive) {
  280. this._isActive = true;
  281. this._mouseMoveMonitor.startMonitoring(e.target, e.buttons, createMouseMoveEventMerger(null), (e) => this._onMouseDownThenMove(e), () => this._stop());
  282. }
  283. }
  284. _stop() {
  285. this._isActive = false;
  286. this._onScrollTimeout.cancel();
  287. }
  288. onHeightChanged() {
  289. this._mouseMoveMonitor.stopMonitoring();
  290. }
  291. onScrollChanged() {
  292. if (!this._isActive) {
  293. return;
  294. }
  295. this._onScrollTimeout.setIfNotSet(() => {
  296. if (!this._lastMouseEvent) {
  297. return;
  298. }
  299. const position = this._findMousePosition(this._lastMouseEvent, false);
  300. if (!position) {
  301. // Ignoring because position is unknown
  302. return;
  303. }
  304. if (this._mouseState.isDragAndDrop) {
  305. // Ignoring because users are dragging the text
  306. return;
  307. }
  308. this._dispatchMouse(position, true);
  309. }, 10);
  310. }
  311. onCursorStateChanged(e) {
  312. this._currentSelection = e.selections[0];
  313. }
  314. _getPositionOutsideEditor(e) {
  315. const editorContent = e.editorPos;
  316. const model = this._context.model;
  317. const viewLayout = this._context.viewLayout;
  318. const mouseColumn = this._getMouseColumn(e);
  319. if (e.posy < editorContent.y) {
  320. const verticalOffset = Math.max(viewLayout.getCurrentScrollTop() - (editorContent.y - e.posy), 0);
  321. const viewZoneData = HitTestContext.getZoneAtCoord(this._context, verticalOffset);
  322. if (viewZoneData) {
  323. const newPosition = this._helpPositionJumpOverViewZone(viewZoneData);
  324. if (newPosition) {
  325. return new MouseTarget(null, 13 /* OUTSIDE_EDITOR */, mouseColumn, newPosition);
  326. }
  327. }
  328. const aboveLineNumber = viewLayout.getLineNumberAtVerticalOffset(verticalOffset);
  329. return new MouseTarget(null, 13 /* OUTSIDE_EDITOR */, mouseColumn, new Position(aboveLineNumber, 1));
  330. }
  331. if (e.posy > editorContent.y + editorContent.height) {
  332. const verticalOffset = viewLayout.getCurrentScrollTop() + (e.posy - editorContent.y);
  333. const viewZoneData = HitTestContext.getZoneAtCoord(this._context, verticalOffset);
  334. if (viewZoneData) {
  335. const newPosition = this._helpPositionJumpOverViewZone(viewZoneData);
  336. if (newPosition) {
  337. return new MouseTarget(null, 13 /* OUTSIDE_EDITOR */, mouseColumn, newPosition);
  338. }
  339. }
  340. const belowLineNumber = viewLayout.getLineNumberAtVerticalOffset(verticalOffset);
  341. return new MouseTarget(null, 13 /* OUTSIDE_EDITOR */, mouseColumn, new Position(belowLineNumber, model.getLineMaxColumn(belowLineNumber)));
  342. }
  343. const possibleLineNumber = viewLayout.getLineNumberAtVerticalOffset(viewLayout.getCurrentScrollTop() + (e.posy - editorContent.y));
  344. if (e.posx < editorContent.x) {
  345. return new MouseTarget(null, 13 /* OUTSIDE_EDITOR */, mouseColumn, new Position(possibleLineNumber, 1));
  346. }
  347. if (e.posx > editorContent.x + editorContent.width) {
  348. return new MouseTarget(null, 13 /* OUTSIDE_EDITOR */, mouseColumn, new Position(possibleLineNumber, model.getLineMaxColumn(possibleLineNumber)));
  349. }
  350. return null;
  351. }
  352. _findMousePosition(e, testEventTarget) {
  353. const positionOutsideEditor = this._getPositionOutsideEditor(e);
  354. if (positionOutsideEditor) {
  355. return positionOutsideEditor;
  356. }
  357. const t = this._createMouseTarget(e, testEventTarget);
  358. const hintedPosition = t.position;
  359. if (!hintedPosition) {
  360. return null;
  361. }
  362. if (t.type === 8 /* CONTENT_VIEW_ZONE */ || t.type === 5 /* GUTTER_VIEW_ZONE */) {
  363. const newPosition = this._helpPositionJumpOverViewZone(t.detail);
  364. if (newPosition) {
  365. return new MouseTarget(t.element, t.type, t.mouseColumn, newPosition, null, t.detail);
  366. }
  367. }
  368. return t;
  369. }
  370. _helpPositionJumpOverViewZone(viewZoneData) {
  371. // Force position on view zones to go above or below depending on where selection started from
  372. const selectionStart = new Position(this._currentSelection.selectionStartLineNumber, this._currentSelection.selectionStartColumn);
  373. const positionBefore = viewZoneData.positionBefore;
  374. const positionAfter = viewZoneData.positionAfter;
  375. if (positionBefore && positionAfter) {
  376. if (positionBefore.isBefore(selectionStart)) {
  377. return positionBefore;
  378. }
  379. else {
  380. return positionAfter;
  381. }
  382. }
  383. return null;
  384. }
  385. _dispatchMouse(position, inSelectionMode) {
  386. if (!position.position) {
  387. return;
  388. }
  389. this._viewController.dispatchMouse({
  390. position: position.position,
  391. mouseColumn: position.mouseColumn,
  392. startedOnLineNumbers: this._mouseState.startedOnLineNumbers,
  393. inSelectionMode: inSelectionMode,
  394. mouseDownCount: this._mouseState.count,
  395. altKey: this._mouseState.altKey,
  396. ctrlKey: this._mouseState.ctrlKey,
  397. metaKey: this._mouseState.metaKey,
  398. shiftKey: this._mouseState.shiftKey,
  399. leftButton: this._mouseState.leftButton,
  400. middleButton: this._mouseState.middleButton,
  401. });
  402. }
  403. }
  404. class MouseDownState {
  405. constructor() {
  406. this._altKey = false;
  407. this._ctrlKey = false;
  408. this._metaKey = false;
  409. this._shiftKey = false;
  410. this._leftButton = false;
  411. this._middleButton = false;
  412. this._startedOnLineNumbers = false;
  413. this._lastMouseDownPosition = null;
  414. this._lastMouseDownPositionEqualCount = 0;
  415. this._lastMouseDownCount = 0;
  416. this._lastSetMouseDownCountTime = 0;
  417. this.isDragAndDrop = false;
  418. }
  419. get altKey() { return this._altKey; }
  420. get ctrlKey() { return this._ctrlKey; }
  421. get metaKey() { return this._metaKey; }
  422. get shiftKey() { return this._shiftKey; }
  423. get leftButton() { return this._leftButton; }
  424. get middleButton() { return this._middleButton; }
  425. get startedOnLineNumbers() { return this._startedOnLineNumbers; }
  426. get count() {
  427. return this._lastMouseDownCount;
  428. }
  429. setModifiers(source) {
  430. this._altKey = source.altKey;
  431. this._ctrlKey = source.ctrlKey;
  432. this._metaKey = source.metaKey;
  433. this._shiftKey = source.shiftKey;
  434. }
  435. setStartButtons(source) {
  436. this._leftButton = source.leftButton;
  437. this._middleButton = source.middleButton;
  438. }
  439. setStartedOnLineNumbers(startedOnLineNumbers) {
  440. this._startedOnLineNumbers = startedOnLineNumbers;
  441. }
  442. trySetCount(setMouseDownCount, newMouseDownPosition) {
  443. // a. Invalidate multiple clicking if too much time has passed (will be hit by IE because the detail field of mouse events contains garbage in IE10)
  444. const currentTime = (new Date()).getTime();
  445. if (currentTime - this._lastSetMouseDownCountTime > MouseDownState.CLEAR_MOUSE_DOWN_COUNT_TIME) {
  446. setMouseDownCount = 1;
  447. }
  448. this._lastSetMouseDownCountTime = currentTime;
  449. // b. Ensure that we don't jump from single click to triple click in one go (will be hit by IE because the detail field of mouse events contains garbage in IE10)
  450. if (setMouseDownCount > this._lastMouseDownCount + 1) {
  451. setMouseDownCount = this._lastMouseDownCount + 1;
  452. }
  453. // c. Invalidate multiple clicking if the logical position is different
  454. if (this._lastMouseDownPosition && this._lastMouseDownPosition.equals(newMouseDownPosition)) {
  455. this._lastMouseDownPositionEqualCount++;
  456. }
  457. else {
  458. this._lastMouseDownPositionEqualCount = 1;
  459. }
  460. this._lastMouseDownPosition = newMouseDownPosition;
  461. // Finally set the lastMouseDownCount
  462. this._lastMouseDownCount = Math.min(setMouseDownCount, this._lastMouseDownPositionEqualCount);
  463. }
  464. }
  465. MouseDownState.CLEAR_MOUSE_DOWN_COUNT_TIME = 400; // ms