diff options
| -rw-r--r-- | ext/bg/css/settings.css | 4 | ||||
| -rw-r--r-- | ext/bg/js/settings/keyboard-mouse-input-field.js | 204 | ||||
| -rw-r--r-- | ext/bg/js/settings/profile-conditions-ui.js | 120 | ||||
| -rw-r--r-- | ext/bg/settings.html | 3 | ||||
| -rw-r--r-- | ext/mixed/img/mouse.svg | 1 | ||||
| -rw-r--r-- | ext/mixed/js/core.js | 8 | ||||
| -rw-r--r-- | resources/icons.svg | 25 | 
7 files changed, 266 insertions, 99 deletions
| 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 <https://www.gnu.org/licenses/>. + */ + +/* 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 @@                      <div class="input-group-btn"><select class="form-control btn btn-default condition-operator"><optgroup label="Operator"></optgroup></select></div>                      <div class="condition-line-break"></div>                      <div class="condition-input"><input type="text" class="form-control condition-input-inner"></div> -                    <div class="input-group-btn"><button class="btn btn-danger condition-remove" title="Remove"><span class="glyphicon glyphicon-remove"></span></button></div> +                    <div class="input-group-btn"><button class="btn btn-default condition-mouse-button" title="Mouse button" style="padding-left: 10px; padding-right: 10px;" hidden><span style="width: 20px; height: 20px; display: block; background: url(/mixed/img/mouse.svg) no-repeat center center; background-size: 20px 20px;"></span></button><button class="btn btn-danger condition-remove" title="Remove"><span class="glyphicon glyphicon-remove"></span></button></div>                  </div></template>              </div> @@ -1164,6 +1164,7 @@          <script src="/bg/js/settings/dictionaries.js"></script>          <script src="/bg/js/settings/dictionary-import-controller.js"></script>          <script src="/bg/js/settings/generic-setting-controller.js"></script> +        <script src="/bg/js/settings/keyboard-mouse-input-field.js"></script>          <script src="/bg/js/settings/popup-preview.js"></script>          <script src="/bg/js/settings/profiles.js"></script>          <script src="/bg/js/settings/profile-conditions-ui.js"></script> 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 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M7.4883 1.0273C4.9609 1.29264 3 3.4984 3 6.1992v.30078h4.5v-5.4727c-.00385.0004-.00787-.0004-.01172 0zm1.0117 0V6.5H13v-.30078c0-2.7049-1.967-4.9121-4.5-5.1719zM3 7.5v2.3008C3 12.6816 5.23 15 8 15s5-2.3184 5-5.1992V7.5H3z" fill="#333"/></svg>
\ 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" />    </g>    <g -     style="display:inline" +     style="display:none"       inkscape:label="Up Arrow"       id="g1012"       inkscape:groupmode="layer"> @@ -1213,4 +1213,19 @@         inkscape:connector-curvature="0"         sodipodi:nodetypes="cccccccc" />    </g> +  <g +     inkscape:groupmode="layer" +     id="layer38" +     inkscape:label="Mouse"> +    <g +       id="g1029"> +      <g +         id="g1033"> +        <path +           style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.5;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal" +           d="M 7.4882812 1.0273438 C 4.9608765 1.2926885 3 3.4984688 3 6.1992188 L 3 6.5 L 7.5 6.5 L 7.5 1.0273438 C 7.4961497 1.0277387 7.4921289 1.0269398 7.4882812 1.0273438 z M 8.5 1.0273438 L 8.5 6.5 L 13 6.5 L 13 6.1992188 C 13 3.4943573 11.03298 1.2871677 8.5 1.0273438 z M 3 7.5 L 3 9.8007812 C 3 12.681581 5.23 15 8 15 C 10.77 15 13 12.681581 13 9.8007812 L 13 7.5 L 3 7.5 z " +           id="rect1013" /> +      </g> +    </g> +  </g>  </svg> |