diff --git a/THANKS b/THANKS index 3fbc86bc5..f35e28d86 100644 --- a/THANKS +++ b/THANKS @@ -27,6 +27,7 @@ valuable ones: - Bleeding Fingers - artyom.stv - hank + - Alec Thomas Thanks to these people for helpful bits and pieces in the Qt bugtracker and IRC channels: diff --git a/TODO b/TODO index f4bedb86c..7048c50f7 100644 --- a/TODO +++ b/TODO @@ -7,7 +7,6 @@ Style ===== _foo = QApplication.instance().obj.foo for global singletons? -Use py3.4 Enums with a backport to qutebrowser.utils? initialize completion models at some nicer place (not in widget) Major features diff --git a/qutebrowser/browser/curcommand.py b/qutebrowser/browser/curcommand.py index c72b5b5a8..47e1b76a3 100644 --- a/qutebrowser/browser/curcommand.py +++ b/qutebrowser/browser/curcommand.py @@ -33,6 +33,7 @@ import qutebrowser.utils.message as message import qutebrowser.commands.utils as cmdutils import qutebrowser.utils.webelem as webelem import qutebrowser.config.config as config +import qutebrowser.browser.hints as hints from qutebrowser.utils.misc import shell_escape @@ -216,21 +217,31 @@ class CurCommandDispatcher(QObject): break @cmdutils.register(instance='mainwindow.tabs.cur') - def hint(self, mode='all', target='normal'): + def hint(self, groupstr='all', targetstr='normal'): """Start hinting. Command handler for :hint. Args: - mode: The hinting mode to use. - target: Where to open the links. + groupstr: The hinting mode to use. + targetstr: Where to open the links. """ widget = self._tabs.currentWidget() frame = widget.page_.currentFrame() if frame is None: message.error("No frame focused!") - else: - widget.hintmanager.start(frame, widget.url(), mode, target) + return + try: + group = getattr(webelem.Group, groupstr) + except AttributeError: + message.error("Unknown hinting group {}!".format(groupstr)) + return + try: + target = getattr(hints.Target, targetstr) + except AttributeError: + message.error("Unknown hinting target {}!".format(targetstr)) + return + widget.hintmanager.start(frame, widget.url(), group, target) @cmdutils.register(instance='mainwindow.tabs.cur', hide=True) def follow_hint(self): diff --git a/qutebrowser/browser/hints.py b/qutebrowser/browser/hints.py index 42df3317a..a30f9e179 100644 --- a/qutebrowser/browser/hints.py +++ b/qutebrowser/browser/hints.py @@ -30,11 +30,16 @@ import qutebrowser.keyinput.modeman as modeman import qutebrowser.utils.message as message import qutebrowser.utils.url as urlutils import qutebrowser.utils.webelem as webelem +from qutebrowser.utils.usertypes import enum ElemTuple = namedtuple('ElemTuple', 'elem, label') +Target = enum('normal', 'tab', 'bgtab', 'yank', 'yank_primary', 'cmd', + 'cmd_tab', 'cmd_bgtab', 'rapid') + + class HintManager(QObject): """Manage drawing hints over links or other elements. @@ -47,10 +52,10 @@ class HintManager(QObject): _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'/'bgtab': Get passed to BrowserTab. - 'yank'/'yank_primary': Yank to clipboard/primary selection - 'cmd'/'cmd_tab'/'cmd_bgtab': Enter link to commandline - 'rapid': Rapid mode with background tabs + normal/tab/bgtab: Get passed to BrowserTab. + yank/yank_primary: Yank to clipboard/primary selection + cmd/cmd_tab/cmd_bgtab: Enter link to commandline + rapid: Rapid mode with background tabs _to_follow: The link to follow when enter is pressed. Signals: @@ -217,11 +222,11 @@ class HintManager(QObject): Args: elem: The QWebElement to click. """ - if self._target == 'rapid': - target = 'bgtab' + if self._target == Target.rapid: + target = Target.bgtab else: target = self._target - self.set_open_target.emit(target) + self.set_open_target.emit(Target.reverse_mapping[target]) point = elem.geometry().topLeft() scrollpos = self._frame.scrollPosition() logging.debug("Clicking on \"{}\" at {}/{} - {}/{}".format( @@ -245,7 +250,7 @@ class HintManager(QObject): Args: link: The URL to open. """ - sel = self._target == 'yank_primary' + sel = self._target == Target.yank_primary mode = QClipboard.Selection if sel else QClipboard.Clipboard QApplication.clipboard().setText(urlutils.urlstring(link), mode) message.info("URL yanked to {}".format("primary selection" if sel @@ -258,9 +263,9 @@ class HintManager(QObject): link: The link to open. """ commands = { - 'cmd': 'open', - 'cmd_tab': 'tabopen', - 'cmd_bgtab': 'backtabopen', + Target.cmd: 'open', + Target.cmd_tab: 'tabopen', + Target.cmd_bgtab: 'backtabopen', } message.set_cmd_text(':{} {}'.format(commands[self._target], urlutils.urlstring(link))) @@ -325,23 +330,20 @@ class HintManager(QObject): return self.openurl.emit(link, newtab) - def start(self, frame, baseurl, mode='all', target='normal'): + def start(self, frame, baseurl, group=webelem.Group.all, + target=Target.normal): """Start hinting. Args: frame: The QWebFrame to place hints in. baseurl: URL of the current page. - mode: The mode to be used. + 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. """ - try: - elems = frame.findAllElements(webelem.SELECTORS[mode]) - except KeyError: - message.error("Hinting mode '{}' does not exist!".format(mode)) - return + elems = frame.findAllElements(webelem.SELECTORS[group]) self._target = target self._baseurl = baseurl if frame is None: @@ -350,7 +352,7 @@ class HintManager(QObject): # on_mode_left, we are extra careful here. raise ValueError("start() was called with frame=None") self._frame = frame - filterfunc = webelem.FILTERS.get(mode, lambda e: True) + filterfunc = webelem.FILTERS.get(group, lambda e: True) visible_elems = [] for e in elems: if filterfunc(e) and webelem.is_visible(e, self._frame): @@ -359,21 +361,17 @@ class HintManager(QObject): message.error("No elements found.") return texts = { - 'normal': "Follow hint...", - 'tab': "Follow hint in new tab...", - 'bgtab': "Follow hint in background tab...", - 'yank': "Yank hint to clipboard...", - 'yank_primary': "Yank hint to primary selection...", - 'cmd': "Set hint in commandline...", - 'cmd_tab': "Set hint in commandline as new tab...", - 'cmd_bgtab': "Set hint in commandline as background tab...", - 'rapid': "Follow hint (rapid mode)...", + Target.normal: "Follow hint...", + Target.tab: "Follow hint in new tab...", + Target.bgtab: "Follow hint in background tab...", + 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...", + Target.cmd_bgtab: "Set hint in commandline as background tab...", + Target.rapid: "Follow hint (rapid mode)...", } - try: - message.text(texts[target]) - except KeyError: - message.error("Hinting target '{}' does not exist!".format(target)) - return + message.text(texts[target]) strings = self._hint_strings(visible_elems) for e, string in zip(visible_elems, strings): label = self._draw_label(e, string) @@ -426,18 +424,18 @@ class HintManager(QObject): return # Handlers which take a QWebElement elem_handlers = { - 'normal': self._click, - 'tab': self._click, - 'bgtab': self._click, - 'rapid': self._click, + Target.normal: self._click, + Target.tab: self._click, + Target.bgtab: self._click, + Target.rapid: self._click, } # Handlers which take a link string link_handlers = { - 'yank': self._yank, - 'yank_primary': self._yank, - 'cmd': self._preset_cmd_text, - 'cmd_tab': self._preset_cmd_text, - 'cmd_bgtab': self._preset_cmd_text, + Target.yank: self._yank, + Target.yank_primary: self._yank, + Target.cmd: self._preset_cmd_text, + Target.cmd_tab: self._preset_cmd_text, + Target.cmd_bgtab: self._preset_cmd_text, } elem = self._elems[keystr].elem if self._target in elem_handlers: @@ -448,7 +446,9 @@ class HintManager(QObject): message.error("No suitable link found for this element.") return link_handlers[self._target](link) - if self._target != 'rapid': + else: + raise ValueError("No suitable handler found!") + if self._target != Target.rapid: modeman.leave('hint') def follow_hint(self): diff --git a/qutebrowser/config/websettings.py b/qutebrowser/config/websettings.py index cdfb1efab..fbd10dd5b 100644 --- a/qutebrowser/config/websettings.py +++ b/qutebrowser/config/websettings.py @@ -30,95 +30,96 @@ from PyQt5.QtCore import pyqtSlot from PyQt5.QtWebKit import QWebSettings import qutebrowser.config.config as config +from qutebrowser.utils.usertypes import enum -ATTRIBUTE = 0 -SETTER = 1 -STATIC_SETTER = 2 +MapType = enum('attribute', 'setter', 'static_setter') MAPPINGS = { # noqa 'auto-load-images': - (ATTRIBUTE, QWebSettings.AutoLoadImages), + (MapType.attribute, QWebSettings.AutoLoadImages), 'dns-prefetch-enabled': - (ATTRIBUTE, QWebSettings.DnsPrefetchEnabled), + (MapType.attribute, QWebSettings.DnsPrefetchEnabled), 'javascript-enabled': - (ATTRIBUTE, QWebSettings.JavascriptEnabled), + (MapType.attribute, QWebSettings.JavascriptEnabled), #'java-enabled': - # (ATTRIBUTE, QWebSettings.JavaEnabled), + # (MapType.attribute, QWebSettings.JavaEnabled), 'plugins-enabled': - (ATTRIBUTE, QWebSettings.PluginsEnabled), + (MapType.attribute, QWebSettings.PluginsEnabled), 'private-browsing-enabled': - (ATTRIBUTE, QWebSettings.PrivateBrowsingEnabled), + (MapType.attribute, QWebSettings.PrivateBrowsingEnabled), 'javascript-can-open-windows': - (ATTRIBUTE, QWebSettings.JavascriptCanOpenWindows), + (MapType.attribute, QWebSettings.JavascriptCanOpenWindows), 'javascript-can-close-windows': - (ATTRIBUTE, QWebSettings.JavascriptCanCloseWindows), + (MapType.attribute, QWebSettings.JavascriptCanCloseWindows), 'javascript-can-access-clipboard': - (ATTRIBUTE, QWebSettings.JavascriptCanAccessClipboard), + (MapType.attribute, QWebSettings.JavascriptCanAccessClipboard), 'developer-extras-enabled': - (ATTRIBUTE, QWebSettings.DeveloperExtrasEnabled), + (MapType.attribute, QWebSettings.DeveloperExtrasEnabled), 'spatial-navigation-enabled': - (ATTRIBUTE, QWebSettings.SpatialNavigationEnabled), + (MapType.attribute, QWebSettings.SpatialNavigationEnabled), 'links-included-in-focus-chain': - (ATTRIBUTE, QWebSettings.LinksIncludedInFocusChain), + (MapType.attribute, QWebSettings.LinksIncludedInFocusChain), 'zoom-text-only': - (ATTRIBUTE, QWebSettings.ZoomTextOnly), + (MapType.attribute, QWebSettings.ZoomTextOnly), 'print-element-backgrounds': - (ATTRIBUTE, QWebSettings.PrintElementBackgrounds), + (MapType.attribute, QWebSettings.PrintElementBackgrounds), 'offline-storage-database-enabled': - (ATTRIBUTE, QWebSettings.OfflineStorageDatabaseEnabled), + (MapType.attribute, QWebSettings.OfflineStorageDatabaseEnabled), 'offline-web-application-storage-enabled': - (ATTRIBUTE, QWebSettings.OfflineWebApplicationCacheEnabled), + (MapType.attribute, QWebSettings.OfflineWebApplicationCacheEnabled), 'local-storage-enabled': - (ATTRIBUTE, QWebSettings.LocalStorageEnabled), + (MapType.attribute, QWebSettings.LocalStorageEnabled), 'local-content-can-access-remote-urls': - (ATTRIBUTE, QWebSettings.LocalContentCanAccessRemoteUrls), + (MapType.attribute, QWebSettings.LocalContentCanAccessRemoteUrls), 'local-content-can-access-file-urls': - (ATTRIBUTE, QWebSettings.LocalContentCanAccessFileUrls), + (MapType.attribute, QWebSettings.LocalContentCanAccessFileUrls), 'xss-auditing-enabled': - (ATTRIBUTE, QWebSettings.XSSAuditingEnabled), + (MapType.attribute, QWebSettings.XSSAuditingEnabled), #'accelerated-compositing-enabled': - # (ATTRIBUTE, QWebSettings.AcceleratedCompositingEnabled), + # (MapType.attribute, QWebSettings.AcceleratedCompositingEnabled), #'tiled-backing-store-enabled': - # (ATTRIBUTE, QWebSettings.TiledBackingStoreEnabled), + # (MapType.attribute, QWebSettings.TiledBackingStoreEnabled), 'frame-flattening-enabled': - (ATTRIBUTE, QWebSettings.FrameFlatteningEnabled), + (MapType.attribute, QWebSettings.FrameFlatteningEnabled), 'site-specific-quirks-enabled': - (ATTRIBUTE, QWebSettings.SiteSpecificQuirksEnabled), + (MapType.attribute, QWebSettings.SiteSpecificQuirksEnabled), 'user-stylesheet': - (SETTER, lambda qws, v: qws.setUserStyleSheetUrl(v)), + (MapType.setter, lambda qws, v: qws.setUserStyleSheetUrl(v)), 'css-media-type': - (SETTER, lambda qws, v: qws.setCSSMediaType(v)), + (MapType.setter, lambda qws, v: qws.setCSSMediaType(v)), 'default-encoding': - (SETTER, lambda qws, v: qws.setDefaultTextEncoding(v)), + (MapType.setter, lambda qws, v: qws.setDefaultTextEncoding(v)), 'font-family-standard': - (SETTER, lambda qws, v: + (MapType.setter, lambda qws, v: qws.setFontFamily(QWebSettings.StandardFont, v)), 'font-family-fixed': - (SETTER, lambda qws, v: + (MapType.setter, lambda qws, v: qws.setFontFamily(QWebSettings.FixedFont, v)), 'font-family-serif': - (SETTER, lambda qws, v: + (MapType.setter, lambda qws, v: qws.setFontFamily(QWebSettings.SerifFont, v)), 'font-family-sans-serif': - (SETTER, lambda qws, v: + (MapType.setter, lambda qws, v: qws.setFontFamily(QWebSettings.SansSerifFont, v)), 'font-family-cursive': - (SETTER, lambda qws, v: + (MapType.setter, lambda qws, v: qws.setFontFamily(QWebSettings.CursiveFont, v)), 'font-family-fantasy': - (SETTER, lambda qws, v: + (MapType.setter, lambda qws, v: qws.setFontFamily(QWebSettings.FantasyFont, v)), 'maximum-pages-in-cache': - (STATIC_SETTER, lambda v: QWebSettings.setMaximumPagesInCache(v)), + (MapType.static_setter, lambda v: + QWebSettings.setMaximumPagesInCache(v)), 'object-cache-capacities': - (STATIC_SETTER, lambda v: QWebSettings.setObjectCacheCapacities(*v)), + (MapType.static_setter, lambda v: + QWebSettings.setObjectCacheCapacities(*v)), 'offline-storage-default-quota': - (STATIC_SETTER, lambda v: + (MapType.static_setter, lambda v: QWebSettings.setOfflineStorageDefaultQuota(v)), 'offline-web-application-cache-quota': - (STATIC_SETTER, lambda v: + (MapType.static_setter, lambda v: QWebSettings.setOfflineWebApplicationCacheQuota(v)), } @@ -130,15 +131,16 @@ def _set_setting(typ, arg, value): """Set a QWebSettings setting. Args: - typ: The type of the item (ATTRIBUTE/SETTER/STATIC_SETTER) + typ: The type of the item + (MapType.attribute/MapType.setter/MapType.static_setter) arg: The argument (attribute/handler) value: The value to set. """ - if typ == ATTRIBUTE: + if typ == MapType.attribute: settings.setAttribute(arg, value) - elif typ == SETTER and value is not None: + elif typ == MapType.setter and value is not None: arg(settings, value) - elif typ == STATIC_SETTER and value is not None: + elif typ == MapType.static_setter and value is not None: arg(value) diff --git a/qutebrowser/keyinput/_basekeyparser.py b/qutebrowser/keyinput/_basekeyparser.py index 95015da7e..0812b36cd 100644 --- a/qutebrowser/keyinput/_basekeyparser.py +++ b/qutebrowser/keyinput/_basekeyparser.py @@ -25,6 +25,7 @@ from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QObject, QTimer from PyQt5.QtGui import QKeySequence import qutebrowser.config.config as config +from qutebrowser.utils.usertypes import enum class BaseKeyParser(QObject): @@ -35,14 +36,16 @@ class BaseKeyParser(QObject): execute() to do whatever they want to. Class Attributes: - MATCH_PARTIAL: Constant for a partial match (no keychain matched yet, - but it's still possible in the future. - MATCH_DEFINITIVE: Constant for a full match (keychain matches exactly). - MATCH_AMBIGUOUS: There are both a partial and a definitive match. - MATCH_NONE: Constant for no match (no more matches possible). + Match: types of a match between a binding and the keystring. + partial: No keychain matched yet, but it's still possible in the + future. + definitive: Keychain matches exactly. + ambiguous: There are both a partial and a definitive match. + none: No more matches possible. - TYPE_CHAIN: execute() was called via a chain-like keybinding - TYPE_SPECIAL: execute() was called via a special keybinding + Types: type of a keybinding. + chain: execute() was called via a chain-like keybinding + special: execute() was called via a special keybinding Attributes: bindings: Bound keybindings @@ -60,13 +63,8 @@ class BaseKeyParser(QObject): keystring_updated = pyqtSignal(str) - MATCH_PARTIAL = 0 - MATCH_DEFINITIVE = 1 - MATCH_AMBIGUOUS = 2 - MATCH_NONE = 3 - - TYPE_CHAIN = 0 - TYPE_SPECIAL = 1 + Match = enum('partial', 'definitive', 'ambiguous', 'none') + Type = enum('chain', 'special') def __init__(self, parent=None, supports_count=None, supports_chains=False): @@ -134,7 +132,7 @@ class BaseKeyParser(QObject): except KeyError: logging.debug("No binding found for {}.".format(modstr + keystr)) return False - self.execute(cmdstr, self.TYPE_SPECIAL) + self.execute(cmdstr, self.Type.special) return True def _handle_single_key(self, e): @@ -173,15 +171,15 @@ class BaseKeyParser(QObject): (match, binding) = self._match_key(cmd_input) - if match == self.MATCH_DEFINITIVE: + if match == self.Match.definitive: self._keystring = '' - self.execute(binding, self.TYPE_CHAIN, count) - elif match == self.MATCH_AMBIGUOUS: + self.execute(binding, self.Type.chain, count) + elif match == self.Match.ambiguous: self._handle_ambiguous_match(binding, count) - elif match == self.MATCH_PARTIAL: + elif match == self.Match.partial: logging.debug("No match for \"{}\" (added {})".format( self._keystring, txt)) - elif match == self.MATCH_NONE: + elif match == self.Match.none: logging.debug("Giving up with \"{}\", no matches".format( self._keystring)) self._keystring = '' @@ -196,11 +194,11 @@ class BaseKeyParser(QObject): Return: A tuple (matchtype, binding). - matchtype: MATCH_DEFINITIVE, MATCH_AMBIGUOUS, MATCH_PARTIAL or - MATCH_NONE - binding: - None with MATCH_PARTIAL/MATCH_NONE - - The found binding with MATCH_DEFINITIVE/ - MATCH_AMBIGUOUS + matchtype: Match.definitive, Match.ambiguous, Match.partial or + Match.none + binding: - None with Match.partial/Match.none + - The found binding with Match.definitive/ + Match.ambiguous """ # A (cmd_input, binding) tuple (k, v of bindings) or None. definitive_match = None @@ -219,13 +217,13 @@ class BaseKeyParser(QObject): partial_match = True break if definitive_match is not None and partial_match: - return (self.MATCH_AMBIGUOUS, definitive_match[1]) + return (self.Match.ambiguous, definitive_match[1]) elif definitive_match is not None: - return (self.MATCH_DEFINITIVE, definitive_match[1]) + return (self.Match.definitive, definitive_match[1]) elif partial_match: - return (self.MATCH_PARTIAL, None) + return (self.Match.partial, None) else: - return (self.MATCH_NONE, None) + return (self.Match.none, None) def _stop_delayed_exec(self): """Stop a delayed execution if any is running.""" @@ -246,7 +244,7 @@ class BaseKeyParser(QObject): if time == 0: # execute immediately self._keystring = '' - self.execute(binding, self.TYPE_CHAIN, count) + self.execute(binding, self.Type.chain, count) else: # execute in `time' ms logging.debug("Scheduling execution of {} in {}ms".format(binding, @@ -271,7 +269,7 @@ class BaseKeyParser(QObject): self._timer = None self._keystring = '' self.keystring_updated.emit(self._keystring) - self.execute(command, self.TYPE_CHAIN, count) + self.execute(command, self.Type.chain, count) def handle(self, e): """Handle a new keypress and call the respective handlers. @@ -329,7 +327,7 @@ class BaseKeyParser(QObject): Args: cmdstr: The command to execute as a string. - keytype: TYPE_CHAIN or TYPE_SPECIAL + keytype: Type.chain or Type.special count: The count if given. """ raise NotImplementedError diff --git a/qutebrowser/keyinput/modeparsers.py b/qutebrowser/keyinput/modeparsers.py index 19deacd38..345b43ee8 100644 --- a/qutebrowser/keyinput/modeparsers.py +++ b/qutebrowser/keyinput/modeparsers.py @@ -127,9 +127,9 @@ class HintKeyParser(CommandKeyParser): """Handle a completed keychain. Emit: - fire_hint: Emitted if keytype is TYPE_CHAIN + fire_hint: Emitted if keytype is chain """ - if keytype == self.TYPE_CHAIN: + if keytype == self.Type.chain: self.fire_hint.emit(cmdstr) else: # execute as command diff --git a/qutebrowser/test/test_neighborlist.py b/qutebrowser/test/test_neighborlist.py index 0b2e590f9..2ae94f067 100644 --- a/qutebrowser/test/test_neighborlist.py +++ b/qutebrowser/test/test_neighborlist.py @@ -133,21 +133,21 @@ class OneTests(TestCase): self.nl = NeighborList([1], default=1) def test_first_wrap(self): - self.nl._mode = NeighborList.WRAP + self.nl._mode = NeighborList.Modes.wrap self.nl.firstitem() self.assertEqual(self.nl.idx, 0) self.assertEqual(self.nl.previtem(), 1) self.assertEqual(self.nl.idx, 0) def test_first_block(self): - self.nl._mode = NeighborList.BLOCK + self.nl._mode = NeighborList.Modes.block self.nl.firstitem() self.assertEqual(self.nl.idx, 0) self.assertEqual(self.nl.previtem(), 1) self.assertEqual(self.nl.idx, 0) def test_first_raise(self): - self.nl._mode = NeighborList.RAISE + self.nl._mode = NeighborList.Modes.exception self.nl.firstitem() self.assertEqual(self.nl.idx, 0) with self.assertRaises(IndexError): @@ -155,21 +155,21 @@ class OneTests(TestCase): self.assertEqual(self.nl.idx, 0) def test_last_wrap(self): - self.nl._mode = NeighborList.WRAP + self.nl._mode = NeighborList.Modes.wrap self.nl.lastitem() self.assertEqual(self.nl.idx, 0) self.assertEqual(self.nl.nextitem(), 1) self.assertEqual(self.nl.idx, 0) def test_last_block(self): - self.nl._mode = NeighborList.BLOCK + self.nl._mode = NeighborList.Modes.block self.nl.lastitem() self.assertEqual(self.nl.idx, 0) self.assertEqual(self.nl.nextitem(), 1) self.assertEqual(self.nl.idx, 0) def test_last_raise(self): - self.nl._mode = NeighborList.RAISE + self.nl._mode = NeighborList.Modes.exception self.nl.lastitem() self.assertEqual(self.nl.idx, 0) with self.assertRaises(IndexError): @@ -179,11 +179,11 @@ class OneTests(TestCase): class BlockTests(TestCase): - """Tests with mode=BLOCK.""" + """Tests with mode=block.""" def setUp(self): self.nl = NeighborList([1, 2, 3, 4, 5], default=3, - mode=NeighborList.BLOCK) + mode=NeighborList.Modes.block) def test_first(self): self.nl.firstitem() @@ -200,11 +200,11 @@ class BlockTests(TestCase): class WrapTests(TestCase): - """Tests with mode=WRAP.""" + """Tests with mode=wrap.""" def setUp(self): self.nl = NeighborList([1, 2, 3, 4, 5], default=3, - mode=NeighborList.WRAP) + mode=NeighborList.Modes.wrap) def test_first(self): self.nl.firstitem() @@ -221,11 +221,11 @@ class WrapTests(TestCase): class RaiseTests(TestCase): - """Tests with mode=RAISE.""" + """Tests with mode=exception.""" def setUp(self): self.nl = NeighborList([1, 2, 3, 4, 5], default=3, - mode=NeighborList.RAISE) + mode=NeighborList.Modes.exception) def test_first(self): self.nl.firstitem() diff --git a/qutebrowser/utils/usertypes.py b/qutebrowser/utils/usertypes.py index e8dfd1ef6..407817dba 100644 --- a/qutebrowser/utils/usertypes.py +++ b/qutebrowser/utils/usertypes.py @@ -26,12 +26,31 @@ import logging _UNSET = object() +def enum(*items, **named): + + """Factory for simple enumerations. + + We really don't need more complex things here, so we don't use python3.4's + enum, because we'd have to backport things to 3.3 and maybe even 3.2. + + Based on: http://stackoverflow.com/a/1695250/2085149 + + Args: + *items: Items to be sequentally enumerated. + **named: Items to have a given position/number. + """ + enums = dict(zip(items, range(len(items))), **named) + reverse = dict((v, k) for k, v in enums.items()) + enums['reverse_mapping'] = reverse + return type('Enum', (), enums) + + class NeighborList: """A list of items which saves it current position. Class attributes: - BLOCK/WRAP/RAISE: Modes, see constructor documentation. + Modes: Different modes, see constructor documentation. Attributes: idx: The current position in the list. @@ -39,20 +58,18 @@ class NeighborList: _mode: The current mode. """ - BLOCK = 0 - WRAP = 1 - RAISE = 2 + Modes = enum('block', 'wrap', 'exception') - def __init__(self, items=None, default=_UNSET, mode=RAISE): + def __init__(self, items=None, default=_UNSET, mode=Modes.exception): """Constructor. Args: items: The list of items to iterate in. _default: The initially selected value. _mode: Behaviour when the first/last item is reached. - BLOCK: Stay on the selected item - WRAP: Wrap around to the other end - RAISE: Raise an IndexError. + Modes.block: Stay on the selected item + Modes.wrap: Wrap around to the other end + Modes.exception: Raise an IndexError. """ if items is None: self._items = [] @@ -80,7 +97,8 @@ class NeighborList: The new item. Raise: - IndexError if the border of the list is reached and mode is RAISE. + IndexError if the border of the list is reached and mode is + exception. """ logging.debug("{} items, idx {}, offset {}".format(len(self._items), self.idx, offset)) @@ -92,13 +110,13 @@ class NeighborList: else: raise IndexError except IndexError: - if self._mode == self.BLOCK: + if self._mode == self.Modes.block: new = self.curitem() - elif self._mode == self.WRAP: + elif self._mode == self.Modes.wrap: self.idx += offset self.idx %= len(self.items) new = self.curitem() - elif self._mode == self.RAISE: + elif self._mode == self.Modes.exception: raise else: self.idx += offset diff --git a/qutebrowser/utils/webelem.py b/qutebrowser/utils/webelem.py index 5a0bad7cc..f3e876ff8 100644 --- a/qutebrowser/utils/webelem.py +++ b/qutebrowser/utils/webelem.py @@ -18,6 +18,7 @@ """Utilities related to QWebElements. Module attributes: + Group: Enum for different kinds of groups. 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 @@ -25,27 +26,33 @@ Module attributes: """ import qutebrowser.utils.url as urlutils +from qutebrowser.utils.usertypes import enum + + +Group = enum('all', 'links', 'images', 'editable', 'url', 'prevnext_rel', + 'prevnext', 'editable_focused') + 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]', - 'prevnext_rel': 'link, [role=link]', - 'prevnext': 'a, button, [role=button]', + Group.all: ('a, textarea, select, input:not([type=hidden]), button, ' + 'frame, iframe, [onclick], [onmousedown], [role=link], ' + '[role=option], [role=button], img'), + Group.links: 'a', + Group.images: 'img', + Group.editable: ('input[type=text], input[type=email], input[type=url], ' + 'input[type=tel], input[type=number], ' + 'input[type=password], input[type=search], textarea'), + Group.url: '[src], [href]', + Group.prevnext_rel: 'link, [role=link]', + Group.prevnext: 'a, button, [role=button]', } -SELECTORS['editable_focused'] = ', '.join( - [sel.strip() + ':focus' for sel in SELECTORS['editable'].split(',')]) +SELECTORS[Group.editable_focused] = ', '.join( + [sel.strip() + ':focus' for sel in SELECTORS[Group.editable].split(',')]) FILTERS = { - 'links': (lambda e: e.hasAttribute('href') and - urlutils.qurl(e.attribute('href')).scheme() != 'javascript'), + Group.links: (lambda e: e.hasAttribute('href') and + urlutils.qurl(e.attribute('href')).scheme() != 'javascript'), } diff --git a/qutebrowser/widgets/webview.py b/qutebrowser/widgets/webview.py index 1f54a324e..b38a66de9 100644 --- a/qutebrowser/widgets/webview.py +++ b/qutebrowser/widgets/webview.py @@ -33,7 +33,10 @@ import qutebrowser.utils.webelem as webelem from qutebrowser.browser.webpage import BrowserPage from qutebrowser.browser.hints import HintManager from qutebrowser.utils.signals import SignalCache -from qutebrowser.utils.usertypes import NeighborList +from qutebrowser.utils.usertypes import NeighborList, enum + + +Target = enum('normal', 'tab', 'bgtab') class WebView(QWebView): @@ -72,7 +75,7 @@ class WebView(QWebView): super().__init__(parent) self._scroll_pos = (-1, -1) self._shutdown_callback = None - self._open_target = 'normal' + self._open_target = Target.normal self._force_open_target = None self._destroyed = {} self._zoom = None @@ -95,7 +98,7 @@ class WebView(QWebView): self._zoom = NeighborList( config.get('general', 'zoom-levels'), default=config.get('general', 'default-zoom'), - mode=NeighborList.BLOCK) + mode=NeighborList.Modes.block) def _on_destroyed(self, sender): """Called when a subsystem has been destroyed during shutdown. @@ -226,9 +229,9 @@ class WebView(QWebView): Emit: open_tab: Emitted if window should be opened in a new tab. """ - if self._open_target == 'tab': + if self._open_target == Target.tab: self.open_tab.emit(url, False) - elif self._open_target == 'bgtab': + elif self._open_target == Target.bgtab: self.open_tab.emit(url, True) else: self.openurl(url) @@ -252,7 +255,7 @@ class WebView(QWebView): return frame = self.page_.currentFrame() elem = frame.findFirstElement( - webelem.SELECTORS['editable_focused']) + webelem.SELECTORS[webelem.Group.editable_focused]) logging.debug("focus element: {}".format(not elem.isNull())) if elem.isNull(): modeman.maybe_leave("insert") @@ -266,7 +269,7 @@ class WebView(QWebView): Args: target: A string to set self._force_open_target to. """ - self._force_open_target = target + self._force_open_target = getattr(Target, target) def paintEvent(self, e): """Extend paintEvent to emit a signal if the scroll position changed. @@ -339,10 +342,10 @@ class WebView(QWebView): elif (e.button() == Qt.MidButton or e.modifiers() & Qt.ControlModifier): if config.get('general', 'background-tabs'): - self._open_target = "bgtab" + self._open_target = Target.bgtab else: - self._open_target = "tab" + self._open_target = Target.tab logging.debug("Setting target: {}".format(self._open_target)) else: - self._open_target = "normal" + self._open_target = Target.normal return super().mousePressEvent(e)