Skip to content

Commit 242fd06

Browse files
committed
feat(GroupChat): Add Flickable + Flow layout with auto-scroll and cursor visibility fixes
Introduces a new Flickable + Flow layout to replace the previous ScrollView structure. Ensures dynamic wrapping of member tags, vertical scrolling, and automatic scroll-to-end when content grows. Keeps focus-based auto-positioning.
1 parent 5081873 commit 242fd06

File tree

4 files changed

+135
-125
lines changed

4 files changed

+135
-125
lines changed

ui/StatusQ/src/StatusQ/Controls/StatusTagItem.qml

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,12 @@ Control {
4747
*/
4848
property string icon
4949

50+
/*!
51+
\qmlproperty int StatusTagItem::elideMode
52+
This property sets elide mode for the text.
53+
*/
54+
property int elideMode: Text.ElideRight
55+
5056
/*!
5157
\qmlsignal
5258
This signal is emitted when the close button is clicked.
@@ -79,23 +85,28 @@ Control {
7985
spacing: 2
8086

8187
StatusIcon {
88+
Layout.preferredWidth: root.icon ? d.tagIconsSize : 0
89+
Layout.preferredHeight: d.tagIconsSize
90+
8291
visible: root.icon
8392
color: Theme.palette.indirectColor1
84-
width: root.icon ? d.tagIconsSize : 0
85-
height: d.tagIconsSize
8693
icon: root.icon
8794
}
8895
StatusBaseText {
96+
Layout.fillWidth: true
97+
8998
color: Theme.palette.indirectColor1
9099
font: root.font
91100
text: root.text
101+
elide: root.elideMode
92102
}
93103
StatusIcon {
94104
Layout.leftMargin: d.tagMargins
105+
Layout.preferredWidth: d.tagIconsSize
106+
Layout.preferredHeight: d.tagIconsSize
107+
95108
visible: !root.isReadonly
96109
color: Theme.palette.indirectColor1
97-
width: d.tagIconsSize
98-
height: d.tagIconsSize
99110
icon: "close"
100111
StatusMouseArea {
101112
enabled: !root.isReadonly

ui/app/AppLayouts/Chat/panels/InlineSelectorPanel.qml

Lines changed: 116 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ import utils
1515
ColumnLayout {
1616
id: root
1717

18-
property alias model: listView.model
19-
property alias delegate: listView.delegate
18+
required property var model
19+
required property Component delegate
2020

2121
property alias suggestionsModel: suggestionsListView.model
2222
property alias suggestionsDelegate: suggestionsListView.delegate
@@ -27,6 +27,7 @@ ColumnLayout {
2727
readonly property alias label: label
2828
readonly property alias warningLabel: warningLabel
2929
readonly property alias edit: edit
30+
readonly property int membersFlickContentWidth: membersFlick.contentWidth
3031

3132
signal confirmed()
3233
signal rejected()
@@ -113,8 +114,8 @@ ColumnLayout {
113114

114115
property bool forceHide: false
115116

116-
parent: scrollView
117-
x: d.isCompactMode ? 0 : Math.min(parent.width, parent.contentWidth)
117+
parent: membersFlick
118+
x: 0
118119
y: parent.height + Theme.halfPadding
119120
visible: edit.text !== "" && !forceHide
120121
padding: Theme.halfPadding
@@ -150,6 +151,7 @@ ColumnLayout {
150151
visible: root.suggestionsModel.count === 0
151152
text: qsTr("No results found")
152153
color: Theme.palette.baseColor1
154+
elide: Text.ElideRight
153155
}
154156

155157
StatusListView {
@@ -189,148 +191,143 @@ ColumnLayout {
189191

190192
readonly property int toLabelWidth: label.implicitWidth + 2 * Theme.halfPadding
191193

192-
Layout.preferredHeight: 44
194+
readonly property int maxContentHeight: 120
195+
readonly property int minContentHeight: 44
196+
197+
Layout.preferredHeight: Math.max(membersBox.minContentHeight,
198+
membersFlick.height + 2 * Theme.halfPadding)
193199
visible: false
194200
color: Theme.palette.baseColor2
195201
radius: Theme.radius
202+
196203
RowLayout {
197204
anchors.fill: parent
198205
spacing: Theme.halfPadding
206+
199207
StatusBaseText {
200208
id: label
201209
Layout.leftMargin: Theme.padding
202210
Layout.alignment: Qt.AlignVCenter
203211
visible: text !== ""
204212
color: Theme.palette.baseColor1
205213
}
206-
Item {
214+
215+
Flickable {
216+
id: membersFlick
217+
218+
function positionViewAtEnd() {
219+
if (contentHeight > height) {
220+
contentY = contentHeight - height
221+
} else {
222+
contentY = 0
223+
}
224+
}
225+
207226
Layout.fillWidth: true
208-
Layout.fillHeight: true
209-
StatusScrollView {
210-
id: scrollView
211-
212-
function positionViewAtEnd() {
213-
if (scrollView.contentWidth > scrollView.width) {
214-
scrollView.flickable.contentX = scrollView.contentWidth - scrollView.width
215-
} else {
216-
scrollView.flickable.contentX = 0
217-
}
227+
Layout.preferredHeight: Math.min(membersFlow.implicitHeight,
228+
membersBox.maxContentHeight)
229+
230+
contentWidth: width
231+
contentHeight: membersFlow.implicitHeight
232+
clip: true
233+
234+
ScrollBar.vertical: StatusScrollBar {}
235+
236+
onContentHeightChanged: positionViewAtEnd()
237+
onHeightChanged: positionViewAtEnd()
238+
239+
Flow {
240+
id: membersFlow
241+
242+
width: membersFlick.width
243+
spacing: Theme.halfPadding
244+
245+
Repeater {
246+
id: membersRepeater
247+
248+
model: root.model
249+
delegate: root.delegate
218250
}
219251

220-
anchors.fill: parent
221-
contentHeight: availableHeight
222-
padding: 0
223-
224-
onContentWidthChanged: positionViewAtEnd()
225-
onWidthChanged: positionViewAtEnd()
226-
227-
RowLayout {
228-
height: scrollView.availableHeight
229-
StatusListView {
230-
id: listView
231-
Layout.fillWidth: true
232-
Layout.preferredHeight: 30
233-
implicitWidth: contentWidth
234-
orientation: ListView.Horizontal
235-
spacing: Theme.halfPadding
236-
interactive: false
237-
}
252+
TextInput {
253+
id: edit
238254

239-
TextInput {
240-
id: edit
241-
property bool pasteOperation: false
242-
Layout.minimumWidth: 4
243-
Layout.fillHeight: true
244-
verticalAlignment: Text.AlignVCenter
245-
font.pixelSize: Theme.primaryTextFontSize
246-
color: Theme.palette.directColor1
247-
248-
selectByMouse: true
249-
selectionColor: Theme.palette.primaryColor2
250-
selectedTextColor: color
251-
onCursorPositionChanged: {
252-
if (scrollView.contentX > cursorRectangle.x)
253-
scrollView.contentX = cursorRectangle.x;
254-
if (scrollView.contentX < ((cursorRectangle.x+Theme.smallPadding)-scrollView.width) && ((cursorRectangle.x+Theme.smallPadding) > scrollView.width))
255-
scrollView.contentX = (cursorRectangle.x-scrollView.width+Theme.smallPadding);
256-
}
257-
258-
cursorDelegate: StatusCursorDelegate {
259-
cursorVisible: edit.cursorVisible
260-
}
261-
262-
onTextEdited: {
263-
if (suggestionsDialog.forceHide && !pasteOperation)
264-
suggestionsDialog.forceHide = false
265-
}
266-
267-
Keys.onPressed: (event) => {
268-
269-
if (event.matches(StandardKey.Paste)) {
270-
event.accepted = true
271-
d.paste()
272-
return
273-
}
255+
property bool pasteOperation: false
274256

275-
if (suggestionsDialog.visible) {
276-
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
277-
root.entryAccepted(suggestionsListView.itemAtIndex(suggestionsListView.currentIndex))
278-
} else if (event.key === Qt.Key_Up) {
279-
suggestionsListView.decrementCurrentIndex()
280-
} else if (event.key === Qt.Key_Down) {
281-
suggestionsListView.incrementCurrentIndex()
282-
}
283-
} else {
284-
if (event.key === Qt.Key_Backspace && edit.text === "") {
285-
root.entryRemoved(listView.itemAtIndex(listView.count - 1))
286-
} else if (event.key === Qt.Key_Return || event.key === Qt.Enter) {
287-
root.enterKeyPressed()
288-
} else if (event.key === Qt.Key_Escape) {
289-
root.rejected()
290-
} else if (event.key === Qt.Key_Up) {
291-
root.upKeyPressed()
292-
} else if (event.key === Qt.Key_Down) {
293-
root.downKeyPressed()
294-
}
295-
}
296-
}
257+
width: Math.max(Math.min(implicitWidth, membersFlick.width + 2 * Theme.padding),
258+
2 * Theme.smallPadding)
259+
height: 30
260+
261+
verticalAlignment: Text.AlignVCenter
262+
font.pixelSize: Theme.primaryTextFontSize
263+
color: Theme.palette.directColor1
264+
265+
selectByMouse: true
266+
selectionColor: Theme.palette.primaryColor2
267+
selectedTextColor: color
268+
269+
cursorDelegate: StatusCursorDelegate {
270+
cursorVisible: edit.cursorVisible
297271
}
298272

299-
// ensure edit cursor is visible
300-
Item {
301-
Layout.fillHeight: true
302-
implicitWidth: 1
273+
onTextEdited: {
274+
if (suggestionsDialog.forceHide && !pasteOperation)
275+
suggestionsDialog.forceHide = false
303276
}
304-
}
305277

306-
ScrollBar.horizontal: StatusScrollBar {
307-
id: scrollBar
308-
parent: scrollView.parent
309-
anchors.top: scrollView.bottom
310-
anchors.left: scrollView.left
311-
anchors.right: scrollView.right
312-
policy: ScrollBar.AsNeeded
313-
visible: resolveVisibility(policy, scrollView.availableWidth, scrollView.contentWidth)
278+
Keys.onPressed: (event) => {
279+
280+
if (event.matches(StandardKey.Paste)) {
281+
event.accepted = true
282+
d.paste()
283+
return
284+
}
285+
286+
if (suggestionsDialog.visible) {
287+
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
288+
root.entryAccepted(suggestionsListView.itemAtIndex(suggestionsListView.currentIndex))
289+
} else if (event.key === Qt.Key_Up) {
290+
suggestionsListView.decrementCurrentIndex()
291+
} else if (event.key === Qt.Key_Down) {
292+
suggestionsListView.incrementCurrentIndex()
293+
}
294+
} else {
295+
if (event.key === Qt.Key_Backspace && edit.text === "") {
296+
root.entryRemoved(membersRepeater.itemAt(membersRepeater.count - 1))
297+
} else if (event.key === Qt.Key_Return || event.key === Qt.Enter) {
298+
root.enterKeyPressed()
299+
} else if (event.key === Qt.Key_Escape) {
300+
root.rejected()
301+
} else if (event.key === Qt.Key_Up) {
302+
root.upKeyPressed()
303+
} else if (event.key === Qt.Key_Down) {
304+
root.downKeyPressed()
305+
}
306+
}
307+
}
314308
}
315309
}
310+
}
316311

317-
StatusBaseText {
318-
id: warningLabel
319-
Layout.alignment: Qt.AlignVCenter | Qt.AlignRight
320-
Layout.rightMargin: Theme.padding
321-
visible: text !== ""
322-
font.pixelSize: Theme.asideTextFontSize
323-
color: Theme.palette.dangerColor1
324-
}
312+
StatusBaseText {
313+
id: warningLabel
314+
Layout.alignment: Qt.AlignVCenter | Qt.AlignRight
315+
Layout.rightMargin: Theme.padding
316+
Layout.preferredWidth: Math.min(membersBox.width / 4, implicitWidth)
317+
318+
visible: text !== ""
319+
font.pixelSize: Theme.asideTextFontSize
320+
wrapMode: Text.Wrap
321+
color: Theme.palette.dangerColor1
325322
}
323+
}
326324

327-
StatusMouseArea {
328-
anchors.fill: parent
329-
propagateComposedEvents: true
330-
onPressed: {
331-
edit.forceActiveFocus()
332-
mouse.accepted = false
333-
}
325+
StatusMouseArea {
326+
anchors.fill: parent
327+
propagateComposedEvents: true
328+
onPressed: {
329+
edit.forceActiveFocus()
330+
mouse.accepted = false
334331
}
335332
}
336333
}

ui/app/AppLayouts/Chat/views/MembersEditSelectorView.qml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,9 @@ MembersSelectorBase {
7272
delegate: StatusTagItem {
7373
readonly property string _pubKey: model.pubKey
7474

75-
height: ListView.view.height
75+
width: Math.min(implicitWidth, root.membersFlickContentWidth)
7676
text: model.preferredDisplayName
77+
elideMode: Text.ElideMiddle
7778

7879
isReadonly: {
7980
if (model.memberRole === Constants.memberRole.owner) return true

ui/app/AppLayouts/Chat/views/MembersSelectorView.qml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,9 @@ MembersSelectorBase {
6060
delegate: StatusTagItem {
6161
readonly property string _pubKey: model.pubKey
6262

63-
height: ListView.view.height
63+
width: Math.min(implicitWidth, root.membersFlickContentWidth)
6464
text: model.localNickname || model.displayName
65+
elideMode: Text.ElideMiddle
6566

6667
onClosed: root.entryRemoved(this)
6768
}

0 commit comments

Comments
 (0)