Use HintContext per invocation of hintmanager

This commit is contained in:
Florian Bruhin 2014-06-07 18:07:09 +02:00
parent d61a7f336e
commit 6a81710c71

View File

@ -41,6 +41,32 @@ Target = enum('normal', 'tab', 'tab_bg', 'yank', 'yank_primary', 'cmd',
'cmd_tab', 'cmd_tab_bg', 'rapid') 'cmd_tab', 'cmd_tab_bg', 'rapid')
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
cmd/cmd_tab/cmd_tab_bg: Enter link to commandline
rapid: Rapid mode with background tabs
to_follow: The link to follow when enter is pressed.
connected_frames: The QWebFrames which are connected to a signal.
"""
def __init__(self):
self.elems = {}
self.target = None
self.baseurl = None
self.to_follow = None
self.frames = []
self.connected_frames = []
class HintManager(QObject): class HintManager(QObject):
"""Manage drawing hints over links or other elements. """Manage drawing hints over links or other elements.
@ -50,16 +76,7 @@ class HintManager(QObject):
HINT_TEXTS: Text displayed for different hinting modes. HINT_TEXTS: Text displayed for different hinting modes.
Attributes: Attributes:
_started: Whether we started hinting at the moment. _context: The HintContext for the current invocation.
_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
cmd/cmd_tab/cmd_tab_bg: Enter link to commandline
rapid: Rapid mode with background tabs
_to_follow: The link to follow when enter is pressed.
Signals: Signals:
hint_strings_updated: Emitted when the possible hint strings changed. hint_strings_updated: Emitted when the possible hint strings changed.
@ -110,13 +127,7 @@ class HintManager(QObject):
frame: The QWebFrame to use for finding elements and drawing. frame: The QWebFrame to use for finding elements and drawing.
""" """
super().__init__(parent) super().__init__(parent)
self._elems = {} self._context = None
self._target = None
self._baseurl = None
self._to_follow = None
self._started = False
self._frames = []
self._connected_frames = []
modeman.instance().left.connect(self.on_mode_left) modeman.instance().left.connect(self.on_mode_left)
modeman.instance().entered.connect(self.on_mode_entered) modeman.instance().entered.connect(self.on_mode_entered)
@ -257,10 +268,10 @@ class HintManager(QObject):
Args: Args:
elem: The QWebElement to click. elem: The QWebElement to click.
""" """
if self._target == Target.rapid: if self._context.target == Target.rapid:
target = Target.tab_bg target = Target.tab_bg
else: else:
target = self._target target = self._context.target
self.set_open_target.emit(Target[target]) self.set_open_target.emit(Target[target])
# FIXME Instead of clicking the center, we could have nicer heuristics. # FIXME Instead of clicking the center, we could have nicer heuristics.
# e.g. parse (-webkit-)border-radius correctly and click text fields at # e.g. parse (-webkit-)border-radius correctly and click text fields at
@ -285,7 +296,7 @@ class HintManager(QObject):
Args: Args:
link: The URL to open. link: The URL to open.
""" """
sel = self._target == Target.yank_primary sel = self._context.target == Target.yank_primary
mode = QClipboard.Selection if sel else QClipboard.Clipboard mode = QClipboard.Selection if sel else QClipboard.Clipboard
QApplication.clipboard().setText(urlutils.urlstring(link), mode) QApplication.clipboard().setText(urlutils.urlstring(link), mode)
message.info("URL yanked to {}".format("primary selection" if sel message.info("URL yanked to {}".format("primary selection" if sel
@ -302,7 +313,7 @@ class HintManager(QObject):
Target.cmd_tab: 'open-tab', Target.cmd_tab: 'open-tab',
Target.cmd_tab_bg: 'open-tab-bg', Target.cmd_tab_bg: 'open-tab-bg',
} }
message.set_cmd_text(':{} {}'.format(commands[self._target], message.set_cmd_text(':{} {}'.format(commands[self._context.target],
urlutils.urlstring(link))) urlutils.urlstring(link)))
def _resolve_link(self, elem, baseurl=None): def _resolve_link(self, elem, baseurl=None):
@ -310,7 +321,8 @@ class HintManager(QObject):
Args: Args:
elem: The QWebElement to get the link of. elem: The QWebElement to get the link of.
baseurl: The baseurl of the current tab (overrides self._baseurl). baseurl: The baseurl of the current tab (overrides baseurl from
self._context).
Return: Return:
A QUrl with the absolute link, or None. A QUrl with the absolute link, or None.
@ -319,7 +331,7 @@ class HintManager(QObject):
if not link: if not link:
return None return None
if baseurl is None: if baseurl is None:
baseurl = self._baseurl baseurl = self._context.baseurl
link = urlutils.qurl(link) link = urlutils.qurl(link)
if link.isRelative(): if link.isRelative():
link = baseurl.resolved(link) link = baseurl.resolved(link)
@ -348,19 +360,19 @@ class HintManager(QObject):
def _connect_frame_signals(self): def _connect_frame_signals(self):
"""Connect the contentsSizeChanged signals to all frames.""" """Connect the contentsSizeChanged signals to all frames."""
for f in self._frames: for f in self._context.frames:
# For some reason we get segfaults sometimes when calling # For some reason we get segfaults sometimes when calling
# frame.contentsSizeChanged.disconnect() later, maybe because Qt # frame.contentsSizeChanged.disconnect() later, maybe because Qt
# already deleted the frame? # already deleted the frame?
# We work around this by never disconnecting this signal, and here # We work around this by never disconnecting this signal, and here
# making sure we don't connect a frame which already was connected # making sure we don't connect a frame which already was connected
# at some point earlier. # at some point earlier.
if f in self._connected_frames: if f in self._context.connected_frames:
logger.debug("Frame {} already connected!".format(f)) logger.debug("Frame {} already connected!".format(f))
else: else:
logger.debug("Connecting frame {}".format(f)) logger.debug("Connecting frame {}".format(f))
f.contentsSizeChanged.connect(self.on_contents_size_changed) f.contentsSizeChanged.connect(self.on_contents_size_changed)
self._connected_frames.append(f) self._context.connected_frames.append(f)
def follow_prevnext(self, frame, baseurl, prev=False, newtab=False): def follow_prevnext(self, frame, baseurl, prev=False, newtab=False):
"""Click a "previous"/"next" element on the page. """Click a "previous"/"next" element on the page.
@ -400,22 +412,23 @@ class HintManager(QObject):
# on_mode_left, we are extra careful here. # on_mode_left, we are extra careful here.
raise ValueError("start() was called with frame=None") raise ValueError("start() was called with frame=None")
elems = [] elems = []
self._frames = webelem.get_child_frames(mainframe) ctx = HintContext()
for f in self._frames: ctx.frames = webelem.get_child_frames(mainframe)
for f in ctx.frames:
elems += f.findAllElements(webelem.SELECTORS[group]) elems += f.findAllElements(webelem.SELECTORS[group])
filterfunc = webelem.FILTERS.get(group, lambda e: True) filterfunc = webelem.FILTERS.get(group, lambda e: True)
visible_elems = [e for e in elems if filterfunc(e) and visible_elems = [e for e in elems if filterfunc(e) and
webelem.is_visible(e, mainframe)] webelem.is_visible(e, mainframe)]
if not visible_elems: if not visible_elems:
raise CommandError("No elements found.") raise CommandError("No elements found.")
self._target = target ctx.target = target
self._baseurl = baseurl ctx.baseurl = baseurl
message.text(self.HINT_TEXTS[target]) message.text(self.HINT_TEXTS[target])
strings = self._hint_strings(visible_elems) strings = self._hint_strings(visible_elems)
for e, string in zip(visible_elems, strings): for e, string in zip(visible_elems, strings):
label = self._draw_label(e, string) label = self._draw_label(e, string)
self._elems[string] = ElemTuple(e, label) ctx.elems[string] = ElemTuple(e, label)
self._started = True self._context = ctx
self._connect_frame_signals() self._connect_frame_signals()
self.hint_strings_updated.emit(strings) self.hint_strings_updated.emit(strings)
modeman.enter('hint', 'HintManager.start') modeman.enter('hint', 'HintManager.start')
@ -423,7 +436,7 @@ class HintManager(QObject):
def handle_partial_key(self, keystr): def handle_partial_key(self, keystr):
"""Handle a new partial keypress.""" """Handle a new partial keypress."""
logger.debug("Handling new keystring: '{}'".format(keystr)) logger.debug("Handling new keystring: '{}'".format(keystr))
for (string, elems) in self._elems.items(): for (string, elems) in self._context.elems.items():
if string.startswith(keystr): if string.startswith(keystr):
matched = string[:len(keystr)] matched = string[:len(keystr)]
rest = string[len(keystr):] rest = string[len(keystr):]
@ -442,7 +455,7 @@ class HintManager(QObject):
def filter_hints(self, filterstr): def filter_hints(self, filterstr):
"""Filter displayed hints according to a text.""" """Filter displayed hints according to a text."""
for elems in self._elems.values(): for elems in self._context.elems.values():
if elems.elem.toPlainText().lower().startswith(filterstr): if elems.elem.toPlainText().lower().startswith(filterstr):
if elems.label.attribute('hidden') == 'true': if elems.label.attribute('hidden') == 'true':
# hidden element which matches again -> unhide it # hidden element which matches again -> unhide it
@ -455,7 +468,7 @@ class HintManager(QObject):
css = self._get_hint_css(elems.elem, elems.label) css = self._get_hint_css(elems.elem, elems.label)
elems.label.setAttribute('style', css) elems.label.setAttribute('style', css)
visible = {} visible = {}
for k, e in self._elems.items(): for k, e in self._context.elems.items():
if e.label.attribute('hidden') != 'true': if e.label.attribute('hidden') != 'true':
visible[k] = e visible[k] = e
if not visible: if not visible:
@ -474,7 +487,7 @@ class HintManager(QObject):
""" """
if not (force or config.get('hints', 'auto-follow')): if not (force or config.get('hints', 'auto-follow')):
self.handle_partial_key(keystr) self.handle_partial_key(keystr)
self._to_follow = keystr self._context.to_follow = keystr
return return
# Handlers which take a QWebElement # Handlers which take a QWebElement
elem_handlers = { elem_handlers = {
@ -491,35 +504,35 @@ class HintManager(QObject):
Target.cmd_tab: self._preset_cmd_text, Target.cmd_tab: self._preset_cmd_text,
Target.cmd_tab_bg: self._preset_cmd_text, Target.cmd_tab_bg: self._preset_cmd_text,
} }
elem = self._elems[keystr].elem elem = self._context.elems[keystr].elem
if self._target in elem_handlers: if self._context.target in elem_handlers:
elem_handlers[self._target](elem) elem_handlers[self._context.target](elem)
elif self._target in link_handlers: elif self._context.target in link_handlers:
link = self._resolve_link(elem) link = self._resolve_link(elem)
if link is None: if link is None:
raise CommandError("No suitable link found for this element.") raise CommandError("No suitable link found for this element.")
link_handlers[self._target](link) link_handlers[self._context.target](link)
else: else:
raise ValueError("No suitable handler found!") raise ValueError("No suitable handler found!")
if self._target != Target.rapid: if self._context.target != Target.rapid:
modeman.maybe_leave('hint', 'followed') modeman.maybe_leave('hint', 'followed')
def follow_hint(self): def follow_hint(self):
"""Follow the currently selected hint.""" """Follow the currently selected hint."""
if not self._to_follow: if not self._context.to_follow:
raise CommandError("No hint to follow") raise CommandError("No hint to follow")
self.fire(self._to_follow, force=True) self.fire(self._context.to_follow, force=True)
@pyqtSlot('QSize') @pyqtSlot('QSize')
def on_contents_size_changed(self, _size): def on_contents_size_changed(self, _size):
"""Reposition hints if contents size changed.""" """Reposition hints if contents size changed."""
if not self._started: if self._context is None:
# We got here because of some earlier hinting, but we can't simply # We got here because of some earlier hinting, but we can't simply
# disconnect frames as this leads to occasional segfaults :-/ # disconnect frames as this leads to occasional segfaults :-/
logger.debug("Not hinting!") logger.debug("Not hinting!")
return return
logger.debug("Contents size changed...!") logger.debug("Contents size changed...!")
for elems in self._elems.values(): for elems in self._context.elems.values():
css = self._get_hint_css(elems.elem, elems.label) css = self._get_hint_css(elems.elem, elems.label)
elems.label.setAttribute('style', css) elems.label.setAttribute('style', css)
@ -532,14 +545,13 @@ class HintManager(QObject):
@pyqtSlot(str) @pyqtSlot(str)
def on_mode_left(self, mode): def on_mode_left(self, mode):
"""Stop hinting when hinting mode was left.""" """Stop hinting when hinting mode was left."""
if mode != 'hint': if mode != 'hint' or self._context is None:
# We have one HintManager per tab, so when this gets called,
# self._context might be None, because the current tab is not
# hinting.
return return
self._started = False for elem in self._context.elems.values():
for elem in self._elems.values():
if not elem.label.isNull(): if not elem.label.isNull():
elem.label.removeFromDocument() elem.label.removeFromDocument()
self._elems = {} self._context = None
self._to_follow = None
self._target = None
self._frames = []
message.clear() message.clear()