Qt 6.x
The Qt SDK
All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends Macros Pages
qquickmaterialtextcontainer.cpp
Go to the documentation of this file.
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
5
6#include <QtCore/qpropertyanimation.h>
7#include <QtGui/qpainter.h>
8#include <QtGui/qpainterpath.h>
9#include <QtQml/qqmlinfo.h>
10
12
13/*
14 This class exists because:
15
16 - Rectangle doesn't support individual radii for each corner (QTBUG-48774).
17 - We need to draw an interrupted (where the placeholder text is) line for outlined containers.
18 - We need to animate the focus line for filled containers, and we can't use "Behavior on"
19 syntax because we only want to animate activeFocus becoming true, not also false. To do this
20 requires imperative code, and we want to keep the QML declarative.
21
22 focusAnimationProgress has to be a property even though it's only used internally,
23 because we have to use QPropertyAnimation on it.
24
25 An advantage of doing the animation in C++ is that we avoid the memory
26 overhead of an animation instance even when we're not using it, and instead
27 create it on demand and delete it when it's done. I tried doing the animation
28 declaratively with states and transitions, but it was more difficult to implement
29 and would have been harder to maintain, as well as having more overhead.
30*/
31
34{
35}
36
38{
39 return m_filled;
40}
41
43{
44 if (filled == m_filled)
45 return;
46
47 m_filled = filled;
48 update();
49}
50
52{
53 return m_fillColor;
54}
55
57{
58 if (fillColor == m_fillColor)
59 return;
60
61 m_fillColor = fillColor;
62 update();
63}
64
66{
67 return m_outlineColor;
68}
69
71{
72 if (outlineColor == m_outlineColor)
73 return;
74
75 m_outlineColor = outlineColor;
76 update();
77}
78
80{
81 return m_outlineColor;
82}
83
85{
86 if (focusedOutlineColor == m_focusedOutlineColor)
87 return;
88
89 m_focusedOutlineColor = focusedOutlineColor;
90 update();
91}
92
94{
95 return m_focusAnimationProgress;
96}
97
99{
100 if (qFuzzyCompare(progress, m_focusAnimationProgress))
101 return;
102
103 m_focusAnimationProgress = progress;
104 update();
105}
106
108{
109 return m_placeholderTextWidth;
110}
111
113{
114 if (qFuzzyCompare(placeholderTextWidth, m_placeholderTextWidth))
115 return;
116
117 m_placeholderTextWidth = placeholderTextWidth;
118 update();
119}
120
122{
123 return m_controlHasActiveFocus;
124}
125
127{
128 if (m_controlHasActiveFocus == controlHasActiveFocus)
129 return;
130
131 m_controlHasActiveFocus = controlHasActiveFocus;
132 if (m_controlHasActiveFocus)
133 controlGotActiveFocus();
134 else
135 controlLostActiveFocus();
137}
138
140{
141 return m_controlHasText;
142}
143
145{
146 if (m_controlHasText == controlHasText)
147 return;
148
149 m_controlHasText = controlHasText;
150 // TextArea's text length is updated after component completion,
151 // so account for that here and in setPlaceholderHasText().
152 maybeSetFocusAnimationProgress();
153 update();
155}
156
158{
159 return m_placeholderHasText;
160}
161
163{
164 if (m_placeholderHasText == placeholderHasText)
165 return;
166
167 m_placeholderHasText = placeholderHasText;
168 maybeSetFocusAnimationProgress();
169 update();
171}
172
174{
175 return m_horizontalPadding;
176}
177
187{
188 if (m_horizontalPadding == horizontalPadding)
189 return;
190 m_horizontalPadding = horizontalPadding;
191 update();
193}
194
196{
197 qreal w = width();
198 qreal h = height();
199 if (w <= 0 || h <= 0)
200 return;
201
202 // Account for pen width.
203 const qreal penWidth = m_filled ? 1 : (m_controlHasActiveFocus ? 2 : 1);
204 w -= penWidth;
205 h -= penWidth;
206
207 const qreal cornerRadius = 4;
208 // This is coincidentally the same as cornerRadius, but use different variable names
209 // to keep the code understandable.
210 const qreal gapPadding = 4;
212
213 QPointF startPos;
214
215 // Top-left rounded corner.
216 if (m_filled || m_focusAnimationProgress == 0) {
217 startPos = QPointF(cornerRadius, 0);
218 } else {
219 // When animating focus on outlined containers, we need to make a gap
220 // at the top left for the placeholder text.
221 // If the text is too wide for the container, it will be elided, so
222 // we shouldn't need to clamp its width here. TODO: check that this is the case for TextArea.
223 const qreal halfPlaceholderWidth = m_placeholderTextWidth / 2;
224 // Left padding plus half of the placeholder text gives the center of the placeholder text gap.
225 // Note that the placeholder gap is always aligned to the left side of the TextField,
226 // not the center, so we can't just use half the container's width.
227 const qreal gapCenterX = m_horizontalPadding + halfPlaceholderWidth;
228 // Start at the center of the gap and animate outwards towards the left-hand side.
229 // Subtract gapPadding to account for the gap between the line and the placeholder text.
230 // Also subtract the pen width because otherwise it extends by that distance too much to the right.
231 // Changing the cap style to Qt::FlatCap would only fix this by half the pen width,
232 // but it has no effect anyway (perhaps it literally only affects end points and not "start" points?).
233 startPos = QPointF(gapCenterX - (m_focusAnimationProgress * halfPlaceholderWidth) - gapPadding - penWidth, 0);
234 }
235 path.moveTo(startPos);
236 path.arcTo(0, 0, cornerRadius * 2, cornerRadius * 2, 90, 90);
237
238 // Bottom-left corner.
239 if (m_filled) {
240 path.lineTo(0, h);
241 } else {
242 path.lineTo(0, h - cornerRadius * 2);
243 path.arcTo(0, h - cornerRadius * 2, cornerRadius * 2, cornerRadius * 2, 180, 90);
244 }
245
246 // Bottom-right corner.
247 if (m_filled) {
248 path.lineTo(w, h);
249 } else {
250 path.lineTo(w - cornerRadius * 2, h);
251 path.arcTo(w - cornerRadius * 2, h - cornerRadius * 2, cornerRadius * 2, cornerRadius * 2, 270, 90);
252 }
253
254 // Top-right rounded corner.
255 path.lineTo(w, cornerRadius);
256 path.arcTo(w - (cornerRadius * 2), 0, cornerRadius * 2, cornerRadius * 2, 0, 90);
257
258 if (m_filled || qFuzzyIsNull(m_focusAnimationProgress)) {
259 // Back to the start.
260 path.lineTo(startPos.x(), startPos.y());
261 } else {
262 // Go to the end (right-hand side) of the gap.
263 const qreal halfPlaceholderWidth = (/*placeholderTextGap * 2 + */m_placeholderTextWidth) / 2;
264 const qreal gapCenterX = m_horizontalPadding + halfPlaceholderWidth;
265 // Just "+ placeholderTextGap" should be enough to get us to the correct place - not
266 // sure why doubling it is necessary...
267 path.lineTo(gapCenterX + (m_focusAnimationProgress * halfPlaceholderWidth) + gapPadding, startPos.y());
268 }
269
270 // Account for pen width.
271 painter->translate(penWidth / 2, penWidth / 2);
272
274
275 auto control = textControl();
276 const bool focused = control && control->hasActiveFocus();
277 // We still want to draw the stroke when it's filled, otherwise it will be a pixel
278 // (the pen width) too narrow on either side.
279 QPen pen;
280 pen.setColor(m_filled ? m_fillColor : (focused ? m_focusedOutlineColor : m_outlineColor));
281 pen.setWidthF(penWidth);
282 painter->setPen(pen);
283 if (m_filled)
284 painter->setBrush(QBrush(m_fillColor));
285
286 // Fill or stroke the container's shape.
287 // If not filling, the default brush will be used, which is Qt::NoBrush.
289
290 // Draw the focus line at the bottom for filled containers.
291 if (m_filled) {
292 if (!qFuzzyCompare(m_focusAnimationProgress, 1.0)) {
293 // Draw the enabled active indicator line (#10) that's at the bottom when it's not focused:
294 // https://m3.material.io/components/text-fields/specs#6d654d1d-262e-4697-858c-9a75e8e7c81d
295 // Don't bother drawing it when the animation has finished, as the focused active indicator
296 // line below will obscure it.
297 pen.setColor(m_outlineColor);
298 painter->setPen(pen);
299 painter->drawLine(0, h, w, h);
300 }
301
302 if (!qFuzzyIsNull(m_focusAnimationProgress)) {
303 // Draw the focused active indicator line (#6) that's at the bottom when it's focused.
304 // Start at the center and expand outwards.
305 const int lineLength = m_focusAnimationProgress * w;
306 const int horizontalCenter = w / 2;
307 pen.setColor(m_focusedOutlineColor);
308 pen.setWidth(2);
309 painter->setPen(pen);
310 painter->drawLine(horizontalCenter - (lineLength / 2), h,
311 horizontalCenter + (lineLength / 2) + pen.width() / 2, h);
312 }
313 }
314}
315
316bool QQuickMaterialTextContainer::shouldAnimateOutline() const
317{
318 return !m_controlHasText && m_placeholderHasText;
319}
320
326QQuickItem *QQuickMaterialTextContainer::textControl() const
327{
329}
330
331void QQuickMaterialTextContainer::controlGotActiveFocus()
332{
333 const bool shouldAnimate = m_filled ? !m_controlHasText : shouldAnimateOutline();
334 if (!shouldAnimate) {
335 // It does have focus, but sometimes we don't need to animate anything, just change colors.
336 if (m_filled && m_controlHasText) {
337 // When a filled container has text already entered, we should just immediately change
338 // the color and thickness of the indicator line.
339 m_focusAnimationProgress = 1;
340 }
341 update();
342 return;
343 }
344
345 startFocusAnimation();
346}
347
348void QQuickMaterialTextContainer::controlLostActiveFocus()
349{
350 // We don't want to animate the active indicator line (at the bottom) of filled containers
351 // when the control loses focus, only when it gets it.
352 if (m_filled || !shouldAnimateOutline()) {
353 // Ensure that we set this so that filled containers go back to a non-accent-colored
354 // active indicator line when losing focus.
355 if (m_filled)
356 m_focusAnimationProgress = 0;
357 update();
358 return;
359 }
360
361 QPropertyAnimation *animation = new QPropertyAnimation(this, "focusAnimationProgress", this);
366}
367
368void QQuickMaterialTextContainer::startFocusAnimation()
369{
370 // Each time setFocusAnimationProgress is called by the animation, it'll call update(),
371 // which will cause us to be re-rendered.
372 QPropertyAnimation *animation = new QPropertyAnimation(this, "focusAnimationProgress", this);
377}
378
379void QQuickMaterialTextContainer::maybeSetFocusAnimationProgress()
380{
381 if (m_filled)
382 return;
383
384 if (m_controlHasText && m_placeholderHasText) {
385 // Show the interrupted outline when there is text.
387 } else if (!m_controlHasText && !m_controlHasActiveFocus) {
388 // If the text was cleared while it didn't have focus, don't animate, just close the gap.
390 }
391}
392
394{
396
397 if (!parentItem())
398 qmlWarning(this) << "Expected parent item by component completion!";
399
400 maybeSetFocusAnimationProgress();
401}
402
void start(QAbstractAnimation::DeletionPolicy policy=KeepWhenStopped)
Starts the animation.
\inmodule QtGui
Definition qbrush.h:30
The QColor class provides colors based on RGB, HSV or CMYK values.
Definition qcolor.h:31
\inmodule QtGui
The QPainter class performs low-level painting on widgets and other paint devices.
Definition qpainter.h:46
void drawPath(const QPainterPath &path)
Draws the given painter path using the current pen for outline and the current brush for filling.
void setPen(const QColor &color)
This is an overloaded member function, provided for convenience. It differs from the above function o...
void drawLine(const QLineF &line)
Draws a line defined by line.
Definition qpainter.h:442
void setBrush(const QBrush &brush)
Sets the painter's brush to the given brush.
@ Antialiasing
Definition qpainter.h:52
void translate(const QPointF &offset)
Translates the coordinate system by the given offset; i.e.
void setRenderHint(RenderHint hint, bool on=true)
Sets the given render hint on the painter if on is true; otherwise clears the render hint.
\inmodule QtGui
Definition qpen.h:25
void setWidth(int width)
Sets the pen width to the given width in pixels with integer precision.
Definition qpen.cpp:618
void setWidthF(qreal width)
Sets the pen width to the given width in pixels with floating point precision.
Definition qpen.cpp:644
int width() const
Returns the pen width with integer precision.
Definition qpen.cpp:586
void setColor(const QColor &color)
Sets the color of this pen's brush to the given color.
Definition qpen.cpp:731
\inmodule QtCore\reentrant
Definition qpoint.h:214
constexpr qreal x() const noexcept
Returns the x coordinate of this point.
Definition qpoint.h:333
constexpr qreal y() const noexcept
Returns the y coordinate of this point.
Definition qpoint.h:338
virtual void componentComplete()=0
Invoked after the root component that caused this instantiation has completed construction.
The QQuickItem class provides the most basic of all visual items in \l {Qt Quick}.
Definition qquickitem.h:64
qreal width
This property holds the width of this item.
Definition qquickitem.h:76
QQuickItem * parentItem() const
QQuickItem * parent
\qmlproperty Item QtQuick::Item::parent This property holds the visual parent of the item.
Definition qquickitem.h:68
qreal height
This property holds the height of this item.
Definition qquickitem.h:77
void update()
Schedules a call to updatePaintNode() for this item.
void setFocusedOutlineColor(const QColor &focusedOutlineColor)
void componentComplete() override
\reimp Derived classes should call the base class method before adding their own actions to perform a...
void setOutlineColor(const QColor &outlineColor)
void setControlHasActiveFocus(bool controlHasActiveFocus)
void setControlHasText(bool controlHasText)
void setHorizontalPadding(int horizontalPadding)
void setPlaceholderTextWidth(qreal placeholderTextWidth)
void setFillColor(const QColor &fillColor)
void setPlaceholderHasText(bool placeholderHasText)
void paint(QPainter *painter) override
This function, which is usually called by the QML Scene Graph, paints the contents of an item in loca...
QQuickMaterialTextContainer(QQuickItem *parent=nullptr)
The QQuickPaintedItem class provides a way to use the QPainter API in the QML Scene Graph.
void setStartValue(const QVariant &value)
void setDuration(int msecs)
void setEndValue(const QVariant &value)
Combined button and popup list for selecting options.
bool qFuzzyCompare(qfloat16 p1, qfloat16 p2) noexcept
Definition qfloat16.h:287
bool qFuzzyIsNull(qfloat16 f) noexcept
Definition qfloat16.h:303
GLfloat GLfloat GLfloat w
[0]
GLfloat GLfloat GLfloat GLfloat h
GLsizei const GLchar *const * path
Q_QML_EXPORT QQmlInfo qmlWarning(const QObject *me)
QQuickItem * qobject_cast< QQuickItem * >(QObject *o)
Definition qquickitem.h:483
#define emit
double qreal
Definition qtypes.h:92
QPropertyAnimation animation
[0]
QPainter painter(this)
[7]
IUIAutomationTreeWalker __RPC__deref_out_opt IUIAutomationElement ** parent