From 95aed8fcb0953440973fabbcf96391bb6c4b80b4 Mon Sep 17 00:00:00 2001 From: CHaBou Date: Thu, 15 Jun 2017 08:38:29 +0200 Subject: [PATCH 01/11] Add customKeypressHandler --- src/test/test.js | 27 +++++++++++++++++++++++++++ src/xterm.js | 21 ++++++++++++++++++++- 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/src/test/test.js b/src/test/test.js index 1644e580..12cd9dc2 100644 --- a/src/test/test.js +++ b/src/test/test.js @@ -377,6 +377,11 @@ describe('xterm.js', function() { stopPropagation: function() {}, type: 'keydown' } + var evKeyPress = { + preventDefault: function() {}, + stopPropagation: function() {}, + type: 'keypress' + } beforeEach(function() { xterm.handler = function() {}; @@ -387,6 +392,11 @@ describe('xterm.js', function() { bind: function() { return function () { return true; } } + }, + keypress: { + bind: function() { + return function () { return true; } + } } } }); @@ -403,13 +413,30 @@ describe('xterm.js', function() { assert.equal(xterm.keyDown(Object.assign({}, evKeyDown, { keyCode: 77 })), false); }); + it('should process the keypress event based on what the handler returns', function () { + assert.equal(xterm.keyPress(Object.assign({}, evKeyPress, { keyCode: 77 })), true); + xterm.attachCustomKeypressHandler(function (ev) { + return ev.keyCode === 77; + }); + assert.equal(xterm.keyPress(Object.assign({}, evKeyPress, { keyCode: 77 })), true); + xterm.attachCustomKeypressHandler(function (ev) { + return ev.keyCode !== 77; + }); + assert.equal(xterm.keyPress(Object.assign({}, evKeyPress, { keyCode: 77 })), false); + }); + it('should alive after reset(ESC c Full Reset (RIS))', function () { xterm.attachCustomKeydownHandler(function (ev) { return ev.keyCode !== 77; }); + xterm.attachCustomKeypressHandler(function (ev) { + return ev.keyCode !== 77; + }); assert.equal(xterm.keyDown(Object.assign({}, evKeyDown, { keyCode: 77 })), false); + assert.equal(xterm.keyPress(Object.assign({}, evKeyPress, { keyCode: 77 })), false); xterm.reset(); assert.equal(xterm.keyDown(Object.assign({}, evKeyDown, { keyCode: 77 })), false); + assert.equal(xterm.keyPress(Object.assign({}, evKeyPress, { keyCode: 77 })), false); }); }); diff --git a/src/xterm.js b/src/xterm.js index 0abcbeae..647d0c6d 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -168,6 +168,7 @@ function Terminal(options) { this.scrollTop = 0; this.scrollBottom = this.rows - 1; this.customKeydownHandler = null; + this.customKeypressHandler = null; this.cursorBlinkInterval = null; // modes @@ -1316,6 +1317,18 @@ Terminal.prototype.attachCustomKeydownHandler = function(customKeydownHandler) { this.customKeydownHandler = customKeydownHandler; } +/** + * Attaches a custom keypress handler which is run before keys are processed, giving consumers of + * xterm.js ultimate control as to what keys should be processed by the terminal and what keys + * should not. + * @param {function} customKeypressHandler The custom KeyboardEvent handler to attach. This is a + * function that takes a KeyboardEvent, allowing consumers to stop propogation and/or prevent + * the default action. The function returns whether the event should be processed by xterm.js. + */ +Terminal.prototype.attachCustomKeypressHandler = function(customKeypressHandler) { + this.customKeypressHandler = customKeypressHandler; +} + /** * Attaches a http(s) link handler, forcing web links to behave differently to * regular tags. This will trigger a refresh as links potentially need to be @@ -1777,6 +1790,10 @@ Terminal.prototype.setgCharset = function(g, charset) { Terminal.prototype.keyPress = function(ev) { var key; + if (this.customKeypressHandler && this.customKeypressHandler(ev) === false) { + return false; + } + this.cancel(ev); if (ev.charCode) { @@ -1802,7 +1819,7 @@ Terminal.prototype.keyPress = function(ev) { this.showCursor(); this.handler(key); - return false; + return true; }; /** @@ -2236,9 +2253,11 @@ Terminal.prototype.reset = function() { this.options.rows = this.rows; this.options.cols = this.cols; var customKeydownHandler = this.customKeydownHandler; + var customKeypressHandler = this.customKeypressHandler; var cursorBlinkInterval = this.cursorBlinkInterval; Terminal.call(this, this.options); this.customKeydownHandler = customKeydownHandler; + this.customKeypressHandler = customKeypressHandler; this.cursorBlinkInterval = cursorBlinkInterval; this.refresh(0, this.rows - 1); this.viewport.syncScrollArea(); From 57921e3fe008f5b841939a18fbf1b0531d87dc3b Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 19 Jun 2017 11:18:55 -0700 Subject: [PATCH 02/11] Render selection on top of rows Fixes #718 --- src/xterm.css | 4 +++- src/xterm.js | 3 +-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/xterm.css b/src/xterm.css index 37f661a7..eef60b45 100644 --- a/src/xterm.css +++ b/src/xterm.css @@ -192,11 +192,13 @@ position: absolute; top: 0; left: 0; + z-index: 1; + opacity: 0.3; } .terminal .xterm-selection div { position: absolute; - background-color: #555; + background-color: #fff; } /* diff --git a/src/xterm.js b/src/xterm.js index 0abcbeae..9bfb275b 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -647,8 +647,7 @@ Terminal.prototype.open = function(parent, focus) { this.viewportScrollArea.classList.add('xterm-scroll-area'); this.viewportElement.appendChild(this.viewportScrollArea); - // Create the selection container. This needs to be added before the - // rowContainer as the selection must be below the text. + // Create the selection container. this.selectionContainer = document.createElement('div'); this.selectionContainer.classList.add('xterm-selection'); this.element.appendChild(this.selectionContainer); From 6dbc476cdc5ad9b23d1d6086585d26ea265b8328 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 19 Jun 2017 11:26:52 -0700 Subject: [PATCH 03/11] Allow mouse events to pass through selection --- src/xterm.css | 1 + 1 file changed, 1 insertion(+) diff --git a/src/xterm.css b/src/xterm.css index eef60b45..6d6f48fa 100644 --- a/src/xterm.css +++ b/src/xterm.css @@ -194,6 +194,7 @@ left: 0; z-index: 1; opacity: 0.3; + pointer-events: none; } .terminal .xterm-selection div { From 8e79d53d7839108fcb0ef5db10ab77e4d1b20224 Mon Sep 17 00:00:00 2001 From: CHaBou Date: Mon, 19 Jun 2017 22:03:19 +0200 Subject: [PATCH 04/11] Add attachCustomKeyEventHandler() Use a uniq customKeyEventHandler for keyDown and keyPress events Mark and warn attachCustomKeydownHandler() as deprecated. --- src/test/test.js | 26 +++++++------------------- src/xterm.js | 27 ++++++++++++--------------- 2 files changed, 19 insertions(+), 34 deletions(-) diff --git a/src/test/test.js b/src/test/test.js index 12cd9dc2..cad1dfc4 100644 --- a/src/test/test.js +++ b/src/test/test.js @@ -371,7 +371,7 @@ describe('xterm.js', function() { }); }); - describe('attachCustomEventHandler', function () { + describe('attachCustomKeyEventHandler', function () { var evKeyDown = { preventDefault: function() {}, stopPropagation: function() {}, @@ -401,35 +401,23 @@ describe('xterm.js', function() { } }); - it('should process the keydown event based on what the handler returns', function () { + it('should process the keydown/keypress event based on what the handler returns', function () { assert.equal(xterm.keyDown(Object.assign({}, evKeyDown, { keyCode: 77 })), true); - xterm.attachCustomKeydownHandler(function (ev) { + assert.equal(xterm.keyPress(Object.assign({}, evKeyPress, { keyCode: 77 })), true); + xterm.attachCustomKeyEventHandler(function (ev) { return ev.keyCode === 77; }); assert.equal(xterm.keyDown(Object.assign({}, evKeyDown, { keyCode: 77 })), true); - xterm.attachCustomKeydownHandler(function (ev) { + assert.equal(xterm.keyPress(Object.assign({}, evKeyPress, { keyCode: 77 })), true); + xterm.attachCustomKeyEventHandler(function (ev) { return ev.keyCode !== 77; }); assert.equal(xterm.keyDown(Object.assign({}, evKeyDown, { keyCode: 77 })), false); - }); - - it('should process the keypress event based on what the handler returns', function () { - assert.equal(xterm.keyPress(Object.assign({}, evKeyPress, { keyCode: 77 })), true); - xterm.attachCustomKeypressHandler(function (ev) { - return ev.keyCode === 77; - }); - assert.equal(xterm.keyPress(Object.assign({}, evKeyPress, { keyCode: 77 })), true); - xterm.attachCustomKeypressHandler(function (ev) { - return ev.keyCode !== 77; - }); assert.equal(xterm.keyPress(Object.assign({}, evKeyPress, { keyCode: 77 })), false); }); it('should alive after reset(ESC c Full Reset (RIS))', function () { - xterm.attachCustomKeydownHandler(function (ev) { - return ev.keyCode !== 77; - }); - xterm.attachCustomKeypressHandler(function (ev) { + xterm.attachCustomKeyEventHandler(function (ev) { return ev.keyCode !== 77; }); assert.equal(xterm.keyDown(Object.assign({}, evKeyDown, { keyCode: 77 })), false); diff --git a/src/xterm.js b/src/xterm.js index 647d0c6d..007b8bc9 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -167,8 +167,7 @@ function Terminal(options) { this.queue = ''; this.scrollTop = 0; this.scrollBottom = this.rows - 1; - this.customKeydownHandler = null; - this.customKeypressHandler = null; + this.customKeyEventHandler = null; this.cursorBlinkInterval = null; // modes @@ -1306,27 +1305,27 @@ Terminal.prototype.writeln = function(data) { }; /** - * Attaches a custom keydown handler which is run before keys are processed, giving consumers of - * xterm.js ultimate control as to what keys should be processed by the terminal and what keys - * should not. + * DEPRECATED: only for backward compatibility. Please use attachCustomKeyEventHandler() instead. * @param {function} customKeydownHandler The custom KeyboardEvent handler to attach. This is a * function that takes a KeyboardEvent, allowing consumers to stop propogation and/or prevent * the default action. The function returns whether the event should be processed by xterm.js. */ Terminal.prototype.attachCustomKeydownHandler = function(customKeydownHandler) { - this.customKeydownHandler = customKeydownHandler; + let message = 'attachCustomKeydownHandler() is DEPRECATED and will be removed soon. Please use attachCustomKeyEventHandler() instead.'; + console.warn(message); + this.attachCustomKeyEventHandler(customKeydownHandler); } /** - * Attaches a custom keypress handler which is run before keys are processed, giving consumers of + * Attaches a custom key event handler which is run before keys are processed, giving consumers of * xterm.js ultimate control as to what keys should be processed by the terminal and what keys * should not. * @param {function} customKeypressHandler The custom KeyboardEvent handler to attach. This is a * function that takes a KeyboardEvent, allowing consumers to stop propogation and/or prevent * the default action. The function returns whether the event should be processed by xterm.js. */ -Terminal.prototype.attachCustomKeypressHandler = function(customKeypressHandler) { - this.customKeypressHandler = customKeypressHandler; +Terminal.prototype.attachCustomKeyEventHandler = function(customKeyEventHandler) { + this.customKeyEventHandler = customKeyEventHandler; } /** @@ -1425,7 +1424,7 @@ Terminal.prototype.selectAll = function() { * @param {KeyboardEvent} ev The keydown event to be handled. */ Terminal.prototype.keyDown = function(ev) { - if (this.customKeydownHandler && this.customKeydownHandler(ev) === false) { + if (this.customKeyEventHandler && this.customKeyEventHandler(ev) === false) { return false; } @@ -1790,7 +1789,7 @@ Terminal.prototype.setgCharset = function(g, charset) { Terminal.prototype.keyPress = function(ev) { var key; - if (this.customKeypressHandler && this.customKeypressHandler(ev) === false) { + if (this.customKeyEventHandler && this.customKeyEventHandler(ev) === false) { return false; } @@ -2252,12 +2251,10 @@ Terminal.prototype.reverseIndex = function() { Terminal.prototype.reset = function() { this.options.rows = this.rows; this.options.cols = this.cols; - var customKeydownHandler = this.customKeydownHandler; - var customKeypressHandler = this.customKeypressHandler; + var customKeyEventHandler = this.customKeyEventHandler; var cursorBlinkInterval = this.cursorBlinkInterval; Terminal.call(this, this.options); - this.customKeydownHandler = customKeydownHandler; - this.customKeypressHandler = customKeypressHandler; + this.customKeyEventHandler = customKeyEventHandler; this.cursorBlinkInterval = cursorBlinkInterval; this.refresh(0, this.rows - 1); this.viewport.syncScrollArea(); From 8811d96a8b8d79db4b31bcf2b2d5c428f47d3d50 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 19 Jun 2017 15:45:18 -0700 Subject: [PATCH 05/11] Fix selection going to clipboard Part of #699 --- src/SelectionManager.ts | 17 +++++++++++++++-- src/xterm.js | 9 ++++++++- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index a769afd1..63720b86 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -293,10 +293,23 @@ export class SelectionManager extends EventEmitter { /** * Queues a refresh, redrawing the selection on the next opportunity. */ - public refresh(): void { + public refresh(fromMouseEvent?: boolean): void { + // Queue the refresh for the renderer if (!this._refreshAnimationFrame) { this._refreshAnimationFrame = window.requestAnimationFrame(() => this._refresh()); } + + // If the refresh comes from a mouse event then emit a newselection event + if (!fromMouseEvent) { + return; + } + // TODO: Only do this when the selection has actually changed + // TODO: Ensure we're not doing more work than absolutely necessary, particularly for large selections + // TODO: Only do this on Linux + const selectionText = this.selectionText; + if (selectionText.length) { + this.emit('newselection', this.selectionText); + } } /** @@ -392,7 +405,7 @@ export class SelectionManager extends EventEmitter { } this._addMouseDownListeners(); - this.refresh(); + this.refresh(true); } /** diff --git a/src/xterm.js b/src/xterm.js index 0abcbeae..fcd42376 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -703,7 +703,14 @@ Terminal.prototype.open = function(parent, focus) { this.viewport = new Viewport(this, this.viewportElement, this.viewportScrollArea, this.charMeasure); this.renderer = new Renderer(this); this.selectionManager = new SelectionManager(this, this.lines, this.rowContainer, this.charMeasure); - this.selectionManager.on('refresh', data => this.renderer.refreshSelection(data.start, data.end)); + this.selectionManager.on('refresh', data => { + this.renderer.refreshSelection(data.start, data.end); + }); + this.selectionManager.on('newselection', text => { + this.textarea.value = text; + this.textarea.focus(); + this.textarea.select(); + }); this.on('scroll', () => this.selectionManager.refresh()); this.viewportElement.addEventListener('scroll', () => this.selectionManager.refresh()); From 32f1782c84eab30d52ec264ca13f1ae1d485b682 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 20 Jun 2017 10:01:39 -0700 Subject: [PATCH 06/11] Fix context menu in firefox Fixes #721 --- src/xterm.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/xterm.js b/src/xterm.js index 0abcbeae..17748296 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -538,8 +538,9 @@ Terminal.prototype.initGlobal = function() { on(this.element, 'paste', pasteHandlerWrapper); if (term.browser.isFirefox) { + // Firefox doesn't appear to fire the contextmenu event on right click on(this.element, 'mousedown', event => { - if (ev.button == 2) { + if (event.button == 2) { rightClickHandler(event, this.textarea, this.selectionManager); } }); From dc165175cfcff9bfb17e5133aed05869c6f80eb6 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 20 Jun 2017 10:18:29 -0700 Subject: [PATCH 07/11] Handle middle click to paste selection This appears to work in Chrome but not Firefox --- src/SelectionManager.ts | 22 ++++++++++------------ src/handlers/Clipboard.ts | 2 +- src/utils/Browser.ts | 1 + src/xterm.js | 20 +++++++++++++++++--- 4 files changed, 29 insertions(+), 16 deletions(-) diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index 63720b86..8c05cd7c 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -2,10 +2,11 @@ * @license MIT */ +import * as Mouse from './utils/Mouse'; +import * as Browser from './utils/Browser'; import { CharMeasure } from './utils/CharMeasure'; import { CircularList } from './utils/CircularList'; import { EventEmitter } from './EventEmitter'; -import * as Mouse from './utils/Mouse'; import { ITerminal } from './Interfaces'; import { SelectionModel } from './SelectionModel'; @@ -299,16 +300,13 @@ export class SelectionManager extends EventEmitter { this._refreshAnimationFrame = window.requestAnimationFrame(() => this._refresh()); } - // If the refresh comes from a mouse event then emit a newselection event - if (!fromMouseEvent) { - return; - } - // TODO: Only do this when the selection has actually changed - // TODO: Ensure we're not doing more work than absolutely necessary, particularly for large selections - // TODO: Only do this on Linux - const selectionText = this.selectionText; - if (selectionText.length) { - this.emit('newselection', this.selectionText); + // If the platform is Linux and the refresh call comes from a mouse event, + // we need to update the selection for middle click to paste selection. + if (Browser.isLinux && fromMouseEvent) { + const selectionText = this.selectionText; + if (selectionText.length) { + this.emit('newselection', this.selectionText); + } } } @@ -560,7 +558,7 @@ export class SelectionManager extends EventEmitter { if (!previousSelectionEnd || previousSelectionEnd[0] !== this._model.selectionEnd[0] || previousSelectionEnd[1] !== this._model.selectionEnd[1]) { - this.refresh(); + this.refresh(true); } } diff --git a/src/handlers/Clipboard.ts b/src/handlers/Clipboard.ts index f22523fd..95945088 100644 --- a/src/handlers/Clipboard.ts +++ b/src/handlers/Clipboard.ts @@ -80,7 +80,7 @@ export function pasteHandler(ev: ClipboardEvent, term: ITerminal) { * @param term The terminal on which to apply the handled paste event * @param selectionManager The terminal's selection manager. */ -export function rightClickHandler(ev: MouseEvent, textarea: HTMLTextAreaElement, selectionManager: ISelectionManager) { +export function moveTextAreaUnderMouseCursor(ev: MouseEvent, textarea: HTMLTextAreaElement, selectionManager: ISelectionManager) { // Bring textarea at the cursor position textarea.style.position = 'fixed'; textarea.style.width = '20px'; diff --git a/src/utils/Browser.ts b/src/utils/Browser.ts index 04da698e..68148aeb 100644 --- a/src/utils/Browser.ts +++ b/src/utils/Browser.ts @@ -20,3 +20,4 @@ export const isMac = contains(['Macintosh', 'MacIntel', 'MacPPC', 'Mac68K'], pla export const isIpad = platform === 'iPad'; export const isIphone = platform === 'iPhone'; export const isMSWindows = contains(['Windows', 'Win16', 'Win32', 'WinCE'], platform); +export const isLinux = platform.indexOf('Linux') >= 0; diff --git a/src/xterm.js b/src/xterm.js index fcd42376..1bcb45d0 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -13,7 +13,7 @@ import { CompositionHelper } from './CompositionHelper'; import { EventEmitter } from './EventEmitter'; import { Viewport } from './Viewport'; -import { rightClickHandler, pasteHandler, copyHandler } from './handlers/Clipboard'; +import { moveTextAreaUnderMouseCursor, pasteHandler, copyHandler } from './handlers/Clipboard'; import { CircularList } from './utils/CircularList'; import { C0 } from './EscapeSequences'; import { InputHandler } from './InputHandler'; @@ -537,15 +537,29 @@ Terminal.prototype.initGlobal = function() { on(this.textarea, 'paste', pasteHandlerWrapper); on(this.element, 'paste', pasteHandlerWrapper); + // Handle right click context menus if (term.browser.isFirefox) { on(this.element, 'mousedown', event => { if (ev.button == 2) { - rightClickHandler(event, this.textarea, this.selectionManager); + moveTextAreaUnderMouseCursor(event, this.textarea, this.selectionManager); } }); } else { on(this.element, 'contextmenu', event => { - rightClickHandler(event, this.textarea, this.selectionManager); + moveTextAreaUnderMouseCursor(event, this.textarea, this.selectionManager); + }); + } + + // Move the textarea under the cursor when middle clicking on Linux to ensure + // middle click to paste selection works. This only appears to work in Chrome + // at the time is writing. + if (term.browser.isLinux) { + // Use auxclick event over mousedown the latter doesn't seem to work. Note + // that the regular click event doesn't fire for the middle mouse button. + on(this.element, 'click', event => { + if (event.button === 1) { + moveTextAreaUnderMouseCursor(event, this.textarea, this.selectionManager); + } }); } }; From 6f5f68df1a15e958e97735daba66f72aab4a72fd Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 20 Jun 2017 10:24:16 -0700 Subject: [PATCH 08/11] Add a comment --- src/xterm.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/xterm.js b/src/xterm.js index 1bcb45d0..4236d4f8 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -721,6 +721,9 @@ Terminal.prototype.open = function(parent, focus) { this.renderer.refreshSelection(data.start, data.end); }); this.selectionManager.on('newselection', text => { + // If there's a new selection, put it into the textarea, focus and select it + // in order to register it as a selection on the OS. This event is fired + // only on Linux to enable middle click to paste selection. this.textarea.value = text; this.textarea.focus(); this.textarea.select(); From 7b46940772da3dd60df628ebc12b65c4710a71dc Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 20 Jun 2017 10:38:33 -0700 Subject: [PATCH 09/11] Have Terminal.hasSelection return false when no selection Fixes #724 --- src/SelectionManager.test.ts | 16 ++++++++++++++++ src/SelectionManager.ts | 7 ++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/SelectionManager.test.ts b/src/SelectionManager.test.ts index eb9322b6..dcc9cb5a 100644 --- a/src/SelectionManager.test.ts +++ b/src/SelectionManager.test.ts @@ -209,4 +209,20 @@ describe('SelectionManager', () => { assert.equal(selectionManager.selectionText, '1\n2\n3\n4\n5'); }); }); + + describe('hasSelection', () => { + it('should return whether there is a selection', () => { + selectionManager.model.selectionStart = [0, 0]; + selectionManager.model.selectionStartLength = 0; + assert.equal(selectionManager.hasSelection, false); + selectionManager.model.selectionEnd = [0, 0]; + assert.equal(selectionManager.hasSelection, false); + selectionManager.model.selectionEnd = [1, 0]; + assert.equal(selectionManager.hasSelection, true); + selectionManager.model.selectionEnd = [0, 1]; + assert.equal(selectionManager.hasSelection, true); + selectionManager.model.selectionEnd = [1, 1]; + assert.equal(selectionManager.hasSelection, true); + }); + }); }); diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index a769afd1..0937f0af 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -183,7 +183,12 @@ export class SelectionManager extends EventEmitter { * Gets whether there is an active text selection. */ public get hasSelection(): boolean { - return !!this._model.finalSelectionStart && !!this._model.finalSelectionEnd; + const start = this._model.finalSelectionStart; + const end = this._model.finalSelectionEnd; + if (!start || !end) { + return false; + } + return start[0] !== end[0] || start[1] !== end[1]; } /** From ef1e2ab9e23e3286772c52b638b20ae8889f3cd1 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 20 Jun 2017 10:49:58 -0700 Subject: [PATCH 10/11] Use auxclick event --- src/xterm.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/xterm.js b/src/xterm.js index 4236d4f8..efcdd524 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -556,7 +556,7 @@ Terminal.prototype.initGlobal = function() { if (term.browser.isLinux) { // Use auxclick event over mousedown the latter doesn't seem to work. Note // that the regular click event doesn't fire for the middle mouse button. - on(this.element, 'click', event => { + on(this.element, 'auxclick', event => { if (event.button === 1) { moveTextAreaUnderMouseCursor(event, this.textarea, this.selectionManager); } From 63d63c27e74a1302f6b09d6a667d6ddb946867f7 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 20 Jun 2017 11:17:12 -0700 Subject: [PATCH 11/11] Ensure outside selection pastes are respected We were resetting the selection on every auxclick, meaning outside selections could not be pasted in when the terminal had a current selection. --- src/SelectionManager.ts | 6 ++++-- src/handlers/Clipboard.ts | 26 ++++++++++++++++++-------- src/xterm.js | 6 +++--- 3 files changed, 25 insertions(+), 13 deletions(-) diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index 8c05cd7c..3138048a 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -293,8 +293,10 @@ export class SelectionManager extends EventEmitter { /** * Queues a refresh, redrawing the selection on the next opportunity. + * @param isNewSelection Whether the selection should be registered as a new + * selection on Linux. */ - public refresh(fromMouseEvent?: boolean): void { + public refresh(isNewSelection?: boolean): void { // Queue the refresh for the renderer if (!this._refreshAnimationFrame) { this._refreshAnimationFrame = window.requestAnimationFrame(() => this._refresh()); @@ -302,7 +304,7 @@ export class SelectionManager extends EventEmitter { // If the platform is Linux and the refresh call comes from a mouse event, // we need to update the selection for middle click to paste selection. - if (Browser.isLinux && fromMouseEvent) { + if (Browser.isLinux && isNewSelection) { const selectionText = this.selectionText; if (selectionText.length) { this.emit('newselection', this.selectionText); diff --git a/src/handlers/Clipboard.ts b/src/handlers/Clipboard.ts index 95945088..aa1c1400 100644 --- a/src/handlers/Clipboard.ts +++ b/src/handlers/Clipboard.ts @@ -75,12 +75,11 @@ export function pasteHandler(ev: ClipboardEvent, term: ITerminal) { } /** - * Bind to right-click event and allow right-click copy and paste. - * @param ev The original right click event to be handled - * @param term The terminal on which to apply the handled paste event - * @param selectionManager The terminal's selection manager. + * Moves the textarea under the mouse cursor and focuses it. + * @param ev The original right click event to be handled. + * @param textarea The terminal's textarea. */ -export function moveTextAreaUnderMouseCursor(ev: MouseEvent, textarea: HTMLTextAreaElement, selectionManager: ISelectionManager) { +export function moveTextAreaUnderMouseCursor(ev: MouseEvent, textarea: HTMLTextAreaElement) { // Bring textarea at the cursor position textarea.style.position = 'fixed'; textarea.style.width = '20px'; @@ -89,10 +88,7 @@ export function moveTextAreaUnderMouseCursor(ev: MouseEvent, textarea: HTMLTextA textarea.style.top = (ev.clientY - 10) + 'px'; textarea.style.zIndex = '1000'; - // Get textarea ready to copy from the context menu - textarea.value = selectionManager.selectionText; textarea.focus(); - textarea.select(); // Reset the terminal textarea's styling setTimeout(function () { @@ -104,3 +100,17 @@ export function moveTextAreaUnderMouseCursor(ev: MouseEvent, textarea: HTMLTextA textarea.style.zIndex = null; }, 4); } + +/** + * Bind to right-click event and allow right-click copy and paste. + * @param ev The original right click event to be handled. + * @param textarea The terminal's textarea. + * @param selectionManager The terminal's selection manager. + */ +export function rightClickHandler(ev: MouseEvent, textarea: HTMLTextAreaElement, selectionManager: ISelectionManager) { + moveTextAreaUnderMouseCursor(ev, textarea); + + // Get textarea ready to copy from the context menu + textarea.value = selectionManager.selectionText; + textarea.select(); +} diff --git a/src/xterm.js b/src/xterm.js index efcdd524..478c6856 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -13,7 +13,7 @@ import { CompositionHelper } from './CompositionHelper'; import { EventEmitter } from './EventEmitter'; import { Viewport } from './Viewport'; -import { moveTextAreaUnderMouseCursor, pasteHandler, copyHandler } from './handlers/Clipboard'; +import { rightClickHandler, moveTextAreaUnderMouseCursor, pasteHandler, copyHandler } from './handlers/Clipboard'; import { CircularList } from './utils/CircularList'; import { C0 } from './EscapeSequences'; import { InputHandler } from './InputHandler'; @@ -541,12 +541,12 @@ Terminal.prototype.initGlobal = function() { if (term.browser.isFirefox) { on(this.element, 'mousedown', event => { if (ev.button == 2) { - moveTextAreaUnderMouseCursor(event, this.textarea, this.selectionManager); + rightClickHandler(event, this.textarea, this.selectionManager); } }); } else { on(this.element, 'contextmenu', event => { - moveTextAreaUnderMouseCursor(event, this.textarea, this.selectionManager); + rightClickHandler(event, this.textarea, this.selectionManager); }); }