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