Repository: qutebrowser/qutebrowser

Issue:
Selecting number hints with keypad fails
Reproducable with `:set hints.mode number`, `f` and `12` with v1.2.1 - looks like the `<Num+2>` keypress gets treated as special key and clears the keystring:

```
20:03:01 DEBUG    commands   command:run:501 Calling qutebrowser.browser.hints.HintManager.start(<qutebrowser.browser.hints.HintManager object at 0x7fcd005905e8>, <Group.all: 1>, <Target.normal: 1>, win_id=0, mode=None, add_history=False, rapid=False)
20:03:01 DEBUG    modes      modeman:_handle_keypress:194 match: 2, forward_unbound_keys: auto, passthrough: False, is_non_alnum: False, dry_run: False --> filter: True (focused: <PyQt5.QtWidgets.QWidget object at 0x7fccd8f84d38>)
20:03:01 DEBUG    hints      hints:_start_cb:596 hints: 00, 01, 02, 03, 04, 05, 06, 07, 08, 09, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43
20:03:01 DEBUG    webelem    webengineelem:rect_on_view:189 Couldn't find rectangle for <qutebrowser.browser.webengine.webengineelem.WebEngineElement html='<img id="avw_pixel_intern" src="/avw-bin/ivw/CP/barfoo/ho/3988383/0.gif?d=1093263708" width="1" height="1" alt="">'> ([{'bottom': 320, 'height': 1, 'left': 445.796875, 'right': 446.796875, 'top': 319, 'width': 1}])
20:03:01 DEBUG    message    message:set_text:264 Follow hint...
20:03:01 DEBUG    statusbar  text:set_text:57 Setting normal text to 'Follow hint...'.
20:03:01 DEBUG    modes      modeman:enter:240 Entering mode KeyMode.hint (reason: HintManager.start)
20:03:01 DEBUG    modes      modeman:_handle_keyrelease:214 filter: True
20:03:19 DEBUG    modes      modeman:_handle_keypress:167 got keypress in mode KeyMode.hint - delegating to <qutebrowser.keyinput.modeparsers.HintKeyParser supports_count=False>
20:03:19 DEBUG    keyboard   basekeyparser:_debug_log:89 Got key: 0x31 / modifiers: 0x20000000 / text: '<Num+1>' / dry_run True
20:03:19 DEBUG    keyboard   basekeyparser:_debug_log:89 Trying match without modifiers
20:03:19 DEBUG    modes      modeman:_handle_keypress:194 match: 1, forward_unbound_keys: auto, passthrough: False, is_non_alnum: True, dry_run: True --> filter: True (focused: <PyQt5.QtWidgets.QWidget object at 0x7fccd8f855e8>)
20:03:19 DEBUG    modes      modeman:_handle_keypress:167 got keypress in mode KeyMode.hint - delegating to <qutebrowser.keyinput.modeparsers.HintKeyParser supports_count=False>
20:03:19 DEBUG    keyboard   basekeyparser:_debug_log:89 Got key: 0x31 / modifiers: 0x20000000 / text: '<Num+1>' / dry_run True
20:03:19 DEBUG    keyboard   basekeyparser:_debug_log:89 Trying match without modifiers
20:03:19 DEBUG    keyboard   modeparsers:handle:267 Got special key, clearing keychain
20:03:19 DEBUG    keyboard   basekeyparser:_debug_log:89 Got key: 0x31 / modifiers: 0x20000000 / text: '<Num+1>' / dry_run False
20:03:19 DEBUG    keyboard   basekeyparser:_debug_log:89 Trying match without modifiers
20:03:19 DEBUG    keyboard   basekeyparser:_debug_log:89 No match for '1' (added <Num+1>)
20:03:19 DEBUG    hints      hints:handle_partial_key:774 Handling new keystring: '1'
20:03:19 DEBUG    modes      modeman:_handle_keypress:194 match: 1, forward_unbound_keys: auto, passthrough: False, is_non_alnum: True, dry_run: False --> filter: True (focused: <PyQt5.QtWidgets.QWidget object at 0x7fccd8f855e8>)
20:03:19 DEBUG    modes      modeman:_handle_keyrelease:214 filter: True
20:03:20 DEBUG    modes      modeman:_handle_keypress:167 got keypress in mode KeyMode.hint - delegating to <qutebrowser.keyinput.modeparsers.HintKeyParser supports_count=False>
20:03:20 DEBUG    keyboard   basekeyparser:_debug_log:89 Got key: 0x32 / modifiers: 0x20000000 / text: '<Num+2>' / dry_run True
20:03:20 DEBUG    keyboard   basekeyparser:_debug_log:89 Trying match without modifiers
20:03:20 DEBUG    modes      modeman:_handle_keypress:194 match: 2, forward_unbound_keys: auto, passthrough: False, is_non_alnum: True, dry_run: True --> filter: True (focused: <PyQt5.QtWidgets.QWidget object at 0x7fccd8f85708>)
20:03:20 DEBUG    modes      modeman:_handle_keypress:167 got keypress in mode KeyMode.hint - delegating to <qutebrowser.keyinput.modeparsers.HintKeyParser supports_count=False>
20:03:20 DEBUG    keyboard   basekeyparser:_debug_log:89 Got key: 0x32 / modifiers: 0x20000000 / text: '<Num+2>' / dry_run True
20:03:20 DEBUG    keyboard   basekeyparser:_debug_log:89 Trying match without modifiers
20:03:20 DEBUG    keyboard   modeparsers:handle:267 Got special key, clearing keychain
20:03:20 DEBUG    keyboard   basekeyparser:_debug_log:89 Clearing keystring (was: 1).
20:03:20 DEBUG    hints      hints:handle_partial_key:774 Handling new keystring: ''
20:03:20 DEBUG    keyboard   basekeyparser:_debug_log:89 Got key: 0x32 / modifiers: 0x20000000 / text: '<Num+2>' / dry_run False
20:03:20 DEBUG    keyboard   basekeyparser:_debug_log:89 Trying match without modifiers
20:03:20 DEBUG    keyboard   basekeyparser:_debug_log:89 No match for '2' (added <Num+2>)
20:03:20 DEBUG    hints      hints:handle_partial_key:774 Handling new keystring: '2'
20:03:20 DEBUG    modes      modeman:_handle_keypress:194 match: 1, forward_unbound_keys: auto, passthrough: False, is_non_alnum: True, dry_run: False --> filter: True (focused: <PyQt5.QtWidgets.QWidget object at 0x7fccd8f85708>)
20:03:20 DEBUG    modes      modeman:_handle_keyrelease:214 filter: True
```


Discussion:

Resolving patch:
diff --git a/qutebrowser/keyinput/keyutils.py b/qutebrowser/keyinput/keyutils.py
--- a/qutebrowser/keyinput/keyutils.py
+++ b/qutebrowser/keyinput/keyutils.py
@@ -58,7 +58,8 @@ def is_special(key, modifiers):
     _assert_plain_key(key)
     _assert_plain_modifier(modifiers)
     return not (_is_printable(key) and
-                modifiers in [Qt.ShiftModifier, Qt.NoModifier])
+                modifiers in [Qt.ShiftModifier, Qt.NoModifier,
+                              Qt.KeypadModifier])
 
 
 def is_modifier_key(key):
@@ -303,7 +304,8 @@ def __str__(self):
                 key_string = key_string.lower()
 
         # "special" binding
-        assert is_special(self.key, self.modifiers)
+        assert (is_special(self.key, self.modifiers) or
+                self.modifiers == Qt.KeypadModifier)
         modifier_string = _modifiers_to_string(modifiers)
         return '<{}{}>'.format(modifier_string, key_string)
 


Test patch:
diff --git a/tests/unit/keyinput/test_keyutils.py b/tests/unit/keyinput/test_keyutils.py
--- a/tests/unit/keyinput/test_keyutils.py
+++ b/tests/unit/keyinput/test_keyutils.py
@@ -518,6 +518,8 @@ def test_is_printable(key, printable):
     (Qt.Key_Escape, Qt.ControlModifier, True),
     (Qt.Key_X, Qt.ControlModifier, True),
     (Qt.Key_X, Qt.NoModifier, False),
+    (Qt.Key_2, Qt.KeypadModifier, False),
+    (Qt.Key_2, Qt.NoModifier, False),
 ])
 def test_is_special(key, modifiers, special):
     assert keyutils.is_special(key, modifiers) == special
diff --git a/tests/unit/keyinput/test_modeparsers.py b/tests/unit/keyinput/test_modeparsers.py
--- a/tests/unit/keyinput/test_modeparsers.py
+++ b/tests/unit/keyinput/test_modeparsers.py
@@ -96,3 +96,11 @@ def test_simple_hint_match(self, keyparser, fake_keyevent):
         assert match == QKeySequence.ExactMatch
 
         keyparser.execute.assert_called_with('follow-hint -s as', None)
+
+    def test_numberkey_hint_match(self, keyparser, fake_keyevent):
+        keyparser.update_bindings(['21', '22'])
+
+        match = keyparser.handle(fake_keyevent(Qt.Key_2, Qt.KeypadModifier))
+        assert match == QKeySequence.PartialMatch
+        match = keyparser.handle(fake_keyevent(Qt.Key_2, Qt.KeypadModifier))
+        assert match == QKeySequence.ExactMatch