summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Bruhin <me@the-compiler.org>2023-08-15 16:26:44 +0200
committerFlorian Bruhin <me@the-compiler.org>2023-08-15 16:26:44 +0200
commitec7664e93f952f2233b55b3e70777060208f7eb3 (patch)
tree7c08d39072ef28d1556adde9ef2cd7e9abca1bc2
parentf7846fc7aa10efa34c611fb3bda4a562ba214520 (diff)
parent19609fdb071b77dc45d468b6db28dd66a819819d (diff)
downloadqutebrowser-ec7664e93f952f2233b55b3e70777060208f7eb3.tar.gz
qutebrowser-ec7664e93f952f2233b55b3e70777060208f7eb3.zip
Merge branch 'qt6-kbd-focus'
-rw-r--r--qutebrowser/app.py2
-rw-r--r--qutebrowser/browser/eventfilter.py35
-rw-r--r--qutebrowser/keyinput/modeman.py6
-rw-r--r--qutebrowser/mainwindow/tabwidget.py1
-rw-r--r--qutebrowser/utils/qtutils.py32
-rw-r--r--tests/end2end/data/insert_mode_settings/html/autofocus.html3
-rw-r--r--tests/end2end/features/misc.feature9
-rw-r--r--tests/unit/utils/test_qtutils.py50
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