1 | // Copyright (C) 2023 The Qt Company Ltd. |
2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only |
3 | |
4 | #include "qquickfocusframe.h" |
5 | |
6 | #include <QtCore/qmetaobject.h> |
7 | |
8 | #include <QtGui/qguiapplication.h> |
9 | |
10 | #include <QtQml/qqmlengine.h> |
11 | #include <QtQml/qqmlcontext.h> |
12 | #include <QtQml/qqmlcomponent.h> |
13 | |
14 | #include <QtQuick/qquickitem.h> |
15 | |
16 | #include "items/qquickstyleitem.h" |
17 | |
18 | QT_BEGIN_NAMESPACE |
19 | |
20 | Q_LOGGING_CATEGORY(lcFocusFrame, "qt.quick.controls.focusframe" ) |
21 | |
22 | QQuickFocusFrameDescription QQuickFocusFrameDescription::Invalid = { .target: nullptr, .margins: QQuickStyleMargins(), .radius: 0 }; |
23 | QScopedPointer<QQuickItem> QQuickFocusFrame::m_focusFrame; |
24 | |
25 | QQuickFocusFrame::QQuickFocusFrame() |
26 | { |
27 | connect(qGuiApp, signal: &QGuiApplication::focusObjectChanged, context: this, slot: [this](QObject *focusObject){ |
28 | if (auto item = qobject_cast<QQuickItem *>(o: focusObject)) |
29 | moveToItem(item); |
30 | else |
31 | m_focusFrame.reset(); |
32 | }); |
33 | } |
34 | |
35 | void QQuickFocusFrame::moveToItem(QQuickItem *item) |
36 | { |
37 | if (!m_focusFrame) { |
38 | const auto context = QQmlEngine::contextForObject(item); |
39 | // In certain cases like QQuickWebEngineView, the item |
40 | // gets focus even though it has no QQmlEngine associated with its context. |
41 | // We need the engine for creating the focus frame component. |
42 | if (!context || !context->engine()) |
43 | return; |
44 | m_focusFrame.reset(other: createFocusFrame(context)); |
45 | if (!m_focusFrame) { |
46 | qWarning() << "Failed to create FocusFrame" ; |
47 | return; |
48 | } |
49 | } |
50 | |
51 | const QQuickFocusFrameDescription &config = getDescriptionForItem(focusItem: item); |
52 | QMetaObject::invokeMethod(obj: m_focusFrame.data(), member: "moveToItem" , |
53 | Q_ARG(QVariant, QVariant::fromValue(config.target)), |
54 | Q_ARG(QVariant, QVariant::fromValue(config.margins)), |
55 | Q_ARG(QVariant, QVariant::fromValue(config.radius))); |
56 | } |
57 | |
58 | QQuickFocusFrameDescription QQuickFocusFrame::getDescriptionForItem(QQuickItem *focusItem) const |
59 | { |
60 | qCDebug(lcFocusFrame) << "new focusobject:" << focusItem; |
61 | const auto parentItem = focusItem->parentItem(); |
62 | if (!parentItem) |
63 | return QQuickFocusFrameDescription::Invalid; |
64 | |
65 | // The item that gets active focus can be a child of the control (e.g |
66 | // editable ComboBox). In that case, resolve the actual control first. |
67 | const auto proxy = focusItem->property(name: "__focusFrameControl" ).value<QQuickItem *>(); |
68 | const auto control = proxy ? proxy : focusItem; |
69 | auto target = control->property(name: "__focusFrameTarget" ).value<QQuickItem *>(); |
70 | qCDebug(lcFocusFrame) << "target:" << target; |
71 | qCDebug(lcFocusFrame) << "control:" << control; |
72 | |
73 | if (!target) { |
74 | // __focusFrameTarget points to the item in the control that should |
75 | // get the focus frame. This is usually the control itself, but can |
76 | // sometimes be a child (CheckBox). We anyway require |
77 | // this property to be set if we are to show the focus frame around |
78 | // the control in the first place. So for controls that don't want |
79 | // a frame (ProgressBar), we simply skip setting it. |
80 | // Also, we should never show a focus frame around custom controls. |
81 | // None of the built-in styles do that, so to be consistent, we |
82 | // shouldn't either. Besides, drawing a focus frame around an unknown |
83 | // item without any way to turn it off can easily be unwanted. A better |
84 | // way for custom controls to get a native focus frame is for us to offer |
85 | // a FocusFrame control (QTBUG-86818). |
86 | return QQuickFocusFrameDescription::Invalid; |
87 | } |
88 | |
89 | // If the control gives us a QQuickStyleItem, we use that to configure the focus frame. |
90 | // By default we assume that the background delegate is a QQuickStyleItem, but the |
91 | // control can override this by setting __focusFrameStyleItem. |
92 | const auto styleItemProperty = control->property(name: "__focusFrameStyleItem" ); |
93 | auto item = styleItemProperty.value<QQuickItem *>(); |
94 | if (!item) { |
95 | const auto styleItemProperty = control->property(name: "background" ); |
96 | item = styleItemProperty.value<QQuickItem *>(); |
97 | } |
98 | qCDebug(lcFocusFrame) << "styleItem:" << item; |
99 | if (!item) |
100 | return QQuickFocusFrameDescription::Invalid; |
101 | if (QQuickStyleItem *styleItem = qobject_cast<QQuickStyleItem *>(object: item)) |
102 | return { .target: target, .margins: QQuickStyleMargins(styleItem->layoutMargins()), .radius: styleItem->focusFrameRadius() }; |
103 | |
104 | // Some controls don't have a QQuickStyleItem. But if the __focusFrameStyleItem |
105 | // has a "__focusFrameRadius" property set, we show a default focus frame using the specified radius instead. |
106 | const QVariant focusFrameRadiusVariant = item->property(name: "__focusFrameRadius" ); |
107 | if (focusFrameRadiusVariant.isValid()) { |
108 | qCDebug(lcFocusFrame) << "'focusFrameRadius' property found, showing a default focus frame" ; |
109 | const QStyleOption opt; |
110 | const qreal radius = qMax(a: 0.0, b: focusFrameRadiusVariant.toReal()); |
111 | return { .target: target, .margins: QQuickStyleMargins(), .radius: radius }; |
112 | } |
113 | |
114 | // The application has set a custom delegate on the control. In that |
115 | // case, it's the delegates responsibility to draw a focus frame. |
116 | qCDebug(lcFocusFrame) << "custom delegates in use, skip showing focus frame" ; |
117 | return QQuickFocusFrameDescription::Invalid; |
118 | } |
119 | |
120 | QT_END_NAMESPACE |
121 | |