Use HintContext per invocation of hintmanager
This commit is contained in:
parent
d61a7f336e
commit
6a81710c71
@ -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()
|
||||||
|
Loading…
Reference in New Issue
Block a user