qutebrowser/qutebrowser/browser/hints.py

546 lines
21 KiB
Python
Raw Normal View History

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-04-21 15:45:29 +02:00
from collections import namedtuple
2014-04-20 19:24:22 +02:00
2014-05-08 20:36:23 +02:00
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QEvent, Qt
from PyQt5.QtGui import QMouseEvent, QClipboard
from PyQt5.QtWidgets import QApplication
2014-04-21 15:20:41 +02:00
2014-04-20 19:24:22 +02:00
import qutebrowser.config.config as config
2014-04-25 12:21:01 +02:00
import qutebrowser.keyinput.modeman as modeman
import qutebrowser.utils.message as message
import qutebrowser.utils.url as urlutils
import qutebrowser.utils.webelem as webelem
2014-05-14 18:00:40 +02:00
from qutebrowser.commands.exceptions import CommandError
2014-05-05 07:45:36 +02:00
from qutebrowser.utils.usertypes import enum
2014-05-23 16:11:55 +02:00
from qutebrowser.utils.log import hints as logger
2014-04-21 15:20:41 +02:00
2014-04-21 15:45:29 +02:00
ElemTuple = namedtuple('ElemTuple', 'elem, label')
2014-05-16 23:01:40 +02:00
Target = enum('normal', 'tab', 'tab_bg', 'yank', 'yank_primary', 'cmd',
'cmd_tab', 'cmd_tab_bg', 'rapid')
2014-05-05 07:45:36 +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-05-13 13:42:30 +02:00
_started: Whether we started hinting at the moment.
2014-05-12 07:49:44 +02:00
_frames: The QWebFrames to use.
2014-04-21 15:45:29 +02:00
_elems: A mapping from keystrings to (elem, label) namedtuples.
_baseurl: The URL of the current page.
2014-04-21 19:29:11 +02:00
_target: What to do with the opened links.
2014-05-16 23:01:40 +02:00
normal/tab/tab_bg: Get passed to BrowserTab.
2014-05-05 07:45:36 +02:00
yank/yank_primary: Yank to clipboard/primary selection
2014-05-16 23:01:40 +02:00
cmd/cmd_tab/cmd_tab_bg: Enter link to commandline
2014-05-05 07:45:36 +02:00
rapid: Rapid mode with background tabs
2014-04-27 21:59:23 +02:00
_to_follow: The link to follow when enter is pressed.
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
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-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...",
Target.cmd: "Set hint in commandline...",
Target.cmd_tab: "Set hint in commandline as new tab...",
2014-05-16 23:01:40 +02:00
Target.cmd_tab_bg: "Set hint in commandline as background tab...",
2014-05-13 21:13:53 +02:00
Target.rapid: "Follow hint (rapid mode)...",
}
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-04-21 15:20:41 +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.
"""
super().__init__(parent)
2014-04-21 15:45:29 +02:00
self._elems = {}
2014-04-21 19:29:11 +02:00
self._target = None
2014-04-21 23:53:13 +02:00
self._baseurl = None
2014-04-27 21:59:23 +02:00
self._to_follow = None
2014-05-13 13:42:30 +02:00
self._started = False
2014-05-12 07:49:44 +02:00
self._frames = []
2014-05-13 13:42:30 +02:00
self._connected_frames = []
2014-05-05 17:56:14 +02:00
modeman.instance().left.connect(self.on_mode_left)
modeman.instance().entered.connect(self.on_mode_entered)
2014-04-19 23:16:39 +02:00
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):
"""Click an element.
Args:
elem: The QWebElement to click.
"""
2014-05-05 07:45:36 +02:00
if self._target == Target.rapid:
2014-05-16 23:01:40 +02:00
target = Target.tab_bg
else:
target = self._target
self.set_open_target.emit(Target[target])
# 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-05-23 16:11:55 +02:00
logger.debug("Clicking on '{}' at {}/{}".format(elem.toPlainText(),
pos.x(), pos.y()))
events = (
2014-05-12 07:49:44 +02:00
QMouseEvent(QEvent.MouseMove, pos, Qt.NoButton, Qt.NoButton,
Qt.NoModifier),
2014-05-12 07:49:44 +02:00
QMouseEvent(QEvent.MouseButtonPress, pos, Qt.LeftButton,
Qt.NoButton, Qt.NoModifier),
2014-05-12 07:49:44 +02:00
QMouseEvent(QEvent.MouseButtonRelease, pos, Qt.LeftButton,
Qt.NoButton, Qt.NoModifier),
)
for evt in events:
self.mouse_event.emit(evt)
def _yank(self, link):
"""Yank an element to the clipboard or primary selection.
Args:
link: The URL to open.
"""
2014-05-05 07:45:36 +02:00
sel = self._target == Target.yank_primary
mode = QClipboard.Selection if sel else QClipboard.Clipboard
QApplication.clipboard().setText(urlutils.urlstring(link), mode)
2014-04-25 16:53:23 +02:00
message.info("URL yanked to {}".format("primary selection" if sel
else "clipboard"))
def _preset_cmd_text(self, link):
"""Preset a commandline text based on a hint URL.
Args:
link: The link to open.
"""
commands = {
2014-05-05 07:45:36 +02:00
Target.cmd: 'open',
2014-05-16 23:01:40 +02:00
Target.cmd_tab: 'open-tab',
Target.cmd_tab_bg: 'open-tab-bg',
}
message.set_cmd_text(':{} {}'.format(commands[self._target],
urlutils.urlstring(link)))
2014-05-01 16:40:14 +02:00
def _resolve_link(self, elem, baseurl=None):
2014-04-21 23:53:13 +02:00
"""Resolve a link and check if we want to keep it.
Args:
elem: The QWebElement to get the link of.
2014-05-01 16:40:14 +02:00
baseurl: The baseurl of the current tab (overrides self._baseurl).
2014-04-21 23:53:13 +02:00
Return:
A QUrl with the absolute link, or None.
"""
link = elem.attribute('href')
if not link:
return None
2014-05-01 16:40:14 +02:00
if baseurl is None:
baseurl = self._baseurl
2014-04-21 23:53:13 +02:00
link = urlutils.qurl(link)
if link.isRelative():
2014-05-01 16:40:14 +02:00
link = baseurl.resolved(link)
2014-04-21 23:53:13 +02:00
return link
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(
webelem.SELECTORS[webelem.Group.prevnext_rel])
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-04-30 18:01:03 +02:00
# Then check for regular links
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:
if regex.match(e.toPlainText()):
return e
return None
2014-05-13 21:13:53 +02:00
def _connect_frame_signals(self):
"""Connect the contentsSizeChanged signals to all frames."""
for f in self._frames:
# 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.
if f in self._connected_frames:
2014-05-23 16:11:55 +02:00
logger.debug("Frame {} already connected!".format(f))
2014-05-13 21:13:53 +02:00
else:
2014-05-23 16:11:55 +02:00
logger.debug("Connecting frame {}".format(f))
2014-05-13 21:13:53 +02:00
f.contentsSizeChanged.connect(self.on_contents_size_changed)
self._connected_frames.append(f)
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-05-14 18:00:40 +02:00
raise CommandError("No {} links found!".format(
"prev" if prev else "forward"))
2014-05-01 16:40:14 +02:00
link = self._resolve_link(elem, baseurl)
2014-05-01 16:35:26 +02:00
if link is None:
2014-05-14 18:00:40 +02:00
raise CommandError("No {} links found!".format(
"prev" if prev else "forward"))
2014-05-01 16:35:26 +02:00
self.openurl.emit(link, newtab)
2014-04-30 18:01:03 +02:00
def start(self, mainframe, baseurl, group=webelem.Group.all,
2014-05-05 07:45:36 +02:00
target=Target.normal):
2014-04-19 17:50:11 +02:00
"""Start hinting.
Args:
mainframe: The main QWebFrame.
baseurl: URL of the current page.
2014-05-05 07:45:36 +02:00
group: Which group of elements to hint.
target: What to do with the link. See attribute docstring.
Emit:
hint_strings_updated: Emitted to update keypraser.
2014-04-19 17:50:11 +02:00
"""
if mainframe is None:
# 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-05-12 07:49:44 +02:00
elems = []
self._frames = webelem.get_child_frames(mainframe)
2014-05-12 07:49:44 +02:00
for f in self._frames:
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)]
if not visible_elems:
2014-05-14 18:00:40 +02:00
raise CommandError("No elements found.")
2014-05-13 21:13:53 +02:00
self._target = target
self._baseurl = baseurl
message.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)
self._elems[string] = ElemTuple(e, label)
2014-05-13 13:42:30 +02:00
self._started = True
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-05-09 16:03:46 +02:00
modeman.enter('hint', 'HintManager.start')
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-05-23 16:11:55 +02:00
logger.debug("Handling new keystring: '{}'".format(keystr))
2014-04-21 15:45:29 +02:00
for (string, elems) in self._elems.items():
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-05-06 17:02:32 +02:00
for elems in self._elems.values():
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 = {}
for k, e in self._elems.items():
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-05-09 16:03:46 +02:00
modeman.leave('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)
self._to_follow = keystr
return
# 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,
}
# Handlers which take a link string
link_handlers = {
2014-05-05 07:45:36 +02:00
Target.yank: self._yank,
Target.yank_primary: self._yank,
Target.cmd: self._preset_cmd_text,
Target.cmd_tab: self._preset_cmd_text,
2014-05-16 23:01:40 +02:00
Target.cmd_tab_bg: self._preset_cmd_text,
}
2014-04-21 16:59:03 +02:00
elem = self._elems[keystr].elem
if self._target in elem_handlers:
elem_handlers[self._target](elem)
elif self._target in link_handlers:
2014-04-21 23:53:13 +02:00
link = self._resolve_link(elem)
if link is None:
2014-05-14 18:00:40 +02:00
raise CommandError("No suitable link found for this element.")
link_handlers[self._target](link)
2014-05-05 07:45:36 +02:00
else:
raise ValueError("No suitable handler found!")
if self._target != Target.rapid:
modeman.maybe_leave('hint', 'followed')
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-04-27 21:59:23 +02:00
if not self._to_follow:
2014-05-14 18:00:40 +02:00
raise CommandError("No hint to follow")
2014-04-27 21:59:23 +02:00
self.fire(self._to_follow, force=True)
@pyqtSlot('QSize')
2014-04-22 17:53:27 +02:00
def on_contents_size_changed(self, _size):
"""Reposition hints if contents size changed."""
2014-05-13 13:42:30 +02:00
if not self._started:
# We got here because of some earlier hinting, but we can't simply
# disconnect frames as this leads to occasional segfaults :-/
2014-05-23 16:11:55 +02:00
logger.debug("Not hinting!")
2014-05-13 13:42:30 +02:00
return
2014-05-23 16:11:55 +02:00
logger.debug("Contents size changed...!")
for elems in self._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
@pyqtSlot(str)
def on_mode_entered(self, mode):
"""Stop hinting when insert mode was entered."""
if mode == 'insert':
modeman.maybe_leave('hint', 'insert mode')
2014-04-24 23:47:02 +02:00
@pyqtSlot(str)
def on_mode_left(self, mode):
"""Stop hinting when hinting mode was left."""
2014-04-25 16:53:23 +02:00
if mode != 'hint':
2014-04-24 23:47:02 +02:00
return
2014-05-13 13:42:30 +02:00
self._started = False
2014-04-24 23:47:02 +02:00
for elem in self._elems.values():
if not elem.label.isNull():
elem.label.removeFromDocument()
2014-04-24 23:47:02 +02:00
self._elems = {}
2014-04-27 21:59:23 +02:00
self._to_follow = None
2014-04-24 23:47:02 +02:00
self._target = None
2014-05-12 07:49:44 +02:00
self._frames = []
2014-04-24 23:47:02 +02:00
message.clear()