keyboardHandler.ts 8.7 KB
Newer Older
E
Erich Gamma 已提交
1 2 3 4 5 6
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/
'use strict';

J
Johannes Rieken 已提交
7
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
A
Alex Dima 已提交
8 9
import * as browser from 'vs/base/browser/browser';
import * as dom from 'vs/base/browser/dom';
J
Johannes Rieken 已提交
10 11 12 13 14
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { GlobalScreenReaderNVDA } from 'vs/editor/common/config/commonEditorConfig';
import { TextAreaHandler } from 'vs/editor/common/controller/textAreaHandler';
import { TextAreaStrategy } from 'vs/editor/common/controller/textAreaState';
import { Range } from 'vs/editor/common/core/range';
A
Alex Dima 已提交
15
import * as editorCommon from 'vs/editor/common/editorCommon';
J
Johannes Rieken 已提交
16 17 18 19
import { ViewEventHandler } from 'vs/editor/common/viewModel/viewEventHandler';
import { IViewController } from 'vs/editor/browser/editorBrowser';
import { Configuration } from 'vs/editor/browser/config/configuration';
import { ViewContext } from 'vs/editor/common/view/viewContext';
20
import { HorizontalRange } from 'vs/editor/common/view/renderingContext';
J
Johannes Rieken 已提交
21
import { TextAreaWrapper } from 'vs/editor/browser/controller/input/textAreaWrapper';
22
import * as viewEvents from 'vs/editor/common/view/viewEvents';
A
Alex Dima 已提交
23
import { FastDomNode } from 'vs/base/browser/fastDomNode';
24 25

export interface IKeyboardHandlerHelper {
A
Alex Dima 已提交
26 27
	viewDomNode: FastDomNode<HTMLElement>;
	textArea: FastDomNode<HTMLTextAreaElement>;
28 29
	visibleRangeForPositionRelativeToEditor(lineNumber: number, column: number): HorizontalRange;
	getVerticalOffsetForLineNumber(lineNumber: number): number;
30 31
	flushAnyAccumulatedEvents(): void;
}
32

33 34 35 36 37 38 39 40 41 42 43
class TextAreaVisiblePosition {
	_textAreaVisiblePosition: void;

	public readonly top: number;
	public readonly left: number;

	constructor(top: number, left: number) {
		this.top = top;
		this.left = left;
	}
}
A
Alex Dima 已提交
44
export class KeyboardHandler extends ViewEventHandler implements IDisposable {
45

J
Johannes Rieken 已提交
46 47 48 49 50 51
	private _context: ViewContext;
	private viewController: IViewController;
	private viewHelper: IKeyboardHandlerHelper;
	private textArea: TextAreaWrapper;
	private textAreaHandler: TextAreaHandler;
	private _toDispose: IDisposable[];
52

J
Johannes Rieken 已提交
53 54 55
	private contentLeft: number;
	private contentWidth: number;
	private scrollLeft: number;
56
	private scrollTop: number;
57

58
	private visiblePosition: TextAreaVisiblePosition;
59

J
Johannes Rieken 已提交
60
	constructor(context: ViewContext, viewController: IViewController, viewHelper: IKeyboardHandlerHelper) {
61
		super();
E
Erich Gamma 已提交
62

63
		this._context = context;
64 65
		this.viewController = viewController;
		this.textArea = new TextAreaWrapper(viewHelper.textArea);
A
Alex Dima 已提交
66
		Configuration.applyFontInfo(this.textArea.actual, this._context.configuration.editor.fontInfo);
67
		this.viewHelper = viewHelper;
68
		this.visiblePosition = null;
69

A
Alex Dima 已提交
70 71
		this.contentLeft = this._context.configuration.editor.layoutInfo.contentLeft;
		this.contentWidth = this._context.configuration.editor.layoutInfo.contentWidth;
72
		this.scrollLeft = 0;
73
		this.scrollTop = 0;
74

75
		this.textAreaHandler = new TextAreaHandler(browser, this._getStrategy(), this.textArea, this._context.model, () => this.viewHelper.flushAnyAccumulatedEvents());
76

77
		this._toDispose = [];
A
Cleanup  
Alex Dima 已提交
78 79
		this._toDispose.push(this.textAreaHandler.onKeyDown((e) => this.viewController.emitKeyDown(<IKeyboardEvent>e._actual)));
		this._toDispose.push(this.textAreaHandler.onKeyUp((e) => this.viewController.emitKeyUp(<IKeyboardEvent>e._actual)));
80 81 82
		this._toDispose.push(this.textAreaHandler.onPaste((e) => this.viewController.paste('keyboard', e.text, e.pasteOnNewLine)));
		this._toDispose.push(this.textAreaHandler.onCut((e) => this.viewController.cut('keyboard')));
		this._toDispose.push(this.textAreaHandler.onType((e) => {
83 84
			if (e.replaceCharCnt) {
				this.viewController.replacePreviousChar('keyboard', e.text, e.replaceCharCnt);
85 86 87 88 89
			} else {
				this.viewController.type('keyboard', e.text);
			}
		}));
		this._toDispose.push(this.textAreaHandler.onCompositionStart((e) => {
90 91
			const lineNumber = e.showAtLineNumber;
			const column = e.showAtColumn;
92

93
			this._context.privateViewEventBus.emit(new viewEvents.ViewRevealRangeRequestEvent(
A
Alex Dima 已提交
94 95 96 97 98
				new Range(lineNumber, column, lineNumber, column),
				editorCommon.VerticalRevealType.Simple,
				true,
				false
			));
99 100

			// Find range pixel position
101 102 103 104 105 106 107 108 109
			const visibleRange = this.viewHelper.visibleRangeForPositionRelativeToEditor(lineNumber, column);

			if (visibleRange) {
				this.visiblePosition = new TextAreaVisiblePosition(
					this.viewHelper.getVerticalOffsetForLineNumber(lineNumber),
					visibleRange.left
				);
				this.textArea.actual.setTop(this.visiblePosition.top - this.scrollTop);
				this.textArea.actual.setLeft(this.contentLeft + this.visiblePosition.left - this.scrollLeft);
110 111 112
			}

			// Show the textarea
A
Alex Dima 已提交
113 114
			this.textArea.actual.setHeight(this._context.configuration.editor.lineHeight);
			this.viewHelper.viewDomNode.addClassName('ime-input');
115 116

			this.viewController.compositionStart('keyboard');
117
		}));
118 119

		this._toDispose.push(this.textAreaHandler.onCompositionUpdate((e) => {
120 121 122
			if (browser.isEdgeOrIE) {
				// Due to isEdgeOrIE (where the textarea was not cleared initially)
				// we cannot assume the text consists only of the composited text
A
Alex Dima 已提交
123
				this.textArea.actual.setWidth(0);
124 125 126 127
			} else {
				// adjust width by its size
				let canvasElem = <HTMLCanvasElement>document.createElement('canvas');
				let context = canvasElem.getContext('2d');
A
Alex Dima 已提交
128
				let cs = dom.getComputedStyle(this.textArea.actual.domNode);
A
Alex Dima 已提交
129 130
				if (browser.isFirefox) {
					// computedStyle.font is empty in Firefox...
131 132
					context.font = `${cs.fontStyle} ${cs.fontVariant} ${cs.fontWeight} ${cs.fontStretch} ${cs.fontSize} / ${cs.lineHeight} ${cs.fontFamily}`;
					let metrics = context.measureText(e.data);
A
Alex Dima 已提交
133
					this.textArea.actual.setWidth(metrics.width + 2); // +2 for Japanese...
A
Alex Dima 已提交
134 135
				} else {
					context.font = cs.font;
136
					let metrics = context.measureText(e.data);
A
Alex Dima 已提交
137
					this.textArea.actual.setWidth(metrics.width);
A
Alex Dima 已提交
138
				}
139
			}
140 141
		}));

142
		this._toDispose.push(this.textAreaHandler.onCompositionEnd((e) => {
A
Alex Dima 已提交
143 144 145 146 147
			this.textArea.actual.unsetHeight();
			this.textArea.actual.unsetWidth();
			this.textArea.actual.setLeft(0);
			this.textArea.actual.setTop(0);
			this.viewHelper.viewDomNode.removeClassName('ime-input');
148

149
			this.visiblePosition = null;
150 151

			this.viewController.compositionEnd('keyboard');
152
		}));
153 154 155
		this._toDispose.push(GlobalScreenReaderNVDA.onChange((value) => {
			this.textAreaHandler.setStrategy(this._getStrategy());
		}));
156 157


158
		this._context.addEventHandler(this);
E
Erich Gamma 已提交
159 160
	}

161
	public dispose(): void {
162
		this._context.removeEventHandler(this);
163 164
		this.textAreaHandler.dispose();
		this.textArea.dispose();
J
Joao Moreno 已提交
165
		this._toDispose = dispose(this._toDispose);
166 167
	}

168
	private _getStrategy(): TextAreaStrategy {
169 170 171
		if (GlobalScreenReaderNVDA.getValue()) {
			return TextAreaStrategy.NVDA;
		}
172
		if (this._context.configuration.editor.viewInfo.experimentalScreenReader) {
173 174 175 176 177
			return TextAreaStrategy.NVDA;
		}
		return TextAreaStrategy.IENarrator;
	}

178
	public focusTextArea(): void {
179
		this.textAreaHandler.focusTextArea();
180 181
	}

182 183
	// --- begin event handlers

A
Alex Dima 已提交
184
	public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean {
185
		// Give textarea same font size & line height as editor, for the IME case (when the textarea is visible)
186
		if (e.fontInfo) {
A
Alex Dima 已提交
187
			Configuration.applyFontInfo(this.textArea.actual, this._context.configuration.editor.fontInfo);
188
		}
189
		if (e.viewInfo.experimentalScreenReader) {
190 191
			this.textAreaHandler.setStrategy(this._getStrategy());
		}
A
Alex Dima 已提交
192 193 194 195
		if (e.layoutInfo) {
			this.contentLeft = this._context.configuration.editor.layoutInfo.contentLeft;
			this.contentWidth = this._context.configuration.editor.layoutInfo.contentWidth;
		}
196 197 198
		return false;
	}

199 200 201
	private _lastCursorSelectionChanged: viewEvents.ViewCursorSelectionChangedEvent = null;
	public onCursorSelectionChanged(e: viewEvents.ViewCursorSelectionChangedEvent): boolean {
		this._lastCursorSelectionChanged = e;
202 203 204
		return false;
	}

A
Alex Dima 已提交
205
	public onFocusChanged(e: viewEvents.ViewFocusChangedEvent): boolean {
A
Alex Dima 已提交
206
		this.textAreaHandler.setHasFocus(e.isFocused);
207 208 209
		return false;
	}

210 211
	public onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean {
		this.scrollLeft = e.scrollLeft;
212 213 214 215
		this.scrollTop = e.scrollTop;
		if (this.visiblePosition) {
			this.textArea.actual.setTop(this.visiblePosition.top - this.scrollTop);
			this.textArea.actual.setLeft(this.contentLeft + this.visiblePosition.left - this.scrollLeft);
216
		}
217 218 219
		return false;
	}

220 221
	// --- end event handlers

222 223 224 225 226 227 228 229
	public writeToTextArea(): void {
		if (this._lastCursorSelectionChanged) {
			let e = this._lastCursorSelectionChanged;
			this._lastCursorSelectionChanged = null;
			this.textAreaHandler.setCursorSelections(e.selection, e.secondarySelections);
		}
	}

230
}