Merge pull request #2323 from jerch/mouse_services

CoreMouseService
This commit is contained in:
jerch
2019-09-05 21:08:48 +02:00
committed by GitHub
11 changed files with 1749 additions and 688 deletions
+10 -10
View File
@@ -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());
inputHandler.setCursorStyle(Params.fromArray([0]));
assert.equal(optionsService.options['cursorStyle'], 'block');
@@ -93,7 +93,7 @@ describe('InputHandler', () => {
it('should toggle Terminal.bracketedPasteMode', () => {
const terminal = new MockInputHandlingTerminal();
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.setModePrivate(Params.fromArray([2004]));
assert.equal(terminal.bracketedPasteMode, true);
@@ -112,7 +112,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'));
@@ -150,7 +150,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'));
@@ -191,7 +191,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'));
@@ -220,7 +220,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'));
@@ -355,7 +355,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);
@@ -370,7 +370,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');
+16 -48
View File
@@ -19,8 +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 { ISelectionService } from 'browser/services/Services';
import { ICoreService, IBufferService, IOptionsService, ILogService, IDirtyRowService, ICoreMouseService } from 'common/services/Services';
import { OscHandler } from 'common/parser/OscParser';
import { DcsHandler } from 'common/parser/DcsParser';
@@ -124,8 +123,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<void>();
public get onCursorMove(): IEvent<void> { return this._onCursorMove.event; }
private _onLineFeed = new EventEmitter<void>();
@@ -140,6 +137,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();
@@ -326,11 +324,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;
@@ -1322,27 +1315,19 @@ 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._terminal.x10Mouse = params.params[i] === 9;
this._terminal.vt200Mouse = params.params[i] === 1000;
this._terminal.normalMouse = params.params[i] > 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.debug('Binding to mouse events.');
this._coreMouseService.activeProtocol = 'ANY';
break;
case 1004: // send focusin/focusout events
// focusin: ^[[I
@@ -1350,23 +1335,15 @@ export class InputHandler extends Disposable implements IInputHandler {
this._terminal.sendFocus = true;
break;
case 1005: // utf8 ext mode mouse
this._terminal.utfMouse = true;
// for wide terminals
// simply encodes large values as utf8 characters
this._coreMouseService.activeEncoding = 'UTF8';
break;
case 1006: // sgr ext mode mouse
this._terminal.sgrMouse = true;
// for wide terminals
// does not add 32 to fields
// press: ^[[<b;x;yM
// release: ^[[<b;x;ym
this._coreMouseService.activeEncoding = 'SGR';
break;
case 1015: // urxvt ext mode mouse
this._terminal.urxvtMouse = true;
// for wide terminals
// numbers for fields
// press: ^[[b;x;yM
// motion: ^[[b;x;yT
this._coreMouseService.activeEncoding = 'URXVT';
break;
case 25: // show cursor
this._terminal.cursorHidden = false;
@@ -1525,28 +1502,19 @@ export class InputHandler extends Disposable implements IInputHandler {
case 1000: // vt200 mouse
case 1002: // button event mouse
case 1003: // any event mouse
this._terminal.x10Mouse = false;
this._terminal.vt200Mouse = false;
this._terminal.normalMouse = false;
this._terminal.mouseEvents = false;
if (this._terminal.element) {
this._terminal.element.classList.remove('enable-mouse-events');
}
if (this._selectionService) {
this._selectionService.enable();
}
this._coreMouseService.activeProtocol = 'NONE';
break;
case 1004: // send focusin/focusout events
this._terminal.sendFocus = false;
break;
case 1005: // utf8 ext mode mouse
this._terminal.utfMouse = false;
this._coreMouseService.activeEncoding = 'DEFAULT';
break;
case 1006: // sgr ext mode mouse
this._terminal.sgrMouse = false;
this._coreMouseService.activeEncoding = 'DEFAULT';
break;
case 1015: // urxvt ext mode mouse
this._terminal.urxvtMouse = false;
this._coreMouseService.activeEncoding = 'DEFAULT';
break;
case 25: // hide cursor
this._terminal.cursorHidden = true;
+172 -254
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -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';
@@ -219,7 +219,7 @@ export class MockInputHandlingTerminal implements IInputHandlingTerminal {
x10Mouse: boolean;
vt200Mouse: boolean;
normalMouse: boolean;
mouseEvents: boolean;
mouseEvents: CoreMouseEventType;
sendFocus: boolean;
utfMouse: boolean;
sgrMouse: boolean;
+2 -8
View File
@@ -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;
+15 -2
View File
@@ -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,19 @@ 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<CoreMouseEventType> = new EventEmitter<CoreMouseEventType>().event;
explainEvents(events: CoreMouseEventType): {[event: string]: boolean} {
throw new Error('Method not implemented.');
}
}
export class MockCoreService implements ICoreService {
serviceBrand: any;
decPrivateModes: IDecPrivateModes = {} as any;
+93
View File
@@ -158,3 +158,96 @@ export interface IRowRange {
start: number;
end: number;
}
/**
* Interface for mouse events in the core.
*/
export const enum CoreMouseButton {
LEFT = 0,
MIDDLE = 1,
RIGHT = 2,
NONE = 3,
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, // buttons, wheel
DOWN = 1, // buttons, wheel
LEFT = 2, // wheel only
RIGHT = 3, // wheel only
MOVE = 32 // buttons only
}
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: CoreMouseButton;
action: CoreMouseAction;
/**
* 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.
*/
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.
* 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;
@@ -0,0 +1,218 @@
/**
* 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, CoreMouseButton, CoreMouseAction } 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: 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[] = [];
cms.onProtocolChange(events => wantedEvents.push(events));
cms.activeProtocol = 'NONE';
assert.deepEqual(wantedEvents, [CoreMouseEventType.NONE]);
cms.activeProtocol = 'ANY';
assert.deepEqual(wantedEvents, [
CoreMouseEventType.NONE,
CoreMouseEventType.DOWN | CoreMouseEventType.UP | CoreMouseEventType.WHEEL | CoreMouseEventType.DRAG | CoreMouseEventType.MOVE
]);
});
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: 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: 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: 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: 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: 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: 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: 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: CoreMouseButton.LEFT, action: CoreMouseAction.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: CoreMouseButton.LEFT, action: CoreMouseAction.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: CoreMouseButton.LEFT, action: CoreMouseAction.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: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true);
assert.deepEqual(reports.pop(), `\x1b[32;${i + 1};1M`);
}
});
});
it('eventCodes with modifiers (DEFAULT encoding)', () => {
// TODO: implement AUX button tests
cms.activeProtocol = 'ANY';
cms.activeEncoding = 'DEFAULT';
// all buttons + down + no modifer
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: 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: 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: 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()) { }
});
});
});
+315
View File
@@ -0,0 +1,315 @@
/**
* 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, CoreMouseButton, CoreMouseAction } from 'common/Types';
/**
* Supported default protocols.
*/
const DEFAULT_PROTOCOLS: {[key: string]: ICoreMouseProtocol} = {
/**
* NONE
* Events: none
* Modifiers: none
*/
NONE: {
events: CoreMouseEventType.NONE,
restrict: () => false
},
/**
* X10
* Events: mousedown
* Modifiers: none
*/
X10: {
events: CoreMouseEventType.DOWN,
restrict: (e: ICoreMouseEvent) => {
// no wheel, no move, no up
if (e.button === CoreMouseButton.WHEEL || e.action !== CoreMouseAction.DOWN) {
return false;
}
// no modifiers
e.ctrl = false;
e.alt = false;
e.shift = false;
return true;
}
},
/**
* VT200
* Events: mousedown / mouseup / wheel
* Modifiers: all
*/
VT200: {
events: CoreMouseEventType.DOWN | CoreMouseEventType.UP | CoreMouseEventType.WHEEL,
restrict: (e: ICoreMouseEvent) => {
// no move
if (e.action === CoreMouseAction.MOVE) {
return false;
}
return true;
}
},
/**
* DRAG
* Events: mousedown / mouseup / wheel / mousedrag
* Modifiers: all
*/
DRAG: {
events: CoreMouseEventType.DOWN | CoreMouseEventType.UP | CoreMouseEventType.WHEEL | CoreMouseEventType.DRAG,
restrict: (e: ICoreMouseEvent) => {
// no move without button
if (e.action === CoreMouseAction.MOVE && e.button === CoreMouseButton.NONE) {
return false;
}
return true;
}
},
/**
* ANY
* Events: all mouse related events
* Modifiers: all
*/
ANY: {
events:
CoreMouseEventType.DOWN | CoreMouseEventType.UP | CoreMouseEventType.WHEEL
| CoreMouseEventType.DRAG | CoreMouseEventType.MOVE,
restrict: (e: ICoreMouseEvent) => true
}
};
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 {
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) {
// special case - only SGR can report button on release
// all others have to go with NONE
code |= CoreMouseButton.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 === CoreMouseAction.UP && e.button !== CoreMouseButton.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<CoreMouseEventType>();
private _lastEvent: ICoreMouseEvent | null = null;
constructor(
@IBufferService private readonly _bufferService: IBufferService,
@ICoreService private readonly _coreService: ICoreService
) {
// register default protocols and encodings
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();
}
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';
this._lastEvent = null;
}
/**
* Event to announce changes in mouse tracking.
*/
public get onProtocolChange(): IEvent<CoreMouseEventType> {
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(e: ICoreMouseEvent): boolean {
// range check for col/row
if (e.col < 0 || e.col >= this._bufferService.cols
|| e.row < 0 || e.row >= this._bufferService.rows) {
return false;
}
// filter nonsense combinations of button + action
if (e.button === CoreMouseButton.WHEEL && e.action === CoreMouseAction.MOVE) {
return false;
}
if (e.button === CoreMouseButton.NONE && e.action !== CoreMouseAction.MOVE) {
return false;
}
if (e.button !== CoreMouseButton.WHEEL && (e.action === CoreMouseAction.LEFT || e.action === CoreMouseAction.RIGHT)) {
return false;
}
// report 1-based coords
e.col++;
e.row++;
// debounce move at grid level
if (e.action === CoreMouseAction.MOVE && this._lastEvent && this._compareEvents(this._lastEvent, e)) {
return false;
}
// apply protocol restrictions
if (!this._protocols[this._activeProtocol].restrict(e)) {
return false;
}
// encode report and send
const report = this._encodings[this._activeEncoding](e);
this._coreService.triggerDataEvent(report, true);
this._lastEvent = e;
return true;
}
public explainEvents(events: CoreMouseEventType): {[event: string]: boolean} {
return {
DOWN: !!(events & CoreMouseEventType.DOWN),
UP: !!(events & CoreMouseEventType.UP),
DRAG: !!(events & CoreMouseEventType.DRAG),
MOVE: !!(events & CoreMouseEventType.MOVE),
WHEEL: !!(events & CoreMouseEventType.WHEEL)
};
}
private _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;
}
}
+32 -1
View File
@@ -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<IBufferService>('BufferService');
@@ -23,6 +23,37 @@ export interface IBufferService {
reset(): void;
}
export const ICoreMouseService = createDecorator<ICoreMouseService>('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<CoreMouseEventType>;
/**
* Human readable version of mouse events.
*/
explainEvents(events: CoreMouseEventType): {[event: string]: boolean};
}
export const ICoreService = createDecorator<ICoreService>('CoreService');
export interface ICoreService {
serviceBrand: any;
File diff suppressed because it is too large Load Diff