Merge branch 'keyboard'
Conflicts: TODO
This commit is contained in:
commit
d4baf77f78
1
THANKS
1
THANKS
@ -54,6 +54,7 @@ channels:
|
|||||||
- scummos
|
- scummos
|
||||||
- svuorela
|
- svuorela
|
||||||
- kpj
|
- kpj
|
||||||
|
- hyde
|
||||||
|
|
||||||
Thanks to these projects which were essential while developing qutebrowser:
|
Thanks to these projects which were essential while developing qutebrowser:
|
||||||
- Python
|
- Python
|
||||||
|
15
TODO
15
TODO
@ -14,7 +14,6 @@ Style
|
|||||||
=====
|
=====
|
||||||
|
|
||||||
Refactor completion widget mess (initializing / changing completions)
|
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...
|
we probably could replace CompletionModel with QStandardModel...
|
||||||
replace foo_bar options with foo-bar
|
replace foo_bar options with foo-bar
|
||||||
reorder options
|
reorder options
|
||||||
@ -52,13 +51,6 @@ Before Blink
|
|||||||
|
|
||||||
session handling / saving
|
session handling / saving
|
||||||
IPC, like dwb -x
|
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
|
Bookmarks
|
||||||
Internationalization
|
Internationalization
|
||||||
Marks
|
Marks
|
||||||
@ -89,6 +81,13 @@ How do we handle empty values in input bar?
|
|||||||
Human-readable error messages for unknown settings / wrong interpolations / ...
|
Human-readable error messages for unknown settings / wrong interpolations / ...
|
||||||
auto-click on active hint
|
auto-click on active hint
|
||||||
click hints on enter
|
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
|
Qt Bugs
|
||||||
========
|
========
|
||||||
|
@ -52,12 +52,14 @@ 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.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.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.commands.parsers import CommandParser, SearchParser
|
||||||
from qutebrowser.browser.hints import HintKeyParser
|
|
||||||
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
|
||||||
from qutebrowser.utils.debug import set_trace # pylint: disable=unused-import
|
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.
|
_timers: List of used QTimers so they don't get GCed.
|
||||||
_shutting_down: True if we're currently shutting down.
|
_shutting_down: True if we're currently shutting down.
|
||||||
_quit_status: The current quitting status.
|
_quit_status: The current quitting status.
|
||||||
_mode: The mode we're currently in.
|
|
||||||
_opened_urls: List of opened URLs.
|
_opened_urls: List of opened URLs.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -93,7 +94,6 @@ class QuteBrowser(QApplication):
|
|||||||
self._timers = []
|
self._timers = []
|
||||||
self._opened_urls = []
|
self._opened_urls = []
|
||||||
self._shutting_down = False
|
self._shutting_down = False
|
||||||
self._mode = None
|
|
||||||
|
|
||||||
sys.excepthook = self._exception_hook
|
sys.excepthook = self._exception_hook
|
||||||
|
|
||||||
@ -125,15 +125,24 @@ class QuteBrowser(QApplication):
|
|||||||
self.commandparser = CommandParser()
|
self.commandparser = CommandParser()
|
||||||
self.searchparser = SearchParser()
|
self.searchparser = SearchParser()
|
||||||
self._keyparsers = {
|
self._keyparsers = {
|
||||||
"normal": CommandKeyParser(self),
|
'normal': NormalKeyParser(self),
|
||||||
"hint": HintKeyParser(self),
|
'hint': HintKeyParser(self),
|
||||||
|
'insert': InsertKeyParser(self),
|
||||||
}
|
}
|
||||||
self._init_cmds()
|
self._init_cmds()
|
||||||
self.mainwindow = MainWindow()
|
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.setQuitOnLastWindowClosed(False)
|
||||||
|
|
||||||
self._connect_signals()
|
self._connect_signals()
|
||||||
self.set_mode("normal")
|
modes.enter("normal")
|
||||||
|
|
||||||
self.mainwindow.show()
|
self.mainwindow.show()
|
||||||
self._python_hacks()
|
self._python_hacks()
|
||||||
@ -246,13 +255,12 @@ class QuteBrowser(QApplication):
|
|||||||
# misc
|
# misc
|
||||||
self.lastWindowClosed.connect(self.shutdown)
|
self.lastWindowClosed.connect(self.shutdown)
|
||||||
tabs.quit.connect(self.shutdown)
|
tabs.quit.connect(self.shutdown)
|
||||||
tabs.set_mode.connect(self.set_mode)
|
|
||||||
tabs.currentChanged.connect(self.mainwindow.update_inspector)
|
tabs.currentChanged.connect(self.mainwindow.update_inspector)
|
||||||
|
|
||||||
# status bar
|
# status bar
|
||||||
tabs.keypress.connect(status.keypress)
|
modes.manager.entered.connect(status.on_mode_entered)
|
||||||
for obj in [kp["normal"], tabs]:
|
modes.manager.left.connect(status.on_mode_left)
|
||||||
obj.set_cmd_text.connect(cmd.set_cmd_text)
|
modes.manager.key_pressed.connect(status.on_key_pressed)
|
||||||
|
|
||||||
# commands
|
# commands
|
||||||
cmd.got_cmd.connect(self.commandparser.run)
|
cmd.got_cmd.connect(self.commandparser.run)
|
||||||
@ -264,7 +272,6 @@ class QuteBrowser(QApplication):
|
|||||||
|
|
||||||
# hints
|
# hints
|
||||||
kp["hint"].fire_hint.connect(tabs.cur.fire_hint)
|
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)
|
kp["hint"].keystring_updated.connect(tabs.cur.handle_hint_key)
|
||||||
tabs.hint_strings_updated.connect(kp["hint"].on_hint_strings_updated)
|
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.error.connect(status.disp_error)
|
||||||
message.bridge.info.connect(status.txt.set_temptext)
|
message.bridge.info.connect(status.txt.set_temptext)
|
||||||
message.bridge.text.connect(status.txt.set_normaltext)
|
message.bridge.text.connect(status.txt.set_normaltext)
|
||||||
|
message.bridge.set_cmd_text.connect(cmd.set_cmd_text)
|
||||||
|
|
||||||
# config
|
# config
|
||||||
self.config.style_changed.connect(style.invalidate_caches)
|
self.config.style_changed.connect(style.invalidate_caches)
|
||||||
@ -401,20 +409,6 @@ class QuteBrowser(QApplication):
|
|||||||
logging.debug("maybe_quit quitting.")
|
logging.debug("maybe_quit quitting.")
|
||||||
self.quit()
|
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)
|
@cmdutils.register(instance='', maxsplit=0)
|
||||||
def pyeval(self, s):
|
def pyeval(self, s):
|
||||||
"""Evaluate a python string and display the results as a webpage.
|
"""Evaluate a python string and display the results as a webpage.
|
||||||
|
@ -207,11 +207,6 @@ class CurCommandDispatcher(QObject):
|
|||||||
"""Fire a completed hint."""
|
"""Fire a completed hint."""
|
||||||
self._tabs.currentWidget().hintmanager.fire(keystr)
|
self._tabs.currentWidget().hintmanager.fire(keystr)
|
||||||
|
|
||||||
@pyqtSlot()
|
|
||||||
def abort_hinting(self):
|
|
||||||
"""Abort hinting."""
|
|
||||||
self._tabs.currentWidget().hintmanager.stop()
|
|
||||||
|
|
||||||
@pyqtSlot(str, int)
|
@pyqtSlot(str, int)
|
||||||
def search(self, text, flags):
|
def search(self, text, flags):
|
||||||
"""Search for text in the current page.
|
"""Search for text in the current page.
|
||||||
|
@ -26,69 +26,20 @@ 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.utils.message as message
|
import qutebrowser.utils.message as message
|
||||||
import qutebrowser.utils.url as urlutils
|
import qutebrowser.utils.url as urlutils
|
||||||
from qutebrowser.utils.keyparser import KeyParser
|
import qutebrowser.utils.webelem as webelem
|
||||||
|
|
||||||
|
|
||||||
ElemTuple = namedtuple('ElemTuple', 'elem, label')
|
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):
|
class HintManager(QObject):
|
||||||
|
|
||||||
"""Manage drawing hints over links or other elements.
|
"""Manage drawing hints over links or other elements.
|
||||||
|
|
||||||
Class attributes:
|
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.
|
HINT_CSS: The CSS template to use for hints.
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
@ -104,34 +55,14 @@ class HintManager(QObject):
|
|||||||
Signals:
|
Signals:
|
||||||
hint_strings_updated: Emitted when the possible hint strings changed.
|
hint_strings_updated: Emitted when the possible hint strings changed.
|
||||||
arg: A list of hint strings.
|
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.
|
mouse_event: Mouse event to be posted in the web view.
|
||||||
arg: A QMouseEvent
|
arg: A QMouseEvent
|
||||||
openurl: Open a new url
|
openurl: Open a new url
|
||||||
arg 0: URL to open as a string.
|
arg 0: URL to open as a string.
|
||||||
arg 1: true if it should be opened in a new tab, else false.
|
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_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 = """
|
HINT_CSS = """
|
||||||
color: {config[colors][hints.fg]};
|
color: {config[colors][hints.fg]};
|
||||||
background: {config[colors][hints.bg]};
|
background: {config[colors][hints.bg]};
|
||||||
@ -146,10 +77,8 @@ class HintManager(QObject):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
hint_strings_updated = pyqtSignal(list)
|
hint_strings_updated = pyqtSignal(list)
|
||||||
set_mode = pyqtSignal(str)
|
|
||||||
mouse_event = pyqtSignal('QMouseEvent')
|
mouse_event = pyqtSignal('QMouseEvent')
|
||||||
set_open_target = pyqtSignal(str)
|
set_open_target = pyqtSignal(str)
|
||||||
set_cmd_text = pyqtSignal(str)
|
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
"""Constructor.
|
"""Constructor.
|
||||||
@ -162,6 +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)
|
||||||
|
|
||||||
def _hint_strings(self, elems):
|
def _hint_strings(self, elems):
|
||||||
"""Calculate the hint strings for elems.
|
"""Calculate the hint strings for elems.
|
||||||
@ -312,19 +242,6 @@ class HintManager(QObject):
|
|||||||
message.info('URL yanked to {}'.format('primary selection' if sel
|
message.info('URL yanked to {}'.format('primary selection' if sel
|
||||||
else 'clipboard'))
|
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):
|
def _resolve_link(self, elem):
|
||||||
"""Resolve a link and check if we want to keep it.
|
"""Resolve a link and check if we want to keep it.
|
||||||
|
|
||||||
@ -353,27 +270,16 @@ class HintManager(QObject):
|
|||||||
|
|
||||||
Emit:
|
Emit:
|
||||||
hint_strings_updated: Emitted to update keypraser.
|
hint_strings_updated: Emitted to update keypraser.
|
||||||
set_mode: Emitted to enter hinting mode
|
|
||||||
"""
|
"""
|
||||||
self._target = target
|
self._target = target
|
||||||
self._baseurl = baseurl
|
self._baseurl = baseurl
|
||||||
self._frame = frame
|
self._frame = frame
|
||||||
elems = frame.findAllElements(self.SELECTORS[mode])
|
elems = frame.findAllElements(webelem.SELECTORS[mode])
|
||||||
filterfunc = self.FILTERS.get(mode, lambda e: True)
|
filterfunc = webelem.FILTERS.get(mode, lambda e: True)
|
||||||
visible_elems = []
|
visible_elems = []
|
||||||
for e in elems:
|
for e in elems:
|
||||||
if not filterfunc(e):
|
if filterfunc(e) and webelem.is_visible(e, self._frame):
|
||||||
continue
|
visible_elems.append(e)
|
||||||
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
|
|
||||||
visible_elems.append(e)
|
|
||||||
if not visible_elems:
|
if not visible_elems:
|
||||||
message.error("No elements found.")
|
message.error("No elements found.")
|
||||||
return
|
return
|
||||||
@ -395,23 +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)
|
||||||
self.set_mode.emit("hint")
|
modes.enter("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()
|
|
||||||
|
|
||||||
def handle_partial_key(self, keystr):
|
def handle_partial_key(self, keystr):
|
||||||
"""Handle a new partial keypress."""
|
"""Handle a new partial keypress."""
|
||||||
@ -451,9 +341,10 @@ class HintManager(QObject):
|
|||||||
'cmd_tab': 'tabopen',
|
'cmd_tab': 'tabopen',
|
||||||
'cmd_bgtab': 'backtabopen',
|
'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':
|
if self._target != 'rapid':
|
||||||
self.stop()
|
modes.leave("hint")
|
||||||
|
|
||||||
@pyqtSlot('QSize')
|
@pyqtSlot('QSize')
|
||||||
def on_contents_size_changed(self, _size):
|
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(),
|
css = self.HINT_CSS.format(left=rect.x(), top=rect.y(),
|
||||||
config=config.instance)
|
config=config.instance)
|
||||||
elems.label.setAttribute("style", css)
|
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()
|
||||||
|
@ -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
|
|
@ -68,16 +68,26 @@ SECTION_DESC = {
|
|||||||
'keybind': (
|
'keybind': (
|
||||||
"Bindings from a key(chain) to a command.\n"
|
"Bindings from a key(chain) to a command.\n"
|
||||||
"For special keys (can't be part of a keychain), enclose them in "
|
"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"
|
"these names:\n"
|
||||||
" Control: Control, Ctrl\n"
|
" Control: Control, Ctrl\n"
|
||||||
" Meta: Meta, Windows, Mod4\n"
|
" Meta: Meta, Windows, Mod4\n"
|
||||||
" Alt: Alt, Mod1\n"
|
" Alt: Alt, Mod1\n"
|
||||||
" Shift: Shift\n"
|
" Shift: Shift\n"
|
||||||
"For simple keys (no @ signs), a capital letter means the key is "
|
"For simple keys (no <>-signs), a capital letter means the key is "
|
||||||
"pressed with Shift. For modifier keys (with @ signs), you need "
|
"pressed with Shift. For special keys (with <>-signs), you need "
|
||||||
"to explicitely add \"Shift-\" to match a key pressed with shift. "
|
"to explicitely add \"Shift-\" to match a key pressed with shift. "
|
||||||
"You can bind multiple commands by separating them with \";;\"."),
|
"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': (
|
||||||
"Aliases for commands.\n"
|
"Aliases for commands.\n"
|
||||||
"By default, no aliases are defined. Example which adds a new command "
|
"By default, no aliases are defined. Example which adds a new command "
|
||||||
@ -166,6 +176,16 @@ DATA = OrderedDict([
|
|||||||
('cmd_timeout',
|
('cmd_timeout',
|
||||||
SettingValue(types.Int(minval=0), "500"),
|
SettingValue(types.Int(minval=0), "500"),
|
||||||
"Timeout for ambiguous keybindings."),
|
"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(
|
('tabbar', sect.KeyValue(
|
||||||
@ -395,15 +415,27 @@ DATA = OrderedDict([
|
|||||||
('PP', 'tabpaste sel'),
|
('PP', 'tabpaste sel'),
|
||||||
('-', 'zoomout'),
|
('-', 'zoomout'),
|
||||||
('+', 'zoomin'),
|
('+', 'zoomin'),
|
||||||
('@Ctrl-Q@', 'quit'),
|
('<Ctrl-Q>', 'quit'),
|
||||||
('@Ctrl-Shift-T@', 'undo'),
|
('<Ctrl-Shift-T>', 'undo'),
|
||||||
('@Ctrl-W@', 'tabclose'),
|
('<Ctrl-W>', 'tabclose'),
|
||||||
('@Ctrl-T@', 'tabopen about:blank'),
|
('<Ctrl-T>', 'tabopen about:blank'),
|
||||||
('@Ctrl-F@', 'scroll_page 0 1'),
|
('<Ctrl-F>', 'scroll_page 0 1'),
|
||||||
('@Ctrl-B@', 'scroll_page 0 -1'),
|
('<Ctrl-B>', 'scroll_page 0 -1'),
|
||||||
('@Ctrl-D@', 'scroll_page 0 0.5'),
|
('<Ctrl-D>', 'scroll_page 0 0.5'),
|
||||||
('@Ctrl-U@', 'scroll_page 0 -0.5'),
|
('<Ctrl-U>', 'scroll_page 0 -0.5'),
|
||||||
('@Backspace@', 'back'),
|
('<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(
|
('aliases', sect.ValueList(
|
||||||
|
18
qutebrowser/keyinput/__init__.py
Normal file
18
qutebrowser/keyinput/__init__.py
Normal 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."""
|
58
qutebrowser/keyinput/hintmode.py
Normal file
58
qutebrowser/keyinput/hintmode.py
Normal 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}
|
30
qutebrowser/keyinput/insertmode.py
Normal file
30
qutebrowser/keyinput/insertmode.py
Normal 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')
|
@ -21,15 +21,18 @@ import re
|
|||||||
import logging
|
import logging
|
||||||
from functools import partial
|
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
|
from PyQt5.QtGui import QKeySequence
|
||||||
|
|
||||||
import qutebrowser.config.config as config
|
import qutebrowser.config.config as config
|
||||||
|
import qutebrowser.utils.message as message
|
||||||
|
from qutebrowser.commands.parsers import (CommandParser, ArgumentCountError,
|
||||||
|
NoSuchCommandError)
|
||||||
|
|
||||||
|
|
||||||
class KeyParser(QObject):
|
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
|
Not intended to be instantiated directly. Subclasses have to override
|
||||||
execute() to do whatever they want to.
|
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_DEFINITIVE: Constant for a full match (keychain matches exactly).
|
||||||
MATCH_AMBIGUOUS: There are both a partial and a definitive match.
|
MATCH_AMBIGUOUS: There are both a partial and a definitive match.
|
||||||
MATCH_NONE: Constant for no match (no more matches possible).
|
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:
|
Attributes:
|
||||||
|
bindings: Bound keybindings
|
||||||
|
special_bindings: Bound special bindings (<Foo>).
|
||||||
_keystring: The currently entered key sequence
|
_keystring: The currently entered key sequence
|
||||||
_timer: QTimer for delayed execution.
|
_timer: QTimer for delayed execution.
|
||||||
bindings: Bound keybindings
|
_confsectname: The name of the configsection.
|
||||||
modifier_bindings: Bound modifier bindings.
|
_supports_count: Whether count is supported
|
||||||
|
_supports_chains: Whether keychains are supported
|
||||||
|
|
||||||
Signals:
|
Signals:
|
||||||
keystring_updated: Emitted when the keystring is updated.
|
keystring_updated: Emitted when the keystring is updated.
|
||||||
@ -60,18 +68,46 @@ class KeyParser(QObject):
|
|||||||
MATCH_AMBIGUOUS = 2
|
MATCH_AMBIGUOUS = 2
|
||||||
MATCH_NONE = 3
|
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)
|
super().__init__(parent)
|
||||||
self._timer = None
|
self._timer = None
|
||||||
|
self._confsectname = None
|
||||||
self._keystring = ''
|
self._keystring = ''
|
||||||
self.bindings = {} if bindings is None else bindings
|
if supports_count is None:
|
||||||
self.modifier_bindings = ({} if modifier_bindings is None
|
supports_count = supports_chains
|
||||||
else modifier_bindings)
|
self._supports_count = supports_count
|
||||||
|
self._supports_chains = supports_chains
|
||||||
|
self.bindings = {}
|
||||||
|
self.special_bindings = {}
|
||||||
|
|
||||||
def _handle_modifier_key(self, e):
|
def _normalize_keystr(self, keystr):
|
||||||
"""Handle a new keypress with modifiers.
|
"""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.
|
Return True if the keypress has been handled, and False if not.
|
||||||
|
|
||||||
@ -92,19 +128,16 @@ class KeyParser(QObject):
|
|||||||
return False
|
return False
|
||||||
mod = e.modifiers()
|
mod = e.modifiers()
|
||||||
modstr = ''
|
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():
|
for (mask, s) in modmask2str.items():
|
||||||
if mod & mask:
|
if mod & mask:
|
||||||
modstr += s + '+'
|
modstr += s + '+'
|
||||||
keystr = QKeySequence(e.key()).toString()
|
keystr = QKeySequence(e.key()).toString()
|
||||||
try:
|
try:
|
||||||
cmdstr = self.modifier_bindings[modstr + keystr]
|
cmdstr = self.special_bindings[modstr + keystr]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
logging.debug('No binding found for {}.'.format(modstr + keystr))
|
logging.debug('No binding found for {}.'.format(modstr + keystr))
|
||||||
return True
|
return False
|
||||||
self.execute(cmdstr)
|
self.execute(cmdstr, self.TYPE_SPECIAL)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _handle_single_key(self, e):
|
def _handle_single_key(self, e):
|
||||||
@ -116,17 +149,20 @@ class KeyParser(QObject):
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
e: the KeyPressEvent from Qt.
|
e: the KeyPressEvent from Qt.
|
||||||
|
|
||||||
|
Return:
|
||||||
|
True if event has been handled, False otherwise.
|
||||||
"""
|
"""
|
||||||
logging.debug('Got key: {} / text: "{}"'.format(e.key(), e.text()))
|
logging.debug('Got key: {} / text: "{}"'.format(e.key(), e.text()))
|
||||||
txt = e.text().strip()
|
txt = e.text().strip()
|
||||||
if not txt:
|
if not txt:
|
||||||
logging.debug('Ignoring, no text')
|
logging.debug('Ignoring, no text')
|
||||||
return
|
return False
|
||||||
|
|
||||||
self._stop_delayed_exec()
|
self._stop_delayed_exec()
|
||||||
self._keystring += txt
|
self._keystring += txt
|
||||||
|
|
||||||
if self.supports_count:
|
if self._supports_count:
|
||||||
(countstr, cmd_input) = re.match(r'^(\d*)(.*)',
|
(countstr, cmd_input) = re.match(r'^(\d*)(.*)',
|
||||||
self._keystring).groups()
|
self._keystring).groups()
|
||||||
count = int(countstr) if countstr else None
|
count = int(countstr) if countstr else None
|
||||||
@ -135,13 +171,14 @@ class KeyParser(QObject):
|
|||||||
count = None
|
count = None
|
||||||
|
|
||||||
if not cmd_input:
|
if not cmd_input:
|
||||||
return
|
# Only a count, no command yet, but we handled it
|
||||||
|
return True
|
||||||
|
|
||||||
(match, binding) = self._match_key(cmd_input)
|
(match, binding) = self._match_key(cmd_input)
|
||||||
|
|
||||||
if match == self.MATCH_DEFINITIVE:
|
if match == self.MATCH_DEFINITIVE:
|
||||||
self._keystring = ''
|
self._keystring = ''
|
||||||
self.execute(binding, count)
|
self.execute(binding, self.TYPE_CHAIN, count)
|
||||||
elif match == self.MATCH_AMBIGUOUS:
|
elif match == self.MATCH_AMBIGUOUS:
|
||||||
self._handle_ambiguous_match(binding, count)
|
self._handle_ambiguous_match(binding, count)
|
||||||
elif match == self.MATCH_PARTIAL:
|
elif match == self.MATCH_PARTIAL:
|
||||||
@ -151,6 +188,8 @@ class KeyParser(QObject):
|
|||||||
logging.debug('Giving up with "{}", no matches'.format(
|
logging.debug('Giving up with "{}", no matches'.format(
|
||||||
self._keystring))
|
self._keystring))
|
||||||
self._keystring = ''
|
self._keystring = ''
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
def _match_key(self, cmd_input):
|
def _match_key(self, cmd_input):
|
||||||
"""Try to match a given keystring with any bound keychain.
|
"""Try to match a given keystring with any bound keychain.
|
||||||
@ -213,7 +252,7 @@ class KeyParser(QObject):
|
|||||||
if time == 0:
|
if time == 0:
|
||||||
# execute immediately
|
# execute immediately
|
||||||
self._keystring = ''
|
self._keystring = ''
|
||||||
self.execute(binding, count)
|
self.execute(binding, self.TYPE_CHAIN, count)
|
||||||
else:
|
else:
|
||||||
# execute in `time' ms
|
# execute in `time' ms
|
||||||
logging.debug("Scheduling execution of {} in {}ms".format(binding,
|
logging.debug("Scheduling execution of {} in {}ms".format(binding,
|
||||||
@ -225,28 +264,6 @@ class KeyParser(QObject):
|
|||||||
count))
|
count))
|
||||||
self._timer.start()
|
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):
|
def delayed_exec(self, command, count):
|
||||||
"""Execute a delayed command.
|
"""Execute a delayed command.
|
||||||
|
|
||||||
@ -260,11 +277,7 @@ class KeyParser(QObject):
|
|||||||
self._timer = None
|
self._timer = None
|
||||||
self._keystring = ''
|
self._keystring = ''
|
||||||
self.keystring_updated.emit(self._keystring)
|
self.keystring_updated.emit(self._keystring)
|
||||||
self.execute(command, count)
|
self.execute(command, self.TYPE_CHAIN, count)
|
||||||
|
|
||||||
def execute(self, cmdstr, count=None):
|
|
||||||
"""Execute an action when a binding is triggered."""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def handle(self, e):
|
def handle(self, e):
|
||||||
"""Handle a new keypress and call the respective handlers.
|
"""Handle a new keypress and call the respective handlers.
|
||||||
@ -275,7 +288,95 @@ class KeyParser(QObject):
|
|||||||
Emit:
|
Emit:
|
||||||
keystring_updated: If a new keystring should be set.
|
keystring_updated: If a new keystring should be set.
|
||||||
"""
|
"""
|
||||||
handled = self._handle_modifier_key(e)
|
handled = self._handle_special_key(e)
|
||||||
if not handled:
|
if handled or not self._supports_chains:
|
||||||
self._handle_single_key(e)
|
return handled
|
||||||
self.keystring_updated.emit(self._keystring)
|
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)
|
215
qutebrowser/keyinput/modes.py
Normal file
215
qutebrowser/keyinput/modes.py
Normal 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
|
51
qutebrowser/keyinput/normalmode.py
Normal file
51
qutebrowser/keyinput/normalmode.py
Normal 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)
|
@ -57,6 +57,11 @@ def clear():
|
|||||||
bridge.text.emit('')
|
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):
|
class MessageBridge(QObject):
|
||||||
|
|
||||||
"""Bridge for messages to be shown in the statusbar."""
|
"""Bridge for messages to be shown in the statusbar."""
|
||||||
@ -64,3 +69,4 @@ class MessageBridge(QObject):
|
|||||||
error = pyqtSignal(str)
|
error = pyqtSignal(str)
|
||||||
info = pyqtSignal(str)
|
info = pyqtSignal(str)
|
||||||
text = pyqtSignal(str)
|
text = pyqtSignal(str)
|
||||||
|
set_cmd_text = pyqtSignal(str)
|
||||||
|
74
qutebrowser/utils/webelem.py
Normal file
74
qutebrowser/utils/webelem.py
Normal 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
|
@ -27,7 +27,9 @@ 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.utils.message as message
|
import qutebrowser.utils.message as message
|
||||||
|
import qutebrowser.utils.webelem as webelem
|
||||||
from qutebrowser.browser.webpage import BrowserPage
|
from qutebrowser.browser.webpage import BrowserPage
|
||||||
from qutebrowser.browser.hints import HintManager
|
from qutebrowser.browser.hints import HintManager
|
||||||
from qutebrowser.utils.signals import SignalCache
|
from qutebrowser.utils.signals import SignalCache
|
||||||
@ -84,6 +86,8 @@ 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.loadFinished.connect(self.on_load_finished)
|
||||||
# FIXME find some way to hide scrollbars without setScrollBarPolicy
|
# FIXME find some way to hide scrollbars without setScrollBarPolicy
|
||||||
|
|
||||||
def _init_neighborlist(self):
|
def _init_neighborlist(self):
|
||||||
@ -109,6 +113,38 @@ class BrowserTab(QWebView):
|
|||||||
logging.debug("Everything destroyed, calling callback")
|
logging.debug("Everything destroyed, calling callback")
|
||||||
self._shutdown_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):
|
def openurl(self, url):
|
||||||
"""Open an URL in the browser.
|
"""Open an URL in the browser.
|
||||||
|
|
||||||
@ -209,6 +245,20 @@ class BrowserTab(QWebView):
|
|||||||
self.setFocus()
|
self.setFocus()
|
||||||
QApplication.postEvent(self, evt)
|
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)
|
@pyqtSlot(str)
|
||||||
def set_force_open_target(self, target):
|
def set_force_open_target(self, target):
|
||||||
"""Change the forced link target. Setter for _force_open_target.
|
"""Change the forced link target. Setter for _force_open_target.
|
||||||
@ -249,12 +299,13 @@ class BrowserTab(QWebView):
|
|||||||
return super().paintEvent(e)
|
return super().paintEvent(e)
|
||||||
|
|
||||||
def mousePressEvent(self, 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 does the following things:
|
||||||
|
- Check if a link was clicked with the middle button or Ctrl and
|
||||||
This also is a bit of a hack, but it seems it's the only possible way.
|
set the _open_target attribute accordingly.
|
||||||
Set the _open_target attribute accordingly.
|
- Emit the editable_elem_selected signal if an editable element was
|
||||||
|
clicked.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
e: The arrived event.
|
e: The arrived event.
|
||||||
@ -262,6 +313,20 @@ class BrowserTab(QWebView):
|
|||||||
Return:
|
Return:
|
||||||
The superclass return value.
|
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:
|
if self._force_open_target is not None:
|
||||||
self._open_target = self._force_open_target
|
self._open_target = self._force_open_target
|
||||||
self._force_open_target = None
|
self._force_open_target = None
|
||||||
|
@ -23,8 +23,9 @@ from PyQt5.QtWidgets import (QWidget, QLineEdit, QProgressBar, QLabel,
|
|||||||
QShortcut)
|
QShortcut)
|
||||||
from PyQt5.QtGui import QPainter, QKeySequence, QValidator
|
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
|
from qutebrowser.config.style import set_register_stylesheet, get_stylesheet
|
||||||
import qutebrowser.commands.keys as keys
|
|
||||||
from qutebrowser.utils.url import urlstring
|
from qutebrowser.utils.url import urlstring
|
||||||
from qutebrowser.commands.parsers import split_cmdline
|
from qutebrowser.commands.parsers import split_cmdline
|
||||||
from qutebrowser.models.cmdhistory import (History, HistoryEmptyError,
|
from qutebrowser.models.cmdhistory import (History, HistoryEmptyError,
|
||||||
@ -157,7 +158,7 @@ class StatusBar(QWidget):
|
|||||||
self.txt.errortext = ''
|
self.txt.errortext = ''
|
||||||
|
|
||||||
@pyqtSlot('QKeyEvent')
|
@pyqtSlot('QKeyEvent')
|
||||||
def keypress(self, e):
|
def on_key_pressed(self, e):
|
||||||
"""Hide temporary error message if a key was pressed.
|
"""Hide temporary error message if a key was pressed.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -169,6 +170,18 @@ class StatusBar(QWidget):
|
|||||||
self.txt.set_temptext('')
|
self.txt.set_temptext('')
|
||||||
self.clear_error()
|
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):
|
def resizeEvent(self, e):
|
||||||
"""Extend resizeEvent of QWidget to emit a resized signal afterwards.
|
"""Extend resizeEvent of QWidget to emit a resized signal afterwards.
|
||||||
|
|
||||||
@ -331,7 +344,7 @@ class _Command(QLineEdit):
|
|||||||
"""
|
"""
|
||||||
# FIXME we should consider the cursor position.
|
# FIXME we should consider the cursor position.
|
||||||
text = self.text()
|
text = self.text()
|
||||||
if text[0] in keys.STARTCHARS:
|
if text[0] in STARTCHARS:
|
||||||
prefix = text[0]
|
prefix = text[0]
|
||||||
text = text[1:]
|
text = text[1:]
|
||||||
else:
|
else:
|
||||||
@ -342,8 +355,18 @@ class _Command(QLineEdit):
|
|||||||
self.setFocus()
|
self.setFocus()
|
||||||
self.show_cmd.emit()
|
self.show_cmd.emit()
|
||||||
|
|
||||||
|
def focusInEvent(self, e):
|
||||||
|
"""Extend focusInEvent to enter command mode."""
|
||||||
|
modes.enter("command")
|
||||||
|
super().focusInEvent(e)
|
||||||
|
|
||||||
def focusOutEvent(self, 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:
|
Args:
|
||||||
e: The QFocusEvent.
|
e: The QFocusEvent.
|
||||||
@ -352,6 +375,7 @@ class _Command(QLineEdit):
|
|||||||
clear_completion_selection: Always emitted.
|
clear_completion_selection: Always emitted.
|
||||||
hide_completion: Always emitted so the completion is hidden.
|
hide_completion: Always emitted so the completion is hidden.
|
||||||
"""
|
"""
|
||||||
|
modes.leave("command")
|
||||||
if e.reason() in [Qt.MouseFocusReason, Qt.TabFocusReason,
|
if e.reason() in [Qt.MouseFocusReason, Qt.TabFocusReason,
|
||||||
Qt.BacktabFocusReason, Qt.OtherFocusReason]:
|
Qt.BacktabFocusReason, Qt.OtherFocusReason]:
|
||||||
self.setText('')
|
self.setText('')
|
||||||
@ -376,7 +400,7 @@ class _CommandValidator(QValidator):
|
|||||||
Return:
|
Return:
|
||||||
A tuple (status, string, pos) as a QValidator should.
|
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)
|
return (QValidator.Acceptable, string, pos)
|
||||||
else:
|
else:
|
||||||
return (QValidator.Invalid, string, pos)
|
return (QValidator.Invalid, string, pos)
|
||||||
|
@ -69,10 +69,6 @@ class TabbedBrowser(TabWidget):
|
|||||||
arg 2: y-position in %.
|
arg 2: y-position in %.
|
||||||
hint_strings_updated: Hint strings were updated.
|
hint_strings_updated: Hint strings were updated.
|
||||||
arg: A list of hint strings.
|
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.
|
shutdown_complete: The shuttdown is completed.
|
||||||
quit: The last tab was closed, quit application.
|
quit: The last tab was closed, quit application.
|
||||||
resized: Emitted when the browser window has resized, so the completion
|
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_link_hovered = pyqtSignal(str, str, str)
|
||||||
cur_scroll_perc_changed = pyqtSignal(int, int)
|
cur_scroll_perc_changed = pyqtSignal(int, int)
|
||||||
hint_strings_updated = pyqtSignal(list)
|
hint_strings_updated = pyqtSignal(list)
|
||||||
set_cmd_text = pyqtSignal(str)
|
|
||||||
set_mode = pyqtSignal(str)
|
|
||||||
keypress = pyqtSignal('QKeyEvent')
|
|
||||||
shutdown_complete = pyqtSignal()
|
shutdown_complete = pyqtSignal()
|
||||||
quit = pyqtSignal()
|
quit = pyqtSignal()
|
||||||
resized = pyqtSignal('QRect')
|
resized = pyqtSignal('QRect')
|
||||||
@ -143,8 +136,6 @@ class TabbedBrowser(TabWidget):
|
|||||||
tab.urlChanged.connect(self._filter.create(self.cur_url_changed))
|
tab.urlChanged.connect(self._filter.create(self.cur_url_changed))
|
||||||
# hintmanager
|
# hintmanager
|
||||||
tab.hintmanager.hint_strings_updated.connect(self.hint_strings_updated)
|
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
|
# misc
|
||||||
tab.titleChanged.connect(self.on_title_changed)
|
tab.titleChanged.connect(self.on_title_changed)
|
||||||
tab.open_tab.connect(self.tabopen)
|
tab.open_tab.connect(self.tabopen)
|
||||||
@ -242,23 +233,15 @@ class TabbedBrowser(TabWidget):
|
|||||||
|
|
||||||
@cmdutils.register(instance='mainwindow.tabs', hide=True)
|
@cmdutils.register(instance='mainwindow.tabs', hide=True)
|
||||||
def tabopencur(self):
|
def tabopencur(self):
|
||||||
"""Set the statusbar to :tabopen and the current URL.
|
"""Set the statusbar to :tabopen and the current URL."""
|
||||||
|
|
||||||
Emit:
|
|
||||||
set_cmd_text prefilled with :tabopen $URL
|
|
||||||
"""
|
|
||||||
url = urlutils.urlstring(self.currentWidget().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)
|
@cmdutils.register(instance='mainwindow.tabs', hide=True)
|
||||||
def opencur(self):
|
def opencur(self):
|
||||||
"""Set the statusbar to :open and the current URL.
|
"""Set the statusbar to :open and the current URL."""
|
||||||
|
|
||||||
Emit:
|
|
||||||
set_cmd_text prefilled with :open $URL
|
|
||||||
"""
|
|
||||||
url = urlutils.urlstring(self.currentWidget().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')
|
@cmdutils.register(instance='mainwindow.tabs', name='undo')
|
||||||
def undo_close(self):
|
def undo_close(self):
|
||||||
@ -360,18 +343,6 @@ class TabbedBrowser(TabWidget):
|
|||||||
else:
|
else:
|
||||||
logging.debug('ignoring title change')
|
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):
|
def resizeEvent(self, e):
|
||||||
"""Extend resizeEvent of QWidget to emit a resized signal afterwards.
|
"""Extend resizeEvent of QWidget to emit a resized signal afterwards.
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user