@theia/core
Version:
Theia is a cloud & desktop IDE framework implemented in TypeScript.
424 lines • 15.7 kB
JavaScript
"use strict";
// *****************************************************************************
// Copyright (C) 2019 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
// *****************************************************************************
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.KeyboardTester = exports.DEFAULT_LAYOUT_DATA = exports.BrowserKeyboardLayoutProvider = void 0;
const inversify_1 = require("inversify");
const os_1 = require("../../common/os");
const event_1 = require("../../common/event");
const logger_1 = require("../../common/logger");
const promise_util_1 = require("../../common/promise-util");
const storage_service_1 = require("../storage-service");
let BrowserKeyboardLayoutProvider = class BrowserKeyboardLayoutProvider {
constructor() {
this.initialized = new promise_util_1.Deferred();
this.nativeLayoutChanged = new event_1.Emitter();
this.tester = new KeyboardTester(loadAllLayouts());
this.source = 'pressed-keys';
this.currentLayout = exports.DEFAULT_LAYOUT_DATA;
}
get onDidChangeNativeLayout() {
return this.nativeLayoutChanged.event;
}
get allLayoutData() {
return this.tester.candidates.slice();
}
get currentLayoutData() {
return this.currentLayout;
}
get currentLayoutSource() {
return this.source;
}
async initialize() {
await this.loadState();
const keyboard = navigator.keyboard;
if (keyboard && keyboard.addEventListener) {
keyboard.addEventListener('layoutchange', async () => {
const newLayout = await this.getNativeLayout();
this.nativeLayoutChanged.fire(newLayout);
});
}
this.initialized.resolve();
}
async getNativeLayout() {
await this.initialized.promise;
if (this.source === 'user-choice') {
return this.currentLayout.raw;
}
const [layout, source] = await this.autodetect();
this.setCurrent(layout, source);
return layout.raw;
}
/**
* Set user-chosen keyboard layout data.
*/
async setLayoutData(layout) {
if (layout === 'autodetect') {
if (this.source === 'user-choice') {
const [newLayout, source] = await this.autodetect();
this.setCurrent(newLayout, source);
this.nativeLayoutChanged.fire(newLayout.raw);
return newLayout;
}
return this.currentLayout;
}
else {
if (this.source !== 'user-choice' || layout !== this.currentLayout) {
this.setCurrent(layout, 'user-choice');
this.nativeLayoutChanged.fire(layout.raw);
}
return layout;
}
}
/**
* Test all known keyboard layouts with the given combination of pressed key and
* produced character. Matching layouts have their score increased (see class
* KeyboardTester). If this leads to a change of the top-scoring layout, a layout
* change event is fired.
*/
validateKey(keyCode) {
if (this.source !== 'pressed-keys') {
return;
}
const accepted = this.tester.updateScores(keyCode);
if (!accepted) {
return;
}
const layout = this.selectLayout();
if (layout !== this.currentLayout && layout !== exports.DEFAULT_LAYOUT_DATA) {
this.setCurrent(layout, 'pressed-keys');
this.nativeLayoutChanged.fire(layout.raw);
}
}
setCurrent(layout, source) {
this.currentLayout = layout;
this.source = source;
this.saveState();
if (this.tester.inputCount && (source === 'pressed-keys' || source === 'navigator.keyboard')) {
const from = source === 'pressed-keys' ? 'pressed keys' : 'browser API';
const hardware = layout.hardware === 'mac' ? 'Mac' : 'PC';
this.logger.info(`Detected keyboard layout from ${from}: ${layout.name} (${hardware})`);
}
}
async autodetect() {
const keyboard = navigator.keyboard;
if (keyboard && keyboard.getLayoutMap) {
try {
const layoutMap = await keyboard.getLayoutMap();
this.testLayoutMap(layoutMap);
return [this.selectLayout(), 'navigator.keyboard'];
}
catch (error) {
this.logger.warn('Failed to obtain keyboard layout map.', error);
}
}
return [this.selectLayout(), 'pressed-keys'];
}
/**
* @param layoutMap a keyboard layout map according to https://wicg.github.io/keyboard-map/
*/
testLayoutMap(layoutMap) {
this.tester.reset();
for (const [code, key] of layoutMap.entries()) {
this.tester.updateScores({ code, character: key });
}
}
/**
* Select a layout based on the current tester state and the operating system
* and language detected from the browser.
*/
selectLayout() {
const candidates = this.tester.candidates;
const scores = this.tester.scores;
const topScore = this.tester.topScore;
const language = navigator.language;
let matchingOScount = 0;
let topScoringCount = 0;
for (let i = 0; i < candidates.length; i++) {
if (scores[i] === topScore) {
const candidate = candidates[i];
if (osMatches(candidate.hardware)) {
if (language && language.startsWith(candidate.language)) {
return candidate;
}
matchingOScount++;
}
topScoringCount++;
}
}
if (matchingOScount >= 1) {
return candidates.find((c, i) => scores[i] === topScore && osMatches(c.hardware));
}
if (topScoringCount >= 1) {
return candidates.find((_, i) => scores[i] === topScore);
}
return exports.DEFAULT_LAYOUT_DATA;
}
saveState() {
const data = {
tester: this.tester.getState(),
source: this.source,
currentLayout: this.currentLayout !== exports.DEFAULT_LAYOUT_DATA ? getLayoutId(this.currentLayout) : undefined
};
return this.storageService.setData('keyboard', data);
}
async loadState() {
const data = await this.storageService.getData('keyboard');
if (data) {
this.tester.setState(data.tester || {});
this.source = data.source || 'pressed-keys';
if (data.currentLayout) {
const layout = this.tester.candidates.find(c => getLayoutId(c) === data.currentLayout);
if (layout) {
this.currentLayout = layout;
}
}
else {
this.currentLayout = exports.DEFAULT_LAYOUT_DATA;
}
}
}
};
__decorate([
(0, inversify_1.inject)(logger_1.ILogger),
__metadata("design:type", Object)
], BrowserKeyboardLayoutProvider.prototype, "logger", void 0);
__decorate([
(0, inversify_1.inject)(storage_service_1.LocalStorageService),
__metadata("design:type", storage_service_1.LocalStorageService)
], BrowserKeyboardLayoutProvider.prototype, "storageService", void 0);
__decorate([
(0, inversify_1.postConstruct)(),
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", Promise)
], BrowserKeyboardLayoutProvider.prototype, "initialize", null);
BrowserKeyboardLayoutProvider = __decorate([
(0, inversify_1.injectable)()
], BrowserKeyboardLayoutProvider);
exports.BrowserKeyboardLayoutProvider = BrowserKeyboardLayoutProvider;
function osMatches(hardware) {
return os_1.isOSX ? hardware === 'mac' : hardware === 'pc';
}
/**
* This is the fallback keyboard layout selected when nothing else matches.
* It has an empty mapping, so user inputs are handled like with a standard US keyboard.
*/
exports.DEFAULT_LAYOUT_DATA = {
name: 'US',
hardware: os_1.isOSX ? 'mac' : 'pc',
language: 'en',
raw: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
info: {},
mapping: {}
}
};
/**
* Holds score values for all known keyboard layouts. Scores are updated
* by comparing key codes with the corresponding character produced by
* the user's keyboard.
*/
class KeyboardTester {
constructor(candidates) {
this.candidates = candidates;
this.topScore = 0;
this.testedInputs = new Map();
this.scores = this.candidates.map(() => 0);
}
get inputCount() {
return this.testedInputs.size;
}
reset() {
for (let i = 0; i < this.scores.length; i++) {
this.scores[i] = 0;
}
this.topScore = 0;
this.testedInputs.clear();
}
updateScores(input) {
let property;
if (input.shiftKey && input.altKey) {
property = 'withShiftAltGr';
}
else if (input.shiftKey) {
property = 'withShift';
}
else if (input.altKey) {
property = 'withAltGr';
}
else {
property = 'value';
}
const inputKey = `${input.code}.${property}`;
if (this.testedInputs.has(inputKey)) {
if (this.testedInputs.get(inputKey) === input.character) {
return false;
}
else {
// The same input keystroke leads to a different character:
// probably a keyboard layout change, so forget all previous scores
this.reset();
}
}
const scores = this.scores;
for (let i = 0; i < this.candidates.length; i++) {
scores[i] += this.testCandidate(this.candidates[i], input, property);
if (scores[i] > this.topScore) {
this.topScore = scores[i];
}
}
this.testedInputs.set(inputKey, input.character);
return true;
}
testCandidate(candidate, input, property) {
const keyMapping = candidate.raw.mapping[input.code];
if (keyMapping && keyMapping[property]) {
return keyMapping[property] === input.character ? 1 : 0;
}
else {
return 0;
}
}
getState() {
const scores = {};
for (let i = 0; i < this.scores.length; i++) {
scores[getLayoutId(this.candidates[i])] = this.scores[i];
}
const testedInputs = {};
for (const [key, character] of this.testedInputs.entries()) {
testedInputs[key] = character;
}
return {
scores,
topScore: this.topScore,
testedInputs
};
}
setState(state) {
this.reset();
if (state.scores) {
const layoutIds = this.candidates.map(getLayoutId);
for (const id in state.scores) {
if (state.scores.hasOwnProperty(id)) {
const index = layoutIds.indexOf(id);
if (index > 0) {
this.scores[index] = state.scores[id];
}
}
}
}
if (state.topScore) {
this.topScore = state.topScore;
}
if (state.testedInputs) {
for (const key in state.testedInputs) {
if (state.testedInputs.hasOwnProperty(key)) {
this.testedInputs.set(key, state.testedInputs[key]);
}
}
}
}
}
exports.KeyboardTester = KeyboardTester;
function getLayoutId(layout) {
return `${layout.language}-${layout.name.replace(' ', '_')}-${layout.hardware}`;
}
/**
* Keyboard layout files are expected to have the following name scheme:
* `language-name-hardware.json`
*
* - `language`: A language subtag according to IETF BCP 47
* - `name`: Display name of the keyboard layout (without dashes)
* - `hardware`: `pc` or `mac`
*/
function loadLayout(fileName) {
const [language, name, hardware] = fileName.split('-');
return {
name: name.replace('_', ' '),
hardware: hardware,
language,
// Webpack knows what to do here and it should bundle all files under `../../../src/common/keyboard/layouts/`
// eslint-disable-next-line import/no-dynamic-require
raw: require('../../../src/common/keyboard/layouts/' + fileName + '.json')
};
}
function loadAllLayouts() {
// The order of keyboard layouts is relevant for autodetection. Layouts with
// lower index have a higher chance of being selected.
// The current ordering approach is to sort by estimated number of developers
// in the respective country (taken from the Stack Overflow Developer Survey),
// but keeping all layouts of the same language together.
return [
'en-US-pc',
'en-US-mac',
'en-Dvorak-pc',
'en-Dvorak-mac',
'en-Dvorak_Lefthanded-pc',
'en-Dvorak_Lefthanded-mac',
'en-Dvorak_Righthanded-pc',
'en-Dvorak_Righthanded-mac',
'en-Colemak-mac',
'en-British-pc',
'en-British-mac',
'de-German-pc',
'de-German-mac',
'de-Swiss_German-pc',
'de-Swiss_German-mac',
'fr-French-pc',
'fr-French-mac',
'fr-Canadian_French-pc',
'fr-Canadian_French-mac',
'fr-Swiss_French-pc',
'fr-Swiss_French-mac',
'fr-Bepo-pc',
'pt-Portuguese-pc',
'pt-Portuguese-mac',
'pt-Brazilian-mac',
'pl-Polish-pc',
'pl-Polish-mac',
'nl-Dutch-pc',
'nl-Dutch-mac',
'es-Spanish-pc',
'es-Spanish-mac',
'it-Italian-pc',
'it-Italian-mac',
'sv-Swedish-pc',
'sv-Swedish-mac',
'tr-Turkish_Q-pc',
'tr-Turkish_Q-mac',
'cs-Czech-pc',
'cs-Czech-mac',
'ro-Romanian-pc',
'ro-Romanian-mac',
'da-Danish-pc',
'da-Danish-mac',
'nb-Norwegian-pc',
'nb-Norwegian-mac',
'hu-Hungarian-pc',
'hu-Hungarian-mac'
].map(loadLayout);
}
//# sourceMappingURL=browser-keyboard-layout-provider.js.map