Modifier key platform names (#519)

* wip

* add environment class

* use Environment class

* use Environment for scanning modifier options

* remove Environment in favor of API

* await promise

* use modifier symbols on macOS

* fix key separator issues

* if else to switch

* simplify variable names
This commit is contained in:
siikamiika 2020-05-09 18:36:00 +03:00 committed by GitHub
parent 48cf646973
commit d6a3825a38
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 288 additions and 165 deletions

View File

@ -21,6 +21,7 @@
<script src="/mixed/js/core.js"></script> <script src="/mixed/js/core.js"></script>
<script src="/mixed/js/dom.js"></script> <script src="/mixed/js/dom.js"></script>
<script src="/mixed/js/environment.js"></script>
<script src="/mixed/js/japanese.js"></script> <script src="/mixed/js/japanese.js"></script>
<script src="/bg/js/anki.js"></script> <script src="/bg/js/anki.js"></script>

View File

@ -24,6 +24,7 @@
* ClipboardMonitor * ClipboardMonitor
* Database * Database
* DictionaryImporter * DictionaryImporter
* Environment
* JsonSchema * JsonSchema
* Mecab * Mecab
* ObjectPropertyAccessor * ObjectPropertyAccessor
@ -35,6 +36,7 @@
* optionsLoad * optionsLoad
* optionsSave * optionsSave
* profileConditionsDescriptor * profileConditionsDescriptor
* profileConditionsDescriptorPromise
* requestJson * requestJson
* requestText * requestText
* utilIsolate * utilIsolate
@ -42,6 +44,7 @@
class Backend { class Backend {
constructor() { constructor() {
this.environment = new Environment();
this.database = new Database(); this.database = new Database();
this.dictionaryImporter = new DictionaryImporter(); this.dictionaryImporter = new DictionaryImporter();
this.translator = new Translator(this.database); this.translator = new Translator(this.database);
@ -100,7 +103,7 @@ class Backend {
['broadcastTab', {async: false, contentScript: true, handler: this._onApiBroadcastTab.bind(this)}], ['broadcastTab', {async: false, contentScript: true, handler: this._onApiBroadcastTab.bind(this)}],
['frameInformationGet', {async: true, contentScript: true, handler: this._onApiFrameInformationGet.bind(this)}], ['frameInformationGet', {async: true, contentScript: true, handler: this._onApiFrameInformationGet.bind(this)}],
['injectStylesheet', {async: true, contentScript: true, handler: this._onApiInjectStylesheet.bind(this)}], ['injectStylesheet', {async: true, contentScript: true, handler: this._onApiInjectStylesheet.bind(this)}],
['getEnvironmentInfo', {async: true, contentScript: true, handler: this._onApiGetEnvironmentInfo.bind(this)}], ['getEnvironmentInfo', {async: false, contentScript: true, handler: this._onApiGetEnvironmentInfo.bind(this)}],
['clipboardGet', {async: true, contentScript: true, handler: this._onApiClipboardGet.bind(this)}], ['clipboardGet', {async: true, contentScript: true, handler: this._onApiClipboardGet.bind(this)}],
['getDisplayTemplatesHtml', {async: true, contentScript: true, handler: this._onApiGetDisplayTemplatesHtml.bind(this)}], ['getDisplayTemplatesHtml', {async: true, contentScript: true, handler: this._onApiGetDisplayTemplatesHtml.bind(this)}],
['getQueryParserTemplatesHtml', {async: true, contentScript: true, handler: this._onApiGetQueryParserTemplatesHtml.bind(this)}], ['getQueryParserTemplatesHtml', {async: true, contentScript: true, handler: this._onApiGetQueryParserTemplatesHtml.bind(this)}],
@ -140,9 +143,12 @@ class Backend {
}, 1000); }, 1000);
this._updateBadge(); this._updateBadge();
await this.environment.prepare();
await this.database.prepare(); await this.database.prepare();
await this.translator.prepare(); await this.translator.prepare();
await profileConditionsDescriptorPromise;
this.optionsSchema = await requestJson(chrome.runtime.getURL('/bg/data/options-schema.json'), 'GET'); this.optionsSchema = await requestJson(chrome.runtime.getURL('/bg/data/options-schema.json'), 'GET');
this.defaultAnkiFieldTemplates = await requestText(chrome.runtime.getURL('/bg/data/default-anki-field-templates.handlebars'), 'GET'); this.defaultAnkiFieldTemplates = await requestText(chrome.runtime.getURL('/bg/data/default-anki-field-templates.handlebars'), 'GET');
this.options = await optionsLoad(); this.options = await optionsLoad();
@ -635,15 +641,8 @@ class Backend {
}); });
} }
async _onApiGetEnvironmentInfo() { _onApiGetEnvironmentInfo() {
const browser = await Backend._getBrowser(); return this.environment.getInfo();
const platform = await new Promise((resolve) => chrome.runtime.getPlatformInfo(resolve));
return {
browser,
platform: {
os: platform.os
}
};
} }
async _onApiClipboardGet() { async _onApiClipboardGet() {
@ -659,7 +658,7 @@ class Backend {
being an extension with clipboard permissions. It effectively asks for the being an extension with clipboard permissions. It effectively asks for the
non-extension permission for clipboard access. non-extension permission for clipboard access.
*/ */
const browser = await Backend._getBrowser(); const {browser} = this.environment.getInfo();
if (browser === 'firefox' || browser === 'firefox-mobile') { if (browser === 'firefox' || browser === 'firefox-mobile') {
return await navigator.clipboard.readText(); return await navigator.clipboard.readText();
} else { } else {
@ -1211,23 +1210,4 @@ class Backend {
// Edge throws exception for no reason here. // Edge throws exception for no reason here.
} }
} }
static async _getBrowser() {
if (EXTENSION_IS_BROWSER_EDGE) {
return 'edge';
}
if (typeof browser !== 'undefined') {
try {
const info = await browser.runtime.getBrowserInfo();
if (info.name === 'Fennec') {
return 'firefox-mobile';
}
} catch (e) {
// NOP
}
return 'firefox';
} else {
return 'chrome';
}
}
} }

View File

@ -15,6 +15,9 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
/* global
* Environment
*/
function _profileConditionTestDomain(urlDomain, domain) { function _profileConditionTestDomain(urlDomain, domain) {
return ( return (
@ -36,25 +39,27 @@ function _profileConditionTestDomainList(url, domainList) {
return false; return false;
} }
const _profileModifierKeys = [ let profileConditionsDescriptor = null;
{optionValue: 'alt', name: 'Alt'},
{optionValue: 'ctrl', name: 'Ctrl'},
{optionValue: 'shift', name: 'Shift'}
];
if (!hasOwn(window, 'netscape')) { const profileConditionsDescriptorPromise = (async () => {
_profileModifierKeys.push({optionValue: 'meta', name: 'Meta'}); const environment = new Environment();
} await environment.prepare();
const _profileModifierValueToName = new Map( const modifiers = environment.getInfo().modifiers;
_profileModifierKeys.map(({optionValue, name}) => [optionValue, name]) const modifierSeparator = modifiers.separator;
const modifierKeyValues = modifiers.keys.map(
({value, name}) => ({optionValue: value, name})
); );
const _profileModifierNameToValue = new Map( const modifierValueToName = new Map(
_profileModifierKeys.map(({optionValue, name}) => [name, optionValue]) modifierKeyValues.map(({optionValue, name}) => [optionValue, name])
); );
const profileConditionsDescriptor = { const modifierNameToValue = new Map(
modifierKeyValues.map(({optionValue, name}) => [name, optionValue])
);
profileConditionsDescriptor = {
popupLevel: { popupLevel: {
name: 'Popup Level', name: 'Popup Level',
description: 'Use profile depending on the level of the popup.', description: 'Use profile depending on the level of the popup.',
@ -122,7 +127,7 @@ const profileConditionsDescriptor = {
modifierKeys: { modifierKeys: {
name: 'Modifier Keys', name: 'Modifier Keys',
description: 'Use profile depending on the active modifier keys.', description: 'Use profile depending on the active modifier keys.',
values: _profileModifierKeys, values: modifierKeyValues,
defaultOperator: 'are', defaultOperator: 'are',
operators: { operators: {
are: { are: {
@ -130,13 +135,14 @@ const profileConditionsDescriptor = {
placeholder: 'Press one or more modifier keys here', placeholder: 'Press one or more modifier keys here',
defaultValue: [], defaultValue: [],
type: 'keyMulti', type: 'keyMulti',
keySeparator: modifierSeparator,
transformInput: (optionValue) => optionValue transformInput: (optionValue) => optionValue
.split(' + ') .split(modifierSeparator)
.filter((v) => v.length > 0) .filter((v) => v.length > 0)
.map((v) => _profileModifierNameToValue.get(v)), .map((v) => modifierNameToValue.get(v)),
transformReverse: (transformedOptionValue) => transformedOptionValue transformReverse: (transformedOptionValue) => transformedOptionValue
.map((v) => _profileModifierValueToName.get(v)) .map((v) => modifierValueToName.get(v))
.join(' + '), .join(modifierSeparator),
test: ({modifierKeys}, optionValue) => areSetsEqual(new Set(modifierKeys), new Set(optionValue)) test: ({modifierKeys}, optionValue) => areSetsEqual(new Set(modifierKeys), new Set(optionValue))
}, },
areNot: { areNot: {
@ -144,13 +150,14 @@ const profileConditionsDescriptor = {
placeholder: 'Press one or more modifier keys here', placeholder: 'Press one or more modifier keys here',
defaultValue: [], defaultValue: [],
type: 'keyMulti', type: 'keyMulti',
keySeparator: modifierSeparator,
transformInput: (optionValue) => optionValue transformInput: (optionValue) => optionValue
.split(' + ') .split(modifierSeparator)
.filter((v) => v.length > 0) .filter((v) => v.length > 0)
.map((v) => _profileModifierNameToValue.get(v)), .map((v) => modifierNameToValue.get(v)),
transformReverse: (transformedOptionValue) => transformedOptionValue transformReverse: (transformedOptionValue) => transformedOptionValue
.map((v) => _profileModifierValueToName.get(v)) .map((v) => modifierValueToName.get(v))
.join(' + '), .join(modifierSeparator),
test: ({modifierKeys}, optionValue) => !areSetsEqual(new Set(modifierKeys), new Set(optionValue)) test: ({modifierKeys}, optionValue) => !areSetsEqual(new Set(modifierKeys), new Set(optionValue))
}, },
include: { include: {
@ -168,3 +175,4 @@ const profileConditionsDescriptor = {
} }
} }
}; };
})();

View File

@ -310,10 +310,14 @@ ConditionsUI.Condition = class Condition {
inputInner.prop('readonly', true); inputInner.prop('readonly', true);
let values = []; let values = [];
let keySeparator = ' + ';
for (const object of objects) { for (const object of objects) {
if (hasOwn(object, 'values')) { if (hasOwn(object, 'values')) {
values = object.values; values = object.values;
} }
if (hasOwn(object, 'keySeparator')) {
keySeparator = object.keySeparator;
}
} }
const pressedKeyIndices = new Set(); const pressedKeyIndices = new Set();
@ -347,7 +351,7 @@ ConditionsUI.Condition = class Condition {
} }
} }
const inputValue = [...pressedKeyIndices].map((i) => values[i].name).join(' + '); const inputValue = [...pressedKeyIndices].map((i) => values[i].name).join(keySeparator);
inputInner.val(inputValue); inputInner.val(inputValue);
inputInner.change(); inputInner.change();
}; };

View File

@ -22,6 +22,7 @@
* ankiTemplatesInitialize * ankiTemplatesInitialize
* ankiTemplatesUpdateValue * ankiTemplatesUpdateValue
* apiForwardLogsToBackend * apiForwardLogsToBackend
* apiGetEnvironmentInfo
* apiOptionsSave * apiOptionsSave
* appearanceInitialize * appearanceInitialize
* audioSettingsInitialize * audioSettingsInitialize
@ -285,6 +286,23 @@ function showExtensionInformation() {
node.textContent = `${manifest.name} v${manifest.version}`; node.textContent = `${manifest.name} v${manifest.version}`;
} }
async function settingsPopulateModifierKeys() {
const scanModifierKeySelect = document.querySelector('#scan-modifier-key');
scanModifierKeySelect.textContent = '';
const environment = await apiGetEnvironmentInfo();
const modifierKeys = [
{value: 'none', name: 'None'},
...environment.modifiers.keys
];
for (const {value, name} of modifierKeys) {
const option = document.createElement('option');
option.value = value;
option.textContent = name;
scanModifierKeySelect.appendChild(option);
}
}
async function onReady() { async function onReady() {
apiForwardLogsToBackend(); apiForwardLogsToBackend();
@ -292,6 +310,7 @@ async function onReady() {
showExtensionInformation(); showExtensionInformation();
await settingsPopulateModifierKeys();
formSetupEventListeners(); formSetupEventListeners();
appearanceInitialize(); appearanceInitialize();
await audioSettingsInitialize(); await audioSettingsInitialize();

View File

@ -23,6 +23,7 @@
* getOptionsFullMutable * getOptionsFullMutable
* getOptionsMutable * getOptionsMutable
* profileConditionsDescriptor * profileConditionsDescriptor
* profileConditionsDescriptorPromise
* settingsSaveOptions * settingsSaveOptions
* utilBackgroundIsolate * utilBackgroundIsolate
*/ */
@ -98,6 +99,7 @@ async function profileFormWrite(optionsFull) {
profileConditionsContainer.cleanup(); profileConditionsContainer.cleanup();
} }
await profileConditionsDescriptorPromise;
profileConditionsContainer = new ConditionsUI.Container( profileConditionsContainer = new ConditionsUI.Container(
profileConditionsDescriptor, profileConditionsDescriptor,
'popupLevel', 'popupLevel',
@ -128,7 +130,7 @@ function profileOptionsPopulateSelect(select, profiles, currentValue, ignoreIndi
} }
async function profileOptionsUpdateTarget(optionsFull) { async function profileOptionsUpdateTarget(optionsFull) {
profileFormWrite(optionsFull); await profileFormWrite(optionsFull);
const optionsContext = getOptionsContext(); const optionsContext = getOptionsContext();
const options = await getOptionsMutable(optionsContext); const options = await getOptionsMutable(optionsContext);

View File

@ -412,13 +412,7 @@
<div class="form-group"> <div class="form-group">
<label for="scan-modifier-key">Scan modifier key</label> <label for="scan-modifier-key">Scan modifier key</label>
<select class="form-control" id="scan-modifier-key"> <select class="form-control" id="scan-modifier-key"></select>
<option value="none">None</option>
<option value="alt">Alt</option>
<option value="ctrl">Ctrl</option>
<option value="shift">Shift</option>
<option data-hide-for-browser="firefox firefox-mobile" value="meta">Meta</option>
</select>
</div> </div>
</div> </div>
@ -1131,6 +1125,7 @@
<script src="/mixed/js/core.js"></script> <script src="/mixed/js/core.js"></script>
<script src="/mixed/js/dom.js"></script> <script src="/mixed/js/dom.js"></script>
<script src="/mixed/js/environment.js"></script>
<script src="/mixed/js/api.js"></script> <script src="/mixed/js/api.js"></script>
<script src="/mixed/js/japanese.js"></script> <script src="/mixed/js/japanese.js"></script>

114
ext/mixed/js/environment.js Normal file
View File

@ -0,0 +1,114 @@
/*
* Copyright (C) 2020 Yomichan Authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
class Environment {
constructor() {
this._cachedEnvironmentInfo = null;
}
async prepare() {
this._cachedEnvironmentInfo = await this._loadEnvironmentInfo();
}
getInfo() {
if (this._cachedEnvironmentInfo === null) { throw new Error('Not prepared'); }
return this._cachedEnvironmentInfo;
}
async _loadEnvironmentInfo() {
const browser = await this._getBrowser();
const platform = await new Promise((resolve) => chrome.runtime.getPlatformInfo(resolve));
const modifierInfo = this._getModifierInfo(browser, platform.os);
return {
browser,
platform: {
os: platform.os
},
modifiers: modifierInfo
};
}
async _getBrowser() {
if (EXTENSION_IS_BROWSER_EDGE) {
return 'edge';
}
if (typeof browser !== 'undefined') {
try {
const info = await browser.runtime.getBrowserInfo();
if (info.name === 'Fennec') {
return 'firefox-mobile';
}
} catch (e) {
// NOP
}
return 'firefox';
} else {
return 'chrome';
}
}
_getModifierInfo(browser, os) {
let osKeys;
let separator;
switch (os) {
case 'win':
separator = ' + ';
osKeys = [
['alt', 'Alt'],
['ctrl', 'Ctrl'],
['shift', 'Shift'],
['meta', 'Windows']
];
break;
case 'mac':
separator = '';
osKeys = [
['alt', '⌥'],
['ctrl', '⌃'],
['shift', '⇧'],
['meta', '⌘']
];
break;
case 'linux':
case 'openbsd':
case 'cros':
case 'android':
separator = ' + ';
osKeys = [
['alt', 'Alt'],
['ctrl', 'Ctrl'],
['shift', 'Shift'],
['meta', 'Super']
];
break;
default:
throw new Error(`Invalid OS: ${os}`);
}
const isFirefox = (browser === 'firefox' || browser === 'firefox-mobile');
const keys = [];
for (const [value, name] of osKeys) {
// Firefox doesn't support event.metaKey on platforms other than macOS
if (value === 'meta' && isFirefox && os !== 'mac') { continue; }
keys.push({value, name});
}
return {keys, separator};
}
}