diff --git a/src/quickcontrols/material/TextArea.qml b/src/quickcontrols/material/TextArea.qml index 32f6d5d5f7..baa0fdc893 100644 --- a/src/quickcontrols/material/TextArea.qml +++ b/src/quickcontrols/material/TextArea.qml @@ -44,6 +44,9 @@ T.TextArea { color: control.placeholderTextColor elide: Text.ElideRight renderType: control.renderType + // When the TextArea is in a Flickable, the background is reparented to it + // so that decorations don't move with the content. We need to do the same. + parent: control.background.parent filled: control.Material.containerStyle === Material.Filled verticalPadding: control.Material.textFieldVerticalPadding diff --git a/src/quickcontrols/material/impl/qquickmaterialplaceholdertext.cpp b/src/quickcontrols/material/impl/qquickmaterialplaceholdertext.cpp index 40557f46a2..6dfb1b8e2a 100644 --- a/src/quickcontrols/material/impl/qquickmaterialplaceholdertext.cpp +++ b/src/quickcontrols/material/impl/qquickmaterialplaceholdertext.cpp @@ -108,7 +108,7 @@ void QQuickMaterialPlaceholderText::updateY() qreal QQuickMaterialPlaceholderText::normalTargetY() const { - auto *textArea = qobject_cast(parentItem()); + auto *textArea = qobject_cast(textControl()); if (textArea && m_controlHeight >= textArea->implicitHeight()) { // TextArea can be multiple lines in height, and we want the // placeholder text to sit in the middle of its default-height @@ -166,7 +166,7 @@ void QQuickMaterialPlaceholderText::setControlImplicitBackgroundHeight(qreal con which is necessary for some y position calculations. We don't really need it for the actual calculations, since we already - have access to the parent item, from which the property comes, but + have access to the control, from which the property comes, but it's simpler just to use it. */ qreal QQuickMaterialPlaceholderText::controlHeight() const diff --git a/src/quickcontrols/material/impl/qquickmaterialtextcontainer.cpp b/src/quickcontrols/material/impl/qquickmaterialtextcontainer.cpp index 2751efaa66..e987ce5610 100644 --- a/src/quickcontrols/material/impl/qquickmaterialtextcontainer.cpp +++ b/src/quickcontrols/material/impl/qquickmaterialtextcontainer.cpp @@ -272,7 +272,8 @@ void QQuickMaterialTextContainer::paint(QPainter *painter) painter->setRenderHint(QPainter::Antialiasing, true); - const bool focused = parentItem() && parentItem()->hasActiveFocus(); + auto control = textControl(); + const bool focused = control && control->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; @@ -317,6 +318,16 @@ bool QQuickMaterialTextContainer::shouldAnimateOutline() const return !m_controlHasText && m_placeholderHasText; } +/*! + \internal + + \sa QQuickPlaceholderText::textControl(). +*/ +QQuickItem *QQuickMaterialTextContainer::textControl() const +{ + return qobject_cast(parent()); +} + void QQuickMaterialTextContainer::controlGotActiveFocus() { const bool shouldAnimate = m_filled ? !m_controlHasText : shouldAnimateOutline(); @@ -367,9 +378,16 @@ void QQuickMaterialTextContainer::startFocusAnimation() void QQuickMaterialTextContainer::maybeSetFocusAnimationProgress() { - // Show the interrupted outline when there is text. - if (!m_filled && m_controlHasText && m_placeholderHasText) + if (m_filled) + return; + + if (m_controlHasText && m_placeholderHasText) { + // Show the interrupted outline when there is text. setFocusAnimationProgress(1); + } else if (!m_controlHasText && !m_controlHasActiveFocus) { + // If the text was cleared while it didn't have focus, don't animate, just close the gap. + setFocusAnimationProgress(0); + } } void QQuickMaterialTextContainer::componentComplete() diff --git a/src/quickcontrols/material/impl/qquickmaterialtextcontainer_p.h b/src/quickcontrols/material/impl/qquickmaterialtextcontainer_p.h index 40fcff148b..ed6876cb46 100644 --- a/src/quickcontrols/material/impl/qquickmaterialtextcontainer_p.h +++ b/src/quickcontrols/material/impl/qquickmaterialtextcontainer_p.h @@ -83,6 +83,7 @@ signals: private: bool shouldAnimateOutline() const; + QQuickItem *textControl() const; void controlGotActiveFocus(); void controlLostActiveFocus(); void startFocusAnimation(); diff --git a/src/quickcontrolsimpl/qquickplaceholdertext.cpp b/src/quickcontrolsimpl/qquickplaceholdertext.cpp index 5f47a4087d..4a1fb0db00 100644 --- a/src/quickcontrolsimpl/qquickplaceholdertext.cpp +++ b/src/quickcontrolsimpl/qquickplaceholdertext.cpp @@ -16,10 +16,25 @@ QQuickPlaceholderText::QQuickPlaceholderText(QQuickItem *parent) : QQuickText(pa void QQuickPlaceholderText::componentComplete() { QQuickText::componentComplete(); - connect(parentItem(), SIGNAL(effectiveHorizontalAlignmentChanged()), this, SLOT(updateAlignment())); + + auto control = textControl(); + if (control) + connect(control, SIGNAL(effectiveHorizontalAlignmentChanged()), this, SLOT(updateAlignment())); updateAlignment(); } +/*! + \internal + + The control that we're representing. This exists because + parentItem() is not always the control - it may be a Flickable + in the case of TextArea. +*/ +QQuickItem *QQuickPlaceholderText::textControl() const +{ + return qobject_cast(parent()); +} + void QQuickPlaceholderText::updateAlignment() { if (QQuickTextInput *input = qobject_cast(parentItem())) { diff --git a/src/quickcontrolsimpl/qquickplaceholdertext_p.h b/src/quickcontrolsimpl/qquickplaceholdertext_p.h index 81434cc65c..867ec24ad9 100644 --- a/src/quickcontrolsimpl/qquickplaceholdertext_p.h +++ b/src/quickcontrolsimpl/qquickplaceholdertext_p.h @@ -32,6 +32,8 @@ public: protected: void componentComplete() override; + QQuickItem *textControl() const; + private Q_SLOTS: void updateAlignment(); }; diff --git a/tests/auto/quickcontrols/qquickmaterialstyle/data/tst_material.qml b/tests/auto/quickcontrols/qquickmaterialstyle/data/tst_material.qml index d85ddc7a44..06098f2fbb 100644 --- a/tests/auto/quickcontrols/qquickmaterialstyle/data/tst_material.qml +++ b/tests/auto/quickcontrols/qquickmaterialstyle/data/tst_material.qml @@ -978,15 +978,61 @@ TestCase { } { - // The non-floating placeholder text should be near the top of TextArea while it has room, but when it - // doesn't have room, it should start behaving like TextField's. + // The non-floating placeholder text should be near the top of TextArea while it has room... let textArea = createTemporaryObject(textAreaComponent, testCase, { placeholderText: "TextArea" }) verify(textArea) let placeholderTextItem = textArea.children[0] verify(placeholderTextItem as MaterialImpl.FloatingPlaceholderText) compare(placeholderTextItem.y, (placeholderTextItem.controlImplicitBackgroundHeight - placeholderTextItem.largestHeight) / 2) + + // ... also when it has a lot of room... + textArea.height = 200 + compare(placeholderTextItem.y, (placeholderTextItem.controlImplicitBackgroundHeight - placeholderTextItem.largestHeight) / 2) + + // ... but when it doesn't have room, it should start behaving like TextField's. textArea.height = 10 compare(placeholderTextItem.y, (textArea.height - placeholderTextItem.height) / 2) } } + + Component { + id: flickableTextAreaComponent + + Flickable { + anchors.horizontalCenter: parent.horizontalCenter + y: 20 + width: 180 + height: 100 + + TextArea.flickable: TextArea { + placeholderText: "Type something..." + text: "a\nb\nc\nd\ne\nf\ng\nh\ni\nj\nk\nl\nm\nn" + } + } + } + + function test_placeholderTextInFlickable() { + let flickable = createTemporaryObject(flickableTextAreaComponent, testCase) + verify(flickable) + + let textArea = flickable.TextArea.flickable + verify(textArea) + let placeholderTextItem = flickable.children[2] + verify(placeholderTextItem as MaterialImpl.FloatingPlaceholderText) + + // The placeholder text should always float at a fixed position at the top + // when text has been set, even when it's in a Flickable. + flickable.contentY = -50 + compare(placeholderTextItem.y, -Math.floor(placeholderTextItem.largestHeight / 2)) + flickable.contentY = 0 + + // When the text is cleared, it shouldn't float. + flickable.height = 160 + textArea.text = "" + compare(placeholderTextItem.y, (placeholderTextItem.controlImplicitBackgroundHeight - placeholderTextItem.largestHeight) / 2) + // The background outline gap should be closed. + let textContainer = flickable.children[1] + verify(textContainer as MaterialImpl.MaterialTextContainer) + compare(textContainer.focusAnimationProgress, 0) + } } diff --git a/tests/manual/quickcontrols/material/pages/TextAreaPage.qml b/tests/manual/quickcontrols/material/pages/TextAreaPage.qml index 080eeab5e1..f6836e2a83 100644 --- a/tests/manual/quickcontrols/material/pages/TextAreaPage.qml +++ b/tests/manual/quickcontrols/material/pages/TextAreaPage.qml @@ -89,6 +89,18 @@ Page { Material.containerStyle: layout.containerStyle } + + Flickable { + width: 200 + height: 100 + + TextArea.flickable: TextArea { + placeholderText: "placeholderText" + text: "a\nb\nc\nd\ne\nf\ng\nh\ni\nj\nk\nl\nm\nn" + + Material.containerStyle: layout.containerStyle + } + } } ColumnLayout {