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 "qquickmaterialplaceholdertext_p.h" |
5 | |
6 | #include <QtCore/qpropertyanimation.h> |
7 | #include <QtCore/qparallelanimationgroup.h> |
8 | #include <QtGui/qpainter.h> |
9 | #include <QtGui/qpainterpath.h> |
10 | #include <QtQml/qqmlinfo.h> |
11 | #include <QtQuickTemplates2/private/qquicktheme_p.h> |
12 | #include <QtQuickTemplates2/private/qquicktextarea_p.h> |
13 | #include <QtQuickTemplates2/private/qquicktextfield_p.h> |
14 | |
15 | QT_BEGIN_NAMESPACE |
16 | |
17 | static const qreal floatingScale = 0.8; |
18 | Q_GLOBAL_STATIC(QEasingCurve, animationEasingCurve, QEasingCurve::OutSine); |
19 | |
20 | /* |
21 | This class makes it easier to animate the various placeholder text changes |
22 | for each type of text container (filled, outlined). |
23 | |
24 | By doing animations in C++, we avoid having a bunch of states, transitions, |
25 | and animations (which are all QObjects) declared in QML, even if that text |
26 | control never gets focus and hence never needs them. |
27 | */ |
28 | |
29 | QQuickMaterialPlaceholderText::QQuickMaterialPlaceholderText(QQuickItem *parent) |
30 | : QQuickPlaceholderText(parent) |
31 | { |
32 | // Ensure that scaling happens on the left side, at the vertical center. |
33 | setTransformOrigin(QQuickItem::Left); |
34 | } |
35 | |
36 | bool QQuickMaterialPlaceholderText::isFilled() const |
37 | { |
38 | return m_filled; |
39 | } |
40 | |
41 | void QQuickMaterialPlaceholderText::setFilled(bool filled) |
42 | { |
43 | if (filled == m_filled) |
44 | return; |
45 | |
46 | m_filled = filled; |
47 | update(); |
48 | void filledChanged(); |
49 | } |
50 | |
51 | bool QQuickMaterialPlaceholderText::controlHasActiveFocus() const |
52 | { |
53 | return m_controlHasActiveFocus; |
54 | } |
55 | |
56 | void QQuickMaterialPlaceholderText::setControlHasActiveFocus(bool controlHasActiveFocus) |
57 | { |
58 | if (m_controlHasActiveFocus == controlHasActiveFocus) |
59 | return; |
60 | |
61 | m_controlHasActiveFocus = controlHasActiveFocus; |
62 | if (m_controlHasActiveFocus) |
63 | controlGotActiveFocus(); |
64 | else |
65 | controlLostActiveFocus(); |
66 | emit controlHasActiveFocusChanged(); |
67 | } |
68 | |
69 | bool QQuickMaterialPlaceholderText::controlHasText() const |
70 | { |
71 | return m_controlHasText; |
72 | } |
73 | |
74 | void QQuickMaterialPlaceholderText::setControlHasText(bool controlHasText) |
75 | { |
76 | if (m_controlHasText == controlHasText) |
77 | return; |
78 | |
79 | m_controlHasText = controlHasText; |
80 | maybeSetFocusAnimationProgress(); |
81 | emit controlHasTextChanged(); |
82 | } |
83 | |
84 | /* |
85 | Placeholder text of outlined text fields should float when: |
86 | - There is placeholder text, and |
87 | - The control has active focus, or |
88 | - The control has text |
89 | */ |
90 | bool QQuickMaterialPlaceholderText::shouldFloat() const |
91 | { |
92 | const bool controlHasActiveFocusOrText = m_controlHasActiveFocus || m_controlHasText; |
93 | return m_filled |
94 | ? controlHasActiveFocusOrText |
95 | : !text().isEmpty() && controlHasActiveFocusOrText; |
96 | } |
97 | |
98 | bool QQuickMaterialPlaceholderText::shouldAnimate() const |
99 | { |
100 | return m_filled |
101 | ? !m_controlHasText |
102 | : !m_controlHasText && !text().isEmpty(); |
103 | } |
104 | |
105 | void QQuickMaterialPlaceholderText::updateY() |
106 | { |
107 | setY(shouldFloat() ? floatingTargetY() : normalTargetY()); |
108 | } |
109 | |
110 | qreal controlTopInset(QQuickItem *textControl) |
111 | { |
112 | if (const auto textArea = qobject_cast<QQuickTextArea *>(object: textControl)) |
113 | return textArea->topInset(); |
114 | |
115 | if (const auto textField = qobject_cast<QQuickTextField *>(object: textControl)) |
116 | return textField->topInset(); |
117 | |
118 | return 0; |
119 | } |
120 | |
121 | qreal QQuickMaterialPlaceholderText::normalTargetY() const |
122 | { |
123 | auto *textArea = qobject_cast<QQuickTextArea *>(object: textControl()); |
124 | if (textArea && m_controlHeight >= textArea->implicitHeight()) { |
125 | // TextArea can be multiple lines in height, and we want the |
126 | // placeholder text to sit in the middle of its default-height |
127 | // (one-line) if its explicit height is greater than or equal to its |
128 | // implicit height - i.e. if it has room for it. If it doesn't have |
129 | // room, just do what TextField does. |
130 | // We should also account for any topInset the user might have specified, |
131 | // which is useful to ensure that the text doesn't get clipped. |
132 | return ((m_controlImplicitBackgroundHeight - m_largestHeight) / 2.0) |
133 | + controlTopInset(textControl: textControl()); |
134 | } |
135 | |
136 | // When the placeholder text shouldn't float, it should sit in the middle of the TextField. |
137 | return (m_controlHeight - height()) / 2.0; |
138 | } |
139 | |
140 | qreal QQuickMaterialPlaceholderText::floatingTargetY() const |
141 | { |
142 | // For filled text fields, the placeholder text sits just above |
143 | // the text when floating. |
144 | if (m_filled) |
145 | return m_verticalPadding; |
146 | |
147 | // Outlined text fields have the placeaholder vertically centered |
148 | // along the outline at the top. |
149 | return (-m_largestHeight / 2.0) + controlTopInset(textControl: textControl()); |
150 | } |
151 | |
152 | /*! |
153 | \internal |
154 | |
155 | The height of the text at its largest size that we set. |
156 | */ |
157 | int QQuickMaterialPlaceholderText::largestHeight() const |
158 | { |
159 | return m_largestHeight; |
160 | } |
161 | |
162 | qreal QQuickMaterialPlaceholderText::controlImplicitBackgroundHeight() const |
163 | { |
164 | return m_controlImplicitBackgroundHeight; |
165 | } |
166 | |
167 | void QQuickMaterialPlaceholderText::setControlImplicitBackgroundHeight(qreal controlImplicitBackgroundHeight) |
168 | { |
169 | if (qFuzzyCompare(p1: m_controlImplicitBackgroundHeight, p2: controlImplicitBackgroundHeight)) |
170 | return; |
171 | |
172 | m_controlImplicitBackgroundHeight = controlImplicitBackgroundHeight; |
173 | updateY(); |
174 | emit controlImplicitBackgroundHeightChanged(); |
175 | } |
176 | |
177 | /*! |
178 | \internal |
179 | |
180 | Exists so that we can call updateY when the control's height changes, |
181 | which is necessary for some y position calculations. |
182 | |
183 | We don't really need it for the actual calculations, since we already |
184 | have access to the control, from which the property comes, but |
185 | it's simpler just to use it. |
186 | */ |
187 | qreal QQuickMaterialPlaceholderText::controlHeight() const |
188 | { |
189 | return m_controlHeight; |
190 | } |
191 | |
192 | void QQuickMaterialPlaceholderText::setControlHeight(qreal controlHeight) |
193 | { |
194 | if (qFuzzyCompare(p1: m_controlHeight, p2: controlHeight)) |
195 | return; |
196 | |
197 | m_controlHeight = controlHeight; |
198 | updateY(); |
199 | } |
200 | |
201 | qreal QQuickMaterialPlaceholderText::verticalPadding() const |
202 | { |
203 | return m_verticalPadding; |
204 | } |
205 | |
206 | void QQuickMaterialPlaceholderText::setVerticalPadding(qreal verticalPadding) |
207 | { |
208 | if (qFuzzyCompare(p1: m_verticalPadding, p2: verticalPadding)) |
209 | return; |
210 | |
211 | m_verticalPadding = verticalPadding; |
212 | emit verticalPaddingChanged(); |
213 | } |
214 | |
215 | void QQuickMaterialPlaceholderText::controlGotActiveFocus() |
216 | { |
217 | if (m_focusOutAnimation) |
218 | m_focusOutAnimation->stop(); |
219 | |
220 | Q_ASSERT(!m_focusInAnimation); |
221 | if (shouldAnimate()) { |
222 | m_focusInAnimation = new QParallelAnimationGroup(this); |
223 | |
224 | QPropertyAnimation *yAnimation = new QPropertyAnimation(this, "y" , this); |
225 | yAnimation->setDuration(300); |
226 | yAnimation->setStartValue(y()); |
227 | yAnimation->setEndValue(floatingTargetY()); |
228 | yAnimation->setEasingCurve(*animationEasingCurve); |
229 | m_focusInAnimation->addAnimation(animation: yAnimation); |
230 | |
231 | auto *scaleAnimation = new QPropertyAnimation(this, "scale" , this); |
232 | scaleAnimation->setDuration(300); |
233 | scaleAnimation->setStartValue(1); |
234 | scaleAnimation->setEndValue(floatingScale); |
235 | yAnimation->setEasingCurve(*animationEasingCurve); |
236 | m_focusInAnimation->addAnimation(animation: scaleAnimation); |
237 | |
238 | m_focusInAnimation->start(policy: QAbstractAnimation::DeleteWhenStopped); |
239 | } else { |
240 | updateY(); |
241 | } |
242 | } |
243 | |
244 | void QQuickMaterialPlaceholderText::controlLostActiveFocus() |
245 | { |
246 | Q_ASSERT(!m_focusOutAnimation); |
247 | if (shouldAnimate()) { |
248 | m_focusOutAnimation = new QParallelAnimationGroup(this); |
249 | |
250 | auto *yAnimation = new QPropertyAnimation(this, "y" , this); |
251 | yAnimation->setDuration(300); |
252 | yAnimation->setStartValue(y()); |
253 | yAnimation->setEndValue(normalTargetY()); |
254 | yAnimation->setEasingCurve(*animationEasingCurve); |
255 | m_focusOutAnimation->addAnimation(animation: yAnimation); |
256 | |
257 | auto *scaleAnimation = new QPropertyAnimation(this, "scale" , this); |
258 | scaleAnimation->setDuration(300); |
259 | scaleAnimation->setStartValue(floatingScale); |
260 | scaleAnimation->setEndValue(1); |
261 | yAnimation->setEasingCurve(*animationEasingCurve); |
262 | m_focusOutAnimation->addAnimation(animation: scaleAnimation); |
263 | |
264 | m_focusOutAnimation->start(policy: QAbstractAnimation::DeleteWhenStopped); |
265 | } else { |
266 | updateY(); |
267 | } |
268 | } |
269 | |
270 | void QQuickMaterialPlaceholderText::maybeSetFocusAnimationProgress() |
271 | { |
272 | updateY(); |
273 | setScale(shouldFloat() ? floatingScale : 1.0); |
274 | } |
275 | |
276 | void QQuickMaterialPlaceholderText::componentComplete() |
277 | { |
278 | // We deliberately do not call QQuickPlaceholderText's implementation here, |
279 | // as Material 3 placeholder text should always be left-aligned. |
280 | QQuickText::componentComplete(); |
281 | |
282 | if (!parentItem()) |
283 | qmlWarning(me: this) << "Expected parent item by component completion!" ; |
284 | |
285 | m_largestHeight = implicitHeight(); |
286 | if (m_largestHeight > 0) { |
287 | emit largestHeightChanged(); |
288 | } else { |
289 | qmlWarning(me: this) << "Expected implicitHeight of placeholder text" << text() |
290 | << "to be greater than 0 by component completion!" ; |
291 | } |
292 | |
293 | maybeSetFocusAnimationProgress(); |
294 | } |
295 | |
296 | QT_END_NAMESPACE |
297 | |