2014-06-19 09:04:37 +02:00
|
|
|
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
|
|
|
|
2014-04-19 17:50:11 +02:00
|
|
|
# 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/>.
|
|
|
|
|
|
|
|
"""A HintManager to draw hints over links."""
|
|
|
|
|
2014-04-20 23:58:14 +02:00
|
|
|
import math
|
2014-07-29 01:45:42 +02:00
|
|
|
import subprocess
|
2014-08-26 19:10:14 +02:00
|
|
|
import collections
|
2014-04-20 19:24:22 +02:00
|
|
|
|
2014-06-20 16:33:01 +02:00
|
|
|
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QEvent, Qt, QUrl
|
2014-04-21 23:33:36 +02:00
|
|
|
from PyQt5.QtGui import QMouseEvent, QClipboard
|
|
|
|
from PyQt5.QtWidgets import QApplication
|
2014-04-21 15:20:41 +02:00
|
|
|
|
2014-08-26 19:10:14 +02:00
|
|
|
from qutebrowser.config import config
|
|
|
|
from qutebrowser.keyinput import modeman
|
|
|
|
from qutebrowser.utils import message, webelem
|
2014-08-26 20:38:10 +02:00
|
|
|
from qutebrowser.commands import userscripts, cmdexc
|
2014-08-26 20:25:11 +02:00
|
|
|
from qutebrowser.utils import usertypes, log, qtutils
|
2014-04-21 15:20:41 +02:00
|
|
|
|
|
|
|
|
2014-08-26 19:10:14 +02:00
|
|
|
ElemTuple = collections.namedtuple('ElemTuple', 'elem, label')
|
2014-04-21 15:45:29 +02:00
|
|
|
|
|
|
|
|
2014-08-26 19:10:14 +02:00
|
|
|
Target = usertypes.enum('Target', 'normal', 'tab', 'tab_bg', 'yank',
|
|
|
|
'yank_primary', 'fill', 'rapid', 'download',
|
|
|
|
'userscript', 'spawn')
|
2014-05-05 07:45:36 +02:00
|
|
|
|
|
|
|
|
2014-06-07 18:07:09 +02:00
|
|
|
class HintContext:
|
|
|
|
|
|
|
|
"""Context namespace used for hinting.
|
|
|
|
|
|
|
|
Attributes:
|
|
|
|
frames: The QWebFrames to use.
|
|
|
|
elems: A mapping from keystrings to (elem, label) namedtuples.
|
|
|
|
baseurl: The URL of the current page.
|
|
|
|
target: What to do with the opened links.
|
|
|
|
normal/tab/tab_bg: Get passed to BrowserTab.
|
|
|
|
yank/yank_primary: Yank to clipboard/primary selection
|
2014-08-03 00:56:42 +02:00
|
|
|
fill: Fill commandline with link.
|
2014-06-07 18:07:09 +02:00
|
|
|
rapid: Rapid mode with background tabs
|
2014-06-19 17:58:46 +02:00
|
|
|
download: Download the link.
|
2014-07-29 01:45:42 +02:00
|
|
|
userscript: Call a custom userscript.
|
|
|
|
spawn: Spawn a simple command.
|
2014-06-07 18:07:09 +02:00
|
|
|
to_follow: The link to follow when enter is pressed.
|
|
|
|
connected_frames: The QWebFrames which are connected to a signal.
|
2014-07-29 01:45:42 +02:00
|
|
|
args: Custom arguments for userscript/spawn
|
2014-06-07 18:07:09 +02:00
|
|
|
"""
|
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
self.elems = {}
|
2014-09-02 21:54:07 +02:00
|
|
|
self.target = None
|
2014-06-07 18:07:09 +02:00
|
|
|
self.baseurl = None
|
|
|
|
self.to_follow = None
|
|
|
|
self.frames = []
|
|
|
|
self.connected_frames = []
|
2014-07-29 01:45:42 +02:00
|
|
|
self.args = []
|
2014-06-07 18:07:09 +02:00
|
|
|
|
2014-08-03 00:56:42 +02:00
|
|
|
def get_args(self, urlstr):
|
|
|
|
"""Get the arguments, with {hint-url} replaced by the given URL."""
|
|
|
|
args = []
|
|
|
|
for arg in self.args:
|
|
|
|
if arg == '{hint-url}':
|
|
|
|
args.append(urlstr)
|
|
|
|
else:
|
|
|
|
args.append(arg)
|
|
|
|
return args
|
|
|
|
|
2014-06-07 18:07:09 +02:00
|
|
|
|
2014-04-21 15:20:41 +02:00
|
|
|
class HintManager(QObject):
|
2014-04-19 17:50:11 +02:00
|
|
|
|
|
|
|
"""Manage drawing hints over links or other elements.
|
|
|
|
|
|
|
|
Class attributes:
|
2014-04-20 23:03:18 +02:00
|
|
|
HINT_CSS: The CSS template to use for hints.
|
2014-05-13 21:13:53 +02:00
|
|
|
HINT_TEXTS: Text displayed for different hinting modes.
|
2014-04-19 17:50:11 +02:00
|
|
|
|
|
|
|
Attributes:
|
2014-06-07 18:07:09 +02:00
|
|
|
_context: The HintContext for the current invocation.
|
2014-04-21 15:20:41 +02:00
|
|
|
|
|
|
|
Signals:
|
|
|
|
hint_strings_updated: Emitted when the possible hint strings changed.
|
2014-04-21 16:59:03 +02:00
|
|
|
arg: A list of hint strings.
|
|
|
|
mouse_event: Mouse event to be posted in the web view.
|
|
|
|
arg: A QMouseEvent
|
2014-05-15 16:27:34 +02:00
|
|
|
openurl: Open a new URL
|
2014-05-01 16:35:26 +02:00
|
|
|
arg 0: URL to open as QUrl.
|
|
|
|
arg 1: True if it should be opened in a new tab, else False.
|
2014-04-21 19:29:11 +02:00
|
|
|
set_open_target: Set a new target to open the links in.
|
2014-06-23 17:30:28 +02:00
|
|
|
download_get: Download an URL.
|
2014-07-10 06:42:52 +02:00
|
|
|
arg 0: The URL to download, as QUrl.
|
|
|
|
arg 1: The QWebPage to download the URL in.
|
2014-04-19 17:50:11 +02:00
|
|
|
"""
|
|
|
|
|
2014-04-19 23:16:39 +02:00
|
|
|
HINT_CSS = """
|
2014-05-06 17:02:32 +02:00
|
|
|
display: {display};
|
2014-04-20 19:24:22 +02:00
|
|
|
color: {config[colors][hints.fg]};
|
|
|
|
background: {config[colors][hints.bg]};
|
|
|
|
font: {config[fonts][hints]};
|
|
|
|
border: {config[hints][border]};
|
|
|
|
opacity: {config[hints][opacity]};
|
2014-04-20 16:42:55 +02:00
|
|
|
z-index: 100000;
|
2014-04-21 23:32:58 +02:00
|
|
|
pointer-events: none;
|
2014-04-19 23:16:39 +02:00
|
|
|
position: absolute;
|
|
|
|
left: {left}px;
|
|
|
|
top: {top}px;
|
|
|
|
"""
|
|
|
|
|
2014-05-13 21:13:53 +02:00
|
|
|
HINT_TEXTS = {
|
|
|
|
Target.normal: "Follow hint...",
|
|
|
|
Target.tab: "Follow hint in new tab...",
|
2014-05-16 23:01:40 +02:00
|
|
|
Target.tab_bg: "Follow hint in background tab...",
|
2014-05-13 21:13:53 +02:00
|
|
|
Target.yank: "Yank hint to clipboard...",
|
|
|
|
Target.yank_primary: "Yank hint to primary selection...",
|
2014-08-03 00:56:42 +02:00
|
|
|
Target.fill: "Set hint in commandline...",
|
2014-05-13 21:13:53 +02:00
|
|
|
Target.rapid: "Follow hint (rapid mode)...",
|
2014-06-19 17:58:46 +02:00
|
|
|
Target.download: "Download hint...",
|
2014-07-29 01:45:42 +02:00
|
|
|
Target.userscript: "Call userscript via hint...",
|
|
|
|
Target.spawn: "Spawn command via hint...",
|
2014-05-13 21:13:53 +02:00
|
|
|
}
|
|
|
|
|
2014-04-21 15:20:41 +02:00
|
|
|
hint_strings_updated = pyqtSignal(list)
|
2014-04-21 16:59:03 +02:00
|
|
|
mouse_event = pyqtSignal('QMouseEvent')
|
2014-05-01 16:35:26 +02:00
|
|
|
openurl = pyqtSignal('QUrl', bool)
|
2014-04-21 19:29:11 +02:00
|
|
|
set_open_target = pyqtSignal(str)
|
2014-07-10 06:42:52 +02:00
|
|
|
download_get = pyqtSignal('QUrl', 'QWebPage')
|
2014-04-21 15:20:41 +02:00
|
|
|
|
2014-04-22 14:23:55 +02:00
|
|
|
def __init__(self, parent=None):
|
2014-04-19 17:50:11 +02:00
|
|
|
"""Constructor.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
frame: The QWebFrame to use for finding elements and drawing.
|
|
|
|
"""
|
2014-04-22 14:23:55 +02:00
|
|
|
super().__init__(parent)
|
2014-06-07 18:07:09 +02:00
|
|
|
self._context = None
|
2014-05-05 17:56:14 +02:00
|
|
|
modeman.instance().left.connect(self.on_mode_left)
|
2014-05-22 17:49:18 +02:00
|
|
|
modeman.instance().entered.connect(self.on_mode_entered)
|
2014-04-19 23:16:39 +02:00
|
|
|
|
2014-08-04 03:14:14 +02:00
|
|
|
def _cleanup(self):
|
|
|
|
"""Clean up after hinting."""
|
|
|
|
for elem in self._context.elems.values():
|
|
|
|
if not elem.label.isNull():
|
|
|
|
elem.label.removeFromDocument()
|
2014-09-03 11:51:24 +02:00
|
|
|
text = self.HINT_TEXTS[self._context.target]
|
|
|
|
message.instance().maybe_reset_text(text)
|
2014-08-04 03:14:14 +02:00
|
|
|
self._context = None
|
|
|
|
|
2014-04-20 23:58:14 +02:00
|
|
|
def _hint_strings(self, elems):
|
|
|
|
"""Calculate the hint strings for elems.
|
|
|
|
|
2014-04-22 08:45:56 +02:00
|
|
|
Inspired by Vimium.
|
2014-04-21 00:24:08 +02:00
|
|
|
|
|
|
|
Args:
|
|
|
|
elems: The elements to get hint strings for.
|
|
|
|
|
|
|
|
Return:
|
|
|
|
A list of hint strings, in the same order as the elements.
|
2014-04-20 23:58:14 +02:00
|
|
|
"""
|
2014-05-02 17:53:16 +02:00
|
|
|
if config.get('hints', 'mode') == 'number':
|
|
|
|
chars = '0123456789'
|
|
|
|
else:
|
|
|
|
chars = config.get('hints', 'chars')
|
2014-04-20 23:58:14 +02:00
|
|
|
# Determine how many digits the link hints will require in the worst
|
|
|
|
# case. Usually we do not need all of these digits for every link
|
|
|
|
# single hint, so we can show shorter hints for a few of the links.
|
|
|
|
needed = math.ceil(math.log(len(elems), len(chars)))
|
|
|
|
# Short hints are the number of hints we can possibly show which are
|
|
|
|
# (needed - 1) digits in length.
|
|
|
|
short_count = math.floor((len(chars) ** needed - len(elems)) /
|
|
|
|
len(chars))
|
|
|
|
long_count = len(elems) - short_count
|
|
|
|
|
|
|
|
strings = []
|
|
|
|
|
|
|
|
if needed > 1:
|
|
|
|
for i in range(short_count):
|
|
|
|
strings.append(self._number_to_hint_str(i, chars, needed - 1))
|
|
|
|
|
|
|
|
start = short_count * len(chars)
|
|
|
|
for i in range(start, start + long_count):
|
|
|
|
strings.append(self._number_to_hint_str(i, chars, needed))
|
|
|
|
|
|
|
|
return self._shuffle_hints(strings, len(chars))
|
|
|
|
|
|
|
|
def _shuffle_hints(self, hints, length):
|
|
|
|
"""Shuffle the given set of hints so that they're scattered.
|
|
|
|
|
|
|
|
Hints starting with the same character will be spread evenly throughout
|
|
|
|
the array.
|
|
|
|
|
|
|
|
Inspired by Vimium.
|
2014-04-21 00:24:08 +02:00
|
|
|
|
|
|
|
Args:
|
|
|
|
hints: A list of hint strings.
|
|
|
|
length: Length of the available charset.
|
|
|
|
|
|
|
|
Return:
|
|
|
|
A list of shuffled hint strings.
|
2014-04-20 23:58:14 +02:00
|
|
|
"""
|
|
|
|
buckets = [[] for i in range(length)]
|
|
|
|
for i, hint in enumerate(hints):
|
|
|
|
buckets[i % len(buckets)].append(hint)
|
|
|
|
result = []
|
|
|
|
for bucket in buckets:
|
|
|
|
result += bucket
|
|
|
|
return result
|
|
|
|
|
|
|
|
def _number_to_hint_str(self, number, chars, digits=0):
|
|
|
|
"""Convert a number like "8" into a hint string like "JK".
|
|
|
|
|
|
|
|
This is used to sequentially generate all of the hint text.
|
|
|
|
The hint string will be "padded with zeroes" to ensure its length is >=
|
|
|
|
digits.
|
|
|
|
|
|
|
|
Inspired by Vimium.
|
2014-04-21 00:24:08 +02:00
|
|
|
|
|
|
|
Args:
|
|
|
|
number: The hint number.
|
|
|
|
chars: The charset to use.
|
|
|
|
digits: The minimum output length.
|
|
|
|
|
|
|
|
Return:
|
|
|
|
A hint string.
|
2014-04-20 23:58:14 +02:00
|
|
|
"""
|
|
|
|
base = len(chars)
|
|
|
|
hintstr = []
|
|
|
|
remainder = 0
|
|
|
|
while True:
|
|
|
|
remainder = number % base
|
|
|
|
hintstr.insert(0, chars[remainder])
|
|
|
|
number -= remainder
|
|
|
|
number //= base
|
|
|
|
if number <= 0:
|
|
|
|
break
|
|
|
|
# Pad the hint string we're returning so that it matches digits.
|
2014-04-21 00:24:08 +02:00
|
|
|
for _ in range(0, digits - len(hintstr)):
|
2014-04-20 23:58:14 +02:00
|
|
|
hintstr.insert(0, chars[0])
|
|
|
|
return ''.join(hintstr)
|
|
|
|
|
2014-05-06 17:02:32 +02:00
|
|
|
def _get_hint_css(self, elem, label=None):
|
|
|
|
"""Get the hint CSS for the element given.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
elem: The QWebElement to get the CSS for.
|
|
|
|
label: The label QWebElement if display: none should be preserved.
|
|
|
|
|
|
|
|
Return:
|
|
|
|
The CSS to set as a string.
|
|
|
|
"""
|
|
|
|
if label is None or label.attribute('hidden') != 'true':
|
|
|
|
display = 'inline'
|
|
|
|
else:
|
|
|
|
display = 'none'
|
|
|
|
rect = elem.geometry()
|
|
|
|
return self.HINT_CSS.format(left=rect.x(), top=rect.y(),
|
|
|
|
config=config.instance(), display=display)
|
|
|
|
|
2014-04-20 23:58:14 +02:00
|
|
|
def _draw_label(self, elem, string):
|
2014-04-21 00:24:08 +02:00
|
|
|
"""Draw a hint label over an element.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
elem: The QWebElement to use.
|
|
|
|
string: The hint string to print.
|
2014-04-21 15:45:29 +02:00
|
|
|
|
|
|
|
Return:
|
|
|
|
The newly created label elment
|
2014-04-21 00:24:08 +02:00
|
|
|
"""
|
2014-05-06 17:02:32 +02:00
|
|
|
css = self._get_hint_css(elem)
|
2014-05-12 07:49:44 +02:00
|
|
|
doc = elem.webFrame().documentElement()
|
2014-04-23 14:34:00 +02:00
|
|
|
# It seems impossible to create an empty QWebElement for which isNull()
|
|
|
|
# is false so we can work with it.
|
|
|
|
# As a workaround, we use appendInside() with markup as argument, and
|
|
|
|
# then use lastChild() to get a reference to it.
|
|
|
|
# See: http://stackoverflow.com/q/7364852/2085149
|
2014-04-20 23:58:14 +02:00
|
|
|
doc.appendInside('<span class="qutehint" style="{}">{}</span>'.format(
|
2014-04-22 09:35:49 +02:00
|
|
|
css, string))
|
2014-04-21 15:45:29 +02:00
|
|
|
return doc.lastChild()
|
2014-04-19 17:50:11 +02:00
|
|
|
|
2014-05-01 15:29:18 +02:00
|
|
|
def _click(self, elem):
|
2014-04-21 23:32:44 +02:00
|
|
|
"""Click an element.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
elem: The QWebElement to click.
|
|
|
|
"""
|
2014-06-07 18:07:09 +02:00
|
|
|
if self._context.target == Target.rapid:
|
2014-05-16 23:01:40 +02:00
|
|
|
target = Target.tab_bg
|
2014-04-29 09:09:42 +02:00
|
|
|
else:
|
2014-06-07 18:07:09 +02:00
|
|
|
target = self._context.target
|
2014-07-28 20:41:42 +02:00
|
|
|
self.set_open_target.emit(target.name)
|
2014-05-12 10:04:27 +02:00
|
|
|
# FIXME Instead of clicking the center, we could have nicer heuristics.
|
|
|
|
# e.g. parse (-webkit-)border-radius correctly and click text fields at
|
|
|
|
# the bottom right, and everything else on the top left or so.
|
2014-05-12 10:05:00 +02:00
|
|
|
pos = webelem.rect_on_view(elem).center()
|
2014-08-26 20:15:41 +02:00
|
|
|
log.hints.debug("Clicking on '{}' at {}/{}".format(elem.toPlainText(),
|
|
|
|
pos.x(), pos.y()))
|
2014-06-06 17:12:54 +02:00
|
|
|
events = (
|
2014-05-12 07:49:44 +02:00
|
|
|
QMouseEvent(QEvent.MouseMove, pos, Qt.NoButton, Qt.NoButton,
|
2014-04-21 23:32:44 +02:00
|
|
|
Qt.NoModifier),
|
2014-05-12 07:49:44 +02:00
|
|
|
QMouseEvent(QEvent.MouseButtonPress, pos, Qt.LeftButton,
|
2014-04-21 23:32:44 +02:00
|
|
|
Qt.NoButton, Qt.NoModifier),
|
2014-05-12 07:49:44 +02:00
|
|
|
QMouseEvent(QEvent.MouseButtonRelease, pos, Qt.LeftButton,
|
2014-04-21 23:32:44 +02:00
|
|
|
Qt.NoButton, Qt.NoModifier),
|
2014-06-06 17:12:54 +02:00
|
|
|
)
|
2014-04-21 23:32:44 +02:00
|
|
|
for evt in events:
|
|
|
|
self.mouse_event.emit(evt)
|
|
|
|
|
2014-06-20 16:33:01 +02:00
|
|
|
def _yank(self, url):
|
2014-04-21 23:32:44 +02:00
|
|
|
"""Yank an element to the clipboard or primary selection.
|
|
|
|
|
|
|
|
Args:
|
2014-06-20 16:33:01 +02:00
|
|
|
url: The URL to open as a QURL.
|
2014-04-21 23:32:44 +02:00
|
|
|
"""
|
2014-08-26 19:23:06 +02:00
|
|
|
qtutils.ensure_valid(url)
|
2014-06-07 18:07:09 +02:00
|
|
|
sel = self._context.target == Target.yank_primary
|
2014-04-21 23:32:44 +02:00
|
|
|
mode = QClipboard.Selection if sel else QClipboard.Clipboard
|
2014-06-20 22:57:04 +02:00
|
|
|
urlstr = url.toString(QUrl.FullyEncoded | QUrl.RemovePassword)
|
2014-06-20 16:33:01 +02:00
|
|
|
QApplication.clipboard().setText(urlstr, mode)
|
2014-04-25 16:53:23 +02:00
|
|
|
message.info("URL yanked to {}".format("primary selection" if sel
|
|
|
|
else "clipboard"))
|
2014-04-21 23:32:44 +02:00
|
|
|
|
2014-06-20 16:33:01 +02:00
|
|
|
def _preset_cmd_text(self, url):
|
2014-04-29 09:09:42 +02:00
|
|
|
"""Preset a commandline text based on a hint URL.
|
|
|
|
|
|
|
|
Args:
|
2014-06-20 16:33:01 +02:00
|
|
|
url: The URL to open as a QUrl.
|
2014-04-29 09:09:42 +02:00
|
|
|
"""
|
2014-08-26 19:23:06 +02:00
|
|
|
qtutils.ensure_valid(url)
|
2014-06-21 23:19:59 +02:00
|
|
|
urlstr = url.toDisplayString(QUrl.FullyEncoded)
|
2014-08-03 00:56:42 +02:00
|
|
|
args = self._context.get_args(urlstr)
|
|
|
|
message.set_cmd_text(' '.join(args))
|
2014-04-29 09:09:42 +02:00
|
|
|
|
2014-07-10 06:42:52 +02:00
|
|
|
def _download(self, elem):
|
2014-06-19 17:58:46 +02:00
|
|
|
"""Download a hint URL.
|
|
|
|
|
|
|
|
Args:
|
2014-07-10 06:42:52 +02:00
|
|
|
elem: The QWebElement to download.
|
2014-06-19 17:58:46 +02:00
|
|
|
"""
|
2014-07-10 06:42:52 +02:00
|
|
|
url = self._resolve_url(elem)
|
|
|
|
if url is None:
|
|
|
|
message.error("No suitable link found for this element.",
|
|
|
|
immediately=True)
|
|
|
|
return
|
2014-08-26 19:23:06 +02:00
|
|
|
qtutils.ensure_valid(url)
|
2014-07-10 06:42:52 +02:00
|
|
|
self.download_get.emit(url, elem.webFrame().page())
|
2014-06-19 17:58:46 +02:00
|
|
|
|
2014-07-29 01:45:42 +02:00
|
|
|
def _call_userscript(self, url):
|
|
|
|
"""Call an userscript from a hint."""
|
2014-08-26 19:23:06 +02:00
|
|
|
qtutils.ensure_valid(url)
|
2014-07-29 02:05:15 +02:00
|
|
|
cmd = self._context.args[0]
|
|
|
|
args = self._context.args[1:]
|
|
|
|
userscripts.run(cmd, *args, url=url)
|
2014-07-29 01:45:42 +02:00
|
|
|
|
|
|
|
def _spawn(self, url):
|
|
|
|
"""Spawn a simple command from a hint."""
|
2014-08-26 19:23:06 +02:00
|
|
|
qtutils.ensure_valid(url)
|
2014-07-29 01:45:42 +02:00
|
|
|
urlstr = url.toString(QUrl.FullyEncoded | QUrl.RemovePassword)
|
2014-08-03 00:56:42 +02:00
|
|
|
args = self._context.get_args(urlstr)
|
2014-07-29 01:45:42 +02:00
|
|
|
subprocess.Popen(args)
|
|
|
|
|
2014-06-20 16:33:01 +02:00
|
|
|
def _resolve_url(self, elem, baseurl=None):
|
|
|
|
"""Resolve a URL and check if we want to keep it.
|
2014-04-21 23:53:13 +02:00
|
|
|
|
|
|
|
Args:
|
2014-06-20 16:33:01 +02:00
|
|
|
elem: The QWebElement to get the URL of.
|
2014-06-07 18:07:09 +02:00
|
|
|
baseurl: The baseurl of the current tab (overrides baseurl from
|
|
|
|
self._context).
|
2014-04-21 23:53:13 +02:00
|
|
|
|
|
|
|
Return:
|
2014-06-20 16:33:01 +02:00
|
|
|
A QUrl with the absolute URL, or None.
|
2014-04-21 23:53:13 +02:00
|
|
|
"""
|
2014-06-20 16:33:01 +02:00
|
|
|
text = elem.attribute('href')
|
|
|
|
if not text:
|
2014-04-21 23:53:13 +02:00
|
|
|
return None
|
2014-05-01 16:40:14 +02:00
|
|
|
if baseurl is None:
|
2014-06-07 18:07:09 +02:00
|
|
|
baseurl = self._context.baseurl
|
2014-06-20 16:33:01 +02:00
|
|
|
url = QUrl(text)
|
|
|
|
if url.isRelative():
|
|
|
|
url = baseurl.resolved(url)
|
2014-08-26 19:23:06 +02:00
|
|
|
qtutils.ensure_valid(url)
|
2014-06-20 16:33:01 +02:00
|
|
|
return url
|
2014-04-21 23:53:13 +02:00
|
|
|
|
2014-05-01 16:35:26 +02:00
|
|
|
def _find_prevnext(self, frame, prev=False):
|
|
|
|
"""Find a prev/next element in frame."""
|
2014-04-30 18:01:03 +02:00
|
|
|
# First check for <link rel="prev(ious)|next">
|
2014-05-06 21:09:09 +02:00
|
|
|
elems = frame.findAllElements(
|
2014-07-16 09:17:59 +02:00
|
|
|
webelem.SELECTORS[webelem.Group.links])
|
2014-06-06 17:12:54 +02:00
|
|
|
rel_values = ('prev', 'previous') if prev else ('next')
|
2014-04-30 18:01:03 +02:00
|
|
|
for e in elems:
|
|
|
|
if e.attribute('rel') in rel_values:
|
2014-05-01 16:35:26 +02:00
|
|
|
return e
|
2014-07-16 09:17:59 +02:00
|
|
|
# Then check for regular links/buttons.
|
2014-05-06 21:09:09 +02:00
|
|
|
elems = frame.findAllElements(
|
|
|
|
webelem.SELECTORS[webelem.Group.prevnext])
|
2014-04-30 18:01:03 +02:00
|
|
|
option = 'prev-regexes' if prev else 'next-regexes'
|
2014-05-01 16:35:26 +02:00
|
|
|
if not elems:
|
|
|
|
return None
|
|
|
|
for regex in config.get('hints', option):
|
|
|
|
for e in elems:
|
2014-07-09 21:14:15 +02:00
|
|
|
if regex.search(e.toPlainText()):
|
2014-05-01 16:35:26 +02:00
|
|
|
return e
|
|
|
|
return None
|
|
|
|
|
2014-05-13 21:13:53 +02:00
|
|
|
def _connect_frame_signals(self):
|
|
|
|
"""Connect the contentsSizeChanged signals to all frames."""
|
2014-06-07 18:07:09 +02:00
|
|
|
for f in self._context.frames:
|
2014-05-13 21:13:53 +02:00
|
|
|
# For some reason we get segfaults sometimes when calling
|
|
|
|
# frame.contentsSizeChanged.disconnect() later, maybe because Qt
|
|
|
|
# already deleted the frame?
|
|
|
|
# We work around this by never disconnecting this signal, and here
|
|
|
|
# making sure we don't connect a frame which already was connected
|
|
|
|
# at some point earlier.
|
2014-06-07 18:07:09 +02:00
|
|
|
if f in self._context.connected_frames:
|
2014-08-26 20:15:41 +02:00
|
|
|
log.hints.debug("Frame {} already connected!".format(f))
|
2014-05-13 21:13:53 +02:00
|
|
|
else:
|
2014-08-26 20:15:41 +02:00
|
|
|
log.hints.debug("Connecting frame {}".format(f))
|
2014-05-13 21:13:53 +02:00
|
|
|
f.contentsSizeChanged.connect(self.on_contents_size_changed)
|
2014-06-07 18:07:09 +02:00
|
|
|
self._context.connected_frames.append(f)
|
2014-05-13 21:13:53 +02:00
|
|
|
|
2014-05-01 16:40:14 +02:00
|
|
|
def follow_prevnext(self, frame, baseurl, prev=False, newtab=False):
|
2014-05-01 16:35:26 +02:00
|
|
|
"""Click a "previous"/"next" element on the page.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
frame: The frame where the element is in.
|
2014-05-01 16:40:14 +02:00
|
|
|
baseurl: The base URL of the current tab.
|
2014-05-01 16:35:26 +02:00
|
|
|
prev: True to open a "previous" link, False to open a "next" link.
|
|
|
|
newtab: True to open in a new tab, False for the current tab.
|
|
|
|
"""
|
|
|
|
elem = self._find_prevnext(frame, prev)
|
|
|
|
if elem is None:
|
2014-08-26 19:10:14 +02:00
|
|
|
raise cmdexc.CommandError("No {} links found!".format(
|
2014-05-14 18:00:40 +02:00
|
|
|
"prev" if prev else "forward"))
|
2014-06-20 16:33:01 +02:00
|
|
|
url = self._resolve_url(elem, baseurl)
|
2014-06-20 23:57:52 +02:00
|
|
|
if url is None or not url.isValid():
|
2014-08-26 19:10:14 +02:00
|
|
|
raise cmdexc.CommandError("No {} links found!".format(
|
2014-05-14 18:00:40 +02:00
|
|
|
"prev" if prev else "forward"))
|
2014-06-20 16:33:01 +02:00
|
|
|
self.openurl.emit(url, newtab)
|
2014-04-30 18:01:03 +02:00
|
|
|
|
2014-05-12 10:04:27 +02:00
|
|
|
def start(self, mainframe, baseurl, group=webelem.Group.all,
|
2014-07-29 01:45:42 +02:00
|
|
|
target=Target.normal, *args):
|
2014-04-19 17:50:11 +02:00
|
|
|
"""Start hinting.
|
|
|
|
|
|
|
|
Args:
|
2014-05-12 10:04:27 +02:00
|
|
|
mainframe: The main QWebFrame.
|
2014-04-21 23:44:45 +02:00
|
|
|
baseurl: URL of the current page.
|
2014-05-05 07:45:36 +02:00
|
|
|
group: Which group of elements to hint.
|
2014-04-21 23:32:44 +02:00
|
|
|
target: What to do with the link. See attribute docstring.
|
2014-07-29 01:45:42 +02:00
|
|
|
*args: Arguments for userscript/download
|
2014-04-21 23:32:44 +02:00
|
|
|
|
|
|
|
Emit:
|
|
|
|
hint_strings_updated: Emitted to update keypraser.
|
2014-04-19 17:50:11 +02:00
|
|
|
"""
|
2014-07-29 00:23:20 +02:00
|
|
|
if not isinstance(target, Target):
|
|
|
|
raise TypeError("Target {} is no Target member!".format(target))
|
2014-05-12 10:04:27 +02:00
|
|
|
if mainframe is None:
|
2014-05-02 18:57:59 +02:00
|
|
|
# This should never happen since we check frame before calling
|
|
|
|
# start. But since we had a bug where frame is None in
|
|
|
|
# on_mode_left, we are extra careful here.
|
|
|
|
raise ValueError("start() was called with frame=None")
|
2014-08-03 00:56:42 +02:00
|
|
|
if target in (Target.userscript, Target.spawn, Target.fill):
|
2014-07-29 01:45:42 +02:00
|
|
|
if not args:
|
2014-08-26 19:10:14 +02:00
|
|
|
raise cmdexc.CommandError(
|
|
|
|
"Additional arguments are required with target "
|
|
|
|
"userscript/spawn/fill.")
|
2014-07-29 01:45:42 +02:00
|
|
|
else:
|
|
|
|
if args:
|
2014-08-26 19:10:14 +02:00
|
|
|
raise cmdexc.CommandError(
|
|
|
|
"Arguments are only allowed with target userscript/spawn.")
|
2014-05-12 07:49:44 +02:00
|
|
|
elems = []
|
2014-06-07 18:07:09 +02:00
|
|
|
ctx = HintContext()
|
|
|
|
ctx.frames = webelem.get_child_frames(mainframe)
|
|
|
|
for f in ctx.frames:
|
2014-05-12 07:49:44 +02:00
|
|
|
elems += f.findAllElements(webelem.SELECTORS[group])
|
2014-05-05 07:45:36 +02:00
|
|
|
filterfunc = webelem.FILTERS.get(group, lambda e: True)
|
2014-05-13 21:13:53 +02:00
|
|
|
visible_elems = [e for e in elems if filterfunc(e) and
|
|
|
|
webelem.is_visible(e, mainframe)]
|
2014-04-21 19:31:28 +02:00
|
|
|
if not visible_elems:
|
2014-08-26 19:10:14 +02:00
|
|
|
raise cmdexc.CommandError("No elements found.")
|
2014-06-07 18:07:09 +02:00
|
|
|
ctx.target = target
|
|
|
|
ctx.baseurl = baseurl
|
2014-07-29 01:45:42 +02:00
|
|
|
ctx.args = args
|
2014-06-26 07:58:00 +02:00
|
|
|
message.instance().set_text(self.HINT_TEXTS[target])
|
2014-04-21 15:45:29 +02:00
|
|
|
strings = self._hint_strings(visible_elems)
|
|
|
|
for e, string in zip(visible_elems, strings):
|
|
|
|
label = self._draw_label(e, string)
|
2014-06-07 18:07:09 +02:00
|
|
|
ctx.elems[string] = ElemTuple(e, label)
|
|
|
|
self._context = ctx
|
2014-05-13 21:13:53 +02:00
|
|
|
self._connect_frame_signals()
|
2014-04-21 15:20:41 +02:00
|
|
|
self.hint_strings_updated.emit(strings)
|
2014-08-04 03:14:14 +02:00
|
|
|
try:
|
2014-08-26 19:10:14 +02:00
|
|
|
modeman.enter(usertypes.KeyMode.hint, 'HintManager.start')
|
2014-08-04 03:14:14 +02:00
|
|
|
except modeman.ModeLockedError:
|
|
|
|
self._cleanup()
|
2014-04-20 17:25:46 +02:00
|
|
|
|
2014-04-21 15:20:41 +02:00
|
|
|
def handle_partial_key(self, keystr):
|
|
|
|
"""Handle a new partial keypress."""
|
2014-08-26 20:15:41 +02:00
|
|
|
log.hints.debug("Handling new keystring: '{}'".format(keystr))
|
2014-06-07 18:07:09 +02:00
|
|
|
for (string, elems) in self._context.elems.items():
|
2014-04-21 15:45:29 +02:00
|
|
|
if string.startswith(keystr):
|
|
|
|
matched = string[:len(keystr)]
|
|
|
|
rest = string[len(keystr):]
|
|
|
|
elems.label.setInnerXml('<font color="{}">{}</font>{}'.format(
|
2014-04-25 16:53:23 +02:00
|
|
|
config.get('colors', 'hints.fg.match'), matched, rest))
|
2014-05-06 17:02:32 +02:00
|
|
|
if elems.label.attribute('hidden') == 'true':
|
|
|
|
# hidden element which matches again -> unhide it
|
|
|
|
elems.label.setAttribute('hidden', 'false')
|
|
|
|
css = self._get_hint_css(elems.elem, elems.label)
|
|
|
|
elems.label.setAttribute('style', css)
|
2014-04-21 15:45:29 +02:00
|
|
|
else:
|
2014-05-06 17:02:32 +02:00
|
|
|
# element doesn't match anymore -> hide it
|
|
|
|
elems.label.setAttribute('hidden', 'true')
|
|
|
|
css = self._get_hint_css(elems.elem, elems.label)
|
|
|
|
elems.label.setAttribute('style', css)
|
2014-04-21 15:20:41 +02:00
|
|
|
|
2014-05-02 17:53:16 +02:00
|
|
|
def filter_hints(self, filterstr):
|
|
|
|
"""Filter displayed hints according to a text."""
|
2014-06-07 18:07:09 +02:00
|
|
|
for elems in self._context.elems.values():
|
2014-05-06 17:02:32 +02:00
|
|
|
if elems.elem.toPlainText().lower().startswith(filterstr):
|
|
|
|
if elems.label.attribute('hidden') == 'true':
|
|
|
|
# hidden element which matches again -> unhide it
|
|
|
|
elems.label.setAttribute('hidden', 'false')
|
|
|
|
css = self._get_hint_css(elems.elem, elems.label)
|
|
|
|
elems.label.setAttribute('style', css)
|
|
|
|
else:
|
|
|
|
# element doesn't match anymore -> hide it
|
|
|
|
elems.label.setAttribute('hidden', 'true')
|
|
|
|
css = self._get_hint_css(elems.elem, elems.label)
|
|
|
|
elems.label.setAttribute('style', css)
|
|
|
|
visible = {}
|
2014-06-07 18:07:09 +02:00
|
|
|
for k, e in self._context.elems.items():
|
2014-05-06 17:02:32 +02:00
|
|
|
if e.label.attribute('hidden') != 'true':
|
|
|
|
visible[k] = e
|
|
|
|
if not visible:
|
2014-05-02 17:53:16 +02:00
|
|
|
# Whoops, filtered all hints
|
2014-08-26 19:10:14 +02:00
|
|
|
modeman.leave(usertypes.KeyMode.hint, 'all filtered')
|
2014-05-06 17:02:32 +02:00
|
|
|
elif len(visible) == 1 and config.get('hints', 'auto-follow'):
|
2014-05-02 17:53:16 +02:00
|
|
|
# unpacking gets us the first (and only) key in the dict.
|
2014-05-06 17:02:32 +02:00
|
|
|
self.fire(*visible)
|
2014-05-02 17:53:16 +02:00
|
|
|
|
2014-04-27 21:59:23 +02:00
|
|
|
def fire(self, keystr, force=False):
|
|
|
|
"""Fire a completed hint.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
keystr: The keychain string to follow.
|
|
|
|
force: When True, follow even when auto-follow is false.
|
|
|
|
"""
|
|
|
|
if not (force or config.get('hints', 'auto-follow')):
|
|
|
|
self.handle_partial_key(keystr)
|
2014-06-07 18:07:09 +02:00
|
|
|
self._context.to_follow = keystr
|
2014-04-27 21:59:23 +02:00
|
|
|
return
|
2014-04-29 09:09:42 +02:00
|
|
|
# Handlers which take a QWebElement
|
|
|
|
elem_handlers = {
|
2014-05-05 07:45:36 +02:00
|
|
|
Target.normal: self._click,
|
|
|
|
Target.tab: self._click,
|
2014-05-16 23:01:40 +02:00
|
|
|
Target.tab_bg: self._click,
|
2014-05-05 07:45:36 +02:00
|
|
|
Target.rapid: self._click,
|
2014-07-10 06:42:52 +02:00
|
|
|
# _download needs a QWebElement to get the frame.
|
|
|
|
Target.download: self._download,
|
2014-04-29 09:09:42 +02:00
|
|
|
}
|
2014-06-20 16:33:01 +02:00
|
|
|
# Handlers which take a QUrl
|
|
|
|
url_handlers = {
|
2014-05-05 07:45:36 +02:00
|
|
|
Target.yank: self._yank,
|
|
|
|
Target.yank_primary: self._yank,
|
2014-08-03 00:56:42 +02:00
|
|
|
Target.fill: self._preset_cmd_text,
|
2014-07-29 01:45:42 +02:00
|
|
|
Target.userscript: self._call_userscript,
|
|
|
|
Target.spawn: self._spawn,
|
2014-04-29 09:09:42 +02:00
|
|
|
}
|
2014-06-07 18:07:09 +02:00
|
|
|
elem = self._context.elems[keystr].elem
|
|
|
|
if self._context.target in elem_handlers:
|
|
|
|
elem_handlers[self._context.target](elem)
|
2014-06-20 16:33:01 +02:00
|
|
|
elif self._context.target in url_handlers:
|
|
|
|
url = self._resolve_url(elem)
|
|
|
|
if url is None:
|
2014-06-26 07:58:00 +02:00
|
|
|
message.error("No suitable link found for this element.",
|
|
|
|
immediately=True)
|
2014-06-21 17:41:25 +02:00
|
|
|
return
|
2014-06-20 16:33:01 +02:00
|
|
|
url_handlers[self._context.target](url)
|
2014-05-05 07:45:36 +02:00
|
|
|
else:
|
|
|
|
raise ValueError("No suitable handler found!")
|
2014-06-07 18:07:09 +02:00
|
|
|
if self._context.target != Target.rapid:
|
2014-08-26 19:10:14 +02:00
|
|
|
modeman.maybe_leave(usertypes.KeyMode.hint, 'followed')
|
2014-04-22 09:35:59 +02:00
|
|
|
|
2014-04-27 21:59:23 +02:00
|
|
|
def follow_hint(self):
|
2014-04-28 00:05:14 +02:00
|
|
|
"""Follow the currently selected hint."""
|
2014-06-07 18:07:09 +02:00
|
|
|
if not self._context.to_follow:
|
2014-08-26 19:10:14 +02:00
|
|
|
raise cmdexc.CommandError("No hint to follow")
|
2014-06-07 18:07:09 +02:00
|
|
|
self.fire(self._context.to_follow, force=True)
|
2014-04-27 21:59:23 +02:00
|
|
|
|
2014-04-22 09:35:59 +02:00
|
|
|
@pyqtSlot('QSize')
|
2014-04-22 17:53:27 +02:00
|
|
|
def on_contents_size_changed(self, _size):
|
2014-04-22 09:35:59 +02:00
|
|
|
"""Reposition hints if contents size changed."""
|
2014-06-07 18:07:09 +02:00
|
|
|
if self._context is None:
|
2014-05-13 13:42:30 +02:00
|
|
|
# We got here because of some earlier hinting, but we can't simply
|
|
|
|
# disconnect frames as this leads to occasional segfaults :-/
|
2014-08-26 20:15:41 +02:00
|
|
|
log.hints.debug("Not hinting!")
|
2014-05-13 13:42:30 +02:00
|
|
|
return
|
2014-08-26 20:15:41 +02:00
|
|
|
log.hints.debug("Contents size changed...!")
|
2014-06-07 18:07:09 +02:00
|
|
|
for elems in self._context.elems.values():
|
2014-05-06 17:02:32 +02:00
|
|
|
css = self._get_hint_css(elems.elem, elems.label)
|
2014-04-25 16:53:23 +02:00
|
|
|
elems.label.setAttribute('style', css)
|
2014-04-24 23:47:02 +02:00
|
|
|
|
2014-08-26 19:10:14 +02:00
|
|
|
@pyqtSlot(usertypes.KeyMode)
|
2014-05-22 17:49:18 +02:00
|
|
|
def on_mode_entered(self, mode):
|
|
|
|
"""Stop hinting when insert mode was entered."""
|
2014-08-26 19:10:14 +02:00
|
|
|
if mode == usertypes.KeyMode.insert:
|
|
|
|
modeman.maybe_leave(usertypes.KeyMode.hint, 'insert mode')
|
2014-05-22 17:49:18 +02:00
|
|
|
|
2014-08-26 19:10:14 +02:00
|
|
|
@pyqtSlot(usertypes.KeyMode)
|
2014-04-24 23:47:02 +02:00
|
|
|
def on_mode_left(self, mode):
|
|
|
|
"""Stop hinting when hinting mode was left."""
|
2014-08-26 19:10:14 +02:00
|
|
|
if mode != usertypes.KeyMode.hint or self._context is None:
|
2014-06-07 18:07:09 +02:00
|
|
|
# We have one HintManager per tab, so when this gets called,
|
|
|
|
# self._context might be None, because the current tab is not
|
|
|
|
# hinting.
|
2014-04-24 23:47:02 +02:00
|
|
|
return
|
2014-08-04 03:14:14 +02:00
|
|
|
self._cleanup()
|