summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Bruhin <me@the-compiler.org>2020-05-22 22:13:22 +0200
committerFlorian Bruhin <me@the-compiler.org>2020-05-22 22:13:22 +0200
commit7a7d74dba1c6dfe1f377958e2df8ce0a2b6fed81 (patch)
treec85fbdd04de8ed580c5d8b612c78477182b58f59
parent12a6590364f69d627cddcbe48cc675593827e4c4 (diff)
parent57bc2b49c6ff6c67a0a8a6967389f03a12473e9f (diff)
downloadqutebrowser-7a7d74dba1c6dfe1f377958e2df8ce0a2b6fed81.tar.gz
qutebrowser-7a7d74dba1c6dfe1f377958e2df8ce0a2b6fed81.zip
Merge branch 'caret-line'
-rw-r--r--doc/changelog.asciidoc1
-rw-r--r--doc/help/commands.asciidoc5
-rw-r--r--doc/help/settings.asciidoc1
-rw-r--r--qutebrowser/browser/browsertab.py27
-rw-r--r--qutebrowser/browser/webengine/webenginetab.py17
-rw-r--r--qutebrowser/browser/webkit/webkittab.py198
-rw-r--r--qutebrowser/components/caretcommands.py10
-rw-r--r--qutebrowser/config/configdata.yml1
-rw-r--r--qutebrowser/javascript/caret.js86
-rw-r--r--qutebrowser/mainwindow/statusbar/bar.py12
-rw-r--r--qutebrowser/mainwindow/tabbedbrowser.py2
-rw-r--r--tests/helpers/fixtures.py30
-rw-r--r--tests/helpers/stubs.py4
-rw-r--r--tests/unit/browser/test_caret.py144
14 files changed, 415 insertions, 123 deletions
diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc
index 7a97507e5..46c72e4e2 100644
--- a/doc/changelog.asciidoc
+++ b/doc/changelog.asciidoc
@@ -40,6 +40,7 @@ Added
debugging.
- New `colors.contextmenu.disabled.{fg,bg}` settings to customize colors for
disabled items in the context menu.
+- New line selection mode (`:toggle-selection --line`), bound to `Shift-V` in caret mode.
Changed
~~~~~~~
diff --git a/doc/help/commands.asciidoc b/doc/help/commands.asciidoc
index a6a5c3e87..8cd8a62c8 100644
--- a/doc/help/commands.asciidoc
+++ b/doc/help/commands.asciidoc
@@ -1896,8 +1896,13 @@ This acts like readline's yank.
[[toggle-selection]]
=== toggle-selection
+Syntax: +:toggle-selection [*--line*]+
+
Toggle caret selection mode.
+==== optional arguments
+* +*-l*+, +*--line*+: Enables line-selection.
+
== 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 8548d834a..d7b07db34 100644
--- a/doc/help/settings.asciidoc
+++ b/doc/help/settings.asciidoc
@@ -446,6 +446,7 @@ Default:
* +pass:[J]+: +pass:[scroll down]+
* +pass:[K]+: +pass:[scroll up]+
* +pass:[L]+: +pass:[scroll right]+
+* +pass:[V]+: +pass:[toggle-selection --line]+
* +pass:[Y]+: +pass:[yank selection -s]+
* +pass:[[]+: +pass:[move-to-start-of-prev-block]+
* +pass:[]]+: +pass:[move-to-start-of-next-block]+
diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py
index 0a52180e6..b42ee1dac 100644
--- a/qutebrowser/browser/browsertab.py
+++ b/qutebrowser/browser/browsertab.py
@@ -427,13 +427,24 @@ class AbstractZoom(QObject):
self._set_factor_internal(self._zoom_factor)
+class SelectionState(enum.Enum):
+
+ """Possible states of selection in caret mode.
+
+ NOTE: Names need to line up with SelectionState in caret.js!
+ """
+
+ none = 1
+ normal = 2
+ line = 3
+
+
class AbstractCaret(QObject):
"""Attribute ``caret`` of AbstractTab for caret browsing."""
#: Signal emitted when the selection was toggled.
- #: (argument - whether the selection is now active)
- selection_toggled = pyqtSignal(bool)
+ selection_toggled = pyqtSignal(SelectionState)
#: Emitted when a ``follow_selection`` action is done.
follow_selected_done = pyqtSignal()
@@ -442,7 +453,6 @@ class AbstractCaret(QObject):
parent: QWidget = None) -> None:
super().__init__(parent)
self._widget = typing.cast(QWidget, None)
- self.selection_enabled = False
self._mode_manager = mode_manager
mode_manager.entered.connect(self._on_mode_entered)
mode_manager.left.connect(self._on_mode_left)
@@ -499,7 +509,7 @@ class AbstractCaret(QObject):
def move_to_end_of_document(self) -> None:
raise NotImplementedError
- def toggle_selection(self) -> None:
+ def toggle_selection(self, line: bool = False) -> None:
raise NotImplementedError
def drop_selection(self) -> None:
@@ -825,6 +835,15 @@ class AbstractTabPrivate:
def shutdown(self) -> None:
raise NotImplementedError
+ def run_js_sync(self, code: str) -> None:
+ """Run javascript sync.
+
+ Result will be returned when running JS is complete.
+ This is only implemented for QtWebKit.
+ For QtWebEngine, always raises UnsupportedOperationError.
+ """
+ raise NotImplementedError
+
class AbstractTab(QWidget):
diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py
index 84d35211a..cd305d11a 100644
--- a/qutebrowser/browser/webengine/webenginetab.py
+++ b/qutebrowser/browser/webengine/webenginetab.py
@@ -395,7 +395,10 @@ class WebEngineCaret(browsertab.AbstractCaret):
if enabled is None:
log.webview.debug("Ignoring selection status None")
return
- self.selection_toggled.emit(enabled)
+ if enabled:
+ self.selection_toggled.emit(browsertab.SelectionState.normal)
+ else:
+ self.selection_toggled.emit(browsertab.SelectionState.none)
@pyqtSlot(usertypes.KeyMode)
def _on_mode_left(self, mode):
@@ -450,8 +453,9 @@ class WebEngineCaret(browsertab.AbstractCaret):
def move_to_end_of_document(self):
self._js_call('moveToEndOfDocument')
- def toggle_selection(self):
- self._js_call('toggleSelection', callback=self.selection_toggled.emit)
+ def toggle_selection(self, line=False):
+ self._js_call('toggleSelection', line,
+ callback=self._toggle_sel_translate)
def drop_selection(self):
self._js_call('dropSelection')
@@ -526,6 +530,10 @@ class WebEngineCaret(browsertab.AbstractCaret):
code = javascript.assemble('caret', command, *args)
self._tab.run_js_async(code, callback)
+ def _toggle_sel_translate(self, state_str):
+ state = browsertab.SelectionState[state_str]
+ self.selection_toggled.emit(state)
+
class WebEngineScroller(browsertab.AbstractScroller):
@@ -1261,6 +1269,9 @@ class WebEngineTabPrivate(browsertab.AbstractTabPrivate):
self._tab.action.exit_fullscreen()
self._widget.shutdown()
+ def run_js_sync(self, code):
+ raise browsertab.UnsupportedOperationError
+
class WebEngineTab(browsertab.AbstractTab):
diff --git a/qutebrowser/browser/webkit/webkittab.py b/qutebrowser/browser/webkit/webkittab.py
index 213c7f277..db97af153 100644
--- a/qutebrowser/browser/webkit/webkittab.py
+++ b/qutebrowser/browser/webkit/webkittab.py
@@ -25,6 +25,7 @@ import xml.etree.ElementTree
from PyQt5.QtCore import pyqtSlot, Qt, QUrl, QPoint, QTimer, QSizeF, QSize
from PyQt5.QtGui import QIcon
+from PyQt5.QtWidgets import QWidget
from PyQt5.QtWebKitWidgets import QWebPage, QWebFrame
from PyQt5.QtWebKit import QWebSettings
from PyQt5.QtWidgets import QWidget
@@ -36,6 +37,7 @@ from qutebrowser.browser.webkit import (webview, tabhistory, webkitelem,
from qutebrowser.utils import qtutils, usertypes, utils, log, debug
from qutebrowser.keyinput import modeman
from qutebrowser.qt import sip
+from qutebrowser.keyinput import modeman
class WebKitAction(browsertab.AbstractAction):
@@ -184,14 +186,18 @@ class WebKitCaret(browsertab.AbstractCaret):
parent: QWidget = None) -> None:
super().__init__(mode_manager, parent)
self._tab = tab
+ self._selection_state = browsertab.SelectionState.none
@pyqtSlot(usertypes.KeyMode)
def _on_mode_entered(self, mode):
if mode != usertypes.KeyMode.caret:
return
- self.selection_enabled = self._widget.hasSelection()
- self.selection_toggled.emit(self.selection_enabled)
+ if self._widget.hasSelection():
+ self._selection_state = browsertab.SelectionState.normal
+ else:
+ self._selection_state = browsertab.SelectionState.none
+ self.selection_toggled.emit(self._selection_state)
settings = self._widget.settings()
settings.setAttribute(QWebSettings.CaretBrowsingEnabled, True)
@@ -206,7 +212,7 @@ class WebKitCaret(browsertab.AbstractCaret):
#
# Note: We can't use hasSelection() here, as that's always
# true in caret mode.
- if not self.selection_enabled:
+ if self._selection_state is browsertab.SelectionState.none:
self._widget.page().currentFrame().evaluateJavaScript(
utils.read_file('javascript/position_caret.js'))
@@ -214,151 +220,189 @@ class WebKitCaret(browsertab.AbstractCaret):
def _on_mode_left(self, _mode):
settings = self._widget.settings()
if settings.testAttribute(QWebSettings.CaretBrowsingEnabled):
- if self.selection_enabled and self._widget.hasSelection():
+ if (self._selection_state is not browsertab.SelectionState.none and
+ self._widget.hasSelection()):
# Remove selection if it exists
self._widget.triggerPageAction(QWebPage.MoveToNextChar)
settings.setAttribute(QWebSettings.CaretBrowsingEnabled, False)
- self.selection_enabled = False
+ self._selection_state = browsertab.SelectionState.none
def move_to_next_line(self, count=1):
- if not self.selection_enabled:
- act = QWebPage.MoveToNextLine
- else:
+ if self._selection_state is not browsertab.SelectionState.none:
act = QWebPage.SelectNextLine
+ else:
+ act = QWebPage.MoveToNextLine
for _ in range(count):
self._widget.triggerPageAction(act)
+ if self._selection_state is browsertab.SelectionState.line:
+ self._select_line_to_end()
def move_to_prev_line(self, count=1):
- if not self.selection_enabled:
- act = QWebPage.MoveToPreviousLine
- else:
+ if self._selection_state is not browsertab.SelectionState.none:
act = QWebPage.SelectPreviousLine
+ else:
+ act = QWebPage.MoveToPreviousLine
for _ in range(count):
self._widget.triggerPageAction(act)
+ if self._selection_state is browsertab.SelectionState.line:
+ self._select_line_to_start()
def move_to_next_char(self, count=1):
- if not self.selection_enabled:
- act = QWebPage.MoveToNextChar
- else:
+ if self._selection_state is browsertab.SelectionState.normal:
act = QWebPage.SelectNextChar
+ elif self._selection_state is browsertab.SelectionState.line:
+ return
+ else:
+ act = QWebPage.MoveToNextChar
for _ in range(count):
self._widget.triggerPageAction(act)
def move_to_prev_char(self, count=1):
- if not self.selection_enabled:
- act = QWebPage.MoveToPreviousChar
- else:
+ if self._selection_state is browsertab.SelectionState.normal:
act = QWebPage.SelectPreviousChar
+ elif self._selection_state is browsertab.SelectionState.line:
+ return
+ else:
+ act = QWebPage.MoveToPreviousChar
for _ in range(count):
self._widget.triggerPageAction(act)
def move_to_end_of_word(self, count=1):
- if not self.selection_enabled:
- act = [QWebPage.MoveToNextWord]
- if utils.is_windows: # pragma: no cover
- act.append(QWebPage.MoveToPreviousChar)
- else:
+ if self._selection_state is browsertab.SelectionState.normal:
act = [QWebPage.SelectNextWord]
if utils.is_windows: # pragma: no cover
act.append(QWebPage.SelectPreviousChar)
+ elif self._selection_state is browsertab.SelectionState.line:
+ return
+ else:
+ act = [QWebPage.MoveToNextWord]
+ if utils.is_windows: # pragma: no cover
+ act.append(QWebPage.MoveToPreviousChar)
for _ in range(count):
for a in act:
self._widget.triggerPageAction(a)
def move_to_next_word(self, count=1):
- if not self.selection_enabled:
- act = [QWebPage.MoveToNextWord]
- if not utils.is_windows: # pragma: no branch
- act.append(QWebPage.MoveToNextChar)
- else:
+ if self._selection_state is browsertab.SelectionState.normal:
act = [QWebPage.SelectNextWord]
if not utils.is_windows: # pragma: no branch
act.append(QWebPage.SelectNextChar)
+ elif self._selection_state is browsertab.SelectionState.line:
+ return
+ else:
+ act = [QWebPage.MoveToNextWord]
+ if not utils.is_windows: # pragma: no branch
+ act.append(QWebPage.MoveToNextChar)
for _ in range(count):
for a in act:
self._widget.triggerPageAction(a)
def move_to_prev_word(self, count=1):
- if not self.selection_enabled:
- act = QWebPage.MoveToPreviousWord
- else:
+ if self._selection_state is browsertab.SelectionState.normal:
act = QWebPage.SelectPreviousWord
+ elif self._selection_state is browsertab.SelectionState.line:
+ return
+ else:
+ act = QWebPage.MoveToPreviousWord
for _ in range(count):
self._widget.triggerPageAction(act)
def move_to_start_of_line(self):
- if not self.selection_enabled:
- act = QWebPage.MoveToStartOfLine
- else:
+ if self._selection_state is browsertab.SelectionState.normal:
act = QWebPage.SelectStartOfLine
+ elif self._selection_state is browsertab.SelectionState.line:
+ return
+ else:
+ act = QWebPage.MoveToStartOfLine
self._widget.triggerPageAction(act)
def move_to_end_of_line(self):
- if not self.selection_enabled:
- act = QWebPage.MoveToEndOfLine
- else:
+ if self._selection_state is browsertab.SelectionState.normal:
act = QWebPage.SelectEndOfLine
+ elif self._selection_state is browsertab.SelectionState.line:
+ return
+ else:
+ act = QWebPage.MoveToEndOfLine
self._widget.triggerPageAction(act)
def move_to_start_of_next_block(self, count=1):
- if not self.selection_enabled:
- act = [QWebPage.MoveToNextLine,
- QWebPage.MoveToStartOfBlock]
- else:
+ if self._selection_state is not browsertab.SelectionState.none:
act = [QWebPage.SelectNextLine,
QWebPage.SelectStartOfBlock]
+ else:
+ act = [QWebPage.MoveToNextLine,
+ QWebPage.MoveToStartOfBlock]
for _ in range(count):
for a in act:
self._widget.triggerPageAction(a)
+ if self._selection_state is browsertab.SelectionState.line:
+ self._select_line_to_end()
def move_to_start_of_prev_block(self, count=1):
- if not self.selection_enabled:
- act = [QWebPage.MoveToPreviousLine,
- QWebPage.MoveToStartOfBlock]
- else:
+ if self._selection_state is not browsertab.SelectionState.none:
act = [QWebPage.SelectPreviousLine,
QWebPage.SelectStartOfBlock]
+ else:
+ act = [QWebPage.MoveToPreviousLine,
+ QWebPage.MoveToStartOfBlock]
for _ in range(count):
for a in act:
self._widget.triggerPageAction(a)
+ if self._selection_state is browsertab.SelectionState.line:
+ self._select_line_to_start()
def move_to_end_of_next_block(self, count=1):
- if not self.selection_enabled:
- act = [QWebPage.MoveToNextLine,
- QWebPage.MoveToEndOfBlock]
- else:
+ if self._selection_state is not browsertab.SelectionState.none:
act = [QWebPage.SelectNextLine,
QWebPage.SelectEndOfBlock]
+ else:
+ act = [QWebPage.MoveToNextLine,
+ QWebPage.MoveToEndOfBlock]
for _ in range(count):
for a in act:
self._widget.triggerPageAction(a)
+ if self._selection_state is browsertab.SelectionState.line:
+ self._select_line_to_end()
def move_to_end_of_prev_block(self, count=1):
- if not self.selection_enabled:
- act = [QWebPage.MoveToPreviousLine, QWebPage.MoveToEndOfBlock]
- else:
+ if self._selection_state is not browsertab.SelectionState.none:
act = [QWebPage.SelectPreviousLine, QWebPage.SelectEndOfBlock]
+ else:
+ act = [QWebPage.MoveToPreviousLine, QWebPage.MoveToEndOfBlock]
for _ in range(count):
for a in act:
self._widget.triggerPageAction(a)
+ if self._selection_state is browsertab.SelectionState.line:
+ self._select_line_to_start()
def move_to_start_of_document(self):
- if not self.selection_enabled:
- act = QWebPage.MoveToStartOfDocument
- else:
+ if self._selection_state is not browsertab.SelectionState.none:
act = QWebPage.SelectStartOfDocument
+ else:
+ act = QWebPage.MoveToStartOfDocument
self._widget.triggerPageAction(act)
+ if self._selection_state is browsertab.SelectionState.line:
+ self._select_line()
def move_to_end_of_document(self):
- if not self.selection_enabled:
- act = QWebPage.MoveToEndOfDocument
- else:
+ if self._selection_state is not browsertab.SelectionState.none:
act = QWebPage.SelectEndOfDocument
+ else:
+ act = QWebPage.MoveToEndOfDocument
self._widget.triggerPageAction(act)
- def toggle_selection(self):
- self.selection_enabled = not self.selection_enabled
- self.selection_toggled.emit(self.selection_enabled)
+ def toggle_selection(self, line=False):
+ if line:
+ self._selection_state = browsertab.SelectionState.line
+ self._select_line()
+ self.reverse_selection()
+ self._select_line()
+ self.reverse_selection()
+ elif self._selection_state is not browsertab.SelectionState.normal:
+ self._selection_state = browsertab.SelectionState.normal
+ else:
+ self._selection_state = browsertab.SelectionState.none
+ self.selection_toggled.emit(self._selection_state)
def drop_selection(self):
self._widget.triggerPageAction(QWebPage.MoveToNextChar)
@@ -375,6 +419,32 @@ class WebKitCaret(browsertab.AbstractCaret):
);
}""")
+ def _select_line(self):
+ self._widget.triggerPageAction(QWebPage.SelectStartOfLine)
+ self.reverse_selection()
+ self._widget.triggerPageAction(QWebPage.SelectEndOfLine)
+ self.reverse_selection()
+
+ def _select_line_to_end(self):
+ # direction of selection (if anchor is to the left or right
+ # of focus) has to be checked before moving selection
+ # to the end of line
+ if self._js_selection_left_to_right():
+ self._widget.triggerPageAction(QWebPage.SelectEndOfLine)
+
+ def _select_line_to_start(self):
+ if not self._js_selection_left_to_right():
+ self._widget.triggerPageAction(QWebPage.SelectStartOfLine)
+
+ def _js_selection_left_to_right(self):
+ """Return True iff the selection's direction is left to right."""
+ return self._tab.private_api.run_js_sync("""
+ var sel = window.getSelection();
+ var position = sel.anchorNode.compareDocumentPosition(sel.focusNode);
+ (!position && sel.anchorOffset < sel.focusOffset ||
+ position === Node.DOCUMENT_POSITION_FOLLOWING);
+ """)
+
def _follow_selected(self, *, tab=False):
if QWebSettings.globalSettings().testAttribute(
QWebSettings.JavascriptEnabled):
@@ -710,6 +780,11 @@ class WebKitTabPrivate(browsertab.AbstractTabPrivate):
def shutdown(self):
self._widget.shutdown()
+ def run_js_sync(self, code):
+ document_element = self._widget.page().mainFrame().documentElement()
+ result = document_element.evaluateJavaScript(code)
+ return result
+
class WebKitTab(browsertab.AbstractTab):
@@ -771,8 +846,7 @@ class WebKitTab(browsertab.AbstractTab):
def run_js_async(self, code, callback=None, *, world=None):
if world is not None and world != usertypes.JsWorld.jseval:
log.webview.warning("Ignoring world ID {}".format(world))
- document_element = self._widget.page().mainFrame().documentElement()
- result = document_element.evaluateJavaScript(code)
+ result = self.private_api.run_js_sync(code)
if callback is not None:
callback(result)
diff --git a/qutebrowser/components/caretcommands.py b/qutebrowser/components/caretcommands.py
index 173653bd9..966b193de 100644
--- a/qutebrowser/components/caretcommands.py
+++ b/qutebrowser/components/caretcommands.py
@@ -185,9 +185,13 @@ def move_to_end_of_document(tab: apitypes.Tab) -> None:
@cmdutils.register(modes=[cmdutils.KeyMode.caret])
@cmdutils.argument('tab', value=cmdutils.Value.cur_tab)
-def toggle_selection(tab: apitypes.Tab) -> None:
- """Toggle caret selection mode."""
- tab.caret.toggle_selection()
+def toggle_selection(tab: apitypes.Tab, line: bool = False) -> None:
+ """Toggle caret selection mode.
+
+ Args:
+ line: Enables line-selection.
+ """
+ tab.caret.toggle_selection(line)
@cmdutils.register(modes=[cmdutils.KeyMode.caret])
diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml
index 660bd661c..01e9fe64b 100644
--- a/qutebrowser/config/configdata.yml
+++ b/qutebrowser/config/configdata.yml
@@ -3078,6 +3078,7 @@ bindings.default:
<Escape>: leave-mode
caret:
v: toggle-selection
+ V: toggle-selection --line
<Space>: toggle-selection
<Ctrl-Space>: drop-selection
c: enter-mode normal
diff --git a/qutebrowser/javascript/caret.js b/qutebrowser/javascript/caret.js
index 55ff6a8b5..d7ba88fe6 100644
--- a/qutebrowser/javascript/caret.js
+++ b/qutebrowser/javascript/caret.js
@@ -706,6 +706,18 @@ window._qutebrowser.caret = (function() {
CaretBrowsing.isCaretVisible = false;
/**
+ * Selection modes.
+ * NOTE: Values need to line up with SelectionState in browsertab.py!
+ *
+ * @type {enum}
+ */
+ CaretBrowsing.SelectionState = {
+ "NONE": "none",
+ "NORMAL": "normal",
+ "LINE": "line",
+ };
+
+ /**
* The actual caret element, an absolute-positioned flashing line.
* @type {Element}
*/
@@ -887,7 +899,11 @@ window._qutebrowser.caret = (function() {
CaretBrowsing.injectCaretStyles();
CaretBrowsing.toggle();
CaretBrowsing.initiated = true;
- CaretBrowsing.selectionEnabled = selectionRange > 0;
+ if (selectionRange > 0) {
+ CaretBrowsing.selectionState = CaretBrowsing.SelectionState.NORMAL;
+ } else {
+ CaretBrowsing.selectionState = CaretBrowsing.SelectionState.NONE;
+ }
};
/**
@@ -1145,16 +1161,45 @@ window._qutebrowser.caret = (function() {
}
};
+ CaretBrowsing.reverseSelection = () => {
+ const sel = window.getSelection();
+ sel.setBaseAndExtent(
+ sel.extentNode, sel.extentOffset, sel.baseNode,
+ sel.baseOffset
+ );
+ };
+
+ CaretBrowsing.selectLine = function() {
+ const sel = window.getSelection();
+ sel.modify("extend", "right", "lineboundary");
+ CaretBrowsing.reverseSelection();
+ sel.modify("extend", "left", "lineboundary");
+ CaretBrowsing.reverseSelection();
+ };
+
+ CaretBrowsing.updateLineSelection = function(direction, granularity) {
+ if (granularity !== "character" && granularity !== "word") {
+ window.
+ getSelection().
+ modify("extend", direction, granularity);
+ CaretBrowsing.selectLine();
+ }
+ };
+
CaretBrowsing.move = function(direction, granularity, count = 1) {
let action = "move";
- if (CaretBrowsing.selectionEnabled) {
+ if (CaretBrowsing.selectionState !== CaretBrowsing.SelectionState.NONE) {
action = "extend";
}
for (let i = 0; i < count; i++) {
- window.
- getSelection().
- modify(action, direction, granularity);
+ if (CaretBrowsing.selectionState === CaretBrowsing.SelectionState.LINE) {
+ CaretBrowsing.updateLineSelection(direction, granularity);
+ } else {
+ window.
+ getSelection().
+ modify(action, direction, granularity);
+ }
}
if (CaretBrowsing.isWindows &&
@@ -1174,7 +1219,7 @@ window._qutebrowser.caret = (function() {
CaretBrowsing.moveToBlock = function(paragraph, boundary, count = 1) {
let action = "move";
- if (CaretBrowsing.selectionEnabled) {
+ if (CaretBrowsing.selectionState !== CaretBrowsing.SelectionState.NONE) {
action = "extend";
}
for (let i = 0; i < count; i++) {
@@ -1185,6 +1230,10 @@ window._qutebrowser.caret = (function() {
window.
getSelection().
modify(action, boundary, "paragraphboundary");
+
+ if (CaretBrowsing.selectionState === CaretBrowsing.SelectionState.LINE) {
+ CaretBrowsing.selectLine();
+ }
}
};
@@ -1294,14 +1343,14 @@ window._qutebrowser.caret = (function() {
funcs.setInitialCursor = () => {
if (!CaretBrowsing.initiated) {
CaretBrowsing.setInitialCursor();
- return CaretBrowsing.selectionEnabled;
+ return CaretBrowsing.selectionState !== CaretBrowsing.SelectionState.NONE;
}
if (window.getSelection().toString().length === 0) {
positionCaret();
}
CaretBrowsing.toggle();
- return CaretBrowsing.selectionEnabled;
+ return CaretBrowsing.selectionState !== CaretBrowsing.SelectionState.NONE;
};
funcs.setFlags = (flags) => {
@@ -1399,17 +1448,22 @@ window._qutebrowser.caret = (function() {
funcs.getSelection = () => window.getSelection().toString();
- funcs.toggleSelection = () => {
- CaretBrowsing.selectionEnabled = !CaretBrowsing.selectionEnabled;
- return CaretBrowsing.selectionEnabled;
+ funcs.toggleSelection = (line) => {
+ if (line) {
+ CaretBrowsing.selectionState =
+ CaretBrowsing.SelectionState.LINE;
+ CaretBrowsing.selectLine();
+ CaretBrowsing.finishMove();
+ } else if (CaretBrowsing.selectionState !== CaretBrowsing.SelectionState.NORMAL) {
+ CaretBrowsing.selectionState = CaretBrowsing.SelectionState.NORMAL;
+ } else {
+ CaretBrowsing.selectionState = CaretBrowsing.SelectionState.NONE;
+ }
+ return CaretBrowsing.selectionState;
};
funcs.reverseSelection = () => {
- const sel = window.getSelection();
- sel.setBaseAndExtent(
- sel.extentNode, sel.extentOffset, sel.baseNode,
- sel.baseOffset
- );
+ CaretBrowsing.reverseSelection();
};
return funcs;
diff --git a/qutebrowser/mainwindow/statusbar/bar.py b/qutebrowser/mainwindow/statusbar/bar.py
index 119b16584..91bdb0b6e 100644
--- a/qutebrowser/mainwindow/statusbar/bar.py
+++ b/qutebrowser/mainwindow/statusbar/bar.py
@@ -373,13 +373,17 @@ class StatusBar(QWidget):
self.maybe_hide()
assert tab.is_private == self._color_flags.private
- @pyqtSlot(bool)
- def on_caret_selection_toggled(self, selection):
+ @pyqtSlot(browsertab.SelectionState)
+ def on_caret_selection_toggled(self, selection_state):
"""Update the statusbar when entering/leaving caret selection mode."""
- log.statusbar.debug("Setting caret selection {}".format(selection))
- if selection:
+ log.statusbar.debug("Setting caret selection {}"
+ .format(selection_state))
+ if selection_state is browsertab.SelectionState.normal:
self._set_mode_text("caret selection")
self._color_flags.caret = ColorFlags.CaretMode.selection
+ elif selection_state is browsertab.SelectionState.line:
+ self._set_mode_text("caret line selection")
+ self._color_flags.caret = ColorFlags.CaretMode.selection
else:
self._set_mode_text("caret")
self._color_flags.caret = ColorFlags.CaretMode.on
diff --git a/qutebrowser/mainwindow/tabbedbrowser.py b/qutebrowser/mainwindow/tabbedbrowser.py
index c1179abbf..25b05a036 100644
--- a/qutebrowser/mainwindow/tabbedbrowser.py
+++ b/qutebrowser/mainwindow/tabbedbrowser.py
@@ -189,7 +189,7 @@ class TabbedBrowser(QWidget):
cur_scroll_perc_changed = pyqtSignal(int, int)
cur_load_status_changed = pyqtSignal(usertypes.LoadStatus)
cur_fullscreen_requested = pyqtSignal(bool)
- cur_caret_selection_toggled = pyqtSignal(bool)
+ cur_caret_selection_toggled = pyqtSignal(browsertab.SelectionState)
close_window = pyqtSignal()
resized = pyqtSignal('QRect')
current_tab_changed = pyqtSignal(browsertab.AbstractTab)
diff --git a/tests/helpers/fixtures.py b/tests/helpers/fixtures.py
index ced27a9f8..60a4f02ba 100644
--- a/tests/helpers/fixtures.py
+++ b/tests/helpers/fixtures.py
@@ -45,11 +45,12 @@ import helpers.stubs as stubsmod
from qutebrowser.config import (config, configdata, configtypes, configexc,
configfiles, configcache, stylesheet)
from qutebrowser.api import config as configapi
-from qutebrowser.utils import objreg, standarddir, utils, usertypes
+from qutebrowser.utils import objreg, standarddir, utils, usertypes, qtutils
from qutebrowser.browser import greasemonkey, history, qutescheme
from qutebrowser.browser.webkit import cookies, cache
from qutebrowser.misc import savemanager, sql, objects, sessions
from qutebrowser.keyinput import modeman
+from qutebrowser.qt import sip
_qute_scheme_handler = None
@@ -64,14 +65,17 @@ class WidgetContainer(QWidget):
self._qtbot = qtbot
self.vbox = QVBoxLayout(self)
qtbot.add_widget(self)
+ self._widget = None
def set_widget(self, widget):
self.vbox.addWidget(widget)
widget.container = self
+ self._widget = widget
def expose(self):
with self._qtbot.waitExposed(self):
self.show()
+ self._widget.setFocus()
@pytest.fixture
@@ -204,19 +208,23 @@ def web_tab_setup(qtbot, tab_registry, session_manager_stub,
@pytest.fixture
def webkit_tab(web_tab_setup, qtbot, cookiejar_and_cache, mode_manager,
- widget_container, webpage):
+ widget_container, download_stub, webpage):
webkittab = pytest.importorskip('qutebrowser.browser.webkit.webkittab')
tab = webkittab.WebKitTab(win_id=0, mode_manager=mode_manager,
private=False)
widget_container.set_widget(tab)
- return tab
+ yield tab
+
+ # Make sure the tab shuts itself down properly
+ tab.private_api.shutdown()
@pytest.fixture
def webengine_tab(web_tab_setup, qtbot, redirect_webengine_data,
- tabbed_browser_stubs, mode_manager, widget_container):
+ tabbed_browser_stubs, mode_manager, widget_container,
+ monkeypatch):
tabwidget = tabbed_browser_stubs[0].widget
tabwidget.current_index = 0
tabwidget.index_of = 0
@@ -227,11 +235,25 @@ def webengine_tab(web_tab_setup, qtbot, redirect_webengine_data,
tab = webenginetab.WebEngineTab(win_id=0, mode_manager=mode_manager,
private=False)
widget_container.set_widget(tab)
+
yield tab
+
# If a page is still loading here, _on_load_finished could get called
# during teardown when session_manager_stub is already deleted.
tab.stop()
+ # Make sure the tab shuts itself down properly
+ tab.private_api.shutdown()
+
+ # If we wait for the GC to clean things up, there's a segfault inside
+ # QtWebEngine sometimes (e.g. if we only run
+ # tests/unit/browser/test_caret.py).
+ # However, with Qt < 5.12, doing this here will lead to an immediate
+ # segfault...
+ monkeypatch.undo() # version_check could be patched
+ if qtutils.version_check('5.12'):
+ sip.delete(tab._widget)
+
@pytest.fixture(params=['webkit', 'webengine'])
def web_tab(request):
diff --git a/tests/helpers/stubs.py b/tests/helpers/stubs.py
index b72907d93..23fe7ac1d 100644
--- a/tests/helpers/stubs.py
+++ b/tests/helpers/stubs.py
@@ -615,6 +615,10 @@ class FakeDownloadManager:
self.downloads.append(download_item)
return download_item
+ def has_downloads_with_nam(self, _nam):
+ """Needed during WebView.shutdown()."""
+ return False
+
class FakeHistoryProgress:
diff --git a/tests/unit/browser/test_caret.py b/tests/unit/browser/test_caret.py
index 9b817c4ac..7d1325612 100644
--- a/tests/unit/browser/test_caret.py
+++ b/tests/unit/browser/test_caret.py
@@ -24,15 +24,19 @@ import textwrap
import pytest
from PyQt5.QtCore import QUrl
-from qutebrowser.utils import utils, qtutils, usertypes
+from qutebrowser.utils import usertypes
+from qutebrowser.browser import browsertab
@pytest.fixture
def caret(web_tab, qtbot, mode_manager):
- with qtbot.wait_signal(web_tab.load_finished):
+ web_tab.container.expose()
+
+ with qtbot.wait_signal(web_tab.load_finished, timeout=10000):
web_tab.load_url(QUrl('qute://testdata/data/caret.html'))
- mode_manager.enter(usertypes.KeyMode.caret)
+ with qtbot.wait_signal(web_tab.caret.selection_toggled):
+ mode_manager.enter(usertypes.KeyMode.caret)
return web_tab.caret
@@ -61,15 +65,21 @@ class Selection:
selection = selection.strip()
assert selection == expected
return
+ elif not selection and not expected:
+ return
self._qtbot.wait(50)
+ assert False, 'Failed to get selection!'
+
def check_multiline(self, expected, *, strip=False):
self.check(textwrap.dedent(expected).strip(), strip=strip)
- def toggle(self):
- with self._qtbot.wait_signal(self._caret.selection_toggled):
- self._caret.toggle_selection()
+ def toggle(self, *, line=False):
+ """Toggle the selection and return the new selection state."""
+ with self._qtbot.wait_signal(self._caret.selection_toggled) as blocker:
+ self._caret.toggle_selection(line=line)
+ return blocker.args[0]
@pytest.fixture
@@ -77,6 +87,18 @@ def selection(qtbot, caret):
return Selection(qtbot, caret)
+def test_toggle(caret, selection, qtbot):
+ """Make sure calling toggleSelection produces the correct callback values.
+
+ This also makes sure that the SelectionState enum in JS lines up with the
+ Python browsertab.SelectionState enum.
+ """
+ assert selection.toggle() == browsertab.SelectionState.normal
+ assert selection.toggle(line=True) == browsertab.SelectionState.line
+ assert selection.toggle() == browsertab.SelectionState.normal
+ assert selection.toggle() == browsertab.SelectionState.none
+
+
class TestDocument:
def test_selecting_entire_document(self, caret, selection):
@@ -287,17 +309,6 @@ def test_drop_selection(caret, selection):
class TestSearch:
- @pytest.fixture(autouse=True)
- def expose(self, web_tab):
- """Expose the web view if needed.
-
- With QtWebEngine 5.13 on macOS/Windows, searching fails (callback
- called with False) when the view isn't exposed.
- """
- if qtutils.version_check('5.13') and not utils.is_linux:
- web_tab.container.expose()
- web_tab.show()
-
# https://bugreports.qt.io/browse/QTBUG-60673
@pytest.mark.qtbug60673
@@ -340,15 +351,6 @@ class TestFollowSelected:
def toggle_js(self, request, config_stub):
config_stub.val.content.javascript.enabled = request.param
- @pytest.fixture(autouse=True)
- def expose(self, web_tab):
- """Expose the web view if needed.
-
- On QtWebKit, or Qt < 5.11 and > 5.12 on QtWebEngine, we need to
- show the tab for selections to work properly.
- """
- web_tab.container.expose()
-
def test_follow_selected_without_a_selection(self, qtbot, caret, selection, web_tab,
mode_manager):
caret.move_to_next_word() # Move cursor away from the link
@@ -405,3 +407,93 @@ class TestReverse:
caret.reverse_selection()
caret.move_to_start_of_line()
selection.check("one two three")
+
+
+class TestLineSelection:
+
+ def test_toggle(self, caret, selection):
+ selection.toggle(line=True)
+ selection.check("one two three")
+
+ def test_toggle_untoggle(self, caret, selection):
+ selection.toggle()
+ selection.check("")
+ selection.toggle(line=True)
+ selection.check("one two three")
+ selection.toggle()
+ selection.check("one two three")
+
+ def test_from_center(self, caret, selection):
+ caret.move_to_next_char(4)
+ selection.toggle(line=True)
+ selection.check("one two three")
+
+ def test_more_lines(self, caret, selection):
+ selection.toggle(line=True)
+ caret.move_to_next_line(2)
+ selection.check_multiline("""
+ one two three
+ eins zwei drei
+
+ four five six
+ """, strip=True)
+
+ def test_not_selecting_char(self, caret, selection):
+ selection.toggle(line=True)
+ caret.move_to_next_char()
+ selection.check("one two three")
+ caret.move_to_prev_char()
+ selection.check("one two three")
+
+ def test_selecting_prev_next_word(self, caret, selection):
+ selection.toggle(line=True)
+ caret.move_to_next_word()
+ selection.check("one two three")
+ caret.move_to_prev_word()
+ selection.check("one two three")
+
+ def test_selecting_end_word(self, caret, selection):
+ selection.toggle(line=True)
+ caret.move_to_end_of_word()
+ selection.check("one two three")
+
+ def test_selecting_prev_next_line(self, caret, selection):
+ selection.toggle(line=True)
+ caret.move_to_next_line()
+ selection.check_multiline("""
+ one two three
+ eins zwei drei
+ """, strip=True)
+ caret.move_to_prev_line()
+ selection.check("one two three")
+
+ def test_not_selecting_start_end_line(self, caret, selection):
+ selection.toggle(line=True)
+ caret.move_to_end_of_line()
+ selection.check("one two three")
+ caret.move_to_start_of_line()
+ selection.check("one two three")
+
+ def test_selecting_block(self, caret, selection):
+ selection.toggle(line=True)
+ caret.move_to_end_of_next_block()
+ selection.check_multiline("""
+ one two three
+ eins zwei drei
+ """, strip=True)
+
+ @pytest.mark.not_mac(
+ reason='https://github.com/qutebrowser/qutebrowser/issues/5459')
+ def test_selecting_start_end_document(self, caret, selection):
+ selection.toggle(line=True)
+ caret.move_to_end_of_document()
+ selection.check_multiline("""
+ one two three
+ eins zwei drei
+
+ four five six
+ vier fünf sechs
+ """, strip=True)
+
+ caret.move_to_start_of_document()
+ selection.check("one two three")