diff options
author | Florian Bruhin <me@the-compiler.org> | 2023-08-15 16:26:44 +0200 |
---|---|---|
committer | Florian Bruhin <me@the-compiler.org> | 2023-08-15 16:26:44 +0200 |
commit | ec7664e93f952f2233b55b3e70777060208f7eb3 (patch) | |
tree | 7c08d39072ef28d1556adde9ef2cd7e9abca1bc2 | |
parent | f7846fc7aa10efa34c611fb3bda4a562ba214520 (diff) | |
parent | 19609fdb071b77dc45d468b6db28dd66a819819d (diff) | |
download | qutebrowser-ec7664e93f952f2233b55b3e70777060208f7eb3.tar.gz qutebrowser-ec7664e93f952f2233b55b3e70777060208f7eb3.zip |
Merge branch 'qt6-kbd-focus'
-rw-r--r-- | qutebrowser/app.py | 2 | ||||
-rw-r--r-- | qutebrowser/browser/eventfilter.py | 35 | ||||
-rw-r--r-- | qutebrowser/keyinput/modeman.py | 6 | ||||
-rw-r--r-- | qutebrowser/mainwindow/tabwidget.py | 1 | ||||
-rw-r--r-- | qutebrowser/utils/qtutils.py | 32 | ||||
-rw-r--r-- | tests/end2end/data/insert_mode_settings/html/autofocus.html | 3 | ||||
-rw-r--r-- | tests/end2end/features/misc.feature | 9 | ||||
-rw-r--r-- | tests/unit/utils/test_qtutils.py | 50 |
8 files changed, 129 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..6404608b3 100644 --- a/qutebrowser/browser/eventfilter.py +++ b/qutebrowser/browser/eventfilter.py @@ -6,10 +6,12 @@ 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 +from qutebrowser.utils import log, message, usertypes, qtutils, version, utils from qutebrowser.keyinput import modeman +from qutebrowser.misc import objects class ChildEventFilter(QObject): @@ -35,17 +37,42 @@ 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: assert obj is self._widget + # 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 ( + children and + objects.backend == usertypes.Backend.QtWebEngine and + version.qtwebengine_versions().webengine >= + utils.VersionNumber(6, 4) + ): + log.misc.debug("Focusing new child") + child.setFocus() + 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/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) 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 <QObject object at 0x...>, we want to end up with: + # <QObject object at 0x..., objectName='...'> + # But if we have RichRepr() as existing repr, we want: + # <RichRepr(), objectName='...'> + 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/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 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 = "<RichRepr(), className='RichRepr'>" + assert qtutils.qobj_repr(obj) == expected |