Merge branch 'caret-line'

This commit is contained in:
Florian Bruhin 2020-05-22 22:13:22 +02:00
commit 7a7d74dba1
14 changed files with 415 additions and 123 deletions

View File

@ -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
~~~~~~~

View File

@ -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.

View File

@ -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]+

View File

@ -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):

View File

@ -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):

View File

@ -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)

View File

@ -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])

View File

@ -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

View File

@ -705,6 +705,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;

View File

@ -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

View File

@ -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)

View File

@ -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):

View File

@ -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:

View File

@ -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")