From 4858bdc4646a0e03348e666333c63eec0411d856 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Fri, 19 Jul 2019 01:45:27 +0200 Subject: [PATCH 01/17] CoreMouseService --- src/InputHandler.test.ts | 20 +- src/InputHandler.ts | 11 +- src/Terminal.ts | 249 ++++------------ src/common/TestUtils.test.ts | 14 +- src/common/Types.d.ts | 61 ++++ src/common/services/CoreMouseService.test.ts | 214 ++++++++++++++ src/common/services/CoreMouseService.ts | 294 +++++++++++++++++++ src/common/services/Services.ts | 28 +- 8 files changed, 684 insertions(+), 207 deletions(-) create mode 100644 src/common/services/CoreMouseService.test.ts create mode 100644 src/common/services/CoreMouseService.ts diff --git a/src/InputHandler.test.ts b/src/InputHandler.test.ts index 38645a0b..4a27c55e 100644 --- a/src/InputHandler.test.ts +++ b/src/InputHandler.test.ts @@ -13,7 +13,7 @@ import { CellData } from 'common/buffer/CellData'; import { Attributes } from 'common/buffer/Constants'; import { AttributeData } from 'common/buffer/AttributeData'; import { Params } from 'common/parser/Params'; -import { MockCoreService, MockBufferService, MockDirtyRowService, MockOptionsService, MockLogService } from 'common/TestUtils.test'; +import { MockCoreService, MockBufferService, MockDirtyRowService, MockOptionsService, MockLogService, MockCoreMouseService } from 'common/TestUtils.test'; import { IBufferService } from 'common/services/Services'; import { DEFAULT_OPTIONS } from 'common/services/OptionsService'; import { clone } from 'common/Clone'; @@ -33,7 +33,7 @@ describe('InputHandler', () => { bufferService.buffer.x = 1; bufferService.buffer.y = 2; bufferService.buffer.ybase = 0; - const inputHandler = new InputHandler(terminal, bufferService, new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService()); + const inputHandler = new InputHandler(terminal, bufferService, new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService()); // Save cursor position inputHandler.saveCursor(); assert.equal(bufferService.buffer.x, 1); @@ -52,7 +52,7 @@ describe('InputHandler', () => { describe('setCursorStyle', () => { it('should call Terminal.setOption with correct params', () => { const optionsService = new MockOptionsService(); - const inputHandler = new InputHandler(new MockInputHandlingTerminal(), new MockBufferService(80, 30), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), optionsService); + const inputHandler = new InputHandler(new MockInputHandlingTerminal(), new MockBufferService(80, 30), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), optionsService, new MockCoreMouseService()); const collect = ' '; inputHandler.setCursorStyle(Params.fromArray([0]), collect); @@ -95,7 +95,7 @@ describe('InputHandler', () => { const terminal = new MockInputHandlingTerminal(); const collect = '?'; terminal.bracketedPasteMode = false; - const inputHandler = new InputHandler(terminal, new MockBufferService(80, 30), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService()); + const inputHandler = new InputHandler(terminal, new MockBufferService(80, 30), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService()); // Set bracketed paste mode inputHandler.setMode(Params.fromArray([2004]), collect); assert.equal(terminal.bracketedPasteMode, true); @@ -114,7 +114,7 @@ describe('InputHandler', () => { it('insertChars', function(): void { const term = new Terminal(); const bufferService = new MockBufferService(80, 30); - const inputHandler = new InputHandler(term, bufferService, new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService()); + const inputHandler = new InputHandler(term, bufferService, new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService()); // insert some data in first and second line inputHandler.parse(Array(bufferService.cols - 9).join('a')); @@ -152,7 +152,7 @@ describe('InputHandler', () => { it('deleteChars', function(): void { const term = new Terminal(); const bufferService = new MockBufferService(80, 30); - const inputHandler = new InputHandler(term, bufferService, new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService()); + const inputHandler = new InputHandler(term, bufferService, new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService()); // insert some data in first and second line inputHandler.parse(Array(bufferService.cols - 9).join('a')); @@ -193,7 +193,7 @@ describe('InputHandler', () => { it('eraseInLine', function(): void { const term = new Terminal(); const bufferService = new MockBufferService(80, 30); - const inputHandler = new InputHandler(term, bufferService, new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService()); + const inputHandler = new InputHandler(term, bufferService, new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService()); // fill 6 lines to test 3 different states inputHandler.parse(Array(bufferService.cols + 1).join('a')); @@ -222,7 +222,7 @@ describe('InputHandler', () => { it('eraseInDisplay', function(): void { const term = new Terminal({cols: 80, rows: 7}); const bufferService = new MockBufferService(80, 7); - const inputHandler = new InputHandler(term, bufferService, new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService()); + const inputHandler = new InputHandler(term, bufferService, new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService()); // fill display with a's for (let i = 0; i < bufferService.rows; ++i) inputHandler.parse(Array(bufferService.cols + 1).join('a')); @@ -357,7 +357,7 @@ describe('InputHandler', () => { describe('print', () => { it('should not cause an infinite loop (regression test)', () => { const term = new Terminal(); - const inputHandler = new InputHandler(term, new MockBufferService(80, 30), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService()); + const inputHandler = new InputHandler(term, new MockBufferService(80, 30), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService()); const container = new Uint32Array(10); container[0] = 0x200B; inputHandler.print(container, 0, 1); @@ -372,7 +372,7 @@ describe('InputHandler', () => { beforeEach(() => { term = new Terminal(); bufferService = new MockBufferService(80, 30); - handler = new InputHandler(term, bufferService, new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService()); + handler = new InputHandler(term, bufferService, new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService()); }); it('should handle DECSET/DECRST 47 (alt screen buffer)', () => { handler.parse('\x1b[?47h\r\n\x1b[31mJUNK\x1b[?47lTEST'); diff --git a/src/InputHandler.ts b/src/InputHandler.ts index cc767a16..6d486592 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -19,7 +19,7 @@ import { NULL_CELL_CODE, NULL_CELL_WIDTH, Attributes, FgFlags, BgFlags, Content import { CellData } from 'common/buffer/CellData'; import { AttributeData } from 'common/buffer/AttributeData'; import { IAttributeData, IDisposable } from 'common/Types'; -import { ICoreService, IBufferService, IOptionsService, ILogService, IDirtyRowService } from 'common/services/Services'; +import { ICoreService, IBufferService, IOptionsService, ILogService, IDirtyRowService, ICoreMouseService } from 'common/services/Services'; import { ISelectionService } from 'browser/services/Services'; /** @@ -134,6 +134,7 @@ export class InputHandler extends Disposable implements IInputHandler { private readonly _dirtyRowService: IDirtyRowService, private readonly _logService: ILogService, private readonly _optionsService: IOptionsService, + private readonly _coreMouseService: ICoreMouseService, private readonly _parser: IEscapeSequenceParser = new EscapeSequenceParser()) { super(); @@ -1285,6 +1286,7 @@ export class InputHandler extends Disposable implements IInputHandler { // 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; @@ -1305,11 +1307,13 @@ export class InputHandler extends Disposable implements IInputHandler { 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 // press: ^[[ this.scrollToBottom()); this._instantiationService.setService(ICoreService, this._coreService); this._coreService.onData(e => this._onData.fire(e)); + this._coreMouseService = this._instantiationService.createInstance(CoreMouseService); + this._instantiationService.setService(ICoreMouseService, this._coreMouseService); this._dirtyRowService = this._instantiationService.createInstance(DirtyRowService); this._instantiationService.setService(IDirtyRowService, this._dirtyRowService); this._logService = this._instantiationService.createInstance(LogService); @@ -309,7 +313,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this._userScrolling = false; // Register input handler and refire/handle events - this._inputHandler = new InputHandler(this, this._bufferService, this._coreService, this._dirtyRowService, this._logService, this.optionsService); + this._inputHandler = new InputHandler(this, this._bufferService, this._coreService, this._dirtyRowService, this._logService, this.optionsService, this._coreMouseService); this._inputHandler.onCursorMove(() => this._onCursorMove.fire()); this._inputHandler.onLineFeed(() => this._onLineFeed.fire()); this.register(this._inputHandler); @@ -709,223 +713,85 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp } /** - * XTerm mouse events - * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking - * To better understand these - * the xterm code is very helpful: - * Relevant files: - * button.c, charproc.c, misc.c - * Relevant functions in xterm/button.c: - * BtnCode, EmitButtonCode, EditorButton, SendMousePosition + * mouse events + * FIXME: move event handler registration into browser MouseService */ public bindMouse(): void { const el = this.element; const self = this; - let pressed = 32; - // mouseup, mousedown, wheel - // left click: ^[[M 3<^[[M#3< - // wheel up: ^[[M`3> function sendButton(ev: MouseEvent | WheelEvent): void { - let button; let pos; - // get the xterm-style button - button = getButton(ev); - // get mouse coordinates pos = self._mouseService.getRawByteCoords(ev, self.screenElement, self.cols, self.rows); if (!pos) return; - sendEvent(button, pos); - + let but: ICoreMouseEvent['button']; + let action: ICoreMouseEvent['action']; + let code: number; switch ((ev).overrideType || ev.type) { - case 'mousedown': - pressed = button; - break; case 'mouseup': - // keep it at the left - // button, just in case. - pressed = 32; - break; - case 'wheel': - // nothing. don't - // interfere with - // `pressed`. - break; - } - } - - // motion example of a left click: - // ^[[M 3<^[[M@4<^[[M@5<^[[M@6<^[[M@7<^[[M#7< - function sendMove(ev: MouseEvent): void { - let button = pressed; - const pos = self._mouseService.getRawByteCoords(ev, self.screenElement, self.cols, self.rows); - if (!pos) return; - - // buttons marked as motions - // are incremented by 32 - button += 32; - - sendEvent(button, pos); - } - - // encode button and - // position to characters - function encode(data: number[], ch: number): void { - if (!self.utfMouse) { - if (ch === 255) { - data.push(0); - return; - } - if (ch > 127) ch = 127; - data.push(ch); - } else { - if (ch > 2047) { - data.push(2047); - return; - } - data.push(ch); - } - } - - // send a mouse event: - // regular/utf8: ^[[M Cb Cx Cy - // urxvt: ^[[ Cb ; Cx ; Cy M - // sgr: ^[[ Cb ; Cx ; Cy M/m - // vt300: ^[[ 24(1/3/5)~ [ Cx , Cy ] \r - // locator: CSI P e ; P b ; P r ; P c ; P p & w - function sendEvent(button: number, pos: {x: number, y: number}): void { - if (self._vt300Mouse) { - // NOTE: Unstable. - // http://www.vt100.net/docs/vt3xx-gp/chapter15.html - button &= 3; - pos.x -= 32; - pos.y -= 32; - let data = C0.ESC + '[24'; - if (button === 0) data += '1'; - else if (button === 1) data += '3'; - else if (button === 2) data += '5'; - else if (button === 3) return; - else data += '0'; - data += '~[' + pos.x + ',' + pos.y + ']\r'; - self._coreService.triggerDataEvent(data, true); - return; - } - - if (self._decLocator) { - // NOTE: Unstable. - button &= 3; - pos.x -= 32; - pos.y -= 32; - if (button === 0) button = 2; - else if (button === 1) button = 4; - else if (button === 2) button = 6; - else if (button === 3) button = 3; - self._coreService.triggerDataEvent(C0.ESC + '[' - + button - + ';' - + (button === 3 ? 4 : 0) - + ';' - + pos.y - + ';' - + pos.x - + ';' - // Not sure what page is meant to be - + (pos).page || 0 - + '&w', true); - return; - } - - if (self.urxvtMouse) { - pos.x -= 32; - pos.y -= 32; - pos.x++; - pos.y++; - self._coreService.triggerDataEvent(C0.ESC + '[' + button + ';' + pos.x + ';' + pos.y + 'M', true); - return; - } - - if (self.sgrMouse) { - pos.x -= 32; - pos.y -= 32; - self._coreService.triggerDataEvent(C0.ESC + '[<' - + (((button & 3) === 3 ? button & ~3 : button) - 32) - + ';' - + pos.x - + ';' - + pos.y - + ((button & 3) === 3 ? 'm' : 'M'), true); - return; - } - - const data: number[] = []; - - encode(data, button); - encode(data, pos.x); - encode(data, pos.y); - - self._coreService.triggerDataEvent(C0.ESC + '[M' + String.fromCharCode.apply(String, data), true); - } - - function getButton(ev: MouseEvent): number { - let button; - let shift; - let meta; - let ctrl; - let mod; - - // two low bits: - // 0 = left - // 1 = middle - // 2 = right - // 3 = release - // wheel up/down: - // 1, and 2 - with 64 added - switch ((ev).overrideType || ev.type) { - case 'mousedown': - button = ev.button !== null && ev.button !== undefined + action = 'up'; + code = ev.button !== null && ev.button !== undefined ? +ev.button : ev.which !== null && ev.which !== undefined ? ev.which - 1 : null; + but = code === 0 ? 'left' : code === 1 ? 'middle' : 'right'; break; - case 'mouseup': - button = 3; + case 'mousedown': + action = 'down'; + code = ev.button !== null && ev.button !== undefined + ? +ev.button + : ev.which !== null && ev.which !== undefined + ? ev.which - 1 + : null; + but = code === 0 ? 'left' : code === 1 ? 'middle' : 'right'; break; case 'DOMMouseScroll': - button = ev.detail < 0 - ? 64 - : 65; + but = 'wheel'; + action = ev.detail < 0 ? 'up' : 'down'; break; case 'wheel': - button = (ev).deltaY < 0 - ? 64 - : 65; + but = 'wheel'; + action = (ev).deltaY < 0 ? 'up' : 'down'; break; } + self._coreMouseService.triggerMouseEvent({ + col: pos.x - 33, // FIXME: why -33 here? + row: pos.y - 33, + button: but, + action, + ctrl: ev.ctrlKey, + alt: ev.altKey, + shift: ev.shiftKey + }); + return; + } - // next three bits are the modifiers: - // 4 = shift, 8 = meta, 16 = control - shift = ev.shiftKey ? 4 : 0; - meta = ev.metaKey ? 8 : 0; - ctrl = ev.ctrlKey ? 16 : 0; - mod = shift | meta | ctrl; + function sendMove(ev: MouseEvent): void { + const pos = self._mouseService.getRawByteCoords(ev, self.screenElement, self.cols, self.rows); + if (!pos) return; - // no mods - if (self.vt200Mouse) { - // ctrl only - mod &= ctrl; - } else if (!self.normalMouse) { - mod = 0; + let but: ICoreMouseEvent['button'] = 'none'; + if (ev.buttons !== undefined) { + but = ev.buttons & 1 ? 'left' : ev.buttons & 2 ? 'right' : ev.buttons & 4 ? 'middle' : 'none'; } - // increment to SP - button = (32 + (mod << 2)) + button; - - return button; + 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; } + this.register(addDisposableDomListener(el, 'mousedown', (ev: MouseEvent) => { // Prevent the focus on the textarea from getting lost @@ -988,10 +854,6 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp return this.cancel(ev); })); - // if (this.normalMouse) { - // on(this.document, 'mousemove', sendMove); - // } - this.register(addDisposableDomListener(el, 'wheel', (ev: WheelEvent) => { if (!this.mouseEvents) { // Convert wheel events into up/down events when the buffer does not have scrollback, this @@ -1790,6 +1652,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this._setup(); this._bufferService.reset(); this._coreService.reset(); + this._coreMouseService.reset(); if (this._selectionService) { this._selectionService.reset(); } diff --git a/src/common/TestUtils.test.ts b/src/common/TestUtils.test.ts index ec36be2f..bbeb4de8 100644 --- a/src/common/TestUtils.test.ts +++ b/src/common/TestUtils.test.ts @@ -3,13 +3,13 @@ * @license MIT */ -import { IBufferService, ICoreService, ILogService, IOptionsService, ITerminalOptions, IPartialTerminalOptions, IDirtyRowService } from 'common/services/Services'; +import { IBufferService, ICoreService, ILogService, IOptionsService, ITerminalOptions, IPartialTerminalOptions, IDirtyRowService, ICoreMouseService } from 'common/services/Services'; import { IEvent, EventEmitter } from 'common/EventEmitter'; import { clone } from 'common/Clone'; import { DEFAULT_OPTIONS } from 'common/services/OptionsService'; import { IBufferSet, IBuffer } from 'common/buffer/Types'; import { BufferSet } from 'common/buffer/BufferSet'; -import { IDecPrivateModes } from 'common/Types'; +import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEventType } from 'common/Types'; export class MockBufferService implements IBufferService { serviceBrand: any; @@ -29,6 +29,16 @@ export class MockBufferService implements IBufferService { reset(): void {} } +export class MockCoreMouseService implements ICoreMouseService { + activeEncoding: string = ''; + activeProtocol: string = ''; + addEncoding(name: string): void {} + addProtocol(name: string): void {} + reset(): void {} + triggerMouseEvent(event: ICoreMouseEvent): boolean { return false; } + onProtocolChange: IEvent = new EventEmitter().event; +} + export class MockCoreService implements ICoreService { serviceBrand: any; decPrivateModes: IDecPrivateModes = {} as any; diff --git a/src/common/Types.d.ts b/src/common/Types.d.ts index b25b50a5..598a6a9e 100644 --- a/src/common/Types.d.ts +++ b/src/common/Types.d.ts @@ -158,3 +158,64 @@ export interface IRowRange { start: number; end: number; } + +/** + * Interface for mouse events in the core. + */ +export interface ICoreMouseEvent { + /** column (zero based). */ + col: number; + /** row (zero based). */ + row: number; + /** + * Button the action occured. Due to restrictions of the tracking protocols + * it is not possible to report multiple buttons at once. + * Wheel is treated as a button. + * There are invalid combinations of buttons and actions possible + * (like move + wheel), those are silently ignored by the CoreMouseService. + */ + button: 'left' | 'middle' | 'right' | 'wheel' | 'none'; + action: 'up' | 'down' | 'move'; + /** + * Modifier states. + * Protocols will add/ignore those based on specific restrictions. + */ + ctrl?: boolean; + alt?: boolean; + shift?: boolean; +} + +/** + * CoreMouseEventType + * To be reported to the browser component which events a mouse + * protocol wants to be catched and forwarded as an ICoreMouseEvent + * to CoreMouseService. + * Known types: + * - mousedown: any mousedown event + * - mouseup: any mouseup event + * - wheel: any wheel event + * - mousedrag: any mousemove event while a button is pressed + * - mousemove: any mousemove event + */ +export type CoreMouseEventType = 'mousedown' | 'mouseup' | 'wheel' | 'mousemove' | 'mousedrag'; + +/** + * Mouse protocol interface. + * A mouse protocol can be registered and activated at the CoreMouseService. + * `events` should contain a list of needed events as a hint for the browser component + * to install/remove the appropriate event handlers. + * `restrict` applies further protocol specific restrictions like not allowed + * modifiers or filtering invalid event types. + */ +export interface ICoreMouseProtocol { + events: CoreMouseEventType[]; + restrict: (e: ICoreMouseEvent) => boolean; +} + +/** + * CoreMouseEncoding + * The tracking encoding can be registered and activated at the CoreMouseService. + * If a ICoreMouseEvent passes all procotol restrictions it will be encoded + * with the active encoding and sent out. + */ +export type CoreMouseEncoding = (event: ICoreMouseEvent) => string; diff --git a/src/common/services/CoreMouseService.test.ts b/src/common/services/CoreMouseService.test.ts new file mode 100644 index 00000000..b4131f00 --- /dev/null +++ b/src/common/services/CoreMouseService.test.ts @@ -0,0 +1,214 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ +import { CoreMouseService } from 'common/services/CoreMouseService'; +import { MockCoreService, MockBufferService } from 'common/TestUtils.test'; +import { assert } from 'chai'; +import { ICoreMouseEvent, CoreMouseEventType } from 'common/Types'; + +// needed mock services +const bufferService = new MockBufferService(300, 100); +const coreService = new MockCoreService(); + +function toBytes(s: string | undefined): number[] { + if (!s) { + return []; + } + const res: number[] = []; + for (let i = 0; i < s.length; ++i) { + res.push(s.charCodeAt(i)); + } + return res; +} + +describe('CoreMouseService', () => { + it('init', () => { + const cms = new CoreMouseService(bufferService, coreService); + assert.equal(cms.activeEncoding, 'DEFAULT'); + assert.equal(cms.activeProtocol, 'NONE'); + }); + it('default protocols - NONE, X10, VT200, DRAG, ANY', () => { + const cms = new CoreMouseService(bufferService, coreService); + assert.deepEqual(Object.keys((cms as any)._protocols), ['NONE', 'X10', 'VT200', 'DRAG', 'ANY']); + }); + it('default encodings - DEFAULT, UTF8, SGR, URXVT', () => { + const cms = new CoreMouseService(bufferService, coreService); + assert.deepEqual(Object.keys((cms as any)._encodings), ['DEFAULT', 'UTF8', 'SGR', 'URXVT']); + }); + it('protocol/encoding setter, reset', () => { + const cms = new CoreMouseService(bufferService, coreService); + cms.activeEncoding = 'SGR'; + cms.activeProtocol = 'ANY'; + assert.equal(cms.activeEncoding, 'SGR'); + assert.equal(cms.activeProtocol, 'ANY'); + cms.reset(); + assert.equal(cms.activeEncoding, 'DEFAULT'); + assert.equal(cms.activeProtocol, 'NONE'); + assert.throws(() => { cms.activeEncoding = 'xyz'; }, 'unknown encoding "xyz"'); + assert.throws(() => { cms.activeProtocol = 'xyz'; }, 'unknown protocol "xyz"'); + }); + it('addEncoding', () => { + const cms = new CoreMouseService(bufferService, coreService); + cms.addEncoding('XYZ', (e: ICoreMouseEvent) => ''); + cms.activeEncoding = 'XYZ'; + assert.equal(cms.activeEncoding, 'XYZ'); + }); + it('addProtocol', () => { + const cms = new CoreMouseService(bufferService, coreService); + cms.addProtocol('XYZ', { events: [], restrict: (e: ICoreMouseEvent) => false }); + cms.activeProtocol = 'XYZ'; + assert.equal(cms.activeProtocol, 'XYZ'); + }); + it('onProtocolChange', () => { + const cms = new CoreMouseService(bufferService, coreService); + const wantedEvents: CoreMouseEventType[][] = []; + cms.onProtocolChange(events => wantedEvents.push(events)); + cms.activeProtocol = 'NONE'; + assert.deepEqual(wantedEvents, [[]]); + cms.activeProtocol = 'ANY'; + assert.deepEqual(wantedEvents, [[], ['mousedown', 'mouseup', 'wheel', 'mousemove']]); + }); + describe('triggerMouseEvent', () => { + let cms: CoreMouseService; + let reports: string[]; + beforeEach(() => { + cms = new CoreMouseService(bufferService, coreService); + reports = []; + coreService.triggerDataEvent = (data: string, userInput?: boolean) => reports.push(data); + }); + it('NONE', () => { + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'left', action: 'down' }), false); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'left', action: 'up' }), false); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'left', action: 'move' }), false); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'middle', action: 'down' }), false); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'right', action: 'down' }), false); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'wheel', action: 'up' }), false); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'none', action: 'move' }), false); + }); + it('X10', () => { + cms.activeProtocol = 'X10'; + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'left', action: 'down' }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'left', action: 'up' }), false); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'left', action: 'move' }), false); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'middle', action: 'down' }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'right', action: 'down' }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'wheel', action: 'up' }), false); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'none', action: 'move' }), false); + }); + it('VT200', () => { + cms.activeProtocol = 'VT200'; + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'left', action: 'down' }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'left', action: 'up' }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'left', action: 'move' }), false); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'middle', action: 'down' }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'right', action: 'down' }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'wheel', action: 'up' }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'none', action: 'move' }), false); + }); + it('DRAG', () => { + cms.activeProtocol = 'DRAG'; + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'left', action: 'down' }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'left', action: 'up' }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'left', action: 'move' }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'middle', action: 'down' }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'right', action: 'down' }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'wheel', action: 'up' }), true); + }); + it('ANY', () => { + cms.activeProtocol = 'ANY'; + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'left', action: 'down' }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'left', action: 'up' }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'left', action: 'move' }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'middle', action: 'down' }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'right', action: 'down' }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'wheel', action: 'up' }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'none', action: 'move' }), true); + // should not report in any case + // invalid button + action combinations + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'wheel', action: 'move' }), false); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'none', action: 'down' }), false); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'none', action: 'up' }), false); + // invalid coords + assert.equal(cms.triggerMouseEvent({ col: -1, row: 0, button: 'left', action: 'down' }), false); + assert.equal(cms.triggerMouseEvent({ col: 500, row: 0, button: 'left', action: 'down' }), false); + assert.equal(cms.triggerMouseEvent({ col: 0, row: -1, button: 'left', action: 'down' }), false); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 500, button: 'left', action: 'down' }), false); + }); + describe('coords', () => { + it('DEFAULT encoding', () => { + cms.activeProtocol = 'ANY'; + for (let i = 0; i < bufferService.cols; ++i) { + assert.equal(cms.triggerMouseEvent({ col: i, row: 0, button: 'left', action: 'down' }), true); + // capped at 95 + if (i < 95) { + assert.deepEqual(toBytes(reports.pop()), [0x1b, 0x5b, 0x4d, 0x20, i + 33, 0x21]); + } else { + assert.deepEqual(toBytes(reports.pop()), [0x1b, 0x5b, 0x4d, 0x20, 0x7f, 0x21]); + } + } + }); + it('UTF8 encoding', () => { + cms.activeProtocol = 'ANY'; + cms.activeEncoding = 'UTF8'; + for (let i = 0; i < bufferService.cols; ++i) { + assert.equal(cms.triggerMouseEvent({ col: i, row: 0, button: 'left', action: 'down' }), true); + assert.deepEqual(toBytes(reports.pop()), [0x1b, 0x5b, 0x4d, 0x20, i + 33, 0x21]); + } + }); + it('SGR encoding', () => { + cms.activeProtocol = 'ANY'; + cms.activeEncoding = 'SGR'; + for (let i = 0; i < bufferService.cols; ++i) { + assert.equal(cms.triggerMouseEvent({ col: i, row: 0, button: 'left', action: 'down' }), true); + assert.deepEqual(reports.pop(), `\x1b[<0;${i + 1};1M`); + } + }); + it('URXVT', () => { + cms.activeProtocol = 'ANY'; + cms.activeEncoding = 'URXVT'; + for (let i = 0; i < bufferService.cols; ++i) { + assert.equal(cms.triggerMouseEvent({ col: i, row: 0, button: 'left', action: 'down' }), true); + assert.deepEqual(reports.pop(), `\x1b[32;${i + 1};1M`); + } + }); + }); + it('eventCodes with modifiers (DEFAULT encoding)', () => { + cms.activeProtocol = 'ANY'; + cms.activeEncoding = 'DEFAULT'; + // all buttons + down + no modifer + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'left', action: 'down', ctrl: false, alt: false, shift: false }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'middle', action: 'down', ctrl: false, alt: false, shift: false }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'right', action: 'down', ctrl: false, alt: false, shift: false }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'wheel', action: 'down', ctrl: false, alt: false, shift: false }), true); + assert.deepEqual(reports, ['\x1b[M !!', '\x1b[M!!!', '\x1b[M"!!', '\x1b[Ma!!']); + while (reports.pop()) { } + + // all buttons + up + no modifier + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'left', action: 'up', ctrl: false, alt: false, shift: false }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'middle', action: 'up', ctrl: false, alt: false, shift: false }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'right', action: 'up', ctrl: false, alt: false, shift: false }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'wheel', action: 'up', ctrl: false, alt: false, shift: false }), true); + assert.deepEqual(reports, ['\x1b[M#!!', '\x1b[M#!!', '\x1b[M#!!', '\x1b[M`!!']); + while (reports.pop()) { } + + // all buttons + move + no modifier + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'left', action: 'move', ctrl: false, alt: false, shift: false }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'middle', action: 'move', ctrl: false, alt: false, shift: false }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'right', action: 'move', ctrl: false, alt: false, shift: false }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'none', action: 'move', ctrl: false, alt: false, shift: false }), true); + assert.deepEqual(reports, ['\x1b[M@!!', '\x1b[MA!!', '\x1b[MB!!', '\x1b[MC!!']); + while (reports.pop()) { } + + // button none + move + modifiers + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'none', action: 'move', ctrl: true, alt: false, shift: false }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'none', action: 'move', ctrl: false, alt: true, shift: false }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'none', action: 'move', ctrl: false, alt: false, shift: true }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'none', action: 'move', ctrl: true, alt: true, shift: false }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'none', action: 'move', ctrl: false, alt: true, shift: true }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'none', action: 'move', ctrl: true, alt: true, shift: true }), true); + assert.deepEqual(reports, ['\x1b[MS!!', '\x1b[MK!!', '\x1b[MG!!', '\x1b[M[!!', '\x1b[MO!!', '\x1b[M_!!']); + while (reports.pop()) { } + }); + }); +}); diff --git a/src/common/services/CoreMouseService.ts b/src/common/services/CoreMouseService.ts new file mode 100644 index 00000000..6549c002 --- /dev/null +++ b/src/common/services/CoreMouseService.ts @@ -0,0 +1,294 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ +import { IBufferService, ICoreService, ICoreMouseService } from 'common/services/Services'; +import { EventEmitter, IEvent } from 'common/EventEmitter'; +import { ICoreMouseProtocol, ICoreMouseEvent, CoreMouseEncoding, CoreMouseEventType } from 'common/Types'; + +/** + * Supported default protocols. + */ +const DEFAULT_PROCOTOLS: {[key: string]: ICoreMouseProtocol} = { + /** + * NONE + * Events: none + * Modifiers: none + */ + NONE: { + events: [], + restrict: () => false + }, + /** + * X10 + * Events: mousedown + * Modifiers: none (TBD) + */ + X10: { + events: ['mousedown'], + restrict: (e: ICoreMouseEvent) => { + // no wheel (TBD), no move, no up + if (e.button === 'wheel' || e.action !== 'down') { + return false; + } + // no modifiers (TDB) + e.ctrl = false; + e.alt = false; + e.shift = false; + return true; + } + }, + /** + * VT200 + * Events: mousedown / mouseup / wheel + * Modifiers: CTRL (TBD) + */ + VT200: { + events: ['mousedown', 'mouseup', 'wheel'], + restrict: (e: ICoreMouseEvent) => { + // no move + if (e.action === 'move') { + return false; + } + // modifiers - only ctrl? + e.alt = false; + e.shift = false; + return true; + } + }, + /** + * DRAG + * Events: mousedown / mouseup / wheel / mousedrag + * Modifiers: CTRL | ALT | SHIFT + */ + DRAG: { + events: ['mousedown', 'mouseup', 'wheel', 'mousedrag'], + restrict: (e: ICoreMouseEvent) => { + // no move without button + if (e.action === 'move' && e.button === 'none') { + return false; + } + // modifiers unclear - let all pass for now + return true; + } + }, + /** + * ANY + * Events: all mouse related events + * Modifiers: CTRL | ALT | SHIFT + */ + ANY: { + events: ['mousedown', 'mouseup', 'wheel', 'mousemove'], + restrict: (e: ICoreMouseEvent) => true + } +}; + +/** + * Mapping of buttons and actions to event codes. (taken from xterm spec) + * More than 3 buttons are not supported. + */ +enum CODEMAP { + // buttons + left = 0, + middle = 1, + right = 2, + none = 3, + wheel = 64, + // actions + up = 0, + down = 1, + move = 32, + // modifiers + shift = 4, + alt = 8, + ctrl = 16 +} + +// helper for default encoders to generate the event code. +function eventCode(e: ICoreMouseEvent, isSGR: boolean): number { + const button = CODEMAP[e.button]; + const action = CODEMAP[e.action]; + const modifier = (e.ctrl ? CODEMAP.ctrl : 0) | (e.shift ? CODEMAP.shift : 0) | (e.alt ? CODEMAP.alt : 0); + let code = button | modifier; + if (e.button === 'wheel') { + code |= action; + } else { + if (e.action === 'move') { + code |= CODEMAP.move; + } else if (e.action === 'up' && !isSGR) { + // special case - only SGR can report button on release + // all others have to go with NONE + code |= CODEMAP.none; + } + } + return code; +} + +const S = String.fromCharCode; + +/** + * Supported default encodings. + */ +const DEFAULT_ENCODINGS: {[key: string]: CoreMouseEncoding} = { + /** + * DEFAULT - CSI M Pb Px Py + * Single byte encoding for coords and event code. + * Can encode values up to 223. The Encoding of higher + * values is not UTF-8 compatible (and currently limited + * to 95 in xterm.js). + */ + DEFAULT: (e: ICoreMouseEvent) => { + let params = [eventCode(e, false) + 32, e.col + 32, e.row + 32]; + // FIXME: we are currently limited to ASCII range + params = params.map(v => (v > 127) ? 127 : v); + // FIXED: params = params.map(v => (v > 255) ? 0 : value); + return `\x1b[M${S(params[0])}${S(params[1])}${S(params[2])}`; + }, + /** + * UTF8 - CSI M Pb Px Py + * Same as DEFAULT, but with optional 2-byte UTF8 + * encoding for values > 223 (can encode up to 2015). + */ + UTF8: (e: ICoreMouseEvent) => { + let params = [eventCode(e, false) + 32, e.col + 32, e.row + 32]; + // limit to 2-byte UTF8 + params = params.map(v => (v > 2047) ? 0 : v); + return `\x1b[M${S(params[0])}${S(params[1])}${S(params[2])}`; + }, + /** + * SGR - CSI < Pb ; Px ; Py M|m + * No encoding limitation. + * Can report button on release and works with a well formed sequence. + */ + SGR: (e: ICoreMouseEvent) => { + const final = (e.action === 'up' && e.button !== 'wheel') ? 'm' : 'M'; + return `\x1b[<${eventCode(e, true)};${e.col};${e.row}${final}`; + }, + /** + * URXVT - CSI Pb ; Px ; Py M + * Same button encoding as default, decimal encoding for coords. + * Ambiguity with other sequences, should not be used. + */ + URXVT: (e: ICoreMouseEvent) => { + return `\x1b[${eventCode(e, false) + 32};${e.col};${e.row}M`; + } +}; + +/** + * CoreMouseService + * + * Provides mouse tracking reports with different protocols and encodings. + * - protocols: NONE (default), X10, VT200, DRAG, ANY + * - encodings: DEFAULT, SGR, UTF8, URXVT + * + * Custom protocols/encodings can be added by `addProtocol` / `addEncoding`. + * To activate a protocol/encoding, set `activeProtocol` / `activeEncoding`. + * Switching a protocol will send a notification event `onProtocolChange` + * with a list of needed events to track. + * + * The service handles the mouse tracking state and decides whether to send + * a tracking report to the backend based on protocol and encoding limitations. + * To send a mouse event call `triggerMouseEvent`. + */ +export class CoreMouseService implements ICoreMouseService { + private _protocols: {[name: string]: ICoreMouseProtocol} = {}; + private _encodings: {[name: string]: CoreMouseEncoding} = {}; + private _activeProtocol: string = ''; + private _activeEncoding: string = ''; + private _onProtocolChange = new EventEmitter(); + + constructor( + @IBufferService private readonly _bufferService: IBufferService, + @ICoreService private readonly _coreService: ICoreService + ) { + // register default protocols and encodings + Object.keys(DEFAULT_PROCOTOLS).forEach(name => this.addProtocol(name, DEFAULT_PROCOTOLS[name])); + Object.keys(DEFAULT_ENCODINGS).forEach(name => this.addEncoding(name, DEFAULT_ENCODINGS[name])); + // call reset to set defaults + this.reset(); + } + + public addProtocol(name: string, protocol: ICoreMouseProtocol): void { + this._protocols[name] = protocol; + } + + public addEncoding(name: string, encoding: CoreMouseEncoding): void { + this._encodings[name] = encoding; + } + + public get activeProtocol(): string { + return this._activeProtocol; + } + + public set activeProtocol(name: string) { + if (!this._protocols[name]) { + throw new Error(`unknown protocol "${name}"`); + } + this._activeProtocol = name; + this._onProtocolChange.fire(this._protocols[name].events); + } + + public get activeEncoding(): string { + return this._activeEncoding; + } + + public set activeEncoding(name: string) { + if (!this._encodings[name]) { + throw new Error(`unknown encoding "${name}"`); + } + this._activeEncoding = name; + } + + public reset(): void { + this.activeProtocol = 'NONE'; + this.activeEncoding = 'DEFAULT'; + } + + /** + * Event to announce changes in mouse tracking. + */ + public get onProtocolChange(): IEvent { + return this._onProtocolChange.event; + } + + /** + * 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 bowser component should be omitted. + * + * Note: The method will change values of the given event object + * to fullfill protocol and encoding restrictions. + */ + public triggerMouseEvent(event: ICoreMouseEvent): boolean { + // range check for col/row + if (event.col < 0 || event.col >= this._bufferService.cols + || event.row < 0 || event.row >= this._bufferService.rows) { + return false; + } + + // filter nonsense combinations of button + action + if (event.button === 'wheel' && event.action === 'move') { + return false; + } + if (event.button === 'none' && event.action !== 'move') { + return false; + } + + // report 1-based coords + event.col++; + event.row++; + + // apply protocol restrictions + if (!this._protocols[this._activeProtocol].restrict(event)) { + return false; + } + + // encode report and send + const report = this._encodings[this._activeEncoding](event); + this._coreService.triggerDataEvent(report, true); + + return true; + } +} diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts index 1af55e9a..8c3a7eb8 100644 --- a/src/common/services/Services.ts +++ b/src/common/services/Services.ts @@ -5,7 +5,7 @@ import { IEvent } from 'common/EventEmitter'; import { IBuffer, IBufferSet } from 'common/buffer/Types'; -import { IDecPrivateModes } from 'common/Types'; +import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEncoding, ICoreMouseProtocol, CoreMouseEventType } from 'common/Types'; import { createDecorator } from 'common/services/ServiceRegistry'; export const IBufferService = createDecorator('BufferService'); @@ -23,6 +23,32 @@ export interface IBufferService { reset(): void; } +export const ICoreMouseService = createDecorator('CoreMouseService'); +export interface ICoreMouseService { + activeProtocol: string; + activeEncoding: string; + addProtocol(name: string, protocol: ICoreMouseProtocol): void; + addEncoding(name: string, encoding: CoreMouseEncoding): void; + reset(): void; + + /** + * 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 bowser component should be omitted. + * + * Note: The method will change values of the given event object + * to fullfill protocol and encoding restrictions. + */ + triggerMouseEvent(event: ICoreMouseEvent): boolean; + + /** + * Event to announce changes in mouse tracking. + */ + onProtocolChange: IEvent; +} + export const ICoreService = createDecorator('CoreService'); export interface ICoreService { serviceBrand: any; From e38a68e3965fc08e8447fb6daca53d9a2e085500 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Fri, 19 Jul 2019 02:46:03 +0200 Subject: [PATCH 02/17] debounce events at grid level --- src/common/services/CoreMouseService.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/common/services/CoreMouseService.ts b/src/common/services/CoreMouseService.ts index 6549c002..36b562ad 100644 --- a/src/common/services/CoreMouseService.ts +++ b/src/common/services/CoreMouseService.ts @@ -196,6 +196,7 @@ export class CoreMouseService implements ICoreMouseService { private _activeProtocol: string = ''; private _activeEncoding: string = ''; private _onProtocolChange = new EventEmitter(); + private _lastEvent: ICoreMouseEvent | null = null; constructor( @IBufferService private readonly _bufferService: IBufferService, @@ -242,6 +243,7 @@ export class CoreMouseService implements ICoreMouseService { public reset(): void { this.activeProtocol = 'NONE'; this.activeEncoding = 'DEFAULT'; + this._lastEvent = null; } /** @@ -280,6 +282,11 @@ export class CoreMouseService implements ICoreMouseService { event.col++; event.row++; + // debounce at grid level + if (this._lastEvent && this._compareEvents(this._lastEvent, event)) { + return false; + } + // apply protocol restrictions if (!this._protocols[this._activeProtocol].restrict(event)) { return false; @@ -289,6 +296,19 @@ export class CoreMouseService implements ICoreMouseService { const report = this._encodings[this._activeEncoding](event); this._coreService.triggerDataEvent(report, true); + this._lastEvent = event; + + return true; + } + + private _compareEvents(e1: ICoreMouseEvent, e2: ICoreMouseEvent): boolean { + 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; } } From df0cf253acb0b6ffd35999796b252f5ceef93b1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Fri, 19 Jul 2019 02:56:24 +0200 Subject: [PATCH 03/17] debounce only move events --- src/common/services/CoreMouseService.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/common/services/CoreMouseService.ts b/src/common/services/CoreMouseService.ts index 36b562ad..cddc058d 100644 --- a/src/common/services/CoreMouseService.ts +++ b/src/common/services/CoreMouseService.ts @@ -282,8 +282,8 @@ export class CoreMouseService implements ICoreMouseService { event.col++; event.row++; - // debounce at grid level - if (this._lastEvent && this._compareEvents(this._lastEvent, event)) { + // debounce move at grid level + if (event.action === 'move' && this._lastEvent && this._compareEvents(this._lastEvent, event)) { return false; } 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 04/17] 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 } }; From b64ed4f540edf8e5eeb1df38297cdae4af6ee053 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Fri, 19 Jul 2019 17:04:12 +0200 Subject: [PATCH 05/17] cleanup sendEvent, switch reported button during move --- src/Terminal.ts | 58 ++++++++++++++++++++----------------------------- 1 file changed, 23 insertions(+), 35 deletions(-) diff --git a/src/Terminal.ts b/src/Terminal.ts index 0c72dad0..88712677 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -713,46 +713,41 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp // get mouse coordinates pos = self._mouseService.getRawByteCoords(ev, self.screenElement, self.cols, self.rows); - if (!pos) return; + if (!pos) { + return false; + } let but: ICoreMouseEvent['button']; let action: ICoreMouseEvent['action']; - let code: number; - // FIXME: cleanup the switch mess switch ((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'; + if (!ev.buttons) { but = 'none'; + // buttons is not supported on macOS, try to get a value from button instead + if (ev.button !== undefined) { + but = ev.button === 0 ? 'left' : ev.button === 1 ? 'middle' : ev.button === 2 ? 'right' : 'none'; + } + } else { + but = ev.buttons & 1 ? 'left' : ev.buttons & 4 ? 'middle' : ev.buttons & 2 ? 'right' : 'none'; } break; case 'mouseup': action = 'up'; - code = ev.button !== null && ev.button !== undefined - ? +ev.button - : ev.which !== null && ev.which !== undefined - ? ev.which - 1 - : null; - but = code === 0 ? 'left' : code === 1 ? 'middle' : 'right'; + but = ev.button === 0 ? 'left' : ev.button === 1 ? 'middle' : ev.button === 2 ? 'right' : 'none'; break; case 'mousedown': action = 'down'; - code = ev.button !== null && ev.button !== undefined - ? +ev.button - : ev.which !== null && ev.which !== undefined - ? ev.which - 1 - : null; - but = code === 0 ? 'left' : code === 1 ? 'middle' : 'right'; + but = ev.button === 0 ? 'left' : ev.button === 1 ? 'middle' : ev.button === 2 ? 'right' : 'none'; break; case 'DOMMouseScroll': - but = 'wheel'; action = ev.detail < 0 ? 'up' : 'down'; + but = 'wheel'; break; case 'wheel': - but = 'wheel'; action = (ev).deltaY < 0 ? 'up' : 'down'; + but = 'wheel'; break; } return self._coreMouseService.triggerMouseEvent({ @@ -804,7 +799,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp } }, mousemove: (ev: MouseEvent) => { - // deal only with move eithout any button + // deal only with move without any button if (!ev.buttons) { sendEvent(ev); } @@ -812,26 +807,19 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp }; this._coreMouseService.onProtocolChange(events => { // apply global changes on events + this.mouseEvents = 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(); - } + this.element.classList.add('enable-mouse-events'); + 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(); - } + this.element.classList.remove('enable-mouse-events'); + this._selectionService.enable(); } + // add/remove handlers from requestedEvents + if (events.indexOf('mousemove') === -1) { el.removeEventListener('mousemove', requestedEvents.mousemove); requestedEvents.mousemove = null; From f239e10ed67ca4b139239ead3a217ad5450d4952 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Fri, 19 Jul 2019 23:55:18 +0200 Subject: [PATCH 06/17] use enums instead --- src/Terminal.ts | 94 ++++++------- src/TestUtils.test.ts | 2 +- src/Types.d.ts | 2 +- src/common/TestUtils.test.ts | 2 +- src/common/Types.d.ts | 40 ++++-- src/common/services/CoreMouseService.test.ts | 139 ++++++++++--------- src/common/services/CoreMouseService.ts | 77 +++++----- src/common/services/Services.ts | 2 +- 8 files changed, 183 insertions(+), 175 deletions(-) diff --git a/src/Terminal.ts b/src/Terminal.ts index 88712677..230e7a92 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -40,7 +40,7 @@ import { AccessibilityManager } from './AccessibilityManager'; import { ITheme, IMarker, IDisposable, ISelectionPosition } from 'xterm'; import { removeTerminalFromCache } from './renderer/atlas/CharAtlasCache'; import { DomRenderer } from './renderer/dom/DomRenderer'; -import { IKeyboardEvent, KeyboardResultType, ICharset, IBufferLine, IAttributeData, ICoreMouseEvent, CoreMouseEventType } from 'common/Types'; +import { IKeyboardEvent, KeyboardResultType, ICharset, IBufferLine, IAttributeData, CoreMouseEventType, CoreMouseButton, CoreMouseAction } from 'common/Types'; import { evaluateKeyboardEvent } from 'common/input/Keyboard'; import { EventEmitter, IEvent } from 'common/EventEmitter'; import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; @@ -142,7 +142,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp public charsets: ICharset[]; // mouse properties - public mouseEvents: CoreMouseEventType[] = []; + public mouseEvents: CoreMouseEventType = CoreMouseEventType.NONE; public sendFocus: boolean; // misc @@ -650,8 +650,8 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this.linkifier.attachToDom(this.element, this._mouseZoneManager); // apply mouse event classes set by escape codes before terminal was attached - this.element.classList.toggle('enable-mouse-events', !!this.mouseEvents.length); - if (this.mouseEvents.length) { + this.element.classList.toggle('enable-mouse-events', !!this.mouseEvents); + if (this.mouseEvents) { this._selectionService.disable(); } else { this._selectionService.enable(); @@ -703,11 +703,12 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp } } - + // TODO: Move mouse event code into its own file. public bindMouse(): void { const self = this; const el = this.element; + // send event to CoreMouseService function sendEvent(ev: MouseEvent | WheelEvent): boolean { let pos; @@ -717,38 +718,43 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp return false; } - let but: ICoreMouseEvent['button']; - let action: ICoreMouseEvent['action']; + let but: CoreMouseButton; + let action: CoreMouseAction; switch ((ev).overrideType || ev.type) { case 'mousemove': - action = 'move'; - but = 'none'; - if (!ev.buttons) { - but = 'none'; + 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 === 0 ? 'left' : ev.button === 1 ? 'middle' : ev.button === 2 ? 'right' : 'none'; + but = ev.button < 3 ? ev.button : CoreMouseButton.NONE; } } else { - but = ev.buttons & 1 ? 'left' : ev.buttons & 4 ? 'middle' : ev.buttons & 2 ? 'right' : 'none'; + but = ev.buttons & 1 ? CoreMouseButton.LEFT : + ev.buttons & 4 ? CoreMouseButton.MIDDLE : + ev.buttons & 2 ? CoreMouseButton.RIGHT : + CoreMouseButton.NONE; } break; case 'mouseup': - action = 'up'; - but = ev.button === 0 ? 'left' : ev.button === 1 ? 'middle' : ev.button === 2 ? 'right' : 'none'; + action = CoreMouseAction.UP; + but = ev.button < 3 ? ev.button : CoreMouseButton.NONE; break; case 'mousedown': - action = 'down'; - but = ev.button === 0 ? 'left' : ev.button === 1 ? 'middle' : ev.button === 2 ? 'right' : 'none'; + action = CoreMouseAction.DOWN; + but = ev.button < 3 ? ev.button : CoreMouseButton.NONE; break; case 'DOMMouseScroll': - action = ev.detail < 0 ? 'up' : 'down'; - but = 'wheel'; + action = ev.detail < 0 ? CoreMouseAction.UP : CoreMouseAction.DOWN; + but = CoreMouseButton.WHEEL; break; case 'wheel': - action = (ev).deltaY < 0 ? 'up' : 'down'; - but = 'wheel'; + action = (ev).deltaY < 0 ? CoreMouseAction.UP : CoreMouseAction.DOWN; + but = CoreMouseButton.WHEEL; break; + default: + // dont handle other event types by accident + return false; } return self._coreMouseService.triggerMouseEvent({ col: pos.x - 33, // FIXME: why -33 here? @@ -762,11 +768,11 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp } /** - * Event handler state handling. + * Event listener 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 + * 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: {[key: string]: any} = { @@ -775,7 +781,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp mousedrag: null, mousemove: null }; - const eventHandlers: {[key: string]: any} = { + const eventListeners: {[key: string]: any} = { mouseup: (ev: MouseEvent) => { sendEvent(ev); if (!ev.buttons) { @@ -808,7 +814,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this._coreMouseService.onProtocolChange(events => { // apply global changes on events this.mouseEvents = events; - if (events.length) { + if (events) { this._logService.info('Binding to mouse events:', events); this.element.classList.add('enable-mouse-events'); this._selectionService.disable(); @@ -820,49 +826,43 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp // add/remove handlers from requestedEvents - if (events.indexOf('mousemove') === -1) { + if (!(events & CoreMouseEventType.MOVE)) { el.removeEventListener('mousemove', requestedEvents.mousemove); requestedEvents.mousemove = null; } else if (!requestedEvents.mousemove) { - el.addEventListener('mousemove', eventHandlers.mousemove); - requestedEvents.mousemove = eventHandlers.mousemove; + el.addEventListener('mousemove', eventListeners.mousemove); + requestedEvents.mousemove = eventListeners.mousemove; } - if (events.indexOf('wheel') === -1) { + if (!(events & CoreMouseEventType.WHEEL)) { el.removeEventListener('wheel', requestedEvents.wheel); requestedEvents.wheel = null; } else if (!requestedEvents.wheel) { - el.addEventListener('wheel', eventHandlers.wheel); - requestedEvents.wheel = eventHandlers.wheel; + el.addEventListener('wheel', eventListeners.wheel); + requestedEvents.wheel = eventListeners.wheel; } - if (events.indexOf('mouseup') === -1) { + if (!(events & CoreMouseEventType.UP)) { // always remove possible leftover handler this._document.removeEventListener('mouseup', requestedEvents.mouseup); requestedEvents.mouseup = null; } else if (!requestedEvents.mouseup) { - requestedEvents.mouseup = eventHandlers.mouseup; + requestedEvents.mouseup = eventListeners.mouseup; } - if (events.indexOf('mousedrag') === -1) { + if (!(events & CoreMouseEventType.DRAG)) { // always remove possible leftover handler this._document.removeEventListener('mousemove', requestedEvents.mousedrag); requestedEvents.mousedrag = null; } else if (!requestedEvents.mousedrag) { - requestedEvents.mousedrag = eventHandlers.mousedrag; + requestedEvents.mousedrag = eventListeners.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. + * "Always on" event listeners. */ this.register(addDisposableDomListener(el, 'mousedown', (ev: MouseEvent) => { ev.preventDefault(); @@ -871,7 +871,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp // 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.length || this._selectionService.shouldForceSelection(ev)) { + if (!this.mouseEvents || this._selectionService.shouldForceSelection(ev)) { return; } @@ -924,13 +924,13 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp })); this.register(addDisposableDomListener(el, 'touchstart', (ev: TouchEvent) => { - if (this.mouseEvents.length) return; + if (this.mouseEvents) return; this.viewport.onTouchStart(ev); return this.cancel(ev); })); this.register(addDisposableDomListener(el, 'touchmove', (ev: TouchEvent) => { - if (this.mouseEvents.length) return; + if (this.mouseEvents) return; this.viewport.onTouchMove(ev); return this.cancel(ev); })); diff --git a/src/TestUtils.test.ts b/src/TestUtils.test.ts index 5805ba3d..f719aa88 100644 --- a/src/TestUtils.test.ts +++ b/src/TestUtils.test.ts @@ -210,7 +210,7 @@ export class MockInputHandlingTerminal implements IInputHandlingTerminal { x10Mouse: boolean; vt200Mouse: boolean; normalMouse: boolean; - mouseEvents: CoreMouseEventType[]; + mouseEvents: CoreMouseEventType; sendFocus: boolean; utfMouse: boolean; sgrMouse: boolean; diff --git a/src/Types.d.ts b/src/Types.d.ts index 80e8848c..d74d29b9 100644 --- a/src/Types.d.ts +++ b/src/Types.d.ts @@ -36,7 +36,7 @@ export interface IInputHandlingTerminal { bracketedPasteMode: boolean; curAttrData: IAttributeData; savedCols: number; - mouseEvents: CoreMouseEventType[]; + mouseEvents: CoreMouseEventType; sendFocus: boolean; cursorHidden: boolean; diff --git a/src/common/TestUtils.test.ts b/src/common/TestUtils.test.ts index bbeb4de8..62b5402d 100644 --- a/src/common/TestUtils.test.ts +++ b/src/common/TestUtils.test.ts @@ -36,7 +36,7 @@ export class MockCoreMouseService implements ICoreMouseService { addProtocol(name: string): void {} reset(): void {} triggerMouseEvent(event: ICoreMouseEvent): boolean { return false; } - onProtocolChange: IEvent = new EventEmitter().event; + onProtocolChange: IEvent = new EventEmitter().event; } export class MockCoreService implements ICoreService { diff --git a/src/common/Types.d.ts b/src/common/Types.d.ts index 598a6a9e..7f86b7ec 100644 --- a/src/common/Types.d.ts +++ b/src/common/Types.d.ts @@ -162,6 +162,20 @@ export interface IRowRange { /** * Interface for mouse events in the core. */ +export const enum CoreMouseButton { + LEFT = 0, + MIDDLE = 1, + RIGHT = 2, + NONE = 3, + WHEEL = 64 +} + +export const enum CoreMouseAction { + UP = 0, + DOWN = 1, + MOVE = 32 +} + export interface ICoreMouseEvent { /** column (zero based). */ col: number; @@ -174,8 +188,8 @@ export interface ICoreMouseEvent { * There are invalid combinations of buttons and actions possible * (like move + wheel), those are silently ignored by the CoreMouseService. */ - button: 'left' | 'middle' | 'right' | 'wheel' | 'none'; - action: 'up' | 'down' | 'move'; + button: CoreMouseButton; + action: CoreMouseAction; /** * Modifier states. * Protocols will add/ignore those based on specific restrictions. @@ -190,14 +204,20 @@ export interface ICoreMouseEvent { * To be reported to the browser component which events a mouse * protocol wants to be catched and forwarded as an ICoreMouseEvent * to CoreMouseService. - * Known types: - * - mousedown: any mousedown event - * - mouseup: any mouseup event - * - wheel: any wheel event - * - mousedrag: any mousemove event while a button is pressed - * - mousemove: any mousemove event */ -export type CoreMouseEventType = 'mousedown' | 'mouseup' | 'wheel' | 'mousemove' | 'mousedrag'; +export const enum CoreMouseEventType { + NONE = 0, + /** any mousedown event */ + DOWN = 1, + /** any mouseup event */ + UP = 2, + /** any mousemove event while a button is held */ + DRAG = 4, + /** any mousemove event without a button */ + MOVE = 8, + /** any wheel event */ + WHEEL = 16 +} /** * Mouse protocol interface. @@ -208,7 +228,7 @@ export type CoreMouseEventType = 'mousedown' | 'mouseup' | 'wheel' | 'mousemove' * modifiers or filtering invalid event types. */ export interface ICoreMouseProtocol { - events: CoreMouseEventType[]; + events: CoreMouseEventType; restrict: (e: ICoreMouseEvent) => boolean; } diff --git a/src/common/services/CoreMouseService.test.ts b/src/common/services/CoreMouseService.test.ts index 4f4392f1..63f39f5d 100644 --- a/src/common/services/CoreMouseService.test.ts +++ b/src/common/services/CoreMouseService.test.ts @@ -5,7 +5,7 @@ import { CoreMouseService } from 'common/services/CoreMouseService'; import { MockCoreService, MockBufferService } from 'common/TestUtils.test'; import { assert } from 'chai'; -import { ICoreMouseEvent, CoreMouseEventType } from 'common/Types'; +import { ICoreMouseEvent, CoreMouseEventType, CoreMouseButton, CoreMouseAction } from 'common/Types'; // needed mock services const bufferService = new MockBufferService(300, 100); @@ -56,18 +56,21 @@ describe('CoreMouseService', () => { }); it('addProtocol', () => { const cms = new CoreMouseService(bufferService, coreService); - cms.addProtocol('XYZ', { events: [], restrict: (e: ICoreMouseEvent) => false }); + cms.addProtocol('XYZ', { events: CoreMouseEventType.NONE, restrict: (e: ICoreMouseEvent) => false }); cms.activeProtocol = 'XYZ'; assert.equal(cms.activeProtocol, 'XYZ'); }); it('onProtocolChange', () => { const cms = new CoreMouseService(bufferService, coreService); - const wantedEvents: CoreMouseEventType[][] = []; + const wantedEvents: CoreMouseEventType[] = []; cms.onProtocolChange(events => wantedEvents.push(events)); cms.activeProtocol = 'NONE'; - assert.deepEqual(wantedEvents, [[]]); + assert.deepEqual(wantedEvents, [CoreMouseEventType.NONE]); cms.activeProtocol = 'ANY'; - assert.deepEqual(wantedEvents, [[], ['mousedown', 'mouseup', 'wheel', 'mousedrag', 'mousemove']]); + assert.deepEqual(wantedEvents, [ + CoreMouseEventType.NONE, + CoreMouseEventType.DOWN | CoreMouseEventType.UP | CoreMouseEventType.WHEEL | CoreMouseEventType.DRAG | CoreMouseEventType.MOVE + ]); }); describe('triggerMouseEvent', () => { let cms: CoreMouseService; @@ -78,68 +81,68 @@ describe('CoreMouseService', () => { coreService.triggerDataEvent = (data: string, userInput?: boolean) => reports.push(data); }); it('NONE', () => { - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'left', action: 'down' }), false); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'left', action: 'up' }), false); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'left', action: 'move' }), false); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'middle', action: 'down' }), false); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'right', action: 'down' }), false); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'wheel', action: 'up' }), false); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'none', action: 'move' }), false); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), false); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.UP }), false); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.MOVE }), false); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.DOWN }), false); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.DOWN }), false); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.UP }), false); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE }), false); }); it('X10', () => { cms.activeProtocol = 'X10'; - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'left', action: 'down' }), true); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'left', action: 'up' }), false); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'left', action: 'move' }), false); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'middle', action: 'down' }), true); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'right', action: 'down' }), true); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'wheel', action: 'up' }), false); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'none', action: 'move' }), false); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.UP }), false); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.MOVE }), false); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.DOWN }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.DOWN }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.UP }), false); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE }), false); }); it('VT200', () => { cms.activeProtocol = 'VT200'; - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'left', action: 'down' }), true); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'left', action: 'up' }), true); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'left', action: 'move' }), false); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'middle', action: 'down' }), true); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'right', action: 'down' }), true); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'wheel', action: 'up' }), true); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'none', action: 'move' }), false); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.UP }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.MOVE }), false); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.DOWN }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.DOWN }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.UP }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE }), false); }); it('DRAG', () => { cms.activeProtocol = 'DRAG'; - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'left', action: 'down' }), true); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'left', action: 'up' }), true); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'left', action: 'move' }), true); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'middle', action: 'down' }), true); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'right', action: 'down' }), true); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'wheel', action: 'up' }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.UP }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.MOVE }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.DOWN }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.DOWN }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.UP }), true); }); it('ANY', () => { cms.activeProtocol = 'ANY'; - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'left', action: 'down' }), true); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'left', action: 'up' }), true); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'left', action: 'move' }), true); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'middle', action: 'down' }), true); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'right', action: 'down' }), true); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'wheel', action: 'up' }), true); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'none', action: 'move' }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.UP }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.MOVE }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.DOWN }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.DOWN }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.UP }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE }), true); // should not report in any case // invalid button + action combinations - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'wheel', action: 'move' }), false); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'none', action: 'down' }), false); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'none', action: 'up' }), false); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.MOVE }), false); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.DOWN }), false); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.UP }), false); // invalid coords - assert.equal(cms.triggerMouseEvent({ col: -1, row: 0, button: 'left', action: 'down' }), false); - assert.equal(cms.triggerMouseEvent({ col: 500, row: 0, button: 'left', action: 'down' }), false); - assert.equal(cms.triggerMouseEvent({ col: 0, row: -1, button: 'left', action: 'down' }), false); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 500, button: 'left', action: 'down' }), false); + assert.equal(cms.triggerMouseEvent({ col: -1, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), false); + assert.equal(cms.triggerMouseEvent({ col: 500, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), false); + assert.equal(cms.triggerMouseEvent({ col: 0, row: -1, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), false); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 500, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), false); }); describe('coords', () => { it('DEFAULT encoding', () => { cms.activeProtocol = 'ANY'; for (let i = 0; i < bufferService.cols; ++i) { - assert.equal(cms.triggerMouseEvent({ col: i, row: 0, button: 'left', action: 'down' }), true); + assert.equal(cms.triggerMouseEvent({ col: i, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true); // capped at 95 if (i < 95) { assert.deepEqual(toBytes(reports.pop()), [0x1b, 0x5b, 0x4d, 0x20, i + 33, 0x21]); @@ -152,7 +155,7 @@ describe('CoreMouseService', () => { cms.activeProtocol = 'ANY'; cms.activeEncoding = 'UTF8'; for (let i = 0; i < bufferService.cols; ++i) { - assert.equal(cms.triggerMouseEvent({ col: i, row: 0, button: 'left', action: 'down' }), true); + assert.equal(cms.triggerMouseEvent({ col: i, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true); assert.deepEqual(toBytes(reports.pop()), [0x1b, 0x5b, 0x4d, 0x20, i + 33, 0x21]); } }); @@ -160,7 +163,7 @@ describe('CoreMouseService', () => { cms.activeProtocol = 'ANY'; cms.activeEncoding = 'SGR'; for (let i = 0; i < bufferService.cols; ++i) { - assert.equal(cms.triggerMouseEvent({ col: i, row: 0, button: 'left', action: 'down' }), true); + assert.equal(cms.triggerMouseEvent({ col: i, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true); assert.deepEqual(reports.pop(), `\x1b[<0;${i + 1};1M`); } }); @@ -168,7 +171,7 @@ describe('CoreMouseService', () => { cms.activeProtocol = 'ANY'; cms.activeEncoding = 'URXVT'; for (let i = 0; i < bufferService.cols; ++i) { - assert.equal(cms.triggerMouseEvent({ col: i, row: 0, button: 'left', action: 'down' }), true); + assert.equal(cms.triggerMouseEvent({ col: i, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true); assert.deepEqual(reports.pop(), `\x1b[32;${i + 1};1M`); } }); @@ -177,36 +180,36 @@ describe('CoreMouseService', () => { cms.activeProtocol = 'ANY'; cms.activeEncoding = 'DEFAULT'; // all buttons + down + no modifer - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'left', action: 'down', ctrl: false, alt: false, shift: false }), true); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'middle', action: 'down', ctrl: false, alt: false, shift: false }), true); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'right', action: 'down', ctrl: false, alt: false, shift: false }), true); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'wheel', action: 'down', ctrl: false, alt: false, shift: false }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN, ctrl: false, alt: false, shift: false }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.DOWN, ctrl: false, alt: false, shift: false }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.DOWN, ctrl: false, alt: false, shift: false }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.DOWN, ctrl: false, alt: false, shift: false }), true); assert.deepEqual(reports, ['\x1b[M !!', '\x1b[M!!!', '\x1b[M"!!', '\x1b[Ma!!']); while (reports.pop()) { } // all buttons + up + no modifier - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'left', action: 'up', ctrl: false, alt: false, shift: false }), true); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'middle', action: 'up', ctrl: false, alt: false, shift: false }), true); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'right', action: 'up', ctrl: false, alt: false, shift: false }), true); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'wheel', action: 'up', ctrl: false, alt: false, shift: false }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.UP, ctrl: false, alt: false, shift: false }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.UP, ctrl: false, alt: false, shift: false }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.UP, ctrl: false, alt: false, shift: false }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.UP, ctrl: false, alt: false, shift: false }), true); assert.deepEqual(reports, ['\x1b[M#!!', '\x1b[M#!!', '\x1b[M#!!', '\x1b[M`!!']); while (reports.pop()) { } // all buttons + move + no modifier - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'left', action: 'move', ctrl: false, alt: false, shift: false }), true); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'middle', action: 'move', ctrl: false, alt: false, shift: false }), true); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'right', action: 'move', ctrl: false, alt: false, shift: false }), true); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'none', action: 'move', ctrl: false, alt: false, shift: false }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.MOVE, ctrl: false, alt: false, shift: false }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.MOVE, ctrl: false, alt: false, shift: false }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.MOVE, ctrl: false, alt: false, shift: false }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: false, alt: false, shift: false }), true); assert.deepEqual(reports, ['\x1b[M@!!', '\x1b[MA!!', '\x1b[MB!!', '\x1b[MC!!']); while (reports.pop()) { } // button none + move + modifiers - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'none', action: 'move', ctrl: true, alt: false, shift: false }), true); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'none', action: 'move', ctrl: false, alt: true, shift: false }), true); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'none', action: 'move', ctrl: false, alt: false, shift: true }), true); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'none', action: 'move', ctrl: true, alt: true, shift: false }), true); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'none', action: 'move', ctrl: false, alt: true, shift: true }), true); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'none', action: 'move', ctrl: true, alt: true, shift: true }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: true, alt: false, shift: false }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: false, alt: true, shift: false }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: false, alt: false, shift: true }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: true, alt: true, shift: false }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: false, alt: true, shift: true }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: true, alt: true, shift: true }), true); assert.deepEqual(reports, ['\x1b[MS!!', '\x1b[MK!!', '\x1b[MG!!', '\x1b[M[!!', '\x1b[MO!!', '\x1b[M_!!']); while (reports.pop()) { } }); diff --git a/src/common/services/CoreMouseService.ts b/src/common/services/CoreMouseService.ts index bafb9b12..22a56553 100644 --- a/src/common/services/CoreMouseService.ts +++ b/src/common/services/CoreMouseService.ts @@ -4,19 +4,19 @@ */ import { IBufferService, ICoreService, ICoreMouseService } from 'common/services/Services'; import { EventEmitter, IEvent } from 'common/EventEmitter'; -import { ICoreMouseProtocol, ICoreMouseEvent, CoreMouseEncoding, CoreMouseEventType } from 'common/Types'; +import { ICoreMouseProtocol, ICoreMouseEvent, CoreMouseEncoding, CoreMouseEventType, CoreMouseButton, CoreMouseAction } from 'common/Types'; /** * Supported default protocols. */ -const DEFAULT_PROCOTOLS: {[key: string]: ICoreMouseProtocol} = { +const DEFAULT_PROTOCOLS: {[key: string]: ICoreMouseProtocol} = { /** * NONE * Events: none * Modifiers: none */ NONE: { - events: [], + events: CoreMouseEventType.NONE, restrict: () => false }, /** @@ -25,10 +25,10 @@ const DEFAULT_PROCOTOLS: {[key: string]: ICoreMouseProtocol} = { * Modifiers: none (TBD) */ X10: { - events: ['mousedown'], + events: CoreMouseEventType.DOWN, restrict: (e: ICoreMouseEvent) => { // no wheel (TBD), no move, no up - if (e.button === 'wheel' || e.action !== 'down') { + if (e.button === CoreMouseButton.WHEEL || e.action !== CoreMouseAction.DOWN) { return false; } // no modifiers (TDB) @@ -44,10 +44,10 @@ const DEFAULT_PROCOTOLS: {[key: string]: ICoreMouseProtocol} = { * Modifiers: CTRL (TBD) */ VT200: { - events: ['mousedown', 'mouseup', 'wheel'], + events: CoreMouseEventType.DOWN | CoreMouseEventType.UP | CoreMouseEventType.WHEEL, restrict: (e: ICoreMouseEvent) => { // no move - if (e.action === 'move') { + if (e.action === CoreMouseAction.MOVE) { return false; } // modifiers - only ctrl? @@ -62,10 +62,10 @@ const DEFAULT_PROCOTOLS: {[key: string]: ICoreMouseProtocol} = { * Modifiers: CTRL | ALT | SHIFT */ DRAG: { - events: ['mousedown', 'mouseup', 'wheel', 'mousedrag'], + events: CoreMouseEventType.DOWN | CoreMouseEventType.UP | CoreMouseEventType.WHEEL | CoreMouseEventType.DRAG, restrict: (e: ICoreMouseEvent) => { // no move without button - if (e.action === 'move' && e.button === 'none') { + if (e.action === CoreMouseAction.MOVE && e.button === CoreMouseButton.NONE) { return false; } // modifiers unclear - let all pass for now @@ -78,47 +78,32 @@ const DEFAULT_PROCOTOLS: {[key: string]: ICoreMouseProtocol} = { * Modifiers: CTRL | ALT | SHIFT */ ANY: { - events: ['mousedown', 'mouseup', 'wheel', 'mousedrag', 'mousemove'], + events: + CoreMouseEventType.DOWN | CoreMouseEventType.UP | CoreMouseEventType.WHEEL + | CoreMouseEventType.DRAG | CoreMouseEventType.MOVE, restrict: (e: ICoreMouseEvent) => true } }; -/** - * Mapping of buttons and actions to event codes. (taken from xterm spec) - * More than 3 buttons are not supported. - */ -enum CODEMAP { - // buttons - left = 0, - middle = 1, - right = 2, - none = 3, - wheel = 64, - // actions - up = 0, - down = 1, - move = 32, - // modifiers - shift = 4, - alt = 8, - ctrl = 16 +const enum Modifiers { + SHIFT = 4, + ALT = 8, + CTRL = 16 } // helper for default encoders to generate the event code. function eventCode(e: ICoreMouseEvent, isSGR: boolean): number { - const button = CODEMAP[e.button]; - const action = CODEMAP[e.action]; - const modifier = (e.ctrl ? CODEMAP.ctrl : 0) | (e.shift ? CODEMAP.shift : 0) | (e.alt ? CODEMAP.alt : 0); - let code = button | modifier; - if (e.button === 'wheel') { - code |= action; + const modifier = (e.ctrl ? Modifiers.CTRL : 0) | (e.shift ? Modifiers.SHIFT : 0) | (e.alt ? Modifiers.ALT : 0); + let code = e.button | modifier; + if (e.button === CoreMouseButton.WHEEL) { + code |= e.action; } else { - if (e.action === 'move') { - code |= CODEMAP.move; - } else if (e.action === 'up' && !isSGR) { + if (e.action === CoreMouseAction.MOVE) { + code |= CoreMouseAction.MOVE; + } else if (e.action === CoreMouseAction.UP && !isSGR) { // special case - only SGR can report button on release // all others have to go with NONE - code |= CODEMAP.none; + code |= CoreMouseButton.NONE; } } return code; @@ -161,7 +146,7 @@ const DEFAULT_ENCODINGS: {[key: string]: CoreMouseEncoding} = { * Can report button on release and works with a well formed sequence. */ SGR: (e: ICoreMouseEvent) => { - const final = (e.action === 'up' && e.button !== 'wheel') ? 'm' : 'M'; + const final = (e.action === CoreMouseAction.UP && e.button !== CoreMouseButton.WHEEL) ? 'm' : 'M'; return `\x1b[<${eventCode(e, true)};${e.col};${e.row}${final}`; }, /** @@ -195,7 +180,7 @@ export class CoreMouseService implements ICoreMouseService { private _encodings: {[name: string]: CoreMouseEncoding} = {}; private _activeProtocol: string = ''; private _activeEncoding: string = ''; - private _onProtocolChange = new EventEmitter(); + private _onProtocolChange = new EventEmitter(); private _lastEvent: ICoreMouseEvent | null = null; constructor( @@ -203,7 +188,7 @@ export class CoreMouseService implements ICoreMouseService { @ICoreService private readonly _coreService: ICoreService ) { // register default protocols and encodings - Object.keys(DEFAULT_PROCOTOLS).forEach(name => this.addProtocol(name, DEFAULT_PROCOTOLS[name])); + Object.keys(DEFAULT_PROTOCOLS).forEach(name => this.addProtocol(name, DEFAULT_PROTOCOLS[name])); Object.keys(DEFAULT_ENCODINGS).forEach(name => this.addEncoding(name, DEFAULT_ENCODINGS[name])); // call reset to set defaults this.reset(); @@ -249,7 +234,7 @@ export class CoreMouseService implements ICoreMouseService { /** * Event to announce changes in mouse tracking. */ - public get onProtocolChange(): IEvent { + public get onProtocolChange(): IEvent { return this._onProtocolChange.event; } @@ -271,10 +256,10 @@ export class CoreMouseService implements ICoreMouseService { } // filter nonsense combinations of button + action - if (event.button === 'wheel' && event.action === 'move') { + if (event.button === CoreMouseButton.WHEEL && event.action === CoreMouseAction.MOVE) { return false; } - if (event.button === 'none' && event.action !== 'move') { + if (event.button === CoreMouseButton.NONE && event.action !== CoreMouseAction.MOVE) { return false; } @@ -283,7 +268,7 @@ export class CoreMouseService implements ICoreMouseService { event.row++; // debounce move at grid level - if (event.action === 'move' && this._lastEvent && this._compareEvents(this._lastEvent, event)) { + if (event.action === CoreMouseAction.MOVE && this._lastEvent && this._compareEvents(this._lastEvent, event)) { return false; } diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts index 8c3a7eb8..61414656 100644 --- a/src/common/services/Services.ts +++ b/src/common/services/Services.ts @@ -46,7 +46,7 @@ export interface ICoreMouseService { /** * Event to announce changes in mouse tracking. */ - onProtocolChange: IEvent; + onProtocolChange: IEvent; } export const ICoreService = createDecorator('CoreService'); From 3e5b075a0ac846cc2a35a414d03bc6de95f522ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Mon, 22 Jul 2019 16:32:24 +0200 Subject: [PATCH 07/17] remove modifier restriction for 1000 --- src/common/services/CoreMouseService.ts | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/common/services/CoreMouseService.ts b/src/common/services/CoreMouseService.ts index 22a56553..659a80ff 100644 --- a/src/common/services/CoreMouseService.ts +++ b/src/common/services/CoreMouseService.ts @@ -22,16 +22,16 @@ const DEFAULT_PROTOCOLS: {[key: string]: ICoreMouseProtocol} = { /** * X10 * Events: mousedown - * Modifiers: none (TBD) + * Modifiers: none */ X10: { events: CoreMouseEventType.DOWN, restrict: (e: ICoreMouseEvent) => { - // no wheel (TBD), no move, no up + // no wheel, no move, no up if (e.button === CoreMouseButton.WHEEL || e.action !== CoreMouseAction.DOWN) { return false; } - // no modifiers (TDB) + // no modifiers e.ctrl = false; e.alt = false; e.shift = false; @@ -41,7 +41,7 @@ const DEFAULT_PROTOCOLS: {[key: string]: ICoreMouseProtocol} = { /** * VT200 * Events: mousedown / mouseup / wheel - * Modifiers: CTRL (TBD) + * Modifiers: all */ VT200: { events: CoreMouseEventType.DOWN | CoreMouseEventType.UP | CoreMouseEventType.WHEEL, @@ -50,16 +50,13 @@ const DEFAULT_PROTOCOLS: {[key: string]: ICoreMouseProtocol} = { if (e.action === CoreMouseAction.MOVE) { return false; } - // modifiers - only ctrl? - e.alt = false; - e.shift = false; return true; } }, /** * DRAG * Events: mousedown / mouseup / wheel / mousedrag - * Modifiers: CTRL | ALT | SHIFT + * Modifiers: all */ DRAG: { events: CoreMouseEventType.DOWN | CoreMouseEventType.UP | CoreMouseEventType.WHEEL | CoreMouseEventType.DRAG, @@ -68,14 +65,13 @@ const DEFAULT_PROTOCOLS: {[key: string]: ICoreMouseProtocol} = { if (e.action === CoreMouseAction.MOVE && e.button === CoreMouseButton.NONE) { return false; } - // modifiers unclear - let all pass for now return true; } }, /** * ANY * Events: all mouse related events - * Modifiers: CTRL | ALT | SHIFT + * Modifiers: all */ ANY: { events: From 776c6dffea382bdace43919faf8fc8707e4ec826 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Tue, 23 Jul 2019 20:16:42 +0200 Subject: [PATCH 08/17] refactor buttons, wheel with left right --- src/Terminal.ts | 27 ++++++++++----- src/common/Types.d.ts | 20 ++++++++--- src/common/services/CoreMouseService.test.ts | 1 + src/common/services/CoreMouseService.ts | 36 +++++++++++++------- 4 files changed, 58 insertions(+), 26 deletions(-) diff --git a/src/Terminal.ts b/src/Terminal.ts index e19a4516..92d904e9 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -734,35 +734,44 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp // 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; + but = ev.button < 3 ? ev.button : undefined; } } else { but = ev.buttons & 1 ? CoreMouseButton.LEFT : ev.buttons & 4 ? CoreMouseButton.MIDDLE : ev.buttons & 2 ? CoreMouseButton.RIGHT : - CoreMouseButton.NONE; + CoreMouseButton.NONE; // fallback to NONE } break; case 'mouseup': action = CoreMouseAction.UP; - but = ev.button < 3 ? ev.button : CoreMouseButton.NONE; + // AUX buttons are currently not supported + but = ev.button < 3 ? ev.button : undefined; break; case 'mousedown': action = CoreMouseAction.DOWN; - but = ev.button < 3 ? ev.button : CoreMouseButton.NONE; - break; - case 'DOMMouseScroll': - action = ev.detail < 0 ? CoreMouseAction.UP : CoreMouseAction.DOWN; - but = CoreMouseButton.WHEEL; + but = ev.button < 3 ? ev.button : undefined; break; case 'wheel': - action = (ev).deltaY < 0 ? CoreMouseAction.UP : CoreMouseAction.DOWN; + // precedence: up/down over left/right + // FIXME: Can we have both in one event? Need to send another wheel event? + if ((ev as WheelEvent).deltaY !== 0) { + action = (ev as WheelEvent).deltaY < 0 ? CoreMouseAction.UP : CoreMouseAction.DOWN; + } else if ((ev as WheelEvent).deltaX !== 0) { + action = (ev as WheelEvent).deltaX < 0 ? CoreMouseAction.LEFT : CoreMouseAction.RIGHT; + } but = CoreMouseButton.WHEEL; break; default: // dont handle other event types by accident return false; } + + // exit if we cannot determine button/action values + if (action === undefined || but === undefined) { + return false; + } + return self._coreMouseService.triggerMouseEvent({ col: pos.x - 33, // FIXME: why -33 here? row: pos.y - 33, diff --git a/src/common/Types.d.ts b/src/common/Types.d.ts index 7f86b7ec..2f742038 100644 --- a/src/common/Types.d.ts +++ b/src/common/Types.d.ts @@ -167,13 +167,25 @@ export const enum CoreMouseButton { MIDDLE = 1, RIGHT = 2, NONE = 3, - WHEEL = 64 + WHEEL = 4, + // additional buttons 1..8 + // untested! + AUX1 = 8, + AUX2 = 9, + AUX3 = 10, + AUX4 = 11, + AUX5 = 12, + AUX6 = 13, + AUX7 = 14, + AUX8 = 15 } export const enum CoreMouseAction { - UP = 0, - DOWN = 1, - MOVE = 32 + UP = 0, // buttons, wheel + DOWN = 1, // buttons, wheel + LEFT = 2, // wheel only + RIGHT = 3, // wheel only + MOVE = 32 // buttons only } export interface ICoreMouseEvent { diff --git a/src/common/services/CoreMouseService.test.ts b/src/common/services/CoreMouseService.test.ts index 63f39f5d..2063a496 100644 --- a/src/common/services/CoreMouseService.test.ts +++ b/src/common/services/CoreMouseService.test.ts @@ -177,6 +177,7 @@ describe('CoreMouseService', () => { }); }); it('eventCodes with modifiers (DEFAULT encoding)', () => { + // TODO: implement AUX button tests cms.activeProtocol = 'ANY'; cms.activeEncoding = 'DEFAULT'; // all buttons + down + no modifer diff --git a/src/common/services/CoreMouseService.ts b/src/common/services/CoreMouseService.ts index 659a80ff..957537e4 100644 --- a/src/common/services/CoreMouseService.ts +++ b/src/common/services/CoreMouseService.ts @@ -89,11 +89,18 @@ const enum Modifiers { // helper for default encoders to generate the event code. function eventCode(e: ICoreMouseEvent, isSGR: boolean): number { - const modifier = (e.ctrl ? Modifiers.CTRL : 0) | (e.shift ? Modifiers.SHIFT : 0) | (e.alt ? Modifiers.ALT : 0); - let code = e.button | modifier; + let code = (e.ctrl ? Modifiers.CTRL : 0) | (e.shift ? Modifiers.SHIFT : 0) | (e.alt ? Modifiers.ALT : 0); if (e.button === CoreMouseButton.WHEEL) { + code |= 64; code |= e.action; } else { + code |= e.button & 3; + if (e.button & 4) { + code |= 64; + } + if (e.button & 8) { + code |= 128; + } if (e.action === CoreMouseAction.MOVE) { code |= CoreMouseAction.MOVE; } else if (e.action === CoreMouseAction.UP && !isSGR) { @@ -244,40 +251,43 @@ export class CoreMouseService implements ICoreMouseService { * Note: The method will change values of the given event object * to fullfill protocol and encoding restrictions. */ - public triggerMouseEvent(event: ICoreMouseEvent): boolean { + public triggerMouseEvent(e: ICoreMouseEvent): boolean { // range check for col/row - if (event.col < 0 || event.col >= this._bufferService.cols - || event.row < 0 || event.row >= this._bufferService.rows) { + 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 (event.button === CoreMouseButton.WHEEL && event.action === CoreMouseAction.MOVE) { + if (e.button === CoreMouseButton.WHEEL && e.action === CoreMouseAction.MOVE) { return false; } - if (event.button === CoreMouseButton.NONE && event.action !== CoreMouseAction.MOVE) { + 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 - event.col++; - event.row++; + e.col++; + e.row++; // debounce move at grid level - if (event.action === CoreMouseAction.MOVE && this._lastEvent && this._compareEvents(this._lastEvent, event)) { + if (e.action === CoreMouseAction.MOVE && this._lastEvent && this._compareEvents(this._lastEvent, e)) { return false; } // apply protocol restrictions - if (!this._protocols[this._activeProtocol].restrict(event)) { + if (!this._protocols[this._activeProtocol].restrict(e)) { return false; } // encode report and send - const report = this._encodings[this._activeEncoding](event); + const report = this._encodings[this._activeEncoding](e); this._coreService.triggerDataEvent(report, true); - this._lastEvent = event; + this._lastEvent = e; return true; } From 4fba080d34dd7dc89293ec3c8e90147ee6e207c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Wed, 24 Jul 2019 15:14:37 +0200 Subject: [PATCH 09/17] deactivate wheel left/right, docs --- bin/test_mousemodes.js | 91 ++++++++++++++++++++++++++---------------- src/Terminal.ts | 34 +++++++++++----- 2 files changed, 80 insertions(+), 45 deletions(-) diff --git a/bin/test_mousemodes.js b/bin/test_mousemodes.js index 12ee8a0c..976a38e0 100644 --- a/bin/test_mousemodes.js +++ b/bin/test_mousemodes.js @@ -48,43 +48,66 @@ stdin.addListener('data', function(data) { } }); -// basic button codes (modifier keys are added on top) -const BUTTONS = { - 0: ['left', 'press'], - 1: ['middle', 'press'], - 2: ['right', 'press'], - 3: ['', 'release'], - 32: ['left', 'move'], - 33: ['middle', 'move'], - 34: ['right', 'move'], - 35: ['', 'move'], - 64: ['wheel', 'up'], - 65: ['wheel', 'down'] +// button definitions +const buttons = { + '': -1, + left: 0, + middle: 1, + right: 2, + released: 3, + wheelUp: 4, + wheelDown: 5, + wheelLeft: 6, + wheelRight: 7, + aux1: 8, + aux2: 9, + aux3: 10, + aux4: 11, + aux5: 12, + aux6: 13, + aux7: 14, + aux8: 15 }; +const reverseButtons = {}; +for (const el in buttons) { + reverseButtons[buttons[el]] = el; +} +// extract button data from buttonCode function evalButtonCode(code) { - // 2 bits: 0 - left, 1 - middle, 2 - right, 3 - release - // higher bits: 4 - shift, 8 - meta, 16 - control - const modifier = {shift: !!(code & 4), meta: !!(code & 8), control: !!(code & 16)}; - const wheel = code & 64; - let action; - let button; - if (wheel) { - action = (code & 1) ? 'down' : 'up'; - button = 'wheel'; - } else { - action = code & 32 ? 'move' : code === 3 ? 'release' : 'press'; - code &= 3; // TODO: more than 3 buttons + wheel - button = code === 0 ? 'left' : code === 1 ? 'middle' : code === 2 ? 'right' : ''; + // more than 15 buttons are not supported + if (code > 255) { + return {button: 'invalid', action: 'invalid', modifier: {}}; } - return {button, action, modifier}; + const modifier = {shift: !!(code & 4), meta: !!(code & 8), control: !!(code & 16)}; + const move = code & 32; + let button = code & 3; + if (code & 128) { + button |= 8; + } + if (code & 64) { + button |= 4 + } + let actionS = 'press'; + let buttonS = reverseButtons[button]; + if (button === 3) { + buttonS = ''; + actionS = 'release'; + } + if (move) { + actionS = 'move'; + } else if (4 <= button && button <= 7) { + buttonS = 'wheel'; + actionS = button === 4 ? 'up' : button === 5 ? 'down' : button === 6 ? 'left' : 'right'; + } + return {button: buttonS, action: actionS, modifier}; } // protocols const PROTOCOLS = { '9 (X10: press only)': '\x1b[?9h', '1000 (VT200: press, release, wheel)': '\x1b[?1000h', - // '1001 (VT200 highlight)': '\x1b[?1001h', // handle of backreport not implemented + // '1001 (VT200 highlight)': '\x1b[?1001h', // handle of backreport - not implemented '1002 (press, release, move on pressed, wheel)': '\x1b[?1002h', '1003 (press, relase, move, wheel)': '\x1b[?1003h' } @@ -96,8 +119,8 @@ const ENC = { // format: CSI M