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
15QT_BEGIN_NAMESPACE
16
17static const qreal floatingScale = 0.8;
18Q_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
29QQuickMaterialPlaceholderText::QQuickMaterialPlaceholderText(QQuickItem *parent)
30 : QQuickPlaceholderText(parent)
31{
32 connect(sender: this, signal: &QQuickMaterialPlaceholderText::effectiveHorizontalAlignmentChanged,
33 context: this, slot: &QQuickMaterialPlaceholderText::adjustTransformOrigin);
34}
35
36bool QQuickMaterialPlaceholderText::isFilled() const
37{
38 return m_filled;
39}
40
41void QQuickMaterialPlaceholderText::setFilled(bool filled)
42{
43 if (filled == m_filled)
44 return;
45
46 m_filled = filled;
47 update();
48 void filledChanged();
49}
50
51bool QQuickMaterialPlaceholderText::controlHasActiveFocus() const
52{
53 return m_controlHasActiveFocus;
54}
55
56void QQuickMaterialPlaceholderText::setControlHasActiveFocus(bool controlHasActiveFocus)
57{
58 if (m_controlHasActiveFocus == controlHasActiveFocus)
59 return;
60
61 m_controlHasActiveFocus = controlHasActiveFocus;
62 controlActiveFocusChanged();
63 emit controlHasActiveFocusChanged();
64}
65
66bool QQuickMaterialPlaceholderText::controlHasText() const
67{
68 return m_controlHasText;
69}
70
71void QQuickMaterialPlaceholderText::setControlHasText(bool controlHasText)
72{
73 if (m_controlHasText == controlHasText)
74 return;
75
76 m_controlHasText = controlHasText;
77 updateFocusAnimation();
78 emit controlHasTextChanged();
79}
80
81/*
82 Placeholder text of outlined text fields should float when:
83 - There is placeholder text, and
84 - The control has active focus, or
85 - The control has text
86*/
87bool QQuickMaterialPlaceholderText::shouldFloat() const
88{
89 const bool controlHasActiveFocusOrText = m_controlHasActiveFocus || m_controlHasText;
90 return m_filled
91 ? controlHasActiveFocusOrText
92 : !text().isEmpty() && controlHasActiveFocusOrText;
93}
94
95bool QQuickMaterialPlaceholderText::shouldAnimate() const
96{
97 return m_filled
98 ? !m_controlHasText
99 : !m_controlHasText && !text().isEmpty();
100}
101
102void QQuickMaterialPlaceholderText::updateY()
103{
104 setY(shouldFloat() ? floatingTargetY() : normalTargetY());
105}
106
107void QQuickMaterialPlaceholderText::updateX()
108{
109 setX(shouldFloat() ? floatingTargetX() : normalTargetX());
110}
111
112qreal controlTopInset(QQuickItem *textControl)
113{
114 if (const auto textArea = qobject_cast<QQuickTextArea *>(object: textControl))
115 return textArea->topInset();
116
117 if (const auto textField = qobject_cast<QQuickTextField *>(object: textControl))
118 return textField->topInset();
119
120 return 0;
121}
122
123qreal QQuickMaterialPlaceholderText::normalTargetY() const
124{
125 auto *textArea = qobject_cast<QQuickTextArea *>(object: textControl());
126 if (textArea && m_controlHeight >= textArea->implicitHeight()) {
127 // TextArea can be multiple lines in height, and we want the
128 // placeholder text to sit in the middle of its default-height
129 // (one-line) if its explicit height is greater than or equal to its
130 // implicit height - i.e. if it has room for it. If it doesn't have
131 // room, just do what TextField does.
132 // We should also account for any topInset the user might have specified,
133 // which is useful to ensure that the text doesn't get clipped.
134 return ((m_controlImplicitBackgroundHeight - m_largestHeight) / 2.0)
135 + controlTopInset(textControl: textControl());
136 }
137
138 // When the placeholder text shouldn't float, it should sit in the middle of the TextField.
139 return (m_controlHeight - height()) / 2.0;
140}
141
142qreal QQuickMaterialPlaceholderText::floatingTargetY() const
143{
144 // For filled text fields, the placeholder text sits just above
145 // the text when floating.
146 if (m_filled)
147 return m_verticalPadding;
148
149 // Outlined text fields have the placeaholder vertically centered
150 // along the outline at the top.
151 return (-m_largestHeight / 2.0) + controlTopInset(textControl: textControl());
152}
153
154qreal QQuickMaterialPlaceholderText::normalTargetX() const
155{
156 return m_leftPadding;
157}
158
159qreal QQuickMaterialPlaceholderText::floatingTargetX() const
160{
161 return m_floatingLeftPadding;
162}
163
164/*!
165 \internal
166
167 The height of the text at its largest size that we set.
168*/
169int QQuickMaterialPlaceholderText::largestHeight() const
170{
171 return m_largestHeight;
172}
173
174qreal QQuickMaterialPlaceholderText::controlImplicitBackgroundHeight() const
175{
176 return m_controlImplicitBackgroundHeight;
177}
178
179void QQuickMaterialPlaceholderText::setControlImplicitBackgroundHeight(qreal controlImplicitBackgroundHeight)
180{
181 if (qFuzzyCompare(p1: m_controlImplicitBackgroundHeight, p2: controlImplicitBackgroundHeight))
182 return;
183
184 m_controlImplicitBackgroundHeight = controlImplicitBackgroundHeight;
185 updateFocusAnimation();
186 emit controlImplicitBackgroundHeightChanged();
187}
188
189/*!
190 \internal
191
192 Exists so that we can call updateY when the control's height changes,
193 which is necessary for some y position calculations.
194
195 We don't really need it for the actual calculations, since we already
196 have access to the control, from which the property comes, but
197 it's simpler just to use it.
198*/
199qreal QQuickMaterialPlaceholderText::controlHeight() const
200{
201 return m_controlHeight;
202}
203
204void QQuickMaterialPlaceholderText::setControlHeight(qreal controlHeight)
205{
206 if (qFuzzyCompare(p1: m_controlHeight, p2: controlHeight))
207 return;
208
209 m_controlHeight = controlHeight;
210 updateFocusAnimation();
211}
212
213qreal QQuickMaterialPlaceholderText::verticalPadding() const
214{
215 return m_verticalPadding;
216}
217
218void QQuickMaterialPlaceholderText::setVerticalPadding(qreal verticalPadding)
219{
220 if (qFuzzyCompare(p1: m_verticalPadding, p2: verticalPadding))
221 return;
222
223 m_verticalPadding = verticalPadding;
224 updateFocusAnimation();
225 emit verticalPaddingChanged();
226}
227
228void QQuickMaterialPlaceholderText::setLeftPadding(int leftPadding)
229{
230 if (leftPadding == m_leftPadding)
231 return;
232
233 m_leftPadding = leftPadding;
234 updateFocusAnimation();
235}
236
237void QQuickMaterialPlaceholderText::setFloatingLeftPadding(int floatingLeftPadding)
238{
239 if (floatingLeftPadding == m_floatingLeftPadding)
240 return;
241
242 m_floatingLeftPadding = floatingLeftPadding;
243 updateFocusAnimation();
244}
245
246void QQuickMaterialPlaceholderText::adjustTransformOrigin()
247{
248 switch (effectiveHAlign()) {
249 case QQuickText::AlignLeft:
250 Q_FALLTHROUGH();
251 case QQuickText::AlignJustify:
252 setTransformOrigin(QQuickItem::Left);
253 break;
254 case QQuickText::AlignRight:
255 setTransformOrigin(QQuickItem::Right);
256 break;
257 case QQuickText::AlignHCenter:
258 setTransformOrigin(QQuickItem::Center);
259 break;
260 }
261}
262
263void QQuickMaterialPlaceholderText::controlActiveFocusChanged()
264{
265 if (m_focusAnimation) {
266 // Focus changes can happen before the animations finish.
267 // In that case, stop the animation, which will eventually delete it.
268 // Until it's deleted, we clear the pointer so that our asserts don't fail
269 // for the wrong reason.
270 m_focusAnimation->stop();
271 m_focusAnimation.clear();
272 }
273 updateFocusAnimation(createIfNeeded: true);
274}
275
276void QQuickMaterialPlaceholderText::updateFocusAnimation(bool createIfNeeded)
277{
278 if (shouldAnimate() && (m_focusAnimation || createIfNeeded)) {
279 int duration = 300;
280 if (m_focusAnimation) {
281 duration = m_focusAnimation->totalDuration() - m_focusAnimation->currentTime();
282 m_focusAnimation->stop();
283 m_focusAnimation.clear();
284 }
285
286 m_focusAnimation = new QParallelAnimationGroup(this);
287
288 auto *yAnimation = new QPropertyAnimation(this, "y", this);
289 yAnimation->setDuration(duration);
290 yAnimation->setStartValue(y());
291 yAnimation->setEndValue(shouldFloat() ? floatingTargetY() : normalTargetY());
292 yAnimation->setEasingCurve(*animationEasingCurve);
293 m_focusAnimation->addAnimation(animation: yAnimation);
294
295 QPropertyAnimation *xAnimation = new QPropertyAnimation(this, "x", this);
296 xAnimation->setDuration(duration);
297 xAnimation->setStartValue(x());
298 xAnimation->setEndValue(shouldFloat() ? floatingTargetX() : normalTargetX());
299 xAnimation->setEasingCurve(*animationEasingCurve);
300 m_focusAnimation->addAnimation(animation: xAnimation);
301
302 auto *scaleAnimation = new QPropertyAnimation(this, "scale", this);
303 scaleAnimation->setDuration(duration);
304 scaleAnimation->setStartValue(scale());
305 scaleAnimation->setEndValue(shouldFloat() ? floatingScale : 1.0);
306 yAnimation->setEasingCurve(*animationEasingCurve);
307 m_focusAnimation->addAnimation(animation: scaleAnimation);
308
309 m_focusAnimation->start(policy: QAbstractAnimation::DeleteWhenStopped);
310 } else {
311 if (m_focusAnimation) {
312 m_focusAnimation->stop();
313 m_focusAnimation.clear();
314 }
315 updateY();
316 updateX();
317 setScale(shouldFloat() ? floatingScale : 1.0);
318 }
319}
320
321void QQuickMaterialPlaceholderText::componentComplete()
322{
323 QQuickPlaceholderText::componentComplete();
324
325 adjustTransformOrigin();
326
327 m_largestHeight = implicitHeight();
328 if (m_largestHeight > 0) {
329 emit largestHeightChanged();
330 } else {
331 qmlWarning(me: this) << "Expected implicitHeight of placeholder text" << text()
332 << "to be greater than 0 by component completion!";
333 }
334
335 updateFocusAnimation();
336}
337
338QT_END_NAMESPACE
339

source code of qtdeclarative/src/quickcontrols/material/impl/qquickmaterialplaceholdertext.cpp