diff options
author | Mitch Curtis <mitch.curtis@qt.io> | 2022-12-28 14:36:32 +0800 |
---|---|---|
committer | Qt Cherry-pick Bot <cherrypick_bot@qt-project.org> | 2023-02-16 22:26:44 +0000 |
commit | a62dd478564f2f5e1fe9bd006551239c7b820073 (patch) | |
tree | c80acdb63fcb115bf738a6fc09cbf56338bd98aa | |
parent | 19bafadc4840c7ed1f77e632821fcb732c0b93c9 (diff) |
Update Material TextField to Material 3
Fixes: QTBUG-72554
Fixes: QTBUG-109218
Change-Id: I0bc6fc3d16630352dcd5c58c5dd2b1bf794741c5
Reviewed-by: Oliver Eftevaag <oliver.eftevaag@qt.io>
(cherry picked from commit 20e3d1b522d1b79239e9ac4a6af47ce3648512bd)
Reviewed-by: Qt Cherry-pick Bot <cherrypick_bot@qt-project.org>
13 files changed, 1002 insertions, 24 deletions
diff --git a/src/quickcontrols/material/TextField.qml b/src/quickcontrols/material/TextField.qml index 598f66f938..e2ec69dc27 100644 --- a/src/quickcontrols/material/TextField.qml +++ b/src/quickcontrols/material/TextField.qml @@ -13,11 +13,17 @@ T.TextField { implicitWidth: implicitBackgroundWidth + leftInset + rightInset || Math.max(contentWidth, placeholder.implicitWidth) + leftPadding + rightPadding implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, - contentHeight + topPadding + bottomPadding, - placeholder.implicitHeight + topPadding + bottomPadding) + contentHeight + topPadding + bottomPadding) - topPadding: 8 - bottomPadding: 16 + leftPadding: Material.textFieldHorizontalPadding + rightPadding: Material.textFieldHorizontalPadding + // Need to account for the placeholder text when it's sitting on top. + topPadding: Material.containerStyle === Material.Filled + ? placeholderText.length > 0 && (activeFocus || length > 0) + ? Material.textFieldVerticalPadding + placeholder.largestHeight + : Material.textFieldVerticalPadding + : Material.textFieldVerticalPadding + bottomPadding: Material.textFieldVerticalPadding color: enabled ? Material.foreground : Material.hintTextColor selectionColor: Material.accentColor @@ -25,28 +31,41 @@ T.TextField { placeholderTextColor: Material.hintTextColor verticalAlignment: TextInput.AlignVCenter + Material.containerStyle: Material.Outlined + cursorDelegate: CursorDelegate { } - PlaceholderText { + FloatingPlaceholderText { id: placeholder x: control.leftPadding - y: control.topPadding width: control.width - (control.leftPadding + control.rightPadding) - height: control.height - (control.topPadding + control.bottomPadding) text: control.placeholderText font: control.font color: control.placeholderTextColor - verticalAlignment: control.verticalAlignment elide: Text.ElideRight renderType: control.renderType - visible: !control.length && !control.preeditText && (!control.activeFocus || control.horizontalAlignment !== Qt.AlignHCenter) + + filled: control.Material.containerStyle === Material.Filled + verticalPadding: control.Material.textFieldVerticalPadding + controlHasActiveFocus: control.activeFocus + controlHasText: control.length > 0 + controlImplicitBackgroundHeight: control.implicitBackgroundHeight } - background: Rectangle { - y: control.height - height - control.bottomPadding + 8 + background: MaterialTextContainer { implicitWidth: 120 - height: control.activeFocus || (enabled && control.hovered) ? 2 : 1 - color: control.activeFocus ? control.Material.accentColor - : ((enabled && control.hovered) ? control.Material.primaryTextColor : control.Material.hintTextColor) + implicitHeight: control.Material.textFieldHeight + + filled: control.Material.containerStyle === Material.Filled + fillColor: control.Material.textFieldFilledContainerColor + outlineColor: (enabled && control.hovered) ? control.Material.primaryTextColor : control.Material.hintTextColor + focusedOutlineColor: control.Material.accentColor + // When the control's size is set larger than its implicit size, use whatever size is smaller + // so that the gap isn't too big. + placeholderTextWidth: Math.min(placeholder.width, placeholder.implicitWidth) * placeholder.scale + controlHasActiveFocus: control.activeFocus + controlHasText: control.length > 0 + placeholderHasText: placeholder.text.length > 0 + horizontalPadding: control.Material.textFieldHorizontalPadding } } diff --git a/src/quickcontrols/material/impl/CMakeLists.txt b/src/quickcontrols/material/impl/CMakeLists.txt index ebf3e1cbf3..abae353f44 100644 --- a/src/quickcontrols/material/impl/CMakeLists.txt +++ b/src/quickcontrols/material/impl/CMakeLists.txt @@ -28,8 +28,10 @@ qt_internal_add_qml_module(qtquickcontrols2materialstyleimplplugin NO_PLUGIN_OPTIONAL SOURCES qquickmaterialbusyindicator.cpp qquickmaterialbusyindicator_p.h + qquickmaterialplaceholdertext.cpp qquickmaterialplaceholdertext_p.h qquickmaterialprogressbar.cpp qquickmaterialprogressbar_p.h qquickmaterialripple.cpp qquickmaterialripple_p.h + qquickmaterialtextcontainer.cpp qquickmaterialtextcontainer_p.h QML_FILES ${qml_files} DEFINES @@ -38,8 +40,10 @@ qt_internal_add_qml_module(qtquickcontrols2materialstyleimplplugin LIBRARIES Qt::CorePrivate Qt::Gui + Qt::Qml Qt::QmlPrivate Qt::QuickControls2ImplPrivate + Qt::Quick Qt::QuickPrivate Qt::QuickTemplates2Private ) diff --git a/src/quickcontrols/material/impl/qquickmaterialplaceholdertext.cpp b/src/quickcontrols/material/impl/qquickmaterialplaceholdertext.cpp new file mode 100644 index 0000000000..b908a62fc0 --- /dev/null +++ b/src/quickcontrols/material/impl/qquickmaterialplaceholdertext.cpp @@ -0,0 +1,245 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qquickmaterialplaceholdertext_p.h" + +#include <QtCore/qpropertyanimation.h> +#include <QtCore/qparallelanimationgroup.h> +#include <QtGui/qpainter.h> +#include <QtGui/qpainterpath.h> +#include <QtQml/qqmlinfo.h> +#include <QtQuickTemplates2/private/qquicktheme_p.h> + +QT_BEGIN_NAMESPACE + +static const qreal floatingScale = 0.8; +Q_GLOBAL_STATIC(QEasingCurve, animationEasingCurve, QEasingCurve::OutSine); + +/* + This class makes it easier to animate the various placeholder text changes + for each type of text container (filled, outlined). + + By doing animations in C++, we avoid having a bunch of states, transitions, + and animations (which are all QObjects) declared in QML, even if that text + control never gets focus and hence never needs them. +*/ + +QQuickMaterialPlaceholderText::QQuickMaterialPlaceholderText(QQuickItem *parent) + : QQuickPlaceholderText(parent) +{ + // Ensure that scaling happens on the left side, at the vertical center. + setTransformOrigin(QQuickItem::Left); +} + +bool QQuickMaterialPlaceholderText::isFilled() const +{ + return m_filled; +} + +void QQuickMaterialPlaceholderText::setFilled(bool filled) +{ + if (filled == m_filled) + return; + + m_filled = filled; + update(); + void filledChanged(); +} + +bool QQuickMaterialPlaceholderText::controlHasActiveFocus() const +{ + return m_controlHasActiveFocus; +} + +void QQuickMaterialPlaceholderText::setControlHasActiveFocus(bool controlHasActiveFocus) +{ + if (m_controlHasActiveFocus == controlHasActiveFocus) + return; + + m_controlHasActiveFocus = controlHasActiveFocus; + if (m_controlHasActiveFocus) + controlGotActiveFocus(); + else + controlLostActiveFocus(); + emit controlHasActiveFocusChanged(); +} + +bool QQuickMaterialPlaceholderText::controlHasText() const +{ + return m_controlHasText; +} + +void QQuickMaterialPlaceholderText::setControlHasText(bool controlHasText) +{ + if (m_controlHasText == controlHasText) + return; + + m_controlHasText = controlHasText; + maybeSetFocusAnimationProgress(); + emit controlHasTextChanged(); +} + +/* + Placeholder text of outlined text fields should float when: + - There is placeholder text, and + - The control has active focus, or + - The control has text +*/ +bool QQuickMaterialPlaceholderText::shouldFloat() const +{ + const bool controlHasActiveFocusOrText = m_controlHasActiveFocus || m_controlHasText; + return m_filled + ? controlHasActiveFocusOrText + : !text().isEmpty() && controlHasActiveFocusOrText; +} + +bool QQuickMaterialPlaceholderText::shouldAnimate() const +{ + return m_filled + ? !m_controlHasText + : !m_controlHasText && !text().isEmpty(); +} + +qreal QQuickMaterialPlaceholderText::normalTargetY() const +{ + // When the placeholder text shouldn't float, it should sit in the middle of the TextField. + // We could just use the control's height minus our height instead of the members, but + // that doesn't work for TextArea, which can be multiple lines in height and hence taller. + // In that case, we want the placeholder text to sit in the middle of its default-height (one-line). + return (m_controlImplicitBackgroundHeight - m_largestHeight) / 2.0; +} + +qreal QQuickMaterialPlaceholderText::floatingTargetY() const +{ + // For filled text fields, the placeholder text sits just above + // the text when floating. + if (m_filled) + return m_verticalPadding; + + // Outlined text fields have the placeaholder vertically centered + // along the outline at the top. + return -m_largestHeight / 2; +} + +/*! + \internal + + The height of the text at its largest size that we set. +*/ +int QQuickMaterialPlaceholderText::largestHeight() const +{ + return m_largestHeight; +} + +qreal QQuickMaterialPlaceholderText::controlImplicitBackgroundHeight() const +{ + return m_controlImplicitBackgroundHeight; +} + +void QQuickMaterialPlaceholderText::setControlImplicitBackgroundHeight(qreal controlImplicitBackgroundHeight) +{ + if (qFuzzyCompare(m_controlImplicitBackgroundHeight, controlImplicitBackgroundHeight)) + return; + + m_controlImplicitBackgroundHeight = controlImplicitBackgroundHeight; + setY(shouldFloat() ? floatingTargetY() : normalTargetY()); + emit controlImplicitBackgroundHeightChanged(); +} + +qreal QQuickMaterialPlaceholderText::verticalPadding() const +{ + return m_verticalPadding; +} + +void QQuickMaterialPlaceholderText::setVerticalPadding(qreal verticalPadding) +{ + if (qFuzzyCompare(m_verticalPadding, verticalPadding)) + return; + + m_verticalPadding = verticalPadding; + emit verticalPaddingChanged(); +} + +void QQuickMaterialPlaceholderText::controlGotActiveFocus() +{ + if (m_focusOutAnimation) + m_focusOutAnimation->stop(); + + Q_ASSERT(!m_focusInAnimation); + if (shouldAnimate()) { + m_focusInAnimation = new QParallelAnimationGroup(this); + + QPropertyAnimation *yAnimation = new QPropertyAnimation(this, "y", this); + yAnimation->setDuration(300); + yAnimation->setStartValue(y()); + yAnimation->setEndValue(floatingTargetY()); + yAnimation->setEasingCurve(*animationEasingCurve); + m_focusInAnimation->addAnimation(yAnimation); + + auto *scaleAnimation = new QPropertyAnimation(this, "scale", this); + scaleAnimation->setDuration(300); + scaleAnimation->setStartValue(1); + scaleAnimation->setEndValue(floatingScale); + yAnimation->setEasingCurve(*animationEasingCurve); + m_focusInAnimation->addAnimation(scaleAnimation); + + m_focusInAnimation->start(QAbstractAnimation::DeleteWhenStopped); + } else { + const int newY = shouldFloat() ? floatingTargetY() : normalTargetY(); + setY(newY); + } +} + +void QQuickMaterialPlaceholderText::controlLostActiveFocus() +{ + Q_ASSERT(!m_focusOutAnimation); + if (shouldAnimate()) { + m_focusOutAnimation = new QParallelAnimationGroup(this); + + auto *yAnimation = new QPropertyAnimation(this, "y", this); + yAnimation->setDuration(300); + yAnimation->setStartValue(y()); + yAnimation->setEndValue(normalTargetY()); + yAnimation->setEasingCurve(*animationEasingCurve); + m_focusOutAnimation->addAnimation(yAnimation); + + auto *scaleAnimation = new QPropertyAnimation(this, "scale", this); + scaleAnimation->setDuration(300); + scaleAnimation->setStartValue(floatingScale); + scaleAnimation->setEndValue(1); + yAnimation->setEasingCurve(*animationEasingCurve); + m_focusOutAnimation->addAnimation(scaleAnimation); + + m_focusOutAnimation->start(QAbstractAnimation::DeleteWhenStopped); + } else { + const int newY = shouldFloat() ? floatingTargetY() : normalTargetY(); + setY(newY); + } +} + +void QQuickMaterialPlaceholderText::maybeSetFocusAnimationProgress() +{ + const bool shouldWeFloat = shouldFloat(); + setY(shouldWeFloat ? floatingTargetY() : normalTargetY()); + setScale(shouldWeFloat ? floatingScale : 1.0); +} + +void QQuickMaterialPlaceholderText::componentComplete() +{ + QQuickPlaceholderText::componentComplete(); + + if (!parentItem()) + qmlWarning(this) << "Expected parent item by component completion!"; + + m_largestHeight = implicitHeight(); + if (m_largestHeight > 0) { + emit largestHeightChanged(); + } else { + qmlWarning(this) << "Expected implicitHeight of placeholder text" << text() + << "to be greater than 0 by component completion!"; + } + + maybeSetFocusAnimationProgress(); +} + +QT_END_NAMESPACE diff --git a/src/quickcontrols/material/impl/qquickmaterialplaceholdertext_p.h b/src/quickcontrols/material/impl/qquickmaterialplaceholdertext_p.h new file mode 100644 index 0000000000..525e079c28 --- /dev/null +++ b/src/quickcontrols/material/impl/qquickmaterialplaceholdertext_p.h @@ -0,0 +1,94 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QQUICKMATERIALPLACEHOLDERTEXT_P_H +#define QQUICKMATERIALPLACEHOLDERTEXT_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtCore/private/qglobal_p.h> +#include <QtGui/qcolor.h> +#include <QtQuickControls2Impl/private/qquickplaceholdertext_p.h> + +QT_BEGIN_NAMESPACE + +class QParallelAnimationGroup; + +class QQuickMaterialPlaceholderText : public QQuickPlaceholderText +{ + Q_OBJECT + Q_PROPERTY(bool filled READ isFilled WRITE setFilled NOTIFY filledChanged FINAL) + Q_PROPERTY(bool controlHasActiveFocus READ controlHasActiveFocus + WRITE setControlHasActiveFocus NOTIFY controlHasActiveFocusChanged FINAL) + Q_PROPERTY(bool controlHasText READ controlHasText WRITE setControlHasText NOTIFY controlHasTextChanged FINAL) + Q_PROPERTY(int largestHeight READ largestHeight NOTIFY largestHeightChanged FINAL) + Q_PROPERTY(qreal verticalPadding READ verticalPadding WRITE setVerticalPadding NOTIFY verticalPaddingChanged FINAL) + Q_PROPERTY(qreal controlImplicitBackgroundHeight READ controlImplicitBackgroundHeight + WRITE setControlImplicitBackgroundHeight NOTIFY controlImplicitBackgroundHeightChanged FINAL) + QML_NAMED_ELEMENT(FloatingPlaceholderText) + QML_ADDED_IN_VERSION(6, 5) + +public: + explicit QQuickMaterialPlaceholderText(QQuickItem *parent = nullptr); + + bool isFilled() const; + void setFilled(bool filled); + + int largestHeight() const; + + bool controlHasActiveFocus() const; + void setControlHasActiveFocus(bool controlHasActiveFocus); + + bool controlHasText() const; + void setControlHasText(bool controlHasText); + + qreal controlImplicitBackgroundHeight() const; + void setControlImplicitBackgroundHeight(qreal controlImplicitBackgroundHeight); + + qreal verticalPadding() const; + void setVerticalPadding(qreal verticalPadding); + +signals: + void filledChanged(); + void largestHeightChanged(); + void controlHasActiveFocusChanged(); + void controlHasTextChanged(); + void controlImplicitBackgroundHeightChanged(); + void verticalPaddingChanged(); + +private: + bool shouldFloat() const; + bool shouldAnimate() const; + + qreal normalTargetY() const; + qreal floatingTargetY() const; + + void controlGotActiveFocus(); + void controlLostActiveFocus(); + + void maybeSetFocusAnimationProgress(); + + void componentComplete() override; + + bool m_filled = false; + bool m_controlHasActiveFocus = false; + bool m_controlHasText = false; + int m_largestHeight = 0; + qreal m_verticalPadding = 0; + qreal m_controlImplicitBackgroundHeight = 0; + QPointer<QParallelAnimationGroup> m_focusInAnimation; + QPointer<QParallelAnimationGroup> m_focusOutAnimation; +}; + +QT_END_NAMESPACE + +#endif // QQUICKMATERIALPLACEHOLDERTEXT_P_H diff --git a/src/quickcontrols/material/impl/qquickmaterialtextcontainer.cpp b/src/quickcontrols/material/impl/qquickmaterialtextcontainer.cpp new file mode 100644 index 0000000000..2751efaa66 --- /dev/null +++ b/src/quickcontrols/material/impl/qquickmaterialtextcontainer.cpp @@ -0,0 +1,385 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qquickmaterialtextcontainer_p.h" + +#include <QtCore/qpropertyanimation.h> +#include <QtGui/qpainter.h> +#include <QtGui/qpainterpath.h> +#include <QtQml/qqmlinfo.h> + +QT_BEGIN_NAMESPACE + +/* + This class exists because: + + - Rectangle doesn't support individual radii for each corner (QTBUG-48774). + - We need to draw an interrupted (where the placeholder text is) line for outlined containers. + - We need to animate the focus line for filled containers, and we can't use "Behavior on" + syntax because we only want to animate activeFocus becoming true, not also false. To do this + requires imperative code, and we want to keep the QML declarative. + + focusAnimationProgress has to be a property even though it's only used internally, + because we have to use QPropertyAnimation on it. + + An advantage of doing the animation in C++ is that we avoid the memory + overhead of an animation instance even when we're not using it, and instead + create it on demand and delete it when it's done. I tried doing the animation + declaratively with states and transitions, but it was more difficult to implement + and would have been harder to maintain, as well as having more overhead. +*/ + +QQuickMaterialTextContainer::QQuickMaterialTextContainer(QQuickItem *parent) + : QQuickPaintedItem(parent) +{ +} + +bool QQuickMaterialTextContainer::isFilled() const +{ + return m_filled; +} + +void QQuickMaterialTextContainer::setFilled(bool filled) +{ + if (filled == m_filled) + return; + + m_filled = filled; + update(); +} + +QColor QQuickMaterialTextContainer::fillColor() const +{ + return m_fillColor; +} + +void QQuickMaterialTextContainer::setFillColor(const QColor &fillColor) +{ + if (fillColor == m_fillColor) + return; + + m_fillColor = fillColor; + update(); +} + +QColor QQuickMaterialTextContainer::outlineColor() const +{ + return m_outlineColor; +} + +void QQuickMaterialTextContainer::setOutlineColor(const QColor &outlineColor) +{ + if (outlineColor == m_outlineColor) + return; + + m_outlineColor = outlineColor; + update(); +} + +QColor QQuickMaterialTextContainer::focusedOutlineColor() const +{ + return m_outlineColor; +} + +void QQuickMaterialTextContainer::setFocusedOutlineColor(const QColor &focusedOutlineColor) +{ + if (focusedOutlineColor == m_focusedOutlineColor) + return; + + m_focusedOutlineColor = focusedOutlineColor; + update(); +} + +qreal QQuickMaterialTextContainer::focusAnimationProgress() const +{ + return m_focusAnimationProgress; +} + +void QQuickMaterialTextContainer::setFocusAnimationProgress(qreal progress) +{ + if (qFuzzyCompare(progress, m_focusAnimationProgress)) + return; + + m_focusAnimationProgress = progress; + update(); +} + +qreal QQuickMaterialTextContainer::placeholderTextWidth() const +{ + return m_placeholderTextWidth; +} + +void QQuickMaterialTextContainer::setPlaceholderTextWidth(qreal placeholderTextWidth) +{ + if (qFuzzyCompare(placeholderTextWidth, m_placeholderTextWidth)) + return; + + m_placeholderTextWidth = placeholderTextWidth; + update(); +} + +bool QQuickMaterialTextContainer::controlHasActiveFocus() const +{ + return m_controlHasActiveFocus; +} + +void QQuickMaterialTextContainer::setControlHasActiveFocus(bool controlHasActiveFocus) +{ + if (m_controlHasActiveFocus == controlHasActiveFocus) + return; + + m_controlHasActiveFocus = controlHasActiveFocus; + if (m_controlHasActiveFocus) + controlGotActiveFocus(); + else + controlLostActiveFocus(); + emit controlHasActiveFocusChanged(); +} + +bool QQuickMaterialTextContainer::controlHasText() const +{ + return m_controlHasText; +} + +void QQuickMaterialTextContainer::setControlHasText(bool controlHasText) +{ + if (m_controlHasText == controlHasText) + return; + + m_controlHasText = controlHasText; + // TextArea's text length is updated after component completion, + // so account for that here and in setPlaceholderHasText(). + maybeSetFocusAnimationProgress(); + update(); + emit controlHasTextChanged(); +} + +bool QQuickMaterialTextContainer::placeholderHasText() const +{ + return m_placeholderHasText; +} + +void QQuickMaterialTextContainer::setPlaceholderHasText(bool placeholderHasText) +{ + if (m_placeholderHasText == placeholderHasText) + return; + + m_placeholderHasText = placeholderHasText; + maybeSetFocusAnimationProgress(); + update(); + emit placeholderHasTextChanged(); +} + +int QQuickMaterialTextContainer::horizontalPadding() const +{ + return m_horizontalPadding; +} + +/*! + \internal + + The text field's horizontal padding. + + We need this to be a property so that the QML can set it, since we can't + access QQuickMaterialStyle's C++ API from this plugin. +*/ +void QQuickMaterialTextContainer::setHorizontalPadding(int horizontalPadding) +{ + if (m_horizontalPadding == horizontalPadding) + return; + m_horizontalPadding = horizontalPadding; + update(); + emit horizontalPaddingChanged(); +} + +void QQuickMaterialTextContainer::paint(QPainter *painter) +{ + qreal w = width(); + qreal h = height(); + if (w <= 0 || h <= 0) + return; + + // Account for pen width. + const qreal penWidth = m_filled ? 1 : (m_controlHasActiveFocus ? 2 : 1); + w -= penWidth; + h -= penWidth; + + const qreal cornerRadius = 4; + // This is coincidentally the same as cornerRadius, but use different variable names + // to keep the code understandable. + const qreal gapPadding = 4; + QPainterPath path; + + QPointF startPos; + + // Top-left rounded corner. + if (m_filled || m_focusAnimationProgress == 0) { + startPos = QPointF(cornerRadius, 0); + } else { + // When animating focus on outlined containers, we need to make a gap + // at the top left for the placeholder text. + // If the text is too wide for the container, it will be elided, so + // we shouldn't need to clamp its width here. TODO: check that this is the case for TextArea. + const qreal halfPlaceholderWidth = m_placeholderTextWidth / 2; + // Left padding plus half of the placeholder text gives the center of the placeholder text gap. + // Note that the placeholder gap is always aligned to the left side of the TextField, + // not the center, so we can't just use half the container's width. + const qreal gapCenterX = m_horizontalPadding + halfPlaceholderWidth; + // Start at the center of the gap and animate outwards towards the left-hand side. + // Subtract gapPadding to account for the gap between the line and the placeholder text. + // Also subtract the pen width because otherwise it extends by that distance too much to the right. + // Changing the cap style to Qt::FlatCap would only fix this by half the pen width, + // but it has no effect anyway (perhaps it literally only affects end points and not "start" points?). + startPos = QPointF(gapCenterX - (m_focusAnimationProgress * halfPlaceholderWidth) - gapPadding - penWidth, 0); + } + path.moveTo(startPos); + path.arcTo(0, 0, cornerRadius * 2, cornerRadius * 2, 90, 90); + + // Bottom-left corner. + if (m_filled) { + path.lineTo(0, h); + } else { + path.lineTo(0, h - cornerRadius * 2); + path.arcTo(0, h - cornerRadius * 2, cornerRadius * 2, cornerRadius * 2, 180, 90); + } + + // Bottom-right corner. + if (m_filled) { + path.lineTo(w, h); + } else { + path.lineTo(w - cornerRadius * 2, h); + path.arcTo(w - cornerRadius * 2, h - cornerRadius * 2, cornerRadius * 2, cornerRadius * 2, 270, 90); + } + + // Top-right rounded corner. + path.lineTo(w, cornerRadius); + path.arcTo(w - (cornerRadius * 2), 0, cornerRadius * 2, cornerRadius * 2, 0, 90); + + if (m_filled || qFuzzyIsNull(m_focusAnimationProgress)) { + // Back to the start. + path.lineTo(startPos.x(), startPos.y()); + } else { + // Go to the end (right-hand side) of the gap. + const qreal halfPlaceholderWidth = (/*placeholderTextGap * 2 + */m_placeholderTextWidth) / 2; + const qreal gapCenterX = m_horizontalPadding + halfPlaceholderWidth; + // Just "+ placeholderTextGap" should be enough to get us to the correct place - not + // sure why doubling it is necessary... + path.lineTo(gapCenterX + (m_focusAnimationProgress * halfPlaceholderWidth) + gapPadding, startPos.y()); + } + + // Account for pen width. + painter->translate(penWidth / 2, penWidth / 2); + + painter->setRenderHint(QPainter::Antialiasing, true); + + const bool focused = parentItem() && parentItem()->hasActiveFocus(); + // We still want to draw the stroke when it's filled, otherwise it will be a pixel + // (the pen width) too narrow on either side. + QPen pen; + pen.setColor(m_filled ? m_fillColor : (focused ? m_focusedOutlineColor : m_outlineColor)); + pen.setWidthF(penWidth); + painter->setPen(pen); + if (m_filled) + painter->setBrush(QBrush(m_fillColor)); + + // Fill or stroke the container's shape. + // If not filling, the default brush will be used, which is Qt::NoBrush. + painter->drawPath(path); + + // Draw the focus line at the bottom for filled containers. + if (m_filled) { + if (!qFuzzyCompare(m_focusAnimationProgress, 1.0)) { + // Draw the enabled active indicator line (#10) that's at the bottom when it's not focused: + // https://m3.material.io/components/text-fields/specs#6d654d1d-262e-4697-858c-9a75e8e7c81d + // Don't bother drawing it when the animation has finished, as the focused active indicator + // line below will obscure it. + pen.setColor(m_outlineColor); + painter->setPen(pen); + painter->drawLine(0, h, w, h); + } + + if (!qFuzzyIsNull(m_focusAnimationProgress)) { + // Draw the focused active indicator line (#6) that's at the bottom when it's focused. + // Start at the center and expand outwards. + const int lineLength = m_focusAnimationProgress * w; + const int horizontalCenter = w / 2; + pen.setColor(m_focusedOutlineColor); + pen.setWidth(2); + painter->setPen(pen); + painter->drawLine(horizontalCenter - (lineLength / 2), h, + horizontalCenter + (lineLength / 2) + pen.width() / 2, h); + } + } +} + +bool QQuickMaterialTextContainer::shouldAnimateOutline() const +{ + return !m_controlHasText && m_placeholderHasText; +} + +void QQuickMaterialTextContainer::controlGotActiveFocus() +{ + const bool shouldAnimate = m_filled ? !m_controlHasText : shouldAnimateOutline(); + if (!shouldAnimate) { + // It does have focus, but sometimes we don't need to animate anything, just change colors. + if (m_filled && m_controlHasText) { + // When a filled container has text already entered, we should just immediately change + // the color and thickness of the indicator line. + m_focusAnimationProgress = 1; + } + update(); + return; + } + + startFocusAnimation(); +} + +void QQuickMaterialTextContainer::controlLostActiveFocus() +{ + // We don't want to animate the active indicator line (at the bottom) of filled containers + // when the control loses focus, only when it gets it. + if (m_filled || !shouldAnimateOutline()) { + // Ensure that we set this so that filled containers go back to a non-accent-colored + // active indicator line when losing focus. + if (m_filled) + m_focusAnimationProgress = 0; + update(); + return; + } + + QPropertyAnimation *animation = new QPropertyAnimation(this, "focusAnimationProgress", this); + animation->setDuration(300); + animation->setStartValue(1); + animation->setEndValue(0); + animation->start(QAbstractAnimation::DeleteWhenStopped); +} + +void QQuickMaterialTextContainer::startFocusAnimation() +{ + // Each time setFocusAnimationProgress is called by the animation, it'll call update(), + // which will cause us to be re-rendered. + QPropertyAnimation *animation = new QPropertyAnimation(this, "focusAnimationProgress", this); + animation->setDuration(300); + animation->setStartValue(0); + animation->setEndValue(1); + animation->start(QAbstractAnimation::DeleteWhenStopped); +} + +void QQuickMaterialTextContainer::maybeSetFocusAnimationProgress() +{ + // Show the interrupted outline when there is text. + if (!m_filled && m_controlHasText && m_placeholderHasText) + setFocusAnimationProgress(1); +} + +void QQuickMaterialTextContainer::componentComplete() +{ + QQuickPaintedItem::componentComplete(); + + if (!parentItem()) + qmlWarning(this) << "Expected parent item by component completion!"; + + maybeSetFocusAnimationProgress(); +} + +QT_END_NAMESPACE diff --git a/src/quickcontrols/material/impl/qquickmaterialtextcontainer_p.h b/src/quickcontrols/material/impl/qquickmaterialtextcontainer_p.h new file mode 100644 index 0000000000..40fcff148b --- /dev/null +++ b/src/quickcontrols/material/impl/qquickmaterialtextcontainer_p.h @@ -0,0 +1,108 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QQUICKMATERIALTEXTCONTAINER_P_H +#define QQUICKMATERIALTEXTCONTAINER_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtCore/private/qglobal_p.h> +#include <QtGui/qcolor.h> +#include <QtQuick/qquickpainteditem.h> + +QT_BEGIN_NAMESPACE + +class QQuickMaterialTextContainer : public QQuickPaintedItem +{ + Q_OBJECT + Q_PROPERTY(bool filled READ isFilled WRITE setFilled FINAL) + Q_PROPERTY(bool controlHasActiveFocus READ controlHasActiveFocus + WRITE setControlHasActiveFocus NOTIFY controlHasActiveFocusChanged FINAL) + Q_PROPERTY(QColor fillColor READ fillColor WRITE setFillColor FINAL) + Q_PROPERTY(QColor outlineColor READ outlineColor WRITE setOutlineColor FINAL) + Q_PROPERTY(QColor focusedOutlineColor READ focusedOutlineColor WRITE setFocusedOutlineColor FINAL) + Q_PROPERTY(qreal focusAnimationProgress READ focusAnimationProgress WRITE setFocusAnimationProgress FINAL) + Q_PROPERTY(qreal placeholderTextWidth READ placeholderTextWidth WRITE setPlaceholderTextWidth FINAL) + Q_PROPERTY(bool controlHasText READ controlHasText WRITE setControlHasText NOTIFY controlHasTextChanged FINAL) + Q_PROPERTY(bool placeholderHasText READ placeholderHasText WRITE setPlaceholderHasText NOTIFY placeholderHasTextChanged FINAL) + Q_PROPERTY(int horizontalPadding READ horizontalPadding WRITE setHorizontalPadding NOTIFY horizontalPaddingChanged FINAL) + QML_NAMED_ELEMENT(MaterialTextContainer) + QML_ADDED_IN_VERSION(6, 5) + +public: + explicit QQuickMaterialTextContainer(QQuickItem *parent = nullptr); + + bool isFilled() const; + void setFilled(bool filled); + + QColor fillColor() const; + void setFillColor(const QColor &fillColor); + + QColor outlineColor() const; + void setOutlineColor(const QColor &outlineColor); + + QColor focusedOutlineColor() const; + void setFocusedOutlineColor(const QColor &focusedOutlineColor); + + qreal focusAnimationProgress() const; + void setFocusAnimationProgress(qreal progress); + + qreal placeholderTextWidth() const; + void setPlaceholderTextWidth(qreal placeholderTextWidth); + + bool controlHasActiveFocus() const; + void setControlHasActiveFocus(bool controlHasActiveFocus); + + bool controlHasText() const; + void setControlHasText(bool controlHasText); + + bool placeholderHasText() const; + void setPlaceholderHasText(bool placeholderHasText); + + int horizontalPadding() const; + void setHorizontalPadding(int horizontalPadding); + + void paint(QPainter *painter) override; + +signals: + void animateChanged(); + void controlHasActiveFocusChanged(); + void controlHasTextChanged(); + void placeholderHasTextChanged(); + void horizontalPaddingChanged(); + +private: + bool shouldAnimateOutline() const; + + void controlGotActiveFocus(); + void controlLostActiveFocus(); + void startFocusAnimation(); + + void maybeSetFocusAnimationProgress(); + + void componentComplete() override; + + QColor m_fillColor; + QColor m_outlineColor; + QColor m_focusedOutlineColor; + qreal m_focusAnimationProgress = 0; + qreal m_placeholderTextWidth = 0; + bool m_filled = false; + bool m_controlHasActiveFocus = false; + bool m_controlHasText = false; + bool m_placeholderHasText = false; + int m_horizontalPadding = 0; +}; + +QT_END_NAMESPACE + +#endif // QQUICKMATERIALTEXTCONTAINER_P_H diff --git a/src/quickcontrols/material/qquickmaterialstyle.cpp b/src/quickcontrols/material/qquickmaterialstyle.cpp index d7eb72f830..6fb4057080 100644 --- a/src/quickcontrols/material/qquickmaterialstyle.cpp +++ b/src/quickcontrols/material/qquickmaterialstyle.cpp @@ -422,6 +422,8 @@ static const QRgb switchDisabledCheckedTrackColorLight = 0x1E1C1B1F; static const QRgb switchDisabledCheckedTrackColorDark = 0x1EE6E1E5; static const QRgb switchDisabledUncheckedIconColorLight = 0x611C1B1F; static const QRgb switchDisabledUncheckedIconColorDark = 0x61E6E1E5; +static const QRgb textFieldFilledContainerColorLight = 0xFFE7E0EC; +static const QRgb textFieldFilledContainerColorDark = 0xFF49454F; static QQuickMaterialStyle::Theme effectiveTheme(QQuickMaterialStyle::Theme theme) { @@ -1158,6 +1160,11 @@ QColor QQuickMaterialStyle::sliderDisabledColor() const return QColor::fromRgba(m_theme == Light ? sliderDisabledColorLight : sliderDisabledColorDark); } +QColor QQuickMaterialStyle::textFieldFilledContainerColor() const +{ + return QColor::fromRgba(m_theme == Light ? textFieldFilledContainerColorLight : textFieldFilledContainerColorDark); +} + QColor QQuickMaterialStyle::color(QQuickMaterialStyle::Color color, QQuickMaterialStyle::Shade shade) const { int count = sizeof(colors) / sizeof(colors[0]); @@ -1299,6 +1306,21 @@ int QQuickMaterialStyle::switchDelegateVerticalPadding() const return globalVariant == Dense ? 4 : 8; } +int QQuickMaterialStyle::textFieldHeight() const +{ + // filled: https://m3.material.io/components/text-fields/specs#8c032848-e442-46df-b25d-28f1315f234b + // outlined: https://m3.material.io/components/text-fields/specs#605e24f1-1c1f-4c00-b385-4bf50733a5ef + return globalVariant == Dense ? 44 : 56; +} +int QQuickMaterialStyle::textFieldHorizontalPadding() const +{ + return globalVariant == Dense ? 12 : 16; +} +int QQuickMaterialStyle::textFieldVerticalPadding() const +{ + return globalVariant == Dense ? 4 : 8; +} + int QQuickMaterialStyle::tooltipHeight() const { // https://material.io/guidelines/components/tooltips.html diff --git a/src/quickcontrols/material/qquickmaterialstyle_p.h b/src/quickcontrols/material/qquickmaterialstyle_p.h index ab3483f04d..83b9e3ede3 100644 --- a/src/quickcontrols/material/qquickmaterialstyle_p.h +++ b/src/quickcontrols/material/qquickmaterialstyle_p.h @@ -73,6 +73,7 @@ class QQuickMaterialStyle : public QQuickAttachedPropertyPropagator Q_PROPERTY(QColor toolTextColor READ toolTextColor NOTIFY toolTextColorChanged FINAL) Q_PROPERTY(QColor spinBoxDisabledIconColor READ spinBoxDisabledIconColor NOTIFY themeChanged FINAL) Q_PROPERTY(QColor sliderDisabledColor READ sliderDisabledColor NOTIFY themeChanged FINAL REVISION(2, 15)) + Q_PROPERTY(QColor textFieldFilledContainerColor READ textFieldFilledContainerColor NOTIFY themeChanged FINAL) Q_PROPERTY(int touchTarget READ touchTarget CONSTANT FINAL) Q_PROPERTY(int buttonHeight READ buttonHeight CONSTANT FINAL) @@ -82,6 +83,9 @@ class QQuickMaterialStyle : public QQuickAttachedPropertyPropagator Q_PROPERTY(int menuItemHeight READ menuItemHeight CONSTANT FINAL) Q_PROPERTY(int menuItemVerticalPadding READ menuItemVerticalPadding CONSTANT FINAL) Q_PROPERTY(int switchDelegateVerticalPadding READ switchDelegateVerticalPadding CONSTANT FINAL) + Q_PROPERTY(int textFieldHeight READ textFieldHeight CONSTANT FINAL) + Q_PROPERTY(int textFieldHorizontalPadding READ textFieldHorizontalPadding CONSTANT FINAL) + Q_PROPERTY(int textFieldVerticalPadding READ textFieldVerticalPadding CONSTANT FINAL) Q_PROPERTY(int tooltipHeight READ tooltipHeight CONSTANT FINAL) QML_NAMED_ELEMENT(Material) @@ -254,6 +258,7 @@ public: QColor toolTextColor() const; QColor spinBoxDisabledIconColor() const; QColor sliderDisabledColor() const; + QColor textFieldFilledContainerColor() const; Q_INVOKABLE QColor color(Color color, Shade shade = Shade500) const; Q_INVOKABLE QColor shade(const QColor &color, Shade shade) const; @@ -266,6 +271,9 @@ public: int menuItemHeight() const; int menuItemVerticalPadding() const; int switchDelegateVerticalPadding() const; + int textFieldHeight() const; + int textFieldHorizontalPadding() const; + int textFieldVerticalPadding() const; int tooltipHeight() const; static void initGlobals(); diff --git a/tests/auto/quickcontrols/controls/data/tst_textfield.qml b/tests/auto/quickcontrols/controls/data/tst_textfield.qml index f5b3d91fe1..b334c9cd6d 100644 --- a/tests/auto/quickcontrols/controls/data/tst_textfield.qml +++ b/tests/auto/quickcontrols/controls/data/tst_textfield.qml @@ -5,6 +5,7 @@ import QtQuick import QtTest import QtQuick.Controls import QtQuick.Layouts +import Qt.test.controls TestCase { id: testCase @@ -157,16 +158,21 @@ TestCase { if (data.textAlignment !== undefined) compare(control.horizontalAlignment, data.textAlignment) - for (var i = 0; i < control.children.length; ++i) { - if (control.children[i].hasOwnProperty("text") && control.children[i].hasOwnProperty("horizontalAlignment")) - compare(control.children[i].effectiveHorizontalAlignment, data.placeholderAlignment) // placeholder + // The placeholder text of the Material style doesn't currently respect the alignment of the control. + if (StyleInfo.styleName !== "Material") { + for (var i = 0; i < control.children.length; ++i) { + if (control.children[i].hasOwnProperty("text") && control.children[i].hasOwnProperty("horizontalAlignment")) + compare(control.children[i].effectiveHorizontalAlignment, data.placeholderAlignment) // placeholder + } } control.verticalAlignment = TextField.AlignBottom compare(control.verticalAlignment, TextField.AlignBottom) - for (var j = 0; j < control.children.length; ++j) { - if (control.children[j].hasOwnProperty("text") && control.children[j].hasOwnProperty("verticalAlignment")) - compare(control.children[j].verticalAlignment, Text.AlignBottom) // placeholder + if (StyleInfo.styleName !== "Material") { + for (var j = 0; j < control.children.length; ++j) { + if (control.children[j].hasOwnProperty("text") && control.children[j].hasOwnProperty("verticalAlignment")) + compare(control.children[j].verticalAlignment, Text.AlignBottom) // placeholder + } } } diff --git a/tests/auto/quickcontrols/qquicktextfield/tst_qquicktextfield.cpp b/tests/auto/quickcontrols/qquicktextfield/tst_qquicktextfield.cpp index 19219bb79e..1cfa1663ef 100644 --- a/tests/auto/quickcontrols/qquicktextfield/tst_qquicktextfield.cpp +++ b/tests/auto/quickcontrols/qquicktextfield/tst_qquicktextfield.cpp @@ -84,9 +84,9 @@ void tst_QQuickTextField::touchscreenDoesNotSelect() if (selectByMouse) { // press-drag-and-release from x1 to x2 - int x1 = 10; - int x2 = 70; - int y = QFontMetrics(textField->font()).height() / 2; + const int x1 = textField->leftPadding(); + const int x2 = textField->width() / 2; + const int y = textField->height() / 2; QTest::touchEvent(&window, touchDevice.data()).press(0, QPoint(x1,y), &window); QTest::touchEvent(&window, touchDevice.data()).move(0, QPoint(x2,y), &window); QTest::touchEvent(&window, touchDevice.data()).release(0, QPoint(x2,y), &window); diff --git a/tests/manual/quickcontrols/material/CMakeLists.txt b/tests/manual/quickcontrols/material/CMakeLists.txt index 004d3e8708..2564f341f1 100644 --- a/tests/manual/quickcontrols/material/CMakeLists.txt +++ b/tests/manual/quickcontrols/material/CMakeLists.txt @@ -24,6 +24,7 @@ set(qmake_immediate_resource_files "pages/DelayButtonPage.qml" "pages/RoundButtonPage.qml" "pages/SwitchPage.qml" + "pages/TextFieldPage.qml" "qmldir" ) diff --git a/tests/manual/quickcontrols/material/material.qml b/tests/manual/quickcontrols/material/material.qml index 78efaf72e3..95aedf17f3 100644 --- a/tests/manual/quickcontrols/material/material.qml +++ b/tests/manual/quickcontrols/material/material.qml @@ -97,7 +97,7 @@ ApplicationWindow { focus: true currentIndex: settings.currentControlIndex anchors.fill: parent - model: ["Button", "DelayButton", "RoundButton", "Switch"] + model: ["Button", "DelayButton", "RoundButton", "Switch", "TextField"] delegate: ItemDelegate { width: listView.width text: modelData diff --git a/tests/manual/quickcontrols/material/pages/TextFieldPage.qml b/tests/manual/quickcontrols/material/pages/TextFieldPage.qml new file mode 100644 index 0000000000..4890047a78 --- /dev/null +++ b/tests/manual/quickcontrols/material/pages/TextFieldPage.qml @@ -0,0 +1,86 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause +import QtQuick +import QtQuick.Controls.Material +import QtQuick.Layouts + +import ".." + +Page { + topPadding: Constants.pageTopPadding + + component TextFieldFlow: Flow { + id: layout + spacing: 40 + + required property int containerStyle + + TextField { + Material.containerStyle: layout.containerStyle + } + + TextField { + placeholderText: "placeholderText" + + Material.containerStyle: layout.containerStyle + } + + TextField { + text: "text" + + Material.containerStyle: layout.containerStyle + } + + TextField { + text: "text" + placeholderText: "placeholderText" + + Material.containerStyle: layout.containerStyle + } + + TextField { + placeholderText: "Disabled placeholder" + enabled: false + + Material.containerStyle: layout.containerStyle + } + + TextField { + text: "Disabled text" + enabled: false + + Material.containerStyle: layout.containerStyle + } + + TextField { + text: "text" + placeholderText: "placeholderText" + enabled: false + + Material.containerStyle: layout.containerStyle + } + } + + ColumnLayout { + width: parent.width + + Label { + text: "Filled" + } + TextFieldFlow { + containerStyle: Material.Filled + + Layout.fillWidth: true + Layout.bottomMargin: 40 + } + + Label { + text: "Outlined" + } + TextFieldFlow { + containerStyle: Material.Outlined + + Layout.fillWidth: true + } + } +} |