diff --git a/ext/bg/css/settings.css b/ext/bg/css/settings.css index 8b9d5037..2dc71d91 100644 --- a/ext/bg/css/settings.css +++ b/ext/bg/css/settings.css @@ -113,6 +113,10 @@ html:root:not([data-options-general-result-output-mode=merge]) #dict-main-group display: none; } +.condition-mouse-button[hidden] { + display: none; +} + .audio-source-list { counter-reset: audio-source-id; } diff --git a/ext/bg/js/settings/keyboard-mouse-input-field.js b/ext/bg/js/settings/keyboard-mouse-input-field.js new file mode 100644 index 00000000..d1dc76e0 --- /dev/null +++ b/ext/bg/js/settings/keyboard-mouse-input-field.js @@ -0,0 +1,204 @@ +/* + * 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 . + */ + +/* global + * DocumentUtil + */ + +class KeyboardMouseInputField extends EventDispatcher { + constructor(inputNode, mouseButton, inputNameMap, keySeparator) { + super(); + this._inputNode = inputNode; + this._keySeparator = keySeparator; + this._keyPriorities = new Map([ + ['meta', -4], + ['ctrl', -3], + ['alt', -2], + ['shift', -1] + ]); + this._mouseButton = mouseButton; + this._inputNameMap = inputNameMap; + this._mouseInputNamePattern = /^mouse(\d+)$/; + this._eventListeners = new EventListenerCollection(); + this._value = ''; + this._type = null; + } + + get value() { + return this._value; + } + + prepare(value, type) { + this.cleanup(); + + this._value = value; + const modifiers = this._splitValue(value); + const {displayValue} = this._getInputStrings(modifiers); + const events = [ + [this._inputNode, 'keydown', this._onModifierKeyDown.bind(this), false] + ]; + if (type === 'modifierInputs' && this._mouseButton !== null) { + events.push( + [this._mouseButton, 'mousedown', this._onMouseButtonMouseDown.bind(this), false], + [this._mouseButton, 'mouseup', this._onMouseButtonMouseUp.bind(this), false], + [this._mouseButton, 'contextmenu', this._onMouseButtonContextMenu.bind(this), false] + ); + } + this._inputNode.value = displayValue; + for (const args of events) { + this._eventListeners.addEventListener(...args); + } + } + + cleanup() { + this._eventListeners.removeAllEventListeners(); + this._value = ''; + this._type = null; + } + + // Private + + _splitValue(value) { + return value.split(/[,;\s]+/).map((v) => v.trim().toLowerCase()).filter((v) => v.length > 0); + } + + _sortInputs(inputs) { + const pattern = this._mouseInputNamePattern; + const keyPriorities = this._keyPriorities; + const inputInfos = inputs.map((value, index) => { + const match = pattern.exec(value); + if (match !== null) { + return [value, 1, Number.parseInt(match[1], 10), index]; + } else { + let priority = keyPriorities.get(value); + if (typeof priority === 'undefined') { priority = 0; } + return [value, 0, priority, index]; + } + }); + inputInfos.sort((a, b) => { + let i = a[1] - b[1]; + if (i !== 0) { return i; } + + i = a[2] - b[2]; + if (i !== 0) { return i; } + + i = a[0].localeCompare(b[0], 'en-US'); // Ensure an invariant culture + if (i !== 0) { return i; } + + i = a[3] - b[3]; + return i; + }); + return inputInfos.map(([value]) => value); + } + + _getInputStrings(inputs) { + let value = ''; + let displayValue = ''; + let first = true; + for (const input of inputs) { + const {name} = this._getInputName(input); + if (first) { + first = false; + } else { + value += ', '; + displayValue += this._keySeparator; + } + value += input; + displayValue += name; + } + return {value, displayValue}; + } + + _getInputName(value) { + const pattern = this._mouseInputNamePattern; + const match = pattern.exec(value); + if (match !== null) { + return {name: `Mouse ${match[1]}`, type: 'mouse'}; + } + + let name = this._inputNameMap.get(value); + if (typeof name === 'undefined') { name = value; } + return {name, type: 'key'}; + } + + _getModifierKeys(e) { + const modifiers = DocumentUtil.getActiveModifiers(e); + // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/metaKey + // https://askubuntu.com/questions/567731/why-is-shift-alt-being-mapped-to-meta + // It works with mouse events on some platforms, so try to determine if metaKey is pressed. + // This is a hack and only works when both Shift and Alt are not pressed. + if ( + !modifiers.has('meta') && + DocumentUtil.getKeyFromEvent(e) === 'Meta' && + !( + modifiers.size === 2 && + modifiers.has('shift') && + modifiers.has('alt') + ) + ) { + modifiers.add('meta'); + } + return modifiers; + } + + _onModifierKeyDown(e) { + e.preventDefault(); + + const key = DocumentUtil.getKeyFromEvent(e); + switch (key) { + case 'Escape': + case 'Backspace': + this._updateInputs([]); + break; + default: + this._addInputs(this._getModifierKeys(e)); + break; + } + } + + _onMouseButtonMouseDown(e) { + e.preventDefault(); + this._addInputs([`mouse${e.button}`]); + } + + _onMouseButtonMouseUp(e) { + e.preventDefault(); + } + + _onMouseButtonContextMenu(e) { + e.preventDefault(); + } + + _addInputs(newInputs) { + const inputs = new Set(this._splitValue(this._value)); + for (const input of newInputs) { + inputs.add(input); + } + this._updateInputs([...inputs]); + } + + _updateInputs(inputs) { + inputs = this._sortInputs(inputs); + + const node = this._inputNode; + const {value, displayValue} = this._getInputStrings(inputs); + node.value = displayValue; + if (this._value === value) { return; } + this._value = value; + this.trigger('change', {value, displayValue}); + } +} diff --git a/ext/bg/js/settings/profile-conditions-ui.js b/ext/bg/js/settings/profile-conditions-ui.js index 4c8d1132..4fb181cf 100644 --- a/ext/bg/js/settings/profile-conditions-ui.js +++ b/ext/bg/js/settings/profile-conditions-ui.js @@ -16,7 +16,7 @@ */ /* global - * DocumentUtil + * KeyboardMouseInputField */ class ProfileConditionsUI { @@ -203,35 +203,15 @@ class ProfileConditionsUI { return value.split(/[,;\s]+/).map((v) => v.trim().toLowerCase()).filter((v) => v.length > 0); } - getModifierKeyStrings(modifiers) { - let value = ''; - let displayValue = ''; - let first = true; - for (const modifier of modifiers) { - let keyName = this._keyNames.get(modifier); - if (typeof keyName === 'undefined') { keyName = modifier; } - - if (first) { - first = false; - } else { - value += ', '; - displayValue += this._keySeparator; - } - value += modifier; - displayValue += keyName; - } - return {value, displayValue}; - } - - sortModifiers(modifiers) { - return modifiers.sort(); - } - getPath(property) { property = (typeof property === 'string' ? `.${property}` : ''); return `profiles[${this.index}]${property}`; } + createKeyboardMouseInputField(inputNode, mouseButton) { + return new KeyboardMouseInputField(inputNode, mouseButton, this._keyNames, this._keySeparator); + } + // Private _onAddConditionGroupButtonClick() { @@ -425,7 +405,9 @@ class ProfileConditionUI { this._operatorInput = null; this._valueInputContainer = null; this._removeButton = null; + this._mouseButton = null; this._value = ''; + this._kbmInputField = null; this._eventListeners = new EventListenerCollection(); this._inputEventListeners = new EventListenerCollection(); } @@ -460,6 +442,7 @@ class ProfileConditionUI { this._operatorOptionContainer = this._operatorInput.querySelector('optgroup'); this._valueInput = this._node.querySelector('.condition-input-inner'); this._removeButton = this._node.querySelector('.condition-remove'); + this._mouseButton = this._node.querySelector('.condition-mouse-button'); const operatorDetails = this._getOperatorDetails(type, operator); this._updateTypes(type); @@ -538,32 +521,7 @@ class ProfileConditionUI { } } - _onModifierKeyDown({validate, normalize}, e) { - e.preventDefault(); - const node = e.currentTarget; - - let modifiers; - const key = DocumentUtil.getKeyFromEvent(e); - switch (key) { - case 'Escape': - case 'Backspace': - modifiers = []; - break; - default: - { - modifiers = this._getModifiers(e); - const currentModifier = this._splitValue(this._value); - for (const modifier of currentModifier) { - modifiers.add(modifier); - } - modifiers = [...modifiers]; - modifiers = this._sortModifiers(modifiers); - } - break; - } - - const {value, displayValue} = this._getModifierKeyStrings(modifiers); - node.value = displayValue; + _onModifierInputChange({validate, normalize}, {value}) { const okay = this._validateValue(value, validate); this._value = value; if (okay) { @@ -588,18 +546,6 @@ class ProfileConditionUI { return this._parent.parent.getOperatorDetails(type, operator); } - _getModifierKeyStrings(modifiers) { - return this._parent.parent.getModifierKeyStrings(modifiers); - } - - _sortModifiers(modifiers) { - return this._parent.parent.sortModifiers(modifiers); - } - - _splitValue(value) { - return this._parent.parent.splitValue(value); - } - _updateTypes(type) { const types = this._getDescriptorTypes(); this._updateSelect(this._typeInput, this._typeOptionContainer, types, type); @@ -623,10 +569,15 @@ class ProfileConditionUI { _updateValueInput(value, {type, validate, normalize}) { this._inputEventListeners.removeAllEventListeners(); + if (this._kbmInputField !== null) { + this._kbmInputField.cleanup(); + this._kbmInputField = null; + } let inputType = 'text'; let inputValue = value; let inputStep = null; + let mouseButtonHidden = true; const events = []; const inputData = {validate, normalize}; const node = this._valueInput; @@ -635,32 +586,35 @@ class ProfileConditionUI { case 'integer': inputType = 'number'; inputStep = '1'; - events.push([node, 'change', this._onValueInputChange.bind(this, inputData), false]); + events.push(['addEventListener', node, 'change', this._onValueInputChange.bind(this, inputData), false]); break; case 'modifierKeys': - { - const modifiers = this._splitValue(value); - const {displayValue} = this._getModifierKeyStrings(modifiers); - inputValue = displayValue; - events.push([node, 'keydown', this._onModifierKeyDown.bind(this, inputData), false]); - } + case 'modifierInputs': + inputValue = null; + mouseButtonHidden = (type !== 'modifierInputs'); + this._kbmInputField = this._parent.parent.createKeyboardMouseInputField(node, this._mouseButton); + this._kbmInputField.prepare(value, type); + events.push(['on', this._kbmInputField, 'change', this._onModifierInputChange.bind(this, inputData), false]); break; default: // 'string' - events.push([node, 'change', this._onValueInputChange.bind(this, inputData), false]); + events.push(['addEventListener', node, 'change', this._onValueInputChange.bind(this, inputData), false]); break; } this._value = value; node.classList.remove('is-invalid'); node.type = inputType; - node.value = inputValue; + if (inputValue !== null) { + node.value = inputValue; + } if (typeof inputStep === 'string') { node.step = inputStep; } else { node.removeAttribute('step'); } + this._mouseButton.hidden = mouseButtonHidden; for (const args of events) { - this._inputEventListeners.addEventListener(...args); + this._inputEventListeners.addGeneric(...args); } this._validateValue(value, validate); @@ -675,24 +629,4 @@ class ProfileConditionUI { _normalizeValue(value, normalize) { return (normalize !== null ? normalize(value) : value); } - - _getModifiers(e) { - const modifiers = DocumentUtil.getActiveModifiers(e); - // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/metaKey - // https://askubuntu.com/questions/567731/why-is-shift-alt-being-mapped-to-meta - // It works with mouse events on some platforms, so try to determine if metaKey is pressed. - // This is a hack and only works when both Shift and Alt are not pressed. - if ( - !modifiers.has('meta') && - DocumentUtil.getKeyFromEvent(e) === 'Meta' && - !( - modifiers.size === 2 && - modifiers.has('shift') && - modifiers.has('alt') - ) - ) { - modifiers.add('meta'); - } - return modifiers; - } } diff --git a/ext/bg/settings.html b/ext/bg/settings.html index a999a9d9..0ad5b79b 100644 --- a/ext/bg/settings.html +++ b/ext/bg/settings.html @@ -125,7 +125,7 @@
-
+
@@ -1164,6 +1164,7 @@ + diff --git a/ext/mixed/img/mouse.svg b/ext/mixed/img/mouse.svg new file mode 100644 index 00000000..80c400e6 --- /dev/null +++ b/ext/mixed/img/mouse.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ext/mixed/js/core.js b/ext/mixed/js/core.js index c5c6fef2..8b044a67 100644 --- a/ext/mixed/js/core.js +++ b/ext/mixed/js/core.js @@ -341,6 +341,14 @@ class EventListenerCollection { return this._eventListeners.length; } + addGeneric(type, object, ...args) { + switch (type) { + case 'addEventListener': return this.addEventListener(object, ...args); + case 'addListener': return this.addListener(object, ...args); + case 'on': return this.on(object, ...args); + } + } + addEventListener(object, ...args) { object.addEventListener(...args); this._eventListeners.push(['removeEventListener', object, ...args]); diff --git a/resources/icons.svg b/resources/icons.svg index e75b35df..e8303ece 100644 --- a/resources/icons.svg +++ b/resources/icons.svg @@ -27,11 +27,11 @@ borderopacity="1.0" inkscape:pageopacity="0.0" inkscape:pageshadow="2" - inkscape:zoom="22.627417" - inkscape:cx="12.059712" - inkscape:cy="6.3977551" + inkscape:zoom="45.254834" + inkscape:cx="6.9125714" + inkscape:cy="9.2321432" inkscape:document-units="px" - inkscape:current-layer="g1012" + inkscape:current-layer="layer38" showgrid="true" units="px" inkscape:snap-center="true" @@ -1202,7 +1202,7 @@ id="rect1210" /> @@ -1213,4 +1213,19 @@ inkscape:connector-curvature="0" sodipodi:nodetypes="cccccccc" /> + + + + + + +