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
This commit is contained in:
Jörg Breitbart
2019-07-19 16:21:30 +02:00
parent df0cf253ac
commit cbddbc6e83
6 changed files with 155 additions and 146 deletions
+8 -40
View File
@@ -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<void>();
public get onCursorMove(): IEvent<void> { return this._onCursorMove.event; }
private _onLineFeed = new EventEmitter<void>();
@@ -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: ^[[<b;x;ym
break;
case 1015: // urxvt ext mode mouse
this._terminal.urxvtMouse = true;
this._coreMouseService.activeEncoding = 'URXVT';
// for wide terminals
// numbers for fields
@@ -1487,31 +1468,18 @@ export class InputHandler extends Disposable implements IInputHandler {
case 1002: // button event mouse
case 1003: // any event mouse
this._coreMouseService.activeProtocol = 'NONE';
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();
}
break;
case 1004: // send focusin/focusout events
this._terminal.sendFocus = false;
break;
case 1005: // utf8 ext mode mouse
this._coreMouseService.activeEncoding = 'DEFAULT';
this._terminal.utfMouse = false;
break;
case 1006: // sgr ext mode mouse
this._coreMouseService.activeEncoding = 'DEFAULT';
this._terminal.sgrMouse = false;
this._coreMouseService.activeEncoding = 'DEFAULT';
break;
case 1015: // urxvt ext mode mouse
this._coreMouseService.activeEncoding = 'DEFAULT';
this._terminal.urxvtMouse = false;
break;
case 25: // hide cursor
this._terminal.cursorHidden = true;
+141 -94
View File
@@ -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 } from 'common/Types';
import { IKeyboardEvent, KeyboardResultType, ICharset, IBufferLine, IAttributeData, ICoreMouseEvent, CoreMouseEventType } from 'common/Types';
import { evaluateKeyboardEvent } from 'common/input/Keyboard';
import { EventEmitter, IEvent } from 'common/EventEmitter';
import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
@@ -142,16 +142,8 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
public charsets: ICharset[];
// mouse properties
private _decLocator: boolean; // This is unstable and never set
public x10Mouse: boolean;
public vt200Mouse: boolean;
private _vt300Mouse: boolean; // This is unstable and never set
public normalMouse: boolean;
public mouseEvents: boolean;
public mouseEvents: CoreMouseEventType[] = [];
public sendFocus: boolean;
public utfMouse: boolean;
public sgrMouse: boolean;
public urxvtMouse: boolean;
// misc
public savedCols: number;
@@ -658,13 +650,12 @@ 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);
if (this.mouseEvents) {
this.element.classList.toggle('enable-mouse-events', !!this.mouseEvents.length);
if (this.mouseEvents.length) {
this._selectionService.disable();
} else {
this._selectionService.enable();
}
this._inputHandler.setBrowserServices(this._selectionService);
if (this.options.screenReaderMode) {
// Note that this must be done *after* the renderer is created in order to
@@ -683,8 +674,8 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
// Listen for mouse events and translate
// them into terminal mouse protocols.
// this.bindMouse();
this.bindMouse();
}
private _createRenderer(): IRenderer {
@@ -712,15 +703,12 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
}
}
/**
* mouse events
* FIXME: move event handler registration into browser MouseService
*/
public bindMouse(): void {
const el = this.element;
const self = this;
function sendButton(ev: MouseEvent | WheelEvent): void {
public bindMouse(): void {
const self = this;
const el = this.element;
function sendEvent(ev: MouseEvent | WheelEvent): boolean {
let pos;
// get mouse coordinates
@@ -730,7 +718,16 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
let but: ICoreMouseEvent['button'];
let action: ICoreMouseEvent['action'];
let code: number;
// FIXME: cleanup the switch mess
switch ((<any>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 = (<WheelEvent>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) {
(<any>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.
+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';
@@ -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;
+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;
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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
}
};