From cbddbc6e831cf0a4f3992038579b31d8641e525a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Fri, 19 Jul 2019 16:21:30 +0200 Subject: [PATCH] some changes in mouse event handling: - use onProtocolChange - remove mouse states from Terminal.ts - remove selectionService from InputHandler - fix up events in VT200 mode - fix move event with no buttons in ANY mode --- src/InputHandler.ts | 48 +--- src/Terminal.ts | 235 +++++++++++-------- src/TestUtils.test.ts | 4 +- src/Types.d.ts | 10 +- src/common/services/CoreMouseService.test.ts | 2 +- src/common/services/CoreMouseService.ts | 2 +- 6 files changed, 155 insertions(+), 146 deletions(-) diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 6d486592..a7d1b1fd 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -20,7 +20,6 @@ import { CellData } from 'common/buffer/CellData'; import { AttributeData } from 'common/buffer/AttributeData'; import { IAttributeData, IDisposable } from 'common/Types'; import { ICoreService, IBufferService, IOptionsService, ILogService, IDirtyRowService, ICoreMouseService } from 'common/services/Services'; -import { ISelectionService } from 'browser/services/Services'; /** * Map collect to glevel. Used in `selectCharset`. @@ -118,8 +117,6 @@ export class InputHandler extends Disposable implements IInputHandler { private _utf8Decoder: Utf8ToUtf32 = new Utf8ToUtf32(); private _workCell: CellData = new CellData(); - private _selectionService: ISelectionService | undefined; - private _onCursorMove = new EventEmitter(); public get onCursorMove(): IEvent { return this._onCursorMove.event; } private _onLineFeed = new EventEmitter(); @@ -310,11 +307,6 @@ export class InputHandler extends Disposable implements IInputHandler { super.dispose(); } - // TODO: When InputHandler moves into common, browser dependencies need to move out - public setBrowserServices(selectionService: ISelectionService): void { - this._selectionService = selectionService; - } - public parse(data: string): void { let buffer = this._bufferService.buffer; const cursorStartX = buffer.x; @@ -1277,28 +1269,20 @@ export class InputHandler extends Disposable implements IInputHandler { break; case 9: // X10 Mouse // no release, no motion, no wheel, no modifiers. + this._coreMouseService.activeProtocol = 'X10'; + break; case 1000: // vt200 mouse // no motion. // no modifiers, except control on the wheel. + this._coreMouseService.activeProtocol = 'VT200'; + break; case 1002: // button event mouse + this._coreMouseService.activeProtocol = 'DRAG'; + break; case 1003: // any event mouse // any event - sends motion events, // even if there is no button held down. - - // TODO: Why are params[0] compares nested within a switch for params[0]? - this._coreMouseService.activeProtocol = param === 9 ? 'X10' : param === 1000 ? 'VT200' : param === 1002 ? 'DRAG' : 'ANY'; - - this._terminal.x10Mouse = param === 9; - this._terminal.vt200Mouse = param === 1000; - this._terminal.normalMouse = param > 1000; - this._terminal.mouseEvents = true; - if (this._terminal.element) { - this._terminal.element.classList.add('enable-mouse-events'); - } - if (this._selectionService) { - this._selectionService.disable(); - } - this._logService.info('Binding to mouse events.'); + this._coreMouseService.activeProtocol = 'ANY'; break; case 1004: // send focusin/focusout events // focusin: ^[[I @@ -1306,13 +1290,11 @@ export class InputHandler extends Disposable implements IInputHandler { this._terminal.sendFocus = true; break; case 1005: // utf8 ext mode mouse - this._terminal.utfMouse = true; this._coreMouseService.activeEncoding = 'UTF8'; // for wide terminals // simply encodes large values as utf8 characters break; case 1006: // sgr ext mode mouse - this._terminal.sgrMouse = true; this._coreMouseService.activeEncoding = 'SGR'; // for wide terminals // does not add 32 to fields @@ -1320,7 +1302,6 @@ export class InputHandler extends Disposable implements IInputHandler { // release: ^[[ev).overrideType || ev.type) { + case 'mousemove': + action = 'move'; + if (ev.buttons !== undefined) { + but = ev.buttons & 1 ? 'left' : ev.buttons & 2 ? 'right' : ev.buttons & 4 ? 'middle' : 'none'; + } else { + but = 'none'; + } + break; case 'mouseup': action = 'up'; code = ev.button !== null && ev.button !== undefined @@ -758,7 +755,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp action = (ev).deltaY < 0 ? 'up' : 'down'; break; } - self._coreMouseService.triggerMouseEvent({ + return self._coreMouseService.triggerMouseEvent({ col: pos.x - 33, // FIXME: why -33 here? row: pos.y - 33, button: but, @@ -767,95 +764,147 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp alt: ev.altKey, shift: ev.shiftKey }); - return; } - function sendMove(ev: MouseEvent): void { - const pos = self._mouseService.getRawByteCoords(ev, self.screenElement, self.cols, self.rows); - if (!pos) return; - - let but: ICoreMouseEvent['button'] = 'none'; - if (ev.buttons !== undefined) { - but = ev.buttons & 1 ? 'left' : ev.buttons & 2 ? 'right' : ev.buttons & 4 ? 'middle' : 'none'; + /** + * Event handler state handling. + * We listen to the onProtocolChange event of CoreMouseService and put + * requested handlers in `requestedEvents`. With this the handlers have + * all bits to do the event handler juggling. + * Note: 'mousedown' currently is an "always on" handler and not managed + * by onProtocolChange. + */ + const requestedEvents: {[key: string]: any} = { + mouseup: null, + wheel: null, + mousedrag: null, + mousemove: null + }; + const eventHandlers: {[key: string]: any} = { + mouseup: (ev: MouseEvent) => { + sendEvent(ev); + if (!ev.buttons) { + // if no other button is held remove global handlers + this._document.removeEventListener('mouseup', requestedEvents.mouseup); + if (requestedEvents.mousedrag) { + this._document.removeEventListener('mousemove', requestedEvents.mousedrag); + } + } + return this.cancel(ev); + }, + wheel: (ev: WheelEvent) => { + sendEvent(ev); + ev.preventDefault(); + return this.cancel(ev); + }, + mousedrag: (ev: MouseEvent) => { + // deal only with move while a button is held + if (ev.buttons) { + sendEvent(ev); + } + }, + mousemove: (ev: MouseEvent) => { + // deal only with move eithout any button + if (!ev.buttons) { + sendEvent(ev); + } + } + }; + this._coreMouseService.onProtocolChange(events => { + // apply global changes on events + if (events.length) { + this._logService.info('Binding to mouse events:', events); + this.mouseEvents = events; + if (this.element) { + this.element.classList.add('enable-mouse-events'); + } + if (this._selectionService) { + this._selectionService.disable(); + } + } else { + this._logService.info('Unbinding from mouse events.'); + this.mouseEvents = events; + if (this.element) { + this.element.classList.remove('enable-mouse-events'); + } + if (this._selectionService) { + this._selectionService.enable(); + } } - self._coreMouseService.triggerMouseEvent({ - col: pos.x - 33, - row: pos.y - 33, - button: but, - action: 'move', - ctrl: ev.ctrlKey, - alt: ev.altKey, - shift: ev.shiftKey - }); - return; - } + if (events.indexOf('mousemove') === -1) { + el.removeEventListener('mousemove', requestedEvents.mousemove); + requestedEvents.mousemove = null; + } else if (!requestedEvents.mousemove) { + el.addEventListener('mousemove', eventHandlers.mousemove); + requestedEvents.mousemove = eventHandlers.mousemove; + } + if (events.indexOf('wheel') === -1) { + el.removeEventListener('wheel', requestedEvents.wheel); + requestedEvents.wheel = null; + } else if (!requestedEvents.wheel) { + el.addEventListener('wheel', eventHandlers.wheel); + requestedEvents.wheel = eventHandlers.wheel; + } + if (events.indexOf('mouseup') === -1) { + // always remove possible leftover handler + this._document.removeEventListener('mouseup', requestedEvents.mouseup); + requestedEvents.mouseup = null; + } else if (!requestedEvents.mouseup) { + requestedEvents.mouseup = eventHandlers.mouseup; + } + + if (events.indexOf('mousedrag') === -1) { + // always remove possible leftover handler + this._document.removeEventListener('mousemove', requestedEvents.mousedrag); + requestedEvents.mousedrag = null; + } else if (!requestedEvents.mousedrag) { + requestedEvents.mousedrag = eventHandlers.mousedrag; + } + }); + // force initial onProtocolChange so we dont miss early mouse requests + this._coreMouseService.activeProtocol = this._coreMouseService.activeProtocol; + + // DEBUG: get rid of annoying popup during testing + this.register(addDisposableDomListener(el, 'contextmenu', (e: any) => { + e.preventDefault(); + return false; + })); + + /** + * "Always on" event handlers. + */ this.register(addDisposableDomListener(el, 'mousedown', (ev: MouseEvent) => { - - // Prevent the focus on the textarea from getting lost - // and make sure we get focused on mousedown ev.preventDefault(); this.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.mouseEvents || this._selectionService.shouldForceSelection(ev)) { + if (!this.mouseEvents.length || this._selectionService.shouldForceSelection(ev)) { return; } - // send the button - sendButton(ev); + sendEvent(ev); - // fix for odd bug - // if (this.vt200Mouse && !this.normalMouse) { - if (this.vt200Mouse) { - (ev).overrideType = 'mouseup'; - sendButton(ev); - return this.cancel(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. + if (requestedEvents.mouseup) { + this._document.addEventListener('mouseup', requestedEvents.mouseup); } - - // TODO: All mouse handling should be pulled into its own file. - - // bind events - let moveHandler: (event: MouseEvent) => void; - if (this.normalMouse) { - moveHandler = (event: MouseEvent) => { - // Do nothing if normal mouse mode is on. This can happen if the mouse is held down when the - // terminal exits normalMouse mode. - if (!this.normalMouse) { - return; - } - sendMove(event); - }; - // TODO: these event listeners should be managed by the disposable, the Terminal reference may - // be kept aroud if Terminal.dispose is fired when the mouse is down - this._document.addEventListener('mousemove', moveHandler); + if (requestedEvents.mousedrag) { + this._document.addEventListener('mousemove', requestedEvents.mousedrag); } - // x10 compatibility mode can't send button releases - const handler = (ev: MouseEvent) => { - if (this.normalMouse && !this.x10Mouse) { - sendButton(ev); - } - if (moveHandler) { - // Even though this should only be attached when this.normalMouse is true, holding the - // mouse button down when normalMouse changes can happen. Just always try to remove it. - this._document.removeEventListener('mousemove', moveHandler); - moveHandler = null; - } - this._document.removeEventListener('mouseup', handler); - return this.cancel(ev); - }; - this._document.addEventListener('mouseup', handler); - return this.cancel(ev); })); this.register(addDisposableDomListener(el, 'wheel', (ev: WheelEvent) => { - if (!this.mouseEvents) { + if (!requestedEvents.wheel) { // 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. if (!this.buffer.hasScrollback) { @@ -876,32 +925,30 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp } return; } - if (this.x10Mouse || this._vt300Mouse || this._decLocator) return; - sendButton(ev); - ev.preventDefault(); })); // allow wheel scrolling in // the shell for example this.register(addDisposableDomListener(el, 'wheel', (ev: WheelEvent) => { - if (this.mouseEvents) return; + if (requestedEvents.wheel) return; this.viewport.onWheel(ev); return this.cancel(ev); })); this.register(addDisposableDomListener(el, 'touchstart', (ev: TouchEvent) => { - if (this.mouseEvents) return; + if (this.mouseEvents.length) return; this.viewport.onTouchStart(ev); return this.cancel(ev); })); this.register(addDisposableDomListener(el, 'touchmove', (ev: TouchEvent) => { - if (this.mouseEvents) return; + if (this.mouseEvents.length) return; this.viewport.onTouchMove(ev); return this.cancel(ev); })); } + /** * Tells the renderer to refresh terminal content between two rows (inclusive) at the next * opportunity. diff --git a/src/TestUtils.test.ts b/src/TestUtils.test.ts index 0ee948d5..5805ba3d 100644 --- a/src/TestUtils.test.ts +++ b/src/TestUtils.test.ts @@ -6,7 +6,7 @@ import { IRenderer, IRenderDimensions, CharacterJoinerHandler } from 'browser/renderer/Types'; import { IInputHandlingTerminal, ICompositionHelper, ITerminal, IBrowser, ITerminalOptions } from './Types'; import { IBuffer, IBufferStringIterator, IBufferSet } from 'common/buffer/Types'; -import { IBufferLine, ICellData, IAttributeData, ICircularList, XtermListener, ICharset } from 'common/Types'; +import { IBufferLine, ICellData, IAttributeData, ICircularList, XtermListener, ICharset, CoreMouseEventType } from 'common/Types'; import { Buffer } from 'common/buffer/Buffer'; import * as Browser from 'common/Platform'; import { IDisposable, IMarker, IEvent, ISelectionPosition } from 'xterm'; @@ -210,7 +210,7 @@ export class MockInputHandlingTerminal implements IInputHandlingTerminal { x10Mouse: boolean; vt200Mouse: boolean; normalMouse: boolean; - mouseEvents: boolean; + mouseEvents: CoreMouseEventType[]; sendFocus: boolean; utfMouse: boolean; sgrMouse: boolean; diff --git a/src/Types.d.ts b/src/Types.d.ts index f96ba07d..80e8848c 100644 --- a/src/Types.d.ts +++ b/src/Types.d.ts @@ -4,7 +4,7 @@ */ import { ITerminalOptions as IPublicTerminalOptions, IDisposable, IMarker, ISelectionPosition } from 'xterm'; -import { ICharset, IAttributeData, CharData } from 'common/Types'; +import { ICharset, IAttributeData, CharData, CoreMouseEventType } from 'common/Types'; import { IEvent, IEventEmitter } from 'common/EventEmitter'; import { IColorSet, ILinkifier, ILinkMatcherOptions, IViewport } from 'browser/Types'; import { IOptionsService } from 'common/services/Services'; @@ -36,14 +36,8 @@ export interface IInputHandlingTerminal { bracketedPasteMode: boolean; curAttrData: IAttributeData; savedCols: number; - x10Mouse: boolean; - vt200Mouse: boolean; - normalMouse: boolean; - mouseEvents: boolean; + mouseEvents: CoreMouseEventType[]; sendFocus: boolean; - utfMouse: boolean; - sgrMouse: boolean; - urxvtMouse: boolean; cursorHidden: boolean; buffers: IBufferSet; diff --git a/src/common/services/CoreMouseService.test.ts b/src/common/services/CoreMouseService.test.ts index b4131f00..4f4392f1 100644 --- a/src/common/services/CoreMouseService.test.ts +++ b/src/common/services/CoreMouseService.test.ts @@ -67,7 +67,7 @@ describe('CoreMouseService', () => { cms.activeProtocol = 'NONE'; assert.deepEqual(wantedEvents, [[]]); cms.activeProtocol = 'ANY'; - assert.deepEqual(wantedEvents, [[], ['mousedown', 'mouseup', 'wheel', 'mousemove']]); + assert.deepEqual(wantedEvents, [[], ['mousedown', 'mouseup', 'wheel', 'mousedrag', 'mousemove']]); }); describe('triggerMouseEvent', () => { let cms: CoreMouseService; diff --git a/src/common/services/CoreMouseService.ts b/src/common/services/CoreMouseService.ts index cddc058d..bafb9b12 100644 --- a/src/common/services/CoreMouseService.ts +++ b/src/common/services/CoreMouseService.ts @@ -78,7 +78,7 @@ const DEFAULT_PROCOTOLS: {[key: string]: ICoreMouseProtocol} = { * Modifiers: CTRL | ALT | SHIFT */ ANY: { - events: ['mousedown', 'mouseup', 'wheel', 'mousemove'], + events: ['mousedown', 'mouseup', 'wheel', 'mousedrag', 'mousemove'], restrict: (e: ICoreMouseEvent) => true } };