Material: fix TextArea decorations when in a Flickable

This patch fixes the following issues when the Material TextArea is
attached to a Flickable:
- Floating placeholder text scrolls with the Flickable.
- When text is cleared without the control having focus:
  - Floating placeholder text is positioned incorrectly.
  - The floating text background outline gap is still open.
- The background outline color is incorrect when the control has focus
  (used primaryTextColor instead of accentColor).

Pick-to: 6.5
Task-number: QTBUG-112650
Change-Id: Icfa3517e4abcb1209ea2291dabdec225011f19ef
Reviewed-by: Richard Moe Gustavsen <richard.gustavsen@qt.io>
This commit is contained in:
Mitch Curtis 2023-04-11 13:44:33 +08:00
parent fd489252a7
commit 2d99c70f98
8 changed files with 105 additions and 8 deletions

View File

@ -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

View File

@ -108,7 +108,7 @@ void QQuickMaterialPlaceholderText::updateY()
qreal QQuickMaterialPlaceholderText::normalTargetY() const
{
auto *textArea = qobject_cast<QQuickTextArea *>(parentItem());
auto *textArea = qobject_cast<QQuickTextArea *>(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

View File

@ -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<QQuickItem *>(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()

View File

@ -83,6 +83,7 @@ signals:
private:
bool shouldAnimateOutline() const;
QQuickItem *textControl() const;
void controlGotActiveFocus();
void controlLostActiveFocus();
void startFocusAnimation();

View File

@ -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<QQuickItem *>(parent());
}
void QQuickPlaceholderText::updateAlignment()
{
if (QQuickTextInput *input = qobject_cast<QQuickTextInput *>(parentItem())) {

View File

@ -32,6 +32,8 @@ public:
protected:
void componentComplete() override;
QQuickItem *textControl() const;
private Q_SLOTS:
void updateAlignment();
};

View File

@ -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)
}
}

View File

@ -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 {