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 | connect(sender: this, signal: &QQuickMaterialPlaceholderText::effectiveHorizontalAlignmentChanged, |
33 | context: this, slot: &QQuickMaterialPlaceholderText::adjustTransformOrigin); |
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 | void QQuickMaterialPlaceholderText::updateX() |
111 | { |
112 | setX(shouldFloat() ? floatingTargetX() : normalTargetX()); |
113 | } |
114 | |
115 | qreal controlTopInset(QQuickItem *textControl) |
116 | { |
117 | if (const auto textArea = qobject_cast<QQuickTextArea *>(object: textControl)) |
118 | return textArea->topInset(); |
119 | |
120 | if (const auto textField = qobject_cast<QQuickTextField *>(object: textControl)) |
121 | return textField->topInset(); |
122 | |
123 | return 0; |
124 | } |
125 | |
126 | qreal QQuickMaterialPlaceholderText::normalTargetY() const |
127 | { |
128 | auto *textArea = qobject_cast<QQuickTextArea *>(object: textControl()); |
129 | if (textArea && m_controlHeight >= textArea->implicitHeight()) { |
130 | // TextArea can be multiple lines in height, and we want the |
131 | // placeholder text to sit in the middle of its default-height |
132 | // (one-line) if its explicit height is greater than or equal to its |
133 | // implicit height - i.e. if it has room for it. If it doesn't have |
134 | // room, just do what TextField does. |
135 | // We should also account for any topInset the user might have specified, |
136 | // which is useful to ensure that the text doesn't get clipped. |
137 | return ((m_controlImplicitBackgroundHeight - m_largestHeight) / 2.0) |
138 | + controlTopInset(textControl: textControl()); |
139 | } |
140 | |
141 | // When the placeholder text shouldn't float, it should sit in the middle of the TextField. |
142 | return (m_controlHeight - height()) / 2.0; |
143 | } |
144 | |
145 | qreal QQuickMaterialPlaceholderText::floatingTargetY() const |
146 | { |
147 | // For filled text fields, the placeholder text sits just above |
148 | // the text when floating. |
149 | if (m_filled) |
150 | return m_verticalPadding; |
151 | |
152 | // Outlined text fields have the placeaholder vertically centered |
153 | // along the outline at the top. |
154 | return (-m_largestHeight / 2.0) + controlTopInset(textControl: textControl()); |
155 | } |
156 | |
157 | qreal QQuickMaterialPlaceholderText::normalTargetX() const |
158 | { |
159 | return m_leftPadding; |
160 | } |
161 | |
162 | qreal QQuickMaterialPlaceholderText::floatingTargetX() const |
163 | { |
164 | return m_floatingLeftPadding; |
165 | } |
166 | |
167 | /*! |
168 | \internal |
169 | |
170 | The height of the text at its largest size that we set. |
171 | */ |
172 | int QQuickMaterialPlaceholderText::largestHeight() const |
173 | { |
174 | return m_largestHeight; |
175 | } |
176 | |
177 | qreal QQuickMaterialPlaceholderText::controlImplicitBackgroundHeight() const |
178 | { |
179 | return m_controlImplicitBackgroundHeight; |
180 | } |
181 | |
182 | void QQuickMaterialPlaceholderText::setControlImplicitBackgroundHeight(qreal controlImplicitBackgroundHeight) |
183 | { |
184 | if (qFuzzyCompare(p1: m_controlImplicitBackgroundHeight, p2: controlImplicitBackgroundHeight)) |
185 | return; |
186 | |
187 | m_controlImplicitBackgroundHeight = controlImplicitBackgroundHeight; |
188 | updateY(); |
189 | emit controlImplicitBackgroundHeightChanged(); |
190 | } |
191 | |
192 | /*! |
193 | \internal |
194 | |
195 | Exists so that we can call updateY when the control's height changes, |
196 | which is necessary for some y position calculations. |
197 | |
198 | We don't really need it for the actual calculations, since we already |
199 | have access to the control, from which the property comes, but |
200 | it's simpler just to use it. |
201 | */ |
202 | qreal QQuickMaterialPlaceholderText::controlHeight() const |
203 | { |
204 | return m_controlHeight; |
205 | } |
206 | |
207 | void QQuickMaterialPlaceholderText::setControlHeight(qreal controlHeight) |
208 | { |
209 | if (qFuzzyCompare(p1: m_controlHeight, p2: controlHeight)) |
210 | return; |
211 | |
212 | m_controlHeight = controlHeight; |
213 | updateY(); |
214 | } |
215 | |
216 | qreal QQuickMaterialPlaceholderText::verticalPadding() const |
217 | { |
218 | return m_verticalPadding; |
219 | } |
220 | |
221 | void QQuickMaterialPlaceholderText::setVerticalPadding(qreal verticalPadding) |
222 | { |
223 | if (qFuzzyCompare(p1: m_verticalPadding, p2: verticalPadding)) |
224 | return; |
225 | |
226 | m_verticalPadding = verticalPadding; |
227 | emit verticalPaddingChanged(); |
228 | } |
229 | |
230 | void QQuickMaterialPlaceholderText::setLeftPadding(int leftPadding) |
231 | { |
232 | if (leftPadding == m_leftPadding) |
233 | return; |
234 | |
235 | m_leftPadding = leftPadding; |
236 | updateX(); |
237 | } |
238 | |
239 | void QQuickMaterialPlaceholderText::setFloatingLeftPadding(int floatingLeftPadding) |
240 | { |
241 | if (floatingLeftPadding == m_floatingLeftPadding) |
242 | return; |
243 | |
244 | m_floatingLeftPadding = floatingLeftPadding; |
245 | updateX(); |
246 | } |
247 | |
248 | void QQuickMaterialPlaceholderText::adjustTransformOrigin() |
249 | { |
250 | switch (effectiveHAlign()) { |
251 | case QQuickText::AlignLeft: |
252 | Q_FALLTHROUGH(); |
253 | case QQuickText::AlignJustify: |
254 | setTransformOrigin(QQuickItem::Left); |
255 | break; |
256 | case QQuickText::AlignRight: |
257 | setTransformOrigin(QQuickItem::Right); |
258 | break; |
259 | case QQuickText::AlignHCenter: |
260 | setTransformOrigin(QQuickItem::Center); |
261 | break; |
262 | } |
263 | } |
264 | |
265 | void QQuickMaterialPlaceholderText::controlGotActiveFocus() |
266 | { |
267 | if (m_focusOutAnimation) { |
268 | // Focus changes can happen before the animations finish. |
269 | // In that case, stop the animation, which will eventually delete it. |
270 | // Until it's deleted, we clear the pointer so that our asserts don't fail |
271 | // for the wrong reason. |
272 | m_focusOutAnimation->stop(); |
273 | m_focusOutAnimation.clear(); |
274 | } |
275 | |
276 | Q_ASSERT(!m_focusInAnimation); |
277 | if (shouldAnimate()) { |
278 | m_focusInAnimation = new QParallelAnimationGroup(this); |
279 | |
280 | QPropertyAnimation *yAnimation = new QPropertyAnimation(this, "y" , this); |
281 | yAnimation->setDuration(300); |
282 | yAnimation->setStartValue(y()); |
283 | yAnimation->setEndValue(floatingTargetY()); |
284 | yAnimation->setEasingCurve(*animationEasingCurve); |
285 | m_focusInAnimation->addAnimation(animation: yAnimation); |
286 | |
287 | QPropertyAnimation *xAnimation = new QPropertyAnimation(this, "x" , this); |
288 | xAnimation->setDuration(300); |
289 | xAnimation->setStartValue(x()); |
290 | xAnimation->setEndValue(floatingTargetX()); |
291 | xAnimation->setEasingCurve(*animationEasingCurve); |
292 | m_focusInAnimation->addAnimation(animation: xAnimation); |
293 | |
294 | auto *scaleAnimation = new QPropertyAnimation(this, "scale" , this); |
295 | scaleAnimation->setDuration(300); |
296 | scaleAnimation->setStartValue(1); |
297 | scaleAnimation->setEndValue(floatingScale); |
298 | yAnimation->setEasingCurve(*animationEasingCurve); |
299 | m_focusInAnimation->addAnimation(animation: scaleAnimation); |
300 | |
301 | m_focusInAnimation->start(policy: QAbstractAnimation::DeleteWhenStopped); |
302 | } else { |
303 | updateY(); |
304 | updateX(); |
305 | } |
306 | } |
307 | |
308 | void QQuickMaterialPlaceholderText::controlLostActiveFocus() |
309 | { |
310 | if (m_focusInAnimation) { |
311 | m_focusInAnimation->stop(); |
312 | m_focusInAnimation.clear(); |
313 | } |
314 | |
315 | Q_ASSERT(!m_focusOutAnimation); |
316 | if (shouldAnimate()) { |
317 | m_focusOutAnimation = new QParallelAnimationGroup(this); |
318 | |
319 | auto *yAnimation = new QPropertyAnimation(this, "y" , this); |
320 | yAnimation->setDuration(300); |
321 | yAnimation->setStartValue(y()); |
322 | yAnimation->setEndValue(normalTargetY()); |
323 | yAnimation->setEasingCurve(*animationEasingCurve); |
324 | m_focusOutAnimation->addAnimation(animation: yAnimation); |
325 | |
326 | QPropertyAnimation *xAnimation = new QPropertyAnimation(this, "x" , this); |
327 | xAnimation->setDuration(300); |
328 | xAnimation->setStartValue(x()); |
329 | xAnimation->setEndValue(normalTargetX()); |
330 | xAnimation->setEasingCurve(*animationEasingCurve); |
331 | m_focusOutAnimation->addAnimation(animation: xAnimation); |
332 | |
333 | auto *scaleAnimation = new QPropertyAnimation(this, "scale" , this); |
334 | scaleAnimation->setDuration(300); |
335 | scaleAnimation->setStartValue(floatingScale); |
336 | scaleAnimation->setEndValue(1); |
337 | yAnimation->setEasingCurve(*animationEasingCurve); |
338 | m_focusOutAnimation->addAnimation(animation: scaleAnimation); |
339 | |
340 | m_focusOutAnimation->start(policy: QAbstractAnimation::DeleteWhenStopped); |
341 | } else { |
342 | updateY(); |
343 | updateX(); |
344 | } |
345 | } |
346 | |
347 | void QQuickMaterialPlaceholderText::maybeSetFocusAnimationProgress() |
348 | { |
349 | updateY(); |
350 | updateX(); |
351 | setScale(shouldFloat() ? floatingScale : 1.0); |
352 | } |
353 | |
354 | void QQuickMaterialPlaceholderText::componentComplete() |
355 | { |
356 | QQuickPlaceholderText::componentComplete(); |
357 | |
358 | adjustTransformOrigin(); |
359 | |
360 | m_largestHeight = implicitHeight(); |
361 | if (m_largestHeight > 0) { |
362 | emit largestHeightChanged(); |
363 | } else { |
364 | qmlWarning(me: this) << "Expected implicitHeight of placeholder text" << text() |
365 | << "to be greater than 0 by component completion!" ; |
366 | } |
367 | |
368 | maybeSetFocusAnimationProgress(); |
369 | } |
370 | |
371 | QT_END_NAMESPACE |
372 | |