summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Bruhin <me@the-compiler.org>2020-04-20 17:12:53 +0200
committerFlorian Bruhin <me@the-compiler.org>2020-04-20 17:12:53 +0200
commit2c85d66a55b7acde246d838fde584a2a066d47d5 (patch)
tree9efd3c64a39b3669b8a1915bc324b64c6614d3fb
parentd60078a492ba8e2d1cc1f08e97d444f93acb798e (diff)
parent2480c2c3c666aa185525b009b7a95731037db574 (diff)
downloadqutebrowser-2c85d66a55b7acde246d838fde584a2a066d47d5.tar.gz
qutebrowser-2c85d66a55b7acde246d838fde584a2a066d47d5.zip
Merge remote-tracking branch 'origin/pr/5352'
-rw-r--r--qutebrowser/browser/browsertab.py2
-rw-r--r--qutebrowser/browser/commands.py1
-rw-r--r--qutebrowser/browser/webengine/webenginetab.py94
-rw-r--r--qutebrowser/browser/webkit/webkittab.py6
-rw-r--r--qutebrowser/config/configdata.yml10
-rw-r--r--tests/end2end/features/search.feature50
6 files changed, 158 insertions, 5 deletions
diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py
index 5a0f8cc27..7e8ec478f 100644
--- a/qutebrowser/browser/browsertab.py
+++ b/qutebrowser/browser/browsertab.py
@@ -317,6 +317,7 @@ class AbstractSearch(QObject):
def search(self, text: str, *,
ignore_case: usertypes.IgnoreCase = usertypes.IgnoreCase.never,
reverse: bool = False,
+ wrap: bool = True,
result_cb: _Callback = None) -> None:
"""Find the given text on the page.
@@ -324,6 +325,7 @@ class AbstractSearch(QObject):
text: The text to search for.
ignore_case: Search case-insensitively.
reverse: Reverse search direction.
+ wrap: Allow wrapping at the top or bottom of the page.
result_cb: Called with a bool indicating whether a match was found.
"""
raise NotImplementedError
diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py
index 2ceae0551..8f7717ea7 100644
--- a/qutebrowser/browser/commands.py
+++ b/qutebrowser/browser/commands.py
@@ -1507,6 +1507,7 @@ class CommandDispatcher:
options = {
'ignore_case': config.val.search.ignore_case,
'reverse': reverse,
+ 'wrap': config.val.search.wrap,
}
self._tabbed_browser.search_text = text
diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py
index c130d92ea..34ba7a1ad 100644
--- a/qutebrowser/browser/webengine/webenginetab.py
+++ b/qutebrowser/browser/webengine/webenginetab.py
@@ -156,6 +156,78 @@ class WebEnginePrinting(browsertab.AbstractPrinting):
self._widget.page().print(printer, callback)
+class _WebEngineSearchWrapHandler:
+
+ """QtWebEngine implementations related to wrapping when searching.
+
+ Attributes:
+ flag_wrap: An additional flag indicating whether the last search
+ used wrapping.
+ _active_match: The 1-based index of the currently active match
+ on the page.
+ _total_matches: The total number of search matches on the page.
+ _nowrap_available: Whether the functionality to prevent wrapping
+ is available.
+ """
+
+ def __init__(self):
+ self._active_match = 0
+ self._total_matches = 0
+ self.flag_wrap = True
+ self._nowrap_available = False
+
+ def connect_signal(self, page):
+ """Connect to the findTextFinished signal of the page.
+
+ Args:
+ page: The QtWebEnginePage to connect to this handler.
+ """
+ if qtutils.version_check("5.14"):
+ page.findTextFinished.connect(self._store_match_data)
+ self._nowrap_available = True
+
+ def _store_match_data(self, result):
+ """Store information on the last match.
+
+ The information will be checked against when wrapping is turned off.
+
+ Args:
+ result: A FindTextResult passed by the findTextFinished signal.
+ """
+ self._active_match = result.activeMatch()
+ self._total_matches = result.numberOfMatches()
+ log.webview.debug("Active search match: {}/{}"
+ .format(self._active_match, self._total_matches))
+
+ def reset_match_data(self):
+ """Reset match information.
+
+ Stale information could lead to next_result or prev_result misbehaving.
+ """
+ self._active_match = 0
+ self._total_matches = 0
+
+ def prevent_wrapping(self, *, going_up):
+ """Prevent wrapping if possible and required.
+
+ Returns True if a wrap was prevented and False if not.
+
+ Args:
+ going_up: Whether the search would scroll the page up or down.
+ """
+ if (not self._nowrap_available or
+ self.flag_wrap or self._total_matches == 0):
+ return False
+ elif going_up and self._active_match == 1:
+ message.info("Search hit TOP")
+ return True
+ elif not going_up and self._active_match == self._total_matches:
+ message.info("Search hit BOTTOM")
+ return True
+ else:
+ return False
+
+
class WebEngineSearch(browsertab.AbstractSearch):
"""QtWebEngine implementations related to searching on the page.
@@ -170,6 +242,11 @@ class WebEngineSearch(browsertab.AbstractSearch):
super().__init__(tab, parent)
self._flags = QWebEnginePage.FindFlags(0) # type: ignore
self._pending_searches = 0
+ # The API necessary to stop wrapping was added in this version
+ self._wrap_handler = _WebEngineSearchWrapHandler()
+
+ def connect_signals(self):
+ self._wrap_handler.connect_signal(self._widget.page())
def _find(self, text, flags, callback, caller):
"""Call findText on the widget."""
@@ -207,10 +284,10 @@ class WebEngineSearch(browsertab.AbstractSearch):
callback(found)
self.finished.emit(found)
- self._widget.findText(text, flags, wrapped_callback)
+ self._widget.page().findText(text, flags, wrapped_callback)
def search(self, text, *, ignore_case=usertypes.IgnoreCase.never,
- reverse=False, result_cb=None):
+ reverse=False, wrap=True, result_cb=None):
# Don't go to next entry on duplicate search
if self.text == text and self.search_displayed:
log.webview.debug("Ignoring duplicate search request"
@@ -219,6 +296,8 @@ class WebEngineSearch(browsertab.AbstractSearch):
self.text = text
self._flags = QWebEnginePage.FindFlags(0) # type: ignore
+ self._wrap_handler.reset_match_data()
+ self._wrap_handler.flag_wrap = wrap
if self._is_case_sensitive(ignore_case):
self._flags |= QWebEnginePage.FindCaseSensitively
if reverse:
@@ -230,18 +309,26 @@ class WebEngineSearch(browsertab.AbstractSearch):
if self.search_displayed:
self.cleared.emit()
self.search_displayed = False
- self._widget.findText('')
+ self._wrap_handler.reset_match_data()
+ self._widget.page().findText('')
def prev_result(self, *, result_cb=None):
# The int() here makes sure we get a copy of the flags.
flags = QWebEnginePage.FindFlags(int(self._flags)) # type: ignore
if flags & QWebEnginePage.FindBackward:
+ if self._wrap_handler.prevent_wrapping(going_up=False):
+ return
flags &= ~QWebEnginePage.FindBackward
else:
+ if self._wrap_handler.prevent_wrapping(going_up=True):
+ return
flags |= QWebEnginePage.FindBackward
self._find(self.text, flags, result_cb, 'prev_result')
def next_result(self, *, result_cb=None):
+ going_up = self._flags & QWebEnginePage.FindBackward
+ if self._wrap_handler.prevent_wrapping(going_up=going_up):
+ return
self._find(self.text, self._flags, result_cb, 'next_result')
@@ -1643,5 +1730,6 @@ class WebEngineTab(browsertab.AbstractTab):
# pylint: disable=protected-access
self.audio._connect_signals()
+ self.search.connect_signals()
self._permissions.connect_signals()
self._scripts.connect_signals()
diff --git a/qutebrowser/browser/webkit/webkittab.py b/qutebrowser/browser/webkit/webkittab.py
index 4aa0abcf5..4d412a38b 100644
--- a/qutebrowser/browser/webkit/webkittab.py
+++ b/qutebrowser/browser/webkit/webkittab.py
@@ -125,7 +125,7 @@ class WebKitSearch(browsertab.AbstractSearch):
self._widget.findText('', QWebPage.HighlightAllOccurrences)
def search(self, text, *, ignore_case=usertypes.IgnoreCase.never,
- reverse=False, result_cb=None):
+ reverse=False, wrap=True, result_cb=None):
# Don't go to next entry on duplicate search
if self.text == text and self.search_displayed:
log.webview.debug("Ignoring duplicate search request"
@@ -137,11 +137,13 @@ class WebKitSearch(browsertab.AbstractSearch):
self.text = text
self.search_displayed = True
- self._flags = QWebPage.FindWrapsAroundDocument
+ self._flags = QWebPage.FindFlags(0) # type: ignore
if self._is_case_sensitive(ignore_case):
self._flags |= QWebPage.FindCaseSensitively
if reverse:
self._flags |= QWebPage.FindBackward
+ if wrap:
+ self._flags |= QWebPage.FindWrapsAroundDocument
# We actually search *twice* - once to highlight everything, then again
# to get a mark so we can navigate.
found = self._widget.findText(text, self._flags)
diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml
index 2bb6106ad..6a8c3d840 100644
--- a/qutebrowser/config/configdata.yml
+++ b/qutebrowser/config/configdata.yml
@@ -48,6 +48,16 @@ search.incremental:
default: True
desc: Find text on a page incrementally, renewing the search for each typed character.
+search.wrap:
+ type: Bool
+ default: True
+ backend:
+ QtWebEngine: Qt 5.14
+ QtWebKit: true
+ desc: >-
+ Wrap around at the top and bottom of the page when advancing through text matches
+ using `:search-next` and `:search-prev`.
+
new_instance_open_target:
type:
name: String
diff --git a/tests/end2end/features/search.feature b/tests/end2end/features/search.feature
index 2c0aa06a5..de815a5b7 100644
--- a/tests/end2end/features/search.feature
+++ b/tests/end2end/features/search.feature
@@ -213,6 +213,56 @@ Feature: Searching on a page
# TODO: wrapping message with scrolling
# TODO: wrapping message without scrolling
+ ## wrapping prevented
+
+ @qtwebkit_skip @qt>=5.14
+ Scenario: Preventing wrapping at the top of the page with QtWebEngine
+ When I set search.ignore_case to always
+ And I set search.wrap to false
+ And I run :search --reverse foo
+ And I wait for "search found foo with flags FindBackward" in the log
+ And I run :search-next
+ And I wait for "next_result found foo with flags FindBackward" in the log
+ And I run :search-next
+ And I wait for "Search hit TOP" in the log
+ Then "foo" should be found
+
+ @qtwebkit_skip @qt>=5.14
+ Scenario: Preventing wrapping at the bottom of the page with QtWebEngine
+ When I set search.ignore_case to always
+ And I set search.wrap to false
+ And I run :search foo
+ And I wait for "search found foo" in the log
+ And I run :search-next
+ And I wait for "next_result found foo" in the log
+ And I run :search-next
+ And I wait for "Search hit BOTTOM" in the log
+ Then "Foo" should be found
+
+ @qtwebengine_skip
+ Scenario: Preventing wrapping at the top of the page with QtWebKit
+ When I set search.ignore_case to always
+ And I set search.wrap to false
+ And I run :search --reverse foo
+ And I wait for "search found foo with flags FindBackward" in the log
+ And I run :search-next
+ And I wait for "next_result found foo with flags FindBackward" in the log
+ And I run :search-next
+ And I wait for "next_result didn't find foo with flags FindBackward" in the log
+ Then the warning "Text 'foo' not found on page!" should be shown
+
+ @qtwebengine_skip
+ Scenario: Preventing wrapping at the bottom of the page with QtWebKit
+ When I set search.ignore_case to always
+ And I set search.wrap to false
+ And I run :search foo
+ And I wait for "search found foo" in the log
+ And I run :search-next
+ And I wait for "next_result found foo" in the log
+ And I run :search-next
+ And I wait for "next_result didn't find foo" in the log
+ Then the warning "Text 'foo' not found on page!" should be shown
+
## follow searched links
@skip # Too flaky
Scenario: Follow a searched link