634 lines
22 KiB
TypeScript
634 lines
22 KiB
TypeScript
/**
|
|
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
|
* @license MIT
|
|
*/
|
|
|
|
import { addDisposableListener } from '../Dom';
|
|
import { IBufferService, IMouseStateService, ICoreService, ILogService, IOptionsService } from '../../common/services/Services';
|
|
import { CoreMouseAction, CoreMouseButton, CoreMouseEventType, ICoreMouseEvent, IDisposable } from '../../common/Types';
|
|
import { C0 } from '../../common/data/EscapeSequences';
|
|
import { DisposableStore, MutableDisposable } from '../../common/Lifecycle';
|
|
import { ICoreBrowserService, IMouseCoordsService, IMouseService, IMouseServiceTarget, IRenderService, ISelectionService } from './Services';
|
|
import { Gesture, EventType as GestureEventType, IGestureEvent } from '../scrollable/touch';
|
|
|
|
type RequestedMouseEvents = Record<'mouseup' | 'wheel' | 'mousedrag' | 'mousemove', EventListener | null>;
|
|
|
|
export const enum MouseEventCssClasses {
|
|
ENABLE_MOUSE_EVENTS = 'enable-mouse-events'
|
|
}
|
|
|
|
interface IMouseBindContext {
|
|
readonly target: IMouseServiceTarget;
|
|
readonly focus: () => void;
|
|
readonly requestedEvents: RequestedMouseEvents;
|
|
readonly mouseupListener: MutableDisposable<IDisposable>;
|
|
readonly mousedragListener: MutableDisposable<IDisposable>;
|
|
}
|
|
|
|
export class MouseService implements IMouseService {
|
|
public serviceBrand: undefined;
|
|
|
|
private _lastEvent: ICoreMouseEvent | null = null;
|
|
private _wheelPartialScroll: number = 0;
|
|
private _touchScrollAccumulator: number = 0;
|
|
private _altMouseCursor: AltMouseCursorController | undefined;
|
|
|
|
constructor(
|
|
@IRenderService private readonly _renderService: IRenderService,
|
|
@IMouseCoordsService private readonly _mouseCoordsService: IMouseCoordsService,
|
|
@IMouseStateService private readonly _mouseStateService: IMouseStateService,
|
|
@ICoreService private readonly _coreService: ICoreService,
|
|
@IBufferService private readonly _bufferService: IBufferService,
|
|
@IOptionsService private readonly _optionsService: IOptionsService,
|
|
@ISelectionService private readonly _selectionService: ISelectionService,
|
|
@ILogService private readonly _logService: ILogService,
|
|
@ICoreBrowserService private readonly _coreBrowserService: ICoreBrowserService
|
|
) {
|
|
}
|
|
|
|
public bindMouse(target: IMouseServiceTarget, register: (disposable: IDisposable) => void, focus: () => void): void {
|
|
const { element, document } = target;
|
|
|
|
/**
|
|
* Event listener state handling.
|
|
* We listen to the onProtocolChange event of MouseStateService and put
|
|
* requested listeners in `requestedEvents`. With this the listeners
|
|
* have all bits to do the event listener juggling.
|
|
* Note: 'mousedown' currently is "always on" and not managed
|
|
* by onProtocolChange.
|
|
*/
|
|
const requestedEvents: RequestedMouseEvents = {
|
|
mouseup: null,
|
|
wheel: null,
|
|
mousedrag: null,
|
|
mousemove: null
|
|
};
|
|
const mouseupListener = new MutableDisposable<IDisposable>();
|
|
const mousedragListener = new MutableDisposable<IDisposable>();
|
|
register(mouseupListener);
|
|
register(mousedragListener);
|
|
const ctx: IMouseBindContext = { target, focus, requestedEvents, mouseupListener, mousedragListener };
|
|
const eventListeners: Record<'mouseup' | 'wheel' | 'mousedrag' | 'mousemove', EventListener> = {
|
|
mouseup: (ev: Event) => this._handleMouseUp(ctx, ev as MouseEvent),
|
|
wheel: (ev: Event) => this._handleWheel(ctx, ev as WheelEvent),
|
|
mousedrag: (ev: Event) => this._handleMouseDrag(ctx, ev as MouseEvent),
|
|
mousemove: (ev: Event) => this._handleMouseMove(ctx, ev as MouseEvent)
|
|
};
|
|
this._altMouseCursor = new AltMouseCursorController(
|
|
element,
|
|
document,
|
|
() => this._mouseStateService.areMouseEventsActive
|
|
&& !!this._optionsService.rawOptions.mouseEventsRequireAlt
|
|
);
|
|
register(this._altMouseCursor);
|
|
register(this._mouseStateService.onProtocolChange(events => {
|
|
this._handleProtocolChange(ctx, eventListeners, events);
|
|
}));
|
|
register(this._optionsService.onSpecificOptionChange('mouseEventsRequireAlt', () => {
|
|
this._syncMouseModeState(element);
|
|
this._altMouseCursor?.sync();
|
|
}));
|
|
// force initial onProtocolChange so we dont miss early mouse requests
|
|
this._mouseStateService.activeProtocol = this._mouseStateService.activeProtocol;
|
|
|
|
/**
|
|
* "Always on" event listeners.
|
|
*/
|
|
register(addDisposableListener(element, 'mousedown', (ev: MouseEvent) => this._handleMouseDown(ctx, ev)));
|
|
register(addDisposableListener(element, 'wheel', (ev: WheelEvent) => this._handlePassiveWheel(ctx, ev), { passive: false }));
|
|
register(Gesture.addTarget(target.screenElement));
|
|
register(addDisposableListener(target.screenElement, GestureEventType.START, () => this._handleTouchStart()));
|
|
register(addDisposableListener(target.screenElement, GestureEventType.CHANGE, (e: IGestureEvent) => this._handleTouchChange(ctx, e)));
|
|
}
|
|
|
|
private _sendEvent(ctx: IMouseBindContext, ev: MouseEvent | WheelEvent): boolean {
|
|
// Get mouse coordinates
|
|
const pos = this._mouseCoordsService.getMouseReportCoords(ev as MouseEvent, ctx.target.screenElement);
|
|
if (!pos) {
|
|
return false;
|
|
}
|
|
|
|
let but: CoreMouseButton;
|
|
let action: CoreMouseAction | undefined;
|
|
switch ((ev as MouseEvent & { overrideType?: string }).overrideType || ev.type) {
|
|
case 'mousemove':
|
|
action = CoreMouseAction.MOVE;
|
|
if (ev.buttons === undefined) {
|
|
// buttons is not supported on macOS, try to get a value from button instead
|
|
but = CoreMouseButton.NONE;
|
|
if (ev.button !== undefined) {
|
|
but = ev.button < 3 ? ev.button : CoreMouseButton.NONE;
|
|
}
|
|
} else {
|
|
// according to MDN buttons only reports up to button 5 (AUX2)
|
|
but = ev.buttons & 1 ? CoreMouseButton.LEFT :
|
|
ev.buttons & 4 ? CoreMouseButton.MIDDLE :
|
|
ev.buttons & 2 ? CoreMouseButton.RIGHT :
|
|
CoreMouseButton.NONE; // fallback to NONE
|
|
}
|
|
break;
|
|
case 'mouseup':
|
|
action = CoreMouseAction.UP;
|
|
but = ev.button < 3 ? ev.button : CoreMouseButton.NONE;
|
|
break;
|
|
case 'mousedown':
|
|
action = CoreMouseAction.DOWN;
|
|
but = ev.button < 3 ? ev.button : CoreMouseButton.NONE;
|
|
break;
|
|
case 'wheel':
|
|
if (!this._mouseStateService.allowCustomWheelEvent(ev as WheelEvent)) {
|
|
return false;
|
|
}
|
|
const deltaY = (ev as WheelEvent).deltaY;
|
|
if (deltaY === 0) {
|
|
return false;
|
|
}
|
|
const lines = this._consumeWheelEvent(
|
|
ev as WheelEvent,
|
|
this._renderService?.dimensions?.device?.cell?.height,
|
|
this._coreBrowserService?.dpr
|
|
);
|
|
if (lines === 0) {
|
|
return false;
|
|
}
|
|
action = deltaY < 0 ? CoreMouseAction.UP : CoreMouseAction.DOWN;
|
|
but = CoreMouseButton.WHEEL;
|
|
break;
|
|
default:
|
|
// dont handle other event types by accident
|
|
return false;
|
|
}
|
|
|
|
// exit if we cannot determine valid button/action values
|
|
// do nothing for higher buttons than wheel
|
|
if (action === undefined || but === undefined || but > CoreMouseButton.WHEEL) {
|
|
return false;
|
|
}
|
|
|
|
if (but !== CoreMouseButton.WHEEL
|
|
&& this._optionsService.rawOptions.mouseEventsRequireAlt
|
|
&& this._mouseStateService.areMouseEventsActive
|
|
&& !ev.altKey) {
|
|
return false;
|
|
}
|
|
|
|
// Alt is only used locally to gate mouse passthrough; do not forward it to the
|
|
// application (e.g. tmux ignores alt-modified mouse reports).
|
|
const stripAltFromReport = but !== CoreMouseButton.WHEEL
|
|
&& this._optionsService.rawOptions.mouseEventsRequireAlt
|
|
&& this._mouseStateService.areMouseEventsActive;
|
|
|
|
return this._triggerMouseEvent({
|
|
col: pos.col,
|
|
row: pos.row,
|
|
x: pos.x,
|
|
y: pos.y,
|
|
button: but,
|
|
action,
|
|
ctrl: ev.ctrlKey,
|
|
alt: stripAltFromReport ? false : ev.altKey,
|
|
shift: ev.shiftKey
|
|
});
|
|
}
|
|
|
|
private _handleMouseUp(ctx: IMouseBindContext, ev: MouseEvent): void {
|
|
this._sendEvent(ctx, ev);
|
|
if (!ev.buttons) {
|
|
// if no other button is held remove global handlers
|
|
ctx.mouseupListener.clear();
|
|
ctx.mousedragListener.clear();
|
|
}
|
|
}
|
|
|
|
private _handleWheel(ctx: IMouseBindContext, ev: WheelEvent): false {
|
|
this._sendEvent(ctx, ev);
|
|
ev.preventDefault();
|
|
ev.stopPropagation();
|
|
return false;
|
|
}
|
|
|
|
private _handleMouseDrag(ctx: IMouseBindContext, ev: MouseEvent): void {
|
|
// deal only with move while a button is held
|
|
if (ev.buttons) {
|
|
this._sendEvent(ctx, ev);
|
|
}
|
|
}
|
|
|
|
private _handleMouseMove(ctx: IMouseBindContext, ev: MouseEvent): void {
|
|
// deal only with move without any button
|
|
if (!ev.buttons) {
|
|
this._sendEvent(ctx, ev);
|
|
}
|
|
}
|
|
|
|
private _handleMouseDown(ctx: IMouseBindContext, ev: MouseEvent): void {
|
|
ev.preventDefault();
|
|
ctx.focus();
|
|
|
|
// Don't send the mouse button to the pty if mouse events are disabled or
|
|
// if the selection manager is having selection forced (ie. a modifier is
|
|
// held).
|
|
if (!this._mouseStateService.areMouseEventsActive || this._selectionService.shouldForceSelection(ev)) {
|
|
return;
|
|
}
|
|
|
|
this._sendEvent(ctx, ev);
|
|
|
|
// Register additional global handlers which should keep reporting outside
|
|
// of the terminal element.
|
|
// Note: Other emulators also do this for 'mousedown' while a button
|
|
// is held, we currently limit 'mousedown' to the terminal only.
|
|
// Use the element's current document in case it moved to another window after open.
|
|
const { element, document: targetDocument } = ctx.target;
|
|
const listenerDocument = element.ownerDocument ?? targetDocument;
|
|
if (ctx.requestedEvents.mouseup) {
|
|
ctx.mouseupListener.value = addDisposableListener(listenerDocument, 'mouseup', ctx.requestedEvents.mouseup);
|
|
}
|
|
if (ctx.requestedEvents.mousedrag) {
|
|
ctx.mousedragListener.value = addDisposableListener(listenerDocument, 'mousemove', ctx.requestedEvents.mousedrag);
|
|
}
|
|
}
|
|
|
|
private _handlePassiveWheel(ctx: IMouseBindContext, ev: WheelEvent): false | void {
|
|
// do nothing, if app side handles wheel itself
|
|
if (ctx.requestedEvents.wheel) {
|
|
return;
|
|
}
|
|
|
|
if (!this._mouseStateService.allowCustomWheelEvent(ev)) {
|
|
return false;
|
|
}
|
|
|
|
if (!this._bufferService.buffer.hasScrollback) {
|
|
// Convert wheel events into up/down events when the buffer does not have scrollback, this
|
|
// enables scrolling in apps hosted in the alt buffer such as vim or tmux even when mouse
|
|
// events are not enabled.
|
|
// This used implementation used get the actual lines/partial lines scrolled from the
|
|
// viewport but since moving to the new viewport implementation has been simplified to
|
|
// simply send a single up or down sequence.
|
|
|
|
// Do nothing if there's no vertical scroll
|
|
const deltaY = ev.deltaY;
|
|
if (deltaY === 0) {
|
|
return false;
|
|
}
|
|
|
|
const lines = this._consumeWheelEvent(
|
|
ev,
|
|
this._renderService?.dimensions?.device?.cell?.height,
|
|
this._coreBrowserService?.dpr
|
|
);
|
|
if (lines === 0) {
|
|
ev.preventDefault();
|
|
ev.stopPropagation();
|
|
return false;
|
|
}
|
|
|
|
// Construct and send sequences
|
|
const sequence = C0.ESC + (this._coreService.decPrivateModes.applicationCursorKeys ? 'O' : '[') + (ev.deltaY < 0 ? 'A' : 'B');
|
|
this._coreService.triggerDataEvent(sequence, true);
|
|
ev.preventDefault();
|
|
ev.stopPropagation();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private _handleTouchStart(): void {
|
|
this._touchScrollAccumulator = 0;
|
|
}
|
|
|
|
private _handleTouchChange(ctx: IMouseBindContext, e: IGestureEvent): void {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
// When mouse protocol has wheel events active, send as mouse wheel events.
|
|
if (ctx.requestedEvents.wheel) {
|
|
this._handleTouchScrollAsWheel(ctx, e);
|
|
return;
|
|
}
|
|
|
|
// When in alt buffer (no scrollback), send up/down key sequences.
|
|
if (!this._bufferService.buffer.hasScrollback) {
|
|
this._handleTouchScrollAsKeys(e);
|
|
return;
|
|
}
|
|
|
|
// Normal scrollback: delegate to viewport scrolling when available.
|
|
ctx.target.handleTouchScroll?.(e.translationY);
|
|
}
|
|
|
|
private _handleTouchScrollAsKeys(e: IGestureEvent): void {
|
|
const cellHeight = this._renderService?.dimensions.css.cell.height;
|
|
if (!cellHeight) {
|
|
return;
|
|
}
|
|
|
|
this._touchScrollAccumulator -= e.translationY;
|
|
const lines = Math.trunc(this._touchScrollAccumulator / cellHeight);
|
|
if (lines === 0) {
|
|
return;
|
|
}
|
|
|
|
this._touchScrollAccumulator -= lines * cellHeight;
|
|
const sequence = C0.ESC
|
|
+ (this._coreService.decPrivateModes.applicationCursorKeys ? 'O' : '[')
|
|
+ (lines < 0 ? 'A' : 'B');
|
|
for (let i = 0; i < Math.abs(lines); i++) {
|
|
this._coreService.triggerDataEvent(sequence, true);
|
|
}
|
|
}
|
|
|
|
private _handleTouchScrollAsWheel(ctx: IMouseBindContext, e: IGestureEvent): void {
|
|
const cellHeight = this._renderService?.dimensions.css.cell.height;
|
|
if (!cellHeight) {
|
|
return;
|
|
}
|
|
|
|
this._touchScrollAccumulator -= e.translationY;
|
|
const lines = Math.trunc(this._touchScrollAccumulator / cellHeight);
|
|
if (lines === 0) {
|
|
return;
|
|
}
|
|
|
|
this._touchScrollAccumulator -= lines * cellHeight;
|
|
const pos = this._mouseCoordsService.getMouseReportCoords(e, ctx.target.screenElement);
|
|
if (!pos) {
|
|
return;
|
|
}
|
|
|
|
for (let i = 0; i < Math.abs(lines); i++) {
|
|
this._triggerMouseEvent({
|
|
col: pos.col,
|
|
row: pos.row,
|
|
x: pos.x,
|
|
y: pos.y,
|
|
button: CoreMouseButton.WHEEL,
|
|
action: lines < 0 ? CoreMouseAction.UP : CoreMouseAction.DOWN,
|
|
ctrl: false,
|
|
alt: false,
|
|
shift: false
|
|
});
|
|
}
|
|
}
|
|
|
|
public reset(): void {
|
|
this._lastEvent = null;
|
|
this._wheelPartialScroll = 0;
|
|
this._touchScrollAccumulator = 0;
|
|
}
|
|
|
|
private _syncMouseModeState(element: HTMLElement): void {
|
|
if (this._mouseStateService.areMouseEventsActive) {
|
|
if (this._optionsService.rawOptions.mouseEventsRequireAlt) {
|
|
this._altMouseCursor?.resetClass();
|
|
this._selectionService.enable();
|
|
} else {
|
|
element.classList.add(MouseEventCssClasses.ENABLE_MOUSE_EVENTS);
|
|
this._selectionService.disable();
|
|
}
|
|
} else {
|
|
element.classList.remove(MouseEventCssClasses.ENABLE_MOUSE_EVENTS);
|
|
this._selectionService.enable();
|
|
}
|
|
}
|
|
|
|
private _handleProtocolChange(ctx: IMouseBindContext, eventListeners: Record<'mouseup' | 'wheel' | 'mousedrag' | 'mousemove', EventListener>, events: CoreMouseEventType): void {
|
|
const { element } = ctx.target;
|
|
const { requestedEvents } = ctx;
|
|
// apply global changes on events
|
|
if (events) {
|
|
if (this._optionsService.rawOptions.logLevel === 'debug') {
|
|
this._logService.debug('Binding to mouse events:', this._explainEvents(events));
|
|
}
|
|
} else {
|
|
this._logService.debug('Unbinding from mouse events.');
|
|
}
|
|
this._syncMouseModeState(element);
|
|
this._altMouseCursor?.sync();
|
|
|
|
// add/remove handlers from requestedEvents
|
|
if (!(events & CoreMouseEventType.MOVE)) {
|
|
if (requestedEvents.mousemove) {
|
|
element.removeEventListener('mousemove', requestedEvents.mousemove);
|
|
}
|
|
requestedEvents.mousemove = null;
|
|
} else if (!requestedEvents.mousemove) {
|
|
element.addEventListener('mousemove', eventListeners.mousemove);
|
|
requestedEvents.mousemove = eventListeners.mousemove;
|
|
}
|
|
|
|
if (!(events & CoreMouseEventType.WHEEL)) {
|
|
if (requestedEvents.wheel) {
|
|
element.removeEventListener('wheel', requestedEvents.wheel);
|
|
}
|
|
requestedEvents.wheel = null;
|
|
} else if (!requestedEvents.wheel) {
|
|
element.addEventListener('wheel', eventListeners.wheel, { passive: false });
|
|
requestedEvents.wheel = eventListeners.wheel;
|
|
}
|
|
|
|
if (!(events & CoreMouseEventType.UP)) {
|
|
ctx.mouseupListener.clear();
|
|
requestedEvents.mouseup = null;
|
|
} else {
|
|
requestedEvents.mouseup ??= eventListeners.mouseup;
|
|
}
|
|
|
|
if (!(events & CoreMouseEventType.DRAG)) {
|
|
ctx.mousedragListener.clear();
|
|
requestedEvents.mousedrag = null;
|
|
} else {
|
|
requestedEvents.mousedrag ??= eventListeners.mousedrag;
|
|
}
|
|
}
|
|
|
|
private _applyScrollModifier(amount: number, ev: WheelEvent): number {
|
|
// Multiply the scroll speed when the modifier key is pressed
|
|
if (ev.altKey || ev.ctrlKey || ev.shiftKey) {
|
|
return amount * this._optionsService.rawOptions.fastScrollSensitivity * this._optionsService.rawOptions.scrollSensitivity;
|
|
}
|
|
return amount * this._optionsService.rawOptions.scrollSensitivity;
|
|
}
|
|
|
|
/**
|
|
* Processes a wheel event, accounting for partial scrolls for trackpad, mouse scrolls.
|
|
* This prevents hyper-sensitive scrolling in alt buffer.
|
|
*/
|
|
private _consumeWheelEvent(ev: WheelEvent, cellHeight?: number, dpr?: number): number {
|
|
// Do nothing if it's not a vertical scroll event
|
|
if (ev.deltaY === 0 || ev.shiftKey) {
|
|
return 0;
|
|
}
|
|
|
|
if (cellHeight === undefined || dpr === undefined) {
|
|
return 0;
|
|
}
|
|
|
|
const targetWheelEventPixels = cellHeight / dpr;
|
|
let amount = this._applyScrollModifier(ev.deltaY, ev);
|
|
|
|
if (ev.deltaMode === WheelEvent.DOM_DELTA_PIXEL) {
|
|
amount /= (targetWheelEventPixels + 0.0); // Prevent integer division
|
|
|
|
const isLikelyTrackpad = Math.abs(ev.deltaY) < 50;
|
|
if (isLikelyTrackpad) {
|
|
amount *= 0.3;
|
|
}
|
|
|
|
this._wheelPartialScroll += amount;
|
|
amount = Math.floor(Math.abs(this._wheelPartialScroll)) * (this._wheelPartialScroll > 0 ? 1 : -1);
|
|
this._wheelPartialScroll %= 1;
|
|
} else if (ev.deltaMode === WheelEvent.DOM_DELTA_PAGE) {
|
|
amount *= this._bufferService.rows;
|
|
}
|
|
return amount;
|
|
}
|
|
|
|
/**
|
|
* Triggers a mouse event to be sent.
|
|
*
|
|
* Returns true if the event passed all protocol restrictions and a report
|
|
* was sent, otherwise false. The return value may be used to decide whether
|
|
* the default event action in the browser component should be omitted.
|
|
*
|
|
* Note: The method will change values of the given event object
|
|
* to fulfill protocol and encoding restrictions.
|
|
*/
|
|
private _triggerMouseEvent(e: ICoreMouseEvent): boolean {
|
|
// range check for col/row
|
|
if (e.col < 0 || e.col >= this._bufferService.cols
|
|
|| e.row < 0 || e.row >= this._bufferService.rows) {
|
|
return false;
|
|
}
|
|
|
|
// filter nonsense combinations of button + action
|
|
if (e.button === CoreMouseButton.WHEEL && e.action === CoreMouseAction.MOVE) {
|
|
return false;
|
|
}
|
|
if (e.button === CoreMouseButton.NONE && e.action !== CoreMouseAction.MOVE) {
|
|
return false;
|
|
}
|
|
if (e.button !== CoreMouseButton.WHEEL && (e.action === CoreMouseAction.LEFT || e.action === CoreMouseAction.RIGHT)) {
|
|
return false;
|
|
}
|
|
|
|
// report 1-based coords
|
|
e.col++;
|
|
e.row++;
|
|
|
|
// debounce move events at grid or pixel level
|
|
if (e.action === CoreMouseAction.MOVE
|
|
&& this._lastEvent
|
|
&& this._equalEvents(this._lastEvent, e, this._mouseStateService.isPixelEncoding)
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
// apply protocol restrictions
|
|
if (!this._mouseStateService.restrictMouseEvent(e)) {
|
|
return false;
|
|
}
|
|
|
|
// encode report and send
|
|
const report = this._mouseStateService.encodeMouseEvent(e);
|
|
if (report) {
|
|
if (this._mouseStateService.isDefaultEncoding) {
|
|
this._coreService.triggerBinaryEvent(report);
|
|
} else {
|
|
this._coreService.triggerDataEvent(report, true);
|
|
}
|
|
}
|
|
|
|
this._lastEvent = e;
|
|
return true;
|
|
}
|
|
|
|
private _explainEvents(events: CoreMouseEventType): { [event: string]: boolean } {
|
|
return {
|
|
down: !!(events & CoreMouseEventType.DOWN),
|
|
up: !!(events & CoreMouseEventType.UP),
|
|
drag: !!(events & CoreMouseEventType.DRAG),
|
|
move: !!(events & CoreMouseEventType.MOVE),
|
|
wheel: !!(events & CoreMouseEventType.WHEEL)
|
|
};
|
|
}
|
|
|
|
private _equalEvents(e1: ICoreMouseEvent, e2: ICoreMouseEvent, pixels: boolean): boolean {
|
|
if (pixels) {
|
|
if (e1.x !== e2.x) return false;
|
|
if (e1.y !== e2.y) return false;
|
|
} else {
|
|
if (e1.col !== e2.col) return false;
|
|
if (e1.row !== e2.row) return false;
|
|
}
|
|
if (e1.button !== e2.button) return false;
|
|
if (e1.action !== e2.action) return false;
|
|
if (e1.ctrl !== e2.ctrl) return false;
|
|
if (e1.alt !== e2.alt) return false;
|
|
if (e1.shift !== e2.shift) return false;
|
|
return true;
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Toggles MouseEventCssClasses.ENABLE_MOUSE_EVENTS on the terminal element while alt is held when
|
|
* `mouseEventsRequireAlt` is active. DOM listeners are only registered while active.
|
|
*/
|
|
export class AltMouseCursorController implements IDisposable {
|
|
private readonly _listeners = new MutableDisposable<IDisposable>();
|
|
|
|
constructor(
|
|
private readonly _element: HTMLElement,
|
|
private readonly _document: Document,
|
|
private readonly _isActive: () => boolean
|
|
) {
|
|
}
|
|
|
|
public dispose(): void {
|
|
this._listeners.dispose();
|
|
}
|
|
|
|
public sync(): void {
|
|
this._listeners.clear();
|
|
|
|
if (!this._isActive()) {
|
|
return;
|
|
}
|
|
|
|
const store = new DisposableStore();
|
|
const syncFromModifier = (ev: KeyboardEvent | MouseEvent): void => this.syncFromModifier(ev);
|
|
store.add(addDisposableListener(this._document, 'keydown', syncFromModifier));
|
|
store.add(addDisposableListener(this._document, 'keyup', syncFromModifier));
|
|
store.add(addDisposableListener(this._element, 'mousemove', syncFromModifier));
|
|
const targetWindow = this._element.ownerDocument?.defaultView;
|
|
if (targetWindow) {
|
|
store.add(addDisposableListener(targetWindow, 'blur', () => {
|
|
if (this._isActive()) {
|
|
this.resetClass();
|
|
}
|
|
}));
|
|
}
|
|
this._listeners.value = store;
|
|
}
|
|
|
|
public resetClass(): void {
|
|
this._updateClass(false);
|
|
}
|
|
|
|
public syncFromModifier(ev: KeyboardEvent | MouseEvent): void {
|
|
if (!this._isActive()) {
|
|
return;
|
|
}
|
|
this._updateClass(ev.getModifierState('Alt'));
|
|
}
|
|
|
|
private _updateClass(altHeld: boolean): void {
|
|
if (altHeld) {
|
|
this._element.classList.add(MouseEventCssClasses.ENABLE_MOUSE_EVENTS);
|
|
} else {
|
|
this._element.classList.remove(MouseEventCssClasses.ENABLE_MOUSE_EVENTS);
|
|
}
|
|
}
|
|
}
|