Rename keyinput stuff

This commit is contained in:
Florian Bruhin 2014-04-25 12:21:01 +02:00
parent 82cb91957d
commit 6e78f67a81
10 changed files with 548 additions and 545 deletions

View File

@ -52,13 +52,12 @@ import qutebrowser.commands.utils as cmdutils
import qutebrowser.config.style as style import qutebrowser.config.style as style
import qutebrowser.config.config as config import qutebrowser.config.config as config
import qutebrowser.network.qutescheme as qutescheme import qutebrowser.network.qutescheme as qutescheme
import qutebrowser.keyinput.modes as modes import qutebrowser.keyinput.modeman as modeman
import qutebrowser.utils.message as message import qutebrowser.utils.message as message
from qutebrowser.widgets.mainwindow import MainWindow from qutebrowser.widgets.mainwindow import MainWindow
from qutebrowser.widgets.crash import CrashDialog from qutebrowser.widgets.crash import CrashDialog
from qutebrowser.keyinput.normalmode import NormalKeyParser from qutebrowser.keyinput.modeparsers import NormalKeyParser, HintKeyParser
from qutebrowser.keyinput.keyparser import PassthroughKeyParser from qutebrowser.keyinput.keyparser import PassthroughKeyParser
from qutebrowser.keyinput.hintmode import HintKeyParser
from qutebrowser.commands.managers import CommandManager, SearchManager from qutebrowser.commands.managers import CommandManager, SearchManager
from qutebrowser.utils.appdirs import AppDirs from qutebrowser.utils.appdirs import AppDirs
from qutebrowser.utils.misc import dotted_getattr from qutebrowser.utils.misc import dotted_getattr
@ -132,22 +131,22 @@ class QuteBrowser(QApplication):
} }
self._init_cmds() self._init_cmds()
self.mainwindow = MainWindow() self.mainwindow = MainWindow()
modes.init(self) modeman.init(self)
modes.manager.register('normal', self._keyparsers['normal'].handle) modeman.manager.register('normal', self._keyparsers['normal'].handle)
modes.manager.register('hint', self._keyparsers['hint'].handle) modeman.manager.register('hint', self._keyparsers['hint'].handle)
modes.manager.register('insert', self._keyparsers['insert'].handle, modeman.manager.register('insert', self._keyparsers['insert'].handle,
passthrough=True) passthrough=True)
modes.manager.register('passthrough', modeman.manager.register('passthrough',
self._keyparsers['passthrough'].handle, self._keyparsers['passthrough'].handle,
passthrough=True) passthrough=True)
modes.manager.register('command', self._keyparsers['command'].handle, modeman.manager.register('command', self._keyparsers['command'].handle,
passthrough=True) passthrough=True)
self.modeman = modes.manager # for commands self.modeman = modeman.manager # for commands
self.installEventFilter(modes.manager) self.installEventFilter(modeman.manager)
self.setQuitOnLastWindowClosed(False) self.setQuitOnLastWindowClosed(False)
self._connect_signals() self._connect_signals()
modes.enter("normal") modeman.enter("normal")
self.mainwindow.show() self.mainwindow.show()
self._python_hacks() self._python_hacks()
@ -263,10 +262,10 @@ class QuteBrowser(QApplication):
tabs.currentChanged.connect(self.mainwindow.update_inspector) tabs.currentChanged.connect(self.mainwindow.update_inspector)
# status bar # status bar
modes.manager.entered.connect(status.on_mode_entered) modeman.manager.entered.connect(status.on_mode_entered)
modes.manager.left.connect(status.on_mode_left) modeman.manager.left.connect(status.on_mode_left)
modes.manager.left.connect(status.cmd.on_mode_left) modeman.manager.left.connect(status.cmd.on_mode_left)
modes.manager.key_pressed.connect(status.on_key_pressed) modeman.manager.key_pressed.connect(status.on_key_pressed)
# commands # commands
cmd.got_cmd.connect(self.commandmanager.run) cmd.got_cmd.connect(self.commandmanager.run)
@ -290,7 +289,7 @@ class QuteBrowser(QApplication):
# config # config
self.config.style_changed.connect(style.invalidate_caches) self.config.style_changed.connect(style.invalidate_caches)
for obj in [tabs, completion, self.mainwindow, config.cmd_history, for obj in [tabs, completion, self.mainwindow, config.cmd_history,
websettings, kp["normal"], modes.manager]: websettings, kp["normal"], modeman.manager]:
self.config.changed.connect(obj.on_config_changed) self.config.changed.connect(obj.on_config_changed)
# statusbar # statusbar
@ -304,7 +303,7 @@ class QuteBrowser(QApplication):
tabs.cur_link_hovered.connect(status.url.set_hover_url) tabs.cur_link_hovered.connect(status.url.set_hover_url)
# command input / completion # command input / completion
modes.manager.left.connect(tabs.on_mode_left) modeman.manager.left.connect(tabs.on_mode_left)
cmd.clear_completion_selection.connect( cmd.clear_completion_selection.connect(
completion.on_clear_completion_selection) completion.on_clear_completion_selection)
cmd.hide_completion.connect(completion.hide) cmd.hide_completion.connect(completion.hide)

View File

@ -26,7 +26,7 @@ from PyQt5.QtGui import QMouseEvent, QClipboard
from PyQt5.QtWidgets import QApplication from PyQt5.QtWidgets import QApplication
import qutebrowser.config.config as config import qutebrowser.config.config as config
import qutebrowser.keyinput.modes as modes import qutebrowser.keyinput.modeman as modeman
import qutebrowser.utils.message as message import qutebrowser.utils.message as message
import qutebrowser.utils.url as urlutils import qutebrowser.utils.url as urlutils
import qutebrowser.utils.webelem as webelem import qutebrowser.utils.webelem as webelem
@ -91,7 +91,7 @@ class HintManager(QObject):
self._frame = None self._frame = None
self._target = None self._target = None
self._baseurl = None self._baseurl = None
modes.manager.left.connect(self.on_mode_left) modeman.manager.left.connect(self.on_mode_left)
def _hint_strings(self, elems): def _hint_strings(self, elems):
"""Calculate the hint strings for elems. """Calculate the hint strings for elems.
@ -301,7 +301,7 @@ class HintManager(QObject):
self._elems[string] = ElemTuple(e, label) self._elems[string] = ElemTuple(e, label)
frame.contentsSizeChanged.connect(self.on_contents_size_changed) frame.contentsSizeChanged.connect(self.on_contents_size_changed)
self.hint_strings_updated.emit(strings) self.hint_strings_updated.emit(strings)
modes.enter("hint") modeman.enter("hint")
def handle_partial_key(self, keystr): def handle_partial_key(self, keystr):
"""Handle a new partial keypress.""" """Handle a new partial keypress."""
@ -344,7 +344,7 @@ class HintManager(QObject):
message.set_cmd_text(':{} {}'.format(commands[self._target], message.set_cmd_text(':{} {}'.format(commands[self._target],
urlutils.urlstring(link))) urlutils.urlstring(link)))
if self._target != 'rapid': if self._target != 'rapid':
modes.leave("hint") modeman.leave("hint")
@pyqtSlot('QSize') @pyqtSlot('QSize')
def on_contents_size_changed(self, _size): def on_contents_size_changed(self, _size):

View File

@ -82,7 +82,7 @@ class Command(QObject):
ArgumentCountError if the argument count is wrong. ArgumentCountError if the argument count is wrong.
InvalidModeError if the command can't be called in this mode. InvalidModeError if the command can't be called in this mode.
""" """
import qutebrowser.keyinput.modes as modeman import qutebrowser.keyinput.modeman as modeman
if self.modes is not None and modeman.manager.mode not in self.modes: if self.modes is not None and modeman.manager.mode not in self.modes:
raise InvalidModeError("This command is only allowed in {} " raise InvalidModeError("This command is only allowed in {} "
"mode.".format('/'.join(self.modes))) "mode.".format('/'.join(self.modes)))

View File

@ -0,0 +1,345 @@
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser 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.
#
# qutebrowser 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 qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Base class for vim-like keysequence parser."""
import re
import logging
from functools import partial
from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QObject, QTimer
from PyQt5.QtGui import QKeySequence
import qutebrowser.config.config as config
class BaseKeyParser(QObject):
"""Parser for vim-like key sequences and shortcuts.
Not intended to be instantiated directly. Subclasses have to override
execute() to do whatever they want to.
Class Attributes:
MATCH_PARTIAL: Constant for a partial match (no keychain matched yet,
but it's still possible in the future.
MATCH_DEFINITIVE: Constant for a full match (keychain matches exactly).
MATCH_AMBIGUOUS: There are both a partial and a definitive match.
MATCH_NONE: Constant for no match (no more matches possible).
TYPE_CHAIN: execute() was called via a chain-like keybinding
TYPE_SPECIAL: execute() was called via a special keybinding
Attributes:
bindings: Bound keybindings
special_bindings: Bound special bindings (<Foo>).
_keystring: The currently entered key sequence
_timer: QTimer for delayed execution.
_confsectname: The name of the configsection.
_supports_count: Whether count is supported
_supports_chains: Whether keychains are supported
Signals:
keystring_updated: Emitted when the keystring is updated.
arg: New keystring.
"""
keystring_updated = pyqtSignal(str)
MATCH_PARTIAL = 0
MATCH_DEFINITIVE = 1
MATCH_AMBIGUOUS = 2
MATCH_NONE = 3
TYPE_CHAIN = 0
TYPE_SPECIAL = 1
def __init__(self, parent=None, supports_count=None,
supports_chains=False):
super().__init__(parent)
self._timer = None
self._confsectname = None
self._keystring = ''
if supports_count is None:
supports_count = supports_chains
self._supports_count = supports_count
self._supports_chains = supports_chains
self.bindings = {}
self.special_bindings = {}
def _normalize_keystr(self, keystr):
"""Normalize a keystring like Ctrl-Q to a keystring like Ctrl+Q.
Args:
keystr: The key combination as a string.
Return:
The normalized keystring.
"""
replacements = [
('Control', 'Ctrl'),
('Windows', 'Meta'),
('Mod1', 'Alt'),
('Mod4', 'Meta'),
]
for (orig, repl) in replacements:
keystr = keystr.replace(orig, repl)
for mod in ['Ctrl', 'Meta', 'Alt', 'Shift']:
keystr = keystr.replace(mod + '-', mod + '+')
keystr = QKeySequence(keystr).toString()
return keystr
def _handle_special_key(self, e):
"""Handle a new keypress with special keys (<Foo>).
Return True if the keypress has been handled, and False if not.
Args:
e: the KeyPressEvent from Qt.
Return:
True if event has been handled, False otherwise.
"""
modmask2str = {
Qt.ControlModifier: 'Ctrl',
Qt.AltModifier: 'Alt',
Qt.MetaModifier: 'Meta',
Qt.ShiftModifier: 'Shift'
}
if e.key() in [Qt.Key_Control, Qt.Key_Alt, Qt.Key_Shift, Qt.Key_Meta]:
# Only modifier pressed
return False
mod = e.modifiers()
modstr = ''
for (mask, s) in modmask2str.items():
if mod & mask:
modstr += s + '+'
keystr = QKeySequence(e.key()).toString().replace("Backtab", "Tab")
try:
cmdstr = self.special_bindings[modstr + keystr]
except KeyError:
logging.debug('No binding found for {}.'.format(modstr + keystr))
return False
self.execute(cmdstr, self.TYPE_SPECIAL)
return True
def _handle_single_key(self, e):
"""Handle a new keypress with a single key (no modifiers).
Separate the keypress into count/command, then check if it matches
any possible command, and either run the command, ignore it, or
display an error.
Args:
e: the KeyPressEvent from Qt.
Return:
True if event has been handled, False otherwise.
"""
logging.debug('Got key: {} / text: "{}"'.format(e.key(), e.text()))
txt = e.text().strip()
if not txt:
logging.debug('Ignoring, no text')
return False
self._stop_delayed_exec()
self._keystring += txt
if self._supports_count:
(countstr, cmd_input) = re.match(r'^(\d*)(.*)',
self._keystring).groups()
count = int(countstr) if countstr else None
else:
cmd_input = self._keystring
count = None
if not cmd_input:
# Only a count, no command yet, but we handled it
return True
(match, binding) = self._match_key(cmd_input)
if match == self.MATCH_DEFINITIVE:
self._keystring = ''
self.execute(binding, self.TYPE_CHAIN, count)
elif match == self.MATCH_AMBIGUOUS:
self._handle_ambiguous_match(binding, count)
elif match == self.MATCH_PARTIAL:
logging.debug('No match for "{}" (added {})'.format(
self._keystring, txt))
elif match == self.MATCH_NONE:
logging.debug('Giving up with "{}", no matches'.format(
self._keystring))
self._keystring = ''
return False
return True
def _match_key(self, cmd_input):
"""Try to match a given keystring with any bound keychain.
Args:
cmd_input: The command string to find.
Return:
A tuple (matchtype, binding).
matchtype: MATCH_DEFINITIVE, MATCH_AMBIGUOUS, MATCH_PARTIAL or
MATCH_NONE
binding: - None with MATCH_PARTIAL/MATCH_NONE
- The found binding with MATCH_DEFINITIVE/
MATCH_AMBIGUOUS
"""
# A (cmd_input, binding) tuple (k, v of bindings) or None.
definitive_match = None
partial_match = False
# Check definitive match
try:
definitive_match = (cmd_input, self.bindings[cmd_input])
except KeyError:
pass
# Check partial match
for binding in self.bindings:
if definitive_match is not None and binding == definitive_match[0]:
# We already matched that one
continue
if len(binding) < len(cmd_input):
# binding is shorter than cmd_input, so it can't possibly match
continue
elif cmd_input[-1] == binding[len(cmd_input) - 1]:
partial_match = True
break
if definitive_match is not None and partial_match:
return (self.MATCH_AMBIGUOUS, definitive_match[1])
elif definitive_match is not None:
return (self.MATCH_DEFINITIVE, definitive_match[1])
elif partial_match:
return (self.MATCH_PARTIAL, None)
else:
return (self.MATCH_NONE, None)
def _stop_delayed_exec(self):
"""Stop a delayed execution if any is running."""
if self._timer is not None:
logging.debug("Stopping delayed execution.")
self._timer.stop()
self._timer = None
def _handle_ambiguous_match(self, binding, count):
"""Handle an ambiguous match.
Args:
binding: The command-string to execute.
count: The count to pass.
"""
logging.debug("Ambiguous match for \"{}\"".format(self._keystring))
time = config.get('general', 'cmd_timeout')
if time == 0:
# execute immediately
self._keystring = ''
self.execute(binding, self.TYPE_CHAIN, count)
else:
# execute in `time' ms
logging.debug("Scheduling execution of {} in {}ms".format(binding,
time))
self._timer = QTimer(self)
self._timer.setSingleShot(True)
self._timer.setInterval(time)
self._timer.timeout.connect(partial(self.delayed_exec, binding,
count))
self._timer.start()
def delayed_exec(self, command, count):
"""Execute a delayed command.
Args:
command/count: As if passed to self.execute()
Emit:
keystring_updated to do a delayed update.
"""
logging.debug("Executing delayed command now!")
self._timer = None
self._keystring = ''
self.keystring_updated.emit(self._keystring)
self.execute(command, self.TYPE_CHAIN, count)
def handle(self, e):
"""Handle a new keypress and call the respective handlers.
Args:
e: the KeyPressEvent from Qt
Emit:
keystring_updated: If a new keystring should be set.
"""
handled = self._handle_special_key(e)
if handled or not self._supports_chains:
return handled
handled = self._handle_single_key(e)
self.keystring_updated.emit(self._keystring)
return handled
def read_config(self, sectname=None):
"""Read the configuration.
Config format: key = command, e.g.:
<Ctrl+Q> = quit
Args:
sectname: Name of the section to read.
"""
if sectname is None:
if self._confsectname is None:
raise ValueError("read_config called with no section, but "
"None defined so far!")
sectname = self._confsectname
else:
self._confsectname = sectname
sect = config.instance[sectname]
if not sect.items():
logging.warn("No keybindings defined!")
for (key, cmd) in sect.items():
if key.startswith('<') and key.endswith('>'):
keystr = self._normalize_keystr(key[1:-1])
logging.debug("registered special key: {} -> {}".format(keystr,
cmd))
self.special_bindings[keystr] = cmd
elif self._supports_chains:
logging.debug("registered key: {} -> {}".format(key, cmd))
self.bindings[key] = cmd
else:
logging.warn(
"Ignoring keychain \"{}\" in section \"{}\" because "
"keychains are not supported there.".format(key, sectname))
def execute(self, cmdstr, keytype, count=None):
"""Handle a completed keychain.
Args:
cmdstr: The command to execute as a string.
keytype: TYPE_CHAIN or TYPE_SPECIAL
count: The count if given.
"""
raise NotImplementedError
@pyqtSlot(str, str)
def on_config_changed(self, section, _option):
"""Re-read the config if a keybinding was changed."""
if self._confsectname is None:
raise AttributeError("on_config_changed called but no section "
"defined!")
if section == self._confsectname:
self.read_config()

View File

@ -15,340 +15,18 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>. # along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Base class for vim-like keysequence parser.""" """Advanced keyparsers."""
import re
import logging import logging
from functools import partial
from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QObject, QTimer from qutebrowser.keyinput._basekeyparser import BaseKeyParser
from PyQt5.QtGui import QKeySequence
import qutebrowser.config.config as config
import qutebrowser.utils.message as message import qutebrowser.utils.message as message
from qutebrowser.commands.managers import (CommandManager, ArgumentCountError, from qutebrowser.commands.managers import (CommandManager, ArgumentCountError,
NoSuchCommandError) NoSuchCommandError)
class KeyParser(QObject): class CommandKeyParser(BaseKeyParser):
"""Parser for vim-like key sequences and shortcuts.
Not intended to be instantiated directly. Subclasses have to override
execute() to do whatever they want to.
Class Attributes:
MATCH_PARTIAL: Constant for a partial match (no keychain matched yet,
but it's still possible in the future.
MATCH_DEFINITIVE: Constant for a full match (keychain matches exactly).
MATCH_AMBIGUOUS: There are both a partial and a definitive match.
MATCH_NONE: Constant for no match (no more matches possible).
TYPE_CHAIN: execute() was called via a chain-like keybinding
TYPE_SPECIAL: execute() was called via a special keybinding
Attributes:
bindings: Bound keybindings
special_bindings: Bound special bindings (<Foo>).
_keystring: The currently entered key sequence
_timer: QTimer for delayed execution.
_confsectname: The name of the configsection.
_supports_count: Whether count is supported
_supports_chains: Whether keychains are supported
Signals:
keystring_updated: Emitted when the keystring is updated.
arg: New keystring.
"""
keystring_updated = pyqtSignal(str)
MATCH_PARTIAL = 0
MATCH_DEFINITIVE = 1
MATCH_AMBIGUOUS = 2
MATCH_NONE = 3
TYPE_CHAIN = 0
TYPE_SPECIAL = 1
def __init__(self, parent=None, supports_count=None,
supports_chains=False):
super().__init__(parent)
self._timer = None
self._confsectname = None
self._keystring = ''
if supports_count is None:
supports_count = supports_chains
self._supports_count = supports_count
self._supports_chains = supports_chains
self.bindings = {}
self.special_bindings = {}
def _normalize_keystr(self, keystr):
"""Normalize a keystring like Ctrl-Q to a keystring like Ctrl+Q.
Args:
keystr: The key combination as a string.
Return:
The normalized keystring.
"""
replacements = [
('Control', 'Ctrl'),
('Windows', 'Meta'),
('Mod1', 'Alt'),
('Mod4', 'Meta'),
]
for (orig, repl) in replacements:
keystr = keystr.replace(orig, repl)
for mod in ['Ctrl', 'Meta', 'Alt', 'Shift']:
keystr = keystr.replace(mod + '-', mod + '+')
keystr = QKeySequence(keystr).toString()
return keystr
def _handle_special_key(self, e):
"""Handle a new keypress with special keys (<Foo>).
Return True if the keypress has been handled, and False if not.
Args:
e: the KeyPressEvent from Qt.
Return:
True if event has been handled, False otherwise.
"""
modmask2str = {
Qt.ControlModifier: 'Ctrl',
Qt.AltModifier: 'Alt',
Qt.MetaModifier: 'Meta',
Qt.ShiftModifier: 'Shift'
}
if e.key() in [Qt.Key_Control, Qt.Key_Alt, Qt.Key_Shift, Qt.Key_Meta]:
# Only modifier pressed
return False
mod = e.modifiers()
modstr = ''
for (mask, s) in modmask2str.items():
if mod & mask:
modstr += s + '+'
keystr = QKeySequence(e.key()).toString().replace("Backtab", "Tab")
try:
cmdstr = self.special_bindings[modstr + keystr]
except KeyError:
logging.debug('No binding found for {}.'.format(modstr + keystr))
return False
self.execute(cmdstr, self.TYPE_SPECIAL)
return True
def _handle_single_key(self, e):
"""Handle a new keypress with a single key (no modifiers).
Separate the keypress into count/command, then check if it matches
any possible command, and either run the command, ignore it, or
display an error.
Args:
e: the KeyPressEvent from Qt.
Return:
True if event has been handled, False otherwise.
"""
logging.debug('Got key: {} / text: "{}"'.format(e.key(), e.text()))
txt = e.text().strip()
if not txt:
logging.debug('Ignoring, no text')
return False
self._stop_delayed_exec()
self._keystring += txt
if self._supports_count:
(countstr, cmd_input) = re.match(r'^(\d*)(.*)',
self._keystring).groups()
count = int(countstr) if countstr else None
else:
cmd_input = self._keystring
count = None
if not cmd_input:
# Only a count, no command yet, but we handled it
return True
(match, binding) = self._match_key(cmd_input)
if match == self.MATCH_DEFINITIVE:
self._keystring = ''
self.execute(binding, self.TYPE_CHAIN, count)
elif match == self.MATCH_AMBIGUOUS:
self._handle_ambiguous_match(binding, count)
elif match == self.MATCH_PARTIAL:
logging.debug('No match for "{}" (added {})'.format(
self._keystring, txt))
elif match == self.MATCH_NONE:
logging.debug('Giving up with "{}", no matches'.format(
self._keystring))
self._keystring = ''
return False
return True
def _match_key(self, cmd_input):
"""Try to match a given keystring with any bound keychain.
Args:
cmd_input: The command string to find.
Return:
A tuple (matchtype, binding).
matchtype: MATCH_DEFINITIVE, MATCH_AMBIGUOUS, MATCH_PARTIAL or
MATCH_NONE
binding: - None with MATCH_PARTIAL/MATCH_NONE
- The found binding with MATCH_DEFINITIVE/
MATCH_AMBIGUOUS
"""
# A (cmd_input, binding) tuple (k, v of bindings) or None.
definitive_match = None
partial_match = False
# Check definitive match
try:
definitive_match = (cmd_input, self.bindings[cmd_input])
except KeyError:
pass
# Check partial match
for binding in self.bindings:
if definitive_match is not None and binding == definitive_match[0]:
# We already matched that one
continue
if len(binding) < len(cmd_input):
# binding is shorter than cmd_input, so it can't possibly match
continue
elif cmd_input[-1] == binding[len(cmd_input) - 1]:
partial_match = True
break
if definitive_match is not None and partial_match:
return (self.MATCH_AMBIGUOUS, definitive_match[1])
elif definitive_match is not None:
return (self.MATCH_DEFINITIVE, definitive_match[1])
elif partial_match:
return (self.MATCH_PARTIAL, None)
else:
return (self.MATCH_NONE, None)
def _stop_delayed_exec(self):
"""Stop a delayed execution if any is running."""
if self._timer is not None:
logging.debug("Stopping delayed execution.")
self._timer.stop()
self._timer = None
def _handle_ambiguous_match(self, binding, count):
"""Handle an ambiguous match.
Args:
binding: The command-string to execute.
count: The count to pass.
"""
logging.debug("Ambiguous match for \"{}\"".format(self._keystring))
time = config.get('general', 'cmd_timeout')
if time == 0:
# execute immediately
self._keystring = ''
self.execute(binding, self.TYPE_CHAIN, count)
else:
# execute in `time' ms
logging.debug("Scheduling execution of {} in {}ms".format(binding,
time))
self._timer = QTimer(self)
self._timer.setSingleShot(True)
self._timer.setInterval(time)
self._timer.timeout.connect(partial(self.delayed_exec, binding,
count))
self._timer.start()
def delayed_exec(self, command, count):
"""Execute a delayed command.
Args:
command/count: As if passed to self.execute()
Emit:
keystring_updated to do a delayed update.
"""
logging.debug("Executing delayed command now!")
self._timer = None
self._keystring = ''
self.keystring_updated.emit(self._keystring)
self.execute(command, self.TYPE_CHAIN, count)
def handle(self, e):
"""Handle a new keypress and call the respective handlers.
Args:
e: the KeyPressEvent from Qt
Emit:
keystring_updated: If a new keystring should be set.
"""
handled = self._handle_special_key(e)
if handled or not self._supports_chains:
return handled
handled = self._handle_single_key(e)
self.keystring_updated.emit(self._keystring)
return handled
def read_config(self, sectname=None):
"""Read the configuration.
Config format: key = command, e.g.:
<Ctrl+Q> = quit
Args:
sectname: Name of the section to read.
"""
if sectname is None:
if self._confsectname is None:
raise ValueError("read_config called with no section, but "
"None defined so far!")
sectname = self._confsectname
else:
self._confsectname = sectname
sect = config.instance[sectname]
if not sect.items():
logging.warn("No keybindings defined!")
for (key, cmd) in sect.items():
if key.startswith('<') and key.endswith('>'):
keystr = self._normalize_keystr(key[1:-1])
logging.debug("registered special key: {} -> {}".format(keystr,
cmd))
self.special_bindings[keystr] = cmd
elif self._supports_chains:
logging.debug("registered key: {} -> {}".format(key, cmd))
self.bindings[key] = cmd
else:
logging.warn(
"Ignoring keychain \"{}\" in section \"{}\" because "
"keychains are not supported there.".format(key, sectname))
def execute(self, cmdstr, keytype, count=None):
"""Handle a completed keychain.
Args:
cmdstr: The command to execute as a string.
keytype: TYPE_CHAIN or TYPE_SPECIAL
count: The count if given.
"""
raise NotImplementedError
@pyqtSlot(str, str)
def on_config_changed(self, section, _option):
"""Re-read the config if a keybinding was changed."""
if self._confsectname is None:
raise AttributeError("on_config_changed called but no section "
"defined!")
if section == self._confsectname:
self.read_config()
class CommandKeyParser(KeyParser):
"""KeyChainParser for command bindings. """KeyChainParser for command bindings.

View File

@ -15,13 +15,45 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>. # along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""KeyChainParser for "hint" mode.""" """KeyChainParser for "hint" and "normal" modes.
Module attributes:
STARTCHARS: Possible chars for starting a commandline input.
"""
from PyQt5.QtCore import pyqtSignal from PyQt5.QtCore import pyqtSignal
import qutebrowser.utils.message as message
from qutebrowser.keyinput.keyparser import CommandKeyParser from qutebrowser.keyinput.keyparser import CommandKeyParser
STARTCHARS = ":/?"
class NormalKeyParser(CommandKeyParser):
"""KeyParser for normalmode with added STARTCHARS detection."""
def __init__(self, parent=None):
super().__init__(parent, supports_count=True, supports_chains=True)
self.read_config('keybind')
def _handle_single_key(self, e):
"""Override _handle_single_key to abort if the key is a startchar.
Args:
e: the KeyPressEvent from Qt.
Return:
True if event has been handled, False otherwise.
"""
txt = e.text().strip()
if not self._keystring and any(txt == c for c in STARTCHARS):
message.set_cmd_text(txt)
return True
return super()._handle_single_key(e)
class HintKeyParser(CommandKeyParser): class HintKeyParser(CommandKeyParser):
"""KeyChainParser for hints. """KeyChainParser for hints.

View File

@ -1,51 +0,0 @@
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser 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.
#
# qutebrowser 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 qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Parse keypresses/keychains in the main window.
Module attributes:
STARTCHARS: Possible chars for starting a commandline input.
"""
import qutebrowser.utils.message as message
from qutebrowser.keyinput.keyparser import CommandKeyParser
STARTCHARS = ":/?"
class NormalKeyParser(CommandKeyParser):
"""KeyParser for normalmode with added STARTCHARS detection."""
def __init__(self, parent=None):
super().__init__(parent, supports_count=True, supports_chains=True)
self.read_config('keybind')
def _handle_single_key(self, e):
"""Override _handle_single_key to abort if the key is a startchar.
Args:
e: the KeyPressEvent from Qt.
Return:
True if event has been handled, False otherwise.
"""
txt = e.text().strip()
if not self._keystring and any(txt == c for c in STARTCHARS):
message.set_cmd_text(txt)
return True
return super()._handle_single_key(e)

View File

@ -22,9 +22,9 @@ from PyQt5.QtWidgets import (QWidget, QLineEdit, QProgressBar, QLabel,
QHBoxLayout, QStackedLayout, QSizePolicy) QHBoxLayout, QStackedLayout, QSizePolicy)
from PyQt5.QtGui import QPainter, QValidator from PyQt5.QtGui import QPainter, QValidator
import qutebrowser.keyinput.modes as modes import qutebrowser.keyinput.modeman as modeman
import qutebrowser.commands.utils as cmdutils import qutebrowser.commands.utils as cmdutils
from qutebrowser.keyinput.normalmode import STARTCHARS from qutebrowser.keyinput.modeparsers import STARTCHARS
from qutebrowser.config.style import set_register_stylesheet, get_stylesheet from qutebrowser.config.style import set_register_stylesheet, get_stylesheet
from qutebrowser.utils.url import urlstring from qutebrowser.utils.url import urlstring
from qutebrowser.commands.managers import split_cmdline from qutebrowser.commands.managers import split_cmdline
@ -173,13 +173,13 @@ class StatusBar(QWidget):
@pyqtSlot(str) @pyqtSlot(str)
def on_mode_entered(self, mode): def on_mode_entered(self, mode):
"""Mark certain modes in the commandline.""" """Mark certain modes in the commandline."""
if mode in modes.manager.passthrough: if mode in modeman.manager.passthrough:
self.txt.normaltext = "-- {} MODE --".format(mode.upper()) self.txt.normaltext = "-- {} MODE --".format(mode.upper())
@pyqtSlot(str) @pyqtSlot(str)
def on_mode_left(self, mode): def on_mode_left(self, mode):
"""Clear marked mode.""" """Clear marked mode."""
if mode in modes.manager.passthrough: if mode in modeman.manager.passthrough:
self.txt.normaltext = "" self.txt.normaltext = ""
def resizeEvent(self, e): def resizeEvent(self, e):
@ -333,7 +333,7 @@ class _Command(QLineEdit):
} }
text = self.text() text = self.text()
self.history.append(text) self.history.append(text)
modes.leave("command") modeman.leave("command")
if text[0] in signals: if text[0] in signals:
signals[text[0]].emit(text.lstrip(text[0])) signals[text[0]].emit(text.lstrip(text[0]))
@ -360,7 +360,7 @@ class _Command(QLineEdit):
def focusInEvent(self, e): def focusInEvent(self, e):
"""Extend focusInEvent to enter command mode.""" """Extend focusInEvent to enter command mode."""
modes.enter("command") modeman.enter("command")
super().focusInEvent(e) super().focusInEvent(e)

View File

@ -27,7 +27,7 @@ from PyQt5.QtWebKitWidgets import QWebView, QWebPage
import qutebrowser.utils.url as urlutils import qutebrowser.utils.url as urlutils
import qutebrowser.config.config as config import qutebrowser.config.config as config
import qutebrowser.keyinput.modes as modes import qutebrowser.keyinput.modeman as modeman
import qutebrowser.utils.message as message import qutebrowser.utils.message as message
import qutebrowser.utils.webelem as webelem import qutebrowser.utils.webelem as webelem
from qutebrowser.browser.webpage import BrowserPage from qutebrowser.browser.webpage import BrowserPage
@ -86,7 +86,7 @@ class BrowserTab(QWebView):
self.page_.setLinkDelegationPolicy(QWebPage.DelegateAllLinks) self.page_.setLinkDelegationPolicy(QWebPage.DelegateAllLinks)
self.page_.linkHovered.connect(self.linkHovered) self.page_.linkHovered.connect(self.linkHovered)
self.linkClicked.connect(self.on_link_clicked) self.linkClicked.connect(self.on_link_clicked)
self.loadStarted.connect(lambda: modes.maybe_leave("insert")) self.loadStarted.connect(lambda: modeman.maybe_leave("insert"))
self.loadFinished.connect(self.on_load_finished) self.loadFinished.connect(self.on_load_finished)
# FIXME find some way to hide scrollbars without setScrollBarPolicy # FIXME find some way to hide scrollbars without setScrollBarPolicy
@ -255,9 +255,9 @@ class BrowserTab(QWebView):
webelem.SELECTORS['editable_focused']) webelem.SELECTORS['editable_focused'])
logging.debug("focus element: {}".format(not elem.isNull())) logging.debug("focus element: {}".format(not elem.isNull()))
if elem.isNull(): if elem.isNull():
modes.maybe_leave("insert") modeman.maybe_leave("insert")
else: else:
modes.enter("insert") modeman.enter("insert")
@pyqtSlot(str) @pyqtSlot(str)
def set_force_open_target(self, target): def set_force_open_target(self, target):
@ -319,11 +319,11 @@ class BrowserTab(QWebView):
hitresult = frame.hitTestContent(pos) hitresult = frame.hitTestContent(pos)
if self._is_editable(hitresult): if self._is_editable(hitresult):
logging.debug("Clicked editable element!") logging.debug("Clicked editable element!")
modes.enter("insert") modeman.enter("insert")
else: else:
logging.debug("Clicked non-editable element!") logging.debug("Clicked non-editable element!")
try: try:
modes.leave("insert") modeman.leave("insert")
except ValueError: except ValueError:
pass pass