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" />
+
+
+
+
+
+
+