Dynamic height for card/rectangle used as ListView delegate
-
Hi, long-time web dev here, building a native app for desktop for the first time on QT/QT Creator. Been learning QT/QML for a day, and I've got this far, but I need help.
I'm building an a fairly simple interface to my design, where in I have a list view whose delegate is a custom component which in itself is a card that display a bunch of information about a gitlab issue. This issue (its text and content are completely dynamic)
Here is an example a card. Every part marked with a red square can have a variable height either due to:
- text length
- tab content varying in height: (i.e.
list
tab is just a set of icons,assignee
is a set of assignees and labels, andestimate
is a simple number entry
The problem I've having is the layout of the cards. I'm trying to use the
RoyLayout
andColumnLayout
types, because I know they have their own intrinsic heights calculated from children. This is important to me, because, and this is really important I don't want to have to specify explicit heights for any of the content. I want the cards to be as compact as possible and grow with their content, including the internal card menu, which is only visible if the card is selected.I'm almost there, but I just can't quite get some of the margins correct (especially on the bottom margin) which seems to be be flush against the bottom of the card, and in fact, despite being invisible, seems to be reserving space for the hidden buttons:
ListView
ColumnLayout { id: taskList Layout.fillWidth: true Layout.fillHeight: true spacing: 0 ListView { id: taskListView Layout.fillHeight: true Layout.fillWidth: true orientation: Qt.Vertical spacing: 10 model: ListModel { id: listModel Component.onCompleted: { for (var i = 0; i < 20; i++) listModel.append({"Name": "Item " + i}) taskListView.currentIndex = -1 } } delegate: TaskCard { margin: 12 cardIndex: index isSelected: taskListView.currentIndex == index onSelected: { taskListView.currentIndex = index } } focus: true clip: true } }
TaskCard
Item { id: taskCard // data props property int cardIndex property int taskId property string name: "[EXAMPLE] Do something great today." property string reference: "aliengen/aProject#1" property var assignee: { assignee_id: 1 name: 'Example guy' } property string stage: "To Do" property real estimate: 0 property real elapsed: 0 property bool shouldShowAssignee: false property bool isSelected: false property int selectedMenuType: 1 // layout props property real margin: 12 height: taskCardContent.childrenRect.height anchors { left: parent.left right: parent.right } // signals signal selected(int id) // states states: [ State { name: 'SELECTED' when: taskCard.isSelected } ] Rectangle { id: taskCardContent anchors { fill: parent leftMargin: taskCard.margin rightMargin: taskCard.margin } height: taskCardContent.childrenRect.height color: styles.c_secondary border { width: taskCard.state == 'SELECTED' ? 1 : 0 color: styles.c_ag_green } MouseArea { onClicked: taskCard.selected(taskCard.cardIndex) anchors.fill: parent } ColumnLayout { id: taskCardContentLayout anchors { left: parent.left right: parent.right leftMargin: 16 rightMargin: 16 } RowLayout { id: taskCardMetaLayout Layout.fillWidth: true Label { text: taskCard.stage font.pixelSize: styles.f_xs font.weight: Font.Medium color: styles.c_ag_green Layout.fillWidth: true Layout.alignment: Qt.AlignLeft } Rectangle { id: taskCardTimingLayout Layout.alignment: Qt.AlignRight Layout.fillWidth: true height: taskCardTimingLayout.childrenRect.height color: styles.c_secondary Label { id: taskCardTimingEstimate text: taskCard.estimate + "h / " font.pixelSize: styles.f_xs font.weight: Font.Normal color: styles.c_ag_green anchors { right: taskCardTimingRemaining.left } } Label { id: taskCardTimingRemaining text: taskCard.elapsed + "h" font.pixelSize: styles.f_xs font.weight: Font.Normal color: styles.c_ag_blue anchors { right: parent.right } } } } RowLayout { id: taskTitleRowLayout Layout.fillWidth: true Label { id: taskCardName text: taskCard.name color: styles.c_primary minimumPixelSize: styles.f_m font.pixelSize: styles.f_m font.weight: Font.Bold wrapMode: Text.WordWrap Layout.fillWidth: true Layout.alignment: Qt.AlignLeft } Button { width: 40 Layout.alignment: Qt.AlignRight id: taskCardTimerButton background: Rectangle { color: 'transparent' } icon.source: './images/IconPlayOutline.svg' icon.name: 'IconPlayOutline' icon.color: styles.c_ag_green } } RowLayout { id: taskCardReferenceLayout Layout.fillWidth: true Label { id: taskCardReference text: taskCard.reference font.pixelSize: styles.f_s font.weight: Font.Medium color: styles.c_accent_1 wrapMode: Text.WordWrap Layout.fillWidth: true } } RowLayout { visible: taskCard.isSelected id: taskCardControlMenuLayout Layout.fillWidth: true ColumnLayout { id: taskCardControlMenuInnerLayout Layout.fillWidth: true RowLayout { id: taskCardControlMenuTabs Layout.fillWidth: true spacing: 0 Button { id: taskCardControlMenuTabListChooser Layout.fillWidth: true Layout.preferredHeight: 20 background: Rectangle { color: styles.c_secondary } contentItem: Text { text: qsTr("LIST") color: selectedMenuType == 1 ? styles.c_ag_green : styles.c_accent_1 font.pixelSize: styles.f_s font.weight: selectedMenuType == 1 ? Font.Bold : Font.Normal horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter } } Button { id: taskCardControlMenuTabAssigneeChooser Layout.fillWidth: true Layout.preferredHeight: 20 background: Rectangle { color: styles.c_secondary } contentItem: Text { text: qsTr("ASSIGNMENT") color: selectedMenuType == 2 ? styles.c_ag_green : styles.c_accent_1 font.pixelSize: styles.f_s font.weight: selectedMenuType == 2 ? Font.Bold : Font.Normal horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter } } Button { id: taskCardControlMenuTabEstimateProvider Layout.fillWidth: true Layout.preferredHeight: 20 background: Rectangle { color: styles.c_secondary } contentItem: Text { text: qsTr("ESTIMATE") color: selectedMenuType == 3 ? styles.c_ag_green : styles.c_accent_1 font.pixelSize: styles.f_s font.weight: selectedMenuType == 3 ? Font.Bold : Font.Normal horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter } } } } } } }
Resulting render:
How can I fix these things?
- There is too much space on the bottom side of the card
- The buttons overflow the box, and the border
- there is no padding on the box on the top side
Any help would be appreciated
-
Solved it for those who care. Had to jump through some hoops:
Task Card
Item { id: taskCard // data props property int cardIndex property int taskId property string name: "[EXAMPLE] Do something great today." property string reference: "aliengen/aProject#1" property var assignee: { assignee_id: 1 name: 'Example guy' } property string listType: 'BACKLOG' property string stage: "To Do" property real estimate: 0 property real elapsed: 0 property bool shouldShowAssignee: false property bool isSelected: false property int selectedMenuType: 1 // layout props property real margin: 12 anchors { left: parent.left right: parent.right } height: taskCard.childrenRect.height // signals signal selected(int id) // states states: [ State { name: 'SELECTED' when: taskCard.isSelected } ] Rectangle { id: taskCardContent anchors { left: parent.left right: parent.right leftMargin: taskCard.margin rightMargin: taskCard.margin } height: taskCardContentLayout.height color: styles.c_secondary border { width: taskCard.state == 'SELECTED' ? 1 : 0 color: styles.c_ag_green } MouseArea { onClicked: taskCard.selected(taskCard.cardIndex) anchors.fill: parent } Rectangle { id: taskCardInnerSpacer height: taskCardContentLayout.height - 2 anchors { left: parent.left right: parent.right leftMargin: 16 rightMargin: 16 topMargin: 12 bottomMargin: 12 } color: styles.c_secondary y: 1 ColumnLayout { id: taskCardContentLayout anchors { left: parent.left right: parent.right } RowLayout { id: taskCardMetaLayout Layout.fillWidth: true Layout.topMargin: 16 spacing: 0 Label { text: taskCard.stage font.pixelSize: styles.f_xs font.weight: Font.Medium color: styles.c_ag_green Layout.fillWidth: true Layout.alignment: Qt.AlignLeft } Rectangle { id: taskCardTimingLayout Layout.alignment: Qt.AlignRight Layout.fillWidth: true height: taskCardTimingLayout.childrenRect.height color: styles.c_secondary Label { id: taskCardTimingEstimate text: taskCard.estimate + "h / " font.pixelSize: styles.f_xs font.weight: Font.Normal color: styles.c_ag_green anchors { right: taskCardTimingRemaining.left } } Label { id: taskCardTimingRemaining text: taskCard.elapsed + "h" font.pixelSize: styles.f_xs font.weight: Font.Normal color: styles.c_ag_blue anchors { right: parent.right } } } } RowLayout { id: taskTitleRowLayout Layout.fillWidth: true spacing: 0 Label { id: taskCardName text: taskCard.name color: styles.c_primary minimumPixelSize: styles.f_m font.pixelSize: styles.f_m font.weight: Font.Bold wrapMode: Text.WordWrap Layout.fillWidth: true Layout.alignment: Qt.AlignLeft } Button { width: 40 Layout.alignment: Qt.AlignRight id: taskCardTimerButton background: Rectangle { color: 'transparent' } icon.source: './images/IconPlayOutline.svg' icon.name: 'IconPlayOutline' icon.color: styles.c_ag_green } } RowLayout { id: taskCardReferenceLayout Layout.fillWidth: true Layout.bottomMargin: taskCard.isSelected ? 0 : 16 spacing: 0 Label { id: taskCardReference text: taskCard.reference font.pixelSize: styles.f_s font.weight: Font.Medium color: styles.c_accent_1 wrapMode: Text.WordWrap Layout.fillWidth: true } } RowLayout { id: taskCardControlsLayout Layout.fillWidth: true Layout.bottomMargin: taskCard.isSelected ? 16 : 0 spacing: 0 visible: taskCard.isSelected ColumnLayout { id: taskCardControlMenuInnerLayout Layout.fillWidth: true spacing: 0 RowLayout { id: taskCardControlMenuTabs Layout.fillWidth: true spacing: 0 Button { id: taskCardControlMenuTabListChooser Layout.fillWidth: true Layout.preferredHeight: 20 background: Rectangle { color: styles.c_secondary } contentItem: Text { text: qsTr("LIST") color: selectedMenuType == 1 ? styles.c_ag_green : styles.c_accent_1 font.pixelSize: styles.f_s font.weight: selectedMenuType == 1 ? Font.Bold : Font.Normal horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter } onClicked: selectedMenuType = 1 } Button { id: taskCardControlMenuTabAssigneeChooser Layout.fillWidth: true Layout.preferredHeight: 20 background: Rectangle { color: styles.c_secondary } contentItem: Text { text: qsTr("ASSIGNMENT") color: selectedMenuType == 2 ? styles.c_ag_green : styles.c_accent_1 font.pixelSize: styles.f_s font.weight: selectedMenuType == 2 ? Font.Bold : Font.Normal horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter } onClicked: selectedMenuType = 2 } Button { id: taskCardControlMenuTabEstimateProvider Layout.fillWidth: true Layout.preferredHeight: 20 background: Rectangle { color: styles.c_secondary } contentItem: Text { text: qsTr("ESTIMATE") color: selectedMenuType == 3 ? styles.c_ag_green : styles.c_accent_1 font.pixelSize: styles.f_s font.weight: selectedMenuType == 3 ? Font.Bold : Font.Normal horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter } onClicked: selectedMenuType = 3 } } RowLayout { id: taskControlsControlContainer Layout.fillWidth: true spacing: 0 Rectangle { id: taskCardControlsListChooser visible: selectedMenuType == 1 Layout.fillWidth: true height: taskCardControlsListChooserLayout.height color: styles.c_secondary anchors.margins: 4 RowLayout { id: taskCardControlsListChooserLayout anchors.left: parent.left anchors.right: parent.right spacing: 0 Button { icon.height: 14 icon.width: 14 icon.source: './images/IconBacklog.svg' icon.name: 'IconBacklog' icon.color: taskCard.listType == 'BACKLOG' ? styles.c_ag_green : styles.c_accent_1 onClicked: taskCard.listType = 'BACKLOG' background: Rectangle { color: styles.c_secondary anchors.fill: parent } Layout.fillWidth: true } Button { icon.height: 14 icon.width: 14 icon.source: './images/IconPlay.svg' icon.name: 'IconThisWeek' icon.color: taskCard.listType == 'THIS' ? styles.c_ag_green : styles.c_accent_1 onClicked: taskCard.listType = 'THIS' background: Rectangle { color: styles.c_secondary anchors.fill: parent } Layout.fillWidth: true } Button { icon.height: 14 icon.width: 14 icon.source: './images/IconNext.svg' icon.name: 'IconNextWeek' icon.color: taskCard.listType == 'NEXT' ? styles.c_ag_green : styles.c_accent_1 onClicked: taskCard.listType = 'NEXT' background: Rectangle { color: styles.c_secondary anchors.fill: parent } Layout.fillWidth: true } Button { icon.height: 14 icon.width: 14 icon.source: './images/IconSnooze.svg' icon.name: 'IconLater' icon.color: taskCard.listType == 'LATER' ? styles.c_ag_green : styles.c_accent_1 onClicked: taskCard.listType = 'LATER' background: Rectangle { color: styles.c_secondary anchors.fill: parent } Layout.fillWidth: true } } } Rectangle { id: taskCardControlsAssigneesChooser visible: selectedMenuType == 2 } Rectangle { id: taskCardControlsEstimateProvider visible: selectedMenuType == 3 } } } } } } } }