From 8e152aaa0ac40a5200658d2b283cdf11b9d7ca0d Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 15 Aug 2023 09:22:30 +0200 Subject: Don't give keyboard focus to tab bar This partially solves #7820. The web view still loses focus for an unknown reason (apparently when swtiching out the rendering process?), but at least it regains focus now when the window is unfocused and then focused again. --- qutebrowser/mainwindow/tabwidget.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qutebrowser/mainwindow/tabwidget.py b/qutebrowser/mainwindow/tabwidget.py index 2e90c46c4..c0c7ee2ad 100644 --- a/qutebrowser/mainwindow/tabwidget.py +++ b/qutebrowser/mainwindow/tabwidget.py @@ -396,6 +396,7 @@ class TabBar(QTabBar): self._win_id = win_id self._our_style = TabBarStyle() self.setStyle(self._our_style) + self.setFocusPolicy(Qt.FocusPolicy.NoFocus) self.vertical = False self._auto_hide_timer = QTimer() self._auto_hide_timer.setSingleShot(True) -- cgit v1.2.3-54-g00ecf From 0d2afd58f3d0e34af21cee7d8a3fc9d855594e9f Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 15 Aug 2023 10:48:43 +0200 Subject: Add qtutils.qobj_repr() Shows objectName() and the metaObject().className() if available. More debug info for #7820 --- qutebrowser/app.py | 2 +- qutebrowser/browser/eventfilter.py | 10 +++++--- qutebrowser/keyinput/modeman.py | 6 ++--- qutebrowser/utils/qtutils.py | 32 ++++++++++++++++++++++++ tests/unit/utils/test_qtutils.py | 50 +++++++++++++++++++++++++++++++++++++- 5 files changed, 91 insertions(+), 9 deletions(-) diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 60eedeb1b..778c248c2 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -561,7 +561,7 @@ class Application(QApplication): @pyqtSlot(QObject) def on_focus_object_changed(self, obj): """Log when the focus object changed.""" - output = repr(obj) + output = qtutils.qobj_repr(obj) if self._last_focus_object != output: log.misc.debug("Focus object changed: {}".format(output)) self._last_focus_object = output diff --git a/qutebrowser/browser/eventfilter.py b/qutebrowser/browser/eventfilter.py index 8dbfbd008..a9ddb93c2 100644 --- a/qutebrowser/browser/eventfilter.py +++ b/qutebrowser/browser/eventfilter.py @@ -8,7 +8,7 @@ from qutebrowser.qt import machinery from qutebrowser.qt.core import QObject, QEvent, Qt, QTimer from qutebrowser.config import config -from qutebrowser.utils import log, message, usertypes +from qutebrowser.utils import log, message, usertypes, qtutils from qutebrowser.keyinput import modeman @@ -35,8 +35,9 @@ class ChildEventFilter(QObject): """Act on ChildAdded events.""" if event.type() == QEvent.Type.ChildAdded: child = event.child() - log.misc.debug("{} got new child {}, installing filter" - .format(obj, child)) + log.misc.debug( + f"{qtutils.qobj_repr(obj)} got new child {qtutils.qobj_repr(child)}, " + "installing filter") # Additional sanity check, but optional if self._widget is not None: @@ -45,7 +46,8 @@ class ChildEventFilter(QObject): child.installEventFilter(self._filter) elif event.type() == QEvent.Type.ChildRemoved: child = event.child() - log.misc.debug("{}: removed child {}".format(obj, child)) + log.misc.debug( + f"{qtutils.qobj_repr(obj)}: removed child {qtutils.qobj_repr(child)}") return False diff --git a/qutebrowser/keyinput/modeman.py b/qutebrowser/keyinput/modeman.py index 582a1bf18..f0337ec88 100644 --- a/qutebrowser/keyinput/modeman.py +++ b/qutebrowser/keyinput/modeman.py @@ -16,7 +16,7 @@ from qutebrowser.commands import runners from qutebrowser.keyinput import modeparsers, basekeyparser from qutebrowser.config import config from qutebrowser.api import cmdutils -from qutebrowser.utils import usertypes, log, objreg, utils +from qutebrowser.utils import usertypes, log, objreg, utils, qtutils from qutebrowser.browser import hints from qutebrowser.misc import objects @@ -308,10 +308,10 @@ class ModeManager(QObject): focus_widget = objects.qapp.focusWidget() log.modes.debug("match: {}, forward_unbound_keys: {}, " "passthrough: {}, is_non_alnum: {}, dry_run: {} " - "--> filter: {} (focused: {!r})".format( + "--> filter: {} (focused: {})".format( match, forward_unbound_keys, parser.passthrough, is_non_alnum, dry_run, - filter_this, focus_widget)) + filter_this, qtutils.qobj_repr(focus_widget))) return filter_this def _handle_keyrelease(self, event: QKeyEvent) -> bool: diff --git a/qutebrowser/utils/qtutils.py b/qutebrowser/utils/qtutils.py index 5e7c6d272..ebcd6578f 100644 --- a/qutebrowser/utils/qtutils.py +++ b/qutebrowser/utils/qtutils.py @@ -639,6 +639,38 @@ def extract_enum_val(val: Union[sip.simplewrapper, int, enum.Enum]) -> int: return val +def qobj_repr(obj: Optional[QObject]) -> str: + """Show nicer debug information for a QObject.""" + py_repr = repr(obj) + if obj is None: + return py_repr + + try: + object_name = obj.objectName() + meta_object = obj.metaObject() + except AttributeError: + # Technically not possible if obj is a QObject, but crashing when trying to get + # some debug info isn't helpful. + return py_repr + + class_name = "" if meta_object is None else meta_object.className() + + if py_repr.startswith("<") and py_repr.endswith(">"): + # With a repr such as , we want to end up with: + # + # But if we have RichRepr() as existing repr, we want: + # + py_repr = py_repr[1:-1] + + parts = [py_repr] + if object_name: + parts.append(f"objectName={object_name!r}") + if class_name and f".{class_name} object at 0x" not in py_repr: + parts.append(f"className={class_name!r}") + + return f"<{', '.join(parts)}>" + + _T = TypeVar("_T") diff --git a/tests/unit/utils/test_qtutils.py b/tests/unit/utils/test_qtutils.py index 5b173882b..541f4e4fe 100644 --- a/tests/unit/utils/test_qtutils.py +++ b/tests/unit/utils/test_qtutils.py @@ -13,8 +13,9 @@ import unittest.mock import pytest from qutebrowser.qt.core import (QDataStream, QPoint, QUrl, QByteArray, QIODevice, - QTimer, QBuffer, QFile, QProcess, QFileDevice, QLibraryInfo, Qt) + QTimer, QBuffer, QFile, QProcess, QFileDevice, QLibraryInfo, Qt, QObject) from qutebrowser.qt.gui import QColor +from qutebrowser.qt import sip from qutebrowser.utils import qtutils, utils, usertypes import overflow_test_cases @@ -1051,3 +1052,50 @@ class TestLibraryPath: def test_extract_enum_val(): value = qtutils.extract_enum_val(Qt.KeyboardModifier.ShiftModifier) assert value == 0x02000000 + + +class TestQObjRepr: + + @pytest.mark.parametrize("obj", [QObject(), object(), None]) + def test_simple(self, obj): + assert qtutils.qobj_repr(obj) == repr(obj) + + def _py_repr(self, obj): + """Get the original repr of an object, with <> stripped off. + + We do this in code instead of recreating it in tests because of output + differences between PyQt5/PyQt6 and between operating systems. + """ + r = repr(obj) + if r.startswith("<") and r.endswith(">"): + return r[1:-1] + return r + + def test_object_name(self): + obj = QObject() + obj.setObjectName("Tux") + expected = f"<{self._py_repr(obj)}, objectName='Tux'>" + assert qtutils.qobj_repr(obj) == expected + + def test_class_name(self): + obj = QTimer() + hidden = sip.cast(obj, QObject) + expected = f"<{self._py_repr(hidden)}, className='QTimer'>" + assert qtutils.qobj_repr(hidden) == expected + + def test_both(self): + obj = QTimer() + obj.setObjectName("Pomodoro") + hidden = sip.cast(obj, QObject) + expected = f"<{self._py_repr(hidden)}, objectName='Pomodoro', className='QTimer'>" + assert qtutils.qobj_repr(hidden) == expected + + def test_rich_repr(self): + class RichRepr(QObject): + def __repr__(self): + return "RichRepr()" + + obj = RichRepr() + assert repr(obj) == "RichRepr()" # sanity check + expected = "" + assert qtutils.qobj_repr(obj) == expected -- cgit v1.2.3-54-g00ecf From f6d44b927aa97de472001e9699944d12dbee3345 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 15 Aug 2023 14:03:25 +0200 Subject: First child widget keyboard focus workaround Fixes #7820 --- qutebrowser/browser/eventfilter.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/qutebrowser/browser/eventfilter.py b/qutebrowser/browser/eventfilter.py index a9ddb93c2..b5659dba0 100644 --- a/qutebrowser/browser/eventfilter.py +++ b/qutebrowser/browser/eventfilter.py @@ -8,8 +8,9 @@ from qutebrowser.qt import machinery from qutebrowser.qt.core import QObject, QEvent, Qt, QTimer from qutebrowser.config import config -from qutebrowser.utils import log, message, usertypes, qtutils +from qutebrowser.utils import log, message, usertypes, qtutils, version, utils from qutebrowser.keyinput import modeman +from qutebrowser.misc import objects class ChildEventFilter(QObject): @@ -43,6 +44,21 @@ class ChildEventFilter(QObject): if self._widget is not None: assert obj is self._widget + # Carry on keyboard focus to the new child + # WORKAROUND for unknown Qt bug losing focus on child change + old_focus_widget = objects.qapp.focusWidget() + if old_focus_widget is not None: + metaobj = old_focus_widget.metaObject() + if ( + metaobj is not None and + metaobj.className() == "QQuickWidget" and + old_focus_widget.parent() is obj and + objects.backend == usertypes.Backend.QtWebEngine and + version.qtwebengine_versions().webengine >= utils.VersionNumber(6, 2) + ): + log.misc.debug("Focusing new child") + child.setFocus() + child.installEventFilter(self._filter) elif event.type() == QEvent.Type.ChildRemoved: child = event.child() -- cgit v1.2.3-54-g00ecf From a6e86629edcffcd30d19fa734f1ace61394169a0 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 15 Aug 2023 14:14:23 +0200 Subject: Improve child widget focus workaround Don't rely on the global QApplication.focusWidget() See #7820 --- qutebrowser/browser/eventfilter.py | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/qutebrowser/browser/eventfilter.py b/qutebrowser/browser/eventfilter.py index b5659dba0..6404608b3 100644 --- a/qutebrowser/browser/eventfilter.py +++ b/qutebrowser/browser/eventfilter.py @@ -6,6 +6,7 @@ from qutebrowser.qt import machinery from qutebrowser.qt.core import QObject, QEvent, Qt, QTimer +from qutebrowser.qt.widgets import QWidget from qutebrowser.config import config from qutebrowser.utils import log, message, usertypes, qtutils, version, utils @@ -44,17 +45,25 @@ class ChildEventFilter(QObject): if self._widget is not None: assert obj is self._widget - # Carry on keyboard focus to the new child - # WORKAROUND for unknown Qt bug losing focus on child change - old_focus_widget = objects.qapp.focusWidget() - if old_focus_widget is not None: - metaobj = old_focus_widget.metaObject() + # WORKAROUND for unknown Qt bug losing focus on child change + # Carry on keyboard focus to the new child if: + # - This is a child event filter on a tab (self._widget is not None) + # - We find an old existing child which is a QQuickWidget and is + # currently focused. + # - We're using QtWebEngine >= 6.4 (older versions are not affected) + children = [ + c for c in self._widget.findChildren( + QWidget, "", Qt.FindChildOption.FindDirectChildrenOnly) + if c is not child and + c.hasFocus() and + c.metaObject() is not None and + c.metaObject().className() == "QQuickWidget" + ] if ( - metaobj is not None and - metaobj.className() == "QQuickWidget" and - old_focus_widget.parent() is obj and + children and objects.backend == usertypes.Backend.QtWebEngine and - version.qtwebengine_versions().webengine >= utils.VersionNumber(6, 2) + version.qtwebengine_versions().webengine >= + utils.VersionNumber(6, 4) ): log.misc.debug("Focusing new child") child.setFocus() -- cgit v1.2.3-54-g00ecf From 19609fdb071b77dc45d468b6db28dd66a819819d Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 15 Aug 2023 14:28:36 +0200 Subject: Add a test for keyboard focus after cross-origin navigation Fails without the fix on main on QtWebEngine 6.4 (works on 6.2). Works fine after the fix. See #7820 --- tests/end2end/data/insert_mode_settings/html/autofocus.html | 3 +++ tests/end2end/features/misc.feature | 9 +++++++++ 2 files changed, 12 insertions(+) diff --git a/tests/end2end/data/insert_mode_settings/html/autofocus.html b/tests/end2end/data/insert_mode_settings/html/autofocus.html index 366f436f6..6ce8c6e6f 100644 --- a/tests/end2end/data/insert_mode_settings/html/autofocus.html +++ b/tests/end2end/data/insert_mode_settings/html/autofocus.html @@ -7,6 +7,9 @@ function setup_event_listener() { var elem = document.getElementById('qute-input-autofocus'); console.log(elem); + elem.addEventListener('focus', function() { + console.log("focused"); + }); elem.addEventListener('input', function() { console.log("contents: " + elem.value); }); diff --git a/tests/end2end/features/misc.feature b/tests/end2end/features/misc.feature index 3940d0243..f7013cfae 100644 --- a/tests/end2end/features/misc.feature +++ b/tests/end2end/features/misc.feature @@ -622,3 +622,12 @@ Feature: Various utility commands. When I open data/invalid_resource.html in a new tab Then "Ignoring invalid * URL: Invalid hostname (contains invalid characters); *" should be logged And no crash should happen + + @flaky @qtwebkit_skip + Scenario: Keyboard focus after cross-origin navigation + When I open qute://gpl + And I open data/insert_mode_settings/html/autofocus.html + And I run :mode-enter insert + And I wait for the javascript message "focused" + And I run :fake-key -g "testing" + Then the javascript message "contents: testing" should be logged -- cgit v1.2.3-54-g00ecf