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