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 if (m_controlHasActiveFocus)
63 controlGotActiveFocus();
64 else
65 controlLostActiveFocus();
66 emit controlHasActiveFocusChanged();
67}
68
69bool QQuickMaterialPlaceholderText::controlHasText() const
70{
71 return m_controlHasText;
72}
73
74void 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*/
90bool QQuickMaterialPlaceholderText::shouldFloat() const
91{
92 const bool controlHasActiveFocusOrText = m_controlHasActiveFocus || m_controlHasText;
93 return m_filled
94 ? controlHasActiveFocusOrText
95 : !text().isEmpty() && controlHasActiveFocusOrText;
96}
97
98bool QQuickMaterialPlaceholderText::shouldAnimate() const
99{
100 return m_filled
101 ? !m_controlHasText
102 : !m_controlHasText && !text().isEmpty();
103}
104
105void QQuickMaterialPlaceholderText::updateY()
106{
107 setY(shouldFloat() ? floatingTargetY() : normalTargetY());
108}
109
110void QQuickMaterialPlaceholderText::updateX()
111{
112 setX(shouldFloat() ? floatingTargetX() : normalTargetX());
113}
114
115qreal 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
126qreal 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
145qreal 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
157qreal QQuickMaterialPlaceholderText::normalTargetX() const
158{
159 return m_leftPadding;
160}
161
162qreal 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*/
172int QQuickMaterialPlaceholderText::largestHeight() const
173{
174 return m_largestHeight;
175}
176
177qreal QQuickMaterialPlaceholderText::controlImplicitBackgroundHeight() const
178{
179 return m_controlImplicitBackgroundHeight;
180}
181
182void 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*/
202qreal QQuickMaterialPlaceholderText::controlHeight() const
203{
204 return m_controlHeight;
205}
206
207void QQuickMaterialPlaceholderText::setControlHeight(qreal controlHeight)
208{
209 if (qFuzzyCompare(p1: m_controlHeight, p2: controlHeight))
210 return;
211
212 m_controlHeight = controlHeight;
213 updateY();
214}
215
216qreal QQuickMaterialPlaceholderText::verticalPadding() const
217{
218 return m_verticalPadding;
219}
220
221void 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
230void QQuickMaterialPlaceholderText::setLeftPadding(int leftPadding)
231{
232 if (leftPadding == m_leftPadding)
233 return;
234
235 m_leftPadding = leftPadding;
236 updateX();
237}
238
239void QQuickMaterialPlaceholderText::setFloatingLeftPadding(int floatingLeftPadding)
240{
241 if (floatingLeftPadding == m_floatingLeftPadding)
242 return;
243
244 m_floatingLeftPadding = floatingLeftPadding;
245 updateX();
246}
247
248void 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
265void 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
308void 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
347void QQuickMaterialPlaceholderText::maybeSetFocusAnimationProgress()
348{
349 updateY();
350 updateX();
351 setScale(shouldFloat() ? floatingScale : 1.0);
352}
353
354void 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
371QT_END_NAMESPACE
372

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