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..b3316ae6 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'; @@ -183,7 +184,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]; } /** @@ -292,11 +298,23 @@ 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(): void { + public refresh(isNewSelection?: boolean): void { + // Queue the refresh for the renderer if (!this._refreshAnimationFrame) { this._refreshAnimationFrame = window.requestAnimationFrame(() => this._refresh()); } + + // 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 && isNewSelection) { + const selectionText = this.selectionText; + if (selectionText.length) { + this.emit('newselection', this.selectionText); + } + } } /** @@ -392,7 +410,7 @@ export class SelectionManager extends EventEmitter { } this._addMouseDownListeners(); - this.refresh(); + this.refresh(true); } /** @@ -547,7 +565,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..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 rightClickHandler(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 rightClickHandler(ev: MouseEvent, textarea: HTMLTextAreaElement, 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 rightClickHandler(ev: MouseEvent, textarea: HTMLTextAreaElement, 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/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.css b/src/xterm.css index 37f661a7..6d6f48fa 100644 --- a/src/xterm.css +++ b/src/xterm.css @@ -192,11 +192,14 @@ position: absolute; top: 0; left: 0; + z-index: 1; + opacity: 0.3; + pointer-events: none; } .terminal .xterm-selection div { position: absolute; - background-color: #555; + background-color: #fff; } /* diff --git a/src/xterm.js b/src/xterm.js index 007b8bc9..a16a4ac7 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 { rightClickHandler, moveTextAreaUnderMouseCursor, pasteHandler, copyHandler } from './handlers/Clipboard'; import { CircularList } from './utils/CircularList'; import { C0 } from './EscapeSequences'; import { InputHandler } from './InputHandler'; @@ -537,9 +537,11 @@ Terminal.prototype.initGlobal = function() { on(this.textarea, 'paste', pasteHandlerWrapper); on(this.element, 'paste', pasteHandlerWrapper); + // Handle right click context menus 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); } }); @@ -548,6 +550,19 @@ Terminal.prototype.initGlobal = function() { rightClickHandler(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, 'auxclick', event => { + if (event.button === 1) { + moveTextAreaUnderMouseCursor(event, this.textarea, this.selectionManager); + } + }); + } }; /** @@ -647,8 +662,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); @@ -703,7 +717,17 @@ 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 => { + // 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(); + }); this.on('scroll', () => this.selectionManager.refresh()); this.viewportElement.addEventListener('scroll', () => this.selectionManager.refresh());