From 5ce49553d837075117e90716234da167800d359a Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 6 May 2014 08:36:32 +0200 Subject: [PATCH 01/30] Stop logging config values. This is just too much noise... --- qutebrowser/config/config.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index ae11aa84e..1edfa1434 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -271,7 +271,6 @@ class ConfigManager(QObject): Return: The value of the option. """ - logging.debug("getting {} -> {}".format(sectname, optname)) try: sect = self.sections[sectname] except KeyError: @@ -285,7 +284,6 @@ class ConfigManager(QObject): mapping = {key: val.value for key, val in sect.values.items()} newval = self._interpolation.before_get(self, sectname, optname, val.value, mapping) - logging.debug("interpolated val: {}".format(newval)) if transformed: newval = val.typ.transform(newval) return newval From f8195dc600e57c04f5d8da8cc0b15c302f0f8f26 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 6 May 2014 08:50:18 +0200 Subject: [PATCH 02/30] Connect tab.open_tab to correct internal slot. --- qutebrowser/widgets/_tabbedbrowser.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qutebrowser/widgets/_tabbedbrowser.py b/qutebrowser/widgets/_tabbedbrowser.py index 6c9ab7cf4..907cc684b 100644 --- a/qutebrowser/widgets/_tabbedbrowser.py +++ b/qutebrowser/widgets/_tabbedbrowser.py @@ -136,9 +136,10 @@ class TabbedBrowser(TabWidget): tab.hintmanager.openurl.connect(self.cur.openurl_slot) # misc tab.titleChanged.connect(self.on_title_changed) - tab.open_tab.connect(self.tabopen) + tab.open_tab.connect(self._tabopen) tab.iconChanged.connect(self.on_icon_changed) + @pyqtSlot(str, bool) def _tabopen(self, url, background=False): """Open a new tab with a given url. From 42c1ea57887fc152bb89d6fadd88f5f539f4b06e Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 6 May 2014 08:53:40 +0200 Subject: [PATCH 03/30] Rename _tabopen to tabopen and tabopen to tabopen_cmd --- qutebrowser/widgets/_tabbedbrowser.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/qutebrowser/widgets/_tabbedbrowser.py b/qutebrowser/widgets/_tabbedbrowser.py index 907cc684b..9adc4faf2 100644 --- a/qutebrowser/widgets/_tabbedbrowser.py +++ b/qutebrowser/widgets/_tabbedbrowser.py @@ -136,11 +136,11 @@ class TabbedBrowser(TabWidget): tab.hintmanager.openurl.connect(self.cur.openurl_slot) # misc tab.titleChanged.connect(self.on_title_changed) - tab.open_tab.connect(self._tabopen) + tab.open_tab.connect(self.tabopen) tab.iconChanged.connect(self.on_icon_changed) @pyqtSlot(str, bool) - def _tabopen(self, url, background=False): + def tabopen(self, url, background=False): """Open a new tab with a given url. Inner logic for tabopen and backtabopen. @@ -226,15 +226,16 @@ class TabbedBrowser(TabWidget): elif last_close == 'blank': tab.openurl('about:blank') - @cmdutils.register(instance='mainwindow.tabs', split=False) - def tabopen(self, url): + @cmdutils.register(instance='mainwindow.tabs', split=False, name='tabopen') + def tabopen_cmd(self, url): """Open a new tab with a given url.""" - self._tabopen(url, background=False) + self.tabopen(url, background=False) - @cmdutils.register(instance='mainwindow.tabs', split=False) - def backtabopen(self, url): + @cmdutils.register(instance='mainwindow.tabs', split=False, + name='backtabopen') + def backtabopen_cmd(self, url): """Open a new tab in background.""" - self._tabopen(url, background=True) + self.tabopen(url, background=True) @cmdutils.register(instance='mainwindow.tabs', hide=True) def tabopencur(self): From e02b84d7ef0e7ea5bf13a3a80be9d6932d9b5848 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 6 May 2014 09:09:23 +0200 Subject: [PATCH 04/30] Add some debug logging for click targets --- qutebrowser/widgets/webview.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/qutebrowser/widgets/webview.py b/qutebrowser/widgets/webview.py index 8d4b7d830..dec2165a8 100644 --- a/qutebrowser/widgets/webview.py +++ b/qutebrowser/widgets/webview.py @@ -269,7 +269,9 @@ class WebView(QWebView): Args: target: A string to set self._force_open_target to. """ - self._force_open_target = getattr(Target, target) + t = getattr(Target, target) + logging.debug("Setting force target to {}/{}".format(target, t)) + self._force_open_target = t def paintEvent(self, e): """Extend paintEvent to emit a signal if the scroll position changed. @@ -338,14 +340,16 @@ class WebView(QWebView): self._open_target = self._force_open_target self._force_open_target = None logging.debug("Setting force target: {}".format( - self._open_target)) + Target[self._open_target])) elif (e.button() == Qt.MidButton or e.modifiers() & Qt.ControlModifier): if config.get('general', 'background-tabs'): self._open_target = Target.bgtab else: self._open_target = Target.tab - logging.debug("Setting target: {}".format(self._open_target)) + logging.debug("Middle click, setting target: {}".format( + Target[self._open_target])) else: self._open_target = Target.normal + logging.debug("Normal click, setting normal target") return super().mousePressEvent(e) From 243b78a93443dd0ec3387585dc6d7283f0b91aec Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 6 May 2014 09:56:35 +0200 Subject: [PATCH 05/30] Add more crashes to TODO --- TODO | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/TODO b/TODO index 5f1d679d4..c6127eb9f 100644 --- a/TODO +++ b/TODO @@ -28,6 +28,18 @@ Crashes @ e5000c315dd29ae9356e1b33ed041917c637c85b +- When opening a hint with F: + + 2014-05-06 09:01:25 [DEBUG] [_tabbedbrowser:tabopen:153] Opening PyQt5.QtCore.QUrl('http://ddg.gg/') + Traceback (most recent call last): + File ".\qutebrowser\widgets\_tabbedbrowser.py", line 155, in tabopen + tab = WebView(self) + File ".\qutebrowser\widgets\webview.py", line 83, in __init__ + self.page_ = BrowserPage(self) + File ".\qutebrowser\browser\webpage.py", line 44, in __init__ + self.setNetworkAccessManager(QApplication.instance().networkmanager) + RuntimeError: wrapped C/C++ object of type NetworkManager has been deleted + - Weird TypeError: QIODevice::read: device not open @@ -43,6 +55,17 @@ Crashes @ e5000c315dd29ae9356e1b33ed041917c637c85b +- When following a hint: + + QNetworkReplyImplPrivate::error: Internal problem, this method must only be called once. + +- When enabling javascript on http://google.com/accounts and reloading: + + OpenType support missing for script 12 + + When javascript is enabled already, hangs. + With minimal browser, prints error but continues to run. + Bugs ==== From a4c8796cc0699a9f5b9b8b6dfd8dd335a185eeed Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 6 May 2014 10:02:29 +0200 Subject: [PATCH 06/30] More info for weird OpenType bug --- TODO | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/TODO b/TODO index c6127eb9f..ad9a332eb 100644 --- a/TODO +++ b/TODO @@ -63,9 +63,15 @@ Crashes OpenType support missing for script 12 + Then it quits. When javascript is enabled already, hangs. With minimal browser, prints error but continues to run. + Also quits on http://de.wikipedia.org/wiki/Hallo-Welt-Programm + + It seems this only happens on Windows. + + Bugs ==== From 70d6efff966aec957dbe66e105ffa8603756f1da Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 6 May 2014 10:04:40 +0200 Subject: [PATCH 07/30] Elements might be deleted already when hint mode is left --- qutebrowser/browser/hints.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/qutebrowser/browser/hints.py b/qutebrowser/browser/hints.py index dde970263..0fe68b3cc 100644 --- a/qutebrowser/browser/hints.py +++ b/qutebrowser/browser/hints.py @@ -472,9 +472,14 @@ class HintManager(QObject): if mode != 'hint': return for elem in self._elems.values(): - elem.label.removeFromDocument() - self._frame.contentsSizeChanged.disconnect( - self.on_contents_size_changed) + if not elem.label.isNull(): + elem.label.removeFromDocument() + if self._frame is not None: + # The frame which was focused in start() might not be available + # anymore, since Qt might already have deleted it (e.g. when a new + # page is loaded). + self._frame.contentsSizeChanged.disconnect( + self.on_contents_size_changed) self._elems = {} self._to_follow = None self._target = None From 74b42eaa93a0561f2d55c4b093dc966422d63848 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 6 May 2014 10:32:35 +0200 Subject: [PATCH 08/30] Update TODO --- TODO | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/TODO b/TODO index ad9a332eb..d60eafb02 100644 --- a/TODO +++ b/TODO @@ -71,11 +71,17 @@ Crashes It seems this only happens on Windows. +- Closing some tabs and undo all them -> quits and CMD.exe hangs + + QHttpNetworkConnectionPrivate::_q_hostLookupFinished could not dequeu request + Bugs ==== - All kind of FIXMEs +- u opens empty URLs +- Wikipedia looks weird Style ===== @@ -104,6 +110,7 @@ Major features Minor features ============== +- clear cookies - keybind/aliases should have completion for commands/arguments - Hiding scrollbars - Ctrl+A/X to increase/decrease last number in URL From de7c6a63b48f66e164bf4baa6e892bcbb326d6b9 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 6 May 2014 10:53:38 +0200 Subject: [PATCH 09/30] Fix shutdown of networkmanager --- TODO | 2 ++ qutebrowser/app.py | 44 +++++++++++++++++++++------------- qutebrowser/widgets/webview.py | 6 ----- 3 files changed, 29 insertions(+), 23 deletions(-) diff --git a/TODO b/TODO index d60eafb02..6955fb510 100644 --- a/TODO +++ b/TODO @@ -110,6 +110,8 @@ Major features Minor features ============== +- Enable disk caching + QNetworkManager.setCache() and use a QNetworkDiskCache probably - clear cookies - keybind/aliases should have completion for commands/arguments - Hiding scrollbars diff --git a/qutebrowser/app.py b/qutebrowser/app.py index ef35657ac..8e1896c34 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -20,9 +20,9 @@ import os import sys import logging -import functools import subprocess import configparser +from functools import partial from signal import signal, SIGINT from argparse import ArgumentParser from base64 import b64encode @@ -102,7 +102,11 @@ class QuteBrowser(QApplication): def __init__(self): super().__init__(sys.argv) - self._quit_status = {} + self._quit_status = { + 'crash': True, + 'tabs': False, + 'networkmanager': False + } self._timers = [] self._opened_urls = [] self._shutting_down = False @@ -392,6 +396,8 @@ class QuteBrowser(QApplication): exc = (exctype, excvalue, tb) sys.__excepthook__(*exc) + self._quit_status['crash'] = False + if not issubclass(exctype, Exception): # probably a KeyboardInterrupt try: @@ -399,8 +405,6 @@ class QuteBrowser(QApplication): return except Exception: self.quit() - self._quit_status['crash'] = False - self._quit_status['shutdown'] = False try: pages = self._recover_pages() except Exception: @@ -483,49 +487,55 @@ class QuteBrowser(QApplication): @pyqtSlot() @cmdutils.register(instance='', name=['quit', 'q'], nargs=0) - def shutdown(self, do_quit=True): + def shutdown(self): """Try to shutdown everything cleanly. For some reason lastWindowClosing sometimes seem to get emitted twice, so we make sure we only run once here. - - Args: - do_quit: Whether to quit after shutting down. """ if self._shutting_down: return self._shutting_down = True - logging.debug("Shutting down... (do_quit={})".format(do_quit)) + logging.debug("Shutting down...") + # Save config if self.config.get('general', 'auto-save-config'): try: self.config.save() except AttributeError: logging.exception("Could not save config.") + # Save command history try: self.cmd_history.save() except AttributeError: logging.exception("Could not save command history.") + # Save window state try: self._save_geometry() self.stateconfig.save() except AttributeError: logging.exception("Could not save window geometry.") + # Save cookies try: self.cookiejar.save() except AttributeError: logging.exception("Could not save cookies.") + # Shut down tabs try: - if do_quit: - self.mainwindow.tabs.shutdown_complete.connect( - self.on_tab_shutdown_complete) - else: - self.mainwindow.tabs.shutdown_complete.connect( - functools.partial(self._maybe_quit, 'shutdown')) + self.mainwindow.tabs.shutdown_complete.connect(partial( + self._maybe_quit, 'tabs')) self.mainwindow.tabs.shutdown() except AttributeError: # mainwindow or tabs could still be None logging.exception("No mainwindow/tabs to shut down.") - if do_quit: - self.quit() + self._maybe_quit('tabs') + # Shut down networkmanager + try: + self.networkmanager.abort_requests() + self.networkmanager.destroyed.connect(partial( + self._maybe_quit, 'networkmanager')) + self.networkmanager.deleteLater() + except AttributeError: + logging.exception("No networkmanager to shut down.") + self._maybe_quit('networkmanager') @pyqtSlot() def on_tab_shutdown_complete(self): diff --git a/qutebrowser/widgets/webview.py b/qutebrowser/widgets/webview.py index dec2165a8..6ccdd7fce 100644 --- a/qutebrowser/widgets/webview.py +++ b/qutebrowser/widgets/webview.py @@ -208,12 +208,6 @@ class WebView(QWebView): self._destroyed[self] = False self.destroyed.connect(functools.partial(self._on_destroyed, self)) self.deleteLater() - - netman = QApplication.instance().networkmanager - self._destroyed[netman] = False - netman.abort_requests() - netman.destroyed.connect(functools.partial(self._on_destroyed, netman)) - netman.deleteLater() logging.debug("Tab shutdown scheduled") @pyqtSlot(str) From ebd2f976adb8d961ecf484d617f0a94e9e587b53 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 6 May 2014 11:39:05 +0200 Subject: [PATCH 10/30] Update TODO --- TODO | 50 +++++++++++++++++++++++++++++++++----------------- 1 file changed, 33 insertions(+), 17 deletions(-) diff --git a/TODO b/TODO index 6955fb510..ec8e5412b 100644 --- a/TODO +++ b/TODO @@ -40,21 +40,6 @@ Crashes self.setNetworkAccessManager(QApplication.instance().networkmanager) RuntimeError: wrapped C/C++ object of type NetworkManager has been deleted -- Weird TypeError: - - QIODevice::read: device not open - QIODevice::read: device not open - Traceback (most recent call last): - File "/home/florian/proj/qutebrowser/git/qutebrowser/app.py", line 567, in command_handler - handler(*args) - File "/home/florian/proj/qutebrowser/git/qutebrowser/widgets/_tabbedbrowser.py", line 216, in tabclose - tab.shutdown(callback=partial(self._cb_tab_shutdown, tab)) - File "/home/florian/proj/qutebrowser/git/qutebrowser/widgets/webview.py", line 215, in shutdown - netman.destroyed.connect(functools.partial(self._on_destroyed, netman)) - TypeError: pyqtSignal must be bound to a QObject, not 'NetworkManager' - - @ e5000c315dd29ae9356e1b33ed041917c637c85b - - When following a hint: QNetworkReplyImplPrivate::error: Internal problem, this method must only be called once. @@ -82,6 +67,23 @@ Bugs - All kind of FIXMEs - u opens empty URLs - Wikipedia looks weird +- F on duckduckgo result page opens in current page + + It seems we don't get a linkClicked signal there. + Maybe it does some weird js stuff? + See also: http://qt-project.org/doc/qt-4.8/qwebpage.html#createWindow + +- Shutdown is still flaky. + + Some pointers: + https://code.google.com/p/webscraping/source/browse/webkit.py + Simply does setPage(None) in __del__ of webview. + + http://www.tenox.net/out/wrp11-qt.py + does del self._window; del self._view; del self._page + + http://pydoc.net/Python/grab/0.4.5/ghost.ghost/ + does webview.close(); del self.manager; del self.page; del self.mainframe Style ===== @@ -135,8 +137,15 @@ hints - filter close hints when it's the same link - ignore keypresses shortly after link following -Qt Bugs -======== +Useful things +============= + +http://www.tenox.net/out/wrp11-qt.py +https://code.google.com/p/webscraping/source/browse/webkit.py +https://github.com/jeanphix/Ghost.py/blob/master/ghost/ghost.py + +Upstream Bugs +============= - Printing under windows produced blank pages https://bugreports.qt-project.org/browse/QTBUG-19571 @@ -154,6 +163,13 @@ Qt Bugs https://bugreports.qt-project.org/browse/QTBUG-20973 https://bugreports.qt-project.org/browse/QTBUG-21036 +- dead_actute + https://bugs.freedesktop.org/show_bug.cgi?id=69476 + +- QNetworkReplyImplPrivate::error: Internal problem, this method must only be called once. + https://bugreports.qt-project.org/browse/QTBUG-30298 + + Keybinding stuff (from dwb) =========================== From 8628584041e281a21ee6589b91ebb732987a58a7 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 6 May 2014 12:03:44 +0200 Subject: [PATCH 11/30] Move probably fixed crashes in TODO --- TODO | 61 ++++++++++++++++++++++++++++++++++-------------------------- 1 file changed, 35 insertions(+), 26 deletions(-) diff --git a/TODO b/TODO index ec8e5412b..075c13839 100644 --- a/TODO +++ b/TODO @@ -14,32 +14,6 @@ Crashes - Chosing printer in :print gives us a segfault on Linux, but :printpreview works... -- Segfault when closing some tab: - QIODevice::read: device not open - QIODevice::read: device not open - QIODevice::read: device not open - Fatal Python error: Segmentation fault - - Current thread 0x00007ff4ed080700 (most recent call first): - File "/home/florian/proj/qutebrowser/git/qutebrowser/__main__.py", line 29 in main - File "/home/florian/proj/qutebrowser/git/qutebrowser/__main__.py", line 33 in - File "/usr/lib/python3.4/runpy.py", line 86 in _run_code - File "/usr/lib/python3.4/runpy.py", line 171 in _run_module_as_main - - @ e5000c315dd29ae9356e1b33ed041917c637c85b - -- When opening a hint with F: - - 2014-05-06 09:01:25 [DEBUG] [_tabbedbrowser:tabopen:153] Opening PyQt5.QtCore.QUrl('http://ddg.gg/') - Traceback (most recent call last): - File ".\qutebrowser\widgets\_tabbedbrowser.py", line 155, in tabopen - tab = WebView(self) - File ".\qutebrowser\widgets\webview.py", line 83, in __init__ - self.page_ = BrowserPage(self) - File ".\qutebrowser\browser\webpage.py", line 44, in __init__ - self.setNetworkAccessManager(QApplication.instance().networkmanager) - RuntimeError: wrapped C/C++ object of type NetworkManager has been deleted - - When following a hint: QNetworkReplyImplPrivate::error: Internal problem, this method must only be called once. @@ -170,6 +144,41 @@ Upstream Bugs https://bugreports.qt-project.org/browse/QTBUG-30298 +Probably fixed crashes +====================== + +- Segfault when closing some tab: + QIODevice::read: device not open + QIODevice::read: device not open + QIODevice::read: device not open + Fatal Python error: Segmentation fault + + Current thread 0x00007ff4ed080700 (most recent call first): + File "/home/florian/proj/qutebrowser/git/qutebrowser/__main__.py", line 29 in main + File "/home/florian/proj/qutebrowser/git/qutebrowser/__main__.py", line 33 in + File "/usr/lib/python3.4/runpy.py", line 86 in _run_code + File "/usr/lib/python3.4/runpy.py", line 171 in _run_module_as_main + + @ e5000c315dd29ae9356e1b33ed041917c637c85b + + Probably fixed by de7c6a63b48f66e164bf4baa6e892bcbb326d6b9 + +- When opening a hint with F: + + 2014-05-06 09:01:25 [DEBUG] [_tabbedbrowser:tabopen:153] Opening PyQt5.QtCore.QUrl('http://ddg.gg/') + Traceback (most recent call last): + File ".\qutebrowser\widgets\_tabbedbrowser.py", line 155, in tabopen + tab = WebView(self) + File ".\qutebrowser\widgets\webview.py", line 83, in __init__ + self.page_ = BrowserPage(self) + File ".\qutebrowser\browser\webpage.py", line 44, in __init__ + self.setNetworkAccessManager(QApplication.instance().networkmanager) + RuntimeError: wrapped C/C++ object of type NetworkManager has been deleted + + Probably fixed by de7c6a63b48f66e164bf4baa6e892bcbb326d6b9 + + + Keybinding stuff (from dwb) =========================== From 3c20b78d8b7810841f84625a6e7a829e71186c8a Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 6 May 2014 12:04:56 +0200 Subject: [PATCH 12/30] Fix config typo --- qutebrowser/config/configdata.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index f78308a85..5e130d27c 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -485,7 +485,7 @@ DATA = OrderedDict([ ('offline-web-application-cache-quota', SettingValue(types.WebKitBytes(maxsize=INT64_MAX), ''), - "Quota for the offline web application cache>"), + "Quota for the offline web application cache."), )), ('hints', sect.KeyValue( From 1a3ed1107001ae477ac8c0f64c01fca65197af8d Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 6 May 2014 12:11:00 +0200 Subject: [PATCH 13/30] Quit properly on debugger exit --- qutebrowser/app.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 8e1896c34..e0e561e00 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -22,6 +22,7 @@ import sys import logging import subprocess import configparser +from bdb import BdbQuit from functools import partial from signal import signal, SIGINT from argparse import ArgumentParser @@ -398,8 +399,8 @@ class QuteBrowser(QApplication): self._quit_status['crash'] = False - if not issubclass(exctype, Exception): - # probably a KeyboardInterrupt + if exctype is BdbQuit or not issubclass(exctype, Exception): + # pdb exit, KeyboardInterrupt, ... try: self.shutdown() return From f6c3e00d591919f2fe273acfe54d9b4f128a7e19 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 6 May 2014 12:20:02 +0200 Subject: [PATCH 14/30] Use NoneString config type for settings with Qt defaults. This fixes wikipedia looking ugly because setUserStylesheet gets called with an empty string. --- qutebrowser/browser/webpage.py | 2 +- qutebrowser/config/_conftypes.py | 11 +++++++++++ qutebrowser/config/configdata.py | 28 +++++++++++++-------------- qutebrowser/network/networkmanager.py | 6 ++++-- 4 files changed, 30 insertions(+), 17 deletions(-) diff --git a/qutebrowser/browser/webpage.py b/qutebrowser/browser/webpage.py index 03b65d5a6..25c677fc7 100644 --- a/qutebrowser/browser/webpage.py +++ b/qutebrowser/browser/webpage.py @@ -72,7 +72,7 @@ class BrowserPage(QWebPage): def userAgentForUrl(self, url): """Override QWebPage::userAgentForUrl to customize the user agent.""" ua = config.get('network', 'user-agent') - if not ua: + if ua is None: return super().userAgentForUrl(url) else: return ua diff --git a/qutebrowser/config/_conftypes.py b/qutebrowser/config/_conftypes.py index bf2c82412..f34893062 100644 --- a/qutebrowser/config/_conftypes.py +++ b/qutebrowser/config/_conftypes.py @@ -165,6 +165,7 @@ class String(BaseType): self.forbidden = forbidden def transform(self, value): + # FIXME that .lower() probably isn't always a good idea... return value.lower() def validate(self, value): @@ -180,6 +181,16 @@ class String(BaseType): self.maxlen)) +class NoneString(String): + + """String which returns None if it's empty.""" + + def transform(self, value): + if not value: + return None + return super().transform(value) + + class ShellCommand(String): """A shellcommand which is split via shlex. diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 5e130d27c..68eed8e64 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -203,11 +203,11 @@ DATA = OrderedDict([ "Value to send in the DNT header."), ('accept-language', - SettingValue(types.String(), 'en-US,en'), + SettingValue(types.NoneString(), 'en-US,en'), "Value to send in the accept-language header."), ('user-agent', - SettingValue(types.String(), ''), + SettingValue(types.NoneString(), ''), "User agent to send. Empty to send the default."), ('accept-cookies', @@ -422,51 +422,51 @@ DATA = OrderedDict([ "User stylesheet to set."), ('css-media-type', - SettingValue(types.String(), ''), + SettingValue(types.NoneString(), ''), "Set the CSS media type."), ('default-encoding', - SettingValue(types.String(), ''), + SettingValue(types.NoneString(), ''), "Default encoding to use for websites."), ('font-family-standard', - SettingValue(types.String(), ''), + SettingValue(types.NoneString(), ''), "Font family for standard fonts."), ('font-family-fixed', - SettingValue(types.String(), ''), + SettingValue(types.NoneString(), ''), "Font family for fixed fonts."), ('font-family-serif', - SettingValue(types.String(), ''), + SettingValue(types.NoneString(), ''), "Font family for serif fonts."), ('font-family-sans-serif', - SettingValue(types.String(), ''), + SettingValue(types.NoneString(), ''), "Font family for sans-serif fonts."), ('font-family-cursive', - SettingValue(types.String(), ''), + SettingValue(types.NoneString(), ''), "Font family for cursive fonts."), ('font-family-fantasy', - SettingValue(types.String(), ''), + SettingValue(types.NoneString(), ''), "Font family for fantasy fonts."), ('font-size-minimum', - SettingValue(types.String(), ''), + SettingValue(types.NoneString(), ''), "The hard minimum font size."), ('font-size-minimum-logical', - SettingValue(types.String(), ''), + SettingValue(types.NoneString(), ''), "The minimum logical font size that is applied when zooming out."), ('font-size-default', - SettingValue(types.String(), ''), + SettingValue(types.NoneString(), ''), "The default font size for regular text."), ('font-size-default-fixed', - SettingValue(types.String(), ''), + SettingValue(types.NoneString(), ''), "The default font size for fixed-pitch text."), ('maximum-pages-in-cache', diff --git a/qutebrowser/network/networkmanager.py b/qutebrowser/network/networkmanager.py index 26a5e1f0f..7932d2a80 100644 --- a/qutebrowser/network/networkmanager.py +++ b/qutebrowser/network/networkmanager.py @@ -72,8 +72,10 @@ class NetworkManager(QNetworkAccessManager): dnt = '0'.encode('ascii') req.setRawHeader('DNT'.encode('ascii'), dnt) req.setRawHeader('X-Do-Not-Track'.encode('ascii'), dnt) - req.setRawHeader('Accept-Language'.encode('ascii'), - config.get('network', 'accept-language')) + accept_language = config.get('network', 'accept-language') + if accept_language is not None: + req.setRawHeader('Accept-Language'.encode('ascii'), + accept_language.encode('ascii')) reply = super().createRequest(op, req, outgoing_data) self._requests[id(reply)] = reply reply.destroyed.connect(lambda obj: self._requests.pop(id(obj))) From a0e71dc86e9632658ff616779d9798d0e954cb60 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 6 May 2014 12:55:26 +0200 Subject: [PATCH 15/30] Don't transform strings in config to lowercase --- qutebrowser/config/_conftypes.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/qutebrowser/config/_conftypes.py b/qutebrowser/config/_conftypes.py index f34893062..9cf08ef20 100644 --- a/qutebrowser/config/_conftypes.py +++ b/qutebrowser/config/_conftypes.py @@ -164,10 +164,6 @@ class String(BaseType): self.maxlen = maxlen self.forbidden = forbidden - def transform(self, value): - # FIXME that .lower() probably isn't always a good idea... - return value.lower() - def validate(self, value): if self.forbidden is not None and any(c in value for c in self.forbidden): From cbc363912e08e3e4a231452431dd95d15c8474fe Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 6 May 2014 13:00:49 +0200 Subject: [PATCH 16/30] Merge None{Int,String} with Int/String conftype --- qutebrowser/config/_conftypes.py | 44 ++++++++++++-------------------- qutebrowser/config/configdata.py | 30 +++++++++++----------- 2 files changed, 31 insertions(+), 43 deletions(-) diff --git a/qutebrowser/config/_conftypes.py b/qutebrowser/config/_conftypes.py index 9cf08ef20..04e0b21d5 100644 --- a/qutebrowser/config/_conftypes.py +++ b/qutebrowser/config/_conftypes.py @@ -155,14 +155,22 @@ class String(BaseType): Attributes: minlen: Minimum length (inclusive). maxlen: Maximum length (inclusive). + forbidden: Forbidden chars in the string. + none: Whether to convert to None for an empty string. """ typestr = 'string' - def __init__(self, minlen=None, maxlen=None, forbidden=None): + def __init__(self, minlen=None, maxlen=None, forbidden=None, none=False): self.minlen = minlen self.maxlen = maxlen self.forbidden = forbidden + self.none = none + + def transform(self, value): + if self.none and not value: + return None + return value def validate(self, value): if self.forbidden is not None and any(c in value @@ -177,16 +185,6 @@ class String(BaseType): self.maxlen)) -class NoneString(String): - - """String which returns None if it's empty.""" - - def transform(self, value): - if not value: - return None - return super().transform(value) - - class ShellCommand(String): """A shellcommand which is split via shlex. @@ -252,18 +250,24 @@ class Int(BaseType): Attributes: minval: Minimum value (inclusive). maxval: Maximum value (inclusive). + none: Whether to accept empty values as None. """ typestr = 'int' - def __init__(self, minval=None, maxval=None): + def __init__(self, minval=None, maxval=None, none=False): self.minval = minval self.maxval = maxval + self.none = none def transform(self, value): + if self.none and not value: + return None return int(value) def validate(self, value): + if self.none and not value: + return try: intval = int(value) except ValueError: @@ -276,22 +280,6 @@ class Int(BaseType): self.maxval)) -class NoneInt(BaseType): - - """Int or None.""" - - def transform(self, value): - if value == '': - return None - else: - return super().transform(value) - - def validate(self, value): - if value == '': - return - super().validate(value) - - class Float(BaseType): """Base class for an float setting. diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 68eed8e64..d74113bf4 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -203,11 +203,11 @@ DATA = OrderedDict([ "Value to send in the DNT header."), ('accept-language', - SettingValue(types.NoneString(), 'en-US,en'), + SettingValue(types.String(none=True), 'en-US,en'), "Value to send in the accept-language header."), ('user-agent', - SettingValue(types.NoneString(), ''), + SettingValue(types.String(none=True), ''), "User agent to send. Empty to send the default."), ('accept-cookies', @@ -422,55 +422,55 @@ DATA = OrderedDict([ "User stylesheet to set."), ('css-media-type', - SettingValue(types.NoneString(), ''), + SettingValue(types.String(none=True), ''), "Set the CSS media type."), ('default-encoding', - SettingValue(types.NoneString(), ''), + SettingValue(types.String(none=True), ''), "Default encoding to use for websites."), ('font-family-standard', - SettingValue(types.NoneString(), ''), + SettingValue(types.String(none=True), ''), "Font family for standard fonts."), ('font-family-fixed', - SettingValue(types.NoneString(), ''), + SettingValue(types.String(none=True), ''), "Font family for fixed fonts."), ('font-family-serif', - SettingValue(types.NoneString(), ''), + SettingValue(types.String(none=True), ''), "Font family for serif fonts."), ('font-family-sans-serif', - SettingValue(types.NoneString(), ''), + SettingValue(types.String(none=True), ''), "Font family for sans-serif fonts."), ('font-family-cursive', - SettingValue(types.NoneString(), ''), + SettingValue(types.String(none=True), ''), "Font family for cursive fonts."), ('font-family-fantasy', - SettingValue(types.NoneString(), ''), + SettingValue(types.String(none=True), ''), "Font family for fantasy fonts."), ('font-size-minimum', - SettingValue(types.NoneString(), ''), + SettingValue(types.String(none=True), ''), "The hard minimum font size."), ('font-size-minimum-logical', - SettingValue(types.NoneString(), ''), + SettingValue(types.String(none=True), ''), "The minimum logical font size that is applied when zooming out."), ('font-size-default', - SettingValue(types.NoneString(), ''), + SettingValue(types.String(none=True), ''), "The default font size for regular text."), ('font-size-default-fixed', - SettingValue(types.NoneString(), ''), + SettingValue(types.String(none=True), ''), "The default font size for fixed-pitch text."), ('maximum-pages-in-cache', - SettingValue(types.NoneInt(), ''), + SettingValue(types.Int(none=True), ''), "The default font size for fixed-pitch text."), ('object-cache-capacities', From 9553cb6872752737679eae8718e7ba042d72847c Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 6 May 2014 13:10:38 +0200 Subject: [PATCH 17/30] Clean up config type order --- qutebrowser/config/_conftypes.py | 488 ++++++++++++++++--------------- 1 file changed, 251 insertions(+), 237 deletions(-) diff --git a/qutebrowser/config/_conftypes.py b/qutebrowser/config/_conftypes.py index 04e0b21d5..2065f9856 100644 --- a/qutebrowser/config/_conftypes.py +++ b/qutebrowser/config/_conftypes.py @@ -185,37 +185,17 @@ class String(BaseType): self.maxlen)) -class ShellCommand(String): +class List(BaseType): - """A shellcommand which is split via shlex. + """Base class for a (string-)list setting.""" - Attributes: - placeholder: If there should be a placeholder. - """ - - typestr = 'shell-cmd' - - def __init__(self, placeholder=False): - self.placeholder = placeholder - super().__init__() - - def validate(self, value): - super().validate(value) - if self.placeholder and '{}' not in value: - raise ValidationError(value, "needs to contain a {}-placeholder.") + typestr = 'string-list' def transform(self, value): - return shlex.split(value) + return value.split(',') - -class HintMode(BaseType): - - """Base class for the hints -> mode setting.""" - - typestr = 'hint-mode' - - valid_values = ValidValues(('number', "Use numeric hints."), - ('letter', "Use the chars in hints -> chars.")) + def validate(self, value): + pass class Bool(BaseType): @@ -280,6 +260,23 @@ class Int(BaseType): self.maxval)) +class IntList(List): + + """Base class for an int-list setting.""" + + typestr = 'int-list' + + def transform(self, value): + vals = super().transform(value) + return map(int, vals) + + def validate(self, value): + try: + self.transform(value) + except ValueError: + raise ValidationError(value, "must be a list of integers!") + + class Float(BaseType): """Base class for an float setting. @@ -311,36 +308,6 @@ class Float(BaseType): self.maxval)) -class List(BaseType): - - """Base class for a (string-)list setting.""" - - typestr = 'string-list' - - def transform(self, value): - return value.split(',') - - def validate(self, value): - pass - - -class IntList(List): - - """Base class for an int-list setting.""" - - typestr = 'int-list' - - def transform(self, value): - vals = super().transform(value) - return map(int, vals) - - def validate(self, value): - try: - self.transform(value) - except ValueError: - raise ValidationError(value, "must be a list of integers!") - - class Perc(BaseType): """Percentage. @@ -350,6 +317,8 @@ class Perc(BaseType): maxval: Maximum value (inclusive). """ + typestr = 'percentage' + def __init__(self, minval=None, maxval=None): self.minval = minval self.maxval = maxval @@ -401,15 +370,6 @@ class PercList(List): raise ValidationError(value, "must be a list of percentages!") -class ZoomPerc(Perc): - - """A percentage which needs to be in the current zoom percentages.""" - - def validate(self, value): - super().validate(value) - # FIXME we should validate the percentage is in the list here. - - class PercOrInt(BaseType): """Percentage or integer. @@ -421,6 +381,8 @@ class PercOrInt(BaseType): maxint: Maximum value for integer (inclusive). """ + typestr = 'percentage-or-int' + def __init__(self, minperc=None, maxperc=None, minint=None, maxint=None): self.minperc = minperc self.maxperc = maxperc @@ -452,130 +414,6 @@ class PercOrInt(BaseType): self.maxint)) -class WebKitBytes(BaseType): - - """A size with an optional suffix. - - Attributes: - maxsize: The maximum size to be used. - - Class attributes: - SUFFIXES: A mapping of size suffixes to multiplicators. - """ - - SUFFIXES = { - 'k': 1024 ** 1, - 'm': 1024 ** 2, - 'g': 1024 ** 3, - 't': 1024 ** 4, - 'p': 1024 ** 5, - 'e': 1024 ** 6, - 'z': 1024 ** 7, - 'y': 1024 ** 8, - } - - def __init__(self, maxsize=None): - self.maxsize = maxsize - - def validate(self, value): - if value == '': - return - try: - val = self.transform(value) - except ValueError: - raise ValidationError(value, "must be a valid integer with " - "optional suffix!") - if self.maxsize is not None and val > self.maxsize: - raise ValidationError(value, "must be {} " - "maximum!".format(self.maxsize)) - - def transform(self, value): - if value == '': - return None - if any(value.lower().endswith(c) for c in self.SUFFIXES): - suffix = value[-1].lower() - val = value[:-1] - multiplicator = self.SUFFIXES[suffix] - else: - val = value - multiplicator = 1 - return int(val) * multiplicator - - -class WebKitBytesList(List): - - """A size with an optional suffix. - - Attributes: - length: The length of the list. - bytestype: The webkit bytes type. - """ - - def __init__(self, maxsize=None, length=None): - self.length = length - self.bytestype = WebKitBytes(maxsize) - - def transform(self, value): - if value == '': - return None - vals = super().transform(value) - return [self.bytestype.transform(val) for val in vals] - - def validate(self, value): - if value == '': - return - vals = super().transform(value) - for val in vals: - self.bytestype.validate(val) - if any(val is None for val in vals): - raise ValidationError(value, "all values need to be set!") - if self.length is not None and len(vals) != self.length: - raise ValidationError(value, "exactly {} values need to be " - "set!".format(self.length)) - - -class Proxy(BaseType): - - """A proxy URL or special value.""" - - valid_values = ValidValues(('system', "Use the system wide proxy."), - ('none', "Don't use any proxy")) - - PROXY_TYPES = { - 'http': QNetworkProxy.HttpProxy, - 'socks': QNetworkProxy.Socks5Proxy, - 'socks5': QNetworkProxy.Socks5Proxy, - } - - def validate(self, value): - if value in self.valid_values: - return - url = QUrl(value) - if (url.isValid() and not url.isEmpty() and - url.scheme() in self.PROXY_TYPES): - return - raise ValidationError(value, "must be a proxy URL (http://... or " - "socks://...) or system/none!") - - def complete(self): - out = [] - for val in self.valid_values: - out.append((val, self.valid_values.descriptions[val])) - out.append(('http://', 'HTTP proxy URL')) - out.append(('socks://', 'SOCKS proxy URL')) - return out - - def transform(self, value): - if value == 'system': - return None - elif value == 'none': - return QNetworkProxy(QNetworkProxy.NoProxy) - url = QUrl(value) - typ = self.PROXY_TYPES[url.scheme()] - return QNetworkProxy(typ, url.host(), url.port(), url.userName(), - url.password()) - - class Command(BaseType): """Base class for a command value with arguments.""" @@ -641,6 +479,225 @@ class Font(BaseType): pass +class Regex(BaseType): + + """A regular expression.""" + + typestr = 'regex' + + def __init__(self, flags=0): + self.flags = flags + + def validate(self, value): + try: + re.compile(value, self.flags) + except RegexError as e: + raise ValidationError(value, "must be a valid regex - " + str(e)) + + def transform(self, value): + return re.compile(value, self.flags) + + +class RegexList(List): + + """A list of regexes.""" + + typestr = 'regex-list' + + def __init__(self, flags=0): + self.flags = flags + + def transform(self, value): + vals = super().transform(value) + return [re.compile(pattern, self.flags) for pattern in vals] + + def validate(self, value): + try: + self.transform(value) + except RegexError as e: + raise ValidationError(value, "must be a list valid regexes - " + + str(e)) + + +class File(BaseType): + + """A file on the local filesystem.""" + + typestr = 'file' + + def validate(self, value): + if not os.path.isfile(value): + raise ValidationError(value, "must be a valid file!") + + +class WebKitBytes(BaseType): + + """A size with an optional suffix. + + Attributes: + maxsize: The maximum size to be used. + + Class attributes: + SUFFIXES: A mapping of size suffixes to multiplicators. + """ + + SUFFIXES = { + 'k': 1024 ** 1, + 'm': 1024 ** 2, + 'g': 1024 ** 3, + 't': 1024 ** 4, + 'p': 1024 ** 5, + 'e': 1024 ** 6, + 'z': 1024 ** 7, + 'y': 1024 ** 8, + } + + typestr = 'bytes' + + def __init__(self, maxsize=None): + self.maxsize = maxsize + + def validate(self, value): + if value == '': + return + try: + val = self.transform(value) + except ValueError: + raise ValidationError(value, "must be a valid integer with " + "optional suffix!") + if self.maxsize is not None and val > self.maxsize: + raise ValidationError(value, "must be {} " + "maximum!".format(self.maxsize)) + + def transform(self, value): + if value == '': + return None + if any(value.lower().endswith(c) for c in self.SUFFIXES): + suffix = value[-1].lower() + val = value[:-1] + multiplicator = self.SUFFIXES[suffix] + else: + val = value + multiplicator = 1 + return int(val) * multiplicator + + +class WebKitBytesList(List): + + """A size with an optional suffix. + + Attributes: + length: The length of the list. + bytestype: The webkit bytes type. + """ + + typestr = 'bytes-list' + + def __init__(self, maxsize=None, length=None): + self.length = length + self.bytestype = WebKitBytes(maxsize) + + def transform(self, value): + if value == '': + return None + vals = super().transform(value) + return [self.bytestype.transform(val) for val in vals] + + def validate(self, value): + if value == '': + return + vals = super().transform(value) + for val in vals: + self.bytestype.validate(val) + if any(val is None for val in vals): + raise ValidationError(value, "all values need to be set!") + if self.length is not None and len(vals) != self.length: + raise ValidationError(value, "exactly {} values need to be " + "set!".format(self.length)) + + +class ShellCommand(String): + + """A shellcommand which is split via shlex. + + Attributes: + placeholder: If there should be a placeholder. + """ + + typestr = 'shell-command' + + def __init__(self, placeholder=False): + self.placeholder = placeholder + super().__init__() + + def validate(self, value): + super().validate(value) + if self.placeholder and '{}' not in value: + raise ValidationError(value, "needs to contain a {}-placeholder.") + + def transform(self, value): + return shlex.split(value) + + +class ZoomPerc(Perc): + + """A percentage which needs to be in the current zoom percentages.""" + + def validate(self, value): + super().validate(value) + # FIXME we should validate the percentage is in the list here. + + +class HintMode(BaseType): + + """Base class for the hints -> mode setting.""" + + valid_values = ValidValues(('number', "Use numeric hints."), + ('letter', "Use the chars in hints -> chars.")) + + +class Proxy(BaseType): + + """A proxy URL or special value.""" + + valid_values = ValidValues(('system', "Use the system wide proxy."), + ('none', "Don't use any proxy")) + + PROXY_TYPES = { + 'http': QNetworkProxy.HttpProxy, + 'socks': QNetworkProxy.Socks5Proxy, + 'socks5': QNetworkProxy.Socks5Proxy, + } + + def validate(self, value): + if value in self.valid_values: + return + url = QUrl(value) + if (url.isValid() and not url.isEmpty() and + url.scheme() in self.PROXY_TYPES): + return + raise ValidationError(value, "must be a proxy URL (http://... or " + "socks://...) or system/none!") + + def complete(self): + out = [] + for val in self.valid_values: + out.append((val, self.valid_values.descriptions[val])) + out.append(('http://', 'HTTP proxy URL')) + out.append(('socks://', 'SOCKS proxy URL')) + return out + + def transform(self, value): + if value == 'system': + return None + elif value == 'none': + return QNetworkProxy(QNetworkProxy.NoProxy) + url = QUrl(value) + typ = self.PROXY_TYPES[url.scheme()] + return QNetworkProxy(typ, url.host(), url.port(), url.userName(), + url.password()) + + class SearchEngineName(BaseType): """A search engine name.""" @@ -668,55 +725,19 @@ class KeyBindingName(BaseType): pass -class Regex(BaseType): +class KeyBinding(Command): - """A regular expression.""" + """The command of a keybinding.""" - def __init__(self, flags=0): - self.flags = flags - - def validate(self, value): - try: - re.compile(value, self.flags) - except RegexError as e: - raise ValidationError(value, "must be a valid regex - " + str(e)) - - def transform(self, value): - return re.compile(value, self.flags) - - -class RegexList(List): - - """A list of regexes.""" - - def __init__(self, flags=0): - self.flags = flags - - def transform(self, value): - vals = super().transform(value) - return [re.compile(pattern, self.flags) for pattern in vals] - - def validate(self, value): - try: - self.transform(value) - except RegexError as e: - raise ValidationError(value, "must be a list valid regexes - " + - str(e)) - - -class File(BaseType): - - """A file on the local filesystem.""" - - def validate(self, value): - if not os.path.isfile(value): - raise ValidationError(value, "must be a valid file!") + pass class WebSettingsFile(File): """QWebSettings file which also can be none.""" + typestr = 'file' + def validate(self, value): if value == '': # empty values are okay @@ -786,10 +807,3 @@ class AcceptCookies(String): valid_values = ValidValues(('default', "Default QtWebKit behaviour"), ('never', "Don't accept cookies at all.")) - - -class KeyBinding(Command): - - """The command of a keybinding.""" - - pass From e683d857996f3b172915396271c9e5a36bc9d950 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 6 May 2014 14:03:48 +0200 Subject: [PATCH 18/30] Add quick and dirty fix for hint clicking --- qutebrowser/browser/hints.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/qutebrowser/browser/hints.py b/qutebrowser/browser/hints.py index 0fe68b3cc..303e82528 100644 --- a/qutebrowser/browser/hints.py +++ b/qutebrowser/browser/hints.py @@ -21,7 +21,7 @@ import logging import math from collections import namedtuple -from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QEvent, Qt +from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QEvent, Qt, QPoint from PyQt5.QtGui import QMouseEvent, QClipboard from PyQt5.QtWidgets import QApplication @@ -227,7 +227,13 @@ class HintManager(QObject): else: target = self._target self.set_open_target.emit(Target[target]) - point = elem.geometry().topLeft() + # FIXME this is a quick & dirty fix, we should: + # a) Have better heuristics where to click at (e.g. end of input + # fields) + # b) Check border/margin/padding to know where to click + # Hinting failed here for example: + # https://lsf.fh-worms.de/ + point = elem.geometry().topLeft() + QPoint(1, 1) scrollpos = self._frame.scrollPosition() logging.debug("Clicking on \"{}\" at {}/{} - {}/{}".format( elem.toPlainText(), point.x(), point.y(), scrollpos.x(), From 5656921a22a25aea1aed1f4c35e488d38548fc81 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 6 May 2014 14:08:40 +0200 Subject: [PATCH 19/30] Don't add empty URLs to URL stack --- TODO | 1 - qutebrowser/widgets/_tabbedbrowser.py | 4 +++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/TODO b/TODO index 075c13839..3bd655b7e 100644 --- a/TODO +++ b/TODO @@ -39,7 +39,6 @@ Bugs ==== - All kind of FIXMEs -- u opens empty URLs - Wikipedia looks weird - F on duckduckgo result page opens in current page diff --git a/qutebrowser/widgets/_tabbedbrowser.py b/qutebrowser/widgets/_tabbedbrowser.py index 9adc4faf2..08626868c 100644 --- a/qutebrowser/widgets/_tabbedbrowser.py +++ b/qutebrowser/widgets/_tabbedbrowser.py @@ -218,7 +218,9 @@ class TabbedBrowser(TabWidget): return last_close = config.get('tabbar', 'last-close') if self.count() > 1: - self._url_stack.append(tab.url()) + url = tab.url() + if not url.isEmpty(): + self._url_stack.append(url) self.removeTab(idx) tab.shutdown(callback=partial(self._cb_tab_shutdown, tab)) elif last_close == 'quit': From 8e122abd355a34e863e9ee2aab19a3b01b773dbf Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 6 May 2014 14:08:51 +0200 Subject: [PATCH 20/30] Update TODO --- TODO | 1 - 1 file changed, 1 deletion(-) diff --git a/TODO b/TODO index 3bd655b7e..55dcc8366 100644 --- a/TODO +++ b/TODO @@ -39,7 +39,6 @@ Bugs ==== - All kind of FIXMEs -- Wikipedia looks weird - F on duckduckgo result page opens in current page It seems we don't get a linkClicked signal there. From 7fb0a7745b49f3d1bd3ed8227f67a08813818e11 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 6 May 2014 14:25:11 +0200 Subject: [PATCH 21/30] Don't treat single words as URL --- qutebrowser/test/test_urlutils.py | 1 + qutebrowser/utils/url.py | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/qutebrowser/test/test_urlutils.py b/qutebrowser/test/test_urlutils.py index 84d82fee2..8ee1bd418 100644 --- a/qutebrowser/test/test_urlutils.py +++ b/qutebrowser/test/test_urlutils.py @@ -137,6 +137,7 @@ class IsUrlNaiveTests(TestCase): 'foo bar', 'localhost test', 'another . test', + 'foo', ] def test_urls(self): diff --git a/qutebrowser/utils/url.py b/qutebrowser/utils/url.py index f44ae3bdc..3e73c557c 100644 --- a/qutebrowser/utils/url.py +++ b/qutebrowser/utils/url.py @@ -73,11 +73,14 @@ def _is_url_naive(url): True if the url really is an URL, False otherwise. """ protocols = ['http', 'https'] + u = qurl(url) + urlstr = urlstring(url) if isinstance(url, QUrl): u = url else: u = QUrl.fromUserInput(url) - if u.scheme() in protocols: + # We don't use u here because fromUserInput appends http:// automatically. + if any(urlstr.startswith(proto) for proto in protocols): return True elif '.' in u.host(): return True From 8010fa8d893c8a64d9393611d90bb4e218840beb Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 6 May 2014 14:37:47 +0200 Subject: [PATCH 22/30] Add unittest to run_checks.py --- scripts/run_checks.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/scripts/run_checks.py b/scripts/run_checks.py index 519c59da2..a00e2967c 100755 --- a/scripts/run_checks.py +++ b/scripts/run_checks.py @@ -29,6 +29,7 @@ import sys import subprocess import os import os.path +import unittest from collections import OrderedDict try: @@ -108,6 +109,13 @@ def check_pep257(args=None): print() +def check_unittest(): + print("====== unittest ======") + suite = unittest.TestLoader().discover('.') + result = unittest.TextTestRunner().run(suite) + print() + status['unittest'] = result.wasSuccessful() + def check_line(): """Run _check_file over a filetree.""" print("====== line ======") @@ -198,6 +206,7 @@ def _get_args(checker): if do_check_257: check_pep257(_get_args('pep257')) +check_unittest() for checker in ['pylint', 'flake8', 'pyroma']: # FIXME what the hell is the flake8 exit status? run(checker, _get_args(checker)) From 4999a59470dbc08de7179ea3955c90b6cd8acdd8 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 6 May 2014 15:36:15 +0200 Subject: [PATCH 23/30] Add pastebin button to crash dialog --- qutebrowser/app.py | 7 +++++++ qutebrowser/utils/misc.py | 23 +++++++++++++++++++++++ qutebrowser/widgets/crash.py | 35 ++++++++++++++++++++++++++++++++--- 3 files changed, 62 insertions(+), 3 deletions(-) diff --git a/qutebrowser/app.py b/qutebrowser/app.py index e0e561e00..be935f887 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -399,6 +399,13 @@ class QuteBrowser(QApplication): self._quit_status['crash'] = False + # Give key input back for crash dialog + try: + self.removeEventFilter(self.modeman) + except AttributeError: + # self.modeman could be None + pass + if exctype is BdbQuit or not issubclass(exctype, Exception): # pdb exit, KeyboardInterrupt, ... try: diff --git a/qutebrowser/utils/misc.py b/qutebrowser/utils/misc.py index caecb4599..92f955874 100644 --- a/qutebrowser/utils/misc.py +++ b/qutebrowser/utils/misc.py @@ -20,6 +20,8 @@ import re import sys import shlex +import urllib.request +from urllib.parse import urljoin, urlencode from functools import reduce from pkg_resources import resource_string @@ -110,3 +112,24 @@ def shell_escape(s): # use single quotes, and put single quotes into double quotes # the string $'b is then quoted as '$'"'"'b' return "'" + s.replace("'", "'\"'\"'") + "'" + + +def pastebin(text): + """Paste the text into a pastebin and return the URL.""" + api_url = 'http://paste.the-compiler.org/api/' + data = { + 'text': text, + 'title': "qutebrowser crash", + 'name': "qutebrowser", + } + encoded_data = urlencode(data).encode('utf-8') + create_url = urljoin(api_url, 'create') + headers = { + 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8' + } + request = urllib.request.Request(create_url, encoded_data, headers) + response = urllib.request.urlopen(request) + url = response.read().decode('utf-8').rstrip() + if not url.startswith('http'): + raise ValueError("Got unexpected response: {}".format(url)) + return url diff --git a/qutebrowser/widgets/crash.py b/qutebrowser/widgets/crash.py index 1e4c6179b..cb1c05689 100644 --- a/qutebrowser/widgets/crash.py +++ b/qutebrowser/widgets/crash.py @@ -19,11 +19,15 @@ import sys import traceback +from urllib.error import URLError +from PyQt5.QtCore import Qt +from PyQt5.QtGui import QClipboard from PyQt5.QtWidgets import (QDialog, QLabel, QTextEdit, QPushButton, - QVBoxLayout, QHBoxLayout) + QVBoxLayout, QHBoxLayout, QApplication) import qutebrowser.config.config as config +import qutebrowser.utils.misc as utils from qutebrowser.utils.version import version @@ -39,6 +43,8 @@ class CrashDialog(QDialog): _hbox: The QHboxLayout containing the buttons _btn_quit: The quit button _btn_restore: the restore button + _btn_pastebin: the pastebin button + _url: Pastebin URL QLabel. """ def __init__(self, pages, cmdhist, exc): @@ -59,7 +65,7 @@ class CrashDialog(QDialog): text = ("Argh! qutebrowser crashed unexpectedly.
" "Please review the info below to remove sensitive data and " "then submit it to " - "crash@qutebrowser.org.

") + "crash@qutebrowser.org or click 'pastebin'.

") if pages: text += ("You can click \"Restore tabs\" to attempt to reopen " "your open tabs.") @@ -68,15 +74,25 @@ class CrashDialog(QDialog): self._vbox.addWidget(self._lbl) self._txt = QTextEdit() - self._txt.setReadOnly(True) + #self._txt.setReadOnly(True) self._txt.setText(self._crash_info(pages, cmdhist, exc)) self._vbox.addWidget(self._txt) + self._url = QLabel() + self._url.setTextInteractionFlags(Qt.TextSelectableByMouse | + Qt.TextSelectableByKeyboard | + Qt.LinksAccessibleByMouse | + Qt.LinksAccessibleByKeyboard) + self._vbox.addWidget(self._url) + self._hbox = QHBoxLayout() self._hbox.addStretch() self._btn_quit = QPushButton() self._btn_quit.setText("Quit") self._btn_quit.clicked.connect(self.reject) + self._btn_pastebin = QPushButton() + self._btn_pastebin.setText("Pastebin") + self._btn_pastebin.clicked.connect(self.pastebin) self._hbox.addWidget(self._btn_quit) if pages: self._btn_restore = QPushButton() @@ -84,6 +100,7 @@ class CrashDialog(QDialog): self._btn_restore.clicked.connect(self.accept) self._btn_restore.setDefault(True) self._hbox.addWidget(self._btn_restore) + self._hbox.addWidget(self._btn_pastebin) self._vbox.addLayout(self._hbox) @@ -116,3 +133,15 @@ class CrashDialog(QDialog): chunks.append('\n'.join([h, body])) return '\n\n'.join(chunks) + + def pastebin(self): + """Paste the crash info into the pastebin.""" + try: + url = utils.pastebin(self._txt.toPlainText()) + except (URLError, ValueError) as e: + self._url.setText('Error while pasting: {}'.format(e)) + return + self._btn_pastebin.setEnabled(False) + self._url.setText("URL copied to clipboard: " + "{}".format(url, url)) + QApplication.clipboard().setText(url, QClipboard.Clipboard) From 764c37c8d6d89ce5779a9638e90a1e96f76db21b Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 6 May 2014 17:02:32 +0200 Subject: [PATCH 24/30] Hide elements instead of deleting them --- qutebrowser/browser/hints.py | 70 +++++++++++++++++++++++++----------- 1 file changed, 49 insertions(+), 21 deletions(-) diff --git a/qutebrowser/browser/hints.py b/qutebrowser/browser/hints.py index 303e82528..a71595f6e 100644 --- a/qutebrowser/browser/hints.py +++ b/qutebrowser/browser/hints.py @@ -70,6 +70,7 @@ class HintManager(QObject): """ HINT_CSS = """ + display: {display}; color: {config[colors][hints.fg]}; background: {config[colors][hints.bg]}; font: {config[fonts][hints]}; @@ -193,6 +194,24 @@ class HintManager(QObject): hintstr.insert(0, chars[0]) return ''.join(hintstr) + 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) + def _draw_label(self, elem, string): """Draw a hint label over an element. @@ -203,9 +222,7 @@ class HintManager(QObject): Return: The newly created label elment """ - rect = elem.geometry() - css = self.HINT_CSS.format(left=rect.x(), top=rect.y(), - config=config.instance()) + css = self._get_hint_css(elem) doc = self._frame.documentElement() # It seems impossible to create an empty QWebElement for which isNull() # is false so we can work with it. @@ -388,34 +405,47 @@ class HintManager(QObject): def handle_partial_key(self, keystr): """Handle a new partial keypress.""" - delete = [] for (string, elems) in self._elems.items(): if string.startswith(keystr): matched = string[:len(keystr)] rest = string[len(keystr):] elems.label.setInnerXml('{}{}'.format( config.get('colors', 'hints.fg.match'), matched, rest)) + 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: - elems.label.removeFromDocument() - delete.append(string) - for key in delete: - del self._elems[key] + # 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) def filter_hints(self, filterstr): """Filter displayed hints according to a text.""" - delete = [] - for (string, elems) in self._elems.items(): - if not elems.elem.toPlainText().lower().startswith(filterstr): - elems.label.removeFromDocument() - delete.append(string) - for key in delete: - del self._elems[key] - if not self._elems: + 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: # Whoops, filtered all hints modeman.leave('hint') - elif len(self._elems) == 1 and config.get('hints', 'auto-follow'): + elif len(visible) == 1 and config.get('hints', 'auto-follow'): # unpacking gets us the first (and only) key in the dict. - self.fire(*self._elems) + self.fire(*visible) def fire(self, keystr, force=False): """Fire a completed hint. @@ -467,9 +497,7 @@ class HintManager(QObject): def on_contents_size_changed(self, _size): """Reposition hints if contents size changed.""" for elems in self._elems.values(): - rect = elems.elem.geometry() - css = self.HINT_CSS.format(left=rect.x(), top=rect.y(), - config=config.instance()) + css = self._get_hint_css(elems.elem, elems.label) elems.label.setAttribute('style', css) @pyqtSlot(str) From e2ded2e0ad096e000688b60c527d5937d1761e26 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 6 May 2014 17:02:44 +0200 Subject: [PATCH 25/30] Add logging to handle_partial_key --- qutebrowser/browser/hints.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qutebrowser/browser/hints.py b/qutebrowser/browser/hints.py index a71595f6e..f7ba636e3 100644 --- a/qutebrowser/browser/hints.py +++ b/qutebrowser/browser/hints.py @@ -405,6 +405,7 @@ class HintManager(QObject): def handle_partial_key(self, keystr): """Handle a new partial keypress.""" + logging.debug("Handling new keystring: '{}'".format(keystr)) for (string, elems) in self._elems.items(): if string.startswith(keystr): matched = string[:len(keystr)] From dd3ab0e336c4bd1a7716b097c24b4d975c422ef2 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 6 May 2014 17:03:01 +0200 Subject: [PATCH 26/30] Don't handle backspace as text keypress --- qutebrowser/keyinput/_basekeyparser.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qutebrowser/keyinput/_basekeyparser.py b/qutebrowser/keyinput/_basekeyparser.py index b7b388005..6c482ffee 100644 --- a/qutebrowser/keyinput/_basekeyparser.py +++ b/qutebrowser/keyinput/_basekeyparser.py @@ -150,7 +150,8 @@ class BaseKeyParser(QObject): """ logging.debug("Got key: {} / text: '{}'".format(e.key(), e.text())) txt = e.text().strip() - if not txt: + if not txt or e.key() == Qt.Key_Backspace: + # backspace is counted as text... logging.debug("Ignoring, no text") return False From 68ff9225250028bfa7e3c0f8a6ad3c850ead7423 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 6 May 2014 17:03:16 +0200 Subject: [PATCH 27/30] Add logging to basekeyparser --- qutebrowser/keyinput/_basekeyparser.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/qutebrowser/keyinput/_basekeyparser.py b/qutebrowser/keyinput/_basekeyparser.py index 6c482ffee..3188e05ca 100644 --- a/qutebrowser/keyinput/_basekeyparser.py +++ b/qutebrowser/keyinput/_basekeyparser.py @@ -173,9 +173,13 @@ class BaseKeyParser(QObject): (match, binding) = self._match_key(cmd_input) if match == self.Match.definitive: + logging.debug("Definitive match for " + "\"{}\".".format(self._keystring)) self._keystring = '' self.execute(binding, self.Type.chain, count) elif match == self.Match.ambiguous: + logging.debug("Ambigious match for " + "\"{}\".".format(self._keystring)) self._handle_ambiguous_match(binding, count) elif match == self.Match.partial: logging.debug("No match for \"{}\" (added {})".format( From 0f8926ca99da30cafe795c58bd691985da5b5e38 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 6 May 2014 17:04:06 +0200 Subject: [PATCH 28/30] Remember last pressed key category --- qutebrowser/keyinput/modeparsers.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/qutebrowser/keyinput/modeparsers.py b/qutebrowser/keyinput/modeparsers.py index 345b43ee8..5ce76c2ae 100644 --- a/qutebrowser/keyinput/modeparsers.py +++ b/qutebrowser/keyinput/modeparsers.py @@ -28,9 +28,11 @@ from PyQt5.QtCore import pyqtSignal, Qt import qutebrowser.utils.message as message import qutebrowser.config.config as config from qutebrowser.keyinput.keyparser import CommandKeyParser +from qutebrowser.utils.usertypes import enum STARTCHARS = ":/?" +LastPress = enum('none', 'filtertext', 'keystring') class NormalKeyParser(CommandKeyParser): @@ -69,6 +71,7 @@ class HintKeyParser(CommandKeyParser): Attributes: _filtertext: The text to filter with. + _last_press: The nature of the last keypress, a LastPress member. """ fire_hint = pyqtSignal(str) @@ -77,6 +80,7 @@ class HintKeyParser(CommandKeyParser): def __init__(self, parent=None): super().__init__(parent, supports_count=False, supports_chains=True) self._filtertext = '' + self._last_press = LastPress.none self.read_config('keybind.hint') def _handle_special_key(self, e): @@ -106,6 +110,7 @@ class HintKeyParser(CommandKeyParser): else: self._filtertext += e.text() self.filter_hints.emit(self._filtertext) + self._last_press = LastPress.filtertext return True def handle(self, e): @@ -120,6 +125,7 @@ class HintKeyParser(CommandKeyParser): handled = self._handle_single_key(e) if handled: self.keystring_updated.emit(self._keystring) + self._last_press = LastPress.keystring return handled return self._handle_special_key(e) From db20cf37018f18abd36aa6237b6447fc1a7fbd15 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 6 May 2014 17:04:34 +0200 Subject: [PATCH 29/30] Handle backspace correctly in HintKeyParser --- qutebrowser/keyinput/modeparsers.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/qutebrowser/keyinput/modeparsers.py b/qutebrowser/keyinput/modeparsers.py index 5ce76c2ae..0959676e6 100644 --- a/qutebrowser/keyinput/modeparsers.py +++ b/qutebrowser/keyinput/modeparsers.py @@ -96,15 +96,26 @@ class HintKeyParser(CommandKeyParser): Emit: filter_hints: Emitted when filter string has changed. + keystring_updated: Emitted when keystring has been changed. """ logging.debug("Got special key {} text {}".format(e.key(), e.text())) - if config.get('hints', 'mode') != 'number': - return super()._handle_special_key(e) - elif e.key() == Qt.Key_Backspace: - if self._filtertext: + if e.key() == Qt.Key_Backspace: + logging.debug("Got backspace, mode {}, filtertext \"{}\", " + "keystring \"{}\"".format( + LastPress[self._last_press], self._filtertext, + self._keystring)) + if self._last_press == LastPress.filtertext and self._filtertext: self._filtertext = self._filtertext[:-1] self.filter_hints.emit(self._filtertext) - return True + return True + elif self._last_press == LastPress.keystring and self._keystring: + self._keystring = self._keystring[:-1] + self.keystring_updated.emit(self._keystring) + return True + else: + return super()._handle_special_key(e) + elif config.get('hints', 'mode') != 'number': + return super()._handle_special_key(e) elif not e.text(): return super()._handle_special_key(e) else: From 34215806ca106267912ea2550d1f88d53aa9c3bd Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 6 May 2014 17:50:57 +0200 Subject: [PATCH 30/30] Add SO links to TODO --- TODO | 2 ++ 1 file changed, 2 insertions(+) diff --git a/TODO b/TODO index 55dcc8366..d1e9468c7 100644 --- a/TODO +++ b/TODO @@ -44,6 +44,7 @@ Bugs It seems we don't get a linkClicked signal there. Maybe it does some weird js stuff? See also: http://qt-project.org/doc/qt-4.8/qwebpage.html#createWindow + Asked here: http://stackoverflow.com/q/23498666/2085149 - Shutdown is still flaky. @@ -130,6 +131,7 @@ Upstream Bugs https://bugreports.qt-project.org/browse/QTBUG-38669 - Web inspector is blank unless .hide()/.show() is called. + Asked on SO: http://stackoverflow.com/q/23499159/2085149 - Weird font rendering https://bugreports.qt-project.org/browse/QTBUG-20973