Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
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// Qt-Security score:significant reason:default
4
6
7#include <QtCore/qpropertyanimation.h>
8#include <QtGui/qpainter.h>
9#include <QtGui/qpainterpath.h>
10#include <QtQml/qqmlinfo.h>
11
13
14/*
15 This class exists because:
16
17 - Rectangle doesn't support individual radii for each corner (QTBUG-48774).
18 - We need to draw an interrupted (where the placeholder text is) line for outlined containers.
19 - We need to animate the focus line for filled containers, and we can't use "Behavior on"
20 syntax because we only want to animate activeFocus becoming true, not also false. To do this
21 requires imperative code, and we want to keep the QML declarative.
22
23 focusAnimationProgress has to be a property even though it's only used internally,
24 because we have to use QPropertyAnimation on it.
25
26 An advantage of doing the animation in C++ is that we avoid the memory
27 overhead of an animation instance even when we're not using it, and instead
28 create it on demand and delete it when it's done. I tried doing the animation
29 declaratively with states and transitions, but it was more difficult to implement
30 and would have been harder to maintain, as well as having more overhead.
31*/
32
33QQuickMaterialTextContainer::QQuickMaterialTextContainer(QQuickItem *parent)
35{
36}
37
38bool QQuickMaterialTextContainer::isFilled() const
39{
40 return m_filled;
41}
42
43void QQuickMaterialTextContainer::setFilled(bool filled)
44{
45 if (filled == m_filled)
46 return;
47
48 m_filled = filled;
49 update();
50}
51
52QColor QQuickMaterialTextContainer::fillColor() const
53{
54 return m_fillColor;
55}
56
57void QQuickMaterialTextContainer::setFillColor(const QColor &fillColor)
58{
59 if (fillColor == m_fillColor)
60 return;
61
62 m_fillColor = fillColor;
63 update();
64}
65
66QColor QQuickMaterialTextContainer::outlineColor() const
67{
68 return m_outlineColor;
69}
70
71void QQuickMaterialTextContainer::setOutlineColor(const QColor &outlineColor)
72{
73 if (outlineColor == m_outlineColor)
74 return;
75
76 m_outlineColor = outlineColor;
77 update();
78}
79
80QColor QQuickMaterialTextContainer::focusedOutlineColor() const
81{
82 return m_outlineColor;
83}
84
85void QQuickMaterialTextContainer::setFocusedOutlineColor(const QColor &focusedOutlineColor)
86{
87 if (focusedOutlineColor == m_focusedOutlineColor)
88 return;
89
90 m_focusedOutlineColor = focusedOutlineColor;
91 update();
92}
93
94qreal QQuickMaterialTextContainer::focusAnimationProgress() const
95{
96 return m_focusAnimationProgress;
97}
98
99void QQuickMaterialTextContainer::setFocusAnimationProgress(qreal progress)
100{
101 if (qFuzzyCompare(progress, m_focusAnimationProgress))
102 return;
103
104 m_focusAnimationProgress = progress;
105 update();
106}
107
108qreal QQuickMaterialTextContainer::placeholderTextWidth() const
109{
110 return m_placeholderTextWidth;
111}
112
113void QQuickMaterialTextContainer::setPlaceholderTextWidth(qreal placeholderTextWidth)
114{
115 if (qFuzzyCompare(placeholderTextWidth, m_placeholderTextWidth))
116 return;
117
118 m_placeholderTextWidth = placeholderTextWidth;
119 update();
120}
121
122QQuickMaterialTextContainer::PlaceHolderHAlignment QQuickMaterialTextContainer::placeholderTextHAlign() const
123{
124 return m_placeholderTextHAlign;
125}
126
127void QQuickMaterialTextContainer::setPlaceholderTextHAlign(PlaceHolderHAlignment placeholderTextHAlign)
128{
129 if (m_placeholderTextHAlign == placeholderTextHAlign)
130 return;
131
132 m_placeholderTextHAlign = placeholderTextHAlign;
133 update();
134}
135
136bool QQuickMaterialTextContainer::controlHasActiveFocus() const
137{
138 return m_controlHasActiveFocus;
139}
140
141void QQuickMaterialTextContainer::setControlHasActiveFocus(bool controlHasActiveFocus)
142{
143 if (m_controlHasActiveFocus == controlHasActiveFocus)
144 return;
145
146 m_controlHasActiveFocus = controlHasActiveFocus;
147 if (m_controlHasActiveFocus)
148 controlGotActiveFocus();
149 else
150 controlLostActiveFocus();
151 emit controlHasActiveFocusChanged();
152}
153
154bool QQuickMaterialTextContainer::controlHasText() const
155{
156 return m_controlHasText;
157}
158
159void QQuickMaterialTextContainer::setControlHasText(bool controlHasText)
160{
161 if (m_controlHasText == controlHasText)
162 return;
163
164 m_controlHasText = controlHasText;
165 // TextArea's text length is updated after component completion,
166 // so account for that here and in setPlaceholderHasText().
167 updateFocusAnimation();
168 update();
169 emit controlHasTextChanged();
170}
171
172bool QQuickMaterialTextContainer::placeholderHasText() const
173{
174 return m_placeholderHasText;
175}
176
177void QQuickMaterialTextContainer::setPlaceholderHasText(bool placeholderHasText)
178{
179 if (m_placeholderHasText == placeholderHasText)
180 return;
181
182 m_placeholderHasText = placeholderHasText;
183 updateFocusAnimation();
184 update();
185 emit placeholderHasTextChanged();
186}
187
188int QQuickMaterialTextContainer::horizontalPadding() const
189{
190 return m_horizontalPadding;
191}
192
193/*!
194 \internal
195
196 The text field's horizontal padding.
197
198 We need this to be a property so that the QML can set it, since we can't
199 access QQuickMaterialStyle's C++ API from this plugin.
200*/
201void QQuickMaterialTextContainer::setHorizontalPadding(int horizontalPadding)
202{
203 if (m_horizontalPadding == horizontalPadding)
204 return;
205 m_horizontalPadding = horizontalPadding;
206 update();
207 emit horizontalPaddingChanged();
208}
209
210void QQuickMaterialTextContainer::paint(QPainter *painter)
211{
212 qreal w = width();
213 qreal h = height();
214 if (w <= 0 || h <= 0)
215 return;
216
217 // Account for pen width.
218 const qreal penWidth = m_filled ? 1 : (m_controlHasActiveFocus ? 2 : 1);
219 w -= penWidth;
220 h -= penWidth;
221
222 const qreal cornerRadius = 4;
223 // This is coincidentally the same as cornerRadius, but use different variable names
224 // to keep the code understandable.
225 const qreal gapPadding = 4;
226 // When animating focus on outlined containers, we need to make a gap
227 // at the top left for the placeholder text.
228 // If the text is too wide for the container, it will be elided, so
229 // we shouldn't need to clamp its width here. TODO: check that this is the case for TextArea.
230 const qreal halfPlaceholderWidth = m_placeholderTextWidth / 2;
231 // Take care of different Alignment cases for the placeholder text.
232 qreal gapCenterX;
233 switch (m_placeholderTextHAlign) {
234 case PlaceHolderHAlignment::AlignHCenter:
235 gapCenterX = width() / 2;
236 break;
237 case PlaceHolderHAlignment::AlignRight:
238 gapCenterX = width() - halfPlaceholderWidth - m_horizontalPadding;
239 break;
240 default:
241 gapCenterX = m_horizontalPadding + halfPlaceholderWidth;
242 break;
243 }
244
245 QPainterPath path;
246
247 QPointF startPos;
248
249 // Top-left rounded corner.
250 if (m_filled || m_focusAnimationProgress == 0) {
251 startPos = QPointF(cornerRadius, 0);
252 } else {
253 // Start at the center of the gap and animate outwards towards the left-hand side.
254 // Subtract gapPadding to account for the gap between the line and the placeholder text.
255 // Also subtract the pen width because otherwise it extends by that distance too much to the right.
256 // Changing the cap style to Qt::FlatCap would only fix this by half the pen width,
257 // but it has no effect anyway (perhaps it literally only affects end points and not "start" points?).
258 startPos = QPointF(gapCenterX - (m_focusAnimationProgress * halfPlaceholderWidth) - gapPadding - penWidth, 0);
259 }
260 path.moveTo(startPos);
261 path.arcTo(0, 0, cornerRadius * 2, cornerRadius * 2, 90, 90);
262
263 // Bottom-left corner.
264 if (m_filled) {
265 path.lineTo(0, h);
266 } else {
267 path.lineTo(0, h - cornerRadius * 2);
268 path.arcTo(0, h - cornerRadius * 2, cornerRadius * 2, cornerRadius * 2, 180, 90);
269 }
270
271 // Bottom-right corner.
272 if (m_filled) {
273 path.lineTo(w, h);
274 } else {
275 path.lineTo(w - cornerRadius * 2, h);
276 path.arcTo(w - cornerRadius * 2, h - cornerRadius * 2, cornerRadius * 2, cornerRadius * 2, 270, 90);
277 }
278
279 // Top-right rounded corner.
280 path.lineTo(w, cornerRadius);
281 path.arcTo(w - (cornerRadius * 2), 0, cornerRadius * 2, cornerRadius * 2, 0, 90);
282
283 if (m_filled || qFuzzyIsNull(m_focusAnimationProgress)) {
284 // Back to the start.
285 path.lineTo(startPos.x(), startPos.y());
286 } else {
287 path.lineTo(gapCenterX + (m_focusAnimationProgress * halfPlaceholderWidth) + gapPadding, startPos.y());
288 }
289
290 // Account for pen width.
291 painter->translate(penWidth / 2, penWidth / 2);
292
293 painter->setRenderHint(QPainter::Antialiasing, true);
294
295 auto control = textControl();
296 const bool focused = control && control->hasActiveFocus();
297 // We still want to draw the stroke when it's filled, otherwise it will be a pixel
298 // (the pen width) too narrow on either side.
299 QPen pen;
300 pen.setColor(m_filled ? m_fillColor : (focused ? m_focusedOutlineColor : m_outlineColor));
301 pen.setWidthF(penWidth);
302 painter->setPen(pen);
303 if (m_filled)
304 painter->setBrush(m_fillColor);
305
306 // Fill or stroke the container's shape.
307 // If not filling, the default brush will be used, which is Qt::NoBrush.
308 painter->drawPath(path);
309
310 // Draw the focus line at the bottom for filled containers.
311 if (m_filled) {
312 if (!qFuzzyCompare(m_focusAnimationProgress, qreal(1.0))) {
313 // Draw the enabled active indicator line (#10) that's at the bottom when it's not focused:
314 // https://m3.material.io/components/text-fields/specs#6d654d1d-262e-4697-858c-9a75e8e7c81d
315 // Don't bother drawing it when the animation has finished, as the focused active indicator
316 // line below will obscure it.
317 pen.setColor(m_outlineColor);
318 painter->setPen(pen);
319 painter->drawLine(0, h, w, h);
320 }
321
322 if (!qFuzzyIsNull(m_focusAnimationProgress)) {
323 // Draw the focused active indicator line (#6) that's at the bottom when it's focused.
324 // Start at the center and expand outwards.
325 const int lineLength = m_focusAnimationProgress * w;
326 const int horizontalCenter = w / 2;
327 pen.setColor(m_focusedOutlineColor);
328 pen.setWidth(2);
329 painter->setPen(pen);
330 painter->drawLine(horizontalCenter - (lineLength / 2), h,
331 horizontalCenter + (lineLength / 2) + pen.width() / 2, h);
332 }
333 }
334}
335
336bool QQuickMaterialTextContainer::shouldAnimateOutline() const
337{
338 return !m_controlHasText && m_placeholderHasText;
339}
340
341/*!
342 \internal
343
344 \sa QQuickPlaceholderText::textControl().
345*/
346QQuickItem *QQuickMaterialTextContainer::textControl() const
347{
348 return qobject_cast<QQuickItem *>(parent());
349}
350
351void QQuickMaterialTextContainer::controlGotActiveFocus()
352{
353 if (m_focusAnimation) {
354 m_focusAnimation->stop();
355 m_focusAnimation.clear();
356 }
357 const bool shouldAnimate = m_filled ? !m_controlHasText : shouldAnimateOutline();
358 if (!shouldAnimate) {
359 // It does have focus, but sometimes we don't need to animate anything, just change colors.
360 if (m_filled && m_controlHasText) {
361 // When a filled container has text already entered, we should just immediately change
362 // the color and thickness of the indicator line.
363 m_focusAnimationProgress = 1;
364 }
365 update();
366 return;
367 }
368
369 updateFocusAnimation(true);
370}
371
372void QQuickMaterialTextContainer::controlLostActiveFocus()
373{
374 if (m_focusAnimation) {
375 m_focusAnimation->stop();
376 m_focusAnimation.clear();
377 }
378 // We don't want to animate the active indicator line (at the bottom) of filled containers
379 // when the control loses focus, only when it gets it.
380 if (m_filled || !shouldAnimateOutline()) {
381 // Ensure that we set this so that filled containers go back to a non-accent-colored
382 // active indicator line when losing focus.
383 if (m_filled)
384 m_focusAnimationProgress = 0;
385 update();
386 return;
387 }
388
389 updateFocusAnimation(true);
390}
391
392void QQuickMaterialTextContainer::updateFocusAnimation(bool createIfNeeded)
393{
394 if (m_filled) {
395 if (m_focusAnimation) {
396 m_focusAnimation->stop();
397 m_focusAnimation.clear();
398 }
399 return;
400 }
401
402 int focusAnimationProgressValue = 0;
403 if (m_controlHasText && m_placeholderHasText) {
404 // Show the interrupted outline when there is text.
405 focusAnimationProgressValue = 1;
406 } else if (!m_controlHasText) {
407 if (m_controlHasActiveFocus && m_placeholderHasText)
408 focusAnimationProgressValue = 1;
409 else
410 focusAnimationProgressValue = 0;
411 }
412
413 if (m_focusAnimation || createIfNeeded) {
414 int duration = 300;
415 if (m_focusAnimation) {
416 duration = m_focusAnimation->totalDuration() - m_focusAnimation->currentTime();
417 m_focusAnimation->stop();
418 }
419 m_focusAnimation = new QPropertyAnimation(this, "focusAnimationProgress", this);
420 m_focusAnimation->setDuration(duration);
421 m_focusAnimation->setStartValue(focusAnimationProgress());
422 m_focusAnimation->setEndValue(focusAnimationProgressValue);
423 m_focusAnimation->start(QAbstractAnimation::DeleteWhenStopped);
424 } else {
425 setFocusAnimationProgress(focusAnimationProgressValue);
426 }
427}
428
429void QQuickMaterialTextContainer::componentComplete()
430{
431 QQuickPaintedItem::componentComplete();
432
433 if (!parentItem())
434 qmlWarning(this) << "Expected parent item by component completion!";
435
436 updateFocusAnimation();
437}
438
439QT_END_NAMESPACE
QObject * parent
Definition qobject.h:73
The QQuickItem class provides the most basic of all visual items in \l {Qt Quick}.
Definition qquickitem.h:64