Merge branch 'keyboard'

Conflicts:
	TODO
This commit is contained in:
Florian Bruhin 2014-04-25 06:57:57 +02:00
commit d4baf77f78
18 changed files with 807 additions and 391 deletions

1
THANKS
View File

@ -54,6 +54,7 @@ channels:
- scummos
- svuorela
- kpj
- hyde
Thanks to these projects which were essential while developing qutebrowser:
- Python

15
TODO
View File

@ -14,7 +14,6 @@ Style
=====
Refactor completion widget mess (initializing / changing completions)
keypress-signal-foo is a bit of a chaos and might be done better
we probably could replace CompletionModel with QStandardModel...
replace foo_bar options with foo-bar
reorder options
@ -52,13 +51,6 @@ Before Blink
session handling / saving
IPC, like dwb -x
Mode handling?
- Problem: how to detect we're going to insert mode:
-> Detect mouse clicks and use QWebFrame::hitTestContent (only mouse)
-> Use javascript: http://stackoverflow.com/a/2848120/2085149
-> Use microFocusChanged and check active element via:
frame = page.currentFrame()
elem = frame.findFirstElement('*:focus')
Bookmarks
Internationalization
Marks
@ -89,6 +81,13 @@ How do we handle empty values in input bar?
Human-readable error messages for unknown settings / wrong interpolations / ...
auto-click on active hint
click hints on enter
- Add mode=[]/no_mode=[] to cmdutils.register so we can avoid executing
commands in the wrong mode
- Add more element-selection-detection code (with options?) based on:
-> javascript: http://stackoverflow.com/a/2848120/2085149
-> microFocusChanged and check active element via:
frame = page.currentFrame()
elem = frame.findFirstElement('*:focus')
Qt Bugs
========

View File

@ -52,12 +52,14 @@ import qutebrowser.commands.utils as cmdutils
import qutebrowser.config.style as style
import qutebrowser.config.config as config
import qutebrowser.network.qutescheme as qutescheme
import qutebrowser.keyinput.modes as modes
import qutebrowser.utils.message as message
from qutebrowser.widgets.mainwindow import MainWindow
from qutebrowser.widgets.crash import CrashDialog
from qutebrowser.commands.keys import CommandKeyParser
from qutebrowser.keyinput.normalmode import NormalKeyParser
from qutebrowser.keyinput.insertmode import InsertKeyParser
from qutebrowser.keyinput.hintmode import HintKeyParser
from qutebrowser.commands.parsers import CommandParser, SearchParser
from qutebrowser.browser.hints import HintKeyParser
from qutebrowser.utils.appdirs import AppDirs
from qutebrowser.utils.misc import dotted_getattr
from qutebrowser.utils.debug import set_trace # pylint: disable=unused-import
@ -83,7 +85,6 @@ class QuteBrowser(QApplication):
_timers: List of used QTimers so they don't get GCed.
_shutting_down: True if we're currently shutting down.
_quit_status: The current quitting status.
_mode: The mode we're currently in.
_opened_urls: List of opened URLs.
"""
@ -93,7 +94,6 @@ class QuteBrowser(QApplication):
self._timers = []
self._opened_urls = []
self._shutting_down = False
self._mode = None
sys.excepthook = self._exception_hook
@ -125,15 +125,24 @@ class QuteBrowser(QApplication):
self.commandparser = CommandParser()
self.searchparser = SearchParser()
self._keyparsers = {
"normal": CommandKeyParser(self),
"hint": HintKeyParser(self),
'normal': NormalKeyParser(self),
'hint': HintKeyParser(self),
'insert': InsertKeyParser(self),
}
self._init_cmds()
self.mainwindow = MainWindow()
modes.init(self)
modes.manager.register('normal', self._keyparsers['normal'].handle)
modes.manager.register('hint', self._keyparsers['hint'].handle)
modes.manager.register('insert', self._keyparsers['insert'].handle,
passthrough=True)
modes.manager.register('command', None, passthrough=True)
self.modeman = modes.manager # for commands
self.installEventFilter(modes.manager)
self.setQuitOnLastWindowClosed(False)
self._connect_signals()
self.set_mode("normal")
modes.enter("normal")
self.mainwindow.show()
self._python_hacks()
@ -246,13 +255,12 @@ class QuteBrowser(QApplication):
# misc
self.lastWindowClosed.connect(self.shutdown)
tabs.quit.connect(self.shutdown)
tabs.set_mode.connect(self.set_mode)
tabs.currentChanged.connect(self.mainwindow.update_inspector)
# status bar
tabs.keypress.connect(status.keypress)
for obj in [kp["normal"], tabs]:
obj.set_cmd_text.connect(cmd.set_cmd_text)
modes.manager.entered.connect(status.on_mode_entered)
modes.manager.left.connect(status.on_mode_left)
modes.manager.key_pressed.connect(status.on_key_pressed)
# commands
cmd.got_cmd.connect(self.commandparser.run)
@ -264,7 +272,6 @@ class QuteBrowser(QApplication):
# hints
kp["hint"].fire_hint.connect(tabs.cur.fire_hint)
kp["hint"].abort_hinting.connect(tabs.cur.abort_hinting)
kp["hint"].keystring_updated.connect(tabs.cur.handle_hint_key)
tabs.hint_strings_updated.connect(kp["hint"].on_hint_strings_updated)
@ -272,6 +279,7 @@ class QuteBrowser(QApplication):
message.bridge.error.connect(status.disp_error)
message.bridge.info.connect(status.txt.set_temptext)
message.bridge.text.connect(status.txt.set_normaltext)
message.bridge.set_cmd_text.connect(cmd.set_cmd_text)
# config
self.config.style_changed.connect(style.invalidate_caches)
@ -401,20 +409,6 @@ class QuteBrowser(QApplication):
logging.debug("maybe_quit quitting.")
self.quit()
@pyqtSlot(str)
def set_mode(self, mode):
"""Set a key input mode.
Args:
mode: The new mode to set, as an index for self._keyparsers.
"""
if self._mode is not None:
oldhandler = self._keyparsers[self._mode]
self.mainwindow.tabs.keypress.disconnect(oldhandler.handle)
handler = self._keyparsers[mode]
self.mainwindow.tabs.keypress.connect(handler.handle)
self._mode = mode
@cmdutils.register(instance='', maxsplit=0)
def pyeval(self, s):
"""Evaluate a python string and display the results as a webpage.

View File

@ -207,11 +207,6 @@ class CurCommandDispatcher(QObject):
"""Fire a completed hint."""
self._tabs.currentWidget().hintmanager.fire(keystr)
@pyqtSlot()
def abort_hinting(self):
"""Abort hinting."""
self._tabs.currentWidget().hintmanager.stop()
@pyqtSlot(str, int)
def search(self, text, flags):
"""Search for text in the current page.

View File

@ -26,69 +26,20 @@ from PyQt5.QtGui import QMouseEvent, QClipboard
from PyQt5.QtWidgets import QApplication
import qutebrowser.config.config as config
import qutebrowser.keyinput.modes as modes
import qutebrowser.utils.message as message
import qutebrowser.utils.url as urlutils
from qutebrowser.utils.keyparser import KeyParser
import qutebrowser.utils.webelem as webelem
ElemTuple = namedtuple('ElemTuple', 'elem, label')
class HintKeyParser(KeyParser):
"""KeyParser for hints.
Class attributes:
supports_count: If the keyparser should support counts.
Signals:
fire_hint: When a hint keybinding was completed.
Arg: the keystring/hint string pressed.
abort_hinting: Esc pressed, so abort hinting.
"""
supports_count = False
fire_hint = pyqtSignal(str)
abort_hinting = pyqtSignal()
def _handle_modifier_key(self, e):
"""We don't support modifiers here, but we'll handle escape in here.
Emit:
abort_hinting: Emitted if hinting was aborted.
"""
if e.key() == Qt.Key_Escape:
self._keystring = ''
self.abort_hinting.emit()
return True
return False
def execute(self, cmdstr, count=None):
"""Handle a completed keychain.
Emit:
fire_hint: Always emitted.
"""
self.fire_hint.emit(cmdstr)
def on_hint_strings_updated(self, strings):
"""Handler for HintManager's hint_strings_updated.
Args:
strings: A list of hint strings.
"""
self.bindings = {s: s for s in strings}
class HintManager(QObject):
"""Manage drawing hints over links or other elements.
Class attributes:
SELECTORS: CSS selectors for the different highlighting modes.
FILTERS: A dictionary of filter functions for the modes.
The filter for "links" filters javascript:-links and a-tags
without "href".
HINT_CSS: The CSS template to use for hints.
Attributes:
@ -104,34 +55,14 @@ class HintManager(QObject):
Signals:
hint_strings_updated: Emitted when the possible hint strings changed.
arg: A list of hint strings.
set_mode: Emitted when the input mode should be changed.
arg: The new mode, as a string.
mouse_event: Mouse event to be posted in the web view.
arg: A QMouseEvent
openurl: Open a new url
arg 0: URL to open as a string.
arg 1: true if it should be opened in a new tab, else false.
set_open_target: Set a new target to open the links in.
set_cmd_text: Emitted when the commandline text should be set.
"""
SELECTORS = {
"all": ("a, textarea, select, input:not([type=hidden]), button, "
"frame, iframe, [onclick], [onmousedown], [role=link], "
"[role=option], [role=button], img"),
"links": "a",
"images": "img",
"editable": ("input[type=text], input[type=email], input[type=url],"
"input[type=tel], input[type=number], "
"input[type=password], input[type=search], textarea"),
"url": "[src], [href]",
}
FILTERS = {
"links": (lambda e: e.hasAttribute("href") and
urlutils.qurl(e.attribute("href")).scheme() != "javascript"),
}
HINT_CSS = """
color: {config[colors][hints.fg]};
background: {config[colors][hints.bg]};
@ -146,10 +77,8 @@ class HintManager(QObject):
"""
hint_strings_updated = pyqtSignal(list)
set_mode = pyqtSignal(str)
mouse_event = pyqtSignal('QMouseEvent')
set_open_target = pyqtSignal(str)
set_cmd_text = pyqtSignal(str)
def __init__(self, parent=None):
"""Constructor.
@ -162,6 +91,7 @@ class HintManager(QObject):
self._frame = None
self._target = None
self._baseurl = None
modes.manager.left.connect(self.on_mode_left)
def _hint_strings(self, elems):
"""Calculate the hint strings for elems.
@ -312,19 +242,6 @@ class HintManager(QObject):
message.info('URL yanked to {}'.format('primary selection' if sel
else 'clipboard'))
def _set_cmd_text(self, link, command):
"""Fill the command line with an element link.
Args:
link: The URL to open.
command: The command to use.
Emit:
set_cmd_text: Always emitted.
"""
self.set_cmd_text.emit(':{} {}'.format(command,
urlutils.urlstring(link)))
def _resolve_link(self, elem):
"""Resolve a link and check if we want to keep it.
@ -353,26 +270,15 @@ class HintManager(QObject):
Emit:
hint_strings_updated: Emitted to update keypraser.
set_mode: Emitted to enter hinting mode
"""
self._target = target
self._baseurl = baseurl
self._frame = frame
elems = frame.findAllElements(self.SELECTORS[mode])
filterfunc = self.FILTERS.get(mode, lambda e: True)
elems = frame.findAllElements(webelem.SELECTORS[mode])
filterfunc = webelem.FILTERS.get(mode, lambda e: True)
visible_elems = []
for e in elems:
if not filterfunc(e):
continue
rect = e.geometry()
if (not rect.isValid()) and rect.x() == 0:
# Most likely an invisible link
continue
framegeom = frame.geometry()
framegeom.translate(frame.scrollPosition())
if not framegeom.contains(rect.topLeft()):
# out of screen
continue
if filterfunc(e) and webelem.is_visible(e, self._frame):
visible_elems.append(e)
if not visible_elems:
message.error("No elements found.")
@ -395,23 +301,7 @@ class HintManager(QObject):
self._elems[string] = ElemTuple(e, label)
frame.contentsSizeChanged.connect(self.on_contents_size_changed)
self.hint_strings_updated.emit(strings)
self.set_mode.emit("hint")
def stop(self):
"""Stop hinting.
Emit:
set_mode: Emitted to leave hinting mode.
"""
for elem in self._elems.values():
elem.label.removeFromDocument()
self._frame.contentsSizeChanged.disconnect(
self.on_contents_size_changed)
self._elems = {}
self._target = None
self._frame = None
self.set_mode.emit("normal")
message.clear()
modes.enter("hint")
def handle_partial_key(self, keystr):
"""Handle a new partial keypress."""
@ -451,9 +341,10 @@ class HintManager(QObject):
'cmd_tab': 'tabopen',
'cmd_bgtab': 'backtabopen',
}
self._set_cmd_text(link, commands[self._target])
message.set_cmd_text(':{} {}'.format(commands[self._target],
urlutils.urlstring(link)))
if self._target != 'rapid':
self.stop()
modes.leave("hint")
@pyqtSlot('QSize')
def on_contents_size_changed(self, _size):
@ -463,3 +354,17 @@ class HintManager(QObject):
css = self.HINT_CSS.format(left=rect.x(), top=rect.y(),
config=config.instance)
elems.label.setAttribute("style", css)
@pyqtSlot(str)
def on_mode_left(self, mode):
"""Stop hinting when hinting mode was left."""
if mode != "hint":
return
for elem in self._elems.values():
elem.label.removeFromDocument()
self._frame.contentsSizeChanged.disconnect(
self.on_contents_size_changed)
self._elems = {}
self._target = None
self._frame = None
message.clear()

View File

@ -1,123 +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 logging
from PyQt5.QtCore import pyqtSignal, pyqtSlot
import qutebrowser.config.config as config
from qutebrowser.utils.keyparser import KeyParser
from qutebrowser.commands.parsers import (CommandParser, ArgumentCountError,
NoSuchCommandError)
STARTCHARS = ":/?"
class CommandKeyParser(KeyParser):
"""Keyparser for command bindings.
Class attributes:
supports_count: If the keyparser should support counts.
Attributes:
commandparser: Commandparser instance.
Signals:
set_cmd_text: Emitted when the statusbar should set a partial command.
arg: Text to set.
"""
set_cmd_text = pyqtSignal(str)
supports_count = True
def __init__(self, parent=None):
super().__init__(parent)
self.commandparser = CommandParser()
self.read_config()
def _run_or_fill(self, cmdstr, count=None, ignore_exc=True):
"""Run the command in cmdstr or fill the statusbar if args missing.
Args:
cmdstr: The command string.
count: Optional command count.
ignore_exc: Ignore exceptions.
Emit:
set_cmd_text: If a partial command should be printed to the
statusbar.
"""
try:
self.commandparser.run(cmdstr, count=count, ignore_exc=ignore_exc)
except NoSuchCommandError:
pass
except ArgumentCountError:
logging.debug('Filling statusbar with partial command {}'.format(
cmdstr))
self.set_cmd_text.emit(':{} '.format(cmdstr))
def _handle_single_key(self, e):
"""Override _handle_single_key to abort if the key is a startchar.
Args:
e: the KeyPressEvent from Qt.
Emit:
set_cmd_text: If the keystring should be shown in the statusbar.
"""
txt = e.text().strip()
if not self._keystring and any(txt == c for c in STARTCHARS):
self.set_cmd_text.emit(txt)
return
super()._handle_single_key(e)
def execute(self, cmdstr, count=None):
"""Handle a completed keychain."""
self._run_or_fill(cmdstr, count, ignore_exc=False)
@pyqtSlot(str, str)
def on_config_changed(self, section, _option):
"""Re-read the config if a keybinding was changed."""
if section == 'keybind':
self.read_config()
def read_config(self):
"""Read the configuration.
Config format: key = command, e.g.:
gg = scrollstart
"""
sect = config.instance['keybind']
if not sect.items():
logging.warn("No keybindings defined!")
for (key, cmd) in sect.items():
if key.startswith('@') and key.endswith('@'):
# normalize keystring
keystr = self._normalize_keystr(key.strip('@'))
logging.debug('registered mod key: {} -> {}'.format(keystr,
cmd))
self.modifier_bindings[keystr] = cmd
else:
logging.debug('registered key: {} -> {}'.format(key, cmd))
self.bindings[key] = cmd

View File

@ -68,16 +68,26 @@ SECTION_DESC = {
'keybind': (
"Bindings from a key(chain) to a command.\n"
"For special keys (can't be part of a keychain), enclose them in "
"@-signs. For modifiers, you can use either - or + as delimiters, and "
"<...>. For modifiers, you can use either - or + as delimiters, and "
"these names:\n"
" Control: Control, Ctrl\n"
" Meta: Meta, Windows, Mod4\n"
" Alt: Alt, Mod1\n"
" Shift: Shift\n"
"For simple keys (no @ signs), a capital letter means the key is "
"pressed with Shift. For modifier keys (with @ signs), you need "
"For simple keys (no <>-signs), a capital letter means the key is "
"pressed with Shift. For special keys (with <>-signs), you need "
"to explicitely add \"Shift-\" to match a key pressed with shift. "
"You can bind multiple commands by separating them with \";;\"."),
'keybind.insert': (
"Keybindings for insert mode.\n"
"Since normal keypresses are passed through, only special keys are "
"supported in this mode.\n"
"An useful command to map here is the hidden command leave_mode."),
'keybind.hint': (
"Keybindings for hint mode.\n"
"Since normal keypresses are passed through, only special keys are "
"supported in this mode.\n"
"An useful command to map here is the hidden command leave_mode."),
'aliases': (
"Aliases for commands.\n"
"By default, no aliases are defined. Example which adds a new command "
@ -166,6 +176,16 @@ DATA = OrderedDict([
('cmd_timeout',
SettingValue(types.Int(minval=0), "500"),
"Timeout for ambiguous keybindings."),
('insert_mode_on_plugins',
SettingValue(types.Bool(), "true"),
"Whether to switch to insert mode when clicking flash and other "
"plugins."),
('auto_insert_mode',
SettingValue(types.Bool(), "true"),
"Whether to automatically enter insert mode if an editable element "
"is focused after page load."),
)),
('tabbar', sect.KeyValue(
@ -395,15 +415,27 @@ DATA = OrderedDict([
('PP', 'tabpaste sel'),
('-', 'zoomout'),
('+', 'zoomin'),
('@Ctrl-Q@', 'quit'),
('@Ctrl-Shift-T@', 'undo'),
('@Ctrl-W@', 'tabclose'),
('@Ctrl-T@', 'tabopen about:blank'),
('@Ctrl-F@', 'scroll_page 0 1'),
('@Ctrl-B@', 'scroll_page 0 -1'),
('@Ctrl-D@', 'scroll_page 0 0.5'),
('@Ctrl-U@', 'scroll_page 0 -0.5'),
('@Backspace@', 'back'),
('<Ctrl-Q>', 'quit'),
('<Ctrl-Shift-T>', 'undo'),
('<Ctrl-W>', 'tabclose'),
('<Ctrl-T>', 'tabopen about:blank'),
('<Ctrl-F>', 'scroll_page 0 1'),
('<Ctrl-B>', 'scroll_page 0 -1'),
('<Ctrl-D>', 'scroll_page 0 0.5'),
('<Ctrl-U>', 'scroll_page 0 -0.5'),
('<Backspace>', 'back'),
)),
('keybind.insert', sect.ValueList(
types.KeyBindingName(), types.KeyBinding(),
('<Escape>', 'leave_mode'),
('<Ctrl-C>', 'leave_mode'),
)),
('keybind.hint', sect.ValueList(
types.KeyBindingName(), types.KeyBinding(),
('<Escape>', 'leave_mode'),
('<Ctrl-C>', 'leave_mode'),
)),
('aliases', sect.ValueList(

View File

@ -0,0 +1,18 @@
# 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/>.
"""Modules related to keyboard input and mode handling."""

View File

@ -0,0 +1,58 @@
# 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/>.
"""KeyChainParser for "hint" mode."""
from PyQt5.QtCore import pyqtSignal, Qt
from qutebrowser.keyinput.keyparser import CommandKeyParser
class HintKeyParser(CommandKeyParser):
"""KeyChainParser for hints.
Signals:
fire_hint: When a hint keybinding was completed.
Arg: the keystring/hint string pressed.
"""
fire_hint = pyqtSignal(str)
def __init__(self, parent=None):
super().__init__(parent, supports_count=False, supports_chains=True)
self.read_config('keybind.hint')
def execute(self, cmdstr, keytype, count=None):
"""Handle a completed keychain.
Emit:
fire_hint: Emitted if keytype is TYPE_CHAIN
"""
if keytype == self.TYPE_CHAIN:
self.fire_hint.emit(cmdstr)
else:
# execute as command
super().execute(cmdstr, keytype, count)
def on_hint_strings_updated(self, strings):
"""Handler for HintManager's hint_strings_updated.
Args:
strings: A list of hint strings.
"""
self.bindings = {s: s for s in strings}

View File

@ -0,0 +1,30 @@
# 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/>.
"""KeyParser for "insert" mode."""
import qutebrowser.keyinput.modes as modes
from qutebrowser.keyinput.keyparser import CommandKeyParser
class InsertKeyParser(CommandKeyParser):
"""KeyParser for insert mode."""
def __init__(self, parent=None):
super().__init__(parent, supports_chains=False)
self.read_config('keybind.insert')

View File

@ -21,15 +21,18 @@ import re
import logging
from functools import partial
from PyQt5.QtCore import pyqtSignal, Qt, QObject, QTimer
from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QObject, QTimer
from PyQt5.QtGui import QKeySequence
import qutebrowser.config.config as config
import qutebrowser.utils.message as message
from qutebrowser.commands.parsers import (CommandParser, ArgumentCountError,
NoSuchCommandError)
class KeyParser(QObject):
"""Parser for vim-like key sequences.
"""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.
@ -40,13 +43,18 @@ class KeyParser(QObject):
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).
supports_count: If the keyparser should support counts.
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.
bindings: Bound keybindings
modifier_bindings: Bound modifier bindings.
_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.
@ -60,18 +68,46 @@ class KeyParser(QObject):
MATCH_AMBIGUOUS = 2
MATCH_NONE = 3
supports_count = False
TYPE_CHAIN = 0
TYPE_SPECIAL = 1
def __init__(self, parent=None, bindings=None, modifier_bindings=None):
def __init__(self, parent=None, supports_count=None,
supports_chains=False):
super().__init__(parent)
self._timer = None
self._confsectname = None
self._keystring = ''
self.bindings = {} if bindings is None else bindings
self.modifier_bindings = ({} if modifier_bindings is None
else modifier_bindings)
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 _handle_modifier_key(self, e):
"""Handle a new keypress with modifiers.
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.
@ -92,19 +128,16 @@ class KeyParser(QObject):
return False
mod = e.modifiers()
modstr = ''
if not mod & (Qt.ControlModifier | Qt.AltModifier | Qt.MetaModifier):
# won't be a shortcut with modifiers
return False
for (mask, s) in modmask2str.items():
if mod & mask:
modstr += s + '+'
keystr = QKeySequence(e.key()).toString()
try:
cmdstr = self.modifier_bindings[modstr + keystr]
cmdstr = self.special_bindings[modstr + keystr]
except KeyError:
logging.debug('No binding found for {}.'.format(modstr + keystr))
return True
self.execute(cmdstr)
return False
self.execute(cmdstr, self.TYPE_SPECIAL)
return True
def _handle_single_key(self, e):
@ -116,17 +149,20 @@ class KeyParser(QObject):
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
return False
self._stop_delayed_exec()
self._keystring += txt
if self.supports_count:
if self._supports_count:
(countstr, cmd_input) = re.match(r'^(\d*)(.*)',
self._keystring).groups()
count = int(countstr) if countstr else None
@ -135,13 +171,14 @@ class KeyParser(QObject):
count = None
if not cmd_input:
return
# 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, count)
self.execute(binding, self.TYPE_CHAIN, count)
elif match == self.MATCH_AMBIGUOUS:
self._handle_ambiguous_match(binding, count)
elif match == self.MATCH_PARTIAL:
@ -151,6 +188,8 @@ class KeyParser(QObject):
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.
@ -213,7 +252,7 @@ class KeyParser(QObject):
if time == 0:
# execute immediately
self._keystring = ''
self.execute(binding, count)
self.execute(binding, self.TYPE_CHAIN, count)
else:
# execute in `time' ms
logging.debug("Scheduling execution of {} in {}ms".format(binding,
@ -225,28 +264,6 @@ class KeyParser(QObject):
count))
self._timer.start()
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 delayed_exec(self, command, count):
"""Execute a delayed command.
@ -260,11 +277,7 @@ class KeyParser(QObject):
self._timer = None
self._keystring = ''
self.keystring_updated.emit(self._keystring)
self.execute(command, count)
def execute(self, cmdstr, count=None):
"""Execute an action when a binding is triggered."""
raise NotImplementedError
self.execute(command, self.TYPE_CHAIN, count)
def handle(self, e):
"""Handle a new keypress and call the respective handlers.
@ -275,7 +288,95 @@ class KeyParser(QObject):
Emit:
keystring_updated: If a new keystring should be set.
"""
handled = self._handle_modifier_key(e)
if not handled:
self._handle_single_key(e)
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.
Attributes:
commandparser: Commandparser instance.
"""
def __init__(self, parent=None, supports_count=None,
supports_chains=False):
super().__init__(parent, supports_count, supports_chains)
self.commandparser = CommandParser()
def _run_or_fill(self, cmdstr, count=None, ignore_exc=True):
"""Run the command in cmdstr or fill the statusbar if args missing.
Args:
cmdstr: The command string.
count: Optional command count.
ignore_exc: Ignore exceptions.
"""
try:
self.commandparser.run(cmdstr, count=count, ignore_exc=ignore_exc)
except NoSuchCommandError:
pass
except ArgumentCountError:
logging.debug('Filling statusbar with partial command {}'.format(
cmdstr))
message.set_cmd_text(':{} '.format(cmdstr))
def execute(self, cmdstr, _keytype, count=None):
self._run_or_fill(cmdstr, count, ignore_exc=False)

View File

@ -0,0 +1,215 @@
# 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/>.
"""Mode manager singleton which handles the current keyboard mode.
Module attributes:
manager: The ModeManager instance.
"""
import logging
from PyQt5.QtGui import QWindow
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QEvent
import qutebrowser.config.config as config
import qutebrowser.commands.utils as cmdutils
import qutebrowser.utils.debug as debug
manager = None
def init(parent=None):
"""Initialize the global ModeManager.
This needs to be done by hand because the import time is before Qt is ready
for everything.
Args:
parent: Parent to use for ModeManager.
"""
global manager
manager = ModeManager(parent)
def enter(mode):
"""Enter the mode 'mode'."""
manager.enter(mode)
def leave(mode):
"""Leave the mode 'mode'."""
manager.leave(mode)
def maybe_leave(mode):
"""Convenience method to leave 'mode' without exceptions."""
try:
manager.leave(mode)
except ValueError:
pass
class ModeManager(QObject):
"""Manager for keyboard modes.
Attributes:
mode: The current mode (readonly property).
passthrough: A list of modes in which to pass through events.
_handlers: A dictionary of modes and their handlers.
_mode_stack: A list of the modes we're currently in, with the active
one on the right.
Signals:
entered: Emitted when a mode is entered.
arg: Name of the entered mode.
left: Emitted when a mode is left.
arg: Name of the left mode.
key_pressed: A key was pressed.
"""
entered = pyqtSignal(str)
left = pyqtSignal(str)
key_pressed = pyqtSignal('QKeyEvent')
def __init__(self, parent=None):
super().__init__(parent)
self._handlers = {}
self.passthrough = []
self._mode_stack = []
@property
def mode(self):
"""Read-only property for the current mode."""
if not self._mode_stack:
return None
return self._mode_stack[-1]
def register(self, mode, handler, passthrough=False):
"""Register a new mode.
Args:
mode: The name of the mode.
handler: Handler for keyPressEvents.
passthrough: Whether to pass keybindings in this mode through to
the widgets.
"""
self._handlers[mode] = handler
if passthrough:
self.passthrough.append(mode)
def enter(self, mode):
"""Enter a new mode.
Args:
mode; The name of the mode to enter.
Emit:
entered: With the new mode name.
"""
logging.debug("Switching mode to {}".format(mode))
if mode not in self._handlers:
raise ValueError("No handler for mode {}".format(mode))
if self._mode_stack and self._mode_stack[-1] == mode:
logging.debug("Already at end of stack, doing nothing")
return
self._mode_stack.append(mode)
logging.debug("New mode stack: {}".format(self._mode_stack))
self.entered.emit(mode)
def leave(self, mode):
"""Leave a mode.
Args:
mode; The name of the mode to leave.
Emit:
left: With the old mode name.
"""
try:
self._mode_stack.remove(mode)
except ValueError:
raise ValueError("Mode {} not on mode stack!".format(mode))
logging.debug("Leaving mode {}".format(mode))
logging.debug("New mode stack: {}".format(self._mode_stack))
self.left.emit(mode)
# FIXME handle modes=[] and not_modes=[] params
@cmdutils.register(instance='modeman', name='leave_mode', hide=True)
def leave_current_mode(self):
if self.mode == "normal":
raise ValueError("Can't leave normal mode!")
self.leave(self.mode)
def eventFilter(self, obj, evt):
"""Filter all events based on the currently set mode.
Also calls the real keypress handler.
Emit:
key_pressed: When a key was actually pressed.
"""
if self.mode is None:
# We got events before mode is set, so just pass them through.
return False
typ = evt.type()
if typ not in [QEvent.KeyPress, QEvent.KeyRelease]:
# We're not interested in non-key-events so we pass them through.
return False
if not isinstance(obj, QWindow):
# We already handled this same event at some point earlier, so
# we're not interested in it anymore.
logging.debug("Got event {} for {} -> ignoring".format(
debug.EVENTS[typ], obj.__class__.__name__))
return False
logging.debug("Got event {} for {}".format(
debug.EVENTS[typ], obj.__class__.__name__))
handler = self._handlers[self.mode]
if self.mode in self.passthrough:
# We're currently in a passthrough mode so we pass everything
# through.*and* let the passthrough keyhandler know.
# FIXME what if we leave the passthrough mode right here?
logging.debug("We're in a passthrough mode -> passing through")
if typ == QEvent.KeyPress:
logging.debug("KeyPress, calling handler {}".format(handler))
self.key_pressed.emit(evt)
if handler is not None:
handler(evt)
else:
logging.debug("KeyRelease, not calling anything")
return False
else:
logging.debug("We're in a non-passthrough mode")
if typ == QEvent.KeyPress:
# KeyPress in a non-passthrough mode - call handler and filter
# event from widgets (unless unhandled and configured to pass
# unhandled events through)
logging.debug("KeyPress, calling handler {} and "
"filtering".format(handler))
self.key_pressed.emit(evt)
handled = handler(evt) if handler is not None else False
return True
else:
# KeyRelease in a non-passthrough mode - filter event and
# ignore it entirely.
logging.debug("KeyRelease, not calling anything and filtering")
return True

View File

@ -0,0 +1,51 @@
# 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 logging
import qutebrowser.utils.message as message
from qutebrowser.keyinput.keyparser import CommandKeyParser
STARTCHARS = ":/?"
class NormalKeyParser(CommandKeyParser):
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

@ -57,6 +57,11 @@ def clear():
bridge.text.emit('')
def set_cmd_text(txt):
"""Set the statusbar command line to a preset text."""
bridge.set_cmd_text.emit(txt)
class MessageBridge(QObject):
"""Bridge for messages to be shown in the statusbar."""
@ -64,3 +69,4 @@ class MessageBridge(QObject):
error = pyqtSignal(str)
info = pyqtSignal(str)
text = pyqtSignal(str)
set_cmd_text = pyqtSignal(str)

View File

@ -0,0 +1,74 @@
# 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/>.
"""Utilities related to QWebElements.
Module attributes:
SELECTORS: CSS selectors for different groups of elements.
FILTERS: A dictionary of filter functions for the modes.
The filter for "links" filters javascript:-links and a-tags
without "href".
"""
import qutebrowser.utils.url as urlutils
SELECTORS = {
'all': ('a, textarea, select, input:not([type=hidden]), button, '
'frame, iframe, [onclick], [onmousedown], [role=link], '
'[role=option], [role=button], img'),
'links': 'a',
'images': 'img',
'editable': ('input[type=text], input[type=email], input[type=url], '
'input[type=tel], input[type=number], '
'input[type=password], input[type=search], textarea'),
'url': '[src], [href]',
}
SELECTORS['editable_focused'] = ', '.join(
[sel.strip() + ':focus' for sel in SELECTORS['editable'].split(',')])
FILTERS = {
'links': (lambda e: e.hasAttribute('href') and
urlutils.qurl(e.attribute('href')).scheme() != 'javascript'),
}
def is_visible(e, frame=None):
"""Check whether the element is currently visible in its frame.
Args:
e: The QWebElement to check.
frame: The QWebFrame in which the element should be visible in.
If None, the element's frame is used.
Return:
True if the element is visible, False otherwise.
"""
if e.isNull():
raise ValueError("Element is a null-element!")
if frame is None:
frame = e.webFrame()
rect = e.geometry()
if (not rect.isValid()) and rect.x() == 0:
# Most likely an invisible link
return False
framegeom = frame.geometry()
framegeom.translate(frame.scrollPosition())
if not framegeom.contains(rect.topLeft()):
# out of screen
return False
return True

View File

@ -27,7 +27,9 @@ from PyQt5.QtWebKitWidgets import QWebView, QWebPage
import qutebrowser.utils.url as urlutils
import qutebrowser.config.config as config
import qutebrowser.keyinput.modes as modes
import qutebrowser.utils.message as message
import qutebrowser.utils.webelem as webelem
from qutebrowser.browser.webpage import BrowserPage
from qutebrowser.browser.hints import HintManager
from qutebrowser.utils.signals import SignalCache
@ -84,6 +86,8 @@ class BrowserTab(QWebView):
self.page_.setLinkDelegationPolicy(QWebPage.DelegateAllLinks)
self.page_.linkHovered.connect(self.linkHovered)
self.linkClicked.connect(self.on_link_clicked)
self.loadStarted.connect(lambda: modes.maybe_leave("insert"))
self.loadFinished.connect(self.on_load_finished)
# FIXME find some way to hide scrollbars without setScrollBarPolicy
def _init_neighborlist(self):
@ -109,6 +113,38 @@ class BrowserTab(QWebView):
logging.debug("Everything destroyed, calling callback")
self._shutdown_callback()
def _is_editable(self, hitresult):
"""Check if a hit result needs keyboard focus.
Args:
hitresult: A QWebHitTestResult
"""
# FIXME is this algorithm accurate?
if hitresult.isContentEditable():
# text fields and the like
return True
if not config.get('general', 'insert_mode_on_plugins'):
return False
elem = hitresult.element()
tag = elem.tagName().lower()
if tag in ['embed', 'applet']:
# Flash/Java/...
return True
if tag == 'object':
# Could be Flash/Java/..., could be image/audio/...
if not elem.hasAttribute("type"):
logging.debug("<object> without type clicked...")
return False
objtype = elem.attribute("type")
if (objtype.startswith("application/") or
elem.hasAttribute("classid")):
# Let's hope flash/java stuff has an application/* mimetype OR
# at least a classid attribute. Oh, and let's home images/...
# DON"T have a classid attribute. HTML sucks.
logging.debug("<object type=\"{}\"> clicked.".format(objtype))
return True
return False
def openurl(self, url):
"""Open an URL in the browser.
@ -209,6 +245,20 @@ class BrowserTab(QWebView):
self.setFocus()
QApplication.postEvent(self, evt)
@pyqtSlot(bool)
def on_load_finished(self, _ok):
"""Handle auto_insert_mode after loading finished."""
if not config.get('general', 'auto_insert_mode'):
return
frame = self.page_.currentFrame()
elem = frame.findFirstElement(
webelem.SELECTORS['editable_focused'])
logging.debug("focus element: {}".format(not elem.isNull()))
if elem.isNull():
modes.maybe_leave("insert")
else:
modes.enter("insert")
@pyqtSlot(str)
def set_force_open_target(self, target):
"""Change the forced link target. Setter for _force_open_target.
@ -249,12 +299,13 @@ class BrowserTab(QWebView):
return super().paintEvent(e)
def mousePressEvent(self, e):
"""Check if a link was clicked with the middle button or Ctrl.
"""Extend QWidget::mousePressEvent().
Extend the superclass mousePressEvent().
This also is a bit of a hack, but it seems it's the only possible way.
Set the _open_target attribute accordingly.
This does the following things:
- Check if a link was clicked with the middle button or Ctrl and
set the _open_target attribute accordingly.
- Emit the editable_elem_selected signal if an editable element was
clicked.
Args:
e: The arrived event.
@ -262,6 +313,20 @@ class BrowserTab(QWebView):
Return:
The superclass return value.
"""
pos = e.pos()
frame = self.page_.frameAt(pos)
pos -= frame.geometry().topLeft()
hitresult = frame.hitTestContent(pos)
if self._is_editable(hitresult):
logging.debug("Clicked editable element!")
modes.enter("insert")
else:
logging.debug("Clicked non-editable element!")
try:
modes.leave("insert")
except ValueError:
pass
if self._force_open_target is not None:
self._open_target = self._force_open_target
self._force_open_target = None

View File

@ -23,8 +23,9 @@ from PyQt5.QtWidgets import (QWidget, QLineEdit, QProgressBar, QLabel,
QShortcut)
from PyQt5.QtGui import QPainter, QKeySequence, QValidator
import qutebrowser.keyinput.modes as modes
from qutebrowser.keyinput.normalmode import STARTCHARS
from qutebrowser.config.style import set_register_stylesheet, get_stylesheet
import qutebrowser.commands.keys as keys
from qutebrowser.utils.url import urlstring
from qutebrowser.commands.parsers import split_cmdline
from qutebrowser.models.cmdhistory import (History, HistoryEmptyError,
@ -157,7 +158,7 @@ class StatusBar(QWidget):
self.txt.errortext = ''
@pyqtSlot('QKeyEvent')
def keypress(self, e):
def on_key_pressed(self, e):
"""Hide temporary error message if a key was pressed.
Args:
@ -169,6 +170,18 @@ class StatusBar(QWidget):
self.txt.set_temptext('')
self.clear_error()
@pyqtSlot(str)
def on_mode_entered(self, mode):
"""Mark certain modes in the commandline."""
if mode in modes.manager.passthrough:
self.txt.normaltext = "-- {} MODE --".format(mode.upper())
@pyqtSlot(str)
def on_mode_left(self, mode):
"""Clear marked mode."""
if mode in modes.manager.passthrough:
self.txt.normaltext = ""
def resizeEvent(self, e):
"""Extend resizeEvent of QWidget to emit a resized signal afterwards.
@ -331,7 +344,7 @@ class _Command(QLineEdit):
"""
# FIXME we should consider the cursor position.
text = self.text()
if text[0] in keys.STARTCHARS:
if text[0] in STARTCHARS:
prefix = text[0]
text = text[1:]
else:
@ -342,8 +355,18 @@ class _Command(QLineEdit):
self.setFocus()
self.show_cmd.emit()
def focusInEvent(self, e):
"""Extend focusInEvent to enter command mode."""
modes.enter("command")
super().focusInEvent(e)
def focusOutEvent(self, e):
"""Clear the statusbar text if it's explicitely unfocused.
"""Extend focusOutEvent to do several tasks.
- Clear the statusbar text if it's explicitely unfocused.
- Leave command mode
- Clear completion selection
- Hide completion
Args:
e: The QFocusEvent.
@ -352,6 +375,7 @@ class _Command(QLineEdit):
clear_completion_selection: Always emitted.
hide_completion: Always emitted so the completion is hidden.
"""
modes.leave("command")
if e.reason() in [Qt.MouseFocusReason, Qt.TabFocusReason,
Qt.BacktabFocusReason, Qt.OtherFocusReason]:
self.setText('')
@ -376,7 +400,7 @@ class _CommandValidator(QValidator):
Return:
A tuple (status, string, pos) as a QValidator should.
"""
if any(string.startswith(c) for c in keys.STARTCHARS):
if any(string.startswith(c) for c in STARTCHARS):
return (QValidator.Acceptable, string, pos)
else:
return (QValidator.Invalid, string, pos)

View File

@ -69,10 +69,6 @@ class TabbedBrowser(TabWidget):
arg 2: y-position in %.
hint_strings_updated: Hint strings were updated.
arg: A list of hint strings.
set_mode: The input mode should be changed.
arg: The new mode as a string.
keypress: A key was pressed.
arg: The QKeyEvent leading to the keypress.
shutdown_complete: The shuttdown is completed.
quit: The last tab was closed, quit application.
resized: Emitted when the browser window has resized, so the completion
@ -88,9 +84,6 @@ class TabbedBrowser(TabWidget):
cur_link_hovered = pyqtSignal(str, str, str)
cur_scroll_perc_changed = pyqtSignal(int, int)
hint_strings_updated = pyqtSignal(list)
set_cmd_text = pyqtSignal(str)
set_mode = pyqtSignal(str)
keypress = pyqtSignal('QKeyEvent')
shutdown_complete = pyqtSignal()
quit = pyqtSignal()
resized = pyqtSignal('QRect')
@ -143,8 +136,6 @@ class TabbedBrowser(TabWidget):
tab.urlChanged.connect(self._filter.create(self.cur_url_changed))
# hintmanager
tab.hintmanager.hint_strings_updated.connect(self.hint_strings_updated)
tab.hintmanager.set_mode.connect(self.set_mode)
tab.hintmanager.set_cmd_text.connect(self.set_cmd_text)
# misc
tab.titleChanged.connect(self.on_title_changed)
tab.open_tab.connect(self.tabopen)
@ -242,23 +233,15 @@ class TabbedBrowser(TabWidget):
@cmdutils.register(instance='mainwindow.tabs', hide=True)
def tabopencur(self):
"""Set the statusbar to :tabopen and the current URL.
Emit:
set_cmd_text prefilled with :tabopen $URL
"""
"""Set the statusbar to :tabopen and the current URL."""
url = urlutils.urlstring(self.currentWidget().url())
self.set_cmd_text.emit(':tabopen ' + url)
message.set_cmd_text(':tabopen ' + url)
@cmdutils.register(instance='mainwindow.tabs', hide=True)
def opencur(self):
"""Set the statusbar to :open and the current URL.
Emit:
set_cmd_text prefilled with :open $URL
"""
"""Set the statusbar to :open and the current URL."""
url = urlutils.urlstring(self.currentWidget().url())
self.set_cmd_text.emit(':open ' + url)
message.set_cmd_text(':open ' + url)
@cmdutils.register(instance='mainwindow.tabs', name='undo')
def undo_close(self):
@ -360,18 +343,6 @@ class TabbedBrowser(TabWidget):
else:
logging.debug('ignoring title change')
def keyPressEvent(self, e):
"""Extend TabWidget (QWidget)'s keyPressEvent to emit a signal.
Args:
e: The QKeyPressEvent
Emit:
keypress: Always emitted.
"""
self.keypress.emit(e)
super().keyPressEvent(e)
def resizeEvent(self, e):
"""Extend resizeEvent of QWidget to emit a resized signal afterwards.