Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/browser/controller/mouseHandler.ts
5222 views
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
6
import * as dom from '../../../base/browser/dom.js';
7
import { StandardWheelEvent, IMouseWheelEvent } from '../../../base/browser/mouseEvent.js';
8
import { Disposable, IDisposable } from '../../../base/common/lifecycle.js';
9
import * as platform from '../../../base/common/platform.js';
10
import { HitTestContext, MouseTarget, MouseTargetFactory, PointerHandlerLastRenderData } from './mouseTarget.js';
11
import { IMouseTarget, IMouseTargetViewZoneData, MouseTargetType } from '../editorBrowser.js';
12
import { ClientCoordinates, EditorMouseEvent, EditorMouseEventFactory, GlobalEditorPointerMoveMonitor, createEditorPagePosition, createCoordinatesRelativeToEditor } from '../editorDom.js';
13
import { ViewController } from '../view/viewController.js';
14
import { EditorZoom } from '../../common/config/editorZoom.js';
15
import { Position } from '../../common/core/position.js';
16
import { Selection } from '../../common/core/selection.js';
17
import { HorizontalPosition } from '../view/renderingContext.js';
18
import { ViewContext } from '../../common/viewModel/viewContext.js';
19
import * as viewEvents from '../../common/viewEvents.js';
20
import { ViewEventHandler } from '../../common/viewEventHandler.js';
21
import { EditorOption } from '../../common/config/editorOptions.js';
22
import { NavigationCommandRevealType } from '../coreCommands.js';
23
import { MouseWheelClassifier } from '../../../base/browser/ui/scrollbar/scrollableElement.js';
24
import type { ViewLinesGpu } from '../viewParts/viewLinesGpu/viewLinesGpu.js';
25
import { TopBottomDragScrolling, LeftRightDragScrolling } from './dragScrolling.js';
26
import { TextDirection } from '../../common/model.js';
27
28
export interface IPointerHandlerHelper {
29
viewDomNode: HTMLElement;
30
linesContentDomNode: HTMLElement;
31
viewLinesDomNode: HTMLElement;
32
viewLinesGpu: ViewLinesGpu | undefined;
33
34
focusTextArea(): void;
35
dispatchTextAreaEvent(event: CustomEvent): void;
36
37
/**
38
* Get the last rendered information for cursors & textarea.
39
*/
40
getLastRenderData(): PointerHandlerLastRenderData;
41
42
/**
43
* Render right now
44
*/
45
renderNow(): void;
46
47
shouldSuppressMouseDownOnViewZone(viewZoneId: string): boolean;
48
shouldSuppressMouseDownOnWidget(widgetId: string): boolean;
49
50
/**
51
* Decode a position from a rendered dom node
52
*/
53
getPositionFromDOMInfo(spanNode: HTMLElement, offset: number): Position | null;
54
55
visibleRangeForPosition(lineNumber: number, column: number): HorizontalPosition | null;
56
getLineWidth(lineNumber: number): number;
57
}
58
59
export class MouseHandler extends ViewEventHandler {
60
61
protected _context: ViewContext;
62
protected viewController: ViewController;
63
protected viewHelper: IPointerHandlerHelper;
64
protected mouseTargetFactory: MouseTargetFactory;
65
protected readonly _mouseDownOperation: MouseDownOperation;
66
private lastMouseLeaveTime: number;
67
private _height: number;
68
private _mouseLeaveMonitor: IDisposable | null = null;
69
70
constructor(context: ViewContext, viewController: ViewController, viewHelper: IPointerHandlerHelper) {
71
super();
72
73
this._context = context;
74
this.viewController = viewController;
75
this.viewHelper = viewHelper;
76
this.mouseTargetFactory = new MouseTargetFactory(this._context, viewHelper);
77
78
this._mouseDownOperation = this._register(new MouseDownOperation(
79
this._context,
80
this.viewController,
81
this.viewHelper,
82
this.mouseTargetFactory,
83
(e, testEventTarget) => this._createMouseTarget(e, testEventTarget),
84
(e) => this._getMouseColumn(e)
85
));
86
87
this.lastMouseLeaveTime = -1;
88
this._height = this._context.configuration.options.get(EditorOption.layoutInfo).height;
89
90
const mouseEvents = new EditorMouseEventFactory(this.viewHelper.viewDomNode);
91
92
this._register(mouseEvents.onContextMenu(this.viewHelper.viewDomNode, (e) => this._onContextMenu(e, true)));
93
94
this._register(mouseEvents.onMouseMove(this.viewHelper.viewDomNode, (e) => {
95
this._onMouseMove(e);
96
97
// See https://github.com/microsoft/vscode/issues/138789
98
// When moving the mouse really quickly, the browser sometimes forgets to
99
// send us a `mouseleave` or `mouseout` event. We therefore install here
100
// a global `mousemove` listener to manually recover if the mouse goes outside
101
// the editor. As soon as the mouse leaves outside of the editor, we
102
// remove this listener
103
104
if (!this._mouseLeaveMonitor) {
105
this._mouseLeaveMonitor = dom.addDisposableListener(this.viewHelper.viewDomNode.ownerDocument, 'mousemove', (e) => {
106
if (!this.viewHelper.viewDomNode.contains(e.target as Node | null)) {
107
// went outside the editor!
108
this._onMouseLeave(new EditorMouseEvent(e, false, this.viewHelper.viewDomNode));
109
}
110
});
111
}
112
}));
113
114
this._register(mouseEvents.onMouseUp(this.viewHelper.viewDomNode, (e) => this._onMouseUp(e)));
115
116
this._register(mouseEvents.onMouseLeave(this.viewHelper.viewDomNode, (e) => this._onMouseLeave(e)));
117
118
// `pointerdown` events can't be used to determine if there's a double click, or triple click
119
// because their `e.detail` is always 0.
120
// We will therefore save the pointer id for the mouse and then reuse it in the `mousedown` event
121
// for `element.setPointerCapture`.
122
let capturePointerId: number = 0;
123
this._register(mouseEvents.onPointerDown(this.viewHelper.viewDomNode, (e, pointerId) => {
124
capturePointerId = pointerId;
125
}));
126
// The `pointerup` listener registered by `GlobalEditorPointerMoveMonitor` does not get invoked 100% of the times.
127
// I speculate that this is because the `pointerup` listener is only registered during the `mousedown` event, and perhaps
128
// the `pointerup` event is already queued for dispatching, which makes it that the new listener doesn't get fired.
129
// See https://github.com/microsoft/vscode/issues/146486 for repro steps.
130
// To compensate for that, we simply register here a `pointerup` listener and just communicate it.
131
this._register(dom.addDisposableListener(this.viewHelper.viewDomNode, dom.EventType.POINTER_UP, (e: PointerEvent) => {
132
this._mouseDownOperation.onPointerUp();
133
}));
134
this._register(mouseEvents.onMouseDown(this.viewHelper.viewDomNode, (e) => this._onMouseDown(e, capturePointerId)));
135
this._setupMouseWheelZoomListener();
136
137
this._context.addEventHandler(this);
138
}
139
140
private _setupMouseWheelZoomListener(): void {
141
142
const classifier = MouseWheelClassifier.INSTANCE;
143
144
let prevMouseWheelTime = 0;
145
let gestureStartZoomLevel = EditorZoom.getZoomLevel();
146
let gestureHasZoomModifiers = false;
147
let gestureAccumulatedDelta = 0;
148
149
const onMouseWheel = (browserEvent: IMouseWheelEvent) => {
150
this.viewController.emitMouseWheel(browserEvent);
151
152
if (!this._context.configuration.options.get(EditorOption.mouseWheelZoom)) {
153
return;
154
}
155
156
const e = new StandardWheelEvent(browserEvent);
157
classifier.acceptStandardWheelEvent(e);
158
159
if (classifier.isPhysicalMouseWheel()) {
160
if (hasMouseWheelZoomModifiers(browserEvent)) {
161
const zoomLevel: number = EditorZoom.getZoomLevel();
162
const delta = e.deltaY > 0 ? 1 : -1;
163
EditorZoom.setZoomLevel(zoomLevel + delta);
164
e.preventDefault();
165
e.stopPropagation();
166
}
167
} else {
168
// we consider mousewheel events that occur within 50ms of each other to be part of the same gesture
169
// we don't want to consider mouse wheel events where ctrl/cmd is pressed during the inertia phase
170
// we also want to accumulate deltaY values from the same gesture and use that to set the zoom level
171
if (Date.now() - prevMouseWheelTime > 50) {
172
// reset if more than 50ms have passed
173
gestureStartZoomLevel = EditorZoom.getZoomLevel();
174
gestureHasZoomModifiers = hasMouseWheelZoomModifiers(browserEvent);
175
gestureAccumulatedDelta = 0;
176
}
177
178
prevMouseWheelTime = Date.now();
179
gestureAccumulatedDelta += e.deltaY;
180
181
if (gestureHasZoomModifiers) {
182
EditorZoom.setZoomLevel(gestureStartZoomLevel + gestureAccumulatedDelta / 5);
183
e.preventDefault();
184
e.stopPropagation();
185
}
186
}
187
};
188
this._register(dom.addDisposableListener(this.viewHelper.viewDomNode, dom.EventType.MOUSE_WHEEL, onMouseWheel, { capture: true, passive: false }));
189
190
function hasMouseWheelZoomModifiers(browserEvent: IMouseWheelEvent): boolean {
191
return (
192
platform.isMacintosh
193
// on macOS we support cmd + two fingers scroll (`metaKey` set)
194
// and also the two fingers pinch gesture (`ctrKey` set)
195
? ((browserEvent.metaKey || browserEvent.ctrlKey) && !browserEvent.shiftKey && !browserEvent.altKey)
196
: (browserEvent.ctrlKey && !browserEvent.metaKey && !browserEvent.shiftKey && !browserEvent.altKey)
197
);
198
}
199
}
200
201
public override dispose(): void {
202
this._context.removeEventHandler(this);
203
if (this._mouseLeaveMonitor) {
204
this._mouseLeaveMonitor.dispose();
205
this._mouseLeaveMonitor = null;
206
}
207
super.dispose();
208
}
209
210
// --- begin event handlers
211
public override onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean {
212
if (e.hasChanged(EditorOption.layoutInfo)) {
213
// layout change
214
const height = this._context.configuration.options.get(EditorOption.layoutInfo).height;
215
if (this._height !== height) {
216
this._height = height;
217
this._mouseDownOperation.onHeightChanged();
218
}
219
}
220
return false;
221
}
222
public override onCursorStateChanged(e: viewEvents.ViewCursorStateChangedEvent): boolean {
223
this._mouseDownOperation.onCursorStateChanged(e);
224
return false;
225
}
226
public override onFocusChanged(e: viewEvents.ViewFocusChangedEvent): boolean {
227
return false;
228
}
229
// --- end event handlers
230
231
public getTargetAtClientPoint(clientX: number, clientY: number): IMouseTarget | null {
232
const clientPos = new ClientCoordinates(clientX, clientY);
233
const pos = clientPos.toPageCoordinates(dom.getWindow(this.viewHelper.viewDomNode));
234
const editorPos = createEditorPagePosition(this.viewHelper.viewDomNode);
235
236
if (pos.y < editorPos.y || pos.y > editorPos.y + editorPos.height || pos.x < editorPos.x || pos.x > editorPos.x + editorPos.width) {
237
return null;
238
}
239
240
const relativePos = createCoordinatesRelativeToEditor(this.viewHelper.viewDomNode, editorPos, pos);
241
return this.mouseTargetFactory.createMouseTarget(this.viewHelper.getLastRenderData(), editorPos, pos, relativePos, null);
242
}
243
244
protected _createMouseTarget(e: EditorMouseEvent, testEventTarget: boolean): IMouseTarget {
245
let target: HTMLElement | null = e.target;
246
if (!this.viewHelper.viewDomNode.contains(target)) {
247
const shadowRoot = dom.getShadowRoot(this.viewHelper.viewDomNode);
248
if (shadowRoot) {
249
const potentialTarget = shadowRoot.elementsFromPoint(e.posx, e.posy).find(
250
(el: Element) => this.viewHelper.viewDomNode.contains(el)
251
) ?? null;
252
target = potentialTarget as HTMLElement;
253
}
254
}
255
return this.mouseTargetFactory.createMouseTarget(this.viewHelper.getLastRenderData(), e.editorPos, e.pos, e.relativePos, testEventTarget ? target : null);
256
}
257
258
private _getMouseColumn(e: EditorMouseEvent): number {
259
return this.mouseTargetFactory.getMouseColumn(e.relativePos);
260
}
261
262
protected _onContextMenu(e: EditorMouseEvent, testEventTarget: boolean): void {
263
this.viewController.emitContextMenu({
264
event: e,
265
target: this._createMouseTarget(e, testEventTarget)
266
});
267
}
268
269
protected _onMouseMove(e: EditorMouseEvent): void {
270
const targetIsWidget = this.mouseTargetFactory.mouseTargetIsWidget(e);
271
if (!targetIsWidget) {
272
e.preventDefault();
273
}
274
275
if (this._mouseDownOperation.isActive()) {
276
// In selection/drag operation
277
return;
278
}
279
const actualMouseMoveTime = e.timestamp;
280
if (actualMouseMoveTime < this.lastMouseLeaveTime) {
281
// Due to throttling, this event occurred before the mouse left the editor, therefore ignore it.
282
return;
283
}
284
285
this.viewController.emitMouseMove({
286
event: e,
287
target: this._createMouseTarget(e, true)
288
});
289
}
290
291
protected _onMouseLeave(e: EditorMouseEvent): void {
292
if (this._mouseLeaveMonitor) {
293
this._mouseLeaveMonitor.dispose();
294
this._mouseLeaveMonitor = null;
295
}
296
this.lastMouseLeaveTime = (new Date()).getTime();
297
this.viewController.emitMouseLeave({
298
event: e,
299
target: null
300
});
301
}
302
303
protected _onMouseUp(e: EditorMouseEvent): void {
304
this.viewController.emitMouseUp({
305
event: e,
306
target: this._createMouseTarget(e, true)
307
});
308
}
309
310
protected _onMouseDown(e: EditorMouseEvent, pointerId: number): void {
311
const t = this._createMouseTarget(e, true);
312
313
const targetIsContent = (t.type === MouseTargetType.CONTENT_TEXT || t.type === MouseTargetType.CONTENT_EMPTY);
314
const targetIsGutter = (t.type === MouseTargetType.GUTTER_GLYPH_MARGIN || t.type === MouseTargetType.GUTTER_LINE_NUMBERS || t.type === MouseTargetType.GUTTER_LINE_DECORATIONS);
315
const targetIsLineNumbers = (t.type === MouseTargetType.GUTTER_LINE_NUMBERS);
316
const selectOnLineNumbers = this._context.configuration.options.get(EditorOption.selectOnLineNumbers);
317
const targetIsViewZone = (t.type === MouseTargetType.CONTENT_VIEW_ZONE || t.type === MouseTargetType.GUTTER_VIEW_ZONE);
318
const targetIsWidget = (t.type === MouseTargetType.CONTENT_WIDGET);
319
320
let shouldHandle = e.leftButton || e.middleButton;
321
if (platform.isMacintosh && e.leftButton && e.ctrlKey) {
322
shouldHandle = false;
323
}
324
325
const focus = () => {
326
e.preventDefault();
327
this.viewHelper.focusTextArea();
328
};
329
330
if (shouldHandle && (targetIsContent || (targetIsLineNumbers && selectOnLineNumbers))) {
331
focus();
332
this._mouseDownOperation.start(t.type, e, pointerId);
333
334
} else if (targetIsGutter) {
335
// Do not steal focus
336
e.preventDefault();
337
} else if (targetIsViewZone) {
338
const viewZoneData = t.detail;
339
if (shouldHandle && this.viewHelper.shouldSuppressMouseDownOnViewZone(viewZoneData.viewZoneId)) {
340
focus();
341
this._mouseDownOperation.start(t.type, e, pointerId);
342
e.preventDefault();
343
}
344
} else if (targetIsWidget && this.viewHelper.shouldSuppressMouseDownOnWidget(t.detail)) {
345
focus();
346
e.preventDefault();
347
}
348
349
this.viewController.emitMouseDown({
350
event: e,
351
target: t
352
});
353
}
354
355
protected _onMouseWheel(e: IMouseWheelEvent): void {
356
this.viewController.emitMouseWheel(e);
357
}
358
}
359
360
class MouseDownOperation extends Disposable {
361
362
private readonly _createMouseTarget: (e: EditorMouseEvent, testEventTarget: boolean) => IMouseTarget;
363
private readonly _getMouseColumn: (e: EditorMouseEvent) => number;
364
365
private readonly _mouseMoveMonitor: GlobalEditorPointerMoveMonitor;
366
private readonly _topBottomDragScrolling: TopBottomDragScrolling;
367
private readonly _leftRightDragScrolling: LeftRightDragScrolling;
368
private readonly _mouseState: MouseDownState;
369
370
private _currentSelection: Selection;
371
private _isActive: boolean;
372
private _lastMouseEvent: EditorMouseEvent | null;
373
374
constructor(
375
private readonly _context: ViewContext,
376
private readonly _viewController: ViewController,
377
private readonly _viewHelper: IPointerHandlerHelper,
378
private readonly _mouseTargetFactory: MouseTargetFactory,
379
createMouseTarget: (e: EditorMouseEvent, testEventTarget: boolean) => IMouseTarget,
380
getMouseColumn: (e: EditorMouseEvent) => number
381
) {
382
super();
383
this._createMouseTarget = createMouseTarget;
384
this._getMouseColumn = getMouseColumn;
385
386
this._mouseMoveMonitor = this._register(new GlobalEditorPointerMoveMonitor(this._viewHelper.viewDomNode));
387
this._topBottomDragScrolling = this._register(new TopBottomDragScrolling(
388
this._context,
389
this._viewHelper,
390
this._mouseTargetFactory,
391
(position, inSelectionMode, revealType) => this._dispatchMouse(position, inSelectionMode, revealType)
392
));
393
this._leftRightDragScrolling = this._register(new LeftRightDragScrolling(
394
this._context,
395
this._viewHelper,
396
this._mouseTargetFactory,
397
(position, inSelectionMode, revealType) => this._dispatchMouse(position, inSelectionMode, revealType)
398
));
399
this._mouseState = new MouseDownState();
400
401
this._currentSelection = new Selection(1, 1, 1, 1);
402
this._isActive = false;
403
this._lastMouseEvent = null;
404
}
405
406
public override dispose(): void {
407
super.dispose();
408
}
409
410
public isActive(): boolean {
411
return this._isActive;
412
}
413
414
private _onMouseDownThenMove(e: EditorMouseEvent): void {
415
this._lastMouseEvent = e;
416
this._mouseState.setModifiers(e);
417
418
const position = this._findMousePosition(e, false);
419
if (!position) {
420
// Ignoring because position is unknown
421
return;
422
}
423
424
if (this._mouseState.isDragAndDrop) {
425
this._viewController.emitMouseDrag({
426
event: e,
427
target: position
428
});
429
} else {
430
if (position.type === MouseTargetType.OUTSIDE_EDITOR) {
431
if (position.outsidePosition === 'above' || position.outsidePosition === 'below') {
432
this._topBottomDragScrolling.start(position, e);
433
this._leftRightDragScrolling.stop();
434
} else {
435
this._leftRightDragScrolling.start(position, e);
436
this._topBottomDragScrolling.stop();
437
}
438
} else {
439
this._topBottomDragScrolling.stop();
440
this._leftRightDragScrolling.stop();
441
this._dispatchMouse(position, true, NavigationCommandRevealType.Minimal);
442
}
443
}
444
}
445
446
public start(targetType: MouseTargetType, e: EditorMouseEvent, pointerId: number): void {
447
this._lastMouseEvent = e;
448
449
this._mouseState.setStartedOnLineNumbers(targetType === MouseTargetType.GUTTER_LINE_NUMBERS);
450
this._mouseState.setStartButtons(e);
451
this._mouseState.setModifiers(e);
452
const position = this._findMousePosition(e, true);
453
if (!position || !position.position) {
454
// Ignoring because position is unknown
455
return;
456
}
457
458
this._mouseState.trySetCount(e.detail, position.position);
459
460
// Overwrite the detail of the MouseEvent, as it will be sent out in an event and contributions might rely on it.
461
e.detail = this._mouseState.count;
462
463
const options = this._context.configuration.options;
464
465
if (!options.get(EditorOption.readOnly)
466
&& options.get(EditorOption.dragAndDrop)
467
&& !options.get(EditorOption.columnSelection)
468
&& !this._mouseState.altKey // we don't support multiple mouse
469
&& e.detail < 2 // only single click on a selection can work
470
&& !this._isActive // the mouse is not down yet
471
&& !this._currentSelection.isEmpty() // we don't drag single cursor
472
&& (position.type === MouseTargetType.CONTENT_TEXT) // single click on text
473
&& position.position && this._currentSelection.containsPosition(position.position) // single click on a selection
474
) {
475
this._mouseState.isDragAndDrop = true;
476
this._isActive = true;
477
478
this._mouseMoveMonitor.startMonitoring(
479
this._viewHelper.viewLinesDomNode,
480
pointerId,
481
e.buttons,
482
(e) => this._onMouseDownThenMove(e),
483
(browserEvent?: MouseEvent | KeyboardEvent) => {
484
const position = this._findMousePosition(this._lastMouseEvent!, false);
485
486
if (dom.isKeyboardEvent(browserEvent)) {
487
// cancel
488
this._viewController.emitMouseDropCanceled();
489
} else {
490
this._viewController.emitMouseDrop({
491
event: this._lastMouseEvent!,
492
target: (position ? this._createMouseTarget(this._lastMouseEvent!, true) : null) // Ignoring because position is unknown, e.g., Content View Zone
493
});
494
}
495
496
this._stop();
497
}
498
);
499
500
return;
501
}
502
503
this._mouseState.isDragAndDrop = false;
504
this._dispatchMouse(position, e.shiftKey, NavigationCommandRevealType.Minimal);
505
506
if (!this._isActive) {
507
this._isActive = true;
508
this._mouseMoveMonitor.startMonitoring(
509
this._viewHelper.viewLinesDomNode,
510
pointerId,
511
e.buttons,
512
(e) => this._onMouseDownThenMove(e),
513
() => this._stop()
514
);
515
}
516
}
517
518
private _stop(): void {
519
this._isActive = false;
520
this._topBottomDragScrolling.stop();
521
this._leftRightDragScrolling.stop();
522
}
523
524
public onHeightChanged(): void {
525
this._mouseMoveMonitor.stopMonitoring();
526
}
527
528
public onPointerUp(): void {
529
this._mouseMoveMonitor.stopMonitoring();
530
}
531
532
public onCursorStateChanged(e: viewEvents.ViewCursorStateChangedEvent): void {
533
this._currentSelection = e.selections[0];
534
}
535
536
private _getPositionOutsideEditor(e: EditorMouseEvent): IMouseTarget | null {
537
const editorContent = e.editorPos;
538
const model = this._context.viewModel;
539
const viewLayout = this._context.viewLayout;
540
541
const mouseColumn = this._getMouseColumn(e);
542
543
if (e.posy < editorContent.y) {
544
const outsideDistance = editorContent.y - e.posy;
545
const verticalOffset = Math.max(viewLayout.getCurrentScrollTop() - outsideDistance, 0);
546
const viewZoneData = HitTestContext.getZoneAtCoord(this._context, verticalOffset);
547
if (viewZoneData) {
548
const newPosition = this._helpPositionJumpOverViewZone(viewZoneData);
549
if (newPosition) {
550
return MouseTarget.createOutsideEditor(mouseColumn, newPosition, 'above', outsideDistance);
551
}
552
}
553
554
const aboveLineNumber = viewLayout.getLineNumberAtVerticalOffset(verticalOffset);
555
return MouseTarget.createOutsideEditor(mouseColumn, new Position(aboveLineNumber, 1), 'above', outsideDistance);
556
}
557
558
if (e.posy > editorContent.y + editorContent.height) {
559
const outsideDistance = e.posy - editorContent.y - editorContent.height;
560
const verticalOffset = viewLayout.getCurrentScrollTop() + e.relativePos.y;
561
const viewZoneData = HitTestContext.getZoneAtCoord(this._context, verticalOffset);
562
if (viewZoneData) {
563
const newPosition = this._helpPositionJumpOverViewZone(viewZoneData);
564
if (newPosition) {
565
return MouseTarget.createOutsideEditor(mouseColumn, newPosition, 'below', outsideDistance);
566
}
567
}
568
569
const belowLineNumber = viewLayout.getLineNumberAtVerticalOffset(verticalOffset);
570
return MouseTarget.createOutsideEditor(mouseColumn, new Position(belowLineNumber, model.getLineMaxColumn(belowLineNumber)), 'below', outsideDistance);
571
}
572
573
const possibleLineNumber = viewLayout.getLineNumberAtVerticalOffset(viewLayout.getCurrentScrollTop() + e.relativePos.y);
574
575
const layoutInfo = this._context.configuration.options.get(EditorOption.layoutInfo);
576
577
const xLeftBoundary = layoutInfo.contentLeft;
578
if (e.relativePos.x <= xLeftBoundary) {
579
const outsideDistance = xLeftBoundary - e.relativePos.x;
580
const isRtl = model.getTextDirection(possibleLineNumber) === TextDirection.RTL;
581
return MouseTarget.createOutsideEditor(mouseColumn, new Position(possibleLineNumber, isRtl ? model.getLineMaxColumn(possibleLineNumber) : 1), 'left', outsideDistance);
582
}
583
584
const contentRight = (
585
layoutInfo.minimap.minimapLeft === 0
586
? layoutInfo.width - layoutInfo.verticalScrollbarWidth // Happens when minimap is hidden
587
: layoutInfo.minimap.minimapLeft
588
);
589
const xRightBoundary = contentRight;
590
if (e.relativePos.x >= xRightBoundary) {
591
const outsideDistance = e.relativePos.x - xRightBoundary;
592
const isRtl = model.getTextDirection(possibleLineNumber) === TextDirection.RTL;
593
return MouseTarget.createOutsideEditor(mouseColumn, new Position(possibleLineNumber, isRtl ? 1 : model.getLineMaxColumn(possibleLineNumber)), 'right', outsideDistance);
594
}
595
596
return null;
597
}
598
599
private _findMousePosition(e: EditorMouseEvent, testEventTarget: boolean): IMouseTarget | null {
600
const positionOutsideEditor = this._getPositionOutsideEditor(e);
601
if (positionOutsideEditor) {
602
return positionOutsideEditor;
603
}
604
605
const t = this._createMouseTarget(e, testEventTarget);
606
const hintedPosition = t.position;
607
if (!hintedPosition) {
608
return null;
609
}
610
611
if (t.type === MouseTargetType.CONTENT_VIEW_ZONE || t.type === MouseTargetType.GUTTER_VIEW_ZONE) {
612
const newPosition = this._helpPositionJumpOverViewZone(t.detail);
613
if (newPosition) {
614
return MouseTarget.createViewZone(t.type, t.element, t.mouseColumn, newPosition, t.detail);
615
}
616
}
617
618
return t;
619
}
620
621
private _helpPositionJumpOverViewZone(viewZoneData: IMouseTargetViewZoneData): Position | null {
622
// Force position on view zones to go above or below depending on where selection started from
623
const selectionStart = new Position(this._currentSelection.selectionStartLineNumber, this._currentSelection.selectionStartColumn);
624
const positionBefore = viewZoneData.positionBefore;
625
const positionAfter = viewZoneData.positionAfter;
626
627
if (positionBefore && positionAfter) {
628
if (positionBefore.isBefore(selectionStart)) {
629
return positionBefore;
630
} else {
631
return positionAfter;
632
}
633
}
634
return null;
635
}
636
637
private _dispatchMouse(position: IMouseTarget, inSelectionMode: boolean, revealType: NavigationCommandRevealType): void {
638
if (!position.position) {
639
return;
640
}
641
this._viewController.dispatchMouse({
642
position: position.position,
643
mouseColumn: position.mouseColumn,
644
startedOnLineNumbers: this._mouseState.startedOnLineNumbers,
645
revealType,
646
647
inSelectionMode: inSelectionMode,
648
mouseDownCount: this._mouseState.count,
649
altKey: this._mouseState.altKey,
650
ctrlKey: this._mouseState.ctrlKey,
651
metaKey: this._mouseState.metaKey,
652
shiftKey: this._mouseState.shiftKey,
653
654
leftButton: this._mouseState.leftButton,
655
middleButton: this._mouseState.middleButton,
656
657
onInjectedText: position.type === MouseTargetType.CONTENT_TEXT && position.detail.injectedText !== null
658
});
659
}
660
}
661
662
class MouseDownState {
663
664
private static readonly CLEAR_MOUSE_DOWN_COUNT_TIME = 400; // ms
665
666
private _altKey: boolean;
667
public get altKey(): boolean { return this._altKey; }
668
669
private _ctrlKey: boolean;
670
public get ctrlKey(): boolean { return this._ctrlKey; }
671
672
private _metaKey: boolean;
673
public get metaKey(): boolean { return this._metaKey; }
674
675
private _shiftKey: boolean;
676
public get shiftKey(): boolean { return this._shiftKey; }
677
678
private _leftButton: boolean;
679
public get leftButton(): boolean { return this._leftButton; }
680
681
private _middleButton: boolean;
682
public get middleButton(): boolean { return this._middleButton; }
683
684
private _startedOnLineNumbers: boolean;
685
public get startedOnLineNumbers(): boolean { return this._startedOnLineNumbers; }
686
687
private _lastMouseDownPosition: Position | null;
688
private _lastMouseDownPositionEqualCount: number;
689
private _lastMouseDownCount: number;
690
private _lastSetMouseDownCountTime: number;
691
public isDragAndDrop: boolean;
692
693
constructor() {
694
this._altKey = false;
695
this._ctrlKey = false;
696
this._metaKey = false;
697
this._shiftKey = false;
698
this._leftButton = false;
699
this._middleButton = false;
700
this._startedOnLineNumbers = false;
701
this._lastMouseDownPosition = null;
702
this._lastMouseDownPositionEqualCount = 0;
703
this._lastMouseDownCount = 0;
704
this._lastSetMouseDownCountTime = 0;
705
this.isDragAndDrop = false;
706
}
707
708
public get count(): number {
709
return this._lastMouseDownCount;
710
}
711
712
public setModifiers(source: EditorMouseEvent) {
713
this._altKey = source.altKey;
714
this._ctrlKey = source.ctrlKey;
715
this._metaKey = source.metaKey;
716
this._shiftKey = source.shiftKey;
717
}
718
719
public setStartButtons(source: EditorMouseEvent) {
720
this._leftButton = source.leftButton;
721
this._middleButton = source.middleButton;
722
}
723
724
public setStartedOnLineNumbers(startedOnLineNumbers: boolean): void {
725
this._startedOnLineNumbers = startedOnLineNumbers;
726
}
727
728
public trySetCount(setMouseDownCount: number, newMouseDownPosition: Position): void {
729
// 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)
730
const currentTime = (new Date()).getTime();
731
if (currentTime - this._lastSetMouseDownCountTime > MouseDownState.CLEAR_MOUSE_DOWN_COUNT_TIME) {
732
setMouseDownCount = 1;
733
}
734
this._lastSetMouseDownCountTime = currentTime;
735
736
// 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)
737
if (setMouseDownCount > this._lastMouseDownCount + 1) {
738
setMouseDownCount = this._lastMouseDownCount + 1;
739
}
740
741
// c. Invalidate multiple clicking if the logical position is different
742
if (this._lastMouseDownPosition && this._lastMouseDownPosition.equals(newMouseDownPosition)) {
743
this._lastMouseDownPositionEqualCount++;
744
} else {
745
this._lastMouseDownPositionEqualCount = 1;
746
}
747
this._lastMouseDownPosition = newMouseDownPosition;
748
749
// Finally set the lastMouseDownCount
750
this._lastMouseDownCount = Math.min(setMouseDownCount, this._lastMouseDownPositionEqualCount);
751
}
752
753
}
754
755