From 695712e50c3871f00959b838909545ff12ae3d6b Mon Sep 17 00:00:00 2001 From: Artur Shaik Date: Thu, 9 Apr 2015 22:55:42 +0600 Subject: [PATCH 001/182] Basic caret and visual modes implementation Allow user switch in caret mode for browsing with caret, and visual mode for select and yank text with keyboard. Default keybindings is c or v for caret mode, and again v for visual mode. All basic movements provided by WebAction enum implemened with vim-like bindings. Yanking with y and Y for selection and clipboard respectively. There is bug/feature in WebKit that after caret enabled, caret doesn't show until mouse click (or sometimes Tab helps). So I add some workaround for that with mouse event. I think should be better aproach. Signed-off-by: Artur Shaik --- qutebrowser/browser/commands.py | 191 ++++++++++++++++++++++++ qutebrowser/browser/webview.py | 38 ++++- qutebrowser/config/configdata.py | 33 ++++ qutebrowser/keyinput/modeman.py | 5 + qutebrowser/keyinput/modeparsers.py | 24 +++ qutebrowser/mainwindow/tabbedbrowser.py | 3 +- qutebrowser/utils/usertypes.py | 2 +- 7 files changed, 293 insertions(+), 3 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 7dc0a35fd..93488bd04 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -40,6 +40,7 @@ from qutebrowser.browser import webelem from qutebrowser.utils import (message, usertypes, log, qtutils, urlutils, objreg, utils) from qutebrowser.misc import editor +from qutebrowser.keyinput import modeman class CommandDispatcher: @@ -1071,3 +1072,193 @@ class CommandDispatcher: elem.evaluateJavaScript("this.value='{}'".format(text)) except webelem.IsNullError: raise cmdexc.CommandError("Element vanished while editing!") + + @cmdutils.register(instance='command-dispatcher', + modes=[usertypes.KeyMode.caret, usertypes.KeyMode.visual], + hide=True, scope='window') + def move_to_next_line(self): + """Move the cursor or select to the next line.""" + modemanager = modeman._get_modeman(self._win_id) + if modemanager.mode == usertypes.KeyMode.caret: + act = QWebPage.MoveToNextLine + else: + act = QWebPage.SelectNextLine + self._current_widget().triggerPageAction(act) + + @cmdutils.register(instance='command-dispatcher', + modes=[usertypes.KeyMode.caret, usertypes.KeyMode.visual], + hide=True, scope='window') + def move_to_prev_line(self): + """Move the cursor or select to the prev line.""" + modemanager = modeman._get_modeman(self._win_id) + if modemanager.mode == usertypes.KeyMode.caret: + act = QWebPage.MoveToPreviousLine + else: + act = QWebPage.SelectPreviousLine + self._current_widget().triggerPageAction(act) + + @cmdutils.register(instance='command-dispatcher', + modes=[usertypes.KeyMode.caret, usertypes.KeyMode.visual], + hide=True, scope='window') + def move_to_next_char(self): + """Move the cursor or select to the next char.""" + modemanager = modeman._get_modeman(self._win_id) + if modemanager.mode == usertypes.KeyMode.caret: + act = QWebPage.MoveToNextChar + else: + act = QWebPage.SelectNextChar + self._current_widget().triggerPageAction(act) + + @cmdutils.register(instance='command-dispatcher', + modes=[usertypes.KeyMode.caret, usertypes.KeyMode.visual], + hide=True, scope='window') + def move_to_prev_char(self): + """Move the cursor or select to the prev char.""" + modemanager = modeman._get_modeman(self._win_id) + if modemanager.mode == usertypes.KeyMode.caret: + act = QWebPage.MoveToPreviousChar + else: + act = QWebPage.SelectPreviousChar + self._current_widget().triggerPageAction(act) + + @cmdutils.register(instance='command-dispatcher', + modes=[usertypes.KeyMode.caret, usertypes.KeyMode.visual], + hide=True, scope='window') + def move_to_end_of_word(self): + """Move the cursor or select to the next word.""" + modemanager = modeman._get_modeman(self._win_id) + if modemanager.mode == usertypes.KeyMode.caret: + act = QWebPage.MoveToNextWord + else: + act = QWebPage.SelectNextWord + self._current_widget().triggerPageAction(act) + + @cmdutils.register(instance='command-dispatcher', + modes=[usertypes.KeyMode.caret, usertypes.KeyMode.visual], + hide=True, scope='window') + def move_to_next_word(self): + """Move the cursor or select to the next word.""" + modemanager = modeman._get_modeman(self._win_id) + if modemanager.mode == usertypes.KeyMode.caret: + act = [QWebPage.MoveToNextWord, QWebPage.MoveToNextChar] + else: + act = [QWebPage.SelectNextWord, QWebPage.SelectNextChar] + for a in act: + self._current_widget().triggerPageAction(a) + + @cmdutils.register(instance='command-dispatcher', + modes=[usertypes.KeyMode.caret, usertypes.KeyMode.visual], + hide=True, scope='window') + def move_to_prev_word(self): + """Move the cursor or select to the prev word.""" + modemanager = modeman._get_modeman(self._win_id) + if modemanager.mode == usertypes.KeyMode.caret: + act = QWebPage.MoveToPreviousWord + else: + act = QWebPage.SelectPreviousWord + self._current_widget().triggerPageAction(act) + + @cmdutils.register(instance='command-dispatcher', + modes=[usertypes.KeyMode.caret, usertypes.KeyMode.visual], + hide=True, scope='window') + def move_to_start_of_line(self): + """Move the cursor or select to the start of line.""" + modemanager = modeman._get_modeman(self._win_id) + if modemanager.mode == usertypes.KeyMode.caret: + act = QWebPage.MoveToStartOfLine + else: + act = QWebPage.SelectStartOfLine + self._current_widget().triggerPageAction(act) + + @cmdutils.register(instance='command-dispatcher', + modes=[usertypes.KeyMode.caret, usertypes.KeyMode.visual], + hide=True, scope='window') + def move_to_end_of_line(self): + """Move the cursor or select to the end of line.""" + modemanager = modeman._get_modeman(self._win_id) + if modemanager.mode == usertypes.KeyMode.caret: + act = QWebPage.MoveToEndOfLine + else: + act = QWebPage.SelectEndOfLine + self._current_widget().triggerPageAction(act) + + @cmdutils.register(instance='command-dispatcher', + modes=[usertypes.KeyMode.caret, usertypes.KeyMode.visual], + hide=True, scope='window') + def move_to_start_of_block(self): + """Move the cursor or select to the start of block.""" + modemanager = modeman._get_modeman(self._win_id) + if modemanager.mode == usertypes.KeyMode.caret: + act = QWebPage.MoveToStartOfBlock + else: + act = QWebPage.SelectStartOfBlock + self._current_widget().triggerPageAction(act) + + @cmdutils.register(instance='command-dispatcher', + modes=[usertypes.KeyMode.caret, usertypes.KeyMode.visual], + hide=True, scope='window') + def move_to_end_of_block(self): + """Move the cursor or select to the end of block.""" + modemanager = modeman._get_modeman(self._win_id) + if modemanager.mode == usertypes.KeyMode.caret: + act = QWebPage.MoveToEndOfBlock + else: + act = QWebPage.SelectEndOfBlock + self._current_widget().triggerPageAction(act) + + @cmdutils.register(instance='command-dispatcher', + modes=[usertypes.KeyMode.caret, usertypes.KeyMode.visual], + hide=True, scope='window') + def move_to_start_of_document(self): + """Move the cursor or select to the start of document.""" + modemanager = modeman._get_modeman(self._win_id) + if modemanager.mode == usertypes.KeyMode.caret: + act = QWebPage.MoveToStartOfDocument + else: + act = QWebPage.SelectStartOfDocument + self._current_widget().triggerPageAction(act) + + @cmdutils.register(instance='command-dispatcher', + modes=[usertypes.KeyMode.caret, usertypes.KeyMode.visual], + hide=True, scope='window') + def move_to_end_of_document(self): + """Move the cursor or select to the end of document.""" + modemanager = modeman._get_modeman(self._win_id) + if modemanager.mode == usertypes.KeyMode.caret: + act = QWebPage.MoveToEndOfDocument + else: + act = QWebPage.SelectEndOfDocument + self._current_widget().triggerPageAction(act) + + @cmdutils.register(instance='command-dispatcher', + modes=[usertypes.KeyMode.visual], + hide=True, scope='window') + def yank_selected(self, sel=False): + """Yank selected text to the clipboard or primary selection. + + Args: + sel: Use the primary selection instead of the clipboard. + """ + s = self._current_widget().selectedText() + if not self._current_widget().hasSelection() or len(s) == 0: + message.info(self._win_id, "Nothing to yank") + return + + clipboard = QApplication.clipboard() + if sel and clipboard.supportsSelection(): + mode = QClipboard.Selection + target = "primary selection" + else: + mode = QClipboard.Clipboard + target = "clipboard" + log.misc.debug("Yanking to {}: '{}'".format(target, s)) + clipboard.setText(s, mode) + message.info(self._win_id, "{} {} yanked to {}" + .format(len(s), "char" if len(s) == 1 else "chars", target)) + + @cmdutils.register(instance='command-dispatcher', + modes=[usertypes.KeyMode.visual], + hide=True, scope='window') + def drop_selection(self): + """Drop selection and stay in visual mode.""" + self._current_widget().triggerPageAction(QWebPage.MoveToNextChar) diff --git a/qutebrowser/browser/webview.py b/qutebrowser/browser/webview.py index ee88e4089..19c34f9c3 100644 --- a/qutebrowser/browser/webview.py +++ b/qutebrowser/browser/webview.py @@ -23,10 +23,11 @@ import sys import itertools import functools -from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QTimer, QUrl +from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QTimer, QUrl, QPoint from PyQt5.QtWidgets import QApplication, QStyleFactory from PyQt5.QtWebKit import QWebSettings from PyQt5.QtWebKitWidgets import QWebView, QWebPage +from PyQt5.QtGui import QMouseEvent from qutebrowser.config import config from qutebrowser.keyinput import modeman @@ -69,6 +70,7 @@ class WebView(QWebView): _check_insertmode: If True, in mouseReleaseEvent we should check if we need to enter/leave insert mode. _default_zoom_changed: Whether the zoom was changed from the default. + _caret_exist: Whether caret already has focus element Signals: scroll_pos_changed: Scroll percentage of current tab changed. @@ -142,6 +144,7 @@ class WebView(QWebView): self.viewing_source = False self.setZoomFactor(float(config.get('ui', 'default-zoom')) / 100) self._default_zoom_changed = False + self._caret_exist = False objreg.get('config').changed.connect(self.on_config_changed) if config.get('input', 'rocker-gestures'): self.setContextMenuPolicy(Qt.PreventContextMenu) @@ -427,6 +430,31 @@ class WebView(QWebView): "entered.".format(mode)) self.setFocusPolicy(Qt.NoFocus) + self._caret_exist = False + elif mode in (usertypes.KeyMode.caret, usertypes.KeyMode.visual): + self.settings().setAttribute(QWebSettings.CaretBrowsingEnabled, True) + + tabbed_browser = objreg.get('tabbed-browser', scope='window', + window=self.win_id) + if self.tab_id == tabbed_browser._now_focused.tab_id and not self._caret_exist: + """ + Here is a workaround for auto position enabled caret. + Unfortunatly, caret doesn't appear until you click + mouse button on element. I have such behavior in dwb, + so I decided to implement this workaround. + May be should be reworked. + """ + frame = self.page().currentFrame() + halfWidth = frame.scrollBarGeometry(Qt.Horizontal).width() / 2 + point = QPoint(halfWidth,1) + event = QMouseEvent(QMouseEvent.MouseButtonPress, point, point, + point, Qt.LeftButton, Qt.LeftButton, Qt.NoModifier) + QApplication.sendEvent(self, event) + + self._caret_exist = True + else: + self._caret_exist = False + @pyqtSlot(usertypes.KeyMode) def on_mode_left(self, mode): """Restore focus policy if status-input modes were left.""" @@ -434,8 +462,16 @@ class WebView(QWebView): usertypes.KeyMode.yesno): log.webview.debug("Restoring focus policy because mode {} was " "left.".format(mode)) + elif mode in (usertypes.KeyMode.caret, usertypes.KeyMode.visual): + if self.settings().testAttribute(QWebSettings.CaretBrowsingEnabled): + if mode == usertypes.KeyMode.visual and self.hasSelection(): + # Remove selection if exist + self.triggerPageAction(QWebPage.MoveToNextChar) + self.settings().setAttribute(QWebSettings.CaretBrowsingEnabled, False) + self.setFocusPolicy(Qt.WheelFocus) + def createWindow(self, wintype): """Called by Qt when a page wants to create a new window. diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index fea34ab91..079b0da87 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -1019,6 +1019,8 @@ KEY_SECTION_DESC = { " * `prompt-accept`: Confirm the entered value.\n" " * `prompt-yes`: Answer yes to a yes/no question.\n" " * `prompt-no`: Answer no to a yes/no question."), + 'caret': ( + ""), } @@ -1083,6 +1085,7 @@ KEY_DATA = collections.OrderedDict([ ('search-next', ['n']), ('search-prev', ['N']), ('enter-mode insert', ['i']), + ('enter-mode caret', ['c', 'v']), ('yank', ['yy']), ('yank -s', ['yY']), ('yank -t', ['yt']), @@ -1178,6 +1181,36 @@ KEY_DATA = collections.OrderedDict([ ('rl-delete-char', ['']), ('rl-backward-delete-char', ['']), ])), + + ('visual', collections.OrderedDict([ + ('yank-selected', ['y']), + ('yank-selected -s', ['Y']), + ('drop-selection', ['v']), + ('enter-mode caret', ['c']), + ])), + + ('caret', collections.OrderedDict([ + ('enter-mode visual', ['v']), + ('enter-mode normal', ['c']), + ])), + + ('caret,visual', collections.OrderedDict([ + ('move-to-next-line', ['j']), + ('move-to-prev-line', ['k']), + ('move-to-next-char', ['l']), + ('move-to-prev-char', ['h']), + ('move-to-end-of-word', ['e']), + ('move-to-next-word', ['w']), + ('move-to-prev-word', ['b']), + ('move-to-start-of-line', ['0']), + ('move-to-end-of-line', ['$']), + ('move-to-start-of-document', ['gg']), + ('move-to-end-of-document', ['G']), + ('scroll -50 0', ['H']), + ('scroll 0 50', ['J']), + ('scroll 0 -50', ['K']), + ('scroll 50 0', ['L']), + ])), ]) diff --git a/qutebrowser/keyinput/modeman.py b/qutebrowser/keyinput/modeman.py index 4699de8e9..d956cfc08 100644 --- a/qutebrowser/keyinput/modeman.py +++ b/qutebrowser/keyinput/modeman.py @@ -25,6 +25,7 @@ from PyQt5.QtGui import QWindow from PyQt5.QtCore import pyqtSignal, Qt, QObject, QEvent from PyQt5.QtWidgets import QApplication from PyQt5.QtWebKitWidgets import QWebView +from PyQt5.QtWebKit import QWebSettings from qutebrowser.keyinput import modeparsers, keyparser from qutebrowser.config import config @@ -79,6 +80,8 @@ def init(win_id, parent): KM.prompt: keyparser.PassthroughKeyParser(win_id, 'prompt', modeman, warn=False), KM.yesno: modeparsers.PromptKeyParser(win_id, modeman), + KM.caret: modeparsers.CaretKeyParser(win_id, modeman), + KM.visual: modeparsers.VisualKeyParser(win_id, modeman), } objreg.register('keyparsers', keyparsers, scope='window', window=win_id) modeman.destroyed.connect( @@ -93,6 +96,8 @@ def init(win_id, parent): passthrough=True) modeman.register(KM.prompt, keyparsers[KM.prompt].handle, passthrough=True) modeman.register(KM.yesno, keyparsers[KM.yesno].handle) + modeman.register(KM.caret, keyparsers[KM.caret].handle, passthrough=True) + modeman.register(KM.visual, keyparsers[KM.visual].handle, passthrough=True) return modeman diff --git a/qutebrowser/keyinput/modeparsers.py b/qutebrowser/keyinput/modeparsers.py index 7ca4eabc7..4a545f53e 100644 --- a/qutebrowser/keyinput/modeparsers.py +++ b/qutebrowser/keyinput/modeparsers.py @@ -218,3 +218,27 @@ class HintKeyParser(keyparser.CommandKeyParser): hintmanager = objreg.get('hintmanager', scope='tab', window=self._win_id, tab='current') hintmanager.handle_partial_key(keystr) + +class CaretKeyParser(keyparser.CommandKeyParser): + + """KeyParser for Caret mode.""" + + def __init__(self, win_id, parent=None): + super().__init__(win_id, parent, supports_count=True, + supports_chains=True) + self.read_config('caret') + + def __repr__(self): + return utils.get_repr(self) + +class VisualKeyParser(keyparser.CommandKeyParser): + + """KeyParser for Visual mode.""" + + def __init__(self, win_id, parent=None): + super().__init__(win_id, parent, supports_count=True, + supports_chains=True) + self.read_config('visual') + + def __repr__(self): + return utils.get_repr(self) diff --git a/qutebrowser/mainwindow/tabbedbrowser.py b/qutebrowser/mainwindow/tabbedbrowser.py index 30cdf1245..5e2d40cf0 100644 --- a/qutebrowser/mainwindow/tabbedbrowser.py +++ b/qutebrowser/mainwindow/tabbedbrowser.py @@ -546,7 +546,8 @@ class TabbedBrowser(tabwidget.TabWidget): tab = self.widget(idx) log.modes.debug("Current tab changed, focusing {!r}".format(tab)) tab.setFocus() - for mode in (usertypes.KeyMode.hint, usertypes.KeyMode.insert): + for mode in (usertypes.KeyMode.hint, usertypes.KeyMode.insert, + usertypes.KeyMode.caret, usertypes.KeyMode.visual): modeman.maybe_leave(self._win_id, mode, 'tab changed') if self._now_focused is not None: objreg.register('last-focused-tab', self._now_focused, update=True, diff --git a/qutebrowser/utils/usertypes.py b/qutebrowser/utils/usertypes.py index 7371f3114..ba5957155 100644 --- a/qutebrowser/utils/usertypes.py +++ b/qutebrowser/utils/usertypes.py @@ -231,7 +231,7 @@ ClickTarget = enum('ClickTarget', ['normal', 'tab', 'tab_bg', 'window']) # Key input modes KeyMode = enum('KeyMode', ['normal', 'hint', 'command', 'yesno', 'prompt', - 'insert', 'passthrough']) + 'insert', 'passthrough', 'caret', 'visual']) # Available command completions From 941eac848efca4298e64c4522e0b2c201f805f44 Mon Sep 17 00:00:00 2001 From: Artur Shaik Date: Mon, 13 Apr 2015 18:37:33 +0600 Subject: [PATCH 002/182] Remove "c" key from normal -> caret mode key bindings --- 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 079b0da87..0765727b5 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -1085,7 +1085,7 @@ KEY_DATA = collections.OrderedDict([ ('search-next', ['n']), ('search-prev', ['N']), ('enter-mode insert', ['i']), - ('enter-mode caret', ['c', 'v']), + ('enter-mode caret', ['v']), ('yank', ['yy']), ('yank -s', ['yY']), ('yank -t', ['yt']), From a6443231e5644d204088c56b7bfdad436b2311d1 Mon Sep 17 00:00:00 2001 From: Artur Shaik Date: Mon, 13 Apr 2015 19:50:27 +0600 Subject: [PATCH 003/182] Add statusbar coloring for caret and visual modes --- qutebrowser/config/configdata.py | 8 +++++ qutebrowser/mainwindow/statusbar/bar.py | 45 ++++++++++++++++++++----- 2 files changed, 45 insertions(+), 8 deletions(-) diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 0765727b5..d4b68cb34 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -758,6 +758,14 @@ DATA = collections.OrderedDict([ SettingValue(typ.QssColor(), 'darkgreen'), "Background color of the statusbar in insert mode."), + ('statusbar.bg.caret', + SettingValue(typ.QssColor(), 'purple'), + "Background color of the statusbar in caret mode."), + + ('statusbar.bg.visual', + SettingValue(typ.QssColor(), '#a12dff'), + "Background color of the statusbar in visual mode."), + ('statusbar.progress.bg', SettingValue(typ.QssColor(), 'white'), "Background color of the progress bar."), diff --git a/qutebrowser/mainwindow/statusbar/bar.py b/qutebrowser/mainwindow/statusbar/bar.py index a1c8aabd0..af33d3fe6 100644 --- a/qutebrowser/mainwindow/statusbar/bar.py +++ b/qutebrowser/mainwindow/statusbar/bar.py @@ -91,6 +91,8 @@ class StatusBar(QWidget): _severity = None _prompt_active = False _insert_active = False + _caret_active = False + _visual_active = False STYLESHEET = """ QWidget#StatusBar { @@ -101,6 +103,14 @@ class StatusBar(QWidget): {{ color['statusbar.bg.insert'] }} } + QWidget#StatusBar[caret_active="true"] { + {{ color['statusbar.bg.caret'] }} + } + + QWidget#StatusBar[visual_active="true"] { + {{ color['statusbar.bg.visual'] }} + } + QWidget#StatusBar[prompt_active="true"] { {{ color['statusbar.bg.prompt'] }} } @@ -253,14 +263,31 @@ class StatusBar(QWidget): """Getter for self.insert_active, so it can be used as Qt property.""" return self._insert_active - def _set_insert_active(self, val): - """Setter for self.insert_active. + @pyqtProperty(bool) + def caret_active(self): + """Getter for self.caret_active, so it can be used as Qt property.""" + return self._caret_active + + @pyqtProperty(bool) + def visual_active(self): + """Getter for self.visual_active, so it can be used as Qt property.""" + return self._visual_active + + def _set_mode_active(self, mode, val): + """Setter for self.insert_active, self.caret_active or self.visual_active. Re-set the stylesheet after setting the value, so everything gets updated by Qt properly. """ - log.statusbar.debug("Setting insert_active to {}".format(val)) - self._insert_active = val + if mode == usertypes.KeyMode.insert: + log.statusbar.debug("Setting insert_active to {}".format(val)) + self._insert_active = val + elif mode == usertypes.KeyMode.caret: + log.statusbar.debug("Setting caret_active to {}".format(val)) + self._caret_active = val + elif mode == usertypes.KeyMode.visual: + log.statusbar.debug("Setting visual_active to {}".format(val)) + self._visual_active = val self.setStyleSheet(style.get_stylesheet(self.STYLESHEET)) def _set_mode_text(self, mode): @@ -438,8 +465,9 @@ class StatusBar(QWidget): window=self._win_id) if mode in mode_manager.passthrough: self._set_mode_text(mode.name) - if mode == usertypes.KeyMode.insert: - self._set_insert_active(True) + if mode in (usertypes.KeyMode.insert, usertypes.KeyMode.caret, + usertypes.KeyMode.visual): + self._set_mode_active(mode, True) @pyqtSlot(usertypes.KeyMode, usertypes.KeyMode) def on_mode_left(self, old_mode, new_mode): @@ -451,8 +479,9 @@ class StatusBar(QWidget): self._set_mode_text(new_mode.name) else: self.txt.set_text(self.txt.Text.normal, '') - if old_mode == usertypes.KeyMode.insert: - self._set_insert_active(False) + if old_mode in (usertypes.KeyMode.insert, usertypes.KeyMode.caret, + usertypes.KeyMode.visual): + self._set_mode_active(old_mode, False) @config.change_filter('ui', 'message-timeout') def set_pop_timer_interval(self): From e603d9a2d07b879ec221f515b14fbe84d1e1faab Mon Sep 17 00:00:00 2001 From: Artur Shaik Date: Mon, 13 Apr 2015 19:55:45 +0600 Subject: [PATCH 004/182] Slight modify of autofocus caret Make mouseclick event point slightly down. Add commented tries of more reliable methods of caret focusing. --- qutebrowser/browser/webview.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/qutebrowser/browser/webview.py b/qutebrowser/browser/webview.py index 19c34f9c3..22dca3878 100644 --- a/qutebrowser/browser/webview.py +++ b/qutebrowser/browser/webview.py @@ -446,10 +446,16 @@ class WebView(QWebView): """ frame = self.page().currentFrame() halfWidth = frame.scrollBarGeometry(Qt.Horizontal).width() / 2 - point = QPoint(halfWidth,1) + point = QPoint(halfWidth,10) event = QMouseEvent(QMouseEvent.MouseButtonPress, point, point, point, Qt.LeftButton, Qt.LeftButton, Qt.NoModifier) QApplication.sendEvent(self, event) + #frame.setFocus() + #frame.documentElement().setFocus() + #frame.documentElement().firstChild().setFocus() + #self.page().focusNextPrevChild(True) + #self.page().setContentEditable(True) + #self.triggerPageAction(QWebPage.MoveToNextChar) self._caret_exist = True else: From 5359463d79abc31b0213519a2c15f007d2fcce56 Mon Sep 17 00:00:00 2001 From: Zach-Button Date: Tue, 21 Apr 2015 14:52:43 -0600 Subject: [PATCH 005/182] Add misc/userscripts - Added misc/userscripts/dmenu_qutebrowser - Added misc/userscripts/qutebrowser_viewsource --- misc/userscripts/dmenu_qutebrowser | 31 +++++++++++++++++++++++++ misc/userscripts/qutebrowser_viewsource | 13 +++++++++++ 2 files changed, 44 insertions(+) create mode 100755 misc/userscripts/dmenu_qutebrowser create mode 100755 misc/userscripts/qutebrowser_viewsource diff --git a/misc/userscripts/dmenu_qutebrowser b/misc/userscripts/dmenu_qutebrowser new file mode 100755 index 000000000..5da1d8462 --- /dev/null +++ b/misc/userscripts/dmenu_qutebrowser @@ -0,0 +1,31 @@ +#!/bin/bash + +# Pipes history, quickmarks, and URL into dmenu. +# +# If run from qutebrowser as a userscript, it runs :open on the URL +# If not, it opens a new qutebrowser window at the URL +# +# Ideal for use with tabs-are-windows. Set a hotkey to launch this script, then: +# :bind o spawn --userscript dmenu_qutebrowser +# +# Use the hotkey to open in new tab/window, press 'o' to open URL in current tab/window +# You can simulate "go" by pressing "o", as the current URL is always first in the list +# +# I personally use "q" to launch this script. For me, my workflow is: +# Default keys Keys with this script +# O q +# o o +# go o +# gO [ I haven't decided what to do with this yet ] +# + +url=$(echo "$QUTE_URL" | cat - ~/.local/share/qutebrowser/history ~/.config/qutebrowser/quickmarks | dmenu -l 15 -p qutebrowser) +url=$(echo $url | sed -E 's/[^ ]+ +//g' | egrep "https?:" || echo $url) + +[ -z "${url// }" ] && exit + +if [ -n "$QUTE_FIFO" ]; then + echo "open $url" >> "$QUTE_FIFO" +else + qutebrowser "$url" +fi diff --git a/misc/userscripts/qutebrowser_viewsource b/misc/userscripts/qutebrowser_viewsource new file mode 100755 index 000000000..891d09dce --- /dev/null +++ b/misc/userscripts/qutebrowser_viewsource @@ -0,0 +1,13 @@ +#!/bin/bash + +# +# This script fetches the unprocessed HTML source for a page and opens it in vim. +# :bind gf spawn --userscript qutebrowser_viewsource +# + +path=/tmp/qutebrowser-src_$(date "+%s") + +curl "$QUTE_URL" > $path +urxvt -e vim "$path" + +rm "$path" From 049955dfd5e32026c9eca7da45282b114737af90 Mon Sep 17 00:00:00 2001 From: Zach-Button Date: Tue, 21 Apr 2015 16:12:05 -0600 Subject: [PATCH 006/182] Change path to use mktemp Path now uses mktemp instead of timestamp --- misc/userscripts/qutebrowser_viewsource | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/userscripts/qutebrowser_viewsource b/misc/userscripts/qutebrowser_viewsource index 891d09dce..9175d43d2 100755 --- a/misc/userscripts/qutebrowser_viewsource +++ b/misc/userscripts/qutebrowser_viewsource @@ -5,7 +5,7 @@ # :bind gf spawn --userscript qutebrowser_viewsource # -path=/tmp/qutebrowser-src_$(date "+%s") +path=/tmp/qutebrowser_$(mktemp XXXXXXXX).html curl "$QUTE_URL" > $path urxvt -e vim "$path" From 205f37fe090b559019d2d16681d6ba4ab9e10232 Mon Sep 17 00:00:00 2001 From: Zach-Button Date: Fri, 24 Apr 2015 14:04:27 -0600 Subject: [PATCH 007/182] Update dmenu_qutebrowser --- misc/userscripts/dmenu_qutebrowser | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/misc/userscripts/dmenu_qutebrowser b/misc/userscripts/dmenu_qutebrowser index 5da1d8462..3a73cdde5 100755 --- a/misc/userscripts/dmenu_qutebrowser +++ b/misc/userscripts/dmenu_qutebrowser @@ -1,5 +1,22 @@ #!/bin/bash +# Copyright 2015 Zach-Button +# +# This file is part of qutebrowser. +# +# qutebrowser is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# qutebrowser is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with qutebrowser. If not, see . + # Pipes history, quickmarks, and URL into dmenu. # # If run from qutebrowser as a userscript, it runs :open on the URL @@ -16,7 +33,7 @@ # O q # o o # go o -# gO [ I haven't decided what to do with this yet ] +# gO gC, then o # url=$(echo "$QUTE_URL" | cat - ~/.local/share/qutebrowser/history ~/.config/qutebrowser/quickmarks | dmenu -l 15 -p qutebrowser) From 329030e913f01e17b2a6fe648378115173ab5b2a Mon Sep 17 00:00:00 2001 From: Zach-Button Date: Fri, 24 Apr 2015 14:05:27 -0600 Subject: [PATCH 008/182] Update qutebrowser_viewsource --- misc/userscripts/qutebrowser_viewsource | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/misc/userscripts/qutebrowser_viewsource b/misc/userscripts/qutebrowser_viewsource index 9175d43d2..0985297d7 100755 --- a/misc/userscripts/qutebrowser_viewsource +++ b/misc/userscripts/qutebrowser_viewsource @@ -1,9 +1,28 @@ #!/bin/bash +# Copyright 2015 Zach-Button +# +# This file is part of qutebrowser. +# +# qutebrowser is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# qutebrowser is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with qutebrowser. If not, see . + # # This script fetches the unprocessed HTML source for a page and opens it in vim. # :bind gf spawn --userscript qutebrowser_viewsource # +# Caveat: Does not use authentication of any kind. Add it in if you want it to. +# path=/tmp/qutebrowser_$(mktemp XXXXXXXX).html From 9e8c7818715108878ba4767c9055a088c0971f65 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 28 Apr 2015 16:12:23 +0200 Subject: [PATCH 009/182] Use clearFocus/setFocus as workaround. --- qutebrowser/browser/webview.py | 33 ++------------------------------- 1 file changed, 2 insertions(+), 31 deletions(-) diff --git a/qutebrowser/browser/webview.py b/qutebrowser/browser/webview.py index 63461d240..0d7da576c 100644 --- a/qutebrowser/browser/webview.py +++ b/qutebrowser/browser/webview.py @@ -72,7 +72,6 @@ class WebView(QWebView): _check_insertmode: If True, in mouseReleaseEvent we should check if we need to enter/leave insert mode. _default_zoom_changed: Whether the zoom was changed from the default. - _caret_exist: Whether caret already has focus element Signals: scroll_pos_changed: Scroll percentage of current tab changed. @@ -138,7 +137,6 @@ class WebView(QWebView): self.viewing_source = False self.setZoomFactor(float(config.get('ui', 'default-zoom')) / 100) self._default_zoom_changed = False - self._caret_exist = False if config.get('input', 'rocker-gestures'): self.setContextMenuPolicy(Qt.PreventContextMenu) self.urlChanged.connect(self.on_url_changed) @@ -438,37 +436,10 @@ class WebView(QWebView): log.webview.debug("Ignoring focus because mode {} was " "entered.".format(mode)) self.setFocusPolicy(Qt.NoFocus) - - self._caret_exist = False elif mode in (usertypes.KeyMode.caret, usertypes.KeyMode.visual): self.settings().setAttribute(QWebSettings.CaretBrowsingEnabled, True) - - tabbed_browser = objreg.get('tabbed-browser', scope='window', - window=self.win_id) - if self.tab_id == tabbed_browser._now_focused.tab_id and not self._caret_exist: - """ - Here is a workaround for auto position enabled caret. - Unfortunatly, caret doesn't appear until you click - mouse button on element. I have such behavior in dwb, - so I decided to implement this workaround. - May be should be reworked. - """ - frame = self.page().currentFrame() - halfWidth = frame.scrollBarGeometry(Qt.Horizontal).width() / 2 - point = QPoint(halfWidth,10) - event = QMouseEvent(QMouseEvent.MouseButtonPress, point, point, - point, Qt.LeftButton, Qt.LeftButton, Qt.NoModifier) - QApplication.sendEvent(self, event) - #frame.setFocus() - #frame.documentElement().setFocus() - #frame.documentElement().firstChild().setFocus() - #self.page().focusNextPrevChild(True) - #self.page().setContentEditable(True) - #self.triggerPageAction(QWebPage.MoveToNextChar) - - self._caret_exist = True - else: - self._caret_exist = False + self.clearFocus() + self.setFocus(Qt.OtherFocusReason) @pyqtSlot(usertypes.KeyMode) def on_mode_left(self, mode): From 32562c6878468420271da511ca5cb8b9e9c2a2e8 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 28 Apr 2015 16:50:42 +0200 Subject: [PATCH 010/182] Fix lint. --- qutebrowser/browser/commands.py | 136 +++++++++++------------- qutebrowser/browser/webview.py | 11 +- qutebrowser/config/configdata.py | 8 +- qutebrowser/keyinput/modeman.py | 9 +- qutebrowser/keyinput/modeparsers.py | 2 + qutebrowser/mainwindow/statusbar/bar.py | 10 +- qutebrowser/mainwindow/tabbedbrowser.py | 2 +- 7 files changed, 83 insertions(+), 95 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 37b35a8f9..0adc9c57a 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -40,6 +40,7 @@ from qutebrowser.config import config, configexc from qutebrowser.browser import webelem, inspector from qutebrowser.utils import (message, usertypes, log, qtutils, urlutils, objreg, utils) +from qutebrowser.utils.usertypes import KeyMode from qutebrowser.misc import editor from qutebrowser.keyinput import modeman @@ -1031,8 +1032,7 @@ class CommandDispatcher: self._open(url, tab, bg, window) @cmdutils.register(instance='command-dispatcher', - modes=[usertypes.KeyMode.insert], - hide=True, scope='window') + modes=[KeyMode.insert], hide=True, scope='window') def open_editor(self): """Open an external editor with the currently selected form field. @@ -1152,166 +1152,153 @@ class CommandDispatcher: for _ in range(count): view.search(view.search_text, flags) - @cmdutils.register(instance='command-dispatcher', - modes=[usertypes.KeyMode.caret, usertypes.KeyMode.visual], - hide=True, scope='window') + @cmdutils.register(instance='command-dispatcher', hide=True, + modes=[KeyMode.caret, KeyMode.visual], scope='window') def move_to_next_line(self): """Move the cursor or select to the next line.""" - modemanager = modeman._get_modeman(self._win_id) - if modemanager.mode == usertypes.KeyMode.caret: + modemanager = modeman.get_modeman(self._win_id) + if modemanager.mode == KeyMode.caret: act = QWebPage.MoveToNextLine else: act = QWebPage.SelectNextLine self._current_widget().triggerPageAction(act) - @cmdutils.register(instance='command-dispatcher', - modes=[usertypes.KeyMode.caret, usertypes.KeyMode.visual], - hide=True, scope='window') + @cmdutils.register(instance='command-dispatcher', hide=True, + modes=[KeyMode.caret, KeyMode.visual], scope='window') def move_to_prev_line(self): """Move the cursor or select to the prev line.""" - modemanager = modeman._get_modeman(self._win_id) - if modemanager.mode == usertypes.KeyMode.caret: + modemanager = modeman.get_modeman(self._win_id) + if modemanager.mode == KeyMode.caret: act = QWebPage.MoveToPreviousLine else: act = QWebPage.SelectPreviousLine self._current_widget().triggerPageAction(act) - @cmdutils.register(instance='command-dispatcher', - modes=[usertypes.KeyMode.caret, usertypes.KeyMode.visual], - hide=True, scope='window') + @cmdutils.register(instance='command-dispatcher', hide=True, + modes=[KeyMode.caret, KeyMode.visual], scope='window') def move_to_next_char(self): """Move the cursor or select to the next char.""" - modemanager = modeman._get_modeman(self._win_id) - if modemanager.mode == usertypes.KeyMode.caret: + modemanager = modeman.get_modeman(self._win_id) + if modemanager.mode == KeyMode.caret: act = QWebPage.MoveToNextChar else: act = QWebPage.SelectNextChar self._current_widget().triggerPageAction(act) - @cmdutils.register(instance='command-dispatcher', - modes=[usertypes.KeyMode.caret, usertypes.KeyMode.visual], - hide=True, scope='window') + @cmdutils.register(instance='command-dispatcher', hide=True, + modes=[KeyMode.caret, KeyMode.visual], scope='window') def move_to_prev_char(self): """Move the cursor or select to the prev char.""" - modemanager = modeman._get_modeman(self._win_id) - if modemanager.mode == usertypes.KeyMode.caret: + modemanager = modeman.get_modeman(self._win_id) + if modemanager.mode == KeyMode.caret: act = QWebPage.MoveToPreviousChar else: act = QWebPage.SelectPreviousChar self._current_widget().triggerPageAction(act) - @cmdutils.register(instance='command-dispatcher', - modes=[usertypes.KeyMode.caret, usertypes.KeyMode.visual], - hide=True, scope='window') + @cmdutils.register(instance='command-dispatcher', hide=True, + modes=[KeyMode.caret, KeyMode.visual], scope='window') def move_to_end_of_word(self): """Move the cursor or select to the next word.""" - modemanager = modeman._get_modeman(self._win_id) - if modemanager.mode == usertypes.KeyMode.caret: + modemanager = modeman.get_modeman(self._win_id) + if modemanager.mode == KeyMode.caret: act = QWebPage.MoveToNextWord else: act = QWebPage.SelectNextWord self._current_widget().triggerPageAction(act) - @cmdutils.register(instance='command-dispatcher', - modes=[usertypes.KeyMode.caret, usertypes.KeyMode.visual], - hide=True, scope='window') + @cmdutils.register(instance='command-dispatcher', hide=True, + modes=[KeyMode.caret, KeyMode.visual], + scope='window') def move_to_next_word(self): """Move the cursor or select to the next word.""" - modemanager = modeman._get_modeman(self._win_id) - if modemanager.mode == usertypes.KeyMode.caret: + modemanager = modeman.get_modeman(self._win_id) + if modemanager.mode == KeyMode.caret: act = [QWebPage.MoveToNextWord, QWebPage.MoveToNextChar] else: act = [QWebPage.SelectNextWord, QWebPage.SelectNextChar] for a in act: self._current_widget().triggerPageAction(a) - @cmdutils.register(instance='command-dispatcher', - modes=[usertypes.KeyMode.caret, usertypes.KeyMode.visual], - hide=True, scope='window') + @cmdutils.register(instance='command-dispatcher', hide=True, + modes=[KeyMode.caret, KeyMode.visual], scope='window') def move_to_prev_word(self): """Move the cursor or select to the prev word.""" - modemanager = modeman._get_modeman(self._win_id) - if modemanager.mode == usertypes.KeyMode.caret: + modemanager = modeman.get_modeman(self._win_id) + if modemanager.mode == KeyMode.caret: act = QWebPage.MoveToPreviousWord else: act = QWebPage.SelectPreviousWord self._current_widget().triggerPageAction(act) - @cmdutils.register(instance='command-dispatcher', - modes=[usertypes.KeyMode.caret, usertypes.KeyMode.visual], - hide=True, scope='window') + @cmdutils.register(instance='command-dispatcher', hide=True, + modes=[KeyMode.caret, KeyMode.visual], scope='window') def move_to_start_of_line(self): """Move the cursor or select to the start of line.""" - modemanager = modeman._get_modeman(self._win_id) - if modemanager.mode == usertypes.KeyMode.caret: + modemanager = modeman.get_modeman(self._win_id) + if modemanager.mode == KeyMode.caret: act = QWebPage.MoveToStartOfLine else: act = QWebPage.SelectStartOfLine self._current_widget().triggerPageAction(act) - @cmdutils.register(instance='command-dispatcher', - modes=[usertypes.KeyMode.caret, usertypes.KeyMode.visual], - hide=True, scope='window') + @cmdutils.register(instance='command-dispatcher', hide=True, + modes=[KeyMode.caret, KeyMode.visual], scope='window') def move_to_end_of_line(self): """Move the cursor or select to the end of line.""" - modemanager = modeman._get_modeman(self._win_id) - if modemanager.mode == usertypes.KeyMode.caret: + modemanager = modeman.get_modeman(self._win_id) + if modemanager.mode == KeyMode.caret: act = QWebPage.MoveToEndOfLine else: act = QWebPage.SelectEndOfLine self._current_widget().triggerPageAction(act) - @cmdutils.register(instance='command-dispatcher', - modes=[usertypes.KeyMode.caret, usertypes.KeyMode.visual], - hide=True, scope='window') + @cmdutils.register(instance='command-dispatcher', hide=True, + modes=[KeyMode.caret, KeyMode.visual], scope='window') def move_to_start_of_block(self): """Move the cursor or select to the start of block.""" - modemanager = modeman._get_modeman(self._win_id) - if modemanager.mode == usertypes.KeyMode.caret: + modemanager = modeman.get_modeman(self._win_id) + if modemanager.mode == KeyMode.caret: act = QWebPage.MoveToStartOfBlock else: act = QWebPage.SelectStartOfBlock self._current_widget().triggerPageAction(act) - @cmdutils.register(instance='command-dispatcher', - modes=[usertypes.KeyMode.caret, usertypes.KeyMode.visual], - hide=True, scope='window') + @cmdutils.register(instance='command-dispatcher', hide=True, + modes=[KeyMode.caret, KeyMode.visual], scope='window') def move_to_end_of_block(self): """Move the cursor or select to the end of block.""" - modemanager = modeman._get_modeman(self._win_id) - if modemanager.mode == usertypes.KeyMode.caret: + modemanager = modeman.get_modeman(self._win_id) + if modemanager.mode == KeyMode.caret: act = QWebPage.MoveToEndOfBlock else: act = QWebPage.SelectEndOfBlock self._current_widget().triggerPageAction(act) - @cmdutils.register(instance='command-dispatcher', - modes=[usertypes.KeyMode.caret, usertypes.KeyMode.visual], - hide=True, scope='window') + @cmdutils.register(instance='command-dispatcher', hide=True, + modes=[KeyMode.caret, KeyMode.visual], scope='window') def move_to_start_of_document(self): """Move the cursor or select to the start of document.""" - modemanager = modeman._get_modeman(self._win_id) - if modemanager.mode == usertypes.KeyMode.caret: + modemanager = modeman.get_modeman(self._win_id) + if modemanager.mode == KeyMode.caret: act = QWebPage.MoveToStartOfDocument else: act = QWebPage.SelectStartOfDocument self._current_widget().triggerPageAction(act) - @cmdutils.register(instance='command-dispatcher', - modes=[usertypes.KeyMode.caret, usertypes.KeyMode.visual], - hide=True, scope='window') + @cmdutils.register(instance='command-dispatcher', hide=True, + modes=[KeyMode.caret, KeyMode.visual], scope='window') def move_to_end_of_document(self): """Move the cursor or select to the end of document.""" - modemanager = modeman._get_modeman(self._win_id) - if modemanager.mode == usertypes.KeyMode.caret: + modemanager = modeman.get_modeman(self._win_id) + if modemanager.mode == KeyMode.caret: act = QWebPage.MoveToEndOfDocument else: act = QWebPage.SelectEndOfDocument self._current_widget().triggerPageAction(act) - @cmdutils.register(instance='command-dispatcher', - modes=[usertypes.KeyMode.visual], - hide=True, scope='window') + @cmdutils.register(instance='command-dispatcher', hide=True, + modes=[KeyMode.visual], scope='window') def yank_selected(self, sel=False): """Yank selected text to the clipboard or primary selection. @@ -1332,12 +1319,11 @@ class CommandDispatcher: target = "clipboard" log.misc.debug("Yanking to {}: '{}'".format(target, s)) clipboard.setText(s, mode) - message.info(self._win_id, "{} {} yanked to {}" - .format(len(s), "char" if len(s) == 1 else "chars", target)) + message.info(self._win_id, "{} {} yanked to {}".format( + len(s), "char" if len(s) == 1 else "chars", target)) - @cmdutils.register(instance='command-dispatcher', - modes=[usertypes.KeyMode.visual], - hide=True, scope='window') + @cmdutils.register(instance='command-dispatcher', hide=True, + modes=[KeyMode.visual], scope='window') def drop_selection(self): """Drop selection and stay in visual mode.""" self._current_widget().triggerPageAction(QWebPage.MoveToNextChar) diff --git a/qutebrowser/browser/webview.py b/qutebrowser/browser/webview.py index 0d7da576c..d123d3c21 100644 --- a/qutebrowser/browser/webview.py +++ b/qutebrowser/browser/webview.py @@ -23,11 +23,10 @@ import sys import itertools import functools -from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QTimer, QUrl, QPoint +from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QTimer, QUrl from PyQt5.QtWidgets import QApplication, QStyleFactory from PyQt5.QtWebKit import QWebSettings from PyQt5.QtWebKitWidgets import QWebView, QWebPage -from PyQt5.QtGui import QMouseEvent from qutebrowser.config import config from qutebrowser.keyinput import modeman @@ -437,7 +436,8 @@ class WebView(QWebView): "entered.".format(mode)) self.setFocusPolicy(Qt.NoFocus) elif mode in (usertypes.KeyMode.caret, usertypes.KeyMode.visual): - self.settings().setAttribute(QWebSettings.CaretBrowsingEnabled, True) + settings = self.settings() + settings.setAttribute(QWebSettings.CaretBrowsingEnabled, True) self.clearFocus() self.setFocus(Qt.OtherFocusReason) @@ -449,11 +449,12 @@ class WebView(QWebView): log.webview.debug("Restoring focus policy because mode {} was " "left.".format(mode)) elif mode in (usertypes.KeyMode.caret, usertypes.KeyMode.visual): - if self.settings().testAttribute(QWebSettings.CaretBrowsingEnabled): + settings = self.settings() + if settings.testAttribute(QWebSettings.CaretBrowsingEnabled): if mode == usertypes.KeyMode.visual and self.hasSelection(): # Remove selection if exist self.triggerPageAction(QWebPage.MoveToNextChar) - self.settings().setAttribute(QWebSettings.CaretBrowsingEnabled, False) + settings.setAttribute(QWebSettings.CaretBrowsingEnabled, False) self.setFocusPolicy(Qt.WheelFocus) diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index dd0a7005f..85e532ad4 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -817,12 +817,12 @@ def data(readonly=False): "Background color of the statusbar in insert mode."), ('statusbar.bg.caret', - SettingValue(typ.QssColor(), 'purple'), - "Background color of the statusbar in caret mode."), + SettingValue(typ.QssColor(), 'purple'), + "Background color of the statusbar in caret mode."), ('statusbar.bg.visual', - SettingValue(typ.QssColor(), '#a12dff'), - "Background color of the statusbar in visual mode."), + SettingValue(typ.QssColor(), '#a12dff'), + "Background color of the statusbar in visual mode."), ('statusbar.progress.bg', SettingValue(typ.QssColor(), 'white'), diff --git a/qutebrowser/keyinput/modeman.py b/qutebrowser/keyinput/modeman.py index 44ac96817..665d3c197 100644 --- a/qutebrowser/keyinput/modeman.py +++ b/qutebrowser/keyinput/modeman.py @@ -24,7 +24,6 @@ import functools from PyQt5.QtCore import pyqtSignal, Qt, QObject, QEvent from PyQt5.QtWidgets import QApplication from PyQt5.QtWebKitWidgets import QWebView -from PyQt5.QtWebKit import QWebSettings from qutebrowser.keyinput import modeparsers, keyparser from qutebrowser.config import config @@ -100,25 +99,25 @@ def init(win_id, parent): return modeman -def _get_modeman(win_id): +def get_modeman(win_id): """Get a modemanager object.""" return objreg.get('mode-manager', scope='window', window=win_id) def enter(win_id, mode, reason=None, only_if_normal=False): """Enter the mode 'mode'.""" - _get_modeman(win_id).enter(mode, reason, only_if_normal) + get_modeman(win_id).enter(mode, reason, only_if_normal) def leave(win_id, mode, reason=None): """Leave the mode 'mode'.""" - _get_modeman(win_id).leave(mode, reason) + get_modeman(win_id).leave(mode, reason) def maybe_leave(win_id, mode, reason=None): """Convenience method to leave 'mode' without exceptions.""" try: - _get_modeman(win_id).leave(mode, reason) + get_modeman(win_id).leave(mode, reason) except NotInModeError as e: # This is rather likely to happen, so we only log to debug log. log.modes.debug("{} (leave reason: {})".format(e, reason)) diff --git a/qutebrowser/keyinput/modeparsers.py b/qutebrowser/keyinput/modeparsers.py index 4a545f53e..121172798 100644 --- a/qutebrowser/keyinput/modeparsers.py +++ b/qutebrowser/keyinput/modeparsers.py @@ -219,6 +219,7 @@ class HintKeyParser(keyparser.CommandKeyParser): window=self._win_id, tab='current') hintmanager.handle_partial_key(keystr) + class CaretKeyParser(keyparser.CommandKeyParser): """KeyParser for Caret mode.""" @@ -231,6 +232,7 @@ class CaretKeyParser(keyparser.CommandKeyParser): def __repr__(self): return utils.get_repr(self) + class VisualKeyParser(keyparser.CommandKeyParser): """KeyParser for Visual mode.""" diff --git a/qutebrowser/mainwindow/statusbar/bar.py b/qutebrowser/mainwindow/statusbar/bar.py index af33d3fe6..0b041c5dc 100644 --- a/qutebrowser/mainwindow/statusbar/bar.py +++ b/qutebrowser/mainwindow/statusbar/bar.py @@ -274,7 +274,7 @@ class StatusBar(QWidget): return self._visual_active def _set_mode_active(self, mode, val): - """Setter for self.insert_active, self.caret_active or self.visual_active. + """Setter for self.{insert,caret,visual}_active. Re-set the stylesheet after setting the value, so everything gets updated by Qt properly. @@ -465,8 +465,8 @@ class StatusBar(QWidget): window=self._win_id) if mode in mode_manager.passthrough: self._set_mode_text(mode.name) - if mode in (usertypes.KeyMode.insert, usertypes.KeyMode.caret, - usertypes.KeyMode.visual): + if mode in (usertypes.KeyMode.insert, usertypes.KeyMode.caret, + usertypes.KeyMode.visual): self._set_mode_active(mode, True) @pyqtSlot(usertypes.KeyMode, usertypes.KeyMode) @@ -479,8 +479,8 @@ class StatusBar(QWidget): self._set_mode_text(new_mode.name) else: self.txt.set_text(self.txt.Text.normal, '') - if old_mode in (usertypes.KeyMode.insert, usertypes.KeyMode.caret, - usertypes.KeyMode.visual): + if old_mode in (usertypes.KeyMode.insert, usertypes.KeyMode.caret, + usertypes.KeyMode.visual): self._set_mode_active(old_mode, False) @config.change_filter('ui', 'message-timeout') diff --git a/qutebrowser/mainwindow/tabbedbrowser.py b/qutebrowser/mainwindow/tabbedbrowser.py index b5c7655a5..d65b3ac20 100644 --- a/qutebrowser/mainwindow/tabbedbrowser.py +++ b/qutebrowser/mainwindow/tabbedbrowser.py @@ -519,7 +519,7 @@ class TabbedBrowser(tabwidget.TabWidget): log.modes.debug("Current tab changed, focusing {!r}".format(tab)) tab.setFocus() for mode in (usertypes.KeyMode.hint, usertypes.KeyMode.insert, - usertypes.KeyMode.caret, usertypes.KeyMode.visual): + usertypes.KeyMode.caret, usertypes.KeyMode.visual): modeman.maybe_leave(self._win_id, mode, 'tab changed') if self._now_focused is not None: objreg.register('last-focused-tab', self._now_focused, update=True, From d496ea2d59a9e17e7f426bb596254be1f8256380 Mon Sep 17 00:00:00 2001 From: Zach-Button Date: Tue, 28 Apr 2015 11:02:45 -0600 Subject: [PATCH 011/182] Update dmenu_qutebrowser --- misc/userscripts/dmenu_qutebrowser | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/misc/userscripts/dmenu_qutebrowser b/misc/userscripts/dmenu_qutebrowser index 3a73cdde5..36651e77a 100755 --- a/misc/userscripts/dmenu_qutebrowser +++ b/misc/userscripts/dmenu_qutebrowser @@ -28,21 +28,21 @@ # Use the hotkey to open in new tab/window, press 'o' to open URL in current tab/window # You can simulate "go" by pressing "o", as the current URL is always first in the list # -# I personally use "q" to launch this script. For me, my workflow is: +# I personally use "o" to launchq this script. For me, my workflow is: # Default keys Keys with this script -# O q +# O o # o o # go o # gO gC, then o +# (This is unnecessarily long. I use this rarely, feel free to make this script accept parameters.) # -url=$(echo "$QUTE_URL" | cat - ~/.local/share/qutebrowser/history ~/.config/qutebrowser/quickmarks | dmenu -l 15 -p qutebrowser) +[ -z "$QUTE_URL" ] && QUTE_URL='http://google.com' + +url=$(echo "$QUTE_URL" | cat - ~/.config/qutebrowser/quickmarks ~/.local/share/qutebrowser/history | dmenu -l 15 -p qutebrowser) url=$(echo $url | sed -E 's/[^ ]+ +//g' | egrep "https?:" || echo $url) [ -z "${url// }" ] && exit -if [ -n "$QUTE_FIFO" ]; then - echo "open $url" >> "$QUTE_FIFO" -else - qutebrowser "$url" -fi +echo "open $url" >> "$QUTE_FIFO" || qutebrowser "$url" + From aeaa20c3b7fbd00d17f2da01a03dbec3e8ad9464 Mon Sep 17 00:00:00 2001 From: Artur Shaik Date: Mon, 4 May 2015 18:00:40 +0600 Subject: [PATCH 012/182] Disable support count for CaretKeyParser Allow using '0' for move caret to beginnig of the line. --- qutebrowser/keyinput/modeparsers.py | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/qutebrowser/keyinput/modeparsers.py b/qutebrowser/keyinput/modeparsers.py index 121172798..068884c0b 100644 --- a/qutebrowser/keyinput/modeparsers.py +++ b/qutebrowser/keyinput/modeparsers.py @@ -228,19 +228,8 @@ class CaretKeyParser(keyparser.CommandKeyParser): super().__init__(win_id, parent, supports_count=True, supports_chains=True) self.read_config('caret') + self._supports_count = False def __repr__(self): return utils.get_repr(self) - -class VisualKeyParser(keyparser.CommandKeyParser): - - """KeyParser for Visual mode.""" - - def __init__(self, win_id, parent=None): - super().__init__(win_id, parent, supports_count=True, - supports_chains=True) - self.read_config('visual') - - def __repr__(self): - return utils.get_repr(self) From d594798db8ecb635ecb7b9ce89d0eb22c27ecc57 Mon Sep 17 00:00:00 2001 From: Artur Shaik Date: Tue, 5 May 2015 10:18:24 +0600 Subject: [PATCH 013/182] Implement caret selection and positioning Added option to webview for selection enabled caret mode. In status bar checking value of this option to identificate about it. Added bindings: for toggle selection mode, drop selection and keep selection mode enabled. In webview added javascript snippet to position caret at top of the viewport after caret enabling. This code mostly was taken from cVim sources. --- qutebrowser/browser/commands.py | 118 +++++++++++++----------- qutebrowser/browser/webview.py | 91 +++++++++++++++++- qutebrowser/config/configdata.py | 19 ++-- qutebrowser/keyinput/basekeyparser.py | 1 + qutebrowser/keyinput/modeman.py | 2 - qutebrowser/mainwindow/statusbar/bar.py | 35 ++++--- qutebrowser/mainwindow/tabbedbrowser.py | 2 +- qutebrowser/utils/usertypes.py | 2 +- 8 files changed, 178 insertions(+), 92 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 0adc9c57a..56d516c7e 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -1153,152 +1153,152 @@ class CommandDispatcher: view.search(view.search_text, flags) @cmdutils.register(instance='command-dispatcher', hide=True, - modes=[KeyMode.caret, KeyMode.visual], scope='window') + modes=[KeyMode.caret], scope='window') def move_to_next_line(self): """Move the cursor or select to the next line.""" - modemanager = modeman.get_modeman(self._win_id) - if modemanager.mode == KeyMode.caret: + webview = self._current_widget() + if not webview.selection_enabled: act = QWebPage.MoveToNextLine else: act = QWebPage.SelectNextLine - self._current_widget().triggerPageAction(act) + webview.triggerPageAction(act) @cmdutils.register(instance='command-dispatcher', hide=True, - modes=[KeyMode.caret, KeyMode.visual], scope='window') + modes=[KeyMode.caret], scope='window') def move_to_prev_line(self): """Move the cursor or select to the prev line.""" - modemanager = modeman.get_modeman(self._win_id) - if modemanager.mode == KeyMode.caret: + webview = self._current_widget() + if not webview.selection_enabled: act = QWebPage.MoveToPreviousLine else: act = QWebPage.SelectPreviousLine - self._current_widget().triggerPageAction(act) + webview.triggerPageAction(act) @cmdutils.register(instance='command-dispatcher', hide=True, - modes=[KeyMode.caret, KeyMode.visual], scope='window') + modes=[KeyMode.caret], scope='window') def move_to_next_char(self): """Move the cursor or select to the next char.""" - modemanager = modeman.get_modeman(self._win_id) - if modemanager.mode == KeyMode.caret: + webview = self._current_widget() + if not webview.selection_enabled: act = QWebPage.MoveToNextChar else: act = QWebPage.SelectNextChar - self._current_widget().triggerPageAction(act) + webview.triggerPageAction(act) @cmdutils.register(instance='command-dispatcher', hide=True, - modes=[KeyMode.caret, KeyMode.visual], scope='window') + modes=[KeyMode.caret], scope='window') def move_to_prev_char(self): """Move the cursor or select to the prev char.""" - modemanager = modeman.get_modeman(self._win_id) - if modemanager.mode == KeyMode.caret: + webview = self._current_widget() + if not webview.selection_enabled: act = QWebPage.MoveToPreviousChar else: act = QWebPage.SelectPreviousChar - self._current_widget().triggerPageAction(act) + webview.triggerPageAction(act) @cmdutils.register(instance='command-dispatcher', hide=True, - modes=[KeyMode.caret, KeyMode.visual], scope='window') + modes=[KeyMode.caret], scope='window') def move_to_end_of_word(self): """Move the cursor or select to the next word.""" - modemanager = modeman.get_modeman(self._win_id) - if modemanager.mode == KeyMode.caret: + webview = self._current_widget() + if not webview.selection_enabled: act = QWebPage.MoveToNextWord else: act = QWebPage.SelectNextWord - self._current_widget().triggerPageAction(act) + webview.triggerPageAction(act) @cmdutils.register(instance='command-dispatcher', hide=True, - modes=[KeyMode.caret, KeyMode.visual], + modes=[KeyMode.caret], scope='window') def move_to_next_word(self): """Move the cursor or select to the next word.""" - modemanager = modeman.get_modeman(self._win_id) - if modemanager.mode == KeyMode.caret: + webview = self._current_widget() + if not webview.selection_enabled: act = [QWebPage.MoveToNextWord, QWebPage.MoveToNextChar] else: act = [QWebPage.SelectNextWord, QWebPage.SelectNextChar] for a in act: - self._current_widget().triggerPageAction(a) + webview.triggerPageAction(a) @cmdutils.register(instance='command-dispatcher', hide=True, - modes=[KeyMode.caret, KeyMode.visual], scope='window') + modes=[KeyMode.caret], scope='window') def move_to_prev_word(self): """Move the cursor or select to the prev word.""" - modemanager = modeman.get_modeman(self._win_id) - if modemanager.mode == KeyMode.caret: + webview = self._current_widget() + if not webview.selection_enabled: act = QWebPage.MoveToPreviousWord else: act = QWebPage.SelectPreviousWord - self._current_widget().triggerPageAction(act) + webview.triggerPageAction(act) @cmdutils.register(instance='command-dispatcher', hide=True, - modes=[KeyMode.caret, KeyMode.visual], scope='window') + modes=[KeyMode.caret], scope='window') def move_to_start_of_line(self): """Move the cursor or select to the start of line.""" - modemanager = modeman.get_modeman(self._win_id) - if modemanager.mode == KeyMode.caret: + webview = self._current_widget() + if not webview.selection_enabled: act = QWebPage.MoveToStartOfLine else: act = QWebPage.SelectStartOfLine - self._current_widget().triggerPageAction(act) + webview.triggerPageAction(act) @cmdutils.register(instance='command-dispatcher', hide=True, - modes=[KeyMode.caret, KeyMode.visual], scope='window') + modes=[KeyMode.caret], scope='window') def move_to_end_of_line(self): """Move the cursor or select to the end of line.""" - modemanager = modeman.get_modeman(self._win_id) - if modemanager.mode == KeyMode.caret: + webview = self._current_widget() + if not webview.selection_enabled: act = QWebPage.MoveToEndOfLine else: act = QWebPage.SelectEndOfLine - self._current_widget().triggerPageAction(act) + webview.triggerPageAction(act) @cmdutils.register(instance='command-dispatcher', hide=True, - modes=[KeyMode.caret, KeyMode.visual], scope='window') + modes=[KeyMode.caret], scope='window') def move_to_start_of_block(self): """Move the cursor or select to the start of block.""" - modemanager = modeman.get_modeman(self._win_id) - if modemanager.mode == KeyMode.caret: + webview = self._current_widget() + if not webview.selection_enabled: act = QWebPage.MoveToStartOfBlock else: act = QWebPage.SelectStartOfBlock - self._current_widget().triggerPageAction(act) + webview.triggerPageAction(act) @cmdutils.register(instance='command-dispatcher', hide=True, - modes=[KeyMode.caret, KeyMode.visual], scope='window') + modes=[KeyMode.caret], scope='window') def move_to_end_of_block(self): """Move the cursor or select to the end of block.""" - modemanager = modeman.get_modeman(self._win_id) - if modemanager.mode == KeyMode.caret: + webview = self._current_widget() + if not webview.selection_enabled: act = QWebPage.MoveToEndOfBlock else: act = QWebPage.SelectEndOfBlock - self._current_widget().triggerPageAction(act) + webview.triggerPageAction(act) @cmdutils.register(instance='command-dispatcher', hide=True, - modes=[KeyMode.caret, KeyMode.visual], scope='window') + modes=[KeyMode.caret], scope='window') def move_to_start_of_document(self): """Move the cursor or select to the start of document.""" - modemanager = modeman.get_modeman(self._win_id) - if modemanager.mode == KeyMode.caret: + webview = self._current_widget() + if not webview.selection_enabled: act = QWebPage.MoveToStartOfDocument else: act = QWebPage.SelectStartOfDocument - self._current_widget().triggerPageAction(act) + webview.triggerPageAction(act) @cmdutils.register(instance='command-dispatcher', hide=True, - modes=[KeyMode.caret, KeyMode.visual], scope='window') + modes=[KeyMode.caret], scope='window') def move_to_end_of_document(self): """Move the cursor or select to the end of document.""" - modemanager = modeman.get_modeman(self._win_id) - if modemanager.mode == KeyMode.caret: + webview = self._current_widget() + if not webview.selection_enabled: act = QWebPage.MoveToEndOfDocument else: act = QWebPage.SelectEndOfDocument - self._current_widget().triggerPageAction(act) + webview.triggerPageAction(act) @cmdutils.register(instance='command-dispatcher', hide=True, - modes=[KeyMode.visual], scope='window') + modes=[KeyMode.caret], scope='window') def yank_selected(self, sel=False): """Yank selected text to the clipboard or primary selection. @@ -1323,9 +1323,17 @@ class CommandDispatcher: len(s), "char" if len(s) == 1 else "chars", target)) @cmdutils.register(instance='command-dispatcher', hide=True, - modes=[KeyMode.visual], scope='window') + modes=[KeyMode.caret], scope='window') + def toggle_selection(self): + """Toggle caret selection mode.""" + self._current_widget().selection_enabled = not self._current_widget().selection_enabled + mainwindow = objreg.get('main-window', scope='window', window=self._win_id) + mainwindow.status.on_mode_entered(usertypes.KeyMode.caret) + + @cmdutils.register(instance='command-dispatcher', hide=True, + modes=[KeyMode.caret], scope='window') def drop_selection(self): - """Drop selection and stay in visual mode.""" + """Drop selection and keep selection mode enabled.""" self._current_widget().triggerPageAction(QWebPage.MoveToNextChar) @cmdutils.register(instance='command-dispatcher', scope='window', diff --git a/qutebrowser/browser/webview.py b/qutebrowser/browser/webview.py index d123d3c21..2afd9dac3 100644 --- a/qutebrowser/browser/webview.py +++ b/qutebrowser/browser/webview.py @@ -106,6 +106,7 @@ class WebView(QWebView): self.keep_icon = False self.search_text = None self.search_flags = 0 + self.selection_enabled = False; self.init_neighborlist() cfg = objreg.get('config') cfg.changed.connect(self.init_neighborlist) @@ -180,6 +181,79 @@ class WebView(QWebView): self.load_status = val self.load_status_changed.emit(val.name) + def _position_caret(self): + """ + JS snippet to position caret at top of the screen. + Was borrowed from cVim source code + """ + self.page().currentFrame().evaluateJavaScript(""" + + function isElementInViewport(node) { + var i; + var boundingRect = node.getClientRects()[0] || node.getBoundingClientRect(); + if (boundingRect.width <= 1 && boundingRect.height <= 1) { + var rects = node.getClientRects(); + for (i = 0; i < rects.length; i++) { + if (rects[i].width > rects[0].height && rects[i].height > rects[0].height) { + boundingRect = rects[i]; + } + } + } + if (boundingRect === void 0) return null; + if (boundingRect.top > innerHeight || boundingRect.left > innerWidth) { + return null; + } + if (boundingRect.width <= 1 || boundingRect.height <= 1) { + var children = node.children; + var visibleChildNode = false; + for (i = 0, l = children.length; i < l; ++i) { + boundingRect = children[i].getClientRects()[0] || children[i].getBoundingClientRect(); + if (boundingRect.width > 1 && boundingRect.height > 1) { + visibleChildNode = true; + break; + } + } + if (visibleChildNode === false) return null; + } + if (boundingRect.top + boundingRect.height < 10 || boundingRect.left + boundingRect.width < -10) { + return null; + } + var computedStyle = window.getComputedStyle(node, null); + if (computedStyle.visibility !== 'visible' || + computedStyle.display === 'none' || + node.hasAttribute('disabled') || + parseInt(computedStyle.width, '10') === 0 || + parseInt(computedStyle.height, '10') === 0) { + return null; + } + return boundingRect.top >= -20; + } + + var walker = document.createTreeWalker(document.body, 4, null, false); + var node; + var textNodes = []; + while (node = walker.nextNode()) { + if (node.nodeType === 3 && node.data.trim() !== '') { + textNodes.push(node); + } + } + for (var i = 0; i < textNodes.length; i++) { + var element = textNodes[i].parentElement; + if (isElementInViewport(element.parentElement)) { + el = element; + break; + } + } + if (el !== undefined) { + var range = document.createRange(); + range.setStart(el, 0); + range.setEnd(el, 0); + var sel = window.getSelection(); + sel.removeAllRanges(); + sel.addRange(range); + } + """) + @pyqtSlot(str, str) def on_config_changed(self, section, option): """Reinitialize the zoom neighborlist if related config changed.""" @@ -435,11 +509,17 @@ class WebView(QWebView): log.webview.debug("Ignoring focus because mode {} was " "entered.".format(mode)) self.setFocusPolicy(Qt.NoFocus) - elif mode in (usertypes.KeyMode.caret, usertypes.KeyMode.visual): + elif mode == usertypes.KeyMode.caret: settings = self.settings() settings.setAttribute(QWebSettings.CaretBrowsingEnabled, True) - self.clearFocus() - self.setFocus(Qt.OtherFocusReason) + self.selection_enabled = False + + tabbed = objreg.get('tabbed-browser', scope='window', window=self.win_id) + if tabbed.currentWidget().tab_id == self.tab_id: + self.clearFocus() + self.setFocus(Qt.OtherFocusReason) + + self._position_caret() @pyqtSlot(usertypes.KeyMode) def on_mode_left(self, mode): @@ -448,13 +528,14 @@ class WebView(QWebView): usertypes.KeyMode.yesno): log.webview.debug("Restoring focus policy because mode {} was " "left.".format(mode)) - elif mode in (usertypes.KeyMode.caret, usertypes.KeyMode.visual): + elif mode == usertypes.KeyMode.caret: settings = self.settings() if settings.testAttribute(QWebSettings.CaretBrowsingEnabled): - if mode == usertypes.KeyMode.visual and self.hasSelection(): + if self.selection_enabled and self.hasSelection(): # Remove selection if exist self.triggerPageAction(QWebPage.MoveToNextChar) settings.setAttribute(QWebSettings.CaretBrowsingEnabled, False) + self.selection_enabled = False; self.setFocusPolicy(Qt.WheelFocus) diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 85e532ad4..219ab0f95 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -820,9 +820,9 @@ def data(readonly=False): SettingValue(typ.QssColor(), 'purple'), "Background color of the statusbar in caret mode."), - ('statusbar.bg.visual', + ('statusbar.bg.caret_selection', SettingValue(typ.QssColor(), '#a12dff'), - "Background color of the statusbar in visual mode."), + "Background color of the statusbar in caret selection enabled mode."), ('statusbar.progress.bg', SettingValue(typ.QssColor(), 'white'), @@ -1259,19 +1259,10 @@ KEY_DATA = collections.OrderedDict([ ('rl-backward-delete-char', ['']), ])), - ('visual', collections.OrderedDict([ - ('yank-selected', ['y']), - ('yank-selected -s', ['Y']), - ('drop-selection', ['v']), - ('enter-mode caret', ['c']), - ])), - ('caret', collections.OrderedDict([ - ('enter-mode visual', ['v']), + ('toggle-selection', ['']), + ('drop-selection', ['']), ('enter-mode normal', ['c']), - ])), - - ('caret,visual', collections.OrderedDict([ ('move-to-next-line', ['j']), ('move-to-prev-line', ['k']), ('move-to-next-char', ['l']), @@ -1283,6 +1274,8 @@ KEY_DATA = collections.OrderedDict([ ('move-to-end-of-line', ['$']), ('move-to-start-of-document', ['gg']), ('move-to-end-of-document', ['G']), + ('yank-selected', ['y']), + ('yank-selected -p', ['Y']), ('scroll -50 0', ['H']), ('scroll 0 50', ['J']), ('scroll 0 -50', ['K']), diff --git a/qutebrowser/keyinput/basekeyparser.py b/qutebrowser/keyinput/basekeyparser.py index c5e70f414..a7408de89 100644 --- a/qutebrowser/keyinput/basekeyparser.py +++ b/qutebrowser/keyinput/basekeyparser.py @@ -180,6 +180,7 @@ class BaseKeyParser(QObject): count, cmd_input = self._split_count() + print(count, cmd_input) if not cmd_input: # Only a count, no command yet, but we handled it return self.Match.other diff --git a/qutebrowser/keyinput/modeman.py b/qutebrowser/keyinput/modeman.py index 665d3c197..3d4760849 100644 --- a/qutebrowser/keyinput/modeman.py +++ b/qutebrowser/keyinput/modeman.py @@ -79,7 +79,6 @@ def init(win_id, parent): warn=False), KM.yesno: modeparsers.PromptKeyParser(win_id, modeman), KM.caret: modeparsers.CaretKeyParser(win_id, modeman), - KM.visual: modeparsers.VisualKeyParser(win_id, modeman), } objreg.register('keyparsers', keyparsers, scope='window', window=win_id) modeman.destroyed.connect( @@ -95,7 +94,6 @@ def init(win_id, parent): modeman.register(KM.prompt, keyparsers[KM.prompt].handle, passthrough=True) modeman.register(KM.yesno, keyparsers[KM.yesno].handle) modeman.register(KM.caret, keyparsers[KM.caret].handle, passthrough=True) - modeman.register(KM.visual, keyparsers[KM.visual].handle, passthrough=True) return modeman diff --git a/qutebrowser/mainwindow/statusbar/bar.py b/qutebrowser/mainwindow/statusbar/bar.py index 0b041c5dc..54c77d3dc 100644 --- a/qutebrowser/mainwindow/statusbar/bar.py +++ b/qutebrowser/mainwindow/statusbar/bar.py @@ -92,7 +92,7 @@ class StatusBar(QWidget): _prompt_active = False _insert_active = False _caret_active = False - _visual_active = False + _caret_selection_active = False STYLESHEET = """ QWidget#StatusBar { @@ -107,8 +107,8 @@ class StatusBar(QWidget): {{ color['statusbar.bg.caret'] }} } - QWidget#StatusBar[visual_active="true"] { - {{ color['statusbar.bg.visual'] }} + QWidget#StatusBar[caret_selection_active="true"] { + {{ color['statusbar.bg.caret_selection'] }} } QWidget#StatusBar[prompt_active="true"] { @@ -269,12 +269,12 @@ class StatusBar(QWidget): return self._caret_active @pyqtProperty(bool) - def visual_active(self): - """Getter for self.visual_active, so it can be used as Qt property.""" - return self._visual_active + def caret_selection_active(self): + """Getter for self.caret_active, so it can be used as Qt property.""" + return self._caret_selection_active def _set_mode_active(self, mode, val): - """Setter for self.{insert,caret,visual}_active. + """Setter for self.{insert,caret}_active. Re-set the stylesheet after setting the value, so everything gets updated by Qt properly. @@ -284,10 +284,17 @@ class StatusBar(QWidget): self._insert_active = val elif mode == usertypes.KeyMode.caret: log.statusbar.debug("Setting caret_active to {}".format(val)) - self._caret_active = val - elif mode == usertypes.KeyMode.visual: - log.statusbar.debug("Setting visual_active to {}".format(val)) - self._visual_active = val + webview = objreg.get("tabbed-browser", scope="window", window=self._win_id).currentWidget() + if val and webview.selection_enabled: + self._set_mode_text("{} selection".format(mode.name)) + self._caret_selection_active = val + self._caret_active = False + else: + if val: + self._set_mode_text(mode.name) + self._caret_active = val + self._caret_selection_active = False + self.setStyleSheet(style.get_stylesheet(self.STYLESHEET)) def _set_mode_text(self, mode): @@ -465,8 +472,7 @@ class StatusBar(QWidget): window=self._win_id) if mode in mode_manager.passthrough: self._set_mode_text(mode.name) - if mode in (usertypes.KeyMode.insert, usertypes.KeyMode.caret, - usertypes.KeyMode.visual): + if mode in (usertypes.KeyMode.insert, usertypes.KeyMode.caret): self._set_mode_active(mode, True) @pyqtSlot(usertypes.KeyMode, usertypes.KeyMode) @@ -479,8 +485,7 @@ class StatusBar(QWidget): self._set_mode_text(new_mode.name) else: self.txt.set_text(self.txt.Text.normal, '') - if old_mode in (usertypes.KeyMode.insert, usertypes.KeyMode.caret, - usertypes.KeyMode.visual): + if old_mode in (usertypes.KeyMode.insert, usertypes.KeyMode.caret): self._set_mode_active(old_mode, False) @config.change_filter('ui', 'message-timeout') diff --git a/qutebrowser/mainwindow/tabbedbrowser.py b/qutebrowser/mainwindow/tabbedbrowser.py index d65b3ac20..df236b077 100644 --- a/qutebrowser/mainwindow/tabbedbrowser.py +++ b/qutebrowser/mainwindow/tabbedbrowser.py @@ -519,7 +519,7 @@ class TabbedBrowser(tabwidget.TabWidget): log.modes.debug("Current tab changed, focusing {!r}".format(tab)) tab.setFocus() for mode in (usertypes.KeyMode.hint, usertypes.KeyMode.insert, - usertypes.KeyMode.caret, usertypes.KeyMode.visual): + usertypes.KeyMode.caret): modeman.maybe_leave(self._win_id, mode, 'tab changed') if self._now_focused is not None: objreg.register('last-focused-tab', self._now_focused, update=True, diff --git a/qutebrowser/utils/usertypes.py b/qutebrowser/utils/usertypes.py index ba5957155..ba568ab56 100644 --- a/qutebrowser/utils/usertypes.py +++ b/qutebrowser/utils/usertypes.py @@ -231,7 +231,7 @@ ClickTarget = enum('ClickTarget', ['normal', 'tab', 'tab_bg', 'window']) # Key input modes KeyMode = enum('KeyMode', ['normal', 'hint', 'command', 'yesno', 'prompt', - 'insert', 'passthrough', 'caret', 'visual']) + 'insert', 'passthrough', 'caret']) # Available command completions From 489c913e586a37a6e982ddf152d6ed7fb6984b8c Mon Sep 17 00:00:00 2001 From: Artur Shaik Date: Tue, 5 May 2015 10:18:24 +0600 Subject: [PATCH 014/182] Implement caret selection and positioning Added option to webview for selection enabled caret mode. In status bar checking value of this option to identificate about it. Added bindings: for toggle selection mode, drop selection and keep selection mode enabled. In webview added javascript snippet to position caret at top of the viewport after caret enabling. This code mostly was taken from cVim sources. --- qutebrowser/browser/commands.py | 118 +++++++++++++----------- qutebrowser/browser/webview.py | 91 +++++++++++++++++- qutebrowser/config/configdata.py | 19 ++-- qutebrowser/keyinput/modeman.py | 2 - qutebrowser/mainwindow/statusbar/bar.py | 35 ++++--- qutebrowser/mainwindow/tabbedbrowser.py | 2 +- qutebrowser/utils/usertypes.py | 2 +- 7 files changed, 177 insertions(+), 92 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 0adc9c57a..56d516c7e 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -1153,152 +1153,152 @@ class CommandDispatcher: view.search(view.search_text, flags) @cmdutils.register(instance='command-dispatcher', hide=True, - modes=[KeyMode.caret, KeyMode.visual], scope='window') + modes=[KeyMode.caret], scope='window') def move_to_next_line(self): """Move the cursor or select to the next line.""" - modemanager = modeman.get_modeman(self._win_id) - if modemanager.mode == KeyMode.caret: + webview = self._current_widget() + if not webview.selection_enabled: act = QWebPage.MoveToNextLine else: act = QWebPage.SelectNextLine - self._current_widget().triggerPageAction(act) + webview.triggerPageAction(act) @cmdutils.register(instance='command-dispatcher', hide=True, - modes=[KeyMode.caret, KeyMode.visual], scope='window') + modes=[KeyMode.caret], scope='window') def move_to_prev_line(self): """Move the cursor or select to the prev line.""" - modemanager = modeman.get_modeman(self._win_id) - if modemanager.mode == KeyMode.caret: + webview = self._current_widget() + if not webview.selection_enabled: act = QWebPage.MoveToPreviousLine else: act = QWebPage.SelectPreviousLine - self._current_widget().triggerPageAction(act) + webview.triggerPageAction(act) @cmdutils.register(instance='command-dispatcher', hide=True, - modes=[KeyMode.caret, KeyMode.visual], scope='window') + modes=[KeyMode.caret], scope='window') def move_to_next_char(self): """Move the cursor or select to the next char.""" - modemanager = modeman.get_modeman(self._win_id) - if modemanager.mode == KeyMode.caret: + webview = self._current_widget() + if not webview.selection_enabled: act = QWebPage.MoveToNextChar else: act = QWebPage.SelectNextChar - self._current_widget().triggerPageAction(act) + webview.triggerPageAction(act) @cmdutils.register(instance='command-dispatcher', hide=True, - modes=[KeyMode.caret, KeyMode.visual], scope='window') + modes=[KeyMode.caret], scope='window') def move_to_prev_char(self): """Move the cursor or select to the prev char.""" - modemanager = modeman.get_modeman(self._win_id) - if modemanager.mode == KeyMode.caret: + webview = self._current_widget() + if not webview.selection_enabled: act = QWebPage.MoveToPreviousChar else: act = QWebPage.SelectPreviousChar - self._current_widget().triggerPageAction(act) + webview.triggerPageAction(act) @cmdutils.register(instance='command-dispatcher', hide=True, - modes=[KeyMode.caret, KeyMode.visual], scope='window') + modes=[KeyMode.caret], scope='window') def move_to_end_of_word(self): """Move the cursor or select to the next word.""" - modemanager = modeman.get_modeman(self._win_id) - if modemanager.mode == KeyMode.caret: + webview = self._current_widget() + if not webview.selection_enabled: act = QWebPage.MoveToNextWord else: act = QWebPage.SelectNextWord - self._current_widget().triggerPageAction(act) + webview.triggerPageAction(act) @cmdutils.register(instance='command-dispatcher', hide=True, - modes=[KeyMode.caret, KeyMode.visual], + modes=[KeyMode.caret], scope='window') def move_to_next_word(self): """Move the cursor or select to the next word.""" - modemanager = modeman.get_modeman(self._win_id) - if modemanager.mode == KeyMode.caret: + webview = self._current_widget() + if not webview.selection_enabled: act = [QWebPage.MoveToNextWord, QWebPage.MoveToNextChar] else: act = [QWebPage.SelectNextWord, QWebPage.SelectNextChar] for a in act: - self._current_widget().triggerPageAction(a) + webview.triggerPageAction(a) @cmdutils.register(instance='command-dispatcher', hide=True, - modes=[KeyMode.caret, KeyMode.visual], scope='window') + modes=[KeyMode.caret], scope='window') def move_to_prev_word(self): """Move the cursor or select to the prev word.""" - modemanager = modeman.get_modeman(self._win_id) - if modemanager.mode == KeyMode.caret: + webview = self._current_widget() + if not webview.selection_enabled: act = QWebPage.MoveToPreviousWord else: act = QWebPage.SelectPreviousWord - self._current_widget().triggerPageAction(act) + webview.triggerPageAction(act) @cmdutils.register(instance='command-dispatcher', hide=True, - modes=[KeyMode.caret, KeyMode.visual], scope='window') + modes=[KeyMode.caret], scope='window') def move_to_start_of_line(self): """Move the cursor or select to the start of line.""" - modemanager = modeman.get_modeman(self._win_id) - if modemanager.mode == KeyMode.caret: + webview = self._current_widget() + if not webview.selection_enabled: act = QWebPage.MoveToStartOfLine else: act = QWebPage.SelectStartOfLine - self._current_widget().triggerPageAction(act) + webview.triggerPageAction(act) @cmdutils.register(instance='command-dispatcher', hide=True, - modes=[KeyMode.caret, KeyMode.visual], scope='window') + modes=[KeyMode.caret], scope='window') def move_to_end_of_line(self): """Move the cursor or select to the end of line.""" - modemanager = modeman.get_modeman(self._win_id) - if modemanager.mode == KeyMode.caret: + webview = self._current_widget() + if not webview.selection_enabled: act = QWebPage.MoveToEndOfLine else: act = QWebPage.SelectEndOfLine - self._current_widget().triggerPageAction(act) + webview.triggerPageAction(act) @cmdutils.register(instance='command-dispatcher', hide=True, - modes=[KeyMode.caret, KeyMode.visual], scope='window') + modes=[KeyMode.caret], scope='window') def move_to_start_of_block(self): """Move the cursor or select to the start of block.""" - modemanager = modeman.get_modeman(self._win_id) - if modemanager.mode == KeyMode.caret: + webview = self._current_widget() + if not webview.selection_enabled: act = QWebPage.MoveToStartOfBlock else: act = QWebPage.SelectStartOfBlock - self._current_widget().triggerPageAction(act) + webview.triggerPageAction(act) @cmdutils.register(instance='command-dispatcher', hide=True, - modes=[KeyMode.caret, KeyMode.visual], scope='window') + modes=[KeyMode.caret], scope='window') def move_to_end_of_block(self): """Move the cursor or select to the end of block.""" - modemanager = modeman.get_modeman(self._win_id) - if modemanager.mode == KeyMode.caret: + webview = self._current_widget() + if not webview.selection_enabled: act = QWebPage.MoveToEndOfBlock else: act = QWebPage.SelectEndOfBlock - self._current_widget().triggerPageAction(act) + webview.triggerPageAction(act) @cmdutils.register(instance='command-dispatcher', hide=True, - modes=[KeyMode.caret, KeyMode.visual], scope='window') + modes=[KeyMode.caret], scope='window') def move_to_start_of_document(self): """Move the cursor or select to the start of document.""" - modemanager = modeman.get_modeman(self._win_id) - if modemanager.mode == KeyMode.caret: + webview = self._current_widget() + if not webview.selection_enabled: act = QWebPage.MoveToStartOfDocument else: act = QWebPage.SelectStartOfDocument - self._current_widget().triggerPageAction(act) + webview.triggerPageAction(act) @cmdutils.register(instance='command-dispatcher', hide=True, - modes=[KeyMode.caret, KeyMode.visual], scope='window') + modes=[KeyMode.caret], scope='window') def move_to_end_of_document(self): """Move the cursor or select to the end of document.""" - modemanager = modeman.get_modeman(self._win_id) - if modemanager.mode == KeyMode.caret: + webview = self._current_widget() + if not webview.selection_enabled: act = QWebPage.MoveToEndOfDocument else: act = QWebPage.SelectEndOfDocument - self._current_widget().triggerPageAction(act) + webview.triggerPageAction(act) @cmdutils.register(instance='command-dispatcher', hide=True, - modes=[KeyMode.visual], scope='window') + modes=[KeyMode.caret], scope='window') def yank_selected(self, sel=False): """Yank selected text to the clipboard or primary selection. @@ -1323,9 +1323,17 @@ class CommandDispatcher: len(s), "char" if len(s) == 1 else "chars", target)) @cmdutils.register(instance='command-dispatcher', hide=True, - modes=[KeyMode.visual], scope='window') + modes=[KeyMode.caret], scope='window') + def toggle_selection(self): + """Toggle caret selection mode.""" + self._current_widget().selection_enabled = not self._current_widget().selection_enabled + mainwindow = objreg.get('main-window', scope='window', window=self._win_id) + mainwindow.status.on_mode_entered(usertypes.KeyMode.caret) + + @cmdutils.register(instance='command-dispatcher', hide=True, + modes=[KeyMode.caret], scope='window') def drop_selection(self): - """Drop selection and stay in visual mode.""" + """Drop selection and keep selection mode enabled.""" self._current_widget().triggerPageAction(QWebPage.MoveToNextChar) @cmdutils.register(instance='command-dispatcher', scope='window', diff --git a/qutebrowser/browser/webview.py b/qutebrowser/browser/webview.py index d123d3c21..2afd9dac3 100644 --- a/qutebrowser/browser/webview.py +++ b/qutebrowser/browser/webview.py @@ -106,6 +106,7 @@ class WebView(QWebView): self.keep_icon = False self.search_text = None self.search_flags = 0 + self.selection_enabled = False; self.init_neighborlist() cfg = objreg.get('config') cfg.changed.connect(self.init_neighborlist) @@ -180,6 +181,79 @@ class WebView(QWebView): self.load_status = val self.load_status_changed.emit(val.name) + def _position_caret(self): + """ + JS snippet to position caret at top of the screen. + Was borrowed from cVim source code + """ + self.page().currentFrame().evaluateJavaScript(""" + + function isElementInViewport(node) { + var i; + var boundingRect = node.getClientRects()[0] || node.getBoundingClientRect(); + if (boundingRect.width <= 1 && boundingRect.height <= 1) { + var rects = node.getClientRects(); + for (i = 0; i < rects.length; i++) { + if (rects[i].width > rects[0].height && rects[i].height > rects[0].height) { + boundingRect = rects[i]; + } + } + } + if (boundingRect === void 0) return null; + if (boundingRect.top > innerHeight || boundingRect.left > innerWidth) { + return null; + } + if (boundingRect.width <= 1 || boundingRect.height <= 1) { + var children = node.children; + var visibleChildNode = false; + for (i = 0, l = children.length; i < l; ++i) { + boundingRect = children[i].getClientRects()[0] || children[i].getBoundingClientRect(); + if (boundingRect.width > 1 && boundingRect.height > 1) { + visibleChildNode = true; + break; + } + } + if (visibleChildNode === false) return null; + } + if (boundingRect.top + boundingRect.height < 10 || boundingRect.left + boundingRect.width < -10) { + return null; + } + var computedStyle = window.getComputedStyle(node, null); + if (computedStyle.visibility !== 'visible' || + computedStyle.display === 'none' || + node.hasAttribute('disabled') || + parseInt(computedStyle.width, '10') === 0 || + parseInt(computedStyle.height, '10') === 0) { + return null; + } + return boundingRect.top >= -20; + } + + var walker = document.createTreeWalker(document.body, 4, null, false); + var node; + var textNodes = []; + while (node = walker.nextNode()) { + if (node.nodeType === 3 && node.data.trim() !== '') { + textNodes.push(node); + } + } + for (var i = 0; i < textNodes.length; i++) { + var element = textNodes[i].parentElement; + if (isElementInViewport(element.parentElement)) { + el = element; + break; + } + } + if (el !== undefined) { + var range = document.createRange(); + range.setStart(el, 0); + range.setEnd(el, 0); + var sel = window.getSelection(); + sel.removeAllRanges(); + sel.addRange(range); + } + """) + @pyqtSlot(str, str) def on_config_changed(self, section, option): """Reinitialize the zoom neighborlist if related config changed.""" @@ -435,11 +509,17 @@ class WebView(QWebView): log.webview.debug("Ignoring focus because mode {} was " "entered.".format(mode)) self.setFocusPolicy(Qt.NoFocus) - elif mode in (usertypes.KeyMode.caret, usertypes.KeyMode.visual): + elif mode == usertypes.KeyMode.caret: settings = self.settings() settings.setAttribute(QWebSettings.CaretBrowsingEnabled, True) - self.clearFocus() - self.setFocus(Qt.OtherFocusReason) + self.selection_enabled = False + + tabbed = objreg.get('tabbed-browser', scope='window', window=self.win_id) + if tabbed.currentWidget().tab_id == self.tab_id: + self.clearFocus() + self.setFocus(Qt.OtherFocusReason) + + self._position_caret() @pyqtSlot(usertypes.KeyMode) def on_mode_left(self, mode): @@ -448,13 +528,14 @@ class WebView(QWebView): usertypes.KeyMode.yesno): log.webview.debug("Restoring focus policy because mode {} was " "left.".format(mode)) - elif mode in (usertypes.KeyMode.caret, usertypes.KeyMode.visual): + elif mode == usertypes.KeyMode.caret: settings = self.settings() if settings.testAttribute(QWebSettings.CaretBrowsingEnabled): - if mode == usertypes.KeyMode.visual and self.hasSelection(): + if self.selection_enabled and self.hasSelection(): # Remove selection if exist self.triggerPageAction(QWebPage.MoveToNextChar) settings.setAttribute(QWebSettings.CaretBrowsingEnabled, False) + self.selection_enabled = False; self.setFocusPolicy(Qt.WheelFocus) diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 85e532ad4..219ab0f95 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -820,9 +820,9 @@ def data(readonly=False): SettingValue(typ.QssColor(), 'purple'), "Background color of the statusbar in caret mode."), - ('statusbar.bg.visual', + ('statusbar.bg.caret_selection', SettingValue(typ.QssColor(), '#a12dff'), - "Background color of the statusbar in visual mode."), + "Background color of the statusbar in caret selection enabled mode."), ('statusbar.progress.bg', SettingValue(typ.QssColor(), 'white'), @@ -1259,19 +1259,10 @@ KEY_DATA = collections.OrderedDict([ ('rl-backward-delete-char', ['']), ])), - ('visual', collections.OrderedDict([ - ('yank-selected', ['y']), - ('yank-selected -s', ['Y']), - ('drop-selection', ['v']), - ('enter-mode caret', ['c']), - ])), - ('caret', collections.OrderedDict([ - ('enter-mode visual', ['v']), + ('toggle-selection', ['']), + ('drop-selection', ['']), ('enter-mode normal', ['c']), - ])), - - ('caret,visual', collections.OrderedDict([ ('move-to-next-line', ['j']), ('move-to-prev-line', ['k']), ('move-to-next-char', ['l']), @@ -1283,6 +1274,8 @@ KEY_DATA = collections.OrderedDict([ ('move-to-end-of-line', ['$']), ('move-to-start-of-document', ['gg']), ('move-to-end-of-document', ['G']), + ('yank-selected', ['y']), + ('yank-selected -p', ['Y']), ('scroll -50 0', ['H']), ('scroll 0 50', ['J']), ('scroll 0 -50', ['K']), diff --git a/qutebrowser/keyinput/modeman.py b/qutebrowser/keyinput/modeman.py index 665d3c197..3d4760849 100644 --- a/qutebrowser/keyinput/modeman.py +++ b/qutebrowser/keyinput/modeman.py @@ -79,7 +79,6 @@ def init(win_id, parent): warn=False), KM.yesno: modeparsers.PromptKeyParser(win_id, modeman), KM.caret: modeparsers.CaretKeyParser(win_id, modeman), - KM.visual: modeparsers.VisualKeyParser(win_id, modeman), } objreg.register('keyparsers', keyparsers, scope='window', window=win_id) modeman.destroyed.connect( @@ -95,7 +94,6 @@ def init(win_id, parent): modeman.register(KM.prompt, keyparsers[KM.prompt].handle, passthrough=True) modeman.register(KM.yesno, keyparsers[KM.yesno].handle) modeman.register(KM.caret, keyparsers[KM.caret].handle, passthrough=True) - modeman.register(KM.visual, keyparsers[KM.visual].handle, passthrough=True) return modeman diff --git a/qutebrowser/mainwindow/statusbar/bar.py b/qutebrowser/mainwindow/statusbar/bar.py index 0b041c5dc..54c77d3dc 100644 --- a/qutebrowser/mainwindow/statusbar/bar.py +++ b/qutebrowser/mainwindow/statusbar/bar.py @@ -92,7 +92,7 @@ class StatusBar(QWidget): _prompt_active = False _insert_active = False _caret_active = False - _visual_active = False + _caret_selection_active = False STYLESHEET = """ QWidget#StatusBar { @@ -107,8 +107,8 @@ class StatusBar(QWidget): {{ color['statusbar.bg.caret'] }} } - QWidget#StatusBar[visual_active="true"] { - {{ color['statusbar.bg.visual'] }} + QWidget#StatusBar[caret_selection_active="true"] { + {{ color['statusbar.bg.caret_selection'] }} } QWidget#StatusBar[prompt_active="true"] { @@ -269,12 +269,12 @@ class StatusBar(QWidget): return self._caret_active @pyqtProperty(bool) - def visual_active(self): - """Getter for self.visual_active, so it can be used as Qt property.""" - return self._visual_active + def caret_selection_active(self): + """Getter for self.caret_active, so it can be used as Qt property.""" + return self._caret_selection_active def _set_mode_active(self, mode, val): - """Setter for self.{insert,caret,visual}_active. + """Setter for self.{insert,caret}_active. Re-set the stylesheet after setting the value, so everything gets updated by Qt properly. @@ -284,10 +284,17 @@ class StatusBar(QWidget): self._insert_active = val elif mode == usertypes.KeyMode.caret: log.statusbar.debug("Setting caret_active to {}".format(val)) - self._caret_active = val - elif mode == usertypes.KeyMode.visual: - log.statusbar.debug("Setting visual_active to {}".format(val)) - self._visual_active = val + webview = objreg.get("tabbed-browser", scope="window", window=self._win_id).currentWidget() + if val and webview.selection_enabled: + self._set_mode_text("{} selection".format(mode.name)) + self._caret_selection_active = val + self._caret_active = False + else: + if val: + self._set_mode_text(mode.name) + self._caret_active = val + self._caret_selection_active = False + self.setStyleSheet(style.get_stylesheet(self.STYLESHEET)) def _set_mode_text(self, mode): @@ -465,8 +472,7 @@ class StatusBar(QWidget): window=self._win_id) if mode in mode_manager.passthrough: self._set_mode_text(mode.name) - if mode in (usertypes.KeyMode.insert, usertypes.KeyMode.caret, - usertypes.KeyMode.visual): + if mode in (usertypes.KeyMode.insert, usertypes.KeyMode.caret): self._set_mode_active(mode, True) @pyqtSlot(usertypes.KeyMode, usertypes.KeyMode) @@ -479,8 +485,7 @@ class StatusBar(QWidget): self._set_mode_text(new_mode.name) else: self.txt.set_text(self.txt.Text.normal, '') - if old_mode in (usertypes.KeyMode.insert, usertypes.KeyMode.caret, - usertypes.KeyMode.visual): + if old_mode in (usertypes.KeyMode.insert, usertypes.KeyMode.caret): self._set_mode_active(old_mode, False) @config.change_filter('ui', 'message-timeout') diff --git a/qutebrowser/mainwindow/tabbedbrowser.py b/qutebrowser/mainwindow/tabbedbrowser.py index d65b3ac20..df236b077 100644 --- a/qutebrowser/mainwindow/tabbedbrowser.py +++ b/qutebrowser/mainwindow/tabbedbrowser.py @@ -519,7 +519,7 @@ class TabbedBrowser(tabwidget.TabWidget): log.modes.debug("Current tab changed, focusing {!r}".format(tab)) tab.setFocus() for mode in (usertypes.KeyMode.hint, usertypes.KeyMode.insert, - usertypes.KeyMode.caret, usertypes.KeyMode.visual): + usertypes.KeyMode.caret): modeman.maybe_leave(self._win_id, mode, 'tab changed') if self._now_focused is not None: objreg.register('last-focused-tab', self._now_focused, update=True, diff --git a/qutebrowser/utils/usertypes.py b/qutebrowser/utils/usertypes.py index ba5957155..ba568ab56 100644 --- a/qutebrowser/utils/usertypes.py +++ b/qutebrowser/utils/usertypes.py @@ -231,7 +231,7 @@ ClickTarget = enum('ClickTarget', ['normal', 'tab', 'tab_bg', 'window']) # Key input modes KeyMode = enum('KeyMode', ['normal', 'hint', 'command', 'yesno', 'prompt', - 'insert', 'passthrough', 'caret', 'visual']) + 'insert', 'passthrough', 'caret']) # Available command completions From 178d0dfa5882c5c414dbcb18e89c036765720a73 Mon Sep 17 00:00:00 2001 From: Artur Shaik Date: Thu, 7 May 2015 11:51:10 +0600 Subject: [PATCH 015/182] Add count for actions. Zero key treat as command. --- qutebrowser/browser/commands.py | 80 +++++++++++++++------------ qutebrowser/keyinput/basekeyparser.py | 4 +- qutebrowser/keyinput/modeparsers.py | 1 - 3 files changed, 48 insertions(+), 37 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 56d516c7e..e395e0306 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -1153,127 +1153,137 @@ class CommandDispatcher: view.search(view.search_text, flags) @cmdutils.register(instance='command-dispatcher', hide=True, - modes=[KeyMode.caret], scope='window') - def move_to_next_line(self): + modes=[KeyMode.caret], scope='window', count='count') + def move_to_next_line(self, count=1): """Move the cursor or select to the next line.""" webview = self._current_widget() if not webview.selection_enabled: act = QWebPage.MoveToNextLine else: act = QWebPage.SelectNextLine - webview.triggerPageAction(act) + for _ in range(count): + webview.triggerPageAction(act) @cmdutils.register(instance='command-dispatcher', hide=True, - modes=[KeyMode.caret], scope='window') - def move_to_prev_line(self): + modes=[KeyMode.caret], scope='window', count='count') + def move_to_prev_line(self, count=1): """Move the cursor or select to the prev line.""" webview = self._current_widget() if not webview.selection_enabled: act = QWebPage.MoveToPreviousLine else: act = QWebPage.SelectPreviousLine - webview.triggerPageAction(act) + for _ in range(count): + webview.triggerPageAction(act) @cmdutils.register(instance='command-dispatcher', hide=True, - modes=[KeyMode.caret], scope='window') - def move_to_next_char(self): + modes=[KeyMode.caret], scope='window', count='count') + def move_to_next_char(self, count=1): """Move the cursor or select to the next char.""" webview = self._current_widget() if not webview.selection_enabled: act = QWebPage.MoveToNextChar else: act = QWebPage.SelectNextChar - webview.triggerPageAction(act) + for _ in range(count): + webview.triggerPageAction(act) @cmdutils.register(instance='command-dispatcher', hide=True, - modes=[KeyMode.caret], scope='window') - def move_to_prev_char(self): + modes=[KeyMode.caret], scope='window', count='count') + def move_to_prev_char(self, count=1): """Move the cursor or select to the prev char.""" webview = self._current_widget() if not webview.selection_enabled: act = QWebPage.MoveToPreviousChar else: act = QWebPage.SelectPreviousChar - webview.triggerPageAction(act) + for _ in range(count): + webview.triggerPageAction(act) @cmdutils.register(instance='command-dispatcher', hide=True, - modes=[KeyMode.caret], scope='window') - def move_to_end_of_word(self): + modes=[KeyMode.caret], scope='window', count='count') + def move_to_end_of_word(self, count=1): """Move the cursor or select to the next word.""" webview = self._current_widget() if not webview.selection_enabled: act = QWebPage.MoveToNextWord else: act = QWebPage.SelectNextWord - webview.triggerPageAction(act) + for _ in range(count): + webview.triggerPageAction(act) @cmdutils.register(instance='command-dispatcher', hide=True, - modes=[KeyMode.caret], - scope='window') - def move_to_next_word(self): + modes=[KeyMode.caret], scope='window', count='count') + def move_to_next_word(self, count=1): """Move the cursor or select to the next word.""" webview = self._current_widget() if not webview.selection_enabled: act = [QWebPage.MoveToNextWord, QWebPage.MoveToNextChar] else: act = [QWebPage.SelectNextWord, QWebPage.SelectNextChar] - for a in act: - webview.triggerPageAction(a) + for _ in range(count): + for a in act: + webview.triggerPageAction(a) @cmdutils.register(instance='command-dispatcher', hide=True, - modes=[KeyMode.caret], scope='window') - def move_to_prev_word(self): + modes=[KeyMode.caret], scope='window', count='count') + def move_to_prev_word(self, count=1): """Move the cursor or select to the prev word.""" webview = self._current_widget() if not webview.selection_enabled: act = QWebPage.MoveToPreviousWord else: act = QWebPage.SelectPreviousWord - webview.triggerPageAction(act) + for _ in range(count): + webview.triggerPageAction(act) @cmdutils.register(instance='command-dispatcher', hide=True, - modes=[KeyMode.caret], scope='window') - def move_to_start_of_line(self): + modes=[KeyMode.caret], scope='window', count='count') + def move_to_start_of_line(self, count=1): """Move the cursor or select to the start of line.""" webview = self._current_widget() if not webview.selection_enabled: act = QWebPage.MoveToStartOfLine else: act = QWebPage.SelectStartOfLine - webview.triggerPageAction(act) + for _ in range(count): + webview.triggerPageAction(act) @cmdutils.register(instance='command-dispatcher', hide=True, - modes=[KeyMode.caret], scope='window') - def move_to_end_of_line(self): + modes=[KeyMode.caret], scope='window', count='count') + def move_to_end_of_line(self, count=1): """Move the cursor or select to the end of line.""" webview = self._current_widget() if not webview.selection_enabled: act = QWebPage.MoveToEndOfLine else: act = QWebPage.SelectEndOfLine - webview.triggerPageAction(act) + for _ in range(count): + webview.triggerPageAction(act) @cmdutils.register(instance='command-dispatcher', hide=True, - modes=[KeyMode.caret], scope='window') - def move_to_start_of_block(self): + modes=[KeyMode.caret], scope='window', count='count') + def move_to_start_of_block(self, count=1): """Move the cursor or select to the start of block.""" webview = self._current_widget() if not webview.selection_enabled: act = QWebPage.MoveToStartOfBlock else: act = QWebPage.SelectStartOfBlock - webview.triggerPageAction(act) + for _ in range(count): + webview.triggerPageAction(act) @cmdutils.register(instance='command-dispatcher', hide=True, - modes=[KeyMode.caret], scope='window') - def move_to_end_of_block(self): + modes=[KeyMode.caret], scope='window', count='count') + def move_to_end_of_block(self, count=1): """Move the cursor or select to the end of block.""" webview = self._current_widget() if not webview.selection_enabled: act = QWebPage.MoveToEndOfBlock else: act = QWebPage.SelectEndOfBlock - webview.triggerPageAction(act) + for _ in range(count): + webview.triggerPageAction(act) @cmdutils.register(instance='command-dispatcher', hide=True, modes=[KeyMode.caret], scope='window') diff --git a/qutebrowser/keyinput/basekeyparser.py b/qutebrowser/keyinput/basekeyparser.py index a7408de89..b52a39824 100644 --- a/qutebrowser/keyinput/basekeyparser.py +++ b/qutebrowser/keyinput/basekeyparser.py @@ -137,6 +137,9 @@ class BaseKeyParser(QObject): (countstr, cmd_input) = re.match(r'^(\d*)(.*)', self._keystring).groups() count = int(countstr) if countstr else None + if count == 0 and not cmd_input: + cmd_input = self._keystring + count = None else: cmd_input = self._keystring count = None @@ -180,7 +183,6 @@ class BaseKeyParser(QObject): count, cmd_input = self._split_count() - print(count, cmd_input) if not cmd_input: # Only a count, no command yet, but we handled it return self.Match.other diff --git a/qutebrowser/keyinput/modeparsers.py b/qutebrowser/keyinput/modeparsers.py index 068884c0b..8ccdf6ad3 100644 --- a/qutebrowser/keyinput/modeparsers.py +++ b/qutebrowser/keyinput/modeparsers.py @@ -228,7 +228,6 @@ class CaretKeyParser(keyparser.CommandKeyParser): super().__init__(win_id, parent, supports_count=True, supports_chains=True) self.read_config('caret') - self._supports_count = False def __repr__(self): return utils.get_repr(self) From d936be450be524af677bf8fa4867c184460fffd7 Mon Sep 17 00:00:00 2001 From: Artur Shaik Date: Thu, 7 May 2015 12:19:35 +0600 Subject: [PATCH 016/182] Add jumps through text blocks in caret mode. --- qutebrowser/browser/commands.py | 48 +++++++++++++++++++++++++------- qutebrowser/config/configdata.py | 4 +++ 2 files changed, 42 insertions(+), 10 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index e395e0306..65bac7901 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -1263,27 +1263,55 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', hide=True, modes=[KeyMode.caret], scope='window', count='count') - def move_to_start_of_block(self, count=1): - """Move the cursor or select to the start of block.""" + def move_to_start_of_next_block(self, count=1): + """Move the cursor or select to the start of next block.""" webview = self._current_widget() if not webview.selection_enabled: - act = QWebPage.MoveToStartOfBlock + act = [QWebPage.MoveToEndOfBlock, QWebPage.MoveToNextLine, QWebPage.MoveToStartOfBlock] else: - act = QWebPage.SelectStartOfBlock + act = [QWebPage.SelectEndOfBlock, QWebPage.SelectNextLine, QWebPage.SelectStartOfBlock] for _ in range(count): - webview.triggerPageAction(act) + for a in act: + webview.triggerPageAction(a) @cmdutils.register(instance='command-dispatcher', hide=True, modes=[KeyMode.caret], scope='window', count='count') - def move_to_end_of_block(self, count=1): - """Move the cursor or select to the end of block.""" + def move_to_start_of_prev_block(self, count=1): + """Move the cursor or select to the start of previous block.""" webview = self._current_widget() if not webview.selection_enabled: - act = QWebPage.MoveToEndOfBlock + act = [QWebPage.MoveToStartOfBlock, QWebPage.MoveToPreviousLine, QWebPage.MoveToStartOfBlock] else: - act = QWebPage.SelectEndOfBlock + act = [QWebPage.SelectStartOfBlock, QWebPage.SelectPreviousLine, QWebPage.SelectStartOfBlock] for _ in range(count): - webview.triggerPageAction(act) + for a in act: + webview.triggerPageAction(a) + + @cmdutils.register(instance='command-dispatcher', hide=True, + modes=[KeyMode.caret], scope='window', count='count') + def move_to_end_of_next_block(self, count=1): + """Move the cursor or select to the end of next block.""" + webview = self._current_widget() + if not webview.selection_enabled: + act = [QWebPage.MoveToEndOfBlock, QWebPage.MoveToNextLine, QWebPage.MoveToEndOfBlock] + else: + act = [QWebPage.SelectEndOfBlock, QWebPage.SelectNextLine, QWebPage.SelectEndOfBlock] + for _ in range(count): + for a in act: + webview.triggerPageAction(a) + + @cmdutils.register(instance='command-dispatcher', hide=True, + modes=[KeyMode.caret], scope='window', count='count') + def move_to_end_of_prev_block(self, count=1): + """Move the cursor or select to the end of previous block.""" + webview = self._current_widget() + if not webview.selection_enabled: + act = [QWebPage.MoveToStartOfBlock, QWebPage.MoveToPreviousLine, QWebPage.MoveToEndOfBlock] + else: + act = [QWebPage.SelectStartOfBlock, QWebPage.SelectPreviousLine, QWebPage.SelectEndOfBlock] + for _ in range(count): + for a in act: + webview.triggerPageAction(a) @cmdutils.register(instance='command-dispatcher', hide=True, modes=[KeyMode.caret], scope='window') diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 219ab0f95..e7da24caf 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -1270,6 +1270,10 @@ KEY_DATA = collections.OrderedDict([ ('move-to-end-of-word', ['e']), ('move-to-next-word', ['w']), ('move-to-prev-word', ['b']), + ('move-to-start-of-next-block', [']']), + ('move-to-start-of-prev-block', ['[']), + ('move-to-end-of-next-block', ['}']), + ('move-to-end-of-prev-block', ['{']), ('move-to-start-of-line', ['0']), ('move-to-end-of-line', ['$']), ('move-to-start-of-document', ['gg']), From 778ad5df3a8fa9b9f7f5eb28ad89e7c5f8d6aa6c Mon Sep 17 00:00:00 2001 From: Artur Shaik Date: Thu, 7 May 2015 12:23:09 +0600 Subject: [PATCH 017/182] Comment clean. --- qutebrowser/mainwindow/statusbar/bar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/mainwindow/statusbar/bar.py b/qutebrowser/mainwindow/statusbar/bar.py index 54c77d3dc..e8311a1e7 100644 --- a/qutebrowser/mainwindow/statusbar/bar.py +++ b/qutebrowser/mainwindow/statusbar/bar.py @@ -270,7 +270,7 @@ class StatusBar(QWidget): @pyqtProperty(bool) def caret_selection_active(self): - """Getter for self.caret_active, so it can be used as Qt property.""" + """Getter for self.caret_selection_active, so it can be used as Qt property.""" return self._caret_selection_active def _set_mode_active(self, mode, val): From 57cad14714c57553b7d60d8b02ae422a733052ad Mon Sep 17 00:00:00 2001 From: Artur Shaik Date: Thu, 7 May 2015 12:40:51 +0600 Subject: [PATCH 018/182] Move JS snippet in external js file. --- qutebrowser/browser/webview.py | 76 +------------------- qutebrowser/javascript/position_caret.js | 89 ++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 74 deletions(-) create mode 100644 qutebrowser/javascript/position_caret.js diff --git a/qutebrowser/browser/webview.py b/qutebrowser/browser/webview.py index 2afd9dac3..5c592736e 100644 --- a/qutebrowser/browser/webview.py +++ b/qutebrowser/browser/webview.py @@ -181,79 +181,6 @@ class WebView(QWebView): self.load_status = val self.load_status_changed.emit(val.name) - def _position_caret(self): - """ - JS snippet to position caret at top of the screen. - Was borrowed from cVim source code - """ - self.page().currentFrame().evaluateJavaScript(""" - - function isElementInViewport(node) { - var i; - var boundingRect = node.getClientRects()[0] || node.getBoundingClientRect(); - if (boundingRect.width <= 1 && boundingRect.height <= 1) { - var rects = node.getClientRects(); - for (i = 0; i < rects.length; i++) { - if (rects[i].width > rects[0].height && rects[i].height > rects[0].height) { - boundingRect = rects[i]; - } - } - } - if (boundingRect === void 0) return null; - if (boundingRect.top > innerHeight || boundingRect.left > innerWidth) { - return null; - } - if (boundingRect.width <= 1 || boundingRect.height <= 1) { - var children = node.children; - var visibleChildNode = false; - for (i = 0, l = children.length; i < l; ++i) { - boundingRect = children[i].getClientRects()[0] || children[i].getBoundingClientRect(); - if (boundingRect.width > 1 && boundingRect.height > 1) { - visibleChildNode = true; - break; - } - } - if (visibleChildNode === false) return null; - } - if (boundingRect.top + boundingRect.height < 10 || boundingRect.left + boundingRect.width < -10) { - return null; - } - var computedStyle = window.getComputedStyle(node, null); - if (computedStyle.visibility !== 'visible' || - computedStyle.display === 'none' || - node.hasAttribute('disabled') || - parseInt(computedStyle.width, '10') === 0 || - parseInt(computedStyle.height, '10') === 0) { - return null; - } - return boundingRect.top >= -20; - } - - var walker = document.createTreeWalker(document.body, 4, null, false); - var node; - var textNodes = []; - while (node = walker.nextNode()) { - if (node.nodeType === 3 && node.data.trim() !== '') { - textNodes.push(node); - } - } - for (var i = 0; i < textNodes.length; i++) { - var element = textNodes[i].parentElement; - if (isElementInViewport(element.parentElement)) { - el = element; - break; - } - } - if (el !== undefined) { - var range = document.createRange(); - range.setStart(el, 0); - range.setEnd(el, 0); - var sel = window.getSelection(); - sel.removeAllRanges(); - sel.addRange(range); - } - """) - @pyqtSlot(str, str) def on_config_changed(self, section, option): """Reinitialize the zoom neighborlist if related config changed.""" @@ -519,7 +446,8 @@ class WebView(QWebView): self.clearFocus() self.setFocus(Qt.OtherFocusReason) - self._position_caret() + self.page().currentFrame().evaluateJavaScript( + utils.read_file('javascript/position_caret.js')) @pyqtSlot(usertypes.KeyMode) def on_mode_left(self, mode): diff --git a/qutebrowser/javascript/position_caret.js b/qutebrowser/javascript/position_caret.js new file mode 100644 index 000000000..a31d83c55 --- /dev/null +++ b/qutebrowser/javascript/position_caret.js @@ -0,0 +1,89 @@ +/** +* Copyright 2014-2015 Florian Bruhin (The Compiler) +* +* This file is part of qutebrowser. +* +* qutebrowser is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* qutebrowser is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with qutebrowser. If not, see . +*/ + +/** + * Snippet to position caret at top of the page when caret mode enabled. + * Some code was borrowed from https://github.com/1995eaton/chromium-vim/blob/master/content_scripts/dom.js + * and https://github.com/1995eaton/chromium-vim/blob/master/content_scripts/visual.js + */ + +function isElementInViewport(node) { + var i; + var boundingRect = node.getClientRects()[0] || node.getBoundingClientRect(); + if (boundingRect.width <= 1 && boundingRect.height <= 1) { + var rects = node.getClientRects(); + for (i = 0; i < rects.length; i++) { + if (rects[i].width > rects[0].height && rects[i].height > rects[0].height) { + boundingRect = rects[i]; + } + } + } + if (boundingRect === void 0) return null; + if (boundingRect.top > innerHeight || boundingRect.left > innerWidth) { + return null; + } + if (boundingRect.width <= 1 || boundingRect.height <= 1) { + var children = node.children; + var visibleChildNode = false; + for (i = 0, l = children.length; i < l; ++i) { + boundingRect = children[i].getClientRects()[0] || children[i].getBoundingClientRect(); + if (boundingRect.width > 1 && boundingRect.height > 1) { + visibleChildNode = true; + break; + } + } + if (visibleChildNode === false) return null; + } + if (boundingRect.top + boundingRect.height < 10 || boundingRect.left + boundingRect.width < -10) { + return null; + } + var computedStyle = window.getComputedStyle(node, null); + if (computedStyle.visibility !== 'visible' || + computedStyle.display === 'none' || + node.hasAttribute('disabled') || + parseInt(computedStyle.width, '10') === 0 || + parseInt(computedStyle.height, '10') === 0) { + return null; + } + return boundingRect.top >= -20; +} + +var walker = document.createTreeWalker(document.body, 4, null, false); +var node; +var textNodes = []; +while (node = walker.nextNode()) { + if (node.nodeType === 3 && node.data.trim() !== '') { + textNodes.push(node); + } +} +for (var i = 0; i < textNodes.length; i++) { + var element = textNodes[i].parentElement; + if (isElementInViewport(element.parentElement)) { + el = element; + break; + } +} +if (el !== undefined) { + var range = document.createRange(); + range.setStart(el, 0); + range.setEnd(el, 0); + var sel = window.getSelection(); + sel.removeAllRanges(); + sel.addRange(range); +} From 99a4765e75424dea0b86171a057828de3946fff5 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 10 May 2015 14:50:56 +0200 Subject: [PATCH 019/182] Fix confusing websetting log output. --- qutebrowser/config/websettings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/config/websettings.py b/qutebrowser/config/websettings.py index 38831a9d5..38c662066 100644 --- a/qutebrowser/config/websettings.py +++ b/qutebrowser/config/websettings.py @@ -84,8 +84,8 @@ class Base: qws: The QWebSettings instance to use, or None to use the global instance. """ - log.config.vdebug("Restoring default {!r}.".format(self._default)) if self._default is not UNSET: + log.config.vdebug("Restoring default {!r}.".format(self._default)) self._set(self._default, qws=qws) def get(self, qws=None): From 8398fe3bdd8a69dc7204f75687b07217b01836fd Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 8 May 2015 00:06:55 +0200 Subject: [PATCH 020/182] Rewrite test_log.py to use pytest. See #660. --- tests/utils/test_log.py | 316 +++++++++++++++++++++------------------- 1 file changed, 163 insertions(+), 153 deletions(-) diff --git a/tests/utils/test_log.py b/tests/utils/test_log.py index d52adf552..d86707d80 100644 --- a/tests/utils/test_log.py +++ b/tests/utils/test_log.py @@ -22,231 +22,241 @@ """Tests for qutebrowser.utils.log.""" import logging -import unittest import argparse +import itertools import sys -from unittest import mock + +import pytest +from PyQt5.QtCore import qWarning from qutebrowser.utils import log -from PyQt5.QtCore import qWarning - -class BaseTest(unittest.TestCase): - - """Base class for logging tests. +@pytest.yield_fixture(autouse=True) +def restore_loggers(): + """Fixture to save/restore the logging state. Based on CPython's Lib/test/test_logging.py. """ + logger_dict = logging.getLogger().manager.loggerDict + logging._acquireLock() + try: + saved_handlers = logging._handlers.copy() + saved_handler_list = logging._handlerList[:] + saved_loggers = saved_loggers = logger_dict.copy() + saved_name_to_level = logging._nameToLevel.copy() + saved_level_to_name = logging._levelToName.copy() + logger_states = {} + for name in saved_loggers: + logger_states[name] = getattr(saved_loggers[name], 'disabled', + None) + finally: + logging._releaseLock() - def setUp(self): - """Save the old logging configuration.""" + root_logger = logging.getLogger("") + root_handlers = root_logger.handlers[:] + original_logging_level = root_logger.getEffectiveLevel() + + yield + + while root_logger.handlers: + h = root_logger.handlers[0] + root_logger.removeHandler(h) + h.close() + root_logger.setLevel(original_logging_level) + for h in root_handlers: + root_logger.addHandler(h) + logging._acquireLock() + try: + logging._levelToName.clear() + logging._levelToName.update(saved_level_to_name) + logging._nameToLevel.clear() + logging._nameToLevel.update(saved_name_to_level) + logging._handlers.clear() + logging._handlers.update(saved_handlers) + logging._handlerList[:] = saved_handler_list logger_dict = logging.getLogger().manager.loggerDict - logging._acquireLock() - try: - self.saved_handlers = logging._handlers.copy() - self.saved_handler_list = logging._handlerList[:] - self.saved_loggers = saved_loggers = logger_dict.copy() - self.saved_name_to_level = logging._nameToLevel.copy() - self.saved_level_to_name = logging._levelToName.copy() - self.logger_states = {} - for name in saved_loggers: - self.logger_states[name] = getattr(saved_loggers[name], - 'disabled', None) - finally: - logging._releaseLock() - - self.root_logger = logging.getLogger("") - self.root_handlers = self.root_logger.handlers[:] - self.original_logging_level = self.root_logger.getEffectiveLevel() - - def tearDown(self): - """Restore the original logging configuration.""" - while self.root_logger.handlers: - h = self.root_logger.handlers[0] - self.root_logger.removeHandler(h) - h.close() - self.root_logger.setLevel(self.original_logging_level) - for h in self.root_handlers: - self.root_logger.addHandler(h) - logging._acquireLock() - try: - logging._levelToName.clear() - logging._levelToName.update(self.saved_level_to_name) - logging._nameToLevel.clear() - logging._nameToLevel.update(self.saved_name_to_level) - logging._handlers.clear() - logging._handlers.update(self.saved_handlers) - logging._handlerList[:] = self.saved_handler_list - logger_dict = logging.getLogger().manager.loggerDict - logger_dict.clear() - logger_dict.update(self.saved_loggers) - logger_states = self.logger_states - for name in self.logger_states: - if logger_states[name] is not None: - self.saved_loggers[name].disabled = logger_states[name] - finally: - logging._releaseLock() + logger_dict.clear() + logger_dict.update(saved_loggers) + logger_states = logger_states + for name in logger_states: + if logger_states[name] is not None: + saved_loggers[name].disabled = logger_states[name] + finally: + logging._releaseLock() -class LogFilterTests(unittest.TestCase): +@pytest.fixture(scope='session') +def log_counter(): + """Counter for logger fixture to get unique loggers.""" + return itertools.count() - """Tests for LogFilter. - Attributes: - logger: The logger we use to create records. +@pytest.fixture +def logger(log_counter): + """Fixture which provides a logger for tests. + + Unique throwaway loggers are used to make sure the tests don't influence + each other. """ + i = next(log_counter) + return logging.getLogger('qutebrowser-unittest-logger-{}'.format(i)) - def setUp(self): - self.logger = logging.getLogger("foo") - def _make_record(self, name, level=logging.DEBUG): +class TestLogFilter: + + """Tests for LogFilter.""" + + def _make_record(self, logger, name, level=logging.DEBUG): """Create a bogus logging record with the supplied logger name.""" - return self.logger.makeRecord(name, level=level, fn=None, lno=0, - msg="", args=None, exc_info=None) + return logger.makeRecord(name, level=level, fn=None, lno=0, msg="", + args=None, exc_info=None) - def test_empty(self): + def test_empty(self, logger): """Test if an empty filter lets all messages through.""" logfilter = log.LogFilter(None) - record = self._make_record("eggs.bacon.spam") - self.assertTrue(logfilter.filter(record)) - record = self._make_record("eggs") - self.assertTrue(logfilter.filter(record)) + record = self._make_record(logger, "eggs.bacon.spam") + assert logfilter.filter(record) + record = self._make_record(logger, "eggs") + assert logfilter.filter(record) - def test_matching(self): + def test_matching(self, logger): """Test if a filter lets an exactly matching log record through.""" logfilter = log.LogFilter(["eggs", "bacon"]) - record = self._make_record("eggs") - self.assertTrue(logfilter.filter(record)) - record = self._make_record("bacon") - self.assertTrue(logfilter.filter(record)) - record = self._make_record("spam") - self.assertFalse(logfilter.filter(record)) + record = self._make_record(logger, "eggs") + assert logfilter.filter(record) + record = self._make_record(logger, "bacon") + assert logfilter.filter(record) + record = self._make_record(logger, "spam") + assert not logfilter.filter(record) logfilter = log.LogFilter(["eggs.bacon"]) - record = self._make_record("eggs.bacon") - self.assertTrue(logfilter.filter(record)) + record = self._make_record(logger, "eggs.bacon") + assert logfilter.filter(record) - def test_equal_start(self): + def test_equal_start(self, logger): """Test if a filter blocks a logger which looks equal but isn't.""" logfilter = log.LogFilter(["eggs"]) - record = self._make_record("eggsauce") - self.assertFalse(logfilter.filter(record)) + record = self._make_record(logger, "eggsauce") + assert not logfilter.filter(record) logfilter = log.LogFilter("eggs.bacon") - record = self._make_record("eggs.baconstrips") - self.assertFalse(logfilter.filter(record)) + record = self._make_record(logger, "eggs.baconstrips") + assert not logfilter.filter(record) - def test_child(self): + def test_child(self, logger): """Test if a filter lets through a logger which is a child.""" logfilter = log.LogFilter(["eggs.bacon", "spam.ham"]) - record = self._make_record("eggs.bacon.spam") - self.assertTrue(logfilter.filter(record)) - record = self._make_record("spam.ham.salami") - self.assertTrue(logfilter.filter(record)) + record = self._make_record(logger, "eggs.bacon.spam") + assert logfilter.filter(record) + record = self._make_record(logger, "spam.ham.salami") + assert logfilter.filter(record) - def test_debug(self): + def test_debug(self, logger): """Test if messages more important than debug are never filtered.""" logfilter = log.LogFilter(["eggs"]) # First check if the filter works as intended with debug messages - record = self._make_record("eggs") - self.assertTrue(logfilter.filter(record)) - record = self._make_record("bacon") - self.assertFalse(logfilter.filter(record)) + record = self._make_record(logger, "eggs") + assert logfilter.filter(record) + record = self._make_record(logger, "bacon") + assert not logfilter.filter(record) # Then check if info is not filtered - record = self._make_record("eggs", level=logging.INFO) - self.assertTrue(logfilter.filter(record)) - record = self._make_record("bacon", level=logging.INFO) - self.assertTrue(logfilter.filter(record)) + record = self._make_record(logger, "eggs", level=logging.INFO) + assert logfilter.filter(record) + record = self._make_record(logger, "bacon", level=logging.INFO) + assert logfilter.filter(record) -class RAMHandlerTests(BaseTest): +class TestRAMHandler: - """Tests for RAMHandler. + """Tests for RAMHandler.""" - Attributes: - logger: The logger we use to log to the handler. - handler: The RAMHandler we're testing. - old_level: The level the root logger had before executing the test. - old_handlers: The handlers the root logger had before executing the - test. - """ + @pytest.fixture + def handler(self, logger): + """Fixture providing a RAMHandler.""" + handler = log.RAMHandler(capacity=2) + handler.setLevel(logging.NOTSET) + logger.addHandler(handler) + return handler - def setUp(self): - super().setUp() - self.logger = logging.getLogger() - self.logger.handlers = [] - self.logger.setLevel(logging.NOTSET) - self.handler = log.RAMHandler(capacity=2) - self.handler.setLevel(logging.NOTSET) - self.logger.addHandler(self.handler) - - def test_filled(self): + def test_filled(self, handler, logger): """Test handler with exactly as much records as it can hold.""" - self.logger.debug("One") - self.logger.debug("Two") - self.assertEqual(len(self.handler._data), 2) - self.assertEqual(self.handler._data[0].msg, "One") - self.assertEqual(self.handler._data[1].msg, "Two") + logger.debug("One") + logger.debug("Two") + assert len(handler._data) == 2 + assert handler._data[0].msg == "One" + assert handler._data[1].msg == "Two" - def test_overflow(self): + def test_overflow(self, handler, logger): """Test handler with more records as it can hold.""" - self.logger.debug("One") - self.logger.debug("Two") - self.logger.debug("Three") - self.assertEqual(len(self.handler._data), 2) - self.assertEqual(self.handler._data[0].msg, "Two") - self.assertEqual(self.handler._data[1].msg, "Three") + logger.debug("One") + logger.debug("Two") + logger.debug("Three") + assert len(handler._data) == 2 + assert handler._data[0].msg == "Two" + assert handler._data[1].msg == "Three" - def test_dump_log(self): + def test_dump_log(self, handler, logger): """Test dump_log().""" - self.logger.debug("One") - self.logger.debug("Two") - self.logger.debug("Three") - self.assertEqual(self.handler.dump_log(), "Two\nThree") + logger.debug("One") + logger.debug("Two") + logger.debug("Three") + assert handler.dump_log() == "Two\nThree" -@mock.patch('qutebrowser.utils.log.qInstallMessageHandler', autospec=True) -class InitLogTests(BaseTest): +class TestInitLog: """Tests for init_log.""" - def setUp(self): - super().setUp() - self.args = argparse.Namespace(debug=True, loglevel=logging.DEBUG, - color=True, loglines=10, logfilter="") + @pytest.fixture(autouse=True) + def setup(self, mocker): + """Mock out qInstallMessageHandler.""" + mocker.patch('qutebrowser.utils.log.qInstallMessageHandler', + autospec=True) - def test_stderr_none(self, _mock): + @pytest.fixture + def args(self): + """Fixture providing an argparse namespace.""" + return argparse.Namespace(debug=True, loglevel=logging.DEBUG, + color=True, loglines=10, logfilter="") + + def test_stderr_none(self, args): """Test init_log with sys.stderr = None.""" old_stderr = sys.stderr sys.stderr = None - log.init_log(self.args) + log.init_log(args) sys.stderr = old_stderr -class HideQtWarningTests(BaseTest): +class TestHideQtWarning: """Tests for hide_qt_warning/QtWarningFilter.""" - def test_unfiltered(self): + def test_unfiltered(self, caplog): """Test a message which is not filtered.""" with log.hide_qt_warning("World", logger='qt-tests'): - with self.assertLogs('qt-tests', logging.WARNING): + with caplog.atLevel(logging.WARNING, logger='qt-tests'): qWarning("Hello World") + assert len(caplog.records()) == 1 + record = caplog.records()[0] + assert record.levelname == 'WARNING' + assert record.message == "Hello World" - def test_filtered_exact(self): + def test_filtered_exact(self, caplog): """Test a message which is filtered (exact match).""" with log.hide_qt_warning("Hello", logger='qt-tests'): - qWarning("Hello") + with caplog.atLevel(logging.WARNING, logger='qt-tests'): + qWarning("Hello") + assert not caplog.records() - def test_filtered_start(self): + def test_filtered_start(self, caplog): """Test a message which is filtered (match at line start).""" with log.hide_qt_warning("Hello", logger='qt-tests'): - qWarning("Hello World") + with caplog.atLevel(logging.WARNING, logger='qt-tests'): + qWarning("Hello World") + assert not caplog.records() - def test_filtered_whitespace(self): + def test_filtered_whitespace(self, caplog): """Test a message which is filtered (match with whitespace).""" with log.hide_qt_warning("Hello", logger='qt-tests'): - qWarning(" Hello World ") - - -if __name__ == '__main__': - unittest.main() + with caplog.atLevel(logging.WARNING, logger='qt-tests'): + qWarning(" Hello World ") + assert not caplog.records() From 021c94eece3ad9f7303c00d6c75a8fd1c538be3d Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 7 May 2015 22:57:19 +0200 Subject: [PATCH 021/182] Rewrite test_enum.py to use pytest. See #660. --- tests/utils/usertypes/test_enum.py | 63 +++++++++++++----------------- 1 file changed, 28 insertions(+), 35 deletions(-) diff --git a/tests/utils/usertypes/test_enum.py b/tests/utils/usertypes/test_enum.py index ed9b6fb09..e1443b7be 100644 --- a/tests/utils/usertypes/test_enum.py +++ b/tests/utils/usertypes/test_enum.py @@ -19,45 +19,38 @@ """Tests for the Enum class.""" -import unittest - from qutebrowser.utils import usertypes +import pytest + # FIXME: Add some more tests, e.g. for is_int -class EnumTests(unittest.TestCase): - - """Test simple enums. - - Attributes: - enum: The enum we're testing. - """ - - def setUp(self): - self.enum = usertypes.enum('Enum', ['one', 'two']) - - def test_values(self): - """Test if enum members resolve to the right values.""" - self.assertEqual(self.enum.one.value, 1) - self.assertEqual(self.enum.two.value, 2) - - def test_name(self): - """Test .name mapping.""" - self.assertEqual(self.enum.one.name, 'one') - self.assertEqual(self.enum.two.name, 'two') - - def test_unknown(self): - """Test invalid values which should raise an AttributeError.""" - with self.assertRaises(AttributeError): - _ = self.enum.three - - def test_start(self): - """Test the start= argument.""" - e = usertypes.enum('Enum', ['three', 'four'], start=3) - self.assertEqual(e.three.value, 3) - self.assertEqual(e.four.value, 4) +@pytest.fixture +def enum(): + return usertypes.enum('Enum', ['one', 'two']) -if __name__ == '__main__': - unittest.main() +def test_values(enum): + """Test if enum members resolve to the right values.""" + assert enum.one.value == 1 + assert enum.two.value == 2 + + +def test_name(enum): + """Test .name mapping.""" + assert enum.one.name == 'one' + assert enum.two.name == 'two' + + +def test_unknown(enum): + """Test invalid values which should raise an AttributeError.""" + with pytest.raises(AttributeError): + _ = enum.three + + +def test_start(): + """Test the start= argument.""" + e = usertypes.enum('Enum', ['three', 'four'], start=3) + assert e.three.value == 3 + assert e.four.value == 4 From 392fb3e1d705a62b6371c91020a219030a910d0e Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 10 May 2015 15:25:04 +0200 Subject: [PATCH 022/182] Rewrite test_neighborlist.py to use pytest. See #660. --- tests/utils/usertypes/test_neighborlist.py | 427 ++++++++++----------- 1 file changed, 197 insertions(+), 230 deletions(-) diff --git a/tests/utils/usertypes/test_neighborlist.py b/tests/utils/usertypes/test_neighborlist.py index ebde77a60..41e4f547a 100644 --- a/tests/utils/usertypes/test_neighborlist.py +++ b/tests/utils/usertypes/test_neighborlist.py @@ -21,364 +21,331 @@ """Tests for the NeighborList class.""" -import unittest - from qutebrowser.utils import usertypes +import pytest -class InitTests(unittest.TestCase): - """Just try to init some neighborlists. +class TestInit: - Attributes: - nl: The NeighborList we're testing. - """ + """Just try to init some neighborlists.""" def test_empty(self): """Test constructing an empty NeighborList.""" nl = usertypes.NeighborList() - self.assertEqual(nl.items, []) + assert nl.items == [] def test_items(self): """Test constructing an NeighborList with items.""" nl = usertypes.NeighborList([1, 2, 3]) - self.assertEqual(nl.items, [1, 2, 3]) + assert nl.items == [1, 2, 3] def test_len(self): """Test len() on NeighborList.""" nl = usertypes.NeighborList([1, 2, 3]) - self.assertEqual(len(nl), 3) + assert len(nl) == 3 def test_contains(self): """Test 'in' on NeighborList.""" nl = usertypes.NeighborList([1, 2, 3]) - self.assertIn(2, nl) - self.assertNotIn(4, nl) + assert 2 in nl + assert 4 not in nl -class DefaultTests(unittest.TestCase): +class TestDefaultArg: - """Test the default argument. - - Attributes: - nl: The NeighborList we're testing. - """ + """Test the default argument.""" def test_simple(self): """Test default with a numeric argument.""" nl = usertypes.NeighborList([1, 2, 3], default=2) - self.assertEqual(nl._idx, 1) + assert nl._idx == 1 def test_none(self): """Test default 'None'.""" nl = usertypes.NeighborList([1, 2, None], default=None) - self.assertEqual(nl._idx, 2) + assert nl._idx == 2 def test_unset(self): """Test unset default value.""" nl = usertypes.NeighborList([1, 2, 3]) - self.assertIsNone(nl._idx) + assert nl._idx is None -class EmptyTests(unittest.TestCase): +class TestEmpty: - """Tests with no items. + """Tests with no items.""" - Attributes: - nl: The NeighborList we're testing. - """ + @pytest.fixture + def neighborlist(self): + return usertypes.NeighborList() - def setUp(self): - self.nl = usertypes.NeighborList() - - def test_curitem(self): + def test_curitem(self, neighborlist): """Test curitem with no item.""" - with self.assertRaises(IndexError): - self.nl.curitem() + with pytest.raises(IndexError): + neighborlist.curitem() - def test_firstitem(self): + def test_firstitem(self, neighborlist): """Test firstitem with no item.""" - with self.assertRaises(IndexError): - self.nl.firstitem() + with pytest.raises(IndexError): + neighborlist.firstitem() - def test_lastitem(self): + def test_lastitem(self, neighborlist): """Test lastitem with no item.""" - with self.assertRaises(IndexError): - self.nl.lastitem() + with pytest.raises(IndexError): + neighborlist.lastitem() - def test_getitem(self): + def test_getitem(self, neighborlist): """Test getitem with no item.""" - with self.assertRaises(IndexError): - self.nl.getitem(1) + with pytest.raises(IndexError): + neighborlist.getitem(1) -class ItemTests(unittest.TestCase): +class TestItems: - """Tests with items. + """Tests with items.""" - Attributes: - nl: The NeighborList we're testing. - """ + @pytest.fixture + def neighborlist(self): + return usertypes.NeighborList([1, 2, 3, 4, 5], default=3) - def setUp(self): - self.nl = usertypes.NeighborList([1, 2, 3, 4, 5], default=3) - - def test_curitem(self): + def test_curitem(self, neighborlist): """Test curitem().""" - self.assertEqual(self.nl._idx, 2) - self.assertEqual(self.nl.curitem(), 3) - self.assertEqual(self.nl._idx, 2) + assert neighborlist._idx == 2 + assert neighborlist.curitem() == 3 + assert neighborlist._idx == 2 - def test_nextitem(self): + def test_nextitem(self, neighborlist): """Test nextitem().""" - self.assertEqual(self.nl.nextitem(), 4) - self.assertEqual(self.nl._idx, 3) - self.assertEqual(self.nl.nextitem(), 5) - self.assertEqual(self.nl._idx, 4) + assert neighborlist.nextitem() == 4 + assert neighborlist._idx == 3 + assert neighborlist.nextitem() == 5 + assert neighborlist._idx == 4 - def test_previtem(self): + def test_previtem(self, neighborlist): """Test previtem().""" - self.assertEqual(self.nl.previtem(), 2) - self.assertEqual(self.nl._idx, 1) - self.assertEqual(self.nl.previtem(), 1) - self.assertEqual(self.nl._idx, 0) + assert neighborlist.previtem() == 2 + assert neighborlist._idx == 1 + assert neighborlist.previtem() == 1 + assert neighborlist._idx == 0 - def test_firstitem(self): + def test_firstitem(self, neighborlist): """Test firstitem().""" - self.assertEqual(self.nl.firstitem(), 1) - self.assertEqual(self.nl._idx, 0) + assert neighborlist.firstitem() == 1 + assert neighborlist._idx == 0 - def test_lastitem(self): + def test_lastitem(self, neighborlist): """Test lastitem().""" - self.assertEqual(self.nl.lastitem(), 5) - self.assertEqual(self.nl._idx, 4) + assert neighborlist.lastitem() == 5 + assert neighborlist._idx == 4 - def test_reset(self): + def test_reset(self, neighborlist): """Test reset().""" - self.nl.nextitem() - self.assertEqual(self.nl._idx, 3) - self.nl.reset() - self.assertEqual(self.nl._idx, 2) + neighborlist.nextitem() + assert neighborlist._idx == 3 + neighborlist.reset() + assert neighborlist._idx == 2 - def test_getitem(self): + def test_getitem(self, neighborlist): """Test getitem().""" - self.assertEqual(self.nl.getitem(2), 5) - self.assertEqual(self.nl._idx, 4) - self.nl.reset() - self.assertEqual(self.nl.getitem(-2), 1) - self.assertEqual(self.nl._idx, 0) + assert neighborlist.getitem(2) == 5 + assert neighborlist._idx == 4 + neighborlist.reset() + assert neighborlist.getitem(-2) == 1 + assert neighborlist._idx == 0 -class OneTests(unittest.TestCase): +class TestSingleItem: - """Tests with a list with only one item. + """Tests with a list with only one item.""" - Attributes: - nl: The NeighborList we're testing. - """ + @pytest.fixture + def neighborlist(self): + return usertypes.NeighborList([1], default=1) - def setUp(self): - self.nl = usertypes.NeighborList([1], default=1) - - def test_first_wrap(self): + def test_first_wrap(self, neighborlist): """Test out of bounds previtem() with mode=wrap.""" - self.nl._mode = usertypes.NeighborList.Modes.wrap - self.nl.firstitem() - self.assertEqual(self.nl._idx, 0) - self.assertEqual(self.nl.previtem(), 1) - self.assertEqual(self.nl._idx, 0) + neighborlist._mode = usertypes.NeighborList.Modes.wrap + neighborlist.firstitem() + assert neighborlist._idx == 0 + assert neighborlist.previtem() == 1 + assert neighborlist._idx == 0 - def test_first_block(self): + def test_first_block(self, neighborlist): """Test out of bounds previtem() with mode=block.""" - self.nl._mode = usertypes.NeighborList.Modes.block - self.nl.firstitem() - self.assertEqual(self.nl._idx, 0) - self.assertEqual(self.nl.previtem(), 1) - self.assertEqual(self.nl._idx, 0) + neighborlist._mode = usertypes.NeighborList.Modes.block + neighborlist.firstitem() + assert neighborlist._idx == 0 + assert neighborlist.previtem() == 1 + assert neighborlist._idx == 0 - def test_first_raise(self): + def test_first_raise(self, neighborlist): """Test out of bounds previtem() with mode=raise.""" - self.nl._mode = usertypes.NeighborList.Modes.exception - self.nl.firstitem() - self.assertEqual(self.nl._idx, 0) - with self.assertRaises(IndexError): - self.nl.previtem() - self.assertEqual(self.nl._idx, 0) + neighborlist._mode = usertypes.NeighborList.Modes.exception + neighborlist.firstitem() + assert neighborlist._idx == 0 + with pytest.raises(IndexError): + neighborlist.previtem() + assert neighborlist._idx == 0 - def test_last_wrap(self): + def test_last_wrap(self, neighborlist): """Test out of bounds nextitem() with mode=wrap.""" - self.nl._mode = usertypes.NeighborList.Modes.wrap - self.nl.lastitem() - self.assertEqual(self.nl._idx, 0) - self.assertEqual(self.nl.nextitem(), 1) - self.assertEqual(self.nl._idx, 0) + neighborlist._mode = usertypes.NeighborList.Modes.wrap + neighborlist.lastitem() + assert neighborlist._idx == 0 + assert neighborlist.nextitem() == 1 + assert neighborlist._idx == 0 - def test_last_block(self): + def test_last_block(self, neighborlist): """Test out of bounds nextitem() with mode=block.""" - self.nl._mode = usertypes.NeighborList.Modes.block - self.nl.lastitem() - self.assertEqual(self.nl._idx, 0) - self.assertEqual(self.nl.nextitem(), 1) - self.assertEqual(self.nl._idx, 0) + neighborlist._mode = usertypes.NeighborList.Modes.block + neighborlist.lastitem() + assert neighborlist._idx == 0 + assert neighborlist.nextitem() == 1 + assert neighborlist._idx == 0 - def test_last_raise(self): + def test_last_raise(self, neighborlist): """Test out of bounds nextitem() with mode=raise.""" - self.nl._mode = usertypes.NeighborList.Modes.exception - self.nl.lastitem() - self.assertEqual(self.nl._idx, 0) - with self.assertRaises(IndexError): - self.nl.nextitem() - self.assertEqual(self.nl._idx, 0) + neighborlist._mode = usertypes.NeighborList.Modes.exception + neighborlist.lastitem() + assert neighborlist._idx == 0 + with pytest.raises(IndexError): + neighborlist.nextitem() + assert neighborlist._idx == 0 -class BlockTests(unittest.TestCase): +class TestBlockMode: - """Tests with mode=block. + """Tests with mode=block.""" - Attributes: - nl: The NeighborList we're testing. - """ - - def setUp(self): - self.nl = usertypes.NeighborList( + @pytest.fixture + def neighborlist(self): + return usertypes.NeighborList( [1, 2, 3, 4, 5], default=3, mode=usertypes.NeighborList.Modes.block) - def test_first(self): + def test_first(self, neighborlist): """Test out of bounds previtem().""" - self.nl.firstitem() - self.assertEqual(self.nl._idx, 0) - self.assertEqual(self.nl.previtem(), 1) - self.assertEqual(self.nl._idx, 0) + neighborlist.firstitem() + assert neighborlist._idx == 0 + assert neighborlist.previtem() == 1 + assert neighborlist._idx == 0 - def test_last(self): + def test_last(self, neighborlist): """Test out of bounds nextitem().""" - self.nl.lastitem() - self.assertEqual(self.nl._idx, 4) - self.assertEqual(self.nl.nextitem(), 5) - self.assertEqual(self.nl._idx, 4) + neighborlist.lastitem() + assert neighborlist._idx == 4 + assert neighborlist.nextitem() == 5 + assert neighborlist._idx == 4 -class WrapTests(unittest.TestCase): +class TestWrapMode: - """Tests with mode=wrap. + """Tests with mode=wrap.""" - Attributes: - nl: The NeighborList we're testing. - """ - - def setUp(self): - self.nl = usertypes.NeighborList( + @pytest.fixture + def neighborlist(self): + return usertypes.NeighborList( [1, 2, 3, 4, 5], default=3, mode=usertypes.NeighborList.Modes.wrap) - def test_first(self): + def test_first(self, neighborlist): """Test out of bounds previtem().""" - self.nl.firstitem() - self.assertEqual(self.nl._idx, 0) - self.assertEqual(self.nl.previtem(), 5) - self.assertEqual(self.nl._idx, 4) + neighborlist.firstitem() + assert neighborlist._idx == 0 + assert neighborlist.previtem() == 5 + assert neighborlist._idx == 4 - def test_last(self): + def test_last(self, neighborlist): """Test out of bounds nextitem().""" - self.nl.lastitem() - self.assertEqual(self.nl._idx, 4) - self.assertEqual(self.nl.nextitem(), 1) - self.assertEqual(self.nl._idx, 0) + neighborlist.lastitem() + assert neighborlist._idx == 4 + assert neighborlist.nextitem() == 1 + assert neighborlist._idx == 0 -class RaiseTests(unittest.TestCase): +class TestExceptionMode: - """Tests with mode=exception. + """Tests with mode=exception.""" - Attributes: - nl: The NeighborList we're testing. - """ - - def setUp(self): - self.nl = usertypes.NeighborList( + @pytest.fixture + def neighborlist(self): + return usertypes.NeighborList( [1, 2, 3, 4, 5], default=3, mode=usertypes.NeighborList.Modes.exception) - def test_first(self): + def test_first(self, neighborlist): """Test out of bounds previtem().""" - self.nl.firstitem() - self.assertEqual(self.nl._idx, 0) - with self.assertRaises(IndexError): - self.nl.previtem() - self.assertEqual(self.nl._idx, 0) + neighborlist.firstitem() + assert neighborlist._idx == 0 + with pytest.raises(IndexError): + neighborlist.previtem() + assert neighborlist._idx == 0 - def test_last(self): + def test_last(self, neighborlist): """Test out of bounds nextitem().""" - self.nl.lastitem() - self.assertEqual(self.nl._idx, 4) - with self.assertRaises(IndexError): - self.nl.nextitem() - self.assertEqual(self.nl._idx, 4) + neighborlist.lastitem() + assert neighborlist._idx == 4 + with pytest.raises(IndexError): + neighborlist.nextitem() + assert neighborlist._idx == 4 -class SnapInTests(unittest.TestCase): +class TestSnapIn: - """Tests for the fuzzyval/_snap_in features. + """Tests for the fuzzyval/_snap_in features.""" - Attributes: - nl: The NeighborList we're testing. - """ + @pytest.fixture + def neighborlist(self): + return usertypes.NeighborList([20, 9, 1, 5]) - def setUp(self): - self.nl = usertypes.NeighborList([20, 9, 1, 5]) - - def test_bigger(self): + def test_bigger(self, neighborlist): """Test fuzzyval with snapping to a bigger value.""" - self.nl.fuzzyval = 7 - self.assertEqual(self.nl.nextitem(), 9) - self.assertEqual(self.nl._idx, 1) - self.assertEqual(self.nl.nextitem(), 1) - self.assertEqual(self.nl._idx, 2) + neighborlist.fuzzyval = 7 + assert neighborlist.nextitem() == 9 + assert neighborlist._idx == 1 + assert neighborlist.nextitem() == 1 + assert neighborlist._idx == 2 - def test_smaller(self): + def test_smaller(self, neighborlist): """Test fuzzyval with snapping to a smaller value.""" - self.nl.fuzzyval = 7 - self.assertEqual(self.nl.previtem(), 5) - self.assertEqual(self.nl._idx, 3) - self.assertEqual(self.nl.previtem(), 1) - self.assertEqual(self.nl._idx, 2) + neighborlist.fuzzyval = 7 + assert neighborlist.previtem() == 5 + assert neighborlist._idx == 3 + assert neighborlist.previtem() == 1 + assert neighborlist._idx == 2 - def test_equal_bigger(self): + def test_equal_bigger(self, neighborlist): """Test fuzzyval with matching value, snapping to a bigger value.""" - self.nl.fuzzyval = 20 - self.assertEqual(self.nl.nextitem(), 9) - self.assertEqual(self.nl._idx, 1) + neighborlist.fuzzyval = 20 + assert neighborlist.nextitem() == 9 + assert neighborlist._idx == 1 - def test_equal_smaller(self): + def test_equal_smaller(self, neighborlist): """Test fuzzyval with matching value, snapping to a smaller value.""" - self.nl.fuzzyval = 5 - self.assertEqual(self.nl.previtem(), 1) - self.assertEqual(self.nl._idx, 2) + neighborlist.fuzzyval = 5 + assert neighborlist.previtem() == 1 + assert neighborlist._idx == 2 - def test_too_big_next(self): + def test_too_big_next(self, neighborlist): """Test fuzzyval/next with a value bigger than any in the list.""" - self.nl.fuzzyval = 30 - self.assertEqual(self.nl.nextitem(), 20) - self.assertEqual(self.nl._idx, 0) + neighborlist.fuzzyval = 30 + assert neighborlist.nextitem() == 20 + assert neighborlist._idx == 0 - def test_too_big_prev(self): + def test_too_big_prev(self, neighborlist): """Test fuzzyval/prev with a value bigger than any in the list.""" - self.nl.fuzzyval = 30 - self.assertEqual(self.nl.previtem(), 20) - self.assertEqual(self.nl._idx, 0) + neighborlist.fuzzyval = 30 + assert neighborlist.previtem() == 20 + assert neighborlist._idx == 0 - def test_too_small_next(self): + def test_too_small_next(self, neighborlist): """Test fuzzyval/next with a value smaller than any in the list.""" - self.nl.fuzzyval = 0 - self.assertEqual(self.nl.nextitem(), 1) - self.assertEqual(self.nl._idx, 2) + neighborlist.fuzzyval = 0 + assert neighborlist.nextitem() == 1 + assert neighborlist._idx == 2 - def test_too_small_prev(self): + def test_too_small_prev(self, neighborlist): """Test fuzzyval/prev with a value smaller than any in the list.""" - self.nl.fuzzyval = 0 - self.assertEqual(self.nl.previtem(), 1) - self.assertEqual(self.nl._idx, 2) - - -if __name__ == '__main__': - unittest.main() + neighborlist.fuzzyval = 0 + assert neighborlist.previtem() == 1 + assert neighborlist._idx == 2 From db0a54b03fd61eaa52f06669914d24652f32ec9d Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 10 May 2015 15:27:35 +0200 Subject: [PATCH 023/182] Rewrite test_crashdialog.py to use pytest. See #660. --- tests/misc/test_crashdialog.py | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/tests/misc/test_crashdialog.py b/tests/misc/test_crashdialog.py index 6c93833b1..ae0078c99 100644 --- a/tests/misc/test_crashdialog.py +++ b/tests/misc/test_crashdialog.py @@ -19,8 +19,6 @@ """Tests for qutebrowser.misc.crashdialog.""" -import unittest - from qutebrowser.misc import crashdialog @@ -52,7 +50,7 @@ Hello world! """ -class ParseFatalStacktraceTests(unittest.TestCase): +class TestParseFatalStacktrace: """Tests for parse_fatal_stacktrace.""" @@ -60,30 +58,22 @@ class ParseFatalStacktraceTests(unittest.TestCase): """Test parse_fatal_stacktrace with a valid text.""" text = VALID_CRASH_TEXT.strip().replace('_', ' ') typ, func = crashdialog.parse_fatal_stacktrace(text) - self.assertEqual(typ, "Segmentation fault") - self.assertEqual(func, 'testfunc') + assert (typ, func) == ("Segmentation fault", 'testfunc') def test_valid_text_thread(self): """Test parse_fatal_stacktrace with a valid text #2.""" text = VALID_CRASH_TEXT_THREAD.strip().replace('_', ' ') typ, func = crashdialog.parse_fatal_stacktrace(text) - self.assertEqual(typ, "Segmentation fault") - self.assertEqual(func, 'testfunc') + assert (typ, func) == ("Segmentation fault", 'testfunc') def test_valid_text_empty(self): """Test parse_fatal_stacktrace with a valid text but empty function.""" text = VALID_CRASH_TEXT_EMPTY.strip().replace('_', ' ') typ, func = crashdialog.parse_fatal_stacktrace(text) - self.assertEqual(typ, 'Aborted') - self.assertEqual(func, '') + assert (typ, func) == ('Aborted', '') def test_invalid_text(self): """Test parse_fatal_stacktrace with an invalid text.""" text = INVALID_CRASH_TEXT.strip().replace('_', ' ') typ, func = crashdialog.parse_fatal_stacktrace(text) - self.assertEqual(typ, '') - self.assertEqual(func, '') - - -if __name__ == '__main__': - unittest.main() + assert (typ, func) == ('', '') From 452e03f9af6a7f4cf96fcbb18bfd002815fb5653 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 10 May 2015 15:53:24 +0200 Subject: [PATCH 024/182] Rewrite test_lineparser.py to use pytest. See #660. --- tests/misc/test_lineparser.py | 136 ++++++++++++++++++---------------- 1 file changed, 73 insertions(+), 63 deletions(-) diff --git a/tests/misc/test_lineparser.py b/tests/misc/test_lineparser.py index c473eda50..87bc9b287 100644 --- a/tests/misc/test_lineparser.py +++ b/tests/misc/test_lineparser.py @@ -23,10 +23,10 @@ import io import os -import unittest -from unittest import mock -from qutebrowser.misc import lineparser +import pytest + +from qutebrowser.misc import lineparser as lineparsermod class LineParserWrapper: @@ -71,116 +71,126 @@ class LineParserWrapper: self._test_save_prepared = True -class TestableAppendLineParser(LineParserWrapper, lineparser.AppendLineParser): +class TestableAppendLineParser(LineParserWrapper, + lineparsermod.AppendLineParser): """Wrapper over AppendLineParser to make it testable.""" pass -class TestableLineParser(LineParserWrapper, lineparser.LineParser): +class TestableLineParser(LineParserWrapper, lineparsermod.LineParser): """Wrapper over LineParser to make it testable.""" pass -class TestableLimitLineParser(LineParserWrapper, lineparser.LimitLineParser): +class TestableLimitLineParser(LineParserWrapper, + lineparsermod.LimitLineParser): """Wrapper over LimitLineParser to make it testable.""" pass -@mock.patch('qutebrowser.misc.lineparser.os.path') -@mock.patch('qutebrowser.misc.lineparser.os') -class BaseLineParserTests(unittest.TestCase): +class TestBaseLineParser: """Tests for BaseLineParser.""" - def setUp(self): - self._confdir = "this really doesn't matter" - self._fname = "and neither does this" - self._lineparser = lineparser.BaseLineParser( - self._confdir, self._fname) + CONFDIR = "this really doesn't matter" + FILENAME = "and neither does this" - def test_prepare_save_existing(self, os_mock, os_path_mock): + @pytest.fixture + def lineparser(self): + """Fixture providing a BaseLineParser.""" + return lineparsermod.BaseLineParser(self.CONFDIR, self.FILENAME) + + def test_prepare_save_existing(self, mocker, lineparser): """Test if _prepare_save does what it's supposed to do.""" - os_path_mock.exists.return_value = True - self._lineparser._prepare_save() - self.assertFalse(os_mock.makedirs.called) + exists_mock = mocker.patch( + 'qutebrowser.misc.lineparser.os.path.exists') + makedirs_mock = mocker.patch('qutebrowser.misc.lineparser.os.makedirs') + exists_mock.return_value = True - def test_prepare_save_missing(self, os_mock, os_path_mock): + lineparser._prepare_save() + assert not makedirs_mock.called + + def test_prepare_save_missing(self, mocker, lineparser): """Test if _prepare_save does what it's supposed to do.""" - os_path_mock.exists.return_value = False - self._lineparser._prepare_save() - os_mock.makedirs.assert_called_with(self._confdir, 0o755) + exists_mock = mocker.patch( + 'qutebrowser.misc.lineparser.os.path.exists') + exists_mock.return_value = False + makedirs_mock = mocker.patch('qutebrowser.misc.lineparser.os.makedirs') + + lineparser._prepare_save() + makedirs_mock.assert_called_with(self.CONFDIR, 0o755) -class AppendLineParserTests(unittest.TestCase): +class TestAppendLineParser: """Tests for AppendLineParser.""" - def setUp(self): - self._lineparser = TestableAppendLineParser('this really', - 'does not matter') - self._lineparser.new_data = ['old data 1', 'old data 2'] - self._expected_data = self._lineparser.new_data - self._lineparser.save() + BASE_DATA = ['old data 1', 'old data 2'] - def _get_expected(self): + @pytest.fixture + def lineparser(self): + """Fixture to get an AppendLineParser for tests.""" + lp = TestableAppendLineParser('this really', 'does not matter') + lp.new_data = self.BASE_DATA + lp.save() + return lp + + def _get_expected(self, new_data): """Get the expected data with newlines.""" - return '\n'.join(self._expected_data) + '\n' + return '\n'.join(self.BASE_DATA + new_data) + '\n' - def test_save(self): + def test_save(self, lineparser): """Test save().""" - self._lineparser.new_data = ['new data 1', 'new data 2'] - self._expected_data += self._lineparser.new_data - self._lineparser.save() - self.assertEqual(self._lineparser._data, self._get_expected()) + new_data = ['new data 1', 'new data 2'] + lineparser.new_data = new_data + lineparser.save() + assert lineparser._data == self._get_expected(new_data) - def test_iter_without_open(self): + def test_iter_without_open(self, lineparser): """Test __iter__ without having called open().""" - with self.assertRaises(ValueError): - iter(self._lineparser) + with pytest.raises(ValueError): + iter(lineparser) - def test_iter(self): + def test_iter(self, lineparser): """Test __iter__.""" - self._lineparser.new_data = ['new data 1', 'new data 2'] - self._expected_data += self._lineparser.new_data - with self._lineparser.open(): - self.assertEqual(list(self._lineparser), self._expected_data) + new_data = ['new data 1', 'new data 2'] + lineparser.new_data = new_data + with lineparser.open(): + assert list(lineparser) == self.BASE_DATA + new_data - @mock.patch('qutebrowser.misc.lineparser.AppendLineParser._open') - def test_iter_not_found(self, open_mock): + def test_iter_not_found(self, mocker): """Test __iter__ with no file.""" + open_mock = mocker.patch( + 'qutebrowser.misc.lineparser.AppendLineParser._open') open_mock.side_effect = FileNotFoundError - linep = lineparser.AppendLineParser('foo', 'bar') - linep.new_data = ['new data 1', 'new data 2'] - expected_data = linep.new_data + new_data = ['new data 1', 'new data 2'] + linep = lineparsermod.AppendLineParser('foo', 'bar') + linep.new_data = new_data with linep.open(): - self.assertEqual(list(linep), expected_data) + assert list(linep) == new_data def test_get_recent_none(self): """Test get_recent with no data.""" linep = TestableAppendLineParser('this really', 'does not matter') - self.assertEqual(linep.get_recent(), []) + assert linep.get_recent() == [] - def test_get_recent_little(self): + def test_get_recent_little(self, lineparser): """Test get_recent with little data.""" - data = [e + '\n' for e in self._expected_data] - self.assertEqual(self._lineparser.get_recent(), data) + data = [e + '\n' for e in self.BASE_DATA] + assert lineparser.get_recent() == data - def test_get_recent_much(self): + def test_get_recent_much(self, lineparser): """Test get_recent with much data.""" size = 64 new_data = ['new data {}'.format(i) for i in range(size)] - self._lineparser.new_data = new_data - self._lineparser.save() - data = '\n'.join(self._expected_data + new_data) + lineparser.new_data = new_data + lineparser.save() + data = '\n'.join(self.BASE_DATA + new_data) data = [e + '\n' for e in data[-(size - 1):].splitlines()] - self.assertEqual(self._lineparser.get_recent(size), data) - - -if __name__ == '__main__': - unittest.main() + assert lineparser.get_recent(size) == data From 18eace37f895534773500658d4c84de57c779840 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 10 May 2015 21:47:05 +0200 Subject: [PATCH 025/182] tox: Add pytest-html. --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 63093ed75..31fb7bdef 100644 --- a/tox.ini +++ b/tox.ini @@ -22,6 +22,7 @@ deps = pytest-capturelog==0.7 pytest-qt==1.3.0 pytest-mock==0.5 + pytest-html==1.1 # We don't use {[testenv:mkvenv]commands} here because that seems to be broken # on Ubuntu Trusty. commands = From 21dcf73e38fb01bbaacabe9cdac8c98bf8c20592 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 10 May 2015 22:10:30 +0200 Subject: [PATCH 026/182] Add testresults.html to .gitignore. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 8b81dfc20..f3ff3652a 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,4 @@ __pycache__ /.coverage /htmlcov /.tox +/testresults.html From 6f620a6a9ecae12a22d2b13882639b834604687d Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 11 May 2015 19:11:49 +0200 Subject: [PATCH 027/182] Handle title correctly for pages without title. Fixes #667. --- qutebrowser/browser/webview.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/qutebrowser/browser/webview.py b/qutebrowser/browser/webview.py index 110c57be7..50635acb0 100644 --- a/qutebrowser/browser/webview.py +++ b/qutebrowser/browser/webview.py @@ -378,6 +378,8 @@ class WebView(QWebView): if url.isValid(): self.cur_url = url self.url_text_changed.emit(url.toDisplayString()) + if not self.title(): + self.titleChanged.emit(self.url().toDisplayString()) @pyqtSlot('QMouseEvent') def on_mouse_event(self, evt): @@ -396,7 +398,7 @@ class WebView(QWebView): @pyqtSlot() def on_load_finished(self): - """Handle auto-insert-mode after loading finished. + """Handle a finished page load. We don't take loadFinished's ok argument here as it always seems to be true when the QWebPage has an ErrorPageExtension implemented. @@ -409,6 +411,12 @@ class WebView(QWebView): self._set_load_status(LoadStatus.warn) else: self._set_load_status(LoadStatus.error) + if not self.title(): + self.titleChanged.emit(self.url().toDisplayString()) + self._handle_auto_insert_mode(ok) + + def _handle_auto_insert_mode(self, ok): + """Handle auto-insert-mode after loading finished.""" if not config.get('input', 'auto-insert-mode'): return mode_manager = objreg.get('mode-manager', scope='window', From 9fadc78e4d8d6e85b5af82d282425fb2d51fd9b3 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 11 May 2015 19:51:49 +0200 Subject: [PATCH 028/182] Update changelog. --- CHANGELOG.asciidoc | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 37feccefa..9096f2431 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -40,6 +40,7 @@ Fixed - Fixed searching for terms starting with a hyphen (e.g. `/-foo`) - Proxy authentication credentials are now remembered between different tabs. +- Fixed updating of the tab title on pages without title. https://github.com/The-Compiler/qutebrowser/releases/tag/v0.2.1[v0.2.1] ----------------------------------------------------------------------- From d3c6ebcf15ceb47768b6aa0bdd9d6a8e40da5a07 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 11 May 2015 20:21:01 +0200 Subject: [PATCH 029/182] Rename caret_selection to caret-selection. --- qutebrowser/config/configdata.py | 2 +- qutebrowser/mainwindow/statusbar/bar.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index e7da24caf..19e1e4da4 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -820,7 +820,7 @@ def data(readonly=False): SettingValue(typ.QssColor(), 'purple'), "Background color of the statusbar in caret mode."), - ('statusbar.bg.caret_selection', + ('statusbar.bg.caret-selection', SettingValue(typ.QssColor(), '#a12dff'), "Background color of the statusbar in caret selection enabled mode."), diff --git a/qutebrowser/mainwindow/statusbar/bar.py b/qutebrowser/mainwindow/statusbar/bar.py index e8311a1e7..aa655f671 100644 --- a/qutebrowser/mainwindow/statusbar/bar.py +++ b/qutebrowser/mainwindow/statusbar/bar.py @@ -108,7 +108,7 @@ class StatusBar(QWidget): } QWidget#StatusBar[caret_selection_active="true"] { - {{ color['statusbar.bg.caret_selection'] }} + {{ color['statusbar.bg.caret-selection'] }} } QWidget#StatusBar[prompt_active="true"] { From a36c0fcd4cd93c379359c9bf0718e13d793b11d3 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 11 May 2015 20:32:27 +0200 Subject: [PATCH 030/182] Fix lint. --- qutebrowser/browser/commands.py | 31 ++++++++++++++++--------- qutebrowser/browser/webview.py | 11 +++++---- qutebrowser/config/configdata.py | 3 ++- qutebrowser/keyinput/modeparsers.py | 1 - qutebrowser/mainwindow/statusbar/bar.py | 5 ++-- 5 files changed, 31 insertions(+), 20 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 65bac7901..a8ea029b9 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -42,7 +42,6 @@ from qutebrowser.utils import (message, usertypes, log, qtutils, urlutils, objreg, utils) from qutebrowser.utils.usertypes import KeyMode from qutebrowser.misc import editor -from qutebrowser.keyinput import modeman class CommandDispatcher: @@ -1267,9 +1266,11 @@ class CommandDispatcher: """Move the cursor or select to the start of next block.""" webview = self._current_widget() if not webview.selection_enabled: - act = [QWebPage.MoveToEndOfBlock, QWebPage.MoveToNextLine, QWebPage.MoveToStartOfBlock] + act = [QWebPage.MoveToEndOfBlock, QWebPage.MoveToNextLine, + QWebPage.MoveToStartOfBlock] else: - act = [QWebPage.SelectEndOfBlock, QWebPage.SelectNextLine, QWebPage.SelectStartOfBlock] + act = [QWebPage.SelectEndOfBlock, QWebPage.SelectNextLine, + QWebPage.SelectStartOfBlock] for _ in range(count): for a in act: webview.triggerPageAction(a) @@ -1280,9 +1281,11 @@ class CommandDispatcher: """Move the cursor or select to the start of previous block.""" webview = self._current_widget() if not webview.selection_enabled: - act = [QWebPage.MoveToStartOfBlock, QWebPage.MoveToPreviousLine, QWebPage.MoveToStartOfBlock] + act = [QWebPage.MoveToStartOfBlock, QWebPage.MoveToPreviousLine, + QWebPage.MoveToStartOfBlock] else: - act = [QWebPage.SelectStartOfBlock, QWebPage.SelectPreviousLine, QWebPage.SelectStartOfBlock] + act = [QWebPage.SelectStartOfBlock, QWebPage.SelectPreviousLine, + QWebPage.SelectStartOfBlock] for _ in range(count): for a in act: webview.triggerPageAction(a) @@ -1293,9 +1296,11 @@ class CommandDispatcher: """Move the cursor or select to the end of next block.""" webview = self._current_widget() if not webview.selection_enabled: - act = [QWebPage.MoveToEndOfBlock, QWebPage.MoveToNextLine, QWebPage.MoveToEndOfBlock] + act = [QWebPage.MoveToEndOfBlock, QWebPage.MoveToNextLine, + QWebPage.MoveToEndOfBlock] else: - act = [QWebPage.SelectEndOfBlock, QWebPage.SelectNextLine, QWebPage.SelectEndOfBlock] + act = [QWebPage.SelectEndOfBlock, QWebPage.SelectNextLine, + QWebPage.SelectEndOfBlock] for _ in range(count): for a in act: webview.triggerPageAction(a) @@ -1306,9 +1311,11 @@ class CommandDispatcher: """Move the cursor or select to the end of previous block.""" webview = self._current_widget() if not webview.selection_enabled: - act = [QWebPage.MoveToStartOfBlock, QWebPage.MoveToPreviousLine, QWebPage.MoveToEndOfBlock] + act = [QWebPage.MoveToStartOfBlock, QWebPage.MoveToPreviousLine, + QWebPage.MoveToEndOfBlock] else: - act = [QWebPage.SelectStartOfBlock, QWebPage.SelectPreviousLine, QWebPage.SelectEndOfBlock] + act = [QWebPage.SelectStartOfBlock, QWebPage.SelectPreviousLine, + QWebPage.SelectEndOfBlock] for _ in range(count): for a in act: webview.triggerPageAction(a) @@ -1364,8 +1371,10 @@ class CommandDispatcher: modes=[KeyMode.caret], scope='window') def toggle_selection(self): """Toggle caret selection mode.""" - self._current_widget().selection_enabled = not self._current_widget().selection_enabled - mainwindow = objreg.get('main-window', scope='window', window=self._win_id) + widget = self._current_widget() + widget.selection_enabled = not widget.selection_enabled + mainwindow = objreg.get('main-window', scope='window', + window=self._win_id) mainwindow.status.on_mode_entered(usertypes.KeyMode.caret) @cmdutils.register(instance='command-dispatcher', hide=True, diff --git a/qutebrowser/browser/webview.py b/qutebrowser/browser/webview.py index 5c592736e..684ce8e3d 100644 --- a/qutebrowser/browser/webview.py +++ b/qutebrowser/browser/webview.py @@ -106,7 +106,7 @@ class WebView(QWebView): self.keep_icon = False self.search_text = None self.search_flags = 0 - self.selection_enabled = False; + self.selection_enabled = False self.init_neighborlist() cfg = objreg.get('config') cfg.changed.connect(self.init_neighborlist) @@ -441,13 +441,14 @@ class WebView(QWebView): settings.setAttribute(QWebSettings.CaretBrowsingEnabled, True) self.selection_enabled = False - tabbed = objreg.get('tabbed-browser', scope='window', window=self.win_id) - if tabbed.currentWidget().tab_id == self.tab_id: + tabbed_browser = objreg.get('tabbed-browser', scope='window', + window=self.win_id) + if tabbed_browser.currentWidget().tab_id == self.tab_id: self.clearFocus() self.setFocus(Qt.OtherFocusReason) self.page().currentFrame().evaluateJavaScript( - utils.read_file('javascript/position_caret.js')) + utils.read_file('javascript/position_caret.js')) @pyqtSlot(usertypes.KeyMode) def on_mode_left(self, mode): @@ -463,7 +464,7 @@ class WebView(QWebView): # Remove selection if exist self.triggerPageAction(QWebPage.MoveToNextChar) settings.setAttribute(QWebSettings.CaretBrowsingEnabled, False) - self.selection_enabled = False; + self.selection_enabled = False self.setFocusPolicy(Qt.WheelFocus) diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 19e1e4da4..ddde678d0 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -822,7 +822,8 @@ def data(readonly=False): ('statusbar.bg.caret-selection', SettingValue(typ.QssColor(), '#a12dff'), - "Background color of the statusbar in caret selection enabled mode."), + "Background color of the statusbar in caret mode with a " + "selection"), ('statusbar.progress.bg', SettingValue(typ.QssColor(), 'white'), diff --git a/qutebrowser/keyinput/modeparsers.py b/qutebrowser/keyinput/modeparsers.py index 8ccdf6ad3..8a6165823 100644 --- a/qutebrowser/keyinput/modeparsers.py +++ b/qutebrowser/keyinput/modeparsers.py @@ -231,4 +231,3 @@ class CaretKeyParser(keyparser.CommandKeyParser): def __repr__(self): return utils.get_repr(self) - diff --git a/qutebrowser/mainwindow/statusbar/bar.py b/qutebrowser/mainwindow/statusbar/bar.py index aa655f671..503cc38c1 100644 --- a/qutebrowser/mainwindow/statusbar/bar.py +++ b/qutebrowser/mainwindow/statusbar/bar.py @@ -270,7 +270,7 @@ class StatusBar(QWidget): @pyqtProperty(bool) def caret_selection_active(self): - """Getter for self.caret_selection_active, so it can be used as Qt property.""" + """Getter for caret_selection_active, so it can be used as property.""" return self._caret_selection_active def _set_mode_active(self, mode, val): @@ -284,7 +284,8 @@ class StatusBar(QWidget): self._insert_active = val elif mode == usertypes.KeyMode.caret: log.statusbar.debug("Setting caret_active to {}".format(val)) - webview = objreg.get("tabbed-browser", scope="window", window=self._win_id).currentWidget() + webview = objreg.get("tabbed-browser", scope="window", + window=self._win_id).currentWidget() if val and webview.selection_enabled: self._set_mode_text("{} selection".format(mode.name)) self._caret_selection_active = val From 37050c49fc01417778af511d683acca08d8ccbfa Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 11 May 2015 20:33:16 +0200 Subject: [PATCH 031/182] Include .js files in MANIFEST. --- MANIFEST.in | 1 + 1 file changed, 1 insertion(+) diff --git a/MANIFEST.in b/MANIFEST.in index eabdf977b..9db0cdca0 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,6 +2,7 @@ global-exclude __pycache__ *.pyc *.pyo recursive-include qutebrowser/html *.html recursive-include qutebrowser/test *.py +recursive-include qutebrowser/javascript *.js graft icons graft scripts/pylint_checkers graft doc/img From 1f94e0fee63e67d2bba818ef8d64ea9b5779247e Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 11 May 2015 20:33:42 +0200 Subject: [PATCH 032/182] js: Remove obsolete argument to createTreeWalker. "createNodeIterator() and createTreeWalker() now have optional arguments and lack a fourth argument which is no longer relevant given entity references never made it into the DOM." --- qutebrowser/javascript/position_caret.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/javascript/position_caret.js b/qutebrowser/javascript/position_caret.js index a31d83c55..9e14b5b3c 100644 --- a/qutebrowser/javascript/position_caret.js +++ b/qutebrowser/javascript/position_caret.js @@ -64,7 +64,7 @@ function isElementInViewport(node) { return boundingRect.top >= -20; } -var walker = document.createTreeWalker(document.body, 4, null, false); +var walker = document.createTreeWalker(document.body, 4, null); var node; var textNodes = []; while (node = walker.nextNode()) { From 756aa3e16f0cc7fe5d7dbfcffef78b936a27ca75 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 11 May 2015 21:10:18 +0200 Subject: [PATCH 033/182] Fix tests because of new '0' key handling. --- tests/keyinput/test_basekeyparser.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/tests/keyinput/test_basekeyparser.py b/tests/keyinput/test_basekeyparser.py index 29d556ac5..d7f7b20a3 100644 --- a/tests/keyinput/test_basekeyparser.py +++ b/tests/keyinput/test_basekeyparser.py @@ -38,7 +38,8 @@ BINDINGS = {'test': {'': 'ctrla', 'a': 'a', 'ba': 'ba', 'ax': 'ax', - 'ccc': 'ccc'}, + 'ccc': 'ccc', + '0': '0'}, 'test2': {'foo': 'bar', '': 'ctrlx'}} @@ -189,6 +190,12 @@ class TestKeyChain: self.kp.execute.assert_called_once_with('ba', self.kp.Type.chain, None) assert self.kp._keystring == '' + def test_0(self, fake_keyevent_factory): + """Test with 0 keypress.""" + self.kp.handle(fake_keyevent_factory(Qt.Key_0, text='0')) + self.kp.execute.assert_called_once_with('0', self.kp.Type.chain, None) + assert self.kp._keystring == '' + def test_ambiguous_keychain(self, fake_keyevent_factory, mocker, stubs): """Test ambiguous keychain.""" mocker.patch('qutebrowser.keyinput.basekeyparser.config', @@ -240,7 +247,9 @@ class TestCount: self.kp.handle(fake_keyevent_factory(Qt.Key_0, text='0')) self.kp.handle(fake_keyevent_factory(Qt.Key_B, text='b')) self.kp.handle(fake_keyevent_factory(Qt.Key_A, text='a')) - self.kp.execute.assert_called_once_with('ba', self.kp.Type.chain, 0) + calls = [mock.call('0', self.kp.Type.chain, None), + mock.call('ba', self.kp.Type.chain, None)] + self.kp.execute.assert_has_calls(calls) assert self.kp._keystring == '' def test_count_42(self, fake_keyevent_factory): From 8b435ec88fcc663ab31f6ba140a60196b0d5ee98 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 11 May 2015 22:23:03 +0200 Subject: [PATCH 034/182] doc: Improve Arch install instructions. --- INSTALL.asciidoc | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/INSTALL.asciidoc b/INSTALL.asciidoc index a35e18e6a..5e615a594 100644 --- a/INSTALL.asciidoc +++ b/INSTALL.asciidoc @@ -84,13 +84,22 @@ There are two Archlinux packages available in the AUR: https://aur.archlinux.org/packages/qutebrowser/[qutebrowser] and https://aur.archlinux.org/packages/qutebrowser-git/[qutebrowser-git]. -You can install them like this: +You can install them (and the needed pypeg2 dependency) like this: ---- -$ mkdir qutebrowser -$ cd qutebrowser -$ wget https://aur.archlinux.org/packages/qu/qutebrowser-git/PKGBUILD +$ wget https://aur.archlinux.org/packages/py/python-pypeg2/python-pypeg2.tar.gz +$ tar xzf python-pypeg2.tar.gz +$ cd python-pypeg2 $ makepkg -si +$ cd .. +$ rm -r python-pypeg2 python-pypeg2.tar.gz + +$ wget https://aur.archlinux.org/packages/qu/qutebrowser/qutebrowser.tar.gz +$ tar xzf qutebrowser.tar.gz +$ cd qutebrowser +$ makepkg -si +$ cd .. +$ rm -r qutebrowser qutebrowser.tar.gz ---- or you could use an AUR helper, e.g. `yaourt -S qutebrowser-git`. From 418934644b9d4816fad1114b8106d31bc766ed5a Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 11 May 2015 22:29:44 +0200 Subject: [PATCH 035/182] Improve docstrings. --- qutebrowser/browser/commands.py | 73 ++++++++++++++++++++++++++------- 1 file changed, 59 insertions(+), 14 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index a8ea029b9..6f0abb3ad 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -1154,7 +1154,11 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', hide=True, modes=[KeyMode.caret], scope='window', count='count') def move_to_next_line(self, count=1): - """Move the cursor or select to the next line.""" + """Move the cursor or selection to the next line. + + Args: + count: How many lines to move. + """ webview = self._current_widget() if not webview.selection_enabled: act = QWebPage.MoveToNextLine @@ -1166,7 +1170,11 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', hide=True, modes=[KeyMode.caret], scope='window', count='count') def move_to_prev_line(self, count=1): - """Move the cursor or select to the prev line.""" + """Move the cursor or selection to the prev line. + + Args: + count: How many lines to move. + """ webview = self._current_widget() if not webview.selection_enabled: act = QWebPage.MoveToPreviousLine @@ -1178,7 +1186,11 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', hide=True, modes=[KeyMode.caret], scope='window', count='count') def move_to_next_char(self, count=1): - """Move the cursor or select to the next char.""" + """Move the cursor or selection to the next char. + + Args: + count: How many lines to move. + """ webview = self._current_widget() if not webview.selection_enabled: act = QWebPage.MoveToNextChar @@ -1190,7 +1202,11 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', hide=True, modes=[KeyMode.caret], scope='window', count='count') def move_to_prev_char(self, count=1): - """Move the cursor or select to the prev char.""" + """Move the cursor or selection to the previous char. + + Args: + count: How many chars to move. + """ webview = self._current_widget() if not webview.selection_enabled: act = QWebPage.MoveToPreviousChar @@ -1202,7 +1218,11 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', hide=True, modes=[KeyMode.caret], scope='window', count='count') def move_to_end_of_word(self, count=1): - """Move the cursor or select to the next word.""" + """Move the cursor or selection to the end of the word. + + Args: + count: How many words to move. + """ webview = self._current_widget() if not webview.selection_enabled: act = QWebPage.MoveToNextWord @@ -1214,7 +1234,11 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', hide=True, modes=[KeyMode.caret], scope='window', count='count') def move_to_next_word(self, count=1): - """Move the cursor or select to the next word.""" + """Move the cursor or selection to the next word. + + Args: + count: How many words to move. + """ webview = self._current_widget() if not webview.selection_enabled: act = [QWebPage.MoveToNextWord, QWebPage.MoveToNextChar] @@ -1227,7 +1251,11 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', hide=True, modes=[KeyMode.caret], scope='window', count='count') def move_to_prev_word(self, count=1): - """Move the cursor or select to the prev word.""" + """Move the cursor or selection to the previous word. + + Args: + count: How many words to move. + """ webview = self._current_widget() if not webview.selection_enabled: act = QWebPage.MoveToPreviousWord @@ -1263,7 +1291,11 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', hide=True, modes=[KeyMode.caret], scope='window', count='count') def move_to_start_of_next_block(self, count=1): - """Move the cursor or select to the start of next block.""" + """Move the cursor or selection to the start of next block. + + Args: + count: How many blocks to move. + """ webview = self._current_widget() if not webview.selection_enabled: act = [QWebPage.MoveToEndOfBlock, QWebPage.MoveToNextLine, @@ -1278,7 +1310,11 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', hide=True, modes=[KeyMode.caret], scope='window', count='count') def move_to_start_of_prev_block(self, count=1): - """Move the cursor or select to the start of previous block.""" + """Move the cursor or selection to the start of previous block. + + Args: + count: How many blocks to move. + """ webview = self._current_widget() if not webview.selection_enabled: act = [QWebPage.MoveToStartOfBlock, QWebPage.MoveToPreviousLine, @@ -1293,7 +1329,11 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', hide=True, modes=[KeyMode.caret], scope='window', count='count') def move_to_end_of_next_block(self, count=1): - """Move the cursor or select to the end of next block.""" + """Move the cursor or selection to the end of next block. + + Args: + count: How many blocks to move. + """ webview = self._current_widget() if not webview.selection_enabled: act = [QWebPage.MoveToEndOfBlock, QWebPage.MoveToNextLine, @@ -1308,7 +1348,11 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', hide=True, modes=[KeyMode.caret], scope='window', count='count') def move_to_end_of_prev_block(self, count=1): - """Move the cursor or select to the end of previous block.""" + """Move the cursor or selection to the end of previous block. + + Args: + count: How many blocks to move. + """ webview = self._current_widget() if not webview.selection_enabled: act = [QWebPage.MoveToStartOfBlock, QWebPage.MoveToPreviousLine, @@ -1323,7 +1367,8 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', hide=True, modes=[KeyMode.caret], scope='window') def move_to_start_of_document(self): - """Move the cursor or select to the start of document.""" + """Move the cursor or selection to the start of the document.""" + webview = self._current_widget() if not webview.selection_enabled: act = QWebPage.MoveToStartOfDocument @@ -1334,7 +1379,7 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', hide=True, modes=[KeyMode.caret], scope='window') def move_to_end_of_document(self): - """Move the cursor or select to the end of document.""" + """Move the cursor or selection to the end of the document.""" webview = self._current_widget() if not webview.selection_enabled: act = QWebPage.MoveToEndOfDocument @@ -1345,7 +1390,7 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', hide=True, modes=[KeyMode.caret], scope='window') def yank_selected(self, sel=False): - """Yank selected text to the clipboard or primary selection. + """Yank the selected text to the clipboard or primary selection. Args: sel: Use the primary selection instead of the clipboard. From f36a7444d72d038666f9c55bcaa01121c4aa9eb8 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 12 May 2015 06:53:13 +0200 Subject: [PATCH 036/182] js: Add .eslintrc. --- .eslintrc | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 .eslintrc diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 000000000..dcd5b3b8c --- /dev/null +++ b/.eslintrc @@ -0,0 +1,47 @@ +# vim: ft=yaml + +env: + browser: true + +rules: + block-scoped-var: 2 + complexity: [2, 5] + dot-location: 2 + default-case: 2 + guard-for-in: 2 + no-div-regex: 2 + no-param-reassign: 2 + no-eq-null: 2 + no-floating-decimal: 2 + no-self-compare: 2 + no-throw-literal: 2 + no-void: 2 + radix: 2 + wrap-iife: 2 + brace-style: [2, "1tbs", {"allowSingleLine": true}] + comma-style: [2, "last"] + consistent-this: [2, "self"] + func-names: 2 + func-style: [2, "declaration"] + indent: [2, 4, {"indentSwitchCase": true}] + linebreak-style: [2, "unix"] + max-nested-callbacks: [2, 3] + no-lonely-if: 2 + no-multiple-empty-lines: [2, {"max": 2}] + no-nested-ternary: 2 + no-unneeded-ternary: 2 + operator-assignment: [2, "always"] + operator-linebreak: [2, "after"] + space-after-keywords: [2, "always"] + space-before-blocks: [2, "always"] + space-before-function-paren: [2, {"anonymous": "always", "named": "never"}] + space-in-brackets: [2, "never"] + space-in-parens: [2, "never"] + space-unary-ops: [2, {"words": true, "nonwords": false}] + spaced-line-comment: [2, "always"] + max-depth: [2, 5] + max-len: [2, 79, 4] + max-params: [2, 5] + max-statements: [2, 20] + no-bitwise: 2 + no-reserved-keys: 2 From 7a67af24f0a988dbf1d3acbb79abf030834c9bdf Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 12 May 2015 07:06:55 +0200 Subject: [PATCH 037/182] js: Fix some lint. --- .eslintrc | 4 +-- qutebrowser/javascript/position_caret.js | 46 ++++++++++++++++-------- 2 files changed, 34 insertions(+), 16 deletions(-) diff --git a/.eslintrc b/.eslintrc index dcd5b3b8c..2eec935a7 100644 --- a/.eslintrc +++ b/.eslintrc @@ -5,7 +5,6 @@ env: rules: block-scoped-var: 2 - complexity: [2, 5] dot-location: 2 default-case: 2 guard-for-in: 2 @@ -42,6 +41,7 @@ rules: max-depth: [2, 5] max-len: [2, 79, 4] max-params: [2, 5] - max-statements: [2, 20] + max-statements: [2, 30] no-bitwise: 2 no-reserved-keys: 2 + global-strict: 0 diff --git a/qutebrowser/javascript/position_caret.js b/qutebrowser/javascript/position_caret.js index 9e14b5b3c..36bf0451f 100644 --- a/qutebrowser/javascript/position_caret.js +++ b/qutebrowser/javascript/position_caret.js @@ -1,40 +1,53 @@ /** -* Copyright 2014-2015 Florian Bruhin (The Compiler) -* +* Copyright 2015 Artur Shaik +* Copyright 2015 Florian Bruhin (The Compiler) +* * This file is part of qutebrowser. -* +* * qutebrowser is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. -* +* * qutebrowser is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. -* +* * You should have received a copy of the GNU General Public License * along with qutebrowser. If not, see . */ +/* eslint-disable max-len */ + /** - * Snippet to position caret at top of the page when caret mode enabled. - * Some code was borrowed from https://github.com/1995eaton/chromium-vim/blob/master/content_scripts/dom.js - * and https://github.com/1995eaton/chromium-vim/blob/master/content_scripts/visual.js + * Snippet to position caret at top of the page when caret mode is enabled. + * Some code was borrowed from: + * + * https://github.com/1995eaton/chromium-vim/blob/master/content_scripts/dom.js + * https://github.com/1995eaton/chromium-vim/blob/master/content_scripts/visual.js */ +/* eslint-enable max-len */ + +"use strict"; + function isElementInViewport(node) { var i; - var boundingRect = node.getClientRects()[0] || node.getBoundingClientRect(); + var boundingRect = (node.getClientRects()[0] || + node.getBoundingClientRect()); if (boundingRect.width <= 1 && boundingRect.height <= 1) { var rects = node.getClientRects(); for (i = 0; i < rects.length; i++) { - if (rects[i].width > rects[0].height && rects[i].height > rects[0].height) { + if (rects[i].width > rects[0].height && + rects[i].height > rects[0].height) { boundingRect = rects[i]; } } } - if (boundingRect === void 0) return null; + if (boundingRect === undefined) { + return null; + } if (boundingRect.top > innerHeight || boundingRect.left > innerWidth) { return null; } @@ -42,15 +55,19 @@ function isElementInViewport(node) { var children = node.children; var visibleChildNode = false; for (i = 0, l = children.length; i < l; ++i) { - boundingRect = children[i].getClientRects()[0] || children[i].getBoundingClientRect(); + boundingRect = (children[i].getClientRects()[0] || + children[i].getBoundingClientRect()); if (boundingRect.width > 1 && boundingRect.height > 1) { visibleChildNode = true; break; } } - if (visibleChildNode === false) return null; + if (visibleChildNode === false) { + return null; + } } - if (boundingRect.top + boundingRect.height < 10 || boundingRect.left + boundingRect.width < -10) { + if (boundingRect.top + boundingRect.height < 10 || + boundingRect.left + boundingRect.width < -10) { return null; } var computedStyle = window.getComputedStyle(node, null); @@ -64,6 +81,7 @@ function isElementInViewport(node) { return boundingRect.top >= -20; } + var walker = document.createTreeWalker(document.body, 4, null); var node; var textNodes = []; From 3f21ac6b6a1ab367f79c78b979ae392986d7af0c Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 12 May 2015 07:10:08 +0200 Subject: [PATCH 038/182] js: Use an IIFE. --- .eslintrc | 2 +- qutebrowser/javascript/position_caret.js | 45 ++++++++++++------------ 2 files changed, 24 insertions(+), 23 deletions(-) diff --git a/.eslintrc b/.eslintrc index 2eec935a7..ab23d427e 100644 --- a/.eslintrc +++ b/.eslintrc @@ -16,7 +16,7 @@ rules: no-throw-literal: 2 no-void: 2 radix: 2 - wrap-iife: 2 + wrap-iife: [2, "inside"] brace-style: [2, "1tbs", {"allowSingleLine": true}] comma-style: [2, "last"] consistent-this: [2, "self"] diff --git a/qutebrowser/javascript/position_caret.js b/qutebrowser/javascript/position_caret.js index 36bf0451f..62e610245 100644 --- a/qutebrowser/javascript/position_caret.js +++ b/qutebrowser/javascript/position_caret.js @@ -81,27 +81,28 @@ function isElementInViewport(node) { return boundingRect.top >= -20; } - -var walker = document.createTreeWalker(document.body, 4, null); -var node; -var textNodes = []; -while (node = walker.nextNode()) { - if (node.nodeType === 3 && node.data.trim() !== '') { - textNodes.push(node); +(function() { + var walker = document.createTreeWalker(document.body, 4, null); + var node; + var textNodes = []; + while (node = walker.nextNode()) { + if (node.nodeType === 3 && node.data.trim() !== '') { + textNodes.push(node); + } } -} -for (var i = 0; i < textNodes.length; i++) { - var element = textNodes[i].parentElement; - if (isElementInViewport(element.parentElement)) { - el = element; - break; + for (var i = 0; i < textNodes.length; i++) { + var element = textNodes[i].parentElement; + if (isElementInViewport(element.parentElement)) { + el = element; + break; + } } -} -if (el !== undefined) { - var range = document.createRange(); - range.setStart(el, 0); - range.setEnd(el, 0); - var sel = window.getSelection(); - sel.removeAllRanges(); - sel.addRange(range); -} + if (el !== undefined) { + var range = document.createRange(); + range.setStart(el, 0); + range.setEnd(el, 0); + var sel = window.getSelection(); + sel.removeAllRanges(); + sel.addRange(range); + } +})(); From d1e88c5e8db02ee3a66d0d4b51cc231a0a04c8e7 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 12 May 2015 07:11:45 +0200 Subject: [PATCH 039/182] js: Add 'var'. --- qutebrowser/javascript/position_caret.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/qutebrowser/javascript/position_caret.js b/qutebrowser/javascript/position_caret.js index 62e610245..d3cdbf43a 100644 --- a/qutebrowser/javascript/position_caret.js +++ b/qutebrowser/javascript/position_caret.js @@ -54,7 +54,8 @@ function isElementInViewport(node) { if (boundingRect.width <= 1 || boundingRect.height <= 1) { var children = node.children; var visibleChildNode = false; - for (i = 0, l = children.length; i < l; ++i) { + var l = children.length; + for (i = 0; i < l; ++i) { boundingRect = (children[i].getClientRects()[0] || children[i].getBoundingClientRect()); if (boundingRect.width > 1 && boundingRect.height > 1) { @@ -85,6 +86,7 @@ function isElementInViewport(node) { var walker = document.createTreeWalker(document.body, 4, null); var node; var textNodes = []; + var el; while (node = walker.nextNode()) { if (node.nodeType === 3 && node.data.trim() !== '') { textNodes.push(node); From 28ec7b4698e49e430a54406876451374618701ac Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 12 May 2015 07:14:10 +0200 Subject: [PATCH 040/182] js: Fix radix parameters. --- qutebrowser/javascript/position_caret.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qutebrowser/javascript/position_caret.js b/qutebrowser/javascript/position_caret.js index d3cdbf43a..a69156713 100644 --- a/qutebrowser/javascript/position_caret.js +++ b/qutebrowser/javascript/position_caret.js @@ -75,8 +75,8 @@ function isElementInViewport(node) { if (computedStyle.visibility !== 'visible' || computedStyle.display === 'none' || node.hasAttribute('disabled') || - parseInt(computedStyle.width, '10') === 0 || - parseInt(computedStyle.height, '10') === 0) { + parseInt(computedStyle.width, 10) === 0 || + parseInt(computedStyle.height, 10) === 0) { return null; } return boundingRect.top >= -20; From a96065861718eb887bab498992253dcad44fe4b2 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 12 May 2015 07:15:37 +0200 Subject: [PATCH 041/182] js: Fix more lint. --- .eslintrc | 4 ++-- qutebrowser/javascript/position_caret.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.eslintrc b/.eslintrc index ab23d427e..bef2e9c1a 100644 --- a/.eslintrc +++ b/.eslintrc @@ -20,7 +20,6 @@ rules: brace-style: [2, "1tbs", {"allowSingleLine": true}] comma-style: [2, "last"] consistent-this: [2, "self"] - func-names: 2 func-style: [2, "declaration"] indent: [2, 4, {"indentSwitchCase": true}] linebreak-style: [2, "unix"] @@ -33,7 +32,7 @@ rules: operator-linebreak: [2, "after"] space-after-keywords: [2, "always"] space-before-blocks: [2, "always"] - space-before-function-paren: [2, {"anonymous": "always", "named": "never"}] + space-before-function-paren: [2, {"anonymous": "never", "named": "never"}] space-in-brackets: [2, "never"] space-in-parens: [2, "never"] space-unary-ops: [2, {"words": true, "nonwords": false}] @@ -45,3 +44,4 @@ rules: no-bitwise: 2 no-reserved-keys: 2 global-strict: 0 + quotes: 0 diff --git a/qutebrowser/javascript/position_caret.js b/qutebrowser/javascript/position_caret.js index a69156713..5ffd882de 100644 --- a/qutebrowser/javascript/position_caret.js +++ b/qutebrowser/javascript/position_caret.js @@ -87,7 +87,7 @@ function isElementInViewport(node) { var node; var textNodes = []; var el; - while (node = walker.nextNode()) { + while ((node = walker.nextNode())) { if (node.nodeType === 3 && node.data.trim() !== '') { textNodes.push(node); } From 785f948bc71b46a9f3707b37ce42da25b1470924 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 12 May 2015 07:49:53 +0200 Subject: [PATCH 042/182] Correct typo. --- misc/userscripts/dmenu_qutebrowser | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/userscripts/dmenu_qutebrowser b/misc/userscripts/dmenu_qutebrowser index 36651e77a..fbb70ca51 100755 --- a/misc/userscripts/dmenu_qutebrowser +++ b/misc/userscripts/dmenu_qutebrowser @@ -28,7 +28,7 @@ # Use the hotkey to open in new tab/window, press 'o' to open URL in current tab/window # You can simulate "go" by pressing "o", as the current URL is always first in the list # -# I personally use "o" to launchq this script. For me, my workflow is: +# I personally use "o" to launch this script. For me, my workflow is: # Default keys Keys with this script # O o # o o From 3cb756699f3eb17d620f40a63dba7ee267cc0edb Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 12 May 2015 07:50:03 +0200 Subject: [PATCH 043/182] Regenerate authors. --- README.asciidoc | 1 + 1 file changed, 1 insertion(+) diff --git a/README.asciidoc b/README.asciidoc index ec9524020..7b23a0451 100644 --- a/README.asciidoc +++ b/README.asciidoc @@ -142,6 +142,7 @@ Contributors, sorted by the number of commits in descending order: * Peter Vilim * John ShaggyTwoDope Jenkins * Jimmy +* Zach-Button * rikn00 * Patric Schmitz * Martin Zimmermann From c88393ccfd0fba4b611e4b34063a72ae58ee9739 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 12 May 2015 09:02:17 +0200 Subject: [PATCH 044/182] Add minimal key tester script. See #658. --- scripts/keytester.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 scripts/keytester.py diff --git a/scripts/keytester.py b/scripts/keytester.py new file mode 100644 index 000000000..effcdd468 --- /dev/null +++ b/scripts/keytester.py @@ -0,0 +1,23 @@ +from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QHBoxLayout + +from qutebrowser.utils import utils + + +class KeyWidget(QWidget): + + """Widget displaying key presses.""" + + def __init__(self, parent=None): + super().__init__(parent) + self._layout = QHBoxLayout(self) + self._label = QLabel(text="Waiting for keypress...") + self._layout.addWidget(self._label) + + def keyPressEvent(self, e): + self._label.setText(utils.keyevent_to_string(e)) + + +app = QApplication([]) +w = KeyWidget() +w.show() +app.exec_() From ae512f451e6f4c3ff1c09512d8ff82d1ed313def Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 12 May 2015 09:10:02 +0200 Subject: [PATCH 045/182] Fix lint. --- scripts/keytester.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/scripts/keytester.py b/scripts/keytester.py index effcdd468..5322848f0 100644 --- a/scripts/keytester.py +++ b/scripts/keytester.py @@ -1,3 +1,28 @@ +#!/usr/bin/env python3 +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2014-2015 Florian Bruhin (The Compiler) + +# This file is part of qutebrowser. +# +# qutebrowser is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# qutebrowser is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with qutebrowser. If not, see . + +"""Small test script to show key presses. + +Use python3 -m scripts.keytester to launch it. +""" + from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QHBoxLayout from qutebrowser.utils import utils From aa2e5a35d6f73efd1380dfda424d8a0170e41285 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 12 May 2015 17:05:01 +0200 Subject: [PATCH 046/182] Add javascript tests for position_caret.js. --- tests/javascript/base.html | 23 ++++ tests/javascript/conftest.py | 109 ++++++++++++++++++ .../position_caret/scrolled_down.html | 8 ++ tests/javascript/position_caret/simple.html | 4 + .../position_caret/test_position_caret.py | 78 +++++++++++++ 5 files changed, 222 insertions(+) create mode 100644 tests/javascript/base.html create mode 100644 tests/javascript/conftest.py create mode 100644 tests/javascript/position_caret/scrolled_down.html create mode 100644 tests/javascript/position_caret/simple.html create mode 100644 tests/javascript/position_caret/test_position_caret.py diff --git a/tests/javascript/base.html b/tests/javascript/base.html new file mode 100644 index 000000000..95085ff37 --- /dev/null +++ b/tests/javascript/base.html @@ -0,0 +1,23 @@ + + + + + + + qutebrowser javascript test + + + + {% block content %} + {% endblock %} + + diff --git a/tests/javascript/conftest.py b/tests/javascript/conftest.py new file mode 100644 index 000000000..dd623c9a4 --- /dev/null +++ b/tests/javascript/conftest.py @@ -0,0 +1,109 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2015 Florian Bruhin (The Compiler) +# +# This file is part of qutebrowser. +# +# qutebrowser is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# qutebrowser is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with qutebrowser. If not, see . + +"""pylint conftest file for javascript test.""" + +import os +import os.path + +import pytest +import jinja2 +from PyQt5.QtWebKit import QWebSettings +from PyQt5.QtWebKitWidgets import QWebView + +import qutebrowser + + +class JSTester: + + """Object returned by js_tester which provides test data and a webview. + + Attributes: + webview: The webview which is used. + _qtbot: The QtBot fixture from pytest-qt. + _jinja_env: The jinja2 environment used to get templates. + """ + + def __init__(self, webview, qtbot): + self.webview = webview + self._qtbot = qtbot + loader = jinja2.FileSystemLoader(os.path.dirname(__file__)) + self._jinja_env = jinja2.Environment(loader=loader, autoescape=True) + + def scroll_anchor(self, name): + """Scroll the main frame to the given anchor.""" + page = self.webview.page() + with self._qtbot.waitSignal(page.scrollRequested): + page.mainFrame().scrollToAnchor(name) + + def load(self, path): + """Load and display the given test data. + + Args: + path: The path to the test file, relative to the javascript/ + folder. + """ + template = self._jinja_env.get_template(path) + with self._qtbot.waitSignal(self.webview.loadFinished): + self.webview.setHtml(template.render()) + + def run_file(self, filename): + """Run a javascript file. + + Args: + filename: The javascript filename, relative to + qutebrowser/javascript. + + Return: + The javascript return value. + """ + base_path = os.path.join(os.path.dirname(qutebrowser.__file__), + 'javascript') + full_path = os.path.join(base_path, filename) + with open(full_path, 'r', encoding='utf-8') as f: + source = f.read() + return self.run(source) + + def run(self, source): + """Run the given javascript source. + + Args: + source: The source to run as a string. + + Return: + The javascript return value. + """ + assert self.webview.settings().testAttribute( + QWebSettings.JavascriptEnabled) + return self.webview.page().mainFrame().evaluateJavaScript(source) + + +@pytest.fixture +def js_tester(qtbot): + """Fixture to test javascript snippets. + + Provides a QWebView with a 640x480px size and a JSTester instance. + + Args: + qtbot: pytestqt.plugin.QtBot fixture. + """ + webview = QWebView() + qtbot.add_widget(webview) + webview.resize(640, 480) + return JSTester(webview, qtbot) diff --git a/tests/javascript/position_caret/scrolled_down.html b/tests/javascript/position_caret/scrolled_down.html new file mode 100644 index 000000000..1f4f5d8ff --- /dev/null +++ b/tests/javascript/position_caret/scrolled_down.html @@ -0,0 +1,8 @@ +{% extends "base.html" %} +{% block content %} +

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer nec odio. Praesent libero. Sed cursus ante dapibus diam. Sed nisi. Nulla quis sem at nibh elementum imperdiet. Duis sagittis ipsum. Praesent mauris. Fusce nec tellus sed augue semper porta. Mauris massa. Vestibulum lacinia arcu eget nulla. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Curabitur sodales ligula in libero.

+ +

MARKER this should be the paragraph the caret is on.

+ +

Some more text

+{% endblock %} diff --git a/tests/javascript/position_caret/simple.html b/tests/javascript/position_caret/simple.html new file mode 100644 index 000000000..048ef0e11 --- /dev/null +++ b/tests/javascript/position_caret/simple.html @@ -0,0 +1,4 @@ +{% extends "base.html" %} +{% block content %} +

MARKER this should be the paragraph the caret is on.

+{% endblock %} diff --git a/tests/javascript/position_caret/test_position_caret.py b/tests/javascript/position_caret/test_position_caret.py new file mode 100644 index 000000000..de23cda46 --- /dev/null +++ b/tests/javascript/position_caret/test_position_caret.py @@ -0,0 +1,78 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2015 Florian Bruhin (The Compiler) +# +# This file is part of qutebrowser. +# +# qutebrowser is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# qutebrowser is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with qutebrowser. If not, see . + +"""Tests for position_caret.js.""" + +import pytest + +from PyQt5.QtCore import Qt +from PyQt5.QtWebKit import QWebSettings +from PyQt5.QtWebKitWidgets import QWebPage + + +@pytest.fixture(autouse=True) +def enable_caret_browsing(): + """Fixture to enable caret browsing globally.""" + QWebSettings.globalSettings().setAttribute( + QWebSettings.CaretBrowsingEnabled, True) + + +class CaretTester: + + """Helper class (for the caret_tester fixture) for asserts. + + Attributes: + js: The js_tester fixture. + """ + + def __init__(self, js_tester): + self.js = js_tester + + def check(self): + """Check whether the caret is before the MARKER text.""" + self.js.run_file('position_caret.js') + self.js.webview.triggerPageAction(QWebPage.SelectNextWord) + assert self.js.webview.selectedText() == "MARKER" + + def check_scrolled(self): + """Check if the page is scrolled down.""" + frame = self.js.webview.page().mainFrame() + minimum = frame.scrollBarMinimum(Qt.Vertical) + value = frame.scrollBarValue(Qt.Vertical) + assert value > minimum + + +@pytest.fixture +def caret_tester(js_tester): + """Helper fixture to test caret browsing positions.""" + return CaretTester(js_tester) + + +def test_simple(caret_tester): + """Test with a simple (one-line) HTML text.""" + caret_tester.js.load('position_caret/simple.html') + caret_tester.check() + + +def test_scrolled_down(caret_tester): + """Test with multiple text blocks with the viewport scrolled down.""" + caret_tester.js.load('position_caret/scrolled_down.html') + caret_tester.js.scroll_anchor('anchor') + caret_tester.check_scrolled() + caret_tester.check() From 27a34d5499d5365859213be80a142a50464e1ba5 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 12 May 2015 17:32:33 +0200 Subject: [PATCH 047/182] Close anchor. --- tests/javascript/position_caret/scrolled_down.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/javascript/position_caret/scrolled_down.html b/tests/javascript/position_caret/scrolled_down.html index 1f4f5d8ff..c517acff1 100644 --- a/tests/javascript/position_caret/scrolled_down.html +++ b/tests/javascript/position_caret/scrolled_down.html @@ -2,7 +2,7 @@ {% block content %}

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer nec odio. Praesent libero. Sed cursus ante dapibus diam. Sed nisi. Nulla quis sem at nibh elementum imperdiet. Duis sagittis ipsum. Praesent mauris. Fusce nec tellus sed augue semper porta. Mauris massa. Vestibulum lacinia arcu eget nulla. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Curabitur sodales ligula in libero.

-

MARKER this should be the paragraph the caret is on.

+

MARKER this should be the paragraph the caret is on.

Some more text

{% endblock %} From 2b440bc8dbe97b7e1d1d9c9ad48cc2c5e17f78ea Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 12 May 2015 17:44:06 +0200 Subject: [PATCH 048/182] Handle QWebPage javascript methods. --- tests/javascript/conftest.py | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/tests/javascript/conftest.py b/tests/javascript/conftest.py index dd623c9a4..cd1a2c69b 100644 --- a/tests/javascript/conftest.py +++ b/tests/javascript/conftest.py @@ -21,15 +21,45 @@ import os import os.path +import logging import pytest import jinja2 from PyQt5.QtWebKit import QWebSettings -from PyQt5.QtWebKitWidgets import QWebView +from PyQt5.QtWebKitWidgets import QWebView, QWebPage import qutebrowser +class TestWebPage(QWebPage): + + """QWebPage subclass which overrides some test methods. + + Attributes: + _logger: The logger used for alerts. + """ + + def __init__(self, parent=None): + super().__init__(parent) + self._logger = logging.getLogger('js-tests') + + def javaScriptAlert(self, _frame, msg): + """Log javascript alerts.""" + self._logger.info("js alert: {}".format(msg)) + + def javaScriptConfirm(self, _frame, msg): + """Fail tests on js confirm() as that should never happen.""" + pytest.fail("js confirm: {}".format(msg)) + + def javaScriptPrompt(self, _frame, msg, _default): + """Fail tests on js prompt() as that should never happen.""" + pytest.fail("js prompt: {}".format(msg)) + + def javaScriptConsoleMessage(self, msg, line, source): + """Fail tests on js console messages as they're used for errors.""" + pytest.fail("js console ({}:{}): {}".format(source, line, msg)) + + class JSTester: """Object returned by js_tester which provides test data and a webview. @@ -42,6 +72,7 @@ class JSTester: def __init__(self, webview, qtbot): self.webview = webview + self.webview.setPage(TestWebPage(self.webview)) self._qtbot = qtbot loader = jinja2.FileSystemLoader(os.path.dirname(__file__)) self._jinja_env = jinja2.Environment(loader=loader, autoescape=True) From 54ae6a63eedf292dec9b602194d005196ac7a90a Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 12 May 2015 17:58:53 +0200 Subject: [PATCH 049/182] Fix lint. --- MANIFEST.in | 1 + qutebrowser/browser/commands.py | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index 9db0cdca0..7ecd44de2 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -30,4 +30,5 @@ exclude qutebrowser.rcc exclude .coveragerc exclude .flake8 exclude .pylintrc +exclude .eslintrc exclude doc/help diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 6f0abb3ad..0fcc91268 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -1368,7 +1368,6 @@ class CommandDispatcher: modes=[KeyMode.caret], scope='window') def move_to_start_of_document(self): """Move the cursor or selection to the start of the document.""" - webview = self._current_widget() if not webview.selection_enabled: act = QWebPage.MoveToStartOfDocument From 7edfdaa2718bdcc957f3f07b969cd07e8dec2050 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 12 May 2015 19:08:54 +0200 Subject: [PATCH 050/182] Add test for invisible elements. --- tests/javascript/conftest.py | 5 +++-- tests/javascript/position_caret/test_position_caret.py | 7 +++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/tests/javascript/conftest.py b/tests/javascript/conftest.py index cd1a2c69b..d97b38625 100644 --- a/tests/javascript/conftest.py +++ b/tests/javascript/conftest.py @@ -83,16 +83,17 @@ class JSTester: with self._qtbot.waitSignal(page.scrollRequested): page.mainFrame().scrollToAnchor(name) - def load(self, path): + def load(self, path, **kwargs): """Load and display the given test data. Args: path: The path to the test file, relative to the javascript/ folder. + **kwargs: Passed to jinja's template.render(). """ template = self._jinja_env.get_template(path) with self._qtbot.waitSignal(self.webview.loadFinished): - self.webview.setHtml(template.render()) + self.webview.setHtml(template.render(**kwargs)) def run_file(self, filename): """Run a javascript file. diff --git a/tests/javascript/position_caret/test_position_caret.py b/tests/javascript/position_caret/test_position_caret.py index de23cda46..98976ea3d 100644 --- a/tests/javascript/position_caret/test_position_caret.py +++ b/tests/javascript/position_caret/test_position_caret.py @@ -76,3 +76,10 @@ def test_scrolled_down(caret_tester): caret_tester.js.scroll_anchor('anchor') caret_tester.check_scrolled() caret_tester.check() + + +@pytest.mark.parametrize('style', ['visibility: hidden', 'display: none']) +def test_invisible(caret_tester, style): + """Test with hidden text elements.""" + caret_tester.js.load('position_caret/invisible.html', style=style) + caret_tester.check() From 2775f2b2eefffb4a7d20c26f920caa1c86d30a57 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 12 May 2015 19:15:16 +0200 Subject: [PATCH 051/182] Add some more tests. --- tests/javascript/position_caret/invisible.html | 5 +++++ tests/javascript/position_caret/scrolled_down_img.html | 9 +++++++++ tests/javascript/position_caret/test_position_caret.py | 9 +++++++++ 3 files changed, 23 insertions(+) create mode 100644 tests/javascript/position_caret/invisible.html create mode 100644 tests/javascript/position_caret/scrolled_down_img.html diff --git a/tests/javascript/position_caret/invisible.html b/tests/javascript/position_caret/invisible.html new file mode 100644 index 000000000..764b3ddb5 --- /dev/null +++ b/tests/javascript/position_caret/invisible.html @@ -0,0 +1,5 @@ +{% extends "base.html" %} +{% block content %} +

This line is hidden.

+

MARKER this should be the paragraph the caret is on.

+{% endblock %} diff --git a/tests/javascript/position_caret/scrolled_down_img.html b/tests/javascript/position_caret/scrolled_down_img.html new file mode 100644 index 000000000..af302fcc0 --- /dev/null +++ b/tests/javascript/position_caret/scrolled_down_img.html @@ -0,0 +1,9 @@ +{% extends "base.html" %} +{% block content %} +

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer nec odio. Praesent libero. Sed cursus ante dapibus diam. Sed nisi. Nulla quis sem at nibh elementum imperdiet. Duis sagittis ipsum. Praesent mauris. Fusce nec tellus sed augue semper porta. Mauris massa. Vestibulum lacinia arcu eget nulla. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Curabitur sodales ligula in libero.

+ +
+ +

MARKER this should be the paragraph the caret is on.

+

Some more text

+{% endblock %} diff --git a/tests/javascript/position_caret/test_position_caret.py b/tests/javascript/position_caret/test_position_caret.py index 98976ea3d..0fbf4e3f1 100644 --- a/tests/javascript/position_caret/test_position_caret.py +++ b/tests/javascript/position_caret/test_position_caret.py @@ -83,3 +83,12 @@ def test_invisible(caret_tester, style): """Test with hidden text elements.""" caret_tester.js.load('position_caret/invisible.html', style=style) caret_tester.check() + + +def test_scrolled_down_img(caret_tester): + """Test with an image at the top with the viewport scrolled down.""" + caret_tester.js.load('position_caret/scrolled_down_img.html') + caret_tester.js.scroll_anchor('anchor') + caret_tester.check_scrolled() + caret_tester.check() + From e62ba57291abd66b65a22e6037f14b6aa1d92928 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 12 May 2015 21:04:18 +0200 Subject: [PATCH 052/182] Always save last window session. len(objreg.window_registry) can actually lag behind because single-shot QTimers are used to remove the windows from the registry - but actually it doesn't even matter if this is the last window or not. We just always save to SessionManager._last_window_session, and it gets used in SessionManager.save. Fixes #650. --- qutebrowser/mainwindow/mainwindow.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/qutebrowser/mainwindow/mainwindow.py b/qutebrowser/mainwindow/mainwindow.py index 6bfae1589..846020b86 100644 --- a/qutebrowser/mainwindow/mainwindow.py +++ b/qutebrowser/mainwindow/mainwindow.py @@ -416,8 +416,7 @@ class MainWindow(QWidget): e.ignore() return e.accept() - if len(objreg.window_registry) == 1: - objreg.get('session-manager').save_last_window_session() + objreg.get('session-manager').save_last_window_session() self._save_geometry() log.destroy.debug("Closing window {}".format(self.win_id)) self._tabbed_browser.shutdown() From 9fde38d96af118c9f1b3541b3485352a8a43f4ae Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 13 May 2015 06:31:48 +0200 Subject: [PATCH 053/182] Reset CaretBrowsingEnabled to original value. --- tests/javascript/position_caret/test_position_caret.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/javascript/position_caret/test_position_caret.py b/tests/javascript/position_caret/test_position_caret.py index 0fbf4e3f1..6fdfef11e 100644 --- a/tests/javascript/position_caret/test_position_caret.py +++ b/tests/javascript/position_caret/test_position_caret.py @@ -26,11 +26,14 @@ from PyQt5.QtWebKit import QWebSettings from PyQt5.QtWebKitWidgets import QWebPage -@pytest.fixture(autouse=True) +@pytest.yield_fixture(autouse=True) def enable_caret_browsing(): """Fixture to enable caret browsing globally.""" - QWebSettings.globalSettings().setAttribute( - QWebSettings.CaretBrowsingEnabled, True) + settings = QWebSettings.globalSettings() + old_value = settings.testAttribute(QWebSettings.CaretBrowsingEnabled) + settings.setAttribute(QWebSettings.CaretBrowsingEnabled, True) + yield + settings.setAttribute(QWebSettings.CaretBrowsingEnabled, old_value) class CaretTester: From e35d284282d2a061e882a900511d68a0bfd3dbe5 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 13 May 2015 06:32:09 +0200 Subject: [PATCH 054/182] Remove blank line. --- tests/javascript/position_caret/test_position_caret.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/javascript/position_caret/test_position_caret.py b/tests/javascript/position_caret/test_position_caret.py index 6fdfef11e..a44cfc87d 100644 --- a/tests/javascript/position_caret/test_position_caret.py +++ b/tests/javascript/position_caret/test_position_caret.py @@ -94,4 +94,3 @@ def test_scrolled_down_img(caret_tester): caret_tester.js.scroll_anchor('anchor') caret_tester.check_scrolled() caret_tester.check() - From dd0e230a321b4cda567a0528df71c2dbf153dfbe Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 13 May 2015 07:01:48 +0200 Subject: [PATCH 055/182] Re-add v keybinding for toggle-selection. See #653. --- 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 ddde678d0..1020493c2 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -1261,7 +1261,7 @@ KEY_DATA = collections.OrderedDict([ ])), ('caret', collections.OrderedDict([ - ('toggle-selection', ['']), + ('toggle-selection', ['v', '']), ('drop-selection', ['']), ('enter-mode normal', ['c']), ('move-to-next-line', ['j']), From ce1b82616d8da873889898de5bc5f347e8de8dd6 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 13 May 2015 07:17:05 +0200 Subject: [PATCH 056/182] Fix spelling. --- qutebrowser/browser/webview.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/browser/webview.py b/qutebrowser/browser/webview.py index 684ce8e3d..b26045c55 100644 --- a/qutebrowser/browser/webview.py +++ b/qutebrowser/browser/webview.py @@ -461,7 +461,7 @@ class WebView(QWebView): settings = self.settings() if settings.testAttribute(QWebSettings.CaretBrowsingEnabled): if self.selection_enabled and self.hasSelection(): - # Remove selection if exist + # Remove selection if it exists self.triggerPageAction(QWebPage.MoveToNextChar) settings.setAttribute(QWebSettings.CaretBrowsingEnabled, False) self.selection_enabled = False From 88fc1864021aa75188a961194e5debace2c2f3ba Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 13 May 2015 07:29:17 +0200 Subject: [PATCH 057/182] Add tmux-like Enter binding. See #653. --- qutebrowser/browser/commands.py | 6 +++++- qutebrowser/config/configdata.py | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 0fcc91268..e5f646dcb 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -38,6 +38,7 @@ import pygments.formatters from qutebrowser.commands import userscripts, cmdexc, cmdutils from qutebrowser.config import config, configexc from qutebrowser.browser import webelem, inspector +from qutebrowser.keyinput import modeman from qutebrowser.utils import (message, usertypes, log, qtutils, urlutils, objreg, utils) from qutebrowser.utils.usertypes import KeyMode @@ -1388,11 +1389,12 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', hide=True, modes=[KeyMode.caret], scope='window') - def yank_selected(self, sel=False): + def yank_selected(self, sel=False, leave=False): """Yank the selected text to the clipboard or primary selection. Args: sel: Use the primary selection instead of the clipboard. + leave: If given, leave visual mode after yanking. """ s = self._current_widget().selectedText() if not self._current_widget().hasSelection() or len(s) == 0: @@ -1410,6 +1412,8 @@ class CommandDispatcher: clipboard.setText(s, mode) message.info(self._win_id, "{} {} yanked to {}".format( len(s), "char" if len(s) == 1 else "chars", target)) + if leave: + modeman.leave(self._win_id, KeyMode.caret, "yank selected") @cmdutils.register(instance='command-dispatcher', hide=True, modes=[KeyMode.caret], scope='window') diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 1020493c2..6fe88b5e1 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -1281,6 +1281,7 @@ KEY_DATA = collections.OrderedDict([ ('move-to-end-of-document', ['G']), ('yank-selected', ['y']), ('yank-selected -p', ['Y']), + ('yank-selected --leave', ['', '']), ('scroll -50 0', ['H']), ('scroll 0 50', ['J']), ('scroll 0 -50', ['K']), From a74a9c8a215615d9e900054dc7b20c716303b7ce Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 13 May 2015 07:54:06 +0200 Subject: [PATCH 058/182] Fix adding of new default section to keyconf. When trying to add a new binding with multiple values, the bindings were added immediately and the next _is_new() check returned False because the command was already bound. With this change, the new bindings first get added to a temporary dict so _is_new() returns the correct result. See #653. --- qutebrowser/config/parsers/keyconf.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/qutebrowser/config/parsers/keyconf.py b/qutebrowser/config/parsers/keyconf.py index b15a9ae4a..059ea5cdc 100644 --- a/qutebrowser/config/parsers/keyconf.py +++ b/qutebrowser/config/parsers/keyconf.py @@ -237,20 +237,31 @@ class KeyConfigParser(QObject): only_new: If set, only keybindings which are completely unused (same command/key not bound) are added. """ + + # {'sectname': {'keychain1': 'command', 'keychain2': 'command'}, ...} + bindings_to_add = collections.OrderedDict() + for sectname, sect in configdata.KEY_DATA.items(): sectname = self._normalize_sectname(sectname) + bindings_to_add[sectname] = collections.OrderedDict() + for command, keychains in sect.items(): + for e in keychains: + if not only_new or self._is_new(sectname, command, e): + assert e not in bindings_to_add[sectname] + bindings_to_add[sectname][e] = command + + for sectname, sect in bindings_to_add.items(): if not sect: if not only_new: self.keybindings[sectname] = collections.OrderedDict() - self._mark_config_dirty() else: - for command, keychains in sect.items(): - for e in keychains: - if not only_new or self._is_new(sectname, command, e): - self._add_binding(sectname, e, command) - self._mark_config_dirty() + for keychain, command in sect.items(): + self._add_binding(sectname, keychain, command) self.changed.emit(sectname) + if bindings_to_add: + self._mark_config_dirty() + def _is_new(self, sectname, command, keychain): """Check if a given binding is new. From 866b299fef8e02a33a992290489cfdb1b0ae6ed9 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 13 May 2015 07:54:06 +0200 Subject: [PATCH 059/182] Fix adding of new default section to keyconf. When trying to add a new binding with multiple values, the bindings were added immediately and the next _is_new() check returned False because the command was already bound. With this change, the new bindings first get added to a temporary dict so _is_new() returns the correct result. See #653. --- qutebrowser/config/parsers/keyconf.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/qutebrowser/config/parsers/keyconf.py b/qutebrowser/config/parsers/keyconf.py index b15a9ae4a..059ea5cdc 100644 --- a/qutebrowser/config/parsers/keyconf.py +++ b/qutebrowser/config/parsers/keyconf.py @@ -237,20 +237,31 @@ class KeyConfigParser(QObject): only_new: If set, only keybindings which are completely unused (same command/key not bound) are added. """ + + # {'sectname': {'keychain1': 'command', 'keychain2': 'command'}, ...} + bindings_to_add = collections.OrderedDict() + for sectname, sect in configdata.KEY_DATA.items(): sectname = self._normalize_sectname(sectname) + bindings_to_add[sectname] = collections.OrderedDict() + for command, keychains in sect.items(): + for e in keychains: + if not only_new or self._is_new(sectname, command, e): + assert e not in bindings_to_add[sectname] + bindings_to_add[sectname][e] = command + + for sectname, sect in bindings_to_add.items(): if not sect: if not only_new: self.keybindings[sectname] = collections.OrderedDict() - self._mark_config_dirty() else: - for command, keychains in sect.items(): - for e in keychains: - if not only_new or self._is_new(sectname, command, e): - self._add_binding(sectname, e, command) - self._mark_config_dirty() + for keychain, command in sect.items(): + self._add_binding(sectname, keychain, command) self.changed.emit(sectname) + if bindings_to_add: + self._mark_config_dirty() + def _is_new(self, sectname, command, keychain): """Check if a given binding is new. From f59a14758980ec76779bf2957c9a71a9ac844912 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 13 May 2015 07:58:33 +0200 Subject: [PATCH 060/182] Leave mode when yanking by default. See #653. --- qutebrowser/browser/commands.py | 6 +++--- qutebrowser/config/configdata.py | 3 +-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index e5f646dcb..3b71fc4f4 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -1389,12 +1389,12 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', hide=True, modes=[KeyMode.caret], scope='window') - def yank_selected(self, sel=False, leave=False): + def yank_selected(self, sel=False, keep=False): """Yank the selected text to the clipboard or primary selection. Args: sel: Use the primary selection instead of the clipboard. - leave: If given, leave visual mode after yanking. + keep: If given, stay in visual mode after yanking. """ s = self._current_widget().selectedText() if not self._current_widget().hasSelection() or len(s) == 0: @@ -1412,7 +1412,7 @@ class CommandDispatcher: clipboard.setText(s, mode) message.info(self._win_id, "{} {} yanked to {}".format( len(s), "char" if len(s) == 1 else "chars", target)) - if leave: + if not keep: modeman.leave(self._win_id, KeyMode.caret, "yank selected") @cmdutils.register(instance='command-dispatcher', hide=True, diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 6fe88b5e1..33be98db8 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -1279,9 +1279,8 @@ KEY_DATA = collections.OrderedDict([ ('move-to-end-of-line', ['$']), ('move-to-start-of-document', ['gg']), ('move-to-end-of-document', ['G']), - ('yank-selected', ['y']), ('yank-selected -p', ['Y']), - ('yank-selected --leave', ['', '']), + ('yank-selected', ['y', '', '']), ('scroll -50 0', ['H']), ('scroll 0 50', ['J']), ('scroll 0 -50', ['K']), From a93bf184aac621fea281b26379ffe28cc638af67 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 13 May 2015 08:05:33 +0200 Subject: [PATCH 061/182] Fix lint. --- qutebrowser/config/parsers/keyconf.py | 1 - 1 file changed, 1 deletion(-) diff --git a/qutebrowser/config/parsers/keyconf.py b/qutebrowser/config/parsers/keyconf.py index 059ea5cdc..21788cb09 100644 --- a/qutebrowser/config/parsers/keyconf.py +++ b/qutebrowser/config/parsers/keyconf.py @@ -237,7 +237,6 @@ class KeyConfigParser(QObject): only_new: If set, only keybindings which are completely unused (same command/key not bound) are added. """ - # {'sectname': {'keychain1': 'command', 'keychain2': 'command'}, ...} bindings_to_add = collections.OrderedDict() From 25005ded8a415864ae7c2dda96ae64fb1e4fe7fa Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 13 May 2015 08:26:19 +0200 Subject: [PATCH 062/182] Add a test for deprecated default bindings. --- tests/config/test_config.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/config/test_config.py b/tests/config/test_config.py index 383fed232..edca7ae0d 100644 --- a/tests/config/test_config.py +++ b/tests/config/test_config.py @@ -157,6 +157,14 @@ class TestConfigParser: self.cfg.get('general', 'bar') # pylint: disable=bad-config-call +def keyconfig_deprecated_test_cases(): + """Generator yielding test cases (command, rgx) for TestKeyConfigParser.""" + for sect in configdata.KEY_DATA.values(): + for command in sect: + for rgx, _repl in configdata.CHANGED_KEY_COMMANDS: + yield (command, rgx) + + class TestKeyConfigParser: """Test config.parsers.keyconf.KeyConfigParser.""" @@ -177,6 +185,11 @@ class TestKeyConfigParser: with pytest.raises(keyconf.KeyConfigError): kcp._read_command(cmdline_test.cmd) + @pytest.mark.parametrize('command, rgx', keyconfig_deprecated_test_cases()) + def test_default_config_no_deprecated(self, command, rgx): + """Make sure the default config contains no deprecated commands.""" + assert rgx.match(command) is None + class TestDefaultConfig: From f6ef65795210629fa40a77128821f828505f9480 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 13 May 2015 08:26:56 +0200 Subject: [PATCH 063/182] Fix default search binding. --- 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 e0e613892..871a4b6a7 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -1097,7 +1097,7 @@ KEY_DATA = collections.OrderedDict([ ])), ('normal', collections.OrderedDict([ - ('search ""', ['']), + ('search', ['']), ('set-cmd-text -s :open', ['o']), ('set-cmd-text :open {url}', ['go']), ('set-cmd-text -s :open -t', ['O']), From 5d13d0073c6d93866b4ae86534f533946d77502a Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 13 May 2015 10:41:23 +0200 Subject: [PATCH 064/182] Add some tests for key config migrations. --- tests/config/test_config.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tests/config/test_config.py b/tests/config/test_config.py index edca7ae0d..9ba458ec3 100644 --- a/tests/config/test_config.py +++ b/tests/config/test_config.py @@ -190,6 +190,37 @@ class TestKeyConfigParser: """Make sure the default config contains no deprecated commands.""" assert rgx.match(command) is None + @pytest.mark.parametrize( + 'old, new_expected', + [ + ('open -t about:blank', 'open -t'), + ('open -w about:blank', 'open -w'), + ('open -b about:blank', 'open -b'), + ('open about:blank', None), + ('open -t example.com', None), + ('download-page', 'download'), + ('cancel-download', 'download-cancel'), + ('search ""', 'search'), + ("search ''", 'search'), + ('search "foo"', None), + ('set-cmd-text "foo bar"', 'set-cmd-text foo bar'), + ("set-cmd-text 'foo bar'", 'set-cmd-text foo bar'), + ('set-cmd-text foo bar', None), + ('set-cmd-text "foo bar "', 'set-cmd-text -s foo bar'), + ("set-cmd-text 'foo bar '", 'set-cmd-text -s foo bar'), + ] + ) + def test_migrations(self, old, new_expected): + """Make sure deprecated commands get migrated correctly.""" + if new_expected is None: + new_expected = old + new = old + for rgx, repl in configdata.CHANGED_KEY_COMMANDS: + if rgx.match(new): + new = rgx.sub(repl, new) + break + assert new == new_expected + class TestDefaultConfig: From f8f8699ab8811d581a802dc77b15572fc9e60aab Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 13 May 2015 10:45:20 +0200 Subject: [PATCH 065/182] Fix key config migration for rapid hinting. --- qutebrowser/config/configdata.py | 6 ++++-- tests/config/test_config.py | 2 ++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 871a4b6a7..c67944eb0 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -1140,8 +1140,8 @@ KEY_DATA = collections.OrderedDict([ ('hint links fill ":open -b {hint-url}"', ['.o']), ('hint links yank', [';y']), ('hint links yank-primary', [';Y']), - ('hint links rapid', [';r']), - ('hint links rapid-win', [';R']), + ('hint --rapid links tab-bg', [';r']), + ('hint --rapid links window', [';R']), ('hint links download', [';d']), ('scroll -50 0', ['h']), ('scroll 0 50', ['j']), @@ -1266,4 +1266,6 @@ CHANGED_KEY_COMMANDS = [ (re.compile(r"^search ''$"), r'search'), (re.compile(r"""^set-cmd-text ['"](.*) ['"]$"""), r'set-cmd-text -s \1'), (re.compile(r"""^set-cmd-text ['"](.*)['"]$"""), r'set-cmd-text \1'), + (re.compile(r"^hint links rapid$"), r'hint --rapid links tab-bg'), + (re.compile(r"^hint links rapid-win$"), r'hint --rapid links window'), ] diff --git a/tests/config/test_config.py b/tests/config/test_config.py index 9ba458ec3..6c78990e4 100644 --- a/tests/config/test_config.py +++ b/tests/config/test_config.py @@ -208,6 +208,8 @@ class TestKeyConfigParser: ('set-cmd-text foo bar', None), ('set-cmd-text "foo bar "', 'set-cmd-text -s foo bar'), ("set-cmd-text 'foo bar '", 'set-cmd-text -s foo bar'), + ('hint links rapid', 'hint --rapid links tab-bg'), + ('hint links rapid-win', 'hint --rapid links window'), ] ) def test_migrations(self, old, new_expected): From a728704cce335ee352707ba220c8c8fdc2902356 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 13 May 2015 21:52:42 +0200 Subject: [PATCH 066/182] toggle-selection cleanup --- qutebrowser/browser/commands.py | 2 +- qutebrowser/mainwindow/statusbar/bar.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 3b71fc4f4..4679eb9b8 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -1423,7 +1423,7 @@ class CommandDispatcher: widget.selection_enabled = not widget.selection_enabled mainwindow = objreg.get('main-window', scope='window', window=self._win_id) - mainwindow.status.on_mode_entered(usertypes.KeyMode.caret) + mainwindow.status.set_mode_active(usertypes.KeyMode.caret, True) @cmdutils.register(instance='command-dispatcher', hide=True, modes=[KeyMode.caret], scope='window') diff --git a/qutebrowser/mainwindow/statusbar/bar.py b/qutebrowser/mainwindow/statusbar/bar.py index 503cc38c1..6df62e258 100644 --- a/qutebrowser/mainwindow/statusbar/bar.py +++ b/qutebrowser/mainwindow/statusbar/bar.py @@ -273,7 +273,7 @@ class StatusBar(QWidget): """Getter for caret_selection_active, so it can be used as property.""" return self._caret_selection_active - def _set_mode_active(self, mode, val): + def set_mode_active(self, mode, val): """Setter for self.{insert,caret}_active. Re-set the stylesheet after setting the value, so everything gets @@ -474,7 +474,7 @@ class StatusBar(QWidget): if mode in mode_manager.passthrough: self._set_mode_text(mode.name) if mode in (usertypes.KeyMode.insert, usertypes.KeyMode.caret): - self._set_mode_active(mode, True) + self.set_mode_active(mode, True) @pyqtSlot(usertypes.KeyMode, usertypes.KeyMode) def on_mode_left(self, old_mode, new_mode): @@ -487,7 +487,7 @@ class StatusBar(QWidget): else: self.txt.set_text(self.txt.Text.normal, '') if old_mode in (usertypes.KeyMode.insert, usertypes.KeyMode.caret): - self._set_mode_active(old_mode, False) + self.set_mode_active(old_mode, False) @config.change_filter('ui', 'message-timeout') def set_pop_timer_interval(self): From 222627b08da95298ed80a24706d60017742e5615 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 13 May 2015 22:25:21 +0200 Subject: [PATCH 067/182] Clean up caret initialisation. --- qutebrowser/browser/webview.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/qutebrowser/browser/webview.py b/qutebrowser/browser/webview.py index b26045c55..b2546d367 100644 --- a/qutebrowser/browser/webview.py +++ b/qutebrowser/browser/webview.py @@ -441,12 +441,11 @@ class WebView(QWebView): settings.setAttribute(QWebSettings.CaretBrowsingEnabled, True) self.selection_enabled = False - tabbed_browser = objreg.get('tabbed-browser', scope='window', - window=self.win_id) - if tabbed_browser.currentWidget().tab_id == self.tab_id: + if self.isVisible(): + # Sometimes the caret isn't immediately visible, but unfocusing + # and refocusing it fixes that. self.clearFocus() self.setFocus(Qt.OtherFocusReason) - self.page().currentFrame().evaluateJavaScript( utils.read_file('javascript/position_caret.js')) From bc54eb86713742de066c1a799919025050bd8cf1 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 13 May 2015 22:27:54 +0200 Subject: [PATCH 068/182] Make get_modeman private again. --- qutebrowser/keyinput/modeman.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/qutebrowser/keyinput/modeman.py b/qutebrowser/keyinput/modeman.py index 3d4760849..fc70ac76b 100644 --- a/qutebrowser/keyinput/modeman.py +++ b/qutebrowser/keyinput/modeman.py @@ -97,25 +97,25 @@ def init(win_id, parent): return modeman -def get_modeman(win_id): +def _get_modeman(win_id): """Get a modemanager object.""" return objreg.get('mode-manager', scope='window', window=win_id) def enter(win_id, mode, reason=None, only_if_normal=False): """Enter the mode 'mode'.""" - get_modeman(win_id).enter(mode, reason, only_if_normal) + _get_modeman(win_id).enter(mode, reason, only_if_normal) def leave(win_id, mode, reason=None): """Leave the mode 'mode'.""" - get_modeman(win_id).leave(mode, reason) + _get_modeman(win_id).leave(mode, reason) def maybe_leave(win_id, mode, reason=None): """Convenience method to leave 'mode' without exceptions.""" try: - get_modeman(win_id).leave(mode, reason) + _get_modeman(win_id).leave(mode, reason) except NotInModeError as e: # This is rather likely to happen, so we only log to debug log. log.modes.debug("{} (leave reason: {})".format(e, reason)) From 947dcd556b864d3d37db05bef39c01962f911775 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 13 May 2015 22:29:21 +0200 Subject: [PATCH 069/182] Clean up CaretKeyParser. --- qutebrowser/keyinput/modeparsers.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/qutebrowser/keyinput/modeparsers.py b/qutebrowser/keyinput/modeparsers.py index 8a6165823..8d47de0c1 100644 --- a/qutebrowser/keyinput/modeparsers.py +++ b/qutebrowser/keyinput/modeparsers.py @@ -222,12 +222,9 @@ class HintKeyParser(keyparser.CommandKeyParser): class CaretKeyParser(keyparser.CommandKeyParser): - """KeyParser for Caret mode.""" + """KeyParser for caret mode.""" def __init__(self, win_id, parent=None): super().__init__(win_id, parent, supports_count=True, supports_chains=True) self.read_config('caret') - - def __repr__(self): - return utils.get_repr(self) From d992caf8fc7af00db9cde36211d08323a6fcf0a7 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 13 May 2015 22:44:37 +0200 Subject: [PATCH 070/182] Clean up statusbar caret handling. --- qutebrowser/mainwindow/statusbar/bar.py | 46 ++++++++++++------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/qutebrowser/mainwindow/statusbar/bar.py b/qutebrowser/mainwindow/statusbar/bar.py index 6df62e258..fe1a274cc 100644 --- a/qutebrowser/mainwindow/statusbar/bar.py +++ b/qutebrowser/mainwindow/statusbar/bar.py @@ -36,6 +36,7 @@ from qutebrowser.mainwindow.statusbar import text as textwidget PreviousWidget = usertypes.enum('PreviousWidget', ['none', 'prompt', 'command']) Severity = usertypes.enum('Severity', ['normal', 'warning', 'error']) +CaretMode = usertypes.enum('CaretMode', ['off', 'on', 'selection']) class StatusBar(QWidget): @@ -77,6 +78,11 @@ class StatusBar(QWidget): For some reason we need to have this as class attribute so pyqtProperty works correctly. + _caret_mode: The current caret mode (off/on/selection). + + For some reason we need to have this as class attribute + so pyqtProperty works correctly. + Signals: resized: Emitted when the statusbar has resized, so the completion widget can adjust its size to it. @@ -91,8 +97,7 @@ class StatusBar(QWidget): _severity = None _prompt_active = False _insert_active = False - _caret_active = False - _caret_selection_active = False + _caret_mode = CaretMode.off STYLESHEET = """ QWidget#StatusBar { @@ -103,11 +108,11 @@ class StatusBar(QWidget): {{ color['statusbar.bg.insert'] }} } - QWidget#StatusBar[caret_active="true"] { + QWidget#StatusBar[caret_mode="on"] { {{ color['statusbar.bg.caret'] }} } - QWidget#StatusBar[caret_selection_active="true"] { + QWidget#StatusBar[caret_mode="selection"] { {{ color['statusbar.bg.caret-selection'] }} } @@ -263,15 +268,10 @@ class StatusBar(QWidget): """Getter for self.insert_active, so it can be used as Qt property.""" return self._insert_active - @pyqtProperty(bool) - def caret_active(self): - """Getter for self.caret_active, so it can be used as Qt property.""" - return self._caret_active - - @pyqtProperty(bool) - def caret_selection_active(self): - """Getter for caret_selection_active, so it can be used as property.""" - return self._caret_selection_active + @pyqtProperty(str) + def caret_mode(self): + """Getter for self._caret_mode, so it can be used as Qt property.""" + return self._caret_mode.name def set_mode_active(self, mode, val): """Setter for self.{insert,caret}_active. @@ -283,19 +283,19 @@ class StatusBar(QWidget): log.statusbar.debug("Setting insert_active to {}".format(val)) self._insert_active = val elif mode == usertypes.KeyMode.caret: - log.statusbar.debug("Setting caret_active to {}".format(val)) webview = objreg.get("tabbed-browser", scope="window", window=self._win_id).currentWidget() - if val and webview.selection_enabled: - self._set_mode_text("{} selection".format(mode.name)) - self._caret_selection_active = val - self._caret_active = False - else: - if val: + log.statusbar.debug("Setting caret_mode - val {}, selection " + "{}".format(val, webview.selection_enabled)) + if val: + if webview.selection_enabled: + self._set_mode_text("{} selection".format(mode.name)) + self._caret_mode = CaretMode.selection + else: self._set_mode_text(mode.name) - self._caret_active = val - self._caret_selection_active = False - + self._caret_mode = CaretMode.on + else: + self._caret_mode = CaretMode.off self.setStyleSheet(style.get_stylesheet(self.STYLESHEET)) def _set_mode_text(self, mode): From 17fc6622bb7fff2467855697dff6acd94615dfe8 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 13 May 2015 23:46:22 +0200 Subject: [PATCH 071/182] Strip NUL bytes when loading history. This is a workaround so people can start qutebrowser again, but the real bug should be found and fixed... See #670. --- qutebrowser/browser/history.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/qutebrowser/browser/history.py b/qutebrowser/browser/history.py index 90f2e5567..b3370d0c1 100644 --- a/qutebrowser/browser/history.py +++ b/qutebrowser/browser/history.py @@ -95,6 +95,12 @@ class WebHistory(QWebHistoryInterface): line)) continue atime, url = data + if atime.startswith('\0'): + log.init.warning( + "Removing NUL bytes from entry {!r} - see " + "https://github.com/The-Compiler/qutebrowser/issues/" + "670".format(data)) + atime = atime.lstrip('\0') # This de-duplicates history entries; only the latest # entry for each URL is kept. If you want to keep # information about previous hits change the items in From c236046a73533f54fb4c4cc34704125172ca2538 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 14 May 2015 14:48:29 +0200 Subject: [PATCH 072/182] Avoid double-opening LineParser. Hopefully helps with diagnosing #670. --- qutebrowser/misc/lineparser.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/qutebrowser/misc/lineparser.py b/qutebrowser/misc/lineparser.py index 5ae90aa8a..5cd6304f5 100644 --- a/qutebrowser/misc/lineparser.py +++ b/qutebrowser/misc/lineparser.py @@ -53,12 +53,14 @@ class BaseLineParser(QObject): configdir: Directory to read the config from. fname: Filename of the config file. binary: Whether to open the file in binary mode. + _opened: Whether the underlying file is open """ super().__init__(parent) self._configdir = configdir self._configfile = os.path.join(self._configdir, fname) self._fname = fname self._binary = binary + self._opened = False def __repr__(self): return utils.get_repr(self, constructor=True, @@ -71,16 +73,23 @@ class BaseLineParser(QObject): if not os.path.exists(self._configdir): os.makedirs(self._configdir, 0o755) + @contextlib.contextmanager def _open(self, mode): """Open self._configfile for reading. Args: mode: The mode to use ('a'/'r'/'w') """ + if self._opened: + raise IOError("Refusing to double-open AppendLineParser.") + self._opened = True if self._binary: - return open(self._configfile, mode + 'b') + with open(self._configfile, mode + 'b') as f: + yield f else: - return open(self._configfile, mode, encoding='utf-8') + with open(self._configfile, mode, encoding='utf-8') as f: + yield f + self._opened = False def _write(self, fp, data): """Write the data to a file. @@ -195,9 +204,13 @@ class LineParser(BaseLineParser): def save(self): """Save the config file.""" + if self._opened: + raise IOError("Refusing to double-open AppendLineParser.") + self._opened = True self._prepare_save() with qtutils.savefile_open(self._configfile, self._binary) as f: self._write(f, self.data) + self._opened = False class LimitLineParser(LineParser): From f49dba6e38a5ed4fbd6a2d8e9920310c2c22ed74 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 15 May 2015 18:59:46 +0200 Subject: [PATCH 073/182] Use fake key events for scrolling. Closes #669. Fixes #218. See #246, #534. --- qutebrowser/browser/commands.py | 75 +++++++++++++++++++++++++++++--- qutebrowser/config/configdata.py | 18 ++++++-- tests/config/test_config.py | 13 ++++++ 3 files changed, 96 insertions(+), 10 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 94afb0dc2..55a0c5d22 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -27,8 +27,8 @@ import posixpath import functools from PyQt5.QtWidgets import QApplication, QTabBar -from PyQt5.QtCore import Qt, QUrl -from PyQt5.QtGui import QClipboard +from PyQt5.QtCore import Qt, QUrl, QEvent +from PyQt5.QtGui import QClipboard, QKeyEvent from PyQt5.QtPrintSupport import QPrintDialog, QPrintPreviewDialog from PyQt5.QtWebKitWidgets import QWebPage import pygments @@ -555,8 +555,8 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', hide=True, scope='window', count='count') - def scroll(self, dx: {'type': float}, dy: {'type': float}, count=1): - """Scroll the current tab by 'count * dx/dy'. + def scroll_px(self, dx: {'type': float}, dy: {'type': float}, count=1): + """Scroll the current tab by 'count * dx/dy' pixels. Args: dx: How much to scroll in x-direction. @@ -569,6 +569,57 @@ class CommandDispatcher: cmdutils.check_overflow(dy, 'int') self._current_widget().page().currentFrame().scroll(dx, dy) + @cmdutils.register(instance='command-dispatcher', hide=True, + scope='window', count='count') + def scroll(self, + direction: {'type': (str, float)}, + dy: {'type': float, 'hide': True}=None, + count=1): + """Scroll the current tab in the given direction. + + Args: + direction: In which direction to scroll + (up/down/left/right/top/bottom). + dy: Deprecated argument to support the old dx/dy form. + count: multiplier + """ + try: + # Check for deprecated dx/dy form (like with scroll-px). + dx = float(direction) + dy = float(dy) + except (ValueError, TypeError): + # Invalid values will get handled later. + pass + else: + message.warning(self._win_id, ":scroll with dx/dy arguments is " + "deprecated - use :scroll-px instead!") + self.scroll_px(dx, dy, count=count) + return + + fake_keys = { + 'up': Qt.Key_Up, + 'down': Qt.Key_Down, + 'left': Qt.Key_Left, + 'right': Qt.Key_Right, + 'top': Qt.Key_Home, + 'bottom': Qt.Key_End, + 'page-up': Qt.Key_PageUp, + 'page-down': Qt.Key_PageDown, + } + try: + key = fake_keys[direction] + except KeyError: + raise cmdexc.CommandError("Invalid value {!r} for direction - " + "expected one of: {}".format( + direction, ', '.join(fake_keys))) + widget = self._current_widget() + press_evt = QKeyEvent(QEvent.KeyPress, key, Qt.NoModifier, 0, 0, 0) + release_evt = QKeyEvent(QEvent.KeyRelease, key, Qt.NoModifier, 0, 0, 0) + + for _ in range(count): + widget.keyPressEvent(press_evt) + widget.keyReleaseEvent(release_evt) + @cmdutils.register(instance='command-dispatcher', hide=True, scope='window', count='count') def scroll_perc(self, perc: {'type': float}=None, @@ -596,10 +647,22 @@ class CommandDispatcher: y: How many pages to scroll down. count: multiplier """ + mult_x = count * x + mult_y = count * y + if mult_y.is_integer(): + if mult_y == 0: + pass + elif mult_y < 0: + self.scroll('page-up', count=-mult_y) + elif mult_y > 0: + self.scroll('page-down', count=mult_y) + mult_y = 0 + if mult_x == 0 and mult_y == 0: + return frame = self._current_widget().page().currentFrame() size = frame.geometry() - dx = count * x * size.width() - dy = count * y * size.height() + dx = mult_x * size.width() + dy = mult_y * size.height() cmdutils.check_overflow(dx, 'int') cmdutils.check_overflow(dy, 'int') frame.scroll(dx, dy) diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index c67944eb0..7df7ed9df 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -1143,10 +1143,10 @@ KEY_DATA = collections.OrderedDict([ ('hint --rapid links tab-bg', [';r']), ('hint --rapid links window', [';R']), ('hint links download', [';d']), - ('scroll -50 0', ['h']), - ('scroll 0 50', ['j']), - ('scroll 0 -50', ['k']), - ('scroll 50 0', ['l']), + ('scroll left', ['h']), + ('scroll down', ['j']), + ('scroll up', ['k']), + ('scroll right', ['l']), ('undo', ['u', '']), ('scroll-perc 0', ['gg']), ('scroll-perc', ['G']), @@ -1260,12 +1260,22 @@ KEY_DATA = collections.OrderedDict([ CHANGED_KEY_COMMANDS = [ (re.compile(r'^open -([twb]) about:blank$'), r'open -\1'), + (re.compile(r'^download-page$'), r'download'), (re.compile(r'^cancel-download$'), r'download-cancel'), + (re.compile(r'^search ""$'), r'search'), (re.compile(r"^search ''$"), r'search'), + (re.compile(r"""^set-cmd-text ['"](.*) ['"]$"""), r'set-cmd-text -s \1'), (re.compile(r"""^set-cmd-text ['"](.*)['"]$"""), r'set-cmd-text \1'), + (re.compile(r"^hint links rapid$"), r'hint --rapid links tab-bg'), (re.compile(r"^hint links rapid-win$"), r'hint --rapid links window'), + + (re.compile(r'^scroll -50 0$'), r'scroll left'), + (re.compile(r'^scroll 0 50$'), r'scroll down'), + (re.compile(r'^scroll 0 -50$'), r'scroll up'), + (re.compile(r'^scroll 50 0$'), r'scroll right'), + (re.compile(r'^scroll ([-\d]+ [-\d]+)$'), r'scroll-px \1'), ] diff --git a/tests/config/test_config.py b/tests/config/test_config.py index 6c78990e4..636d89c9b 100644 --- a/tests/config/test_config.py +++ b/tests/config/test_config.py @@ -198,18 +198,31 @@ class TestKeyConfigParser: ('open -b about:blank', 'open -b'), ('open about:blank', None), ('open -t example.com', None), + ('download-page', 'download'), ('cancel-download', 'download-cancel'), + ('search ""', 'search'), ("search ''", 'search'), ('search "foo"', None), + ('set-cmd-text "foo bar"', 'set-cmd-text foo bar'), ("set-cmd-text 'foo bar'", 'set-cmd-text foo bar'), ('set-cmd-text foo bar', None), ('set-cmd-text "foo bar "', 'set-cmd-text -s foo bar'), ("set-cmd-text 'foo bar '", 'set-cmd-text -s foo bar'), + ('hint links rapid', 'hint --rapid links tab-bg'), ('hint links rapid-win', 'hint --rapid links window'), + + ('scroll -50 0', 'scroll left'), + ('scroll 0 50', 'scroll down'), + ('scroll 0 -50', 'scroll up'), + ('scroll 50 0', 'scroll right'), + ('scroll -50 10', 'scroll-px -50 10'), + ('scroll 50 50', 'scroll-px 50 50'), + ('scroll 0 0', 'scroll-px 0 0'), + ('scroll 23 42', 'scroll-px 23 42'), ] ) def test_migrations(self, old, new_expected): From aaf09dc573a555de87eb38a50d0f6ac0a78e72e1 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 15 May 2015 19:19:30 +0200 Subject: [PATCH 074/182] Add possibility to hide command args from docs. --- qutebrowser/commands/command.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/qutebrowser/commands/command.py b/qutebrowser/commands/command.py index 88b5e9cab..269c17e06 100644 --- a/qutebrowser/commands/command.py +++ b/qutebrowser/commands/command.py @@ -61,7 +61,7 @@ class Command: """ AnnotationInfo = collections.namedtuple('AnnotationInfo', - ['kwargs', 'type', 'flag']) + ['kwargs', 'type', 'flag', 'hide']) def __init__(self, *, handler, name, instance=None, maxsplit=None, hide=False, completion=None, modes=None, not_modes=None, @@ -304,7 +304,8 @@ class Command: self.flags_with_args += [short_flag, long_flag] else: args.append(name) - self.pos_args.append((param.name, name)) + if not annotation_info.hide: + self.pos_args.append((param.name, name)) return args def _parse_annotation(self, param): @@ -321,11 +322,11 @@ class Command: flag: The short name/flag if overridden. name: The long name if overridden. """ - info = {'kwargs': {}, 'type': None, 'flag': None} + info = {'kwargs': {}, 'type': None, 'flag': None, 'hide': False} if param.annotation is not inspect.Parameter.empty: log.commands.vdebug("Parsing annotation {}".format( param.annotation)) - for field in ('type', 'flag', 'name'): + for field in ('type', 'flag', 'name', 'hide'): if field in param.annotation: info[field] = param.annotation[field] if 'nargs' in param.annotation: From 1a6779429388f9dce298103d0bde5eb34009fc5b Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 15 May 2015 19:19:49 +0200 Subject: [PATCH 075/182] Regenerate docs. --- doc/help/commands.asciidoc | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/doc/help/commands.asciidoc b/doc/help/commands.asciidoc index 2e6633c9a..cca5156fa 100644 --- a/doc/help/commands.asciidoc +++ b/doc/help/commands.asciidoc @@ -712,9 +712,10 @@ How many steps to zoom out. |<>|Remove chars backward from the cursor to the beginning of the line. |<>|Remove chars from the cursor to the beginning of the word. |<>|Paste the most recently deleted text. -|<>|Scroll the current tab by 'count * dx/dy'. +|<>|Scroll the current tab in the given direction. |<>|Scroll the frame page-wise. |<>|Scroll to a specific percentage of the page. +|<>|Scroll the current tab by 'count * dx/dy' pixels. |<>|Continue the search to the ([count]th) next term. |<>|Continue the search to the ([count]th) previous term. |============== @@ -880,13 +881,13 @@ This acts like readline's yank. [[scroll]] === scroll -Syntax: +:scroll 'dx' 'dy'+ +Syntax: +:scroll 'direction' ['dy']+ -Scroll the current tab by 'count * dx/dy'. +Scroll the current tab in the given direction. ==== positional arguments -* +'dx'+: How much to scroll in x-direction. -* +'dy'+: How much to scroll in x-direction. +* +'direction'+: In which direction to scroll (up/down/left/right/top/bottom). + ==== count multiplier @@ -921,6 +922,19 @@ The percentage can be given either as argument or as count. If no percentage is ==== count Percentage to scroll. +[[scroll-px]] +=== scroll-px +Syntax: +:scroll-px 'dx' 'dy'+ + +Scroll the current tab by 'count * dx/dy' pixels. + +==== positional arguments +* +'dx'+: How much to scroll in x-direction. +* +'dy'+: How much to scroll in x-direction. + +==== count +multiplier + [[search-next]] === search-next Continue the search to the ([count]th) next term. From 1a1a8ba26f4cdebeda57cbcdb9324081e69036dc Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 15 May 2015 19:28:41 +0200 Subject: [PATCH 076/182] Update changelog. --- CHANGELOG.asciidoc | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 9096f2431..1f3f79aff 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -21,6 +21,8 @@ Added ~~~~~ - New commands `:message-info`, `:message-error` and `:message-warning` to show messages in the statusbar, e.g. from an userscript. +- There are now some example userscripts in `misc/userscripts`. +- New command `:scroll-px` which replaces `:scroll` for pixel-exact scrolling. Changed ~~~~~~~ @@ -31,6 +33,17 @@ Changed - New bindings `` (rapid), `` (foreground) and `` (background) to switch hint modes while hinting. - `` is now accepted as an additional alias for ``/`` - `:hint tab` and `F` now respect the `background-tabs` setting. To enforce a foreground tab (what `F` did before), use `:hint tab-fg` or `;f`. +- `:scroll` now takes a direction argument (`up`/`down`/`left`/`right`/`top`/`bottom`/`page-up`/`page-down`) instead of two pixel arguments (`dx`/`dy`). The old form still works but is deprecated. + +Deprecated +~~~~~~~~~~ + +- `:scroll` with two pixel-arguments is now deprecated - `:scroll-px` should be used instead. + +Fixed +~~~~~ + +- Scrolling should now work more reliably on some pages where arrow keys worked but `hjkl` didn't. v0.2.2 (unreleased) ------------------- @@ -41,6 +54,9 @@ Fixed - Fixed searching for terms starting with a hyphen (e.g. `/-foo`) - Proxy authentication credentials are now remembered between different tabs. - Fixed updating of the tab title on pages without title. +- Fixed AssertionError when closing many windows quickly. +- Various fixes for deprecated key bindings and auto-migrations. +- Workaround for qutebrowser not starting when there are NUL-bytes in the history (because of a currently unknown bug) https://github.com/The-Compiler/qutebrowser/releases/tag/v0.2.1[v0.2.1] ----------------------------------------------------------------------- From 12940eb542b1a53e55882ae4e0dc4ae2a85bf430 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 15 May 2015 20:15:09 +0200 Subject: [PATCH 077/182] Handle QtInfoMsg (Qt 5.5) in qt_message_handler. --- qutebrowser/utils/log.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/qutebrowser/utils/log.py b/qutebrowser/utils/log.py index 768a126df..ed9fa2163 100644 --- a/qutebrowser/utils/log.py +++ b/qutebrowser/utils/log.py @@ -29,8 +29,7 @@ import faulthandler import traceback import warnings -from PyQt5.QtCore import (QtDebugMsg, QtWarningMsg, QtCriticalMsg, QtFatalMsg, - qInstallMessageHandler) +from PyQt5 import QtCore # Optional imports try: import colorama @@ -153,15 +152,15 @@ def init_log(args): root.setLevel(logging.NOTSET) logging.captureWarnings(True) warnings.simplefilter('default') - qInstallMessageHandler(qt_message_handler) + QtCore.qInstallMessageHandler(qt_message_handler) @contextlib.contextmanager def disable_qt_msghandler(): """Contextmanager which temporarily disables the Qt message handler.""" - old_handler = qInstallMessageHandler(None) + old_handler = QtCore.qInstallMessageHandler(None) yield - qInstallMessageHandler(old_handler) + QtCore.qInstallMessageHandler(old_handler) def _init_handlers(level, color, ram_capacity): @@ -244,11 +243,16 @@ def qt_message_handler(msg_type, context, msg): # Note we map critical to ERROR as it's actually "just" an error, and fatal # to critical. qt_to_logging = { - QtDebugMsg: logging.DEBUG, - QtWarningMsg: logging.WARNING, - QtCriticalMsg: logging.ERROR, - QtFatalMsg: logging.CRITICAL, + QtCore.QtDebugMsg: logging.DEBUG, + QtCore.QtWarningMsg: logging.WARNING, + QtCore.QtCriticalMsg: logging.ERROR, + QtCore.QtFatalMsg: logging.CRITICAL, } + try: + qt_to_logging[QtCore.QtInfoMsg] = logging.INFO + except AttributeError: + # Qt < 5.5 + pass # Change levels of some well-known messages to debug so they don't get # shown to the user. # suppressed_msgs is a list of regexes matching the message texts to hide. From 7e2c67a7e491bdc0b780eb00aeb9a26b7a56d34a Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 15 May 2015 20:25:29 +0200 Subject: [PATCH 078/182] Fix tests/lint. --- qutebrowser/utils/log.py | 1 + tests/utils/test_log.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/qutebrowser/utils/log.py b/qutebrowser/utils/log.py index ed9fa2163..92bdef222 100644 --- a/qutebrowser/utils/log.py +++ b/qutebrowser/utils/log.py @@ -249,6 +249,7 @@ def qt_message_handler(msg_type, context, msg): QtCore.QtFatalMsg: logging.CRITICAL, } try: + # pylint: disable=no-member qt_to_logging[QtCore.QtInfoMsg] = logging.INFO except AttributeError: # Qt < 5.5 diff --git a/tests/utils/test_log.py b/tests/utils/test_log.py index d52adf552..cd34572d7 100644 --- a/tests/utils/test_log.py +++ b/tests/utils/test_log.py @@ -204,7 +204,8 @@ class RAMHandlerTests(BaseTest): self.assertEqual(self.handler.dump_log(), "Two\nThree") -@mock.patch('qutebrowser.utils.log.qInstallMessageHandler', autospec=True) +@mock.patch('qutebrowser.utils.log.QtCore.qInstallMessageHandler', + autospec=True) class InitLogTests(BaseTest): """Tests for init_log.""" From e3bfe7344227d56c6c2e132f13341675004cd373 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 15 May 2015 21:32:42 +0200 Subject: [PATCH 079/182] Fix :scroll-page. --- qutebrowser/browser/commands.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 55a0c5d22..23bdcc0dd 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -653,9 +653,9 @@ class CommandDispatcher: if mult_y == 0: pass elif mult_y < 0: - self.scroll('page-up', count=-mult_y) + self.scroll('page-up', count=-int(mult_y)) elif mult_y > 0: - self.scroll('page-down', count=mult_y) + self.scroll('page-down', count=int(mult_y)) mult_y = 0 if mult_x == 0 and mult_y == 0: return From f855d5f3493fec7edff50f2296fa29874ef9485b Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 15 May 2015 23:53:08 +0200 Subject: [PATCH 080/182] Add support for smooth scrolling. See #612. --- CHANGELOG.asciidoc | 1 + doc/help/settings.asciidoc | 12 ++++++++++++ qutebrowser/config/configdata.py | 4 ++++ qutebrowser/config/websettings.py | 2 ++ 4 files changed, 19 insertions(+) diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 1f3f79aff..ae5e0bee1 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -23,6 +23,7 @@ Added - New commands `:message-info`, `:message-error` and `:message-warning` to show messages in the statusbar, e.g. from an userscript. - There are now some example userscripts in `misc/userscripts`. - New command `:scroll-px` which replaces `:scroll` for pixel-exact scrolling. +- New setting `ui -> smooth-scrolling`. Changed ~~~~~~~ diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc index dd7599db2..0d92f7d2c 100644 --- a/doc/help/settings.asciidoc +++ b/doc/help/settings.asciidoc @@ -40,6 +40,7 @@ |<>|Whether to expand each subframe to its contents. |<>|User stylesheet to use (absolute filename or CSS string). Will expand environment variables. |<>|Set the CSS media type. +|<>|Whether to enable smooth scrolling for webpages. |<>|Whether to remove finished downloads automatically. |<>|Whether to hide the statusbar unless a message is shown. |<>|The format to use for the window title. The following placeholders are defined: @@ -531,6 +532,17 @@ Set the CSS media type. Default: empty +[[ui-smooth-scrolling]] +=== smooth-scrolling +Whether to enable smooth scrolling for webpages. + +Valid values: + + * +true+ + * +false+ + +Default: +pass:[false]+ + [[ui-remove-finished-downloads]] === remove-finished-downloads Whether to remove finished downloads automatically. diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 7df7ed9df..8e71cb465 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -276,6 +276,10 @@ def data(readonly=False): SettingValue(typ.String(none_ok=True), ''), "Set the CSS media type."), + ('smooth-scrolling', + SettingValue(typ.Bool(), 'false'), + "Whether to enable smooth scrolling for webpages."), + ('remove-finished-downloads', SettingValue(typ.Bool(), 'false'), "Whether to remove finished downloads automatically."), diff --git a/qutebrowser/config/websettings.py b/qutebrowser/config/websettings.py index 38c662066..d9ced2689 100644 --- a/qutebrowser/config/websettings.py +++ b/qutebrowser/config/websettings.py @@ -322,6 +322,8 @@ MAPPINGS = { 'css-media-type': NullStringSetter(getter=QWebSettings.cssMediaType, setter=QWebSettings.setCSSMediaType), + 'smooth-scrolling': + Attribute(QWebSettings.ScrollAnimatorEnabled), #'accelerated-compositing': # Attribute(QWebSettings.AcceleratedCompositingEnabled), #'tiled-backing-store': From cd53318c7f935b53ebb8e2460a8315ecf0679a40 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sat, 16 May 2015 00:31:13 +0200 Subject: [PATCH 081/182] Add setting to enable/disable WebGL. See #612. --- CHANGELOG.asciidoc | 1 + doc/help/settings.asciidoc | 12 ++++++++++++ qutebrowser/config/configdata.py | 4 ++++ qutebrowser/config/websettings.py | 2 ++ 4 files changed, 19 insertions(+) diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index ae5e0bee1..2afcc1e7d 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -24,6 +24,7 @@ Added - There are now some example userscripts in `misc/userscripts`. - New command `:scroll-px` which replaces `:scroll` for pixel-exact scrolling. - New setting `ui -> smooth-scrolling`. +- New setting `content -> webgl` to enable/disable https://www.khronos.org/webgl/[WebGL]. Changed ~~~~~~~ diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc index 0d92f7d2c..6c85de979 100644 --- a/doc/help/settings.asciidoc +++ b/doc/help/settings.asciidoc @@ -135,6 +135,7 @@ |<>|Whether images are automatically loaded in web pages. |<>|Enables or disables the running of JavaScript programs. |<>|Enables or disables plugins in Web pages. +|<>|Enables or disables WebGL. |<>|Allow websites to request geolocations. |<>|Allow websites to show notifications. |<>|Whether JavaScript programs can open new windows. @@ -1150,6 +1151,17 @@ Valid values: Default: +pass:[false]+ +[[content-webgl]] +=== webgl +Enables or disables WebGL. + +Valid values: + + * +true+ + * +false+ + +Default: +pass:[true]+ + [[content-geolocation]] === geolocation Allow websites to request geolocations. diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 8e71cb465..9ed96c9b4 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -609,6 +609,10 @@ def data(readonly=False): 'Qt plugins with a mimetype such as "application/x-qt-plugin" ' "are not affected by this setting."), + ('webgl', + SettingValue(typ.Bool(), 'true'), + "Enables or disables WebGL."), + ('geolocation', SettingValue(typ.BoolAsk(), 'ask'), "Allow websites to request geolocations."), diff --git a/qutebrowser/config/websettings.py b/qutebrowser/config/websettings.py index d9ced2689..f56005abd 100644 --- a/qutebrowser/config/websettings.py +++ b/qutebrowser/config/websettings.py @@ -254,6 +254,8 @@ MAPPINGS = { # Attribute(QWebSettings.JavaEnabled), 'allow-plugins': Attribute(QWebSettings.PluginsEnabled), + 'webgl': + Attribute(QWebSettings.WebGLEnabled), 'local-content-can-access-remote-urls': Attribute(QWebSettings.LocalContentCanAccessRemoteUrls), 'local-content-can-access-file-urls': From 0cabedfeeff3951979bc50251ec1c4065f4ca8b3 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sat, 16 May 2015 00:39:20 +0200 Subject: [PATCH 082/182] Add settings to enable/disable CSS regions. See #612. --- CHANGELOG.asciidoc | 1 + doc/help/settings.asciidoc | 12 ++++++++++++ qutebrowser/config/configdata.py | 4 ++++ qutebrowser/config/websettings.py | 2 ++ 4 files changed, 19 insertions(+) diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 2afcc1e7d..1b1a3cc03 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -25,6 +25,7 @@ Added - New command `:scroll-px` which replaces `:scroll` for pixel-exact scrolling. - New setting `ui -> smooth-scrolling`. - New setting `content -> webgl` to enable/disable https://www.khronos.org/webgl/[WebGL]. +- New setting `content -> css-regions` to enable/disable support for http://dev.w3.org/csswg/css-regions/[CSS Regions]. Changed ~~~~~~~ diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc index 6c85de979..b82f2ff4e 100644 --- a/doc/help/settings.asciidoc +++ b/doc/help/settings.asciidoc @@ -136,6 +136,7 @@ |<>|Enables or disables the running of JavaScript programs. |<>|Enables or disables plugins in Web pages. |<>|Enables or disables WebGL. +|<>|Enable or disable support for CSS regions. |<>|Allow websites to request geolocations. |<>|Allow websites to show notifications. |<>|Whether JavaScript programs can open new windows. @@ -1162,6 +1163,17 @@ Valid values: Default: +pass:[true]+ +[[content-css-regions]] +=== css-regions +Enable or disable support for CSS regions. + +Valid values: + + * +true+ + * +false+ + +Default: +pass:[true]+ + [[content-geolocation]] === geolocation Allow websites to request geolocations. diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 9ed96c9b4..2cac44d99 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -613,6 +613,10 @@ def data(readonly=False): SettingValue(typ.Bool(), 'true'), "Enables or disables WebGL."), + ('css-regions', + SettingValue(typ.Bool(), 'true'), + "Enable or disable support for CSS regions."), + ('geolocation', SettingValue(typ.BoolAsk(), 'ask'), "Allow websites to request geolocations."), diff --git a/qutebrowser/config/websettings.py b/qutebrowser/config/websettings.py index f56005abd..8de55aed7 100644 --- a/qutebrowser/config/websettings.py +++ b/qutebrowser/config/websettings.py @@ -256,6 +256,8 @@ MAPPINGS = { Attribute(QWebSettings.PluginsEnabled), 'webgl': Attribute(QWebSettings.WebGLEnabled), + 'css-regions': + Attribute(QWebSettings.CSSRegionsEnabled), 'local-content-can-access-remote-urls': Attribute(QWebSettings.LocalContentCanAccessRemoteUrls), 'local-content-can-access-file-urls': From ad338e7a1746877d0a7002ee038b57e58d0fe208 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sat, 16 May 2015 00:42:26 +0200 Subject: [PATCH 083/182] Add setting to enable/disable hyperlink auditing. See #612. --- CHANGELOG.asciidoc | 1 + doc/help/settings.asciidoc | 12 ++++++++++++ qutebrowser/config/configdata.py | 4 ++++ qutebrowser/config/websettings.py | 2 ++ 4 files changed, 19 insertions(+) diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 1b1a3cc03..de3c12d53 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -26,6 +26,7 @@ Added - New setting `ui -> smooth-scrolling`. - New setting `content -> webgl` to enable/disable https://www.khronos.org/webgl/[WebGL]. - New setting `content -> css-regions` to enable/disable support for http://dev.w3.org/csswg/css-regions/[CSS Regions]. +- New setting `content -> hyperlink-auditing` to enable/disable support for https://html.spec.whatwg.org/multipage/semantics.html#hyperlink-auditing[hyperlink auditing]. Changed ~~~~~~~ diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc index b82f2ff4e..62763e6d7 100644 --- a/doc/help/settings.asciidoc +++ b/doc/help/settings.asciidoc @@ -137,6 +137,7 @@ |<>|Enables or disables plugins in Web pages. |<>|Enables or disables WebGL. |<>|Enable or disable support for CSS regions. +|<>|Enable or disable hyperlink auditing (
). |<>|Allow websites to request geolocations. |<>|Allow websites to show notifications. |<>|Whether JavaScript programs can open new windows. @@ -1174,6 +1175,17 @@ Valid values: Default: +pass:[true]+ +[[content-hyperlink-auditing]] +=== hyperlink-auditing +Enable or disable hyperlink auditing (). + +Valid values: + + * +true+ + * +false+ + +Default: +pass:[false]+ + [[content-geolocation]] === geolocation Allow websites to request geolocations. diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 2cac44d99..9011170c0 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -617,6 +617,10 @@ def data(readonly=False): SettingValue(typ.Bool(), 'true'), "Enable or disable support for CSS regions."), + ('hyperlink-auditing', + SettingValue(typ.Bool(), 'false'), + "Enable or disable hyperlink auditing ()."), + ('geolocation', SettingValue(typ.BoolAsk(), 'ask'), "Allow websites to request geolocations."), diff --git a/qutebrowser/config/websettings.py b/qutebrowser/config/websettings.py index 8de55aed7..b89fbd3c5 100644 --- a/qutebrowser/config/websettings.py +++ b/qutebrowser/config/websettings.py @@ -258,6 +258,8 @@ MAPPINGS = { Attribute(QWebSettings.WebGLEnabled), 'css-regions': Attribute(QWebSettings.CSSRegionsEnabled), + 'hyperlink-auditing': + Attribute(QWebSettings.HyperlinkAuditingEnabled), 'local-content-can-access-remote-urls': Attribute(QWebSettings.LocalContentCanAccessRemoteUrls), 'local-content-can-access-file-urls': From 137badc77fbf3539befc23ee9edf18631afb5438 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sat, 16 May 2015 12:57:29 +0200 Subject: [PATCH 084/182] Add some more informations to keytester script. See #658, #420. --- scripts/keytester.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/scripts/keytester.py b/scripts/keytester.py index 5322848f0..dd91c86bc 100644 --- a/scripts/keytester.py +++ b/scripts/keytester.py @@ -39,7 +39,14 @@ class KeyWidget(QWidget): self._layout.addWidget(self._label) def keyPressEvent(self, e): - self._label.setText(utils.keyevent_to_string(e)) + lines = [ + str(utils.keyevent_to_string(e)), + '', + 'key: 0x{:x}'.format(int(e.key())), + 'modifiers: 0x{:x}'.format(int(e.modifiers())), + 'text: {!r}'.format(e.text()), + ] + self._label.setText('\n'.join(lines)) app = QApplication([]) From c91344cdf51c55cc4ee72649e7983fdebcc59e3c Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sat, 16 May 2015 14:12:16 +0200 Subject: [PATCH 085/182] scripts: Add docstring for keytester. --- scripts/keytester.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/keytester.py b/scripts/keytester.py index dd91c86bc..0f429d66c 100644 --- a/scripts/keytester.py +++ b/scripts/keytester.py @@ -39,6 +39,7 @@ class KeyWidget(QWidget): self._layout.addWidget(self._label) def keyPressEvent(self, e): + """Show pressed keys.""" lines = [ str(utils.keyevent_to_string(e)), '', From 677cfc94102befa27b89b9ca024a40ae0888ee3c Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sat, 16 May 2015 14:12:33 +0200 Subject: [PATCH 086/182] tox: envsitespackagedir workaround for tox 2.0.1. --- tox.ini | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 31fb7bdef..2617feb99 100644 --- a/tox.ini +++ b/tox.ini @@ -15,7 +15,8 @@ envdir = {toxinidir}/.venv usedevelop = true [testenv:unittests] -setenv = QT_QPA_PLATFORM_PLUGIN_PATH={envsitepackagesdir}/PyQt5/plugins/platforms +# https://bitbucket.org/hpk42/tox/issue/246/ +setenv = QT_QPA_PLATFORM_PLUGIN_PATH={envdir}/lib/python3.4/PyQt5/plugins/platforms deps = py==1.4.27 pytest==2.7.0 From dd83a40df47f8f2a50b7e371c43be9ecf77906ee Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sat, 16 May 2015 14:13:24 +0200 Subject: [PATCH 087/182] tox: Set passenv for tox 2.0. --- tox.ini | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tox.ini b/tox.ini index 2617feb99..5c2f38390 100644 --- a/tox.ini +++ b/tox.ini @@ -17,6 +17,7 @@ usedevelop = true [testenv:unittests] # https://bitbucket.org/hpk42/tox/issue/246/ setenv = QT_QPA_PLATFORM_PLUGIN_PATH={envdir}/lib/python3.4/PyQt5/plugins/platforms +passenv = DISPLAY XAUTHORITY HOME deps = py==1.4.27 pytest==2.7.0 @@ -31,6 +32,7 @@ commands = {envpython} -m py.test --strict {posargs} [testenv:coverage] +passenv = DISPLAY XAUTHORITY HOME deps = {[testenv:unittests]deps} coverage==3.7.1 @@ -64,6 +66,7 @@ commands = [testenv:pep257] skip_install = true deps = pep257==0.5.0 +passenv = LANG # Disabled checks: # D102: Docstring missing, will be handled by others # D209: Blank line before closing """ (removed from PEP257) From 4dbc4ba93f2a4a144f97e78bab7068401e1c4088 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sat, 16 May 2015 14:22:56 +0200 Subject: [PATCH 088/182] tox: Fix QT_QPA_PLATFORM_PLUGIN_PATH. See 677cfc94102befa27b89b9ca024a40ae0888ee3c. --- tox.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index 5c2f38390..e2bdf35f7 100644 --- a/tox.ini +++ b/tox.ini @@ -15,8 +15,8 @@ envdir = {toxinidir}/.venv usedevelop = true [testenv:unittests] -# https://bitbucket.org/hpk42/tox/issue/246/ -setenv = QT_QPA_PLATFORM_PLUGIN_PATH={envdir}/lib/python3.4/PyQt5/plugins/platforms +# https://bitbucket.org/hpk42/tox/issue/246/ - only needed for Windows though +setenv = QT_QPA_PLATFORM_PLUGIN_PATH={envdir}/Lib/site-packages/PyQt5/plugins/platforms passenv = DISPLAY XAUTHORITY HOME deps = py==1.4.27 From 9b372de4a95151060be985f2a17e3ca1025cf6e7 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sat, 16 May 2015 15:51:41 +0200 Subject: [PATCH 089/182] Use fake-key scrolling for :scroll-perc 0/100. --- qutebrowser/browser/commands.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 23bdcc0dd..cdc45110c 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -165,12 +165,17 @@ class CommandDispatcher: perc = 100 elif perc is None: perc = count - perc = qtutils.check_overflow(perc, 'int', fatal=False) - frame = self._current_widget().page().currentFrame() - m = frame.scrollBarMaximum(orientation) - if m == 0: - return - frame.setScrollBarValue(orientation, int(m * perc / 100)) + if perc == 0: + self.scroll('top') + elif perc == 100: + self.scroll('bottom') + else: + perc = qtutils.check_overflow(perc, 'int', fatal=False) + frame = self._current_widget().page().currentFrame() + m = frame.scrollBarMaximum(orientation) + if m == 0: + return + frame.setScrollBarValue(orientation, int(m * perc / 100)) def _tab_move_absolute(self, idx): """Get an index for moving a tab absolutely. @@ -616,6 +621,10 @@ class CommandDispatcher: press_evt = QKeyEvent(QEvent.KeyPress, key, Qt.NoModifier, 0, 0, 0) release_evt = QKeyEvent(QEvent.KeyRelease, key, Qt.NoModifier, 0, 0, 0) + # Count doesn't make sense with top/bottom + if direction in ('top', 'bottom'): + count = 1 + for _ in range(count): widget.keyPressEvent(press_evt) widget.keyReleaseEvent(release_evt) From c762340a0cb52b28d1000529fc42f4caf5d6081b Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sat, 16 May 2015 22:12:27 +0200 Subject: [PATCH 090/182] Add --datadir/--cachedir arguments. Closes #136. --- doc/qutebrowser.1.asciidoc | 6 +++ qutebrowser/browser/adblock.py | 12 ++++- qutebrowser/browser/cache.py | 87 +++++++++++++++++++------------ qutebrowser/browser/history.py | 38 ++++++++------ qutebrowser/config/parsers/ini.py | 8 ++- qutebrowser/config/websettings.py | 25 +++++---- qutebrowser/misc/crashsignal.py | 10 +++- qutebrowser/misc/lineparser.py | 38 ++++++++++---- qutebrowser/misc/sessions.py | 17 +++++- qutebrowser/qutebrowser.py | 4 ++ qutebrowser/utils/standarddir.py | 18 +++++-- tests/config/test_config.py | 2 +- tests/misc/test_lineparser.py | 1 + tests/utils/test_standarddir.py | 27 ++++++++++ 14 files changed, 213 insertions(+), 80 deletions(-) diff --git a/doc/qutebrowser.1.asciidoc b/doc/qutebrowser.1.asciidoc index f559fb96a..2bbc9fdb8 100644 --- a/doc/qutebrowser.1.asciidoc +++ b/doc/qutebrowser.1.asciidoc @@ -41,6 +41,12 @@ show it. *-c* 'CONFDIR', *--confdir* 'CONFDIR':: Set config directory (empty for no config storage). +*--datadir* 'DATADIR':: + Set data directory (empty for no data storage). + +*--cachedir* 'CACHEDIR':: + Set cache directory (empty for no cache storage). + *-V*, *--version*:: Show version and quit. diff --git a/qutebrowser/browser/adblock.py b/qutebrowser/browser/adblock.py index 26d846074..573939d8b 100644 --- a/qutebrowser/browser/adblock.py +++ b/qutebrowser/browser/adblock.py @@ -27,7 +27,7 @@ import zipfile from qutebrowser.config import config from qutebrowser.utils import objreg, standarddir, log, message -from qutebrowser.commands import cmdutils +from qutebrowser.commands import cmdutils, cmdexc def guess_zip_filename(zf): @@ -90,12 +90,18 @@ class HostBlocker: self.blocked_hosts = set() self._in_progress = [] self._done_count = 0 - self._hosts_file = os.path.join(standarddir.data(), 'blocked-hosts') + data_dir = standarddir.data() + if data_dir is None: + self._hosts_file = None + else: + self._hosts_file = os.path.join(data_dir, 'blocked-hosts') objreg.get('config').changed.connect(self.on_config_changed) def read_hosts(self): """Read hosts from the existing blocked-hosts file.""" self.blocked_hosts = set() + if self._hosts_file is None: + return if os.path.exists(self._hosts_file): try: with open(self._hosts_file, 'r', encoding='utf-8') as f: @@ -111,6 +117,8 @@ class HostBlocker: @cmdutils.register(instance='host-blocker', win_id='win_id') def adblock_update(self, win_id): """Update the adblock block lists.""" + if self._hosts_file is None: + raise cmdexc.CommandError("No data storage is configured!") self.blocked_hosts = set() self._done_count = 0 urls = config.get('content', 'host-block-lists') diff --git a/qutebrowser/browser/cache.py b/qutebrowser/browser/cache.py index 9b7a8de05..03f6c7a2e 100644 --- a/qutebrowser/browser/cache.py +++ b/qutebrowser/browser/cache.py @@ -21,6 +21,7 @@ import os.path +from PyQt5.QtCore import pyqtSlot from PyQt5.QtNetwork import QNetworkDiskCache, QNetworkCacheMetaData from qutebrowser.config import config @@ -29,23 +30,41 @@ from qutebrowser.utils import utils, standarddir, objreg class DiskCache(QNetworkDiskCache): - """Disk cache which sets correct cache dir and size.""" + """Disk cache which sets correct cache dir and size. + + Attributes: + _activated: Whether the cache should be used. + """ def __init__(self, parent=None): super().__init__(parent) - self.setCacheDirectory(os.path.join(standarddir.cache(), 'http')) + cache_dir = standarddir.cache() + if config.get('general', 'private-browsing') or cache_dir is None: + self._activated = False + else: + self._activated = True + self.setCacheDirectory(os.path.join(standarddir.cache(), 'http')) self.setMaximumCacheSize(config.get('storage', 'cache-size')) - objreg.get('config').changed.connect(self.cache_size_changed) + objreg.get('config').changed.connect(self.on_config_changed) def __repr__(self): return utils.get_repr(self, size=self.cacheSize(), maxsize=self.maximumCacheSize(), path=self.cacheDirectory()) - @config.change_filter('storage', 'cache-size') - def cache_size_changed(self): - """Update cache size if the config was changed.""" - self.setMaximumCacheSize(config.get('storage', 'cache-size')) + @pyqtSlot() + def on_config_changed(self, section, option): + """Update cache size/activated if the config was changed.""" + if (section, option) == ('storage', 'cache-size'): + self.setMaximumCacheSize(config.get('storage', 'cache-size')) + elif (section, option) == ('general', 'private-browsing'): + if (config.get('general', 'private-browsing') or + standarddir.cache() is None): + self._activated = False + else: + self._activated = True + self.setCacheDirectory( + os.path.join(standarddir.cache(), 'http')) def cacheSize(self): """Return the current size taken up by the cache. @@ -53,10 +72,10 @@ class DiskCache(QNetworkDiskCache): Return: An int. """ - if config.get('general', 'private-browsing'): - return 0 - else: + if self._activated: return super().cacheSize() + else: + return 0 def fileMetaData(self, filename): """Return the QNetworkCacheMetaData for the cache file filename. @@ -67,10 +86,10 @@ class DiskCache(QNetworkDiskCache): Return: A QNetworkCacheMetaData object. """ - if config.get('general', 'private-browsing'): - return QNetworkCacheMetaData() - else: + if self._activated: return super().fileMetaData(filename) + else: + return QNetworkCacheMetaData() def data(self, url): """Return the data associated with url. @@ -81,10 +100,10 @@ class DiskCache(QNetworkDiskCache): return: A QIODevice or None. """ - if config.get('general', 'private-browsing'): - return None - else: + if self._activated: return super().data(url) + else: + return None def insert(self, device): """Insert the data in device and the prepared meta data into the cache. @@ -92,10 +111,10 @@ class DiskCache(QNetworkDiskCache): Args: device: A QIODevice. """ - if config.get('general', 'private-browsing'): - return - else: + if self._activated: super().insert(device) + else: + return None def metaData(self, url): """Return the meta data for the url url. @@ -106,10 +125,10 @@ class DiskCache(QNetworkDiskCache): Return: A QNetworkCacheMetaData object. """ - if config.get('general', 'private-browsing'): - return QNetworkCacheMetaData() - else: + if self._activated: return super().metaData(url) + else: + return QNetworkCacheMetaData() def prepare(self, meta_data): """Return the device that should be populated with the data. @@ -120,10 +139,10 @@ class DiskCache(QNetworkDiskCache): Return: A QIODevice or None. """ - if config.get('general', 'private-browsing'): - return None - else: + if self._activated: return super().prepare(meta_data) + else: + return None def remove(self, url): """Remove the cache entry for url. @@ -131,10 +150,10 @@ class DiskCache(QNetworkDiskCache): Return: True on success, False otherwise. """ - if config.get('general', 'private-browsing'): - return False - else: + if self._activated: return super().remove(url) + else: + return False def updateMetaData(self, meta_data): """Update the cache meta date for the meta_data's url to meta_data. @@ -142,14 +161,14 @@ class DiskCache(QNetworkDiskCache): Args: meta_data: A QNetworkCacheMetaData object. """ - if config.get('general', 'private-browsing'): - return - else: + if self._activated: super().updateMetaData(meta_data) + else: + return def clear(self): """Remove all items from the cache.""" - if config.get('general', 'private-browsing'): - return - else: + if self._activated: super().clear() + else: + return diff --git a/qutebrowser/browser/history.py b/qutebrowser/browser/history.py index b3370d0c1..2ffbf70c1 100644 --- a/qutebrowser/browser/history.py +++ b/qutebrowser/browser/history.py @@ -83,6 +83,28 @@ class WebHistory(QWebHistoryInterface): self._lineparser = lineparser.AppendLineParser( standarddir.data(), 'history', parent=self) self._history_dict = collections.OrderedDict() + self._read_history() + self._new_history = [] + self._saved_count = 0 + objreg.get('save-manager').add_saveable( + 'history', self.save, self.item_added) + + def __repr__(self): + return utils.get_repr(self, length=len(self)) + + def __getitem__(self, key): + return self._new_history[key] + + def __iter__(self): + return iter(self._history_dict.values()) + + def __len__(self): + return len(self._history_dict) + + def _read_history(self): + """Read the initial history.""" + if standarddir.data() is None: + return with self._lineparser.open(): for line in self._lineparser: data = line.rstrip().split(maxsplit=1) @@ -108,22 +130,6 @@ class WebHistory(QWebHistoryInterface): # list of atimes. self._history_dict[url] = HistoryEntry(atime, url) self._history_dict.move_to_end(url) - self._new_history = [] - self._saved_count = 0 - objreg.get('save-manager').add_saveable( - 'history', self.save, self.item_added) - - def __repr__(self): - return utils.get_repr(self, length=len(self)) - - def __getitem__(self, key): - return self._new_history[key] - - def __iter__(self): - return iter(self._history_dict.values()) - - def __len__(self): - return len(self._history_dict) def get_recent(self): """Get the most recent history entries.""" diff --git a/qutebrowser/config/parsers/ini.py b/qutebrowser/config/parsers/ini.py index e8d24d249..a430ff454 100644 --- a/qutebrowser/config/parsers/ini.py +++ b/qutebrowser/config/parsers/ini.py @@ -47,11 +47,15 @@ class ReadConfigParser(configparser.ConfigParser): self.optionxform = lambda opt: opt # be case-insensitive self._configdir = configdir self._fname = fname + if self._configdir is None: + self._configfile = None + return self._configfile = os.path.join(self._configdir, fname) if not os.path.isfile(self._configfile): return log.init.debug("Reading config from {}".format(self._configfile)) - self.read(self._configfile, encoding='utf-8') + if self._configfile is not None: + self.read(self._configfile, encoding='utf-8') def __repr__(self): return utils.get_repr(self, constructor=True, @@ -64,6 +68,8 @@ class ReadWriteConfigParser(ReadConfigParser): def save(self): """Save the config file.""" + if self._configdir is None: + return if not os.path.exists(self._configdir): os.makedirs(self._configdir, 0o755) log.destroy.debug("Saving config to {}".format(self._configfile)) diff --git a/qutebrowser/config/websettings.py b/qutebrowser/config/websettings.py index b89fbd3c5..117abbb26 100644 --- a/qutebrowser/config/websettings.py +++ b/qutebrowser/config/websettings.py @@ -377,16 +377,20 @@ MAPPINGS = { def init(): """Initialize the global QWebSettings.""" - if config.get('general', 'private-browsing'): + cache_path = standarddir.cache() + data_path = standarddir.data() + if config.get('general', 'private-browsing') or cache_path is None: QWebSettings.setIconDatabasePath('') else: - QWebSettings.setIconDatabasePath(standarddir.cache()) - QWebSettings.setOfflineWebApplicationCachePath( - os.path.join(standarddir.cache(), 'application-cache')) - QWebSettings.globalSettings().setLocalStoragePath( - os.path.join(standarddir.data(), 'local-storage')) - QWebSettings.setOfflineStoragePath( - os.path.join(standarddir.data(), 'offline-storage')) + QWebSettings.setIconDatabasePath(cache_path) + if cache_path is not None: + QWebSettings.setOfflineWebApplicationCachePath( + os.path.join(cache_path, 'application-cache')) + if data_path is not None: + QWebSettings.globalSettings().setLocalStoragePath( + os.path.join(data_path, 'local-storage')) + QWebSettings.setOfflineStoragePath( + os.path.join(data_path, 'offline-storage')) for sectname, section in MAPPINGS.items(): for optname, mapping in section.items(): @@ -402,11 +406,12 @@ def init(): def update_settings(section, option): """Update global settings when qwebsettings changed.""" + cache_path = standarddir.cache() if (section, option) == ('general', 'private-browsing'): - if config.get('general', 'private-browsing'): + if config.get('general', 'private-browsing') or cache_path is None: QWebSettings.setIconDatabasePath('') else: - QWebSettings.setIconDatabasePath(standarddir.cache()) + QWebSettings.setIconDatabasePath(cache_path) else: try: mapping = MAPPINGS[section][option] diff --git a/qutebrowser/misc/crashsignal.py b/qutebrowser/misc/crashsignal.py index 76e14edd9..ca5350c9e 100644 --- a/qutebrowser/misc/crashsignal.py +++ b/qutebrowser/misc/crashsignal.py @@ -63,7 +63,10 @@ class CrashHandler(QObject): def handle_segfault(self): """Handle a segfault from a previous run.""" - logname = os.path.join(standarddir.data(), 'crash.log') + data_dir = None + if data_dir is None: + return + logname = os.path.join(data_dir, 'crash.log') try: # First check if an old logfile exists. if os.path.exists(logname): @@ -118,7 +121,10 @@ class CrashHandler(QObject): def _init_crashlogfile(self): """Start a new logfile and redirect faulthandler to it.""" - logname = os.path.join(standarddir.data(), 'crash.log') + data_dir = standarddir.data() + if data_dir is None: + return + logname = os.path.join(data_dir, 'crash.log') try: self._crash_log_file = open(logname, 'w', encoding='ascii') except OSError: diff --git a/qutebrowser/misc/lineparser.py b/qutebrowser/misc/lineparser.py index 5cd6304f5..c34760217 100644 --- a/qutebrowser/misc/lineparser.py +++ b/qutebrowser/misc/lineparser.py @@ -35,7 +35,7 @@ class BaseLineParser(QObject): """A LineParser without any real data. Attributes: - _configdir: The directory to read the config from. + _configdir: Directory to read the config from, or None. _configfile: The config file path. _fname: Filename of the config. _binary: Whether to open the file in binary mode. @@ -57,7 +57,10 @@ class BaseLineParser(QObject): """ super().__init__(parent) self._configdir = configdir - self._configfile = os.path.join(self._configdir, fname) + if self._configdir is None: + self._configfile = None + else: + self._configfile = os.path.join(self._configdir, fname) self._fname = fname self._binary = binary self._opened = False @@ -68,10 +71,17 @@ class BaseLineParser(QObject): binary=self._binary) def _prepare_save(self): - """Prepare saving of the file.""" + """Prepare saving of the file. + + Return: + True if the file should be saved, False otherwise. + """ + if self._configdir is None: + return False log.destroy.debug("Saving to {}".format(self._configfile)) if not os.path.exists(self._configdir): os.makedirs(self._configdir, 0o755) + return True @contextlib.contextmanager def _open(self, mode): @@ -80,6 +90,7 @@ class BaseLineParser(QObject): Args: mode: The mode to use ('a'/'r'/'w') """ + assert self._configfile is not None if self._opened: raise IOError("Refusing to double-open AppendLineParser.") self._opened = True @@ -159,7 +170,9 @@ class AppendLineParser(BaseLineParser): return data def save(self): - self._prepare_save() + do_save = self._prepare_save() + if not do_save: + return with self._open('a') as f: self._write(f, self.new_data) self.new_data = [] @@ -182,7 +195,7 @@ class LineParser(BaseLineParser): binary: Whether to open the file in binary mode. """ super().__init__(configdir, fname, binary=binary, parent=parent) - if not os.path.isfile(self._configfile): + if configdir is None or not os.path.isfile(self._configfile): self.data = [] else: log.init.debug("Reading {}".format(self._configfile)) @@ -206,8 +219,11 @@ class LineParser(BaseLineParser): """Save the config file.""" if self._opened: raise IOError("Refusing to double-open AppendLineParser.") + do_save = self._prepare_save() + if not do_save: + return self._opened = True - self._prepare_save() + assert self._configfile is not None with qtutils.savefile_open(self._configfile, self._binary) as f: self._write(f, self.data) self._opened = False @@ -226,14 +242,14 @@ class LimitLineParser(LineParser): """Constructor. Args: - configdir: Directory to read the config from. + configdir: Directory to read the config from, or None. fname: Filename of the config file. limit: Config tuple (section, option) which contains a limit. binary: Whether to open the file in binary mode. """ super().__init__(configdir, fname, binary=binary, parent=parent) self._limit = limit - if limit is not None: + if limit is not None and configdir is not None: objreg.get('config').changed.connect(self.cleanup_file) def __repr__(self): @@ -244,6 +260,7 @@ class LimitLineParser(LineParser): @pyqtSlot(str, str) def cleanup_file(self, section, option): """Delete the file if the limit was changed to 0.""" + assert self._configfile is not None if (section, option) != self._limit: return value = config.get(section, option) @@ -256,6 +273,9 @@ class LimitLineParser(LineParser): limit = config.get(*self._limit) if limit == 0: return - self._prepare_save() + do_save = self._prepare_save() + if not do_save: + return + assert self._configfile is not None with qtutils.savefile_open(self._configfile, self._binary) as f: self._write(f, self.data[-limit:]) diff --git a/qutebrowser/misc/sessions.py b/qutebrowser/misc/sessions.py index 2d9cc222d..bd75c634b 100644 --- a/qutebrowser/misc/sessions.py +++ b/qutebrowser/misc/sessions.py @@ -82,10 +82,14 @@ class SessionManager(QObject): def __init__(self, parent=None): super().__init__(parent) self._current = None - self._base_path = os.path.join(standarddir.data(), 'sessions') + data_dir = standarddir.data() + if data_dir is None: + self._base_path = None + else: + self._base_path = os.path.join(standarddir.data(), 'sessions') self._last_window_session = None self.did_load = False - if not os.path.exists(self._base_path): + if self._base_path is not None and not os.path.exists(self._base_path): os.mkdir(self._base_path) def _get_session_path(self, name, check_exists=False): @@ -100,6 +104,11 @@ class SessionManager(QObject): if os.path.isabs(path) and ((not check_exists) or os.path.exists(path)): return path + elif self._base_path is None: + if check_exists: + raise SessionNotFoundError(name) + else: + return None else: path = os.path.join(self._base_path, name + '.yml') if check_exists and not os.path.exists(path): @@ -194,6 +203,8 @@ class SessionManager(QObject): else: name = 'default' path = self._get_session_path(name) + if path is None: + raise SessionError("No data storage configured.") log.sessions.debug("Saving session {} to {}...".format(name, path)) if last_window: @@ -289,6 +300,8 @@ class SessionManager(QObject): def list_sessions(self): """Get a list of all session names.""" sessions = [] + if self._base_path is None: + return sessions for filename in os.listdir(self._base_path): base, ext = os.path.splitext(filename) if ext == '.yml': diff --git a/qutebrowser/qutebrowser.py b/qutebrowser/qutebrowser.py index 51b82604c..1ae9f60e2 100644 --- a/qutebrowser/qutebrowser.py +++ b/qutebrowser/qutebrowser.py @@ -48,6 +48,10 @@ def get_argparser(): description=qutebrowser.__description__) parser.add_argument('-c', '--confdir', help="Set config directory (empty " "for no config storage).") + parser.add_argument('--datadir', help="Set data directory (empty for " + "no data storage).") + parser.add_argument('--cachedir', help="Set cache directory (empty for " + "no cache storage).") parser.add_argument('-V', '--version', help="Show version and quit.", action='store_true') parser.add_argument('-s', '--set', help="Set a temporary setting for " diff --git a/qutebrowser/utils/standarddir.py b/qutebrowser/utils/standarddir.py index f2a765513..aeba60b77 100644 --- a/qutebrowser/utils/standarddir.py +++ b/qutebrowser/utils/standarddir.py @@ -80,7 +80,9 @@ def _from_args(typ, args): path: The overridden path, or None to turn off storage. """ typ_to_argparse_arg = { - QStandardPaths.ConfigLocation: 'confdir' + QStandardPaths.ConfigLocation: 'confdir', + QStandardPaths.DataLocation: 'datadir', + QStandardPaths.CacheLocation: 'cachedir', } if args is None: return (False, None) @@ -135,8 +137,18 @@ def init(args): """Initialize all standard dirs.""" global _args _args = args - # http://www.brynosaurus.com/cachedir/spec.html - cachedir_tag = os.path.join(cache(), 'CACHEDIR.TAG') + _init_cachedir_tag() + + +def _init_cachedir_tag(): + """Create CACHEDIR.TAG if it doesn't exist. + + See http://www.brynosaurus.com/cachedir/spec.html + """ + cache_dir = cache() + if cache_dir is None: + return + cachedir_tag = os.path.join(cache_dir, 'CACHEDIR.TAG') if not os.path.exists(cachedir_tag): try: with open(cachedir_tag, 'w', encoding='utf-8') as f: diff --git a/tests/config/test_config.py b/tests/config/test_config.py index 636d89c9b..123b2a412 100644 --- a/tests/config/test_config.py +++ b/tests/config/test_config.py @@ -280,7 +280,7 @@ class TestConfigInit: def test_config_none(self, monkeypatch): """Test initializing with config path set to None.""" - args = types.SimpleNamespace(confdir='') + args = types.SimpleNamespace(confdir='', datadir='', cachedir='') for k, v in self.env.items(): monkeypatch.setenv(k, v) standarddir.init(args) diff --git a/tests/misc/test_lineparser.py b/tests/misc/test_lineparser.py index c473eda50..7dfb364e8 100644 --- a/tests/misc/test_lineparser.py +++ b/tests/misc/test_lineparser.py @@ -69,6 +69,7 @@ class LineParserWrapper: def _prepare_save(self): """Keep track if _prepare_save has been called.""" self._test_save_prepared = True + return True class TestableAppendLineParser(LineParserWrapper, lineparser.AppendLineParser): diff --git a/tests/utils/test_standarddir.py b/tests/utils/test_standarddir.py index e98ab80f9..4e1519ded 100644 --- a/tests/utils/test_standarddir.py +++ b/tests/utils/test_standarddir.py @@ -22,6 +22,7 @@ import os import os.path import sys +import types from PyQt5.QtWidgets import QApplication import pytest @@ -114,3 +115,29 @@ class TestGetStandardDirWindows: """Test cache dir.""" expected = ['qutebrowser_test', 'cache'] assert standarddir.cache().split(os.sep)[-2:] == expected + + +class TestArguments: + + """Tests with confdir/cachedir/datadir arguments.""" + + @pytest.mark.parametrize('arg, expected', [('', None), ('foo', 'foo')]) + def test_confdir(self, arg, expected): + """Test --confdir.""" + args = types.SimpleNamespace(confdir=arg, cachedir=None, datadir=None) + standarddir.init(args) + assert standarddir.config() == expected + + @pytest.mark.parametrize('arg, expected', [('', None), ('foo', 'foo')]) + def test_confdir(self, arg, expected): + """Test --cachedir.""" + args = types.SimpleNamespace(confdir=None, cachedir=arg, datadir=None) + standarddir.init(args) + assert standarddir.cache() == expected + + @pytest.mark.parametrize('arg, expected', [('', None), ('foo', 'foo')]) + def test_datadir(self, arg, expected): + """Test --datadir.""" + args = types.SimpleNamespace(confdir=None, cachedir=None, datadir=arg) + standarddir.init(args) + assert standarddir.data() == expected From 42c27ddbc0ba9cbca8ae73b3b1055c6dec6b1ef4 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sat, 16 May 2015 22:30:00 +0200 Subject: [PATCH 091/182] Use temp dir for standarddir arg tests. --- tests/utils/test_standarddir.py | 37 ++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/tests/utils/test_standarddir.py b/tests/utils/test_standarddir.py index 4e1519ded..415fc03d9 100644 --- a/tests/utils/test_standarddir.py +++ b/tests/utils/test_standarddir.py @@ -23,6 +23,7 @@ import os import os.path import sys import types +import collections from PyQt5.QtWidgets import QApplication import pytest @@ -117,27 +118,39 @@ class TestGetStandardDirWindows: assert standarddir.cache().split(os.sep)[-2:] == expected +DirArgTest = collections.namedtuple('DirArgTest', 'arg, expected') + + class TestArguments: """Tests with confdir/cachedir/datadir arguments.""" - @pytest.mark.parametrize('arg, expected', [('', None), ('foo', 'foo')]) - def test_confdir(self, arg, expected): + @pytest.fixture(params=[DirArgTest('', None), DirArgTest('foo', 'foo')]) + def testcase(self, request, tmpdir): + """Fixture providing testcases.""" + if request.param.expected is None: + return request.param + else: + arg = str(tmpdir / request.param.arg) + return DirArgTest(arg, arg) + + def test_confdir(self, testcase): """Test --confdir.""" - args = types.SimpleNamespace(confdir=arg, cachedir=None, datadir=None) + args = types.SimpleNamespace(confdir=testcase.arg, cachedir=None, + datadir=None) standarddir.init(args) - assert standarddir.config() == expected + assert standarddir.config() == testcase.expected - @pytest.mark.parametrize('arg, expected', [('', None), ('foo', 'foo')]) - def test_confdir(self, arg, expected): + def test_confdir(self, testcase): """Test --cachedir.""" - args = types.SimpleNamespace(confdir=None, cachedir=arg, datadir=None) + args = types.SimpleNamespace(confdir=None, cachedir=testcase.arg, + datadir=None) standarddir.init(args) - assert standarddir.cache() == expected + assert standarddir.cache() == testcase.expected - @pytest.mark.parametrize('arg, expected', [('', None), ('foo', 'foo')]) - def test_datadir(self, arg, expected): + def test_datadir(self, testcase): """Test --datadir.""" - args = types.SimpleNamespace(confdir=None, cachedir=None, datadir=arg) + args = types.SimpleNamespace(confdir=None, cachedir=None, + datadir=testcase.arg) standarddir.init(args) - assert standarddir.data() == expected + assert standarddir.data() == testcase.expected From 183049ef2ef591108b358e7f6c1ca3be295b3344 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sat, 16 May 2015 22:48:13 +0200 Subject: [PATCH 092/182] Make sure self._opened is reset on exceptions. --- qutebrowser/misc/lineparser.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/qutebrowser/misc/lineparser.py b/qutebrowser/misc/lineparser.py index c34760217..4a5e1f3ff 100644 --- a/qutebrowser/misc/lineparser.py +++ b/qutebrowser/misc/lineparser.py @@ -94,13 +94,15 @@ class BaseLineParser(QObject): if self._opened: raise IOError("Refusing to double-open AppendLineParser.") self._opened = True - if self._binary: - with open(self._configfile, mode + 'b') as f: - yield f - else: - with open(self._configfile, mode, encoding='utf-8') as f: - yield f - self._opened = False + try: + if self._binary: + with open(self._configfile, mode + 'b') as f: + yield f + else: + with open(self._configfile, mode, encoding='utf-8') as f: + yield f + finally: + self._opened = False def _write(self, fp, data): """Write the data to a file. @@ -223,10 +225,12 @@ class LineParser(BaseLineParser): if not do_save: return self._opened = True - assert self._configfile is not None - with qtutils.savefile_open(self._configfile, self._binary) as f: - self._write(f, self.data) - self._opened = False + try: + assert self._configfile is not None + with qtutils.savefile_open(self._configfile, self._binary) as f: + self._write(f, self.data) + finally: + self._opened = False class LimitLineParser(LineParser): From aab54113171dc83c2fd23f2844ac7a7c8d6fb453 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sat, 16 May 2015 23:06:33 +0200 Subject: [PATCH 093/182] Fix test function name. --- tests/utils/test_standarddir.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/utils/test_standarddir.py b/tests/utils/test_standarddir.py index 415fc03d9..ede5f8964 100644 --- a/tests/utils/test_standarddir.py +++ b/tests/utils/test_standarddir.py @@ -141,7 +141,7 @@ class TestArguments: standarddir.init(args) assert standarddir.config() == testcase.expected - def test_confdir(self, testcase): + def test_cachedir(self, testcase): """Test --cachedir.""" args = types.SimpleNamespace(confdir=None, cachedir=testcase.arg, datadir=None) From 54131e9d3e9e2559dfada27ea0dcdff8f7731f9c Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sat, 16 May 2015 23:10:20 +0200 Subject: [PATCH 094/182] Add --basedir arg with multiple instance support. Closes #510. --- doc/qutebrowser.1.asciidoc | 3 +++ qutebrowser/app.py | 19 ++++++++++------ qutebrowser/misc/ipc.py | 37 +++++++++++++++++++++++--------- qutebrowser/qutebrowser.py | 2 ++ qutebrowser/utils/standarddir.py | 14 ++++++++++++ tests/utils/test_standarddir.py | 10 +++++++++ 6 files changed, 69 insertions(+), 16 deletions(-) diff --git a/doc/qutebrowser.1.asciidoc b/doc/qutebrowser.1.asciidoc index 2bbc9fdb8..dd7857c80 100644 --- a/doc/qutebrowser.1.asciidoc +++ b/doc/qutebrowser.1.asciidoc @@ -47,6 +47,9 @@ show it. *--cachedir* 'CACHEDIR':: Set cache directory (empty for no cache storage). +*--basedir* 'BASEDIR':: + Base directory for all storage. Other --*dir arguments are ignored if this is given. + *-V*, *--version*:: Show version and quit. diff --git a/qutebrowser/app.py b/qutebrowser/app.py index e5dba1fac..2928b3bf8 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -84,11 +84,11 @@ def run(args): objreg.register('signal-handler', signal_handler) try: - sent = ipc.send_to_running_instance(args.command) + sent = ipc.send_to_running_instance(args) if sent: sys.exit(0) log.init.debug("Starting IPC server...") - server = ipc.IPCServer(qApp) + server = ipc.IPCServer(args, qApp) objreg.register('ipc-server', server) server.got_args.connect(lambda args, cwd: process_pos_args(args, cwd=cwd, via_ipc=True)) @@ -96,7 +96,7 @@ def run(args): # This could be a race condition... log.init.debug("Got AddressInUseError, trying again.") time.sleep(500) - sent = ipc.send_to_running_instance(args.command) + sent = ipc.send_to_running_instance(args) if sent: sys.exit(0) else: @@ -199,7 +199,7 @@ def _process_args(args): process_pos_args(args.command) _open_startpage() - _open_quickstart() + _open_quickstart(args) def _load_session(name): @@ -303,8 +303,15 @@ def _open_startpage(win_id=None): tabbed_browser.tabopen(url) -def _open_quickstart(): - """Open quickstart if it's the first start.""" +def _open_quickstart(args): + """Open quickstart if it's the first start. + + Args: + args: The argparse namespace. + """ + if args.datadir is not None or args.basedir is not None: + # With --datadir or --basedir given, don't open quickstart. + return state_config = objreg.get('state-config') try: quickstart_done = state_config['general']['quickstart-done'] == '1' diff --git a/qutebrowser/misc/ipc.py b/qutebrowser/misc/ipc.py index d38606cc7..ba0ec87c9 100644 --- a/qutebrowser/misc/ipc.py +++ b/qutebrowser/misc/ipc.py @@ -23,6 +23,7 @@ import os import json import getpass import binascii +import hashlib from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject from PyQt5.QtNetwork import QLocalSocket, QLocalServer, QAbstractSocket @@ -31,12 +32,20 @@ from PyQt5.QtWidgets import QMessageBox from qutebrowser.utils import log, usertypes -SOCKETNAME = 'qutebrowser-{}'.format(getpass.getuser()) CONNECT_TIMEOUT = 100 WRITE_TIMEOUT = 1000 READ_TIMEOUT = 5000 +def _get_socketname(args): + """Get a socketname to use.""" + parts = ['qutebrowser', getpass.getuser()] + if args.basedir is not None: + md5 = hashlib.md5(args.basedir.encode('utf-8')) + parts.append(md5.hexdigest()) + return '-'.join(parts) + + class Error(Exception): """Exception raised when there was a problem with IPC.""" @@ -80,6 +89,7 @@ class IPCServer(QObject): _timer: A timer to handle timeouts. _server: A QLocalServer to accept new connections. _socket: The QLocalSocket we're currently connected to. + _socketname: The socketname to use. Signals: got_args: Emitted when there was an IPC connection and arguments were @@ -88,16 +98,22 @@ class IPCServer(QObject): got_args = pyqtSignal(list, str) - def __init__(self, parent=None): - """Start the IPC server and listen to commands.""" + def __init__(self, args, parent=None): + """Start the IPC server and listen to commands. + + Args: + args: The argparse namespace. + parent: The parent to be used. + """ super().__init__(parent) self.ignored = False + self._socketname = _get_socketname(args) self._remove_server() self._timer = usertypes.Timer(self, 'ipc-timeout') self._timer.setInterval(READ_TIMEOUT) self._timer.timeout.connect(self.on_timeout) self._server = QLocalServer(self) - ok = self._server.listen(SOCKETNAME) + ok = self._server.listen(self._socketname) if not ok: if self._server.serverError() == QAbstractSocket.AddressInUseError: raise AddressInUseError(self._server) @@ -108,9 +124,10 @@ class IPCServer(QObject): def _remove_server(self): """Remove an existing server.""" - ok = QLocalServer.removeServer(SOCKETNAME) + ok = QLocalServer.removeServer(self._socketname) if not ok: - raise Error("Error while removing server {}!".format(SOCKETNAME)) + raise Error("Error while removing server {}!".format( + self._socketname)) @pyqtSlot(int) def on_error(self, error): @@ -223,23 +240,23 @@ def _socket_error(action, socket): action, socket.errorString(), socket.error())) -def send_to_running_instance(cmdlist): +def send_to_running_instance(args): """Try to send a commandline to a running instance. Blocks for CONNECT_TIMEOUT ms. Args: - cmdlist: A list to send (URLs/commands) + args: The argparse namespace. Return: True if connecting was successful, False if no connection was made. """ socket = QLocalSocket() - socket.connectToServer(SOCKETNAME) + socket.connectToServer(_get_socketname(args)) connected = socket.waitForConnected(100) if connected: log.ipc.info("Opening in existing instance") - json_data = {'args': cmdlist} + json_data = {'args': args.command} try: cwd = os.getcwd() except OSError: diff --git a/qutebrowser/qutebrowser.py b/qutebrowser/qutebrowser.py index 1ae9f60e2..21604c406 100644 --- a/qutebrowser/qutebrowser.py +++ b/qutebrowser/qutebrowser.py @@ -52,6 +52,8 @@ def get_argparser(): "no data storage).") parser.add_argument('--cachedir', help="Set cache directory (empty for " "no cache storage).") + parser.add_argument('--basedir', help="Base directory for all storage. " + "Other --*dir arguments are ignored if this is given.") parser.add_argument('-V', '--version', help="Show version and quit.", action='store_true') parser.add_argument('-s', '--set', help="Set a temporary setting for " diff --git a/qutebrowser/utils/standarddir.py b/qutebrowser/utils/standarddir.py index aeba60b77..9dd8c5752 100644 --- a/qutebrowser/utils/standarddir.py +++ b/qutebrowser/utils/standarddir.py @@ -84,8 +84,22 @@ def _from_args(typ, args): QStandardPaths.DataLocation: 'datadir', QStandardPaths.CacheLocation: 'cachedir', } + basedir_suffix = { + QStandardPaths.ConfigLocation: 'config', + QStandardPaths.DataLocation: 'data', + QStandardPaths.CacheLocation: 'cache', + QStandardPaths.DownloadLocation: 'download', + QStandardPaths.RuntimeLocation: 'runtime', + } + if args is None: return (False, None) + + if getattr(args, 'basedir', None) is not None: + basedir = args.basedir + suffix = basedir_suffix[typ] + return (True, os.path.join(basedir, suffix)) + try: argname = typ_to_argparse_arg[typ] except KeyError: diff --git a/tests/utils/test_standarddir.py b/tests/utils/test_standarddir.py index ede5f8964..6be44d92c 100644 --- a/tests/utils/test_standarddir.py +++ b/tests/utils/test_standarddir.py @@ -154,3 +154,13 @@ class TestArguments: datadir=testcase.arg) standarddir.init(args) assert standarddir.data() == testcase.expected + + @pytest.mark.parametrize('typ', ['config', 'data', 'cache', 'download', + 'runtime']) + def test_basedir(self, tmpdir, typ): + """Test --basedir.""" + expected = str(tmpdir / typ) + args = types.SimpleNamespace(basedir=str(tmpdir)) + standarddir.init(args) + func = getattr(standarddir, typ) + assert func() == expected From d7999577ddfaf5c6946a514bfcab3a7a62824198 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sat, 16 May 2015 23:13:36 +0200 Subject: [PATCH 095/182] Fix shutdown from pdb. --- qutebrowser/misc/crashsignal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/misc/crashsignal.py b/qutebrowser/misc/crashsignal.py index ca5350c9e..48d14a797 100644 --- a/qutebrowser/misc/crashsignal.py +++ b/qutebrowser/misc/crashsignal.py @@ -186,7 +186,7 @@ class CrashHandler(QObject): # pdb exit, KeyboardInterrupt, ... status = 0 if is_ignored_exception else 2 try: - qapp.shutdown(status) + self._quitter.shutdown(status) return except Exception: log.init.exception("Error while shutting down") From a1f7eed5a70ecada37bd7bcaeac50134e5b9a6ea Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sat, 16 May 2015 23:26:15 +0200 Subject: [PATCH 096/182] Add --temp-basedir option. --- doc/qutebrowser.1.asciidoc | 3 +++ qutebrowser/app.py | 8 ++++++++ qutebrowser/qutebrowser.py | 2 ++ 3 files changed, 13 insertions(+) diff --git a/doc/qutebrowser.1.asciidoc b/doc/qutebrowser.1.asciidoc index dd7857c80..2a403de5f 100644 --- a/doc/qutebrowser.1.asciidoc +++ b/doc/qutebrowser.1.asciidoc @@ -96,6 +96,9 @@ show it. *--pdb-postmortem*:: Drop into pdb on exceptions. +*--temp-basedir*:: + Use a temporary basedir. + *--qt-name* 'NAME':: Set the window name. diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 2928b3bf8..02f8af907 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -26,6 +26,8 @@ import configparser import functools import json import time +import shutil +import tempfile from PyQt5.QtWidgets import QApplication, QMessageBox from PyQt5.QtGui import QDesktopServices, QPixmap, QIcon, QCursor, QWindow @@ -66,6 +68,9 @@ def run(args): print(version.GPL_BOILERPLATE.strip()) sys.exit(0) + if args.temp_basedir: + args.basedir = tempfile.mkdtemp() + quitter = Quitter(args) objreg.register('quitter', quitter) @@ -638,6 +643,9 @@ class Quitter: # Re-enable faulthandler to stdout, then remove crash log log.destroy.debug("Deactivating crash log...") objreg.get('crash-handler').destroy_crashlogfile() + # Delete temp basedir + if self._args.temp_basedir: + shutil.rmtree(self._args.basedir) # If we don't kill our custom handler here we might get segfaults log.destroy.debug("Deactiving message handler...") qInstallMessageHandler(None) diff --git a/qutebrowser/qutebrowser.py b/qutebrowser/qutebrowser.py index 21604c406..03f3c582b 100644 --- a/qutebrowser/qutebrowser.py +++ b/qutebrowser/qutebrowser.py @@ -94,6 +94,8 @@ def get_argparser(): "show a crash dialog.") debug.add_argument('--pdb-postmortem', action='store_true', help="Drop into pdb on exceptions.") + debug.add_argument('--temp-basedir', action='store_true', help="Use a " + "temporary basedir.") # For the Qt args, we use store_const with const=True rather than # store_true because we want the default to be None, to make # utils.qt:get_args easier. From 62426380e5331049fdb04cf63bc8ccdd79121705 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sat, 16 May 2015 23:29:23 +0200 Subject: [PATCH 097/182] Update changelog. --- CHANGELOG.asciidoc | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index de3c12d53..20d905ada 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -27,6 +27,10 @@ Added - New setting `content -> webgl` to enable/disable https://www.khronos.org/webgl/[WebGL]. - New setting `content -> css-regions` to enable/disable support for http://dev.w3.org/csswg/css-regions/[CSS Regions]. - New setting `content -> hyperlink-auditing` to enable/disable support for https://html.spec.whatwg.org/multipage/semantics.html#hyperlink-auditing[hyperlink auditing]. +- Support for Qt 5.5 and tox 2.0 +- New arguments `--datadir` and `--cachedir` to set the data/cache location. +- New arguments `--basedir` and `--temp-basedir` (intended for debugging) to set a different base directory for all data, which allows multiple invocations. + Changed ~~~~~~~ From 9be5992a9a564df9838181374f5519395fbda3a2 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 27 Mar 2015 07:59:13 +0100 Subject: [PATCH 098/182] Smoke test WIP --- doc/qutebrowser.1.asciidoc | 3 ++ qutebrowser/app.py | 52 +++++++++++++++---------------- qutebrowser/config/config.py | 26 ++++++++-------- qutebrowser/misc/checkpyver.py | 2 +- qutebrowser/misc/crashdialog.py | 35 +++++++++++++++++++++ qutebrowser/misc/earlyinit.py | 19 +++++++----- qutebrowser/qutebrowser.py | 2 ++ qutebrowser/utils/error.py | 54 +++++++++++++++++++++++++++++++++ qutebrowser/utils/usertypes.py | 5 +++ tox.ini | 7 +++++ 10 files changed, 156 insertions(+), 49 deletions(-) create mode 100644 qutebrowser/utils/error.py diff --git a/doc/qutebrowser.1.asciidoc b/doc/qutebrowser.1.asciidoc index 87d0329d7..3cf07a057 100644 --- a/doc/qutebrowser.1.asciidoc +++ b/doc/qutebrowser.1.asciidoc @@ -80,6 +80,9 @@ It was inspired by other browsers/addons like dwb and Vimperator/Pentadactyl. *--no-crash-dialog*:: Don't show a crash dialog. +*--no-err-windows*:: + Don't show any error windows. + *--qt-name* 'NAME':: Set the window name. diff --git a/qutebrowser/app.py b/qutebrowser/app.py index be0ede32e..360984070 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -30,7 +30,7 @@ import functools import traceback import faulthandler -from PyQt5.QtWidgets import QApplication, QDialog, QMessageBox +from PyQt5.QtWidgets import QApplication, QDialog from PyQt5.QtGui import QDesktopServices, QPixmap, QIcon from PyQt5.QtCore import (pyqtSlot, qInstallMessageHandler, QTimer, QUrl, QObject, Qt, QSocketNotifier) @@ -48,7 +48,7 @@ from qutebrowser.misc import (crashdialog, readline, ipc, earlyinit, from qutebrowser.misc import utilcmds # pylint: disable=unused-import from qutebrowser.keyinput import modeman from qutebrowser.utils import (log, version, message, utils, qtutils, urlutils, - debug, objreg, usertypes, standarddir) + debug, objreg, usertypes, standarddir, error) # We import utilcmds to run the cmdutils.register decorators. @@ -102,22 +102,20 @@ class Application(QApplication): print(qutebrowser.__copyright__) print() print(version.GPL_BOILERPLATE.strip()) - sys.exit(0) + sys.exit(usertypes.Exit.ok) try: sent = ipc.send_to_running_instance(self._args.command) if sent: - sys.exit(0) + sys.exit(usertypes.Exit.ok) log.init.debug("Starting IPC server...") ipc.init() except ipc.IPCError as e: - text = ('{}\n\nMaybe another instance is running but ' - 'frozen?'.format(e)) - msgbox = QMessageBox(QMessageBox.Critical, "Error while " - "connecting to running instance!", text) - msgbox.exec_() + error.handle_fatal_exc( + e, self._args, "Error while connecting to running instance!", + post_text="Maybe another instance is running but frozen?") # We didn't really initialize much so far, so we just quit hard. - sys.exit(1) + sys.exit(usertypes.Exit.err_ipc) log.init.debug("Starting init...") self.setQuitOnLastWindowClosed(False) @@ -129,11 +127,9 @@ class Application(QApplication): try: self._init_modules() except (OSError, UnicodeDecodeError) as e: - msgbox = QMessageBox( - QMessageBox.Critical, "Error while initializing!", - "Error while initializing: {}".format(e)) - msgbox.exec_() - sys.exit(1) + error.handle_fatal_exc(e, self._args, "Error while initializing!", + pre_text="Error while initializing") + sys.exit(usertypes.Exit.err_init) QTimer.singleShot(0, self._process_args) log.init.debug("Initializing eventfilter...") @@ -179,7 +175,8 @@ class Application(QApplication): log.init.debug("Initializing web history...") history.init() log.init.debug("Initializing crashlog...") - self._handle_segfault() + if not self._args.no_err_windows: + self._handle_segfault() log.init.debug("Initializing sessions...") session_manager = sessions.SessionManager(self) objreg.register('session-manager', session_manager) @@ -243,6 +240,7 @@ class Application(QApplication): def _init_crashlogfile(self): """Start a new logfile and redirect faulthandler to it.""" + assert not self._args.no_err_windows logname = os.path.join(standarddir.data(), 'crash.log') try: self._crashlogfile = open(logname, 'w', encoding='ascii') @@ -637,17 +635,20 @@ class Application(QApplication): except TypeError: log.destroy.exception("Error while preventing shutdown") QApplication.closeAllWindows() - self._crashdlg = crashdialog.ExceptionCrashDialog( - self._args.debug, pages, cmd_history, exc, objects) - ret = self._crashdlg.exec_() - if ret == QDialog.Accepted: # restore - self.restart(shutdown=False, pages=pages) + if self._args.no_err_windows: + crashdialog.dump_exception_info(exc, pages, cmd_history, objects) + else: + self._crashdlg = crashdialog.ExceptionCrashDialog( + self._args.debug, pages, cmd_history, exc, objects) + ret = self._crashdlg.exec_() + if ret == QDialog.Accepted: # restore + self.restart(shutdown=False, pages=pages) # We might risk a segfault here, but that's better than continuing to # run in some undefined state, so we only do the most needed shutdown # here. qInstallMessageHandler(None) self._destroy_crashlogfile() - sys.exit(1) + sys.exit(usertypes.Exit.exception) def _get_restart_args(self, pages): """Get the current working directory and args to relaunch qutebrowser. @@ -867,10 +868,9 @@ class Application(QApplication): try: save_manager.save(key, is_exit=True) except OSError as e: - msgbox = QMessageBox( - QMessageBox.Critical, "Error while saving!", - "Error while saving {}: {}".format(key, e)) - msgbox.exec_() + error.handle_fatal_exc( + e, self._args, "Error while saving!", + pre_text="Error while saving {}".format(key)) # Re-enable faulthandler to stdout, then remove crash log log.destroy.debug("Deactiving crash log...") self._destroy_crashlogfile() diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index 3fb0aefd3..68c8961cd 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -33,12 +33,12 @@ import collections import collections.abc from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QUrl, QSettings -from PyQt5.QtWidgets import QMessageBox from qutebrowser.config import configdata, configexc, textwrapper from qutebrowser.config.parsers import ini, keyconf from qutebrowser.commands import cmdexc, cmdutils -from qutebrowser.utils import message, objreg, utils, standarddir, log, qtutils +from qutebrowser.utils import (message, objreg, utils, standarddir, log, + qtutils, error, usertypes) from qutebrowser.utils.usertypes import Completion @@ -114,9 +114,9 @@ def section(sect): def _init_main_config(): """Initialize the main config.""" + app = objreg.get('app') + args = objreg.get('args') try: - app = objreg.get('app') - args = objreg.get('args') config_obj = ConfigManager(standarddir.config(), 'qutebrowser.conf', args.relaxed_config, app) except (configexc.Error, configparser.Error, UnicodeDecodeError) as e: @@ -127,12 +127,11 @@ def _init_main_config(): e.section, e.option) # pylint: disable=no-member except AttributeError: pass - errstr += "\n{}".format(e) - msgbox = QMessageBox(QMessageBox.Critical, - "Error while reading config!", errstr) - msgbox.exec_() + errstr += "\n" + error.handle_fatal_exc(e, args, "Error while reading config!", + pre_text=errstr) # We didn't really initialize much so far, so we just quit hard. - sys.exit(1) + sys.exit(usertypes.Exit.err_config) else: objreg.register('config', config_obj) if standarddir.config() is not None: @@ -152,6 +151,7 @@ def _init_main_config(): def _init_key_config(): """Initialize the key config.""" + args = objreg.get('args') try: key_config = keyconf.KeyConfigParser(standarddir.config(), 'keys.conf') except (keyconf.KeyConfigError, UnicodeDecodeError) as e: @@ -159,12 +159,10 @@ def _init_key_config(): errstr = "Error while reading key config:\n" if e.lineno is not None: errstr += "In line {}: ".format(e.lineno) - errstr += str(e) - msgbox = QMessageBox(QMessageBox.Critical, - "Error while reading key config!", errstr) - msgbox.exec_() + error.handle_fatal_exc(e, args, "Error while reading key config!", + pre_text=errstr) # We didn't really initialize much so far, so we just quit hard. - sys.exit(1) + sys.exit(usertypes.Exit.err_key_config) else: objreg.register('key-config', key_config) if standarddir.config() is not None: diff --git a/qutebrowser/misc/checkpyver.py b/qutebrowser/misc/checkpyver.py index 5bb41dd1a..cdf1ebbe3 100644 --- a/qutebrowser/misc/checkpyver.py +++ b/qutebrowser/misc/checkpyver.py @@ -47,7 +47,7 @@ def check_python_version(): version_str = '.'.join(map(str, sys.version_info[:3])) text = ("At least Python 3.4 is required to run qutebrowser, but " + version_str + " is installed!\n") - if Tk: + if Tk and '--no-err-windows' not in sys.argv: root = Tk() root.withdraw() messagebox.showerror("qutebrowser: Fatal error!", text) diff --git a/qutebrowser/misc/crashdialog.py b/qutebrowser/misc/crashdialog.py index dd20e7a07..bc193e8fc 100644 --- a/qutebrowser/misc/crashdialog.py +++ b/qutebrowser/misc/crashdialog.py @@ -535,3 +535,38 @@ class ReportErrorDialog(QDialog): btn.clicked.connect(self.close) hbox.addWidget(btn) vbox.addLayout(hbox) + + +def dump_exception_info(exc, pages, cmdhist, objects): + """Dump exception info to stderr. + + Args: + exc: An exception tuple (type, value, traceback) + pages: A list of lists of the open pages (URLs as strings) + cmdhist: A list with the command history (as strings) + objects: A list of all QObjects as string. + """ + print(file=sys.stderr) + print("\n\n===== Handling exception with --no-err-windows... =====\n\n", + file=sys.stderr) + print("\n---- Exceptions ----", file=sys.stderr) + print(''.join(traceback.format_exception(*exc)), file=sys.stderr) + print("\n---- Version info ----", file=sys.stderr) + try: + print(version.version(), file=sys.stderr) + except Exception: + traceback.print_exc() + print("\n---- Config ----", file=sys.stderr) + try: + conf = objreg.get('config') + print(conf.dump_userconfig(), file=sys.stderr) + except Exception: + traceback.print_exc() + print("\n---- Commandline args ----", file=sys.stderr) + print(' '.join(sys.argv[1:]), file=sys.stderr) + print("\n---- Open pages ----", file=sys.stderr) + print('\n\n'.join('\n'.join(e) for e in pages), file=sys.stderr) + print("\n---- Command history ----", file=sys.stderr) + print('\n'.join(cmdhist), file=sys.stderr) + print("\n---- Objects ----", file=sys.stderr) + print(objects, file=sys.stderr) diff --git a/qutebrowser/misc/earlyinit.py b/qutebrowser/misc/earlyinit.py index 017686072..f37f12f80 100644 --- a/qutebrowser/misc/earlyinit.py +++ b/qutebrowser/misc/earlyinit.py @@ -77,12 +77,15 @@ def _die(message, exception=None): print(file=sys.stderr) traceback.print_exc() app = QApplication(sys.argv) - message += '


Error:
{}'.format(exception) - msgbox = QMessageBox(QMessageBox.Critical, "qutebrowser: Fatal error!", - message) - msgbox.setTextFormat(Qt.RichText) - msgbox.resize(msgbox.sizeHint()) - msgbox.exec_() + if '--no-err-windows' in sys.argv: + print("Exiting because of --no-err-windows.", file=sys.stderr) + else: + message += '


Error:
{}'.format(exception) + msgbox = QMessageBox(QMessageBox.Critical, "qutebrowser: Fatal error!", + message) + msgbox.setTextFormat(Qt.RichText) + msgbox.resize(msgbox.sizeHint()) + msgbox.exec_() app.quit() sys.exit(1) @@ -179,13 +182,13 @@ def check_pyqt_core(): text = text.replace('', '') text = text.replace('
', '\n') text += '\n\nError: {}'.format(e) - if tkinter: + if tkinter and '--no-err-windows' not in sys.argv: root = tkinter.Tk() root.withdraw() tkinter.messagebox.showerror("qutebrowser: Fatal error!", text) else: print(text, file=sys.stderr) - if '--debug' in sys.argv: + if '--debug' in sys.argv or '--no-err-windows' in sys.argv: print(file=sys.stderr) traceback.print_exc() sys.exit(1) diff --git a/qutebrowser/qutebrowser.py b/qutebrowser/qutebrowser.py index eb5cb651c..5c52a8ada 100644 --- a/qutebrowser/qutebrowser.py +++ b/qutebrowser/qutebrowser.py @@ -84,6 +84,8 @@ def get_argparser(): action='store_true') debug.add_argument('--no-crash-dialog', action='store_true', help="Don't " "show a crash dialog.") + debug.add_argument('--no-err-windows', action='store_true', help="Don't " + "show any error windows (used for tests/smoke.py).") # For the Qt args, we use store_const with const=True rather than # store_true because we want the default to be None, to make # utils.qt:get_args easier. diff --git a/qutebrowser/utils/error.py b/qutebrowser/utils/error.py new file mode 100644 index 000000000..d637f4d33 --- /dev/null +++ b/qutebrowser/utils/error.py @@ -0,0 +1,54 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2015 Florian Bruhin (The Compiler) +# +# This file is part of qutebrowser. +# +# qutebrowser is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# qutebrowser is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with qutebrowser. If not, see . + +"""Tools related to error printing/displaying.""" + +from PyQt5.QtWidgets import QMessageBox + +from qutebrowser.utils import log + + +def handle_fatal_exc(exc, args, title, *, pre_text='', post_text=''): + """Handle a fatal "expected" exception by displaying an error box. + + If --no-err-windows is given as argument, the text is logged to the error + logger instead. + + Args: + exc: The Exception object being handled. + args: The argparser namespace. + title: The title to be used for the error message. + pre_text: The text to be displayed before the exception text. + post_text: The text to be displayed after the exception text. + """ + if args.no_err_windows: + log.misc.exception("Handling fatal {} with --no-err-windows!".format( + exc.__class__.__name__)) + log.misc.error("title: {}".format(title)) + log.misc.error("pre_text: {}".format(pre_text)) + log.misc.error("post_text: {}".format(post_text)) + else: + if pre_text: + msg_text = '{}: {}'.format(pre_text, exc) + else: + msg_text = str(exc) + if post_text: + msg_text += '\n\n{}'.format(post_text) + msgbox = QMessageBox(QMessageBox.Critical, title, msg_text) + msgbox.exec_() diff --git a/qutebrowser/utils/usertypes.py b/qutebrowser/utils/usertypes.py index 18f04b4d5..a1299fae8 100644 --- a/qutebrowser/utils/usertypes.py +++ b/qutebrowser/utils/usertypes.py @@ -240,6 +240,11 @@ Completion = enum('Completion', ['command', 'section', 'option', 'value', 'quickmark_by_name', 'url', 'sessions']) +# Exit statuses for errors +Exit = enum('Exit', ['ok', 'reserved', 'exception', 'err_ipc', 'err_init', + 'err_config', 'err_key_config']) + + class Question(QObject): """A question asked to the user, e.g. via the status bar. diff --git a/tox.ini b/tox.ini index 5a65c29ba..d260dc362 100644 --- a/tox.ini +++ b/tox.ini @@ -98,3 +98,10 @@ commands = {envpython} scripts/src2asciidoc.py git --no-pager diff --exit-code --stat {envpython} scripts/asciidoc2html.py {posargs} + +[testenv:smoke] +deps = + -rrequirements.txt +commands = + {[testenv:mkvenv]commands} + {envpython} -m qutebrowser --debug --no-err-windows --nowindow --temp-basedir about:blank ":later 500 quit" From b619d835e64c857cb97ac9c8417f9b5f4ba77785 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 17 May 2015 00:29:28 +0200 Subject: [PATCH 099/182] Make usertypes.Exit an IntEnum. --- qutebrowser/utils/usertypes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qutebrowser/utils/usertypes.py b/qutebrowser/utils/usertypes.py index 28a30f941..29daf8c06 100644 --- a/qutebrowser/utils/usertypes.py +++ b/qutebrowser/utils/usertypes.py @@ -240,9 +240,9 @@ Completion = enum('Completion', ['command', 'section', 'option', 'value', 'quickmark_by_name', 'url', 'sessions']) -# Exit statuses for errors +# Exit statuses for errors. Needs to be an int for sys.exit. Exit = enum('Exit', ['ok', 'reserved', 'exception', 'err_ipc', 'err_init', - 'err_config', 'err_key_config']) + 'err_config', 'err_key_config'], is_int=True) class Question(QObject): From 002346a125757ae37a8a789385a00563b03c4ee5 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 17 May 2015 00:39:20 +0200 Subject: [PATCH 100/182] Clean up exception_hook. --- qutebrowser/misc/crashsignal.py | 57 +++++++++++++++++++++------------ 1 file changed, 36 insertions(+), 21 deletions(-) diff --git a/qutebrowser/misc/crashsignal.py b/qutebrowser/misc/crashsignal.py index c860cdb6e..54c0f0a15 100644 --- a/qutebrowser/misc/crashsignal.py +++ b/qutebrowser/misc/crashsignal.py @@ -27,6 +27,7 @@ import signal import functools import faulthandler import os.path +import collections from PyQt5.QtCore import (pyqtSlot, qInstallMessageHandler, QObject, QSocketNotifier, QTimer, QUrl) @@ -37,6 +38,10 @@ from qutebrowser.misc import earlyinit, crashdialog from qutebrowser.utils import usertypes, standarddir, log, objreg, debug +ExceptionInfo = collections.namedtuple('ExceptionInfo', + 'pages, cmd_history, objects') + + class CrashHandler(QObject): """Handler for crashes, reports and exceptions. @@ -160,6 +165,31 @@ class CrashHandler(QObject): except OSError: log.destroy.exception("Could not remove crash log!") + def _get_exception_info(self): + """Get info needed for the exception hook/dialog. + + Return: + An ExceptionInfo namedtuple. + """ + try: + pages = self._recover_pages(forgiving=True) + except Exception: + log.destroy.exception("Error while recovering pages") + pages = [] + + try: + cmd_history = objreg.get('command-history')[-5:] + except Exception: + log.destroy.exception("Error while getting history: {}") + cmd_history = [] + + try: + objects = debug.get_all_objects() + except Exception: + log.destroy.exception("Error while getting objects") + objects = "" + return ExceptionInfo(pages, cmd_history, objects) + def exception_hook(self, exctype, excvalue, tb): # noqa """Handle uncaught python exceptions. @@ -195,24 +225,7 @@ class CrashHandler(QObject): return self._quitter.quit_status['crash'] = False - - try: - pages = self._recover_pages(forgiving=True) - except Exception: - log.destroy.exception("Error while recovering pages") - pages = [] - - try: - cmd_history = objreg.get('command-history')[-5:] - except Exception: - log.destroy.exception("Error while getting history: {}") - cmd_history = [] - - try: - objects = debug.get_all_objects() - except Exception: - log.destroy.exception("Error while getting objects") - objects = "" + info = self._get_exception_info() try: objreg.get('ipc-server').ignored = True @@ -226,13 +239,15 @@ class CrashHandler(QObject): log.destroy.exception("Error while preventing shutdown") self._app.closeAllWindows() if self._args.no_err_windows: - crashdialog.dump_exception_info(exc, pages, cmd_history, objects) + crashdialog.dump_exception_info(exc, info.pages, info.cmd_history, + info.objects) else: self._crash_dialog = crashdialog.ExceptionCrashDialog( - self._args.debug, pages, cmd_history, exc, objects) + self._args.debug, info.pages, info.cmd_history, exc, + info.objects) ret = self._crash_dialog.exec_() if ret == QDialog.Accepted: # restore - self._quitter.restart(pages) + self._quitter.restart(info.pages) # We might risk a segfault here, but that's better than continuing to # run in some undefined state, so we only do the most needed shutdown From 315725a3ac641f0b8bbd49220ab334abe9e39958 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 17 May 2015 00:48:15 +0200 Subject: [PATCH 101/182] Print info with --no-err-windows on earlyinit errs. --- qutebrowser/misc/earlyinit.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/qutebrowser/misc/earlyinit.py b/qutebrowser/misc/earlyinit.py index 0416ed9a1..3bc214389 100644 --- a/qutebrowser/misc/earlyinit.py +++ b/qutebrowser/misc/earlyinit.py @@ -80,11 +80,13 @@ def _die(message, exception=None): """ from PyQt5.QtWidgets import QApplication, QMessageBox from PyQt5.QtCore import Qt - if '--debug' in sys.argv and exception is not None: + if (('--debug' in sys.argv or '--no-err-windows' in sys.argv) and + exception is not None): print(file=sys.stderr) traceback.print_exc() app = QApplication(sys.argv) if '--no-err-windows' in sys.argv: + print(message, file=sys.stderr) print("Exiting because of --no-err-windows.", file=sys.stderr) else: message += '


Error:
{}'.format(exception) From b94fcf2c3ca3ed08bb3627bcad8aff536a541ccb Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 17 May 2015 00:52:37 +0200 Subject: [PATCH 102/182] Clean up sys.exit call. --- qutebrowser/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/app.py b/qutebrowser/app.py index e75e9e4e9..cfc84c654 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -66,7 +66,7 @@ def run(args): print(qutebrowser.__copyright__) print() print(version.GPL_BOILERPLATE.strip()) - sys.exit(0) + sys.exit(usertypes.Exit.ok) if args.temp_basedir: args.basedir = tempfile.mkdtemp() From f6ad556f3429f80bcb8f5e7b547b3fdbc1001ef1 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 17 May 2015 01:03:34 +0200 Subject: [PATCH 103/182] Get rid of --no-crash-dialog. --- CHANGELOG.asciidoc | 5 +++++ doc/qutebrowser.1.asciidoc | 3 --- qutebrowser/misc/crashsignal.py | 3 +-- qutebrowser/qutebrowser.py | 2 -- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 20d905ada..bfe904975 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -48,6 +48,11 @@ Deprecated - `:scroll` with two pixel-arguments is now deprecated - `:scroll-px` should be used instead. +Removed +~~~~~~~ + +- The `--no-crash-dialog` argument which was intended for debugging only was removed as it's replaced by `--no-err-windows` which suppresses all error windows. + Fixed ~~~~~ diff --git a/doc/qutebrowser.1.asciidoc b/doc/qutebrowser.1.asciidoc index 4c8ce2556..4ac83c587 100644 --- a/doc/qutebrowser.1.asciidoc +++ b/doc/qutebrowser.1.asciidoc @@ -90,9 +90,6 @@ show it. *--debug-exit*:: Turn on debugging of late exit. -*--no-crash-dialog*:: - Don't show a crash dialog. - *--pdb-postmortem*:: Drop into pdb on exceptions. diff --git a/qutebrowser/misc/crashsignal.py b/qutebrowser/misc/crashsignal.py index 54c0f0a15..7598f3312 100644 --- a/qutebrowser/misc/crashsignal.py +++ b/qutebrowser/misc/crashsignal.py @@ -212,8 +212,7 @@ class CrashHandler(QObject): if self._args.pdb_postmortem: pdb.post_mortem(tb) - if (is_ignored_exception or self._args.no_crash_dialog or - self._args.pdb_postmortem): + if is_ignored_exception or self._args.pdb_postmortem: # pdb exit, KeyboardInterrupt, ... status = 0 if is_ignored_exception else 2 try: diff --git a/qutebrowser/qutebrowser.py b/qutebrowser/qutebrowser.py index b2d61fb48..caebd4967 100644 --- a/qutebrowser/qutebrowser.py +++ b/qutebrowser/qutebrowser.py @@ -90,8 +90,6 @@ def get_argparser(): "the main window.") debug.add_argument('--debug-exit', help="Turn on debugging of late exit.", action='store_true') - debug.add_argument('--no-crash-dialog', action='store_true', help="Don't " - "show a crash dialog.") debug.add_argument('--pdb-postmortem', action='store_true', help="Drop into pdb on exceptions.") debug.add_argument('--temp-basedir', action='store_true', help="Use a " From f54c416ddd561f67cf656566370f6896963c11cc Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 17 May 2015 01:07:36 +0200 Subject: [PATCH 104/182] tox: Fixes for smoke environment. --- tox.ini | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index 28b0c4bf9..fe077c647 100644 --- a/tox.ini +++ b/tox.ini @@ -113,11 +113,14 @@ commands = {envpython} scripts/asciidoc2html.py {posargs} [testenv:smoke] +passenv = DISPLAY XAUTHORITY HOME deps = -rrequirements.txt +# We don't use {[testenv:mkvenv]commands} here because that seems to be broken +# on Ubuntu Trusty. commands = - {[testenv:mkvenv]commands} - {envpython} -m qutebrowser --debug --no-err-windows --nowindow --temp-basedir about:blank ":later 500 quit" + {envpython} scripts/link_pyqt.py --tox {envdir} + {envpython} -m qutebrowser --no-err-windows --nowindow --temp-basedir about:blank ":later 500 quit" [pytest] norecursedirs = .tox .venv From 7fc99f3d80bfb5160852c6df869f0894137448fe Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 17 May 2015 01:09:33 +0200 Subject: [PATCH 105/182] adblock: Don't show message with --basedir given. --- qutebrowser/browser/adblock.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/qutebrowser/browser/adblock.py b/qutebrowser/browser/adblock.py index 573939d8b..bec74f096 100644 --- a/qutebrowser/browser/adblock.py +++ b/qutebrowser/browser/adblock.py @@ -110,7 +110,9 @@ class HostBlocker: except OSError: log.misc.exception("Failed to read host blocklist!") else: - if config.get('content', 'host-block-lists') is not None: + args = objreg.get('args') + if (config.get('content', 'host-block-lists') is not None and + args.basedir is None): message.info('current', "Run :adblock-update to get adblock lists.") From b5dc4ea04032b69094e48fd8c27373c1fac02cab Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 17 May 2015 01:18:19 +0200 Subject: [PATCH 106/182] tox: Use absolute path for -rrequirements.txt. This fixes a FileNotFoundError on Ubuntu Trusty. --- tox.ini | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tox.ini b/tox.ini index fe077c647..e134d6176 100644 --- a/tox.ini +++ b/tox.ini @@ -52,7 +52,7 @@ commands = skip_install = true setenv = PYTHONPATH={toxinidir}/scripts deps = - -rrequirements.txt + -r{toxinidir}/requirements.txt astroid==1.3.6 beautifulsoup4==4.3.2 pylint==1.4.3 @@ -76,7 +76,7 @@ commands = {envpython} -m pep257 scripts tests qutebrowser --ignore=D102,D103,D2 [testenv:flake8] skip_install = true deps = - -rrequirements.txt + -r{toxinidir}/requirements.txt pyflakes==0.8.1 pep8==1.5.7 # rq.filter: <1.6.0 flake8==2.4.0 @@ -105,7 +105,7 @@ commands = skip_install = true whitelist_externals = git deps = - -rrequirements.txt + -r{toxinidir}/requirements.txt commands = {[testenv:mkvenv]commands} {envpython} scripts/src2asciidoc.py @@ -115,7 +115,7 @@ commands = [testenv:smoke] passenv = DISPLAY XAUTHORITY HOME deps = - -rrequirements.txt + -r{toxinidir}/requirements.txt # We don't use {[testenv:mkvenv]commands} here because that seems to be broken # on Ubuntu Trusty. commands = From f17238d3d4068723d86f712b82bb9de235eaaade Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 17 May 2015 01:19:23 +0200 Subject: [PATCH 107/182] tox: Set QT_QPA_PLATFORM_PLUGIN_PATH for smoke. This fixes smoke tests on Windows (I think). --- tox.ini | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tox.ini b/tox.ini index e134d6176..64d19f0dc 100644 --- a/tox.ini +++ b/tox.ini @@ -113,6 +113,8 @@ commands = {envpython} scripts/asciidoc2html.py {posargs} [testenv:smoke] +# https://bitbucket.org/hpk42/tox/issue/246/ - only needed for Windows though +setenv = QT_QPA_PLATFORM_PLUGIN_PATH={envdir}/Lib/site-packages/PyQt5/plugins/platforms passenv = DISPLAY XAUTHORITY HOME deps = -r{toxinidir}/requirements.txt From 8ab2772dd9bd1747649fd68b781870d2c70231fe Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 17 May 2015 01:29:47 +0200 Subject: [PATCH 108/182] Use atexit to remove temp basedir. This hopefully fixes a PermissionError on Windows. --- qutebrowser/app.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qutebrowser/app.py b/qutebrowser/app.py index cfc84c654..64c277c9f 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -28,6 +28,7 @@ import json import time import shutil import tempfile +import atexit from PyQt5.QtWidgets import QApplication from PyQt5.QtGui import QDesktopServices, QPixmap, QIcon, QCursor, QWindow @@ -643,7 +644,7 @@ class Quitter: objreg.get('crash-handler').destroy_crashlogfile() # Delete temp basedir if self._args.temp_basedir: - shutil.rmtree(self._args.basedir) + atexit.register(shutil.rmtree, self._args.basedir) # If we don't kill our custom handler here we might get segfaults log.destroy.debug("Deactiving message handler...") qInstallMessageHandler(None) From b60f673468f67551d89a099d171405ca5029886d Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 17 May 2015 14:14:23 +0200 Subject: [PATCH 109/182] Fix @pyqtSlot signature for on_config_changed. --- qutebrowser/browser/cache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/browser/cache.py b/qutebrowser/browser/cache.py index 03f6c7a2e..c649cc129 100644 --- a/qutebrowser/browser/cache.py +++ b/qutebrowser/browser/cache.py @@ -52,7 +52,7 @@ class DiskCache(QNetworkDiskCache): maxsize=self.maximumCacheSize(), path=self.cacheDirectory()) - @pyqtSlot() + @pyqtSlot(str, str) def on_config_changed(self, section, option): """Update cache size/activated if the config was changed.""" if (section, option) == ('storage', 'cache-size'): From 5ef40829aa3d368c6f2409c43d160b60e0c6e23b Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 17 May 2015 18:14:21 +0200 Subject: [PATCH 110/182] tox: Pass $USERNAME and $USER for smoke env. This fixes getpass.getuser() on Windows for the smoke tests. --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 64d19f0dc..aa7fd87e5 100644 --- a/tox.ini +++ b/tox.ini @@ -115,7 +115,7 @@ commands = [testenv:smoke] # https://bitbucket.org/hpk42/tox/issue/246/ - only needed for Windows though setenv = QT_QPA_PLATFORM_PLUGIN_PATH={envdir}/Lib/site-packages/PyQt5/plugins/platforms -passenv = DISPLAY XAUTHORITY HOME +passenv = DISPLAY XAUTHORITY HOME USERNAME USER deps = -r{toxinidir}/requirements.txt # We don't use {[testenv:mkvenv]commands} here because that seems to be broken From 10985c350566791d0f463fc66cb6f57276fa5132 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 17 May 2015 18:44:36 +0200 Subject: [PATCH 111/182] Fix handling of Meta/Control on OS X. Fixes #110. See #420. See http://doc.qt.io/qt-5.4/osx-issues.html#special-keys : To provide the expected behavior for Qt applications on OS X, the Qt::Meta, Qt::MetaModifier, and Qt::META enum values correspond to the Control keys on the standard Apple keyboard, and the Qt::Control, Qt::ControlModifier, and Qt::CTRL enum values correspond to the Command keys. --- qutebrowser/utils/utils.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/qutebrowser/utils/utils.py b/qutebrowser/utils/utils.py index b86642081..b7d64a07f 100644 --- a/qutebrowser/utils/utils.py +++ b/qutebrowser/utils/utils.py @@ -333,10 +333,12 @@ def keyevent_to_string(e): (Qt.ShiftModifier, 'Shift'), ]) if sys.platform == 'darwin': - # FIXME verify this feels right on a real Mac as well. - # In my Virtualbox VM, the Ctrl key shows up as meta. + # Qt swaps Ctrl/Meta on OS X, so we switch it back here so the user can + # use it in the config as expected. See: # https://github.com/The-Compiler/qutebrowser/issues/110 + # http://doc.qt.io/qt-5.4/osx-issues.html#special-keys modmask2str[Qt.MetaModifier] = 'Ctrl' + modmask2str[Qt.ControlModifier] = 'Meta' modifiers = (Qt.Key_Control, Qt.Key_Alt, Qt.Key_Shift, Qt.Key_Meta, Qt.Key_AltGr, Qt.Key_Super_L, Qt.Key_Super_R, Qt.Key_Hyper_L, Qt.Key_Hyper_R, Qt.Key_Direction_L, From e24d2e1b8cc2fe33b515fabfc7a86e7e478fecf1 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 17 May 2015 18:50:08 +0200 Subject: [PATCH 112/182] Update changelog. --- CHANGELOG.asciidoc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index bfe904975..2ae9b7301 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -30,7 +30,7 @@ Added - Support for Qt 5.5 and tox 2.0 - New arguments `--datadir` and `--cachedir` to set the data/cache location. - New arguments `--basedir` and `--temp-basedir` (intended for debugging) to set a different base directory for all data, which allows multiple invocations. - +- New argument `--no-err-windows` to suppress all error windows. Changed ~~~~~~~ @@ -70,6 +70,7 @@ Fixed - Fixed AssertionError when closing many windows quickly. - Various fixes for deprecated key bindings and auto-migrations. - Workaround for qutebrowser not starting when there are NUL-bytes in the history (because of a currently unknown bug) +- Fixed handling of keybindings containing Ctrl/Meta on OS X. https://github.com/The-Compiler/qutebrowser/releases/tag/v0.2.1[v0.2.1] ----------------------------------------------------------------------- From a9f5d45c3462d4519704ac88c535cedb524ba74b Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 17 May 2015 18:52:25 +0200 Subject: [PATCH 113/182] Fix tests on OS X. --- tests/utils/test_utils.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/tests/utils/test_utils.py b/tests/utils/test_utils.py index 40c254a0b..09b8e0d8f 100644 --- a/tests/utils/test_utils.py +++ b/tests/utils/test_utils.py @@ -19,7 +19,6 @@ """Tests for qutebrowser.utils.utils.""" -import sys import enum import datetime import os.path @@ -320,10 +319,7 @@ class TestKeyEventToString: evt = fake_keyevent_factory( key=Qt.Key_A, modifiers=(Qt.ControlModifier | Qt.AltModifier | Qt.MetaModifier | Qt.ShiftModifier)) - if sys.platform == 'darwin': - assert utils.keyevent_to_string(evt) == 'Ctrl+Alt+Shift+A' - else: - assert utils.keyevent_to_string(evt) == 'Ctrl+Alt+Meta+Shift+A' + assert utils.keyevent_to_string(evt) == 'Ctrl+Alt+Meta+Shift+A' class TestNormalize: From 81ba49e79b30a4bb9060688e48e394889f5837a6 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 17 May 2015 18:59:40 +0200 Subject: [PATCH 114/182] Fix tests on OS X, take two. --- qutebrowser/utils/utils.py | 21 +++++++++++++-------- tests/utils/test_utils.py | 6 ++++-- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/qutebrowser/utils/utils.py b/qutebrowser/utils/utils.py index b7d64a07f..298aba2b0 100644 --- a/qutebrowser/utils/utils.py +++ b/qutebrowser/utils/utils.py @@ -326,19 +326,24 @@ def keyevent_to_string(e): A name of the key (combination) as a string or None if only modifiers are pressed.. """ - modmask2str = collections.OrderedDict([ - (Qt.ControlModifier, 'Ctrl'), - (Qt.AltModifier, 'Alt'), - (Qt.MetaModifier, 'Meta'), - (Qt.ShiftModifier, 'Shift'), - ]) if sys.platform == 'darwin': # Qt swaps Ctrl/Meta on OS X, so we switch it back here so the user can # use it in the config as expected. See: # https://github.com/The-Compiler/qutebrowser/issues/110 # http://doc.qt.io/qt-5.4/osx-issues.html#special-keys - modmask2str[Qt.MetaModifier] = 'Ctrl' - modmask2str[Qt.ControlModifier] = 'Meta' + modmask2str = collections.OrderedDict([ + (Qt.MetaModifier, 'Ctrl'), + (Qt.AltModifier, 'Alt'), + (Qt.ControlModifier, 'Meta'), + (Qt.ShiftModifier, 'Shift'), + ]) + else: + modmask2str = collections.OrderedDict([ + (Qt.ControlModifier, 'Ctrl'), + (Qt.AltModifier, 'Alt'), + (Qt.MetaModifier, 'Meta'), + (Qt.ShiftModifier, 'Shift'), + ]) modifiers = (Qt.Key_Control, Qt.Key_Alt, Qt.Key_Shift, Qt.Key_Meta, Qt.Key_AltGr, Qt.Key_Super_L, Qt.Key_Super_R, Qt.Key_Hyper_L, Qt.Key_Hyper_R, Qt.Key_Direction_L, diff --git a/tests/utils/test_utils.py b/tests/utils/test_utils.py index 09b8e0d8f..588753587 100644 --- a/tests/utils/test_utils.py +++ b/tests/utils/test_utils.py @@ -19,6 +19,7 @@ """Tests for qutebrowser.utils.utils.""" +import sys import enum import datetime import os.path @@ -312,10 +313,11 @@ class TestKeyEventToString: def test_key_and_modifier(self, fake_keyevent_factory): """Test with key and modifier pressed.""" evt = fake_keyevent_factory(key=Qt.Key_A, modifiers=Qt.ControlModifier) - assert utils.keyevent_to_string(evt) == 'Ctrl+A' + expected = 'Meta+A' if sys.platform == 'darwin' else 'Ctrl+A' + assert utils.keyevent_to_string(evt) == expected def test_key_and_modifiers(self, fake_keyevent_factory): - """Test with key and multiple modifier pressed.""" + """Test with key and multiple modifiers pressed.""" evt = fake_keyevent_factory( key=Qt.Key_A, modifiers=(Qt.ControlModifier | Qt.AltModifier | Qt.MetaModifier | Qt.ShiftModifier)) From 54eae77328233bf2d09119d401dc56a2f6f3fd1c Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 17 May 2015 19:04:07 +0200 Subject: [PATCH 115/182] Fix tests on OS X, take three. --- tests/keyinput/test_basekeyparser.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/tests/keyinput/test_basekeyparser.py b/tests/keyinput/test_basekeyparser.py index a68cc685b..8b0086b45 100644 --- a/tests/keyinput/test_basekeyparser.py +++ b/tests/keyinput/test_basekeyparser.py @@ -21,6 +21,7 @@ """Tests for BaseKeyParser.""" +import sys import logging from unittest import mock @@ -131,8 +132,12 @@ class TestSpecialKeys: def test_valid_key(self, fake_keyevent_factory): """Test a valid special keyevent.""" - self.kp.handle(fake_keyevent_factory(Qt.Key_A, Qt.ControlModifier)) - self.kp.handle(fake_keyevent_factory(Qt.Key_X, Qt.ControlModifier)) + if sys.platform == 'darwin': + modifier = Qt.MetaModifier + else: + modifier = Qt.ControlModifier + self.kp.handle(fake_keyevent_factory(Qt.Key_A, modifier)) + self.kp.handle(fake_keyevent_factory(Qt.Key_X, modifier)) self.kp.execute.assert_called_once_with('ctrla', self.kp.Type.special) def test_invalid_key(self, fake_keyevent_factory): @@ -167,8 +172,12 @@ class TestKeyChain: def test_valid_special_key(self, fake_keyevent_factory): """Test valid special key.""" - self.kp.handle(fake_keyevent_factory(Qt.Key_A, Qt.ControlModifier)) - self.kp.handle(fake_keyevent_factory(Qt.Key_X, Qt.ControlModifier)) + if sys.platform == 'darwin': + modifier = Qt.MetaModifier + else: + modifier = Qt.ControlModifier + self.kp.handle(fake_keyevent_factory(Qt.Key_A, modifier)) + self.kp.handle(fake_keyevent_factory(Qt.Key_X, modifier)) self.kp.execute.assert_called_once_with('ctrla', self.kp.Type.special) assert self.kp._keystring == '' From dd292b07817245e62c77df5f540a9141f5555da3 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 18 May 2015 21:31:54 +0200 Subject: [PATCH 116/182] Don't depend on objreg for CommandDispatcher. See #640. --- qutebrowser/browser/commands.py | 98 +++++++++++-------------- qutebrowser/mainwindow/mainwindow.py | 30 +++++--- qutebrowser/mainwindow/tabbedbrowser.py | 8 +- 3 files changed, 64 insertions(+), 72 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index cdc45110c..b94a76853 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -56,46 +56,40 @@ class CommandDispatcher: Attributes: _editor: The ExternalEditor object. _win_id: The window ID the CommandDispatcher is associated with. + _tabbed_browser: The TabbedBrowser used. """ - def __init__(self, win_id): + def __init__(self, win_id, tabbed_browser): self._editor = None self._win_id = win_id + self._tabbed_browser = tabbed_browser def __repr__(self): return utils.get_repr(self) - def _tabbed_browser(self, window=False): - """Convienence method to get the right tabbed-browser. - - Args: - window: If True, open a new window. - """ + def _new_tabbed_browser(self): + """Get a tabbed-browser from a new window.""" from qutebrowser.mainwindow import mainwindow - if window: - new_window = mainwindow.MainWindow() - new_window.show() - win_id = new_window.win_id - else: - win_id = self._win_id - return objreg.get('tabbed-browser', scope='window', window=win_id) + new_window = mainwindow.MainWindow() + new_window.show() + return new_window.tabbed_browser def _count(self): """Convenience method to get the widget count.""" - return self._tabbed_browser().count() + return self._tabbed_browser.count() def _set_current_index(self, idx): """Convenience method to set the current widget index.""" - return self._tabbed_browser().setCurrentIndex(idx) + return self._tabbed_browser.setCurrentIndex(idx) def _current_index(self): """Convenience method to get the current widget index.""" - return self._tabbed_browser().currentIndex() + return self._tabbed_browser.currentIndex() def _current_url(self): """Convenience method to get the current url.""" try: - return self._tabbed_browser().current_url() + return self._tabbed_browser.current_url() except qtutils.QtValueError as e: msg = "Current URL is invalid" if e.reason: @@ -105,7 +99,7 @@ class CommandDispatcher: def _current_widget(self): """Get the currently active widget from a command.""" - widget = self._tabbed_browser().currentWidget() + widget = self._tabbed_browser.currentWidget() if widget is None: raise cmdexc.CommandError("No WebView available yet!") return widget @@ -120,10 +114,10 @@ class CommandDispatcher: window: Whether to open in a new window """ urlutils.raise_cmdexc_if_invalid(url) - tabbed_browser = self._tabbed_browser() + tabbed_browser = self._tabbed_browser cmdutils.check_exclusive((tab, background, window), 'tbw') if window: - tabbed_browser = self._tabbed_browser(window=True) + tabbed_browser = self._new_tabbed_browser() tabbed_browser.tabopen(url) elif tab: tabbed_browser.tabopen(url, background=False, explicit=True) @@ -144,12 +138,11 @@ class CommandDispatcher: The widget with the given tab ID if count is given. None if no widget was found. """ - tabbed_browser = self._tabbed_browser() if count is None: - return tabbed_browser.currentWidget() + return self._tabbed_browser.currentWidget() elif 1 <= count <= self._count(): cmdutils.check_overflow(count + 1, 'int') - return tabbed_browser.widget(count - 1) + return self._tabbed_browser.widget(count - 1) else: return None @@ -213,7 +206,7 @@ class CommandDispatcher: window=self._win_id) except KeyError: raise cmdexc.CommandError("No last focused tab!") - idx = self._tabbed_browser().indexOf(tab) + idx = self._tabbed_browser.indexOf(tab) if idx == -1: raise cmdexc.CommandError("Last focused tab vanished!") self._set_current_index(idx) @@ -271,16 +264,15 @@ class CommandDispatcher: tab = self._cntwidget(count) if tab is None: return - tabbed_browser = self._tabbed_browser() - tabbar = tabbed_browser.tabBar() + tabbar = self._tabbed_browser.tabBar() selection_override = self._get_selection_override(left, right, opposite) if selection_override is None: - tabbed_browser.close_tab(tab) + self._tabbed_browser.close_tab(tab) else: old_selection_behavior = tabbar.selectionBehaviorOnRemove() tabbar.setSelectionBehaviorOnRemove(selection_override) - tabbed_browser.close_tab(tab) + self._tabbed_browser.close_tab(tab) tabbar.setSelectionBehaviorOnRemove(old_selection_behavior) @cmdutils.register(instance='command-dispatcher', name='open', @@ -315,7 +307,7 @@ class CommandDispatcher: if count is None: # We want to open a URL in the current tab, but none exists # yet. - self._tabbed_browser().tabopen(url) + self._tabbed_browser.tabopen(url) else: # Explicit count with a tab that doesn't exist. return @@ -391,12 +383,14 @@ class CommandDispatcher: """ if bg and window: raise cmdexc.CommandError("Only one of -b/-w can be given!") - cur_tabbed_browser = self._tabbed_browser() curtab = self._current_widget() - cur_title = cur_tabbed_browser.page_title(self._current_index()) + cur_title = self._tabbed_browser.page_title(self._current_index()) # The new tab could be in a new tabbed_browser (e.g. because of # tabs-are-windows being set) - new_tabbed_browser = self._tabbed_browser(window) + if window: + new_tabbed_browser = self._new_tabbed_browser() + else: + new_tabbed_browser = self._tabbed_browser newtab = new_tabbed_browser.tabopen(background=bg, explicit=True) new_tabbed_browser = objreg.get('tabbed-browser', scope='window', window=newtab.win_id) @@ -414,9 +408,8 @@ class CommandDispatcher: """Detach the current tab to its own window.""" url = self._current_url() self._open(url, window=True) - tabbed_browser = self._tabbed_browser() cur_widget = self._current_widget() - tabbed_browser.close_tab(cur_widget) + self._tabbed_browser.close_tab(cur_widget) def _back_forward(self, tab, bg, window, count, forward): """Helper function for :back/:forward.""" @@ -686,7 +679,7 @@ class CommandDispatcher: """ clipboard = QApplication.clipboard() if title: - s = self._tabbed_browser().page_title(self._current_index()) + s = self._tabbed_browser.page_title(self._current_index()) else: s = self._current_url().toString( QUrl.FullyEncoded | QUrl.RemovePassword) @@ -752,22 +745,21 @@ class CommandDispatcher: right: Keep tabs to the right of the current. """ cmdutils.check_exclusive((left, right), 'lr') - tabbed_browser = self._tabbed_browser() - cur_idx = tabbed_browser.currentIndex() + cur_idx = self._tabbed_browser.currentIndex() assert cur_idx != -1 - for i, tab in enumerate(tabbed_browser.widgets()): + for i, tab in enumerate(self._tabbed_browser.widgets()): if (i == cur_idx or (left and i < cur_idx) or (right and i > cur_idx)): continue else: - tabbed_browser.close_tab(tab) + self._tabbed_browser.close_tab(tab) @cmdutils.register(instance='command-dispatcher', scope='window') def undo(self): """Re-open a closed tab (optionally skipping [count] closed tabs).""" try: - self._tabbed_browser().undo() + self._tabbed_browser.undo() except IndexError: raise cmdexc.CommandError("Nothing to undo!") @@ -880,20 +872,19 @@ class CommandDispatcher: if not 0 <= new_idx < self._count(): raise cmdexc.CommandError("Can't move tab to position {}!".format( new_idx)) - tabbed_browser = self._tabbed_browser() tab = self._current_widget() cur_idx = self._current_index() - icon = tabbed_browser.tabIcon(cur_idx) - label = tabbed_browser.page_title(cur_idx) + icon = self._tabbed_browser.tabIcon(cur_idx) + label = self._tabbed_browser.page_title(cur_idx) cmdutils.check_overflow(cur_idx, 'int') cmdutils.check_overflow(new_idx, 'int') - tabbed_browser.setUpdatesEnabled(False) + self._tabbed_browser.setUpdatesEnabled(False) try: - tabbed_browser.removeTab(cur_idx) - tabbed_browser.insertTab(new_idx, tab, icon, label) + self._tabbed_browser.removeTab(cur_idx) + self._tabbed_browser.insertTab(new_idx, tab, icon, label) self._set_current_index(new_idx) finally: - tabbed_browser.setUpdatesEnabled(True) + self._tabbed_browser.setUpdatesEnabled(True) @cmdutils.register(instance='command-dispatcher', scope='window', win_id='win_id') @@ -949,11 +940,10 @@ class CommandDispatcher: } idx = self._current_index() - tabbed_browser = self._tabbed_browser() if idx != -1: - env['QUTE_TITLE'] = tabbed_browser.page_title(idx) + env['QUTE_TITLE'] = self._tabbed_browser.page_title(idx) - webview = tabbed_browser.currentWidget() + webview = self._tabbed_browser.currentWidget() if webview is None: mainframe = None else: @@ -963,7 +953,7 @@ class CommandDispatcher: mainframe = webview.page().mainFrame() try: - url = tabbed_browser.current_url() + url = self._tabbed_browser.current_url() except qtutils.QtValueError: pass else: @@ -1055,7 +1045,7 @@ class CommandDispatcher: full=True, linenos='table') highlighted = pygments.highlight(html, lexer, formatter) current_url = self._current_url() - tab = self._tabbed_browser().tabopen(explicit=True) + tab = self._tabbed_browser.tabopen(explicit=True) tab.setHtml(highlighted, current_url) tab.viewing_source = True @@ -1128,7 +1118,7 @@ class CommandDispatcher: else: text = elem.evaluateJavaScript('this.value') self._editor = editor.ExternalEditor( - self._win_id, self._tabbed_browser()) + self._win_id, self._tabbed_browser) self._editor.editing_finished.connect( functools.partial(self.on_editing_finished, elem)) self._editor.edit(text) diff --git a/qutebrowser/mainwindow/mainwindow.py b/qutebrowser/mainwindow/mainwindow.py index 846020b86..4bafd76e5 100644 --- a/qutebrowser/mainwindow/mainwindow.py +++ b/qutebrowser/mainwindow/mainwindow.py @@ -22,6 +22,7 @@ import binascii import base64 import itertools +import functools from PyQt5.QtCore import pyqtSlot, QRect, QPoint, QTimer, Qt from PyQt5.QtWidgets import QWidget, QVBoxLayout, QApplication @@ -33,7 +34,7 @@ from qutebrowser.mainwindow import tabbedbrowser from qutebrowser.mainwindow.statusbar import bar from qutebrowser.completion import completionwidget from qutebrowser.keyinput import modeman -from qutebrowser.browser import hints, downloads, downloadview +from qutebrowser.browser import hints, downloads, downloadview, commands win_id_gen = itertools.count(0) @@ -89,8 +90,8 @@ class MainWindow(QWidget): Attributes: status: The StatusBar widget. + tabbed_browser: The TabbedBrowser widget. _downloadview: The DownloadView widget. - _tabbed_browser: The TabbedBrowser widget. _vbox: The main QVBoxLayout. _commandrunner: The main CommandRunner instance. """ @@ -138,9 +139,16 @@ class MainWindow(QWidget): self._downloadview = downloadview.DownloadView(self.win_id) - self._tabbed_browser = tabbedbrowser.TabbedBrowser(self.win_id) - objreg.register('tabbed-browser', self._tabbed_browser, scope='window', + self.tabbed_browser = tabbedbrowser.TabbedBrowser(self.win_id) + objreg.register('tabbed-browser', self.tabbed_browser, scope='window', window=self.win_id) + dispatcher = commands.CommandDispatcher(self.win_id, + self.tabbed_browser) + objreg.register('command-dispatcher', dispatcher, scope='window', + window=self.win_id) + self.tabbed_browser.destroyed.connect( + functools.partial(objreg.delete, 'command-dispatcher', + scope='window', window=self.win_id)) # We need to set an explicit parent for StatusBar because it does some # show/hide magic immediately which would mean it'd show up as a @@ -185,15 +193,15 @@ class MainWindow(QWidget): def _add_widgets(self): """Add or readd all widgets to the VBox.""" - self._vbox.removeWidget(self._tabbed_browser) + self._vbox.removeWidget(self.tabbed_browser) self._vbox.removeWidget(self._downloadview) self._vbox.removeWidget(self.status) position = config.get('ui', 'downloads-position') if position == 'north': self._vbox.addWidget(self._downloadview) - self._vbox.addWidget(self._tabbed_browser) + self._vbox.addWidget(self.tabbed_browser) elif position == 'south': - self._vbox.addWidget(self._tabbed_browser) + self._vbox.addWidget(self.tabbed_browser) self._vbox.addWidget(self._downloadview) else: raise ValueError("Invalid position {}!".format(position)) @@ -260,7 +268,7 @@ class MainWindow(QWidget): prompter = self._get_object('prompter') # misc - self._tabbed_browser.close_window.connect(self.close) + self.tabbed_browser.close_window.connect(self.close) mode_manager.entered.connect(hints.on_mode_entered) # status bar @@ -381,12 +389,12 @@ class MainWindow(QWidget): super().resizeEvent(e) self.resize_completion() self._downloadview.updateGeometry() - self._tabbed_browser.tabBar().refresh() + self.tabbed_browser.tabBar().refresh() def closeEvent(self, e): """Override closeEvent to display a confirmation if needed.""" confirm_quit = config.get('ui', 'confirm-quit') - tab_count = self._tabbed_browser.count() + tab_count = self.tabbed_browser.count() download_manager = objreg.get('download-manager', scope='window', window=self.win_id) download_count = download_manager.rowCount() @@ -419,4 +427,4 @@ class MainWindow(QWidget): objreg.get('session-manager').save_last_window_session() self._save_geometry() log.destroy.debug("Closing window {}".format(self.win_id)) - self._tabbed_browser.shutdown() + self.tabbed_browser.shutdown() diff --git a/qutebrowser/mainwindow/tabbedbrowser.py b/qutebrowser/mainwindow/tabbedbrowser.py index ab0849f8b..69bda97d4 100644 --- a/qutebrowser/mainwindow/tabbedbrowser.py +++ b/qutebrowser/mainwindow/tabbedbrowser.py @@ -29,7 +29,7 @@ from PyQt5.QtGui import QIcon from qutebrowser.config import config from qutebrowser.keyinput import modeman from qutebrowser.mainwindow import tabwidget -from qutebrowser.browser import signalfilter, commands, webview +from qutebrowser.browser import signalfilter, webview from qutebrowser.utils import log, usertypes, utils, qtutils, objreg, urlutils @@ -107,12 +107,6 @@ class TabbedBrowser(tabwidget.TabWidget): self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self._undo_stack = [] self._filter = signalfilter.SignalFilter(win_id, self) - dispatcher = commands.CommandDispatcher(win_id) - objreg.register('command-dispatcher', dispatcher, scope='window', - window=win_id) - self.destroyed.connect( - functools.partial(objreg.delete, 'command-dispatcher', - scope='window', window=win_id)) self._now_focused = None # FIXME adjust this to font size # https://github.com/The-Compiler/qutebrowser/issues/119 From 8eb483d66be774c71e35beaa53763faed2f5e5a5 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 18 May 2015 21:35:14 +0200 Subject: [PATCH 117/182] Set Qt.ItemNeverHasChildren for leaf model items. This allows Qt to do some optimizations. --- qutebrowser/browser/downloads.py | 2 +- qutebrowser/completion/models/base.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/qutebrowser/browser/downloads.py b/qutebrowser/browser/downloads.py index 6d12761f6..ab055cbf4 100644 --- a/qutebrowser/browser/downloads.py +++ b/qutebrowser/browser/downloads.py @@ -1038,7 +1038,7 @@ class DownloadManager(QAbstractListModel): """Override flags so items aren't selectable. The default would be Qt.ItemIsEnabled | Qt.ItemIsSelectable.""" - return Qt.ItemIsEnabled + return Qt.ItemIsEnabled | Qt.ItemNeverHasChildren def rowCount(self, parent=QModelIndex()): """Get count of active downloads.""" diff --git a/qutebrowser/completion/models/base.py b/qutebrowser/completion/models/base.py index f4b0cf4eb..b56444013 100644 --- a/qutebrowser/completion/models/base.py +++ b/qutebrowser/completion/models/base.py @@ -109,7 +109,8 @@ class BaseCompletionModel(QStandardItemModel): qtutils.ensure_valid(index) if index.parent().isValid(): # item - return Qt.ItemIsEnabled | Qt.ItemIsSelectable + return (Qt.ItemIsEnabled | Qt.ItemIsSelectable | + Qt.ItemNeverHasChildren) else: # category return Qt.NoItemFlags From c00dccfbb229719c59a352fb6e586209b4da6a75 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 18 May 2015 22:23:39 +0200 Subject: [PATCH 118/182] src2asciidoc: Improve error output on missing count arg. --- scripts/src2asciidoc.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/scripts/src2asciidoc.py b/scripts/src2asciidoc.py index 767552ebd..df775a93a 100755 --- a/scripts/src2asciidoc.py +++ b/scripts/src2asciidoc.py @@ -211,7 +211,11 @@ def _get_command_doc_count(cmd, parser): if cmd.count_arg is not None: yield "" yield "==== count" - yield parser.arg_descs[cmd.count_arg] + try: + yield parser.arg_descs[cmd.count_arg] + except KeyError as e: + raise KeyError("No description for count arg {!r} of command " + "{!r}!".format(cmd.count_arg, cmd.name)) def _get_command_doc_notes(cmd): From 91ad91cc7b89a0752b0f540cf83dc3cf6ea00894 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 18 May 2015 22:27:44 +0200 Subject: [PATCH 119/182] Spelling fixes. --- qutebrowser/browser/commands.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 68c2540b7..7dcc02758 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -1330,7 +1330,7 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', hide=True, modes=[KeyMode.caret], scope='window', count='count') def move_to_start_of_line(self, count=1): - """Move the cursor or select to the start of line.""" + """Move the cursor or selection to the start of the line.""" webview = self._current_widget() if not webview.selection_enabled: act = QWebPage.MoveToStartOfLine @@ -1342,7 +1342,7 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', hide=True, modes=[KeyMode.caret], scope='window', count='count') def move_to_end_of_line(self, count=1): - """Move the cursor or select to the end of line.""" + """Move the cursor or selection to the end of line.""" webview = self._current_widget() if not webview.selection_enabled: act = QWebPage.MoveToEndOfLine From 03e59051dcb3b83a614dbed0a90d5f2c33b86392 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 18 May 2015 22:28:11 +0200 Subject: [PATCH 120/182] Remove count for move-to-end-of-line. --- qutebrowser/browser/commands.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 7dcc02758..0c01260d7 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -1328,28 +1328,26 @@ class CommandDispatcher: webview.triggerPageAction(act) @cmdutils.register(instance='command-dispatcher', hide=True, - modes=[KeyMode.caret], scope='window', count='count') - def move_to_start_of_line(self, count=1): + modes=[KeyMode.caret], scope='window') + def move_to_start_of_line(self): """Move the cursor or selection to the start of the line.""" webview = self._current_widget() if not webview.selection_enabled: act = QWebPage.MoveToStartOfLine else: act = QWebPage.SelectStartOfLine - for _ in range(count): - webview.triggerPageAction(act) + webview.triggerPageAction(act) @cmdutils.register(instance='command-dispatcher', hide=True, - modes=[KeyMode.caret], scope='window', count='count') - def move_to_end_of_line(self, count=1): + modes=[KeyMode.caret], scope='window') + def move_to_end_of_line(self): """Move the cursor or selection to the end of line.""" webview = self._current_widget() if not webview.selection_enabled: act = QWebPage.MoveToEndOfLine else: act = QWebPage.SelectEndOfLine - for _ in range(count): - webview.triggerPageAction(act) + webview.triggerPageAction(act) @cmdutils.register(instance='command-dispatcher', hide=True, modes=[KeyMode.caret], scope='window', count='count') From f2e2748c590fe1e41c930f82cb82873a299f07f9 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 18 May 2015 22:31:05 +0200 Subject: [PATCH 121/182] Fix quotes. --- qutebrowser/mainwindow/statusbar/bar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/mainwindow/statusbar/bar.py b/qutebrowser/mainwindow/statusbar/bar.py index fe1a274cc..5f633eaaa 100644 --- a/qutebrowser/mainwindow/statusbar/bar.py +++ b/qutebrowser/mainwindow/statusbar/bar.py @@ -283,7 +283,7 @@ class StatusBar(QWidget): log.statusbar.debug("Setting insert_active to {}".format(val)) self._insert_active = val elif mode == usertypes.KeyMode.caret: - webview = objreg.get("tabbed-browser", scope="window", + webview = objreg.get('tabbed-browser', scope='window', window=self._win_id).currentWidget() log.statusbar.debug("Setting caret_mode - val {}, selection " "{}".format(val, webview.selection_enabled)) From e0dee14df4cf7af8eceb2ae68cbb2c1338b9c046 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 18 May 2015 22:34:21 +0200 Subject: [PATCH 122/182] Regenerate docs. --- README.asciidoc | 1 + doc/help/commands.asciidoc | 129 +++++++++++++++++++++++++++++++++++++ doc/help/settings.asciidoc | 14 ++++ 3 files changed, 144 insertions(+) diff --git a/README.asciidoc b/README.asciidoc index 7b23a0451..8742f11cb 100644 --- a/README.asciidoc +++ b/README.asciidoc @@ -138,6 +138,7 @@ Contributors, sorted by the number of commits in descending order: * Raphael Pierzina * Joel Torstensson * Claude +* Artur Shaik * ZDarian * Peter Vilim * John ShaggyTwoDope Jenkins diff --git a/doc/help/commands.asciidoc b/doc/help/commands.asciidoc index cca5156fa..c33bccaaf 100644 --- a/doc/help/commands.asciidoc +++ b/doc/help/commands.asciidoc @@ -689,12 +689,28 @@ How many steps to zoom out. |<>|Go back in the commandline history. |<>|Select the next completion item. |<>|Select the previous completion item. +|<>|Drop selection and keep selection mode enabled. |<>|Enter a key mode. |<>|Follow the currently selected hint. |<>|Leave the mode we're currently in. |<>|Show an error message in the statusbar. |<>|Show an info message in the statusbar. |<>|Show a warning message in the statusbar. +|<>|Move the cursor or selection to the end of the document. +|<>|Move the cursor or selection to the end of line. +|<>|Move the cursor or selection to the end of next block. +|<>|Move the cursor or selection to the end of previous block. +|<>|Move the cursor or selection to the end of the word. +|<>|Move the cursor or selection to the next char. +|<>|Move the cursor or selection to the next line. +|<>|Move the cursor or selection to the next word. +|<>|Move the cursor or selection to the previous char. +|<>|Move the cursor or selection to the prev line. +|<>|Move the cursor or selection to the previous word. +|<>|Move the cursor or selection to the start of the document. +|<>|Move the cursor or selection to the start of the line. +|<>|Move the cursor or selection to the start of next block. +|<>|Move the cursor or selection to the start of previous block. |<>|Open an external editor with the currently selected form field. |<>|Accept the current prompt. |<>|Answer no to a yes/no prompt. @@ -718,6 +734,8 @@ How many steps to zoom out. |<>|Scroll the current tab by 'count * dx/dy' pixels. |<>|Continue the search to the ([count]th) next term. |<>|Continue the search to the ([count]th) previous term. +|<>|Toggle caret selection mode. +|<>|Yank the selected text to the clipboard or primary selection. |============== [[command-accept]] === command-accept @@ -739,6 +757,10 @@ Select the next completion item. === completion-item-prev Select the previous completion item. +[[drop-selection]] +=== drop-selection +Drop selection and keep selection mode enabled. + [[enter-mode]] === enter-mode Syntax: +:enter-mode 'mode'+ @@ -783,6 +805,99 @@ Show a warning message in the statusbar. ==== positional arguments * +'text'+: The text to show. +[[move-to-end-of-document]] +=== move-to-end-of-document +Move the cursor or selection to the end of the document. + +[[move-to-end-of-line]] +=== move-to-end-of-line +Move the cursor or selection to the end of line. + +[[move-to-end-of-next-block]] +=== move-to-end-of-next-block +Move the cursor or selection to the end of next block. + +==== count +How many blocks to move. + +[[move-to-end-of-prev-block]] +=== move-to-end-of-prev-block +Move the cursor or selection to the end of previous block. + +==== count +How many blocks to move. + +[[move-to-end-of-word]] +=== move-to-end-of-word +Move the cursor or selection to the end of the word. + +==== count +How many words to move. + +[[move-to-next-char]] +=== move-to-next-char +Move the cursor or selection to the next char. + +==== count +How many lines to move. + +[[move-to-next-line]] +=== move-to-next-line +Move the cursor or selection to the next line. + +==== count +How many lines to move. + +[[move-to-next-word]] +=== move-to-next-word +Move the cursor or selection to the next word. + +==== count +How many words to move. + +[[move-to-prev-char]] +=== move-to-prev-char +Move the cursor or selection to the previous char. + +==== count +How many chars to move. + +[[move-to-prev-line]] +=== move-to-prev-line +Move the cursor or selection to the prev line. + +==== count +How many lines to move. + +[[move-to-prev-word]] +=== move-to-prev-word +Move the cursor or selection to the previous word. + +==== count +How many words to move. + +[[move-to-start-of-document]] +=== move-to-start-of-document +Move the cursor or selection to the start of the document. + +[[move-to-start-of-line]] +=== move-to-start-of-line +Move the cursor or selection to the start of the line. + +[[move-to-start-of-next-block]] +=== move-to-start-of-next-block +Move the cursor or selection to the start of next block. + +==== count +How many blocks to move. + +[[move-to-start-of-prev-block]] +=== move-to-start-of-prev-block +Move the cursor or selection to the start of previous block. + +==== count +How many blocks to move. + [[open-editor]] === open-editor Open an external editor with the currently selected form field. @@ -949,6 +1064,20 @@ Continue the search to the ([count]th) previous term. ==== count How many elements to ignore. +[[toggle-selection]] +=== toggle-selection +Toggle caret selection mode. + +[[yank-selected]] +=== yank-selected +Syntax: +:yank-selected [*--sel*] [*--keep*]+ + +Yank the selected text to the clipboard or primary selection. + +==== optional arguments +* +*-s*+, +*--sel*+: Use the primary selection instead of the clipboard. +* +*-k*+, +*--keep*+: If given, stay in visual mode after yanking. + == Debugging commands These commands are mainly intended for debugging. They are hidden if qutebrowser was started without the `--debug`-flag. diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc index 62763e6d7..7731b9d78 100644 --- a/doc/help/settings.asciidoc +++ b/doc/help/settings.asciidoc @@ -191,6 +191,8 @@ |<>|Background color of the statusbar if there is a warning. |<>|Background color of the statusbar if there is a prompt. |<>|Background color of the statusbar in insert mode. +|<>|Background color of the statusbar in caret mode. +|<>|Background color of the statusbar in caret mode with a selection |<>|Background color of the progress bar. |<>|Default foreground color of the URL in the statusbar. |<>|Foreground color of the URL in the statusbar on successful load. @@ -1546,6 +1548,18 @@ Background color of the statusbar in insert mode. Default: +pass:[darkgreen]+ +[[colors-statusbar.bg.caret]] +=== statusbar.bg.caret +Background color of the statusbar in caret mode. + +Default: +pass:[purple]+ + +[[colors-statusbar.bg.caret-selection]] +=== statusbar.bg.caret-selection +Background color of the statusbar in caret mode with a selection + +Default: +pass:[#a12dff]+ + [[colors-statusbar.progress.bg]] === statusbar.progress.bg Background color of the progress bar. From 9a1cf2b03a4afadfd3af95cd1e9492ad376d72db Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 18 May 2015 22:36:10 +0200 Subject: [PATCH 123/182] Fix deprecated config. --- qutebrowser/config/configdata.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 3bb1f1079..cb3a47be7 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -1303,10 +1303,10 @@ KEY_DATA = collections.OrderedDict([ ('move-to-end-of-document', ['G']), ('yank-selected -p', ['Y']), ('yank-selected', ['y', '', '']), - ('scroll -50 0', ['H']), - ('scroll 0 50', ['J']), - ('scroll 0 -50', ['K']), - ('scroll 50 0', ['L']), + ('scroll left', ['H']), + ('scroll down', ['J']), + ('scroll up', ['K']), + ('scroll right', ['L']), ])), ]) From 37b431f72fc6eaacf115c258aa2d94a8382b1c67 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 18 May 2015 22:37:48 +0200 Subject: [PATCH 124/182] Fix lint. --- scripts/src2asciidoc.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/src2asciidoc.py b/scripts/src2asciidoc.py index df775a93a..d764b1c7e 100755 --- a/scripts/src2asciidoc.py +++ b/scripts/src2asciidoc.py @@ -213,9 +213,9 @@ def _get_command_doc_count(cmd, parser): yield "==== count" try: yield parser.arg_descs[cmd.count_arg] - except KeyError as e: + except KeyError: raise KeyError("No description for count arg {!r} of command " - "{!r}!".format(cmd.count_arg, cmd.name)) + "{!r}!".format(cmd.count_arg, cmd.name)) def _get_command_doc_notes(cmd): From beb970d7d53ed855cec85ba17c04216ad765f323 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 18 May 2015 23:04:11 +0200 Subject: [PATCH 125/182] Strip whitespace for position_caret tests. It seems on Windows, QWebPage.SelectNextWord includes the trailing space. This should fix those tests on Windows. --- tests/javascript/position_caret/test_position_caret.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/javascript/position_caret/test_position_caret.py b/tests/javascript/position_caret/test_position_caret.py index a44cfc87d..11f67090f 100644 --- a/tests/javascript/position_caret/test_position_caret.py +++ b/tests/javascript/position_caret/test_position_caret.py @@ -51,7 +51,7 @@ class CaretTester: """Check whether the caret is before the MARKER text.""" self.js.run_file('position_caret.js') self.js.webview.triggerPageAction(QWebPage.SelectNextWord) - assert self.js.webview.selectedText() == "MARKER" + assert self.js.webview.selectedText().rstrip() == "MARKER" def check_scrolled(self): """Check if the page is scrolled down.""" From 98d1fca220c0d82169315411be5db67f1fddc971 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 18 May 2015 23:32:01 +0200 Subject: [PATCH 126/182] Use monkeypatch instead of mocker in some tests. See #660. --- tests/browser/test_webelem.py | 4 +-- tests/config/test_configtypes.py | 5 ++-- tests/keyinput/test_basekeyparser.py | 12 ++++---- tests/keyinput/test_modeparsers.py | 11 ++++---- tests/misc/test_editor.py | 42 ++++++++++++++-------------- tests/misc/test_readline.py | 7 +++-- tests/utils/test_urlutils.py | 16 +++++------ 7 files changed, 50 insertions(+), 47 deletions(-) diff --git a/tests/browser/test_webelem.py b/tests/browser/test_webelem.py index ba58820a9..419d2a55f 100644 --- a/tests/browser/test_webelem.py +++ b/tests/browser/test_webelem.py @@ -378,10 +378,10 @@ class TestIsEditable: webelem.config = old_config @pytest.fixture - def stubbed_config(self, config_stub, mocker): + def stubbed_config(self, config_stub, monkeypatch): """Fixture to create a config stub with an input section.""" config_stub.data = {'input': {}} - mocker.patch('qutebrowser.browser.webelem.config', new=config_stub) + monkeypatch.setattr('qutebrowser.browser.webelem.config', config_stub) return config_stub def test_input_plain(self): diff --git a/tests/config/test_configtypes.py b/tests/config/test_configtypes.py index 08a095177..871b6cf4d 100644 --- a/tests/config/test_configtypes.py +++ b/tests/config/test_configtypes.py @@ -883,11 +883,12 @@ class TestCommand: """Test Command.""" @pytest.fixture(autouse=True) - def setup(self, mocker, stubs): + def setup(self, monkeypatch, stubs): self.t = configtypes.Command() cmd_utils = stubs.FakeCmdUtils({'cmd1': stubs.FakeCommand("desc 1"), 'cmd2': stubs.FakeCommand("desc 2")}) - mocker.patch('qutebrowser.config.configtypes.cmdutils', new=cmd_utils) + monkeypatch.setattr('qutebrowser.config.configtypes.cmdutils', + cmd_utils) def test_validate_empty(self): """Test validate with an empty string.""" diff --git a/tests/keyinput/test_basekeyparser.py b/tests/keyinput/test_basekeyparser.py index bcd43c5a8..0b960b242 100644 --- a/tests/keyinput/test_basekeyparser.py +++ b/tests/keyinput/test_basekeyparser.py @@ -55,10 +55,10 @@ def fake_keyconfig(): @pytest.fixture -def mock_timer(mocker, stubs): +def mock_timer(monkeypatch, stubs): """Mock the Timer class used by the usertypes module with a stub.""" - mocker.patch('qutebrowser.keyinput.basekeyparser.usertypes.Timer', - new=stubs.FakeTimer) + monkeypatch.setattr('qutebrowser.keyinput.basekeyparser.usertypes.Timer', + stubs.FakeTimer) class TestSplitCount: @@ -206,11 +206,11 @@ class TestKeyChain: assert self.kp._keystring == '' def test_ambiguous_keychain(self, fake_keyevent_factory, config_stub, - mocker): + monkeypatch): """Test ambiguous keychain.""" config_stub.data = CONFIG - mocker.patch('qutebrowser.keyinput.basekeyparser.config', - new=config_stub) + monkeypatch.setattr('qutebrowser.keyinput.basekeyparser.config', + config_stub) timer = self.kp._ambiguous_timer assert not timer.isActive() # We start with 'a' where the keychain gives us an ambiguous result. diff --git a/tests/keyinput/test_modeparsers.py b/tests/keyinput/test_modeparsers.py index f61d98f67..05444892c 100644 --- a/tests/keyinput/test_modeparsers.py +++ b/tests/keyinput/test_modeparsers.py @@ -49,13 +49,14 @@ class TestsNormalKeyParser: # pylint: disable=protected-access @pytest.yield_fixture(autouse=True) - def setup(self, mocker, stubs, config_stub): + def setup(self, monkeypatch, stubs, config_stub): """Set up mocks and read the test config.""" - mocker.patch('qutebrowser.keyinput.basekeyparser.usertypes.Timer', - new=stubs.FakeTimer) + monkeypatch.setattr( + 'qutebrowser.keyinput.basekeyparser.usertypes.Timer', + stubs.FakeTimer) config_stub.data = CONFIG - mocker.patch('qutebrowser.keyinput.modeparsers.config', - new=config_stub) + monkeypatch.setattr('qutebrowser.keyinput.modeparsers.config', + config_stub) objreg.register('key-config', fake_keyconfig) self.kp = modeparsers.NormalKeyParser(0) diff --git a/tests/misc/test_editor.py b/tests/misc/test_editor.py index 6cc7ffc6d..abc2eabb0 100644 --- a/tests/misc/test_editor.py +++ b/tests/misc/test_editor.py @@ -41,18 +41,18 @@ class TestArg: """ @pytest.yield_fixture(autouse=True) - def setup(self, mocker, stubs): - mocker.patch('qutebrowser.misc.editor.QProcess', - new_callable=stubs.FakeQProcess) + def setup(self, monkeypatch, stubs): + monkeypatch.setattr('qutebrowser.misc.editor.QProcess', + stubs.FakeQProcess()) self.editor = editor.ExternalEditor(0) yield self.editor._cleanup() # pylint: disable=protected-access @pytest.fixture - def stubbed_config(self, config_stub, mocker): + def stubbed_config(self, config_stub, monkeypatch): """Fixture to create a config stub with an input section.""" config_stub.data = {'input': {}} - mocker.patch('qutebrowser.misc.editor.config', new=config_stub) + monkeypatch.setattr('qutebrowser.misc.editor.config', config_stub) return config_stub def test_simple_start_args(self, stubbed_config): @@ -98,14 +98,14 @@ class TestFileHandling: """ @pytest.fixture(autouse=True) - def setup(self, mocker, stubs, config_stub): - mocker.patch('qutebrowser.misc.editor.message', - new=stubs.MessageModule()) - mocker.patch('qutebrowser.misc.editor.QProcess', - new_callable=stubs.FakeQProcess) + def setup(self, monkeypatch, stubs, config_stub): + monkeypatch.setattr('qutebrowser.misc.editor.message', + stubs.MessageModule()) + monkeypatch.setattr('qutebrowser.misc.editor.QProcess', + stubs.FakeQProcess()) config_stub.data = {'general': {'editor': [''], 'editor-encoding': 'utf-8'}} - mocker.patch('qutebrowser.misc.editor.config', config_stub) + monkeypatch.setattr('qutebrowser.misc.editor.config', config_stub) self.editor = editor.ExternalEditor(0) def test_file_handling_closed_ok(self): @@ -147,12 +147,12 @@ class TestModifyTests: """ @pytest.fixture(autouse=True) - def setup(self, mocker, stubs, config_stub): - mocker.patch('qutebrowser.misc.editor.QProcess', - new_callable=stubs.FakeQProcess) + def setup(self, monkeypatch, stubs, config_stub): + monkeypatch.setattr('qutebrowser.misc.editor.QProcess', + stubs.FakeQProcess()) config_stub.data = {'general': {'editor': [''], 'editor-encoding': 'utf-8'}} - mocker.patch('qutebrowser.misc.editor.config', new=config_stub) + monkeypatch.setattr('qutebrowser.misc.editor.config', config_stub) self.editor = editor.ExternalEditor(0) self.editor.editing_finished = mock.Mock() @@ -219,14 +219,14 @@ class TestErrorMessage: """ @pytest.yield_fixture(autouse=True) - def setup(self, mocker, stubs, config_stub): - mocker.patch('qutebrowser.misc.editor.QProcess', - new_callable=stubs.FakeQProcess) - mocker.patch('qutebrowser.misc.editor.message', - new=stubs.MessageModule()) + def setup(self, monkeypatch, stubs, config_stub): + monkeypatch.setattr('qutebrowser.misc.editor.QProcess', + stubs.FakeQProcess()) + monkeypatch.setattr('qutebrowser.misc.editor.message', + stubs.MessageModule()) config_stub.data = {'general': {'editor': [''], 'editor-encoding': 'utf-8'}} - mocker.patch('qutebrowser.misc.editor.config', new=config_stub) + monkeypatch.setattr('qutebrowser.misc.editor.config', config_stub) self.editor = editor.ExternalEditor(0) yield self.editor._cleanup() # pylint: disable=protected-access diff --git a/tests/misc/test_readline.py b/tests/misc/test_readline.py index 9ace12b48..523c6f579 100644 --- a/tests/misc/test_readline.py +++ b/tests/misc/test_readline.py @@ -31,10 +31,11 @@ from qutebrowser.misc import readline @pytest.fixture -def mocked_qapp(mocker, stubs): +def mocked_qapp(monkeypatch, stubs): """Fixture that mocks readline.QApplication and returns it.""" - return mocker.patch('qutebrowser.misc.readline.QApplication', - new_callable=stubs.FakeQApplication) + stub = stubs.FakeQApplication() + monkeypatch.setattr('qutebrowser.misc.readline.QApplication', stub) + return stub class TestNoneWidget: diff --git a/tests/utils/test_urlutils.py b/tests/utils/test_urlutils.py index 52a3323ff..264de925b 100644 --- a/tests/utils/test_urlutils.py +++ b/tests/utils/test_urlutils.py @@ -81,10 +81,10 @@ class TestSearchUrl: """Test _get_search_url.""" @pytest.fixture(autouse=True) - def mock_config(self, config_stub, mocker): + def mock_config(self, config_stub, monkeypatch): """Fixture to patch urlutils.config with a stub.""" init_config_stub(config_stub) - mocker.patch('qutebrowser.utils.urlutils.config', config_stub) + monkeypatch.setattr('qutebrowser.utils.urlutils.config', config_stub) def test_default_engine(self): """Test default search engine.""" @@ -159,24 +159,24 @@ class TestIsUrl: ) @pytest.mark.parametrize('url', URLS) - def test_urls(self, mocker, config_stub, url): + def test_urls(self, monkeypatch, config_stub, url): """Test things which are URLs.""" init_config_stub(config_stub, 'naive') - mocker.patch('qutebrowser.utils.urlutils.config', config_stub) + monkeypatch.setattr('qutebrowser.utils.urlutils.config', config_stub) assert urlutils.is_url(url), url @pytest.mark.parametrize('url', NOT_URLS) - def test_not_urls(self, mocker, config_stub, url): + def test_not_urls(self, monkeypatch, config_stub, url): """Test things which are not URLs.""" init_config_stub(config_stub, 'naive') - mocker.patch('qutebrowser.utils.urlutils.config', config_stub) + monkeypatch.setattr('qutebrowser.utils.urlutils.config', config_stub) assert not urlutils.is_url(url), url @pytest.mark.parametrize('autosearch', [True, False]) - def test_search_autosearch(self, mocker, config_stub, autosearch): + def test_search_autosearch(self, monkeypatch, config_stub, autosearch): """Test explicit search with auto-search=True.""" init_config_stub(config_stub, autosearch) - mocker.patch('qutebrowser.utils.urlutils.config', config_stub) + monkeypatch.setattr('qutebrowser.utils.urlutils.config', config_stub) assert not urlutils.is_url('test foo') From b78d5f57aa3d4cd30f6c7d182a7fe32f18a8737e Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 19 May 2015 06:13:29 +0200 Subject: [PATCH 127/182] Make new-instance-open-target docs more clear. --- doc/help/settings.asciidoc | 8 ++++---- qutebrowser/config/configtypes.py | 10 ++++++---- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc index 7731b9d78..46539c3b7 100644 --- a/doc/help/settings.asciidoc +++ b/doc/help/settings.asciidoc @@ -398,10 +398,10 @@ How to open links in an existing instance if a new one is launched. Valid values: - * +tab+: Open a new tab in the existing window and activate it. - * +tab-bg+: Open a new background tab in the existing window and activate it. - * +tab-silent+: Open a new tab in the existing window without activating it. - * +tab-bg-silent+: Open a new background tab in the existing window without activating it. + * +tab+: Open a new tab in the existing window and activate the window. + * +tab-bg+: Open a new background tab in the existing window and activate the window. + * +tab-silent+: Open a new tab in the existing window without activating the window. + * +tab-bg-silent+: Open a new background tab in the existing window without activating the window. * +window+: Open in a new window. Default: +pass:[window]+ diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py index 78cd88cf8..b9c760116 100644 --- a/qutebrowser/config/configtypes.py +++ b/qutebrowser/config/configtypes.py @@ -1436,15 +1436,17 @@ class NewInstanceOpenTarget(BaseType): """How to open links in an existing instance if a new one is launched.""" valid_values = ValidValues(('tab', "Open a new tab in the existing " - "window and activate it."), + "window and activate the window."), ('tab-bg', "Open a new background tab in the " - "existing window and activate it."), + "existing window and activate the " + "window."), ('tab-silent', "Open a new tab in the existing " "window without activating " - "it."), + "the window."), ('tab-bg-silent', "Open a new background tab " "in the existing window " - "without activating it."), + "without activating the " + "window."), ('window', "Open in a new window.")) From 7ca9a007f855907d3d77973606e58b263fb8a200 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 19 May 2015 06:40:42 +0200 Subject: [PATCH 128/182] Update changelog. --- CHANGELOG.asciidoc | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 2ae9b7301..137a4bd8c 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -31,6 +31,7 @@ Added - New arguments `--datadir` and `--cachedir` to set the data/cache location. - New arguments `--basedir` and `--temp-basedir` (intended for debugging) to set a different base directory for all data, which allows multiple invocations. - New argument `--no-err-windows` to suppress all error windows. +- New visual/caret mode (bound to `v`) to select text by keyboard. Changed ~~~~~~~ From cc884510030f3a466ef7b5c91860b424362f6fc6 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 19 May 2015 06:43:42 +0200 Subject: [PATCH 129/182] Update cheatsheet. --- misc/cheatsheet.svg | 43 +++++++++++++++++++++++-------------------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/misc/cheatsheet.svg b/misc/cheatsheet.svg index 1d9b2c5a1..2d58ffe89 100644 --- a/misc/cheatsheet.svg +++ b/misc/cheatsheet.svg @@ -33,8 +33,8 @@ inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:zoom="0.8791156" - inkscape:cx="641.54005" - inkscape:cy="233.0095" + inkscape:cx="330.38958" + inkscape:cy="406.80913" inkscape:document-units="px" inkscape:current-layer="layer1" width="1024px" @@ -3326,27 +3326,15 @@ style="font-size:8px">tab co: closeother tabscd: clea + x="274.21381" + y="343.17578" + id="tspan4052">(10) cache) + visualmode From 069d7b26a28d2922ae1277e301cee927606fdf08 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 19 May 2015 07:44:45 +0200 Subject: [PATCH 130/182] pytest: Use common fixture for fake_keyconfig. --- tests/keyinput/conftest.py | 45 ++++++++++++++++++++++++++++ tests/keyinput/test_basekeyparser.py | 21 +------------ tests/keyinput/test_modeparsers.py | 15 ++-------- 3 files changed, 49 insertions(+), 32 deletions(-) create mode 100644 tests/keyinput/conftest.py diff --git a/tests/keyinput/conftest.py b/tests/keyinput/conftest.py new file mode 100644 index 000000000..614017876 --- /dev/null +++ b/tests/keyinput/conftest.py @@ -0,0 +1,45 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2015 Florian Bruhin (The Compiler) : +# +# This file is part of qutebrowser. +# +# qutebrowser is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# qutebrowser is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with qutebrowser. If not, see . + +"""pytest fixtures for tests.keyinput.""" + +import pytest + +from unittest import mock +from qutebrowser.utils import objreg + + +BINDINGS = {'test': {'': 'ctrla', + 'a': 'a', + 'ba': 'ba', + 'ax': 'ax', + 'ccc': 'ccc', + '0': '0'}, + 'test2': {'foo': 'bar', '': 'ctrlx'}, + 'normal': {'a': 'a', 'ba': 'ba'}} + + +@pytest.yield_fixture +def fake_keyconfig(): + """Create a mock of a KeyConfiguration and register it into objreg.""" + fake_keyconfig = mock.Mock(spec=['get_bindings_for']) + fake_keyconfig.get_bindings_for.side_effect = lambda s: BINDINGS[s] + objreg.register('key-config', fake_keyconfig) + yield + objreg.delete('key-config') diff --git a/tests/keyinput/test_basekeyparser.py b/tests/keyinput/test_basekeyparser.py index 0b960b242..e8f7f9325 100644 --- a/tests/keyinput/test_basekeyparser.py +++ b/tests/keyinput/test_basekeyparser.py @@ -29,31 +29,12 @@ from PyQt5.QtCore import Qt import pytest from qutebrowser.keyinput import basekeyparser -from qutebrowser.utils import objreg, log +from qutebrowser.utils import log CONFIG = {'input': {'timeout': 100}} -BINDINGS = {'test': {'': 'ctrla', - 'a': 'a', - 'ba': 'ba', - 'ax': 'ax', - 'ccc': 'ccc', - '0': '0'}, - 'test2': {'foo': 'bar', '': 'ctrlx'}} - - -@pytest.yield_fixture -def fake_keyconfig(): - """Create a mock of a KeyConfiguration and register it into objreg.""" - fake_keyconfig = mock.Mock(spec=['get_bindings_for']) - fake_keyconfig.get_bindings_for.side_effect = lambda s: BINDINGS[s] - objreg.register('key-config', fake_keyconfig) - yield - objreg.delete('key-config') - - @pytest.fixture def mock_timer(monkeypatch, stubs): """Mock the Timer class used by the usertypes module with a stub.""" diff --git a/tests/keyinput/test_modeparsers.py b/tests/keyinput/test_modeparsers.py index 05444892c..47789b5b4 100644 --- a/tests/keyinput/test_modeparsers.py +++ b/tests/keyinput/test_modeparsers.py @@ -19,25 +19,18 @@ """Tests for mode parsers.""" +from unittest import mock + from PyQt5.QtCore import Qt -from unittest import mock import pytest from qutebrowser.keyinput import modeparsers -from qutebrowser.utils import objreg CONFIG = {'input': {'partial-timeout': 100}} -BINDINGS = {'normal': {'a': 'a', 'ba': 'ba'}} - - -fake_keyconfig = mock.Mock(spec=['get_bindings_for']) -fake_keyconfig.get_bindings_for.side_effect = lambda s: BINDINGS[s] - - class TestsNormalKeyParser: """Tests for NormalKeyParser. @@ -49,7 +42,7 @@ class TestsNormalKeyParser: # pylint: disable=protected-access @pytest.yield_fixture(autouse=True) - def setup(self, monkeypatch, stubs, config_stub): + def setup(self, monkeypatch, stubs, config_stub, fake_keyconfig): """Set up mocks and read the test config.""" monkeypatch.setattr( 'qutebrowser.keyinput.basekeyparser.usertypes.Timer', @@ -58,11 +51,9 @@ class TestsNormalKeyParser: monkeypatch.setattr('qutebrowser.keyinput.modeparsers.config', config_stub) - objreg.register('key-config', fake_keyconfig) self.kp = modeparsers.NormalKeyParser(0) self.kp.execute = mock.Mock() yield - objreg.delete('key-config') def test_keychain(self, fake_keyevent_factory): """Test valid keychain.""" From ad181ec7ebe8d5d6e6ddd19d1901c5236124a8b5 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 19 May 2015 08:58:27 +0200 Subject: [PATCH 131/182] Spelling fix on cheatsheet. --- misc/cheatsheet.svg | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/misc/cheatsheet.svg b/misc/cheatsheet.svg index 2d58ffe89..13cf3101e 100644 --- a/misc/cheatsheet.svg +++ b/misc/cheatsheet.svg @@ -13,7 +13,7 @@ height="640" id="svg2" sodipodi:version="0.32" - inkscape:version="0.91 r13725" + inkscape:version="0.48.5 r10040" version="1.0" sodipodi:docname="cheatsheet.svg" inkscape:output_extension="org.inkscape.output.svg.inkscape" @@ -33,16 +33,16 @@ inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:zoom="0.8791156" - inkscape:cx="330.38958" - inkscape:cy="406.80913" + inkscape:cx="768.67127" + inkscape:cy="133.80749" inkscape:document-units="px" inkscape:current-layer="layer1" width="1024px" height="640px" showgrid="false" - inkscape:window-width="1366" - inkscape:window-height="768" - inkscape:window-x="0" + inkscape:window-width="636" + inkscape:window-height="536" + inkscape:window-x="2560" inkscape:window-y="0" showguides="true" inkscape:guide-bbox="true" @@ -1939,7 +1939,7 @@ x="542.06946" sodipodi:role="line" id="tspan4938" - style="font-size:8px">scollscroll Date: Tue, 19 May 2015 12:38:13 +0200 Subject: [PATCH 132/182] tox: Update pytest to 2.7.1. --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index aa7fd87e5..d7e1b2050 100644 --- a/tox.ini +++ b/tox.ini @@ -20,7 +20,7 @@ setenv = QT_QPA_PLATFORM_PLUGIN_PATH={envdir}/Lib/site-packages/PyQt5/plugins/pl passenv = DISPLAY XAUTHORITY HOME deps = py==1.4.27 - pytest==2.7.0 + pytest==2.7.1 pytest-capturelog==0.7 pytest-qt==1.3.0 pytest-mock==0.5 From e187cda29240a041355ae4ff594279b4dfa6ea8f Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 19 May 2015 16:23:50 +0200 Subject: [PATCH 133/182] Sort attrs for utils.get_repr(). --- qutebrowser/utils/utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qutebrowser/utils/utils.py b/qutebrowser/utils/utils.py index 298aba2b0..920da944c 100644 --- a/qutebrowser/utils/utils.py +++ b/qutebrowser/utils/utils.py @@ -510,7 +510,8 @@ def get_repr(obj, constructor=False, **attrs): """ cls = qualname(obj.__class__) parts = [] - for name, val in attrs.items(): + items = sorted(attrs.items()) + for name, val in items: parts.append('{}={!r}'.format(name, val)) if constructor: return '{}({})'.format(cls, ', '.join(parts)) From 1f048a38f8f671f5166b86eb6bcb8d025fe6a532 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 20 May 2015 13:19:28 +0200 Subject: [PATCH 134/182] urlutils: Remove dead code in _get_search_url. term should always contain something. --- qutebrowser/utils/urlutils.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/qutebrowser/utils/urlutils.py b/qutebrowser/utils/urlutils.py index ba4ed1786..c7e0c0e65 100644 --- a/qutebrowser/utils/urlutils.py +++ b/qutebrowser/utils/urlutils.py @@ -74,8 +74,7 @@ def _get_search_url(txt): """ log.url.debug("Finding search engine for '{}'".format(txt)) engine, term = _parse_search_term(txt) - if not term: - raise FuzzyUrlError("No search term given") + assert term if engine is None: template = config.get('searchengines', 'DEFAULT') else: From c0b41d8c624f6d4039fa33f7edc77e7e3e0b8349 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 20 May 2015 13:20:01 +0200 Subject: [PATCH 135/182] urlutils: Use utils.raises in _is_url_naive. --- qutebrowser/utils/urlutils.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/qutebrowser/utils/urlutils.py b/qutebrowser/utils/urlutils.py index c7e0c0e65..d267c9422 100644 --- a/qutebrowser/utils/urlutils.py +++ b/qutebrowser/utils/urlutils.py @@ -29,7 +29,7 @@ from PyQt5.QtCore import QUrl from PyQt5.QtNetwork import QHostInfo, QHostAddress from qutebrowser.config import config, configexc -from qutebrowser.utils import log, qtutils, message +from qutebrowser.utils import log, qtutils, message, utils from qutebrowser.commands import cmdexc @@ -94,11 +94,7 @@ def _is_url_naive(urlstr): True if the URL really is a URL, False otherwise. """ url = qurl_from_user_input(urlstr) - try: - ipaddress.ip_address(urlstr) - except ValueError: - pass - else: + if not utils.raises(ValueError, ipaddress.ip_address, urlstr): # Valid IPv4/IPv6 address return True From 40cc35403052629b8fab2d6b9aa80982510e16f9 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 20 May 2015 13:21:48 +0200 Subject: [PATCH 136/182] urlutils: Pass URL string to _is_url_dns. --- qutebrowser/utils/urlutils.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/qutebrowser/utils/urlutils.py b/qutebrowser/utils/urlutils.py index d267c9422..78112f1c6 100644 --- a/qutebrowser/utils/urlutils.py +++ b/qutebrowser/utils/urlutils.py @@ -114,15 +114,16 @@ def _is_url_naive(urlstr): return False -def _is_url_dns(url): +def _is_url_dns(urlstr): """Check if a URL is really a URL via DNS. Args: - url: The URL to check for as QUrl, ideally via qurl_from_user_input. + url: The URL to check for as a string. Return: True if the URL really is a URL, False otherwise. """ + url = qurl_from_user_input(urlstr) if not url.isValid(): return False host = url.host() @@ -251,7 +252,7 @@ def is_url(urlstr): log.url.debug("Checking via DNS") # We want to use qurl_from_user_input here, as the user might enter # "foo.de" and that should be treated as URL here. - url = _is_url_dns(qurl_from_user_input(urlstr)) + url = _is_url_dns(urlstr) elif autosearch == 'naive': log.url.debug("Checking via naive check") url = _is_url_naive(urlstr) From e590bf26ad00c4fcdaa30ab7fe68e16912b1b5e4 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 20 May 2015 13:22:19 +0200 Subject: [PATCH 137/182] urlutils: Check bogus IPs in _is_url_dns. --- qutebrowser/utils/urlutils.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/qutebrowser/utils/urlutils.py b/qutebrowser/utils/urlutils.py index 78112f1c6..98637635a 100644 --- a/qutebrowser/utils/urlutils.py +++ b/qutebrowser/utils/urlutils.py @@ -126,6 +126,14 @@ def _is_url_dns(urlstr): url = qurl_from_user_input(urlstr) if not url.isValid(): return False + + if (utils.raises(ValueError, ipaddress.ip_address, urlstr) and + not QHostAddress(urlstr).isNull()): + log.url.debug("Bogus IP URL -> False") + # Qt treats things like "23.42" or "1337" or "0xDEAD" as valid URLs + # which we don't want to. + return False + host = url.host() log.url.debug("DNS request for {}".format(host)) if not host: From 14df72a7a178932f5d4506c0165b214f7083df0c Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 20 May 2015 13:23:02 +0200 Subject: [PATCH 138/182] urlutils: Add get_errstring(). --- qutebrowser/utils/urlutils.py | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/qutebrowser/utils/urlutils.py b/qutebrowser/utils/urlutils.py index 98637635a..33cb589fc 100644 --- a/qutebrowser/utils/urlutils.py +++ b/qutebrowser/utils/urlutils.py @@ -315,20 +315,15 @@ def invalid_url_error(win_id, url, action): if url.isValid(): raise ValueError("Calling invalid_url_error with valid URL {}".format( url.toDisplayString())) - errstring = "Trying to {} with invalid URL".format(action) - if url.errorString(): - errstring += " - {}".format(url.errorString()) + errstring = get_errstring( + url, "Trying to {} with invalid URL".format(action)) message.error(win_id, errstring) def raise_cmdexc_if_invalid(url): """Check if the given QUrl is invalid, and if so, raise a CommandError.""" if not url.isValid(): - errstr = "Invalid URL {}".format(url.toDisplayString()) - url_error = url.errorString() - if url_error: - errstr += " - {}".format(url_error) - raise cmdexc.CommandError(errstr) + raise cmdexc.CommandError(get_errstring(url)) def filename_from_url(url): @@ -359,6 +354,23 @@ def host_tuple(url): return (url.scheme(), url.host(), url.port()) +def get_errstring(url, base="Invalid URL"): + """Get an error string for an URL. + + Args: + url: The URL as a QUrl. + base: The base error string. + + Return: + A new string with url.errorString() is appended if available. + """ + url_error = url.errorString() + if url_error: + return base + " - {}".format(url_error) + else: + return base + + class FuzzyUrlError(Exception): """Exception raised by fuzzy_url on problems. From f8db4b814795763e68ca966245320722387a6cbf Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 20 May 2015 13:23:41 +0200 Subject: [PATCH 139/182] urlutils: Improve debug logging. --- qutebrowser/utils/urlutils.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/qutebrowser/utils/urlutils.py b/qutebrowser/utils/urlutils.py index 33cb589fc..e15fa94b5 100644 --- a/qutebrowser/utils/urlutils.py +++ b/qutebrowser/utils/urlutils.py @@ -125,6 +125,7 @@ def _is_url_dns(urlstr): """ url = qurl_from_user_input(urlstr) if not url.isValid(): + log.url.debug("Invalid URL -> False") return False if (utils.raises(ValueError, ipaddress.ip_address, urlstr) and @@ -135,9 +136,10 @@ def _is_url_dns(urlstr): return False host = url.host() - log.url.debug("DNS request for {}".format(host)) if not host: + log.url.debug("URL has no host -> False") return False + log.url.debug("Doing DNS request for {}".format(host)) info = QHostInfo.fromName(host) return not info.error() @@ -257,7 +259,7 @@ def is_url(urlstr): log.url.debug("Is an special URL.") url = True elif autosearch == 'dns': - log.url.debug("Checking via DNS") + log.url.debug("Checking via DNS check") # We want to use qurl_from_user_input here, as the user might enter # "foo.de" and that should be treated as URL here. url = _is_url_dns(urlstr) @@ -266,6 +268,7 @@ def is_url(urlstr): url = _is_url_naive(urlstr) else: raise ValueError("Invalid autosearch value") + log.url.debug("url = {}".format(url)) return url and qurl_from_user_input(urlstr).isValid() From 6f904759b530d5f7934676ad1e45a712e7c7b526 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 20 May 2015 13:24:16 +0200 Subject: [PATCH 140/182] urlutils: Fix str() of FuzzyUrlError. --- qutebrowser/utils/urlutils.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/qutebrowser/utils/urlutils.py b/qutebrowser/utils/urlutils.py index e15fa94b5..9cda7c56e 100644 --- a/qutebrowser/utils/urlutils.py +++ b/qutebrowser/utils/urlutils.py @@ -379,17 +379,19 @@ class FuzzyUrlError(Exception): """Exception raised by fuzzy_url on problems. Attributes: + msg: The error message to use. url: The QUrl which caused the error. """ def __init__(self, msg, url=None): super().__init__(msg) - if url is not None: - assert not url.isValid() + if url is not None and url.isValid(): + raise ValueError("Got valid URL {}!".format(url.toDisplayString())) self.url = url + self.msg = msg def __str__(self): if self.url is None or not self.url.errorString(): - return str(super()) + return self.msg else: - return '{}: {}'.format(str(super()), self.url.errorString()) + return '{}: {}'.format(self.msg, self.url.errorString()) From 269676318b8735ec9ee0df5ee31e1841b4c90590 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 20 May 2015 13:24:48 +0200 Subject: [PATCH 141/182] urlutils: Raise exception on errors in host_tuple. --- qutebrowser/browser/network/networkmanager.py | 22 ++++++++++++++----- qutebrowser/utils/urlutils.py | 22 +++++++++++++++++-- 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/qutebrowser/browser/network/networkmanager.py b/qutebrowser/browser/network/networkmanager.py index 8e74f05c3..3b4c71ed0 100644 --- a/qutebrowser/browser/network/networkmanager.py +++ b/qutebrowser/browser/network/networkmanager.py @@ -195,10 +195,20 @@ class NetworkManager(QNetworkAccessManager): errors = [SslError(e) for e in errors] ssl_strict = config.get('network', 'ssl-strict') if ssl_strict == 'ask': - host_tpl = urlutils.host_tuple(reply.url()) - if set(errors).issubset(self._accepted_ssl_errors[host_tpl]): + try: + host_tpl = urlutils.host_tuple(reply.url()) + except ValueError: + host_tpl = None + is_accepted = False + is_rejected = False + else: + is_accepted = set(errors).issubset( + self._accepted_ssl_errors[host_tpl]) + is_rejected = set(errors).issubset( + self._rejected_ssl_errors[host_tpl]) + if is_accepted: reply.ignoreSslErrors() - elif set(errors).issubset(self._rejected_ssl_errors[host_tpl]): + elif is_rejected: pass else: err_string = '\n'.join('- ' + err.errorString() for err in @@ -208,9 +218,11 @@ class NetworkManager(QNetworkAccessManager): owner=reply) if answer: reply.ignoreSslErrors() - self._accepted_ssl_errors[host_tpl] += errors + d = self._accepted_ssl_errors else: - self._rejected_ssl_errors[host_tpl] += errors + d = self._rejected_ssl_errors + if host_tpl is not None: + d[host_tpl] += errors elif ssl_strict: pass else: diff --git a/qutebrowser/utils/urlutils.py b/qutebrowser/utils/urlutils.py index 9cda7c56e..5428ba921 100644 --- a/qutebrowser/utils/urlutils.py +++ b/qutebrowser/utils/urlutils.py @@ -350,11 +350,29 @@ def filename_from_url(url): def host_tuple(url): - """Get a (scheme, host, port) tuple. + """Get a (scheme, host, port) tuple from a QUrl. This is suitable to identify a connection, e.g. for SSL errors. """ - return (url.scheme(), url.host(), url.port()) + if not url.isValid(): + raise ValueError(get_errstring(url)) + scheme, host, port = url.scheme(), url.host(), url.port() + assert scheme + if not host: + raise ValueError("Got URL {} without host.".format( + url.toDisplayString())) + if port == -1: + port_mapping = { + 'http': 80, + 'https': 443, + 'ftp': 21, + } + try: + port = port_mapping[scheme] + except KeyError: + raise ValueError("Got URL {} with unknown port.".format( + url.toDisplayString())) + return scheme, host, port def get_errstring(url, base="Invalid URL"): From ddb6743b262b139801405395a1f1a7a1117d1347 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 20 May 2015 13:33:50 +0200 Subject: [PATCH 142/182] urlutils: Clean up qurl_from_user_input in is_url. --- qutebrowser/utils/urlutils.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/qutebrowser/utils/urlutils.py b/qutebrowser/utils/urlutils.py index 5428ba921..a7c0b0320 100644 --- a/qutebrowser/utils/urlutils.py +++ b/qutebrowser/utils/urlutils.py @@ -236,6 +236,7 @@ def is_url(urlstr): urlstr = urlstr.strip() qurl = QUrl(urlstr) + qurl_userinput = qurl_from_user_input(urlstr) if not autosearch: # no autosearch, so everything is a URL unless it has an explicit @@ -246,6 +247,9 @@ def is_url(urlstr): else: return False + if not qurl_userinput.isValid(): + return False + if _has_explicit_scheme(qurl): # URLs with explicit schemes are always URLs log.url.debug("Contains explicit scheme") @@ -269,7 +273,7 @@ def is_url(urlstr): else: raise ValueError("Invalid autosearch value") log.url.debug("url = {}".format(url)) - return url and qurl_from_user_input(urlstr).isValid() + return url and qurl_userinput.isValid() def qurl_from_user_input(urlstr): From aaab05793e4dc91d59e06f005f8ff71408013cef Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 20 May 2015 13:34:36 +0200 Subject: [PATCH 143/182] urlutils: Handle localhost correctly in all cases. --- qutebrowser/utils/urlutils.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/qutebrowser/utils/urlutils.py b/qutebrowser/utils/urlutils.py index a7c0b0320..3ed82b0db 100644 --- a/qutebrowser/utils/urlutils.py +++ b/qutebrowser/utils/urlutils.py @@ -108,8 +108,6 @@ def _is_url_naive(urlstr): return False elif '.' in url.host(): return True - elif url.host() == 'localhost': - return True else: return False @@ -258,6 +256,9 @@ def is_url(urlstr): # A URL will never contain a space log.url.debug("Contains space -> no URL") url = False + elif qurl_userinput.host() in ('localhost', '127.0.0.1', '::1'): + log.url.debug("Is localhost.") + url = True elif is_special_url(qurl): # Special URLs are always URLs, even with autosearch=False log.url.debug("Is an special URL.") From 0252f5fdbf564c044823eca5f293b91e67b25441 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 20 May 2015 13:37:44 +0200 Subject: [PATCH 144/182] tox: Update pytest-html to 1.2. --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index d7e1b2050..cf1d596d6 100644 --- a/tox.ini +++ b/tox.ini @@ -24,7 +24,7 @@ deps = pytest-capturelog==0.7 pytest-qt==1.3.0 pytest-mock==0.5 - pytest-html==1.1 + pytest-html==1.2 # We don't use {[testenv:mkvenv]commands} here because that seems to be broken # on Ubuntu Trusty. commands = From 43898ebb7146cd2c9fdc9e59910a20bcddece396 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 20 May 2015 13:38:56 +0200 Subject: [PATCH 145/182] Update changelog. --- CHANGELOG.asciidoc | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 137a4bd8c..c18927953 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -58,6 +58,7 @@ Fixed ~~~~~ - Scrolling should now work more reliably on some pages where arrow keys worked but `hjkl` didn't. +- Small improvements when checking if an input is an URL or not. v0.2.2 (unreleased) ------------------- From ee0eabc202b0b5eebda7bf7dd296828927e6035f Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 21 May 2015 07:45:20 +0200 Subject: [PATCH 146/182] scripts: Add --profile-dot to run_profile. --- scripts/run_profile.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/scripts/run_profile.py b/scripts/run_profile.py index 698407a23..8b0c85aae 100755 --- a/scripts/run_profile.py +++ b/scripts/run_profile.py @@ -39,18 +39,29 @@ if '--profile-keep' in sys.argv: profilefile = os.path.join(os.getcwd(), 'profile') else: profilefile = os.path.join(tempdir, 'profile') + if '--profile-noconv' in sys.argv: sys.argv.remove('--profile-noconv') noconv = True else: noconv = False +if '--profile-dot' in sys.argv: + sys.argv.remove('--profile-dot') + dot = True +else: + dot = False + callgraphfile = os.path.join(tempdir, 'callgraph') profiler = cProfile.Profile() profiler.run('qutebrowser.qutebrowser.main()') profiler.dump_stats(profilefile) if not noconv: - subprocess.call(['pyprof2calltree', '-k', '-i', profilefile, - '-o', callgraphfile]) + if dot: + subprocess.call('gprof2dot -f pstats profile | dot -Tpng | feh -F -', + shell=True) # yep, shell=True. I know what I'm doing. + else: + subprocess.call(['pyprof2calltree', '-k', '-i', profilefile, + '-o', callgraphfile]) shutil.rmtree(tempdir) From 8d15bbdded8eece4c90eed6fe9a08fec02d37e7a Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 24 May 2015 21:00:46 +0200 Subject: [PATCH 147/182] utils.version: Add SIP line on ImportError. --- qutebrowser/utils/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/utils/version.py b/qutebrowser/utils/version.py index 6823685d7..827762af4 100644 --- a/qutebrowser/utils/version.py +++ b/qutebrowser/utils/version.py @@ -130,7 +130,7 @@ def _module_versions(): try: import sipconfig # pylint: disable=import-error,unused-variable except ImportError: - pass + lines.append('SIP: ?') else: try: lines.append('SIP: {}'.format( From 120d2e12b058659075ab11536d96a981682a09fa Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 25 May 2015 01:21:57 +0200 Subject: [PATCH 148/182] Improve QtValueError wording for ensure_not_null. --- qutebrowser/utils/qtutils.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/qutebrowser/utils/qtutils.py b/qutebrowser/utils/qtutils.py index 939e1ed81..f175dc202 100644 --- a/qutebrowser/utils/qtutils.py +++ b/qutebrowser/utils/qtutils.py @@ -131,7 +131,7 @@ def ensure_valid(obj): def ensure_not_null(obj): """Ensure a Qt object with an .isNull() method is not null.""" if obj.isNull(): - raise QtValueError(obj) + raise QtValueError(obj, null=True) def check_qdatastream(stream): @@ -322,12 +322,15 @@ class QtValueError(ValueError): """Exception which gets raised by ensure_valid.""" - def __init__(self, obj): + def __init__(self, obj, null=False): try: self.reason = obj.errorString() except AttributeError: self.reason = None - err = "{} is not valid".format(obj) + if null: + err = "{} is null".format(obj) + else: + err = "{} is not valid".format(obj) if self.reason: err += ": {}".format(self.reason) super().__init__(err) From 0f13d9325b59ff7ed60b564e30532f8bce7b33a2 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 25 May 2015 01:26:52 +0200 Subject: [PATCH 149/182] Don't use parametrization for deprecated keys. This showed up as 2400 tests for what basically is one. --- tests/config/test_config.py | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/tests/config/test_config.py b/tests/config/test_config.py index 123b2a412..d5fab2ed1 100644 --- a/tests/config/test_config.py +++ b/tests/config/test_config.py @@ -157,14 +157,6 @@ class TestConfigParser: self.cfg.get('general', 'bar') # pylint: disable=bad-config-call -def keyconfig_deprecated_test_cases(): - """Generator yielding test cases (command, rgx) for TestKeyConfigParser.""" - for sect in configdata.KEY_DATA.values(): - for command in sect: - for rgx, _repl in configdata.CHANGED_KEY_COMMANDS: - yield (command, rgx) - - class TestKeyConfigParser: """Test config.parsers.keyconf.KeyConfigParser.""" @@ -185,10 +177,13 @@ class TestKeyConfigParser: with pytest.raises(keyconf.KeyConfigError): kcp._read_command(cmdline_test.cmd) - @pytest.mark.parametrize('command, rgx', keyconfig_deprecated_test_cases()) - def test_default_config_no_deprecated(self, command, rgx): + @pytest.mark.parametrize('rgx', [rgx for rgx, _repl + in configdata.CHANGED_KEY_COMMANDS]) + def test_default_config_no_deprecated(self, rgx): """Make sure the default config contains no deprecated commands.""" - assert rgx.match(command) is None + for sect in configdata.KEY_DATA.values(): + for command in sect: + assert rgx.match(command) is None @pytest.mark.parametrize( 'old, new_expected', From 6d879bbca32c51ba501400826179dac6958a29ba Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 25 May 2015 01:38:17 +0200 Subject: [PATCH 150/182] Exclude resources.py from coverage. --- .coveragerc | 1 + 1 file changed, 1 insertion(+) diff --git a/.coveragerc b/.coveragerc index ff714c43d..16bebb0cc 100644 --- a/.coveragerc +++ b/.coveragerc @@ -3,6 +3,7 @@ branch = true omit = qutebrowser/__main__.py */__init__.py + qutebrowser/resources.py [report] exclude_lines = From a345b02729034e39f2b245592510b17307e4e1df Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 25 May 2015 11:28:50 +0200 Subject: [PATCH 151/182] Fix exception when downloading links without name. We also set a default name to prevent "is a directory" errors. This is a regression introduced in 8f33fcfc52cf598d0aa11a347992c87010d3e37a. Fixes #682. --- CHANGELOG.asciidoc | 1 + qutebrowser/browser/downloads.py | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index c18927953..0af6c0d0f 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -73,6 +73,7 @@ Fixed - Various fixes for deprecated key bindings and auto-migrations. - Workaround for qutebrowser not starting when there are NUL-bytes in the history (because of a currently unknown bug) - Fixed handling of keybindings containing Ctrl/Meta on OS X. +- Fixed crash when downloading an URL without filename (e.g. magnet links) via "Save as...". https://github.com/The-Compiler/qutebrowser/releases/tag/v0.2.1[v0.2.1] ----------------------------------------------------------------------- diff --git a/qutebrowser/browser/downloads.py b/qutebrowser/browser/downloads.py index ab055cbf4..790459f6a 100644 --- a/qutebrowser/browser/downloads.py +++ b/qutebrowser/browser/downloads.py @@ -686,8 +686,11 @@ class DownloadManager(QAbstractListModel): if fileobj is not None or filename is not None: return self.fetch_request(request, page, fileobj, filename, auto_remove, suggested_fn) - encoding = sys.getfilesystemencoding() - suggested_fn = utils.force_encoding(suggested_fn, encoding) + if suggested_fn is None: + suggested_fn = 'qutebrowser-download' + else: + encoding = sys.getfilesystemencoding() + suggested_fn = utils.force_encoding(suggested_fn, encoding) q = self._prepare_question() q.default = _path_suggestion(suggested_fn) message_bridge = objreg.get('message-bridge', scope='window', From 45dea54e3c2cacbd887ce4e0a006d92afd740e53 Mon Sep 17 00:00:00 2001 From: Tobias Patzl Date: Mon, 25 May 2015 15:23:14 +0200 Subject: [PATCH 152/182] Add setting to disable mousewheel tab switching. See #374. --- doc/help/settings.asciidoc | 12 ++++++++++++ qutebrowser/config/configdata.py | 4 ++++ qutebrowser/mainwindow/tabwidget.py | 11 +++++++++++ 3 files changed, 27 insertions(+) diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc index 46539c3b7..d3c571aa3 100644 --- a/doc/help/settings.asciidoc +++ b/doc/help/settings.asciidoc @@ -111,6 +111,7 @@ |<>|Spacing between tab edge and indicator. |<>|Whether to open windows instead of tabs. |<>|The format to use for the tab title. The following placeholders are defined: +|<>|Switch between tabs using the mouse wheel. |============== .Quick reference for section ``storage'' @@ -1031,6 +1032,17 @@ The format to use for the tab title. The following placeholders are defined: Default: +pass:[{index}: {title}]+ +[[tabs-mousewheel-tab-switching]] +=== mousewheel-tab-switching +Switch between tabs using the mouse wheel. + +Valid values: + + * +true+ + * +false+ + +Default: +pass:[true]+ + == storage Settings related to cache and storage. diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index cb3a47be7..02b8c6008 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -522,6 +522,10 @@ def data(readonly=False): "* `{index}`: The index of this tab.\n" "* `{id}`: The internal tab ID of this tab."), + ('mousewheel-tab-switching', + SettingValue(typ.Bool(), 'true'), + "Switch between tabs using the mouse wheel."), + readonly=readonly )), diff --git a/qutebrowser/mainwindow/tabwidget.py b/qutebrowser/mainwindow/tabwidget.py index df44ebbba..1ad97370e 100644 --- a/qutebrowser/mainwindow/tabwidget.py +++ b/qutebrowser/mainwindow/tabwidget.py @@ -480,6 +480,17 @@ class TabBar(QTabBar): new_idx = super().insertTab(idx, icon, '') self.set_page_title(new_idx, text) + def wheelEvent(self, event): + """Override wheelEvent to make the action configurable.""" + if config.get('tabs', 'mousewheel-tab-switching'): + super().wheelEvent(event) + else: + tabbed_browser = objreg.get('tabbed-browser', scope='window', + window=self._win_id) + focused_tab = tabbed_browser.currentWidget() + if focused_tab is not None: + focused_tab.wheelEvent(event) + class TabBarStyle(QCommonStyle): From 61519e63839b81ee1532c8021709534f6f49e9f3 Mon Sep 17 00:00:00 2001 From: Tobias Patzl Date: Mon, 25 May 2015 20:21:37 +0200 Subject: [PATCH 153/182] move part of the logic to `TabbedBrowser` --- qutebrowser/mainwindow/tabbedbrowser.py | 9 +++++++++ qutebrowser/mainwindow/tabwidget.py | 14 ++++++++------ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/qutebrowser/mainwindow/tabbedbrowser.py b/qutebrowser/mainwindow/tabbedbrowser.py index c465c8ca4..7b3477018 100644 --- a/qutebrowser/mainwindow/tabbedbrowser.py +++ b/qutebrowser/mainwindow/tabbedbrowser.py @@ -577,3 +577,12 @@ class TabbedBrowser(tabwidget.TabWidget): """ super().resizeEvent(e) self.resized.emit(self.geometry()) + + def wheelEvent(self, e): + """Override wheelEvent of QWidget to forward it to the focused tab. + + Args: + e: The QWheelEvent + """ + if self._now_focused is not None: + self._now_focused.wheelEvent(e) diff --git a/qutebrowser/mainwindow/tabwidget.py b/qutebrowser/mainwindow/tabwidget.py index 1ad97370e..bbbfdf045 100644 --- a/qutebrowser/mainwindow/tabwidget.py +++ b/qutebrowser/mainwindow/tabwidget.py @@ -480,16 +480,18 @@ class TabBar(QTabBar): new_idx = super().insertTab(idx, icon, '') self.set_page_title(new_idx, text) - def wheelEvent(self, event): - """Override wheelEvent to make the action configurable.""" + def wheelEvent(self, e): + """Override wheelEvent to make the action configurable. + + Args: + e: The QWheelEvent + """ if config.get('tabs', 'mousewheel-tab-switching'): - super().wheelEvent(event) + super().wheelEvent(e) else: tabbed_browser = objreg.get('tabbed-browser', scope='window', window=self._win_id) - focused_tab = tabbed_browser.currentWidget() - if focused_tab is not None: - focused_tab.wheelEvent(event) + tabbed_browser.wheelEvent(e) class TabBarStyle(QCommonStyle): From b858b6ac755ffc6633a24a1a85c7902ce78c1edc Mon Sep 17 00:00:00 2001 From: Tobias Patzl Date: Tue, 26 May 2015 10:24:32 +0200 Subject: [PATCH 154/182] call `e.ignore()` when the event is not handled --- qutebrowser/mainwindow/tabbedbrowser.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/qutebrowser/mainwindow/tabbedbrowser.py b/qutebrowser/mainwindow/tabbedbrowser.py index 7b3477018..ba5a5c725 100644 --- a/qutebrowser/mainwindow/tabbedbrowser.py +++ b/qutebrowser/mainwindow/tabbedbrowser.py @@ -586,3 +586,5 @@ class TabbedBrowser(tabwidget.TabWidget): """ if self._now_focused is not None: self._now_focused.wheelEvent(e) + else: + e.ignore() From 6b98c48985fd9ef4c3c6b7095b60bdd69358dd20 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 26 May 2015 10:30:21 +0200 Subject: [PATCH 155/182] Regenerate authors. --- README.asciidoc | 1 + 1 file changed, 1 insertion(+) diff --git a/README.asciidoc b/README.asciidoc index 8742f11cb..ad7a95ff2 100644 --- a/README.asciidoc +++ b/README.asciidoc @@ -150,6 +150,7 @@ Contributors, sorted by the number of commits in descending order: * Error 800 * Brian Jackson * sbinix +* Tobias Patzl * Johannes Altmanninger * Samir Benmendil * Regina Hug From e300b2e30d0a275cd88ea4d0d2f462d40c3ef88f Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 26 May 2015 12:10:36 +0200 Subject: [PATCH 156/182] Update changelog. --- CHANGELOG.asciidoc | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 0af6c0d0f..2981578e6 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -32,6 +32,7 @@ Added - New arguments `--basedir` and `--temp-basedir` (intended for debugging) to set a different base directory for all data, which allows multiple invocations. - New argument `--no-err-windows` to suppress all error windows. - New visual/caret mode (bound to `v`) to select text by keyboard. +- New setting `tabs -> mousewheel-tab-switching` to control mousewheel behavior on the tab bar. Changed ~~~~~~~ From 27e82ce6c8a4b535babc4370fd958cd1dcd1cdb0 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 25 May 2015 15:27:29 +0200 Subject: [PATCH 157/182] Improve exception handling in qsavefile_open. Sometimes exceptions were shadowed with new exceptions because of the file flushing. --- qutebrowser/utils/qtutils.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/qutebrowser/utils/qtutils.py b/qutebrowser/utils/qtutils.py index f175dc202..af2a27a74 100644 --- a/qutebrowser/utils/qtutils.py +++ b/qutebrowser/utils/qtutils.py @@ -180,7 +180,7 @@ def deserialize_stream(stream, obj): def savefile_open(filename, binary=False, encoding='utf-8'): """Context manager to easily use a QSaveFile.""" f = QSaveFile(filename) - new_f = None + cancelled = False try: ok = f.open(QIODevice.WriteOnly) if not ok: @@ -192,13 +192,15 @@ def savefile_open(filename, binary=False, encoding='utf-8'): yield new_f except: f.cancelWriting() + cancelled = True raise - finally: + else: if new_f is not None: new_f.flush() + finally: commit_ok = f.commit() - if not commit_ok: - raise OSError(f.errorString()) + if not commit_ok and not cancelled: + raise OSError("Commit failed!") @contextlib.contextmanager From 92abf4bdf877f12d334c1eee1ed9e4540f1215ea Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 26 May 2015 19:25:45 +0200 Subject: [PATCH 158/182] tox: Update pytest-html to 1.3.1. Upstream changelog: 1.3.1: Fix encoding issue in Python 3 1.3: Bump version number to 1.3 Simplify example in README Show extra content in report regardless of test result Support extra content in JSON format --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index cf1d596d6..9166f180c 100644 --- a/tox.ini +++ b/tox.ini @@ -24,7 +24,7 @@ deps = pytest-capturelog==0.7 pytest-qt==1.3.0 pytest-mock==0.5 - pytest-html==1.2 + pytest-html==1.3.1 # We don't use {[testenv:mkvenv]commands} here because that seems to be broken # on Ubuntu Trusty. commands = From e10da78a1a065385157ce6dbc5f6296a102fddc0 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 26 May 2015 08:06:21 +0200 Subject: [PATCH 159/182] urlutils: Remove some more dead code. --- qutebrowser/utils/urlutils.py | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/qutebrowser/utils/urlutils.py b/qutebrowser/utils/urlutils.py index 3ed82b0db..143e7cfc5 100644 --- a/qutebrowser/utils/urlutils.py +++ b/qutebrowser/utils/urlutils.py @@ -94,6 +94,8 @@ def _is_url_naive(urlstr): True if the URL really is a URL, False otherwise. """ url = qurl_from_user_input(urlstr) + assert url.isValid() + if not utils.raises(ValueError, ipaddress.ip_address, urlstr): # Valid IPv4/IPv6 address return True @@ -104,9 +106,7 @@ def _is_url_naive(urlstr): if not QHostAddress(urlstr).isNull(): return False - if not url.isValid(): - return False - elif '.' in url.host(): + if '.' in url.host(): return True else: return False @@ -122,9 +122,7 @@ def _is_url_dns(urlstr): True if the URL really is a URL, False otherwise. """ url = qurl_from_user_input(urlstr) - if not url.isValid(): - log.url.debug("Invalid URL -> False") - return False + assert url.isValid() if (utils.raises(ValueError, ipaddress.ip_address, urlstr) and not QHostAddress(urlstr).isNull()): @@ -246,16 +244,13 @@ def is_url(urlstr): return False if not qurl_userinput.isValid(): + # This will also catch URLs containing spaces. return False if _has_explicit_scheme(qurl): # URLs with explicit schemes are always URLs log.url.debug("Contains explicit scheme") url = True - elif ' ' in urlstr: - # A URL will never contain a space - log.url.debug("Contains space -> no URL") - url = False elif qurl_userinput.host() in ('localhost', '127.0.0.1', '::1'): log.url.debug("Is localhost.") url = True @@ -274,7 +269,7 @@ def is_url(urlstr): else: raise ValueError("Invalid autosearch value") log.url.debug("url = {}".format(url)) - return url and qurl_userinput.isValid() + return url def qurl_from_user_input(urlstr): From fa69786b0f2f3e338f2ed552c89ecf0075ced20a Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 25 May 2015 20:42:45 +0200 Subject: [PATCH 160/182] PyQIODevice: Raise ValueError when closed. --- qutebrowser/utils/qtutils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qutebrowser/utils/qtutils.py b/qutebrowser/utils/qtutils.py index af2a27a74..4525a5c32 100644 --- a/qutebrowser/utils/qtutils.py +++ b/qutebrowser/utils/qtutils.py @@ -235,9 +235,9 @@ class PyQIODevice(io.BufferedIOBase): return self._dev.size() def _check_open(self): - """Check if the device is open, raise OSError if not.""" + """Check if the device is open, raise ValueError if not.""" if not self._dev.isOpen(): - raise OSError("IO operation on closed device!") + raise ValueError("IO operation on closed device!") def _check_random(self): """Check if the device supports random access, raise OSError if not.""" From ba9c782824ddf479dd5ed76594c0defa1ae88838 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 25 May 2015 20:43:28 +0200 Subject: [PATCH 161/182] PyQIODevice: First attempt at fixing read(). This was completely broken because one read overload doesn't exist in PyQt and apparently it was never tested... --- qutebrowser/utils/qtutils.py | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/qutebrowser/utils/qtutils.py b/qutebrowser/utils/qtutils.py index 4525a5c32..841e94d87 100644 --- a/qutebrowser/utils/qtutils.py +++ b/qutebrowser/utils/qtutils.py @@ -311,13 +311,28 @@ class PyQIODevice(io.BufferedIOBase): raise OSError(self._dev.errorString()) return num - def read(self, size): + def read(self, size=-1): self._check_open() - buf = bytes() - num = self._dev.read(buf, size) - if num == -1: - raise OSError(self._dev.errorString()) - return num + self._check_readable() + if size == 0: + # Read no data + return b'' + elif size < 0: + # Read all data + if self._dev.bytesAvailable() > 0: + buf = self._dev.readAll() + if not buf: + raise OSError(self._dev.errorString()) + else: + return b'' + else: + if self._dev.bytesAvailable() > 0: + buf = self._dev.read(size) + if not buf: + raise OSError(self._dev.errorString()) + else: + return b'' + return buf class QtValueError(ValueError): From 35f0b26f4a5b3b17a70f918789f3dd7721d0b99d Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 25 May 2015 20:44:16 +0200 Subject: [PATCH 162/182] PyQIODevice: Remove readinto(). Our implementation was broken, and the BufferedIOBase mixin does a better job at doing this. --- qutebrowser/utils/qtutils.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/qutebrowser/utils/qtutils.py b/qutebrowser/utils/qtutils.py index 841e94d87..98aee36de 100644 --- a/qutebrowser/utils/qtutils.py +++ b/qutebrowser/utils/qtutils.py @@ -300,10 +300,6 @@ class PyQIODevice(io.BufferedIOBase): def writable(self): return self._dev.isWritable() - def readinto(self, b): - self._check_open() - return self._dev.read(b, len(b)) - def write(self, b): self._check_open() num = self._dev.write(b) From b2d763f993d8b164b1ae0e72044a0c8073608492 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 25 May 2015 20:52:51 +0200 Subject: [PATCH 163/182] PyQIODevice: Check if device is readable/writable. --- qutebrowser/utils/qtutils.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/qutebrowser/utils/qtutils.py b/qutebrowser/utils/qtutils.py index 98aee36de..f4b737162 100644 --- a/qutebrowser/utils/qtutils.py +++ b/qutebrowser/utils/qtutils.py @@ -244,6 +244,16 @@ class PyQIODevice(io.BufferedIOBase): if not self.seekable(): raise OSError("Random access not allowed!") + def _check_readable(self): + """Check if the device is readable, raise OSError if not.""" + if not self._dev.isReadable(): + raise OSError("Trying to read unreadable file!") + + def _check_writable(self): + """Check if the device is writable, raise OSError if not.""" + if not self.writable(): + raise OSError("Trying to write to unwritable file!") + def fileno(self): raise io.UnsupportedOperation @@ -285,6 +295,7 @@ class PyQIODevice(io.BufferedIOBase): def readline(self, size=-1): self._check_open() + self._check_readable() if size == -1: size = 0 return self._dev.readLine(size) @@ -302,6 +313,7 @@ class PyQIODevice(io.BufferedIOBase): def write(self, b): self._check_open() + self._check_writable() num = self._dev.write(b) if num == -1 or num < len(b): raise OSError(self._dev.errorString()) From 0788054dd321403195b1b8b32609a6f22204bce3 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 25 May 2015 20:53:44 +0200 Subject: [PATCH 164/182] PyQIODevice: Expose underlying device. --- qutebrowser/utils/qtutils.py | 50 ++++++++++++++++++------------------ 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/qutebrowser/utils/qtutils.py b/qutebrowser/utils/qtutils.py index f4b737162..f66dfa769 100644 --- a/qutebrowser/utils/qtutils.py +++ b/qutebrowser/utils/qtutils.py @@ -223,20 +223,20 @@ class PyQIODevice(io.BufferedIOBase): """Wrapper for a QIODevice which provides a python interface. Attributes: - _dev: The underlying QIODevice. + dev: The underlying QIODevice. """ # pylint: disable=missing-docstring def __init__(self, dev): - self._dev = dev + self.dev = dev def __len__(self): - return self._dev.size() + return self.dev.size() def _check_open(self): """Check if the device is open, raise ValueError if not.""" - if not self._dev.isOpen(): + if not self.dev.isOpen(): raise ValueError("IO operation on closed device!") def _check_random(self): @@ -246,7 +246,7 @@ class PyQIODevice(io.BufferedIOBase): def _check_readable(self): """Check if the device is readable, raise OSError if not.""" - if not self._dev.isReadable(): + if not self.dev.isReadable(): raise OSError("Trying to read unreadable file!") def _check_writable(self): @@ -261,62 +261,62 @@ class PyQIODevice(io.BufferedIOBase): self._check_open() self._check_random() if whence == io.SEEK_SET: - ok = self._dev.seek(offset) + ok = self.dev.seek(offset) elif whence == io.SEEK_CUR: - ok = self._dev.seek(self.tell() + offset) + ok = self.dev.seek(self.tell() + offset) elif whence == io.SEEK_END: - ok = self._dev.seek(len(self) + offset) + ok = self.dev.seek(len(self) + offset) else: raise io.UnsupportedOperation("whence = {} is not " "supported!".format(whence)) if not ok: - raise OSError(self._dev.errorString()) + raise OSError(self.dev.errorString()) def truncate(self, size=None): # pylint: disable=unused-argument raise io.UnsupportedOperation def close(self): - self._dev.close() + self.dev.close() @property def closed(self): - return not self._dev.isOpen() + return not self.dev.isOpen() def flush(self): self._check_open() - self._dev.waitForBytesWritten(-1) + self.dev.waitForBytesWritten(-1) def isatty(self): self._check_open() return False def readable(self): - return self._dev.isReadable() + return self.dev.isReadable() def readline(self, size=-1): self._check_open() self._check_readable() if size == -1: size = 0 - return self._dev.readLine(size) + return self.dev.readLine(size) def seekable(self): - return not self._dev.isSequential() + return not self.dev.isSequential() def tell(self): self._check_open() self._check_random() - return self._dev.pos() + return self.dev.pos() def writable(self): - return self._dev.isWritable() + return self.dev.isWritable() def write(self, b): self._check_open() self._check_writable() - num = self._dev.write(b) + num = self.dev.write(b) if num == -1 or num < len(b): - raise OSError(self._dev.errorString()) + raise OSError(self.dev.errorString()) return num def read(self, size=-1): @@ -327,17 +327,17 @@ class PyQIODevice(io.BufferedIOBase): return b'' elif size < 0: # Read all data - if self._dev.bytesAvailable() > 0: - buf = self._dev.readAll() + if self.dev.bytesAvailable() > 0: + buf = self.dev.readAll() if not buf: - raise OSError(self._dev.errorString()) + raise OSError(self.dev.errorString()) else: return b'' else: - if self._dev.bytesAvailable() > 0: - buf = self._dev.read(size) + if self.dev.bytesAvailable() > 0: + buf = self.dev.read(size) if not buf: - raise OSError(self._dev.errorString()) + raise OSError(self.dev.errorString()) else: return b'' return buf From 48de8b145bcb3bc17cf729c9857de09dbb7e94fb Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 26 May 2015 07:53:19 +0200 Subject: [PATCH 165/182] PyQIODevice: Properly fix read/readLine. --- qutebrowser/utils/qtutils.py | 44 ++++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/qutebrowser/utils/qtutils.py b/qutebrowser/utils/qtutils.py index f66dfa769..7a7050643 100644 --- a/qutebrowser/utils/qtutils.py +++ b/qutebrowser/utils/qtutils.py @@ -296,9 +296,25 @@ class PyQIODevice(io.BufferedIOBase): def readline(self, size=-1): self._check_open() self._check_readable() - if size == -1: - size = 0 - return self.dev.readLine(size) + + if size < 0: + qt_size = 0 # no maximum size + elif size == 0: + return QByteArray() + else: + qt_size = size + 1 # Qt also counts the NUL byte + + if self.dev.canReadLine(): + buf = self.dev.readLine(qt_size) + else: + if size < 0: + buf = self.dev.readAll() + else: + buf = self.dev.read(size) + + if buf is None: + raise OSError(self.dev.errorString()) + return buf def seekable(self): return not self.dev.isSequential() @@ -322,24 +338,12 @@ class PyQIODevice(io.BufferedIOBase): def read(self, size=-1): self._check_open() self._check_readable() - if size == 0: - # Read no data - return b'' - elif size < 0: - # Read all data - if self.dev.bytesAvailable() > 0: - buf = self.dev.readAll() - if not buf: - raise OSError(self.dev.errorString()) - else: - return b'' + if size < 0: + buf = self.dev.readAll() else: - if self.dev.bytesAvailable() > 0: - buf = self.dev.read(size) - if not buf: - raise OSError(self.dev.errorString()) - else: - return b'' + buf = self.dev.read(size) + if buf is None: + raise OSError(self.dev.errorString()) return buf From 6a26bc23abbfe81bcb68026a6c10d140cbd98d72 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 26 May 2015 07:52:41 +0200 Subject: [PATCH 166/182] PyQIODevice: Remove unneeded check. --- qutebrowser/utils/qtutils.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/qutebrowser/utils/qtutils.py b/qutebrowser/utils/qtutils.py index 7a7050643..e38025022 100644 --- a/qutebrowser/utils/qtutils.py +++ b/qutebrowser/utils/qtutils.py @@ -195,8 +195,7 @@ def savefile_open(filename, binary=False, encoding='utf-8'): cancelled = True raise else: - if new_f is not None: - new_f.flush() + new_f.flush() finally: commit_ok = f.commit() if not commit_ok and not cancelled: From 460308f388fe1d65b5df0964d7491a5981cfd1da Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 26 May 2015 07:52:59 +0200 Subject: [PATCH 167/182] PyQIODevice: Don't use errorString for failed seek. --- qutebrowser/utils/qtutils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/utils/qtutils.py b/qutebrowser/utils/qtutils.py index e38025022..58772e46a 100644 --- a/qutebrowser/utils/qtutils.py +++ b/qutebrowser/utils/qtutils.py @@ -269,7 +269,7 @@ class PyQIODevice(io.BufferedIOBase): raise io.UnsupportedOperation("whence = {} is not " "supported!".format(whence)) if not ok: - raise OSError(self.dev.errorString()) + raise OSError("seek failed!") def truncate(self, size=None): # pylint: disable=unused-argument raise io.UnsupportedOperation From b8dd71a343205f7ee0e1ac82a4e538ef3f2aac00 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 26 May 2015 11:22:54 +0200 Subject: [PATCH 168/182] PyQIODevice: Add .open()/.close(). --- qutebrowser/utils/qtutils.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/qutebrowser/utils/qtutils.py b/qutebrowser/utils/qtutils.py index 58772e46a..dc88b8c22 100644 --- a/qutebrowser/utils/qtutils.py +++ b/qutebrowser/utils/qtutils.py @@ -253,6 +253,22 @@ class PyQIODevice(io.BufferedIOBase): if not self.writable(): raise OSError("Trying to write to unwritable file!") + def open(self, mode): + """Open the underlying device and ensure opening succeeded. + + Raises OSError if opening failed. + + Args: + mode: QIODevice::OpenMode flags. + """ + ok = self.dev.open(mode) + if not ok: + raise OSError(self.dev.errorString()) + + def close(self): + """Close the underlying device.""" + self.dev.close() + def fileno(self): raise io.UnsupportedOperation @@ -274,9 +290,6 @@ class PyQIODevice(io.BufferedIOBase): def truncate(self, size=None): # pylint: disable=unused-argument raise io.UnsupportedOperation - def close(self): - self.dev.close() - @property def closed(self): return not self.dev.isOpen() From 6452c8f883c2531aea8fbb39e33228a58569ab76 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 26 May 2015 11:28:03 +0200 Subject: [PATCH 169/182] PyQIODevice: Add context manager support. --- qutebrowser/utils/qtutils.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/qutebrowser/utils/qtutils.py b/qutebrowser/utils/qtutils.py index dc88b8c22..6573306ab 100644 --- a/qutebrowser/utils/qtutils.py +++ b/qutebrowser/utils/qtutils.py @@ -260,10 +260,15 @@ class PyQIODevice(io.BufferedIOBase): Args: mode: QIODevice::OpenMode flags. + + Return: + A contextlib.closing() object so this can be used as + contextmanager. """ ok = self.dev.open(mode) if not ok: raise OSError(self.dev.errorString()) + return contextlib.closing(self) def close(self): """Close the underlying device.""" From a969fe021d803a25b4ba788bfcc814af059d0b8d Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 27 May 2015 07:45:09 +0200 Subject: [PATCH 170/182] tox: Install requirements.txt for tests. --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 9166f180c..7c53068e7 100644 --- a/tox.ini +++ b/tox.ini @@ -19,6 +19,7 @@ usedevelop = true setenv = QT_QPA_PLATFORM_PLUGIN_PATH={envdir}/Lib/site-packages/PyQt5/plugins/platforms passenv = DISPLAY XAUTHORITY HOME deps = + -r{toxinidir}/requirements.txt py==1.4.27 pytest==2.7.1 pytest-capturelog==0.7 From 6f3fa9dca65b2fded0640868edcc64962af44cf5 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 27 May 2015 07:49:27 +0200 Subject: [PATCH 171/182] tox: Show more information when testing. --- tox.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index 7c53068e7..467ba8dfd 100644 --- a/tox.ini +++ b/tox.ini @@ -30,7 +30,7 @@ deps = # on Ubuntu Trusty. commands = {envpython} scripts/link_pyqt.py --tox {envdir} - {envpython} -m py.test --strict {posargs} + {envpython} -m py.test --strict -rfEsw {posargs} [testenv:coverage] passenv = DISPLAY XAUTHORITY HOME @@ -41,7 +41,7 @@ deps = cov-core==1.15.0 commands = {[testenv:mkvenv]commands} - {envpython} -m py.test --strict --cov qutebrowser --cov-report term --cov-report html {posargs} + {envpython} -m py.test --strict -rfEswx -v --cov qutebrowser --cov-report term --cov-report html {posargs} [testenv:misc] commands = From ddf86600d10a802b90e450e03471acfb4233b4ca Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 27 May 2015 07:50:56 +0200 Subject: [PATCH 172/182] tests: Rename Testable* classes. This hides some pytest warnings as it tried to collect those classes. --- tests/misc/test_lineparser.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/misc/test_lineparser.py b/tests/misc/test_lineparser.py index 613ef3d65..17ae8415b 100644 --- a/tests/misc/test_lineparser.py +++ b/tests/misc/test_lineparser.py @@ -72,7 +72,7 @@ class LineParserWrapper: return True -class TestableAppendLineParser(LineParserWrapper, +class AppendLineParserTestable(LineParserWrapper, lineparsermod.AppendLineParser): """Wrapper over AppendLineParser to make it testable.""" @@ -80,14 +80,14 @@ class TestableAppendLineParser(LineParserWrapper, pass -class TestableLineParser(LineParserWrapper, lineparsermod.LineParser): +class LineParserTestable(LineParserWrapper, lineparsermod.LineParser): """Wrapper over LineParser to make it testable.""" pass -class TestableLimitLineParser(LineParserWrapper, +class LimitLineParserTestable(LineParserWrapper, lineparsermod.LimitLineParser): """Wrapper over LimitLineParser to make it testable.""" @@ -137,7 +137,7 @@ class TestAppendLineParser: @pytest.fixture def lineparser(self): """Fixture to get an AppendLineParser for tests.""" - lp = TestableAppendLineParser('this really', 'does not matter') + lp = AppendLineParserTestable('this really', 'does not matter') lp.new_data = self.BASE_DATA lp.save() return lp @@ -178,7 +178,7 @@ class TestAppendLineParser: def test_get_recent_none(self): """Test get_recent with no data.""" - linep = TestableAppendLineParser('this really', 'does not matter') + linep = AppendLineParserTestable('this really', 'does not matter') assert linep.get_recent() == [] def test_get_recent_little(self, lineparser): From 1b48dc8749eb2135ed269e881fc610acb790da52 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 27 May 2015 07:54:25 +0200 Subject: [PATCH 173/182] tox: Also provide sipconfig in link_pyqt.py. --- scripts/link_pyqt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/link_pyqt.py b/scripts/link_pyqt.py index ee1ebf2de..b6621126d 100644 --- a/scripts/link_pyqt.py +++ b/scripts/link_pyqt.py @@ -70,7 +70,7 @@ def link_pyqt(sys_path, venv_path): if not globbed_sip: raise Error("Did not find sip in {}!".format(sys_path)) - files = ['PyQt5'] + files = ['PyQt5', 'sipconfig.py'] files += [os.path.basename(e) for e in globbed_sip] for fn in files: source = os.path.join(sys_path, fn) From 2a269e9cd9b2865b3448f7a57a62c078f6cb22d8 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 27 May 2015 08:10:02 +0200 Subject: [PATCH 174/182] tox: Make sipconfig.py optional in link_pyqt.py. For some reason sipconfig.py doesn't exist at all on Windows... --- scripts/link_pyqt.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/scripts/link_pyqt.py b/scripts/link_pyqt.py index b6621126d..7c8a77b58 100644 --- a/scripts/link_pyqt.py +++ b/scripts/link_pyqt.py @@ -70,13 +70,16 @@ def link_pyqt(sys_path, venv_path): if not globbed_sip: raise Error("Did not find sip in {}!".format(sys_path)) - files = ['PyQt5', 'sipconfig.py'] - files += [os.path.basename(e) for e in globbed_sip] - for fn in files: + files = [('PyQt5', True), ('sipconfig.py', False)] + files += [(os.path.basename(e), True) for e in globbed_sip] + for fn, required in files: source = os.path.join(sys_path, fn) dest = os.path.join(venv_path, fn) if not os.path.exists(source): - raise FileNotFoundError(source) + if required: + raise FileNotFoundError(source) + else: + continue if os.path.exists(dest): if os.path.isdir(dest) and not os.path.islink(dest): shutil.rmtree(dest) From 091353a7735eea5b118296612c820f5667c57707 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 27 May 2015 08:30:26 +0200 Subject: [PATCH 175/182] Mention :adblock-update in quickstart. --- doc/quickstart.asciidoc | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/quickstart.asciidoc b/doc/quickstart.asciidoc index ac3be3a9c..e0b22d378 100644 --- a/doc/quickstart.asciidoc +++ b/doc/quickstart.asciidoc @@ -11,6 +11,7 @@ What to do now * View the http://qutebrowser.org/img/cheatsheet-big.png[key binding cheatsheet] to make yourself familiar with the key bindings: + image:http://qutebrowser.org/img/cheatsheet-small.png["qutebrowser key binding cheatsheet",link="http://qutebrowser.org/img/cheatsheet-big.png"] +* Run `:adblock-update` to download adblock lists and activate adblocking. * If you just cloned the repository, you'll need to run `scripts/asciidoc2html.py` to generate the documentation. * Go to the link:qute://settings[settings page] to set up qutebrowser the way you want it. From 534dbfc4c2634d2f43a379a8f5abb8fb9af112a0 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 27 May 2015 08:51:24 +0200 Subject: [PATCH 176/182] tox: Update check-manifest to 0.25. Upstream changelog: Stop dynamic computation of install_requires in setup.py: this doesn't work well in the presence of the pip 7 wheel cache. Use PEP-426 environment markers instead (this means we now require setuptools version 0.7 or newer). --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 467ba8dfd..f39af4ee3 100644 --- a/tox.ini +++ b/tox.ini @@ -97,7 +97,7 @@ commands = [testenv:check-manifest] skip_install = true deps = - check-manifest==0.24 + check-manifest==0.25 commands = {[testenv:mkvenv]commands} {envdir}/bin/check-manifest --ignore 'qutebrowser/git-commit-id,qutebrowser/html/doc,qutebrowser/html/doc/*,*/__pycache__' From c76221c14e15e5339e06e793d98c964a494dbf72 Mon Sep 17 00:00:00 2001 From: Martin Tournoij Date: Fri, 29 May 2015 02:07:20 +0200 Subject: [PATCH 177/182] Use a specific 'qutebrowser_editor_' prefix for instead of 'tmp'. Why does this matter? In my vimrc I have this: " When using dwb ; assume markdown, and don't store in viminfo since these are " temporary files autocmd BufRead,BufNewFile /home/martin/.cache/dwb/edit* setlocal ft=markdown viminfo= I would like to do the same with qutebrowser, but this is not possible with a file name like '/tmp/tmpSJsgSG4' --- qutebrowser/misc/editor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qutebrowser/misc/editor.py b/qutebrowser/misc/editor.py index 81323bf5e..fc27b922c 100644 --- a/qutebrowser/misc/editor.py +++ b/qutebrowser/misc/editor.py @@ -122,7 +122,8 @@ class ExternalEditor(QObject): raise ValueError("Already editing a file!") self._text = text try: - self._oshandle, self._filename = tempfile.mkstemp(text=True) + self._oshandle, self._filename = tempfile.mkstemp(text=True, + prefix='qutebrowser_editor_') if text: encoding = config.get('general', 'editor-encoding') with open(self._filename, 'w', encoding=encoding) as f: From d20872d576482d15d632e06c79696b68c45c46fe Mon Sep 17 00:00:00 2001 From: Martin Tournoij Date: Fri, 29 May 2015 14:49:52 +0200 Subject: [PATCH 178/182] Fix feedback from #690 --- misc/userscripts/qutebrowser_viewsource | 2 +- qutebrowser/app.py | 2 +- qutebrowser/misc/editor.py | 4 ++-- qutebrowser/utils/urlutils.py | 2 ++ 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/misc/userscripts/qutebrowser_viewsource b/misc/userscripts/qutebrowser_viewsource index 0985297d7..a1e40c67d 100755 --- a/misc/userscripts/qutebrowser_viewsource +++ b/misc/userscripts/qutebrowser_viewsource @@ -27,6 +27,6 @@ path=/tmp/qutebrowser_$(mktemp XXXXXXXX).html curl "$QUTE_URL" > $path -urxvt -e vim "$path" +xterm -e vim "$path" rm "$path" diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 64c277c9f..9b0ab3ba0 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -70,7 +70,7 @@ def run(args): sys.exit(usertypes.Exit.ok) if args.temp_basedir: - args.basedir = tempfile.mkdtemp() + args.basedir = tempfile.mkdtemp(prefix='qutebrowser-prefix-') quitter = Quitter(args) objreg.register('quitter', quitter) diff --git a/qutebrowser/misc/editor.py b/qutebrowser/misc/editor.py index fc27b922c..32e4100ca 100644 --- a/qutebrowser/misc/editor.py +++ b/qutebrowser/misc/editor.py @@ -122,8 +122,8 @@ class ExternalEditor(QObject): raise ValueError("Already editing a file!") self._text = text try: - self._oshandle, self._filename = tempfile.mkstemp(text=True, - prefix='qutebrowser_editor_') + self._oshandle, self._filename = tempfile.mkstemp( + text=True, prefix='qutebrowser-editor-') if text: encoding = config.get('general', 'editor-encoding') with open(self._filename, 'w', encoding=encoding) as f: diff --git a/qutebrowser/utils/urlutils.py b/qutebrowser/utils/urlutils.py index 143e7cfc5..22b84cf2b 100644 --- a/qutebrowser/utils/urlutils.py +++ b/qutebrowser/utils/urlutils.py @@ -166,6 +166,7 @@ def fuzzy_url(urlstr, cwd=None, relative=False, do_search=True): path = None stripped = urlstr.strip() + if path is not None and os.path.exists(path): log.url.debug("URL is a local file") url = QUrl.fromLocalFile(path) @@ -181,6 +182,7 @@ def fuzzy_url(urlstr, cwd=None, relative=False, do_search=True): url = qurl_from_user_input(stripped) log.url.debug("Converting fuzzy term {} to URL -> {}".format( urlstr, url.toDisplayString())) + if do_search and config.get('general', 'auto-search'): qtutils.ensure_valid(url) else: From 48735315f861f3051ee916baf48843625d0d600e Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 29 May 2015 16:52:54 +0200 Subject: [PATCH 179/182] docs: Fix typo. --- CONTRIBUTING.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.asciidoc b/CONTRIBUTING.asciidoc index cf185c3fe..7b0fe6087 100644 --- a/CONTRIBUTING.asciidoc +++ b/CONTRIBUTING.asciidoc @@ -86,7 +86,7 @@ Useful utilities Checkers ~~~~~~~~ -qutbebrowser uses http://tox.readthedocs.org/en/latest/[tox] to run its +qutebrowser uses http://tox.readthedocs.org/en/latest/[tox] to run its unittests and several linters/checkers. Currently, the following tools will be invoked when you run `tox`: From f7b517f3aadfa56eb9de78b7db1919a0a8676b42 Mon Sep 17 00:00:00 2001 From: Martin Tournoij Date: Fri, 29 May 2015 17:08:01 +0200 Subject: [PATCH 180/182] Revert some accidental changes >_< --- misc/userscripts/qutebrowser_viewsource | 2 +- qutebrowser/utils/urlutils.py | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/misc/userscripts/qutebrowser_viewsource b/misc/userscripts/qutebrowser_viewsource index a1e40c67d..0985297d7 100755 --- a/misc/userscripts/qutebrowser_viewsource +++ b/misc/userscripts/qutebrowser_viewsource @@ -27,6 +27,6 @@ path=/tmp/qutebrowser_$(mktemp XXXXXXXX).html curl "$QUTE_URL" > $path -xterm -e vim "$path" +urxvt -e vim "$path" rm "$path" diff --git a/qutebrowser/utils/urlutils.py b/qutebrowser/utils/urlutils.py index 22b84cf2b..143e7cfc5 100644 --- a/qutebrowser/utils/urlutils.py +++ b/qutebrowser/utils/urlutils.py @@ -166,7 +166,6 @@ def fuzzy_url(urlstr, cwd=None, relative=False, do_search=True): path = None stripped = urlstr.strip() - if path is not None and os.path.exists(path): log.url.debug("URL is a local file") url = QUrl.fromLocalFile(path) @@ -182,7 +181,6 @@ def fuzzy_url(urlstr, cwd=None, relative=False, do_search=True): url = qurl_from_user_input(stripped) log.url.debug("Converting fuzzy term {} to URL -> {}".format( urlstr, url.toDisplayString())) - if do_search and config.get('general', 'auto-search'): qtutils.ensure_valid(url) else: From c1dadeff6fb8f1d3f68db2e906acf0152be750af Mon Sep 17 00:00:00 2001 From: Martin Tournoij Date: Fri, 29 May 2015 20:48:43 +0200 Subject: [PATCH 181/182] Fix silly mistake... --- qutebrowser/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 9b0ab3ba0..d125d1d1a 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -70,7 +70,7 @@ def run(args): sys.exit(usertypes.Exit.ok) if args.temp_basedir: - args.basedir = tempfile.mkdtemp(prefix='qutebrowser-prefix-') + args.basedir = tempfile.mkdtemp(prefix='qutebrowser-basedir-') quitter = Quitter(args) objreg.register('quitter', quitter) From 0d19d1bcf781b85e9003691fcab642a9e6da4140 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 29 May 2015 23:47:01 +0200 Subject: [PATCH 182/182] Regenerate authors --- README.asciidoc | 1 + 1 file changed, 1 insertion(+) diff --git a/README.asciidoc b/README.asciidoc index ad7a95ff2..aa94eef98 100644 --- a/README.asciidoc +++ b/README.asciidoc @@ -147,6 +147,7 @@ Contributors, sorted by the number of commits in descending order: * rikn00 * Patric Schmitz * Martin Zimmermann +* Martin Tournoij * Error 800 * Brian Jackson * sbinix