diff options
author | Florian Bruhin <me@the-compiler.org> | 2020-04-20 17:12:53 +0200 |
---|---|---|
committer | Florian Bruhin <me@the-compiler.org> | 2020-04-20 17:12:53 +0200 |
commit | 2c85d66a55b7acde246d838fde584a2a066d47d5 (patch) | |
tree | 9efd3c64a39b3669b8a1915bc324b64c6614d3fb | |
parent | d60078a492ba8e2d1cc1f08e97d444f93acb798e (diff) | |
parent | 2480c2c3c666aa185525b009b7a95731037db574 (diff) | |
download | qutebrowser-2c85d66a55b7acde246d838fde584a2a066d47d5.tar.gz qutebrowser-2c85d66a55b7acde246d838fde584a2a066d47d5.zip |
Merge remote-tracking branch 'origin/pr/5352'
-rw-r--r-- | qutebrowser/browser/browsertab.py | 2 | ||||
-rw-r--r-- | qutebrowser/browser/commands.py | 1 | ||||
-rw-r--r-- | qutebrowser/browser/webengine/webenginetab.py | 94 | ||||
-rw-r--r-- | qutebrowser/browser/webkit/webkittab.py | 6 | ||||
-rw-r--r-- | qutebrowser/config/configdata.yml | 10 | ||||
-rw-r--r-- | tests/end2end/features/search.feature | 50 |
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 |