feat(StatusQ): DoubleFlickableWithFolding component providing sticky and foldable headers

It's build on top DoubleFlickable, managing two Flickables in an
effective way. It adds specific behavior of foldable and always visible
section headers.

Closes: feat/issue-13193
This commit is contained in:
Michał Cieślak 2024-01-22 15:17:41 +00:00 committed by Michał
parent 62857410e6
commit cbc15b368e
4 changed files with 391 additions and 0 deletions

View File

@ -0,0 +1,277 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import StatusQ.Core 0.1
import StatusQ.Core.Utils 0.1
import Storybook 1.0
SplitView {
id: root
orientation: Qt.Vertical
readonly property int header1Size: 40
readonly property int header2Size: 40
function fillModel(model, count) {
const content = []
for (let i = 0; i < count; i++)
content.push({})
model.clear()
model.append(content)
}
function adjustModel(model, newCount) {
const countDiff = newCount - model.count
const randPos = () => Math.floor(Math.random() * model.count)
if (countDiff > 0) {
for (let i = 0; i < countDiff; i++)
model.insert(randPos(), {})
} else {
for (let i = 0; i < -countDiff; i++)
model.remove(randPos())
}
}
ListModel {
id: firstModel
Component.onCompleted: fillModel(this, firstSlider.value)
}
ListModel {
id: secondModel
Component.onCompleted: fillModel(this, secondSlider.value)
}
Item {
SplitView.fillWidth: true
SplitView.fillHeight: true
Rectangle {
id: frame
anchors.centerIn: parent
width: Math.round(parent.width / 2)
height: Math.round(parent.height / 2)
border.width: 1
color: "transparent"
DoubleFlickableWithFolding {
id: doubleFlickable
anchors.fill: parent
clip: clipCheckBox.checked
z: -1
ScrollBar.vertical: ScrollBar { policy: ScrollBar.AlwaysOn }
flickable1: GridView {
id: grid1
width: frame.width
model: firstModel
cellWidth: 120
cellHeight: 30
header: Item {
id: header1
height: root.header1Size
width: GridView.view.width
Rectangle {
parent: doubleFlickable.contentItem
y: doubleFlickable.gridHeader1YInContentItem
z: 1
width: header1.width
height: header1.height
color: "orange"
Label {
anchors.centerIn: parent
font.bold: true
text: (doubleFlickable.flickable1Folded ? "⬇" : "➡️")
+ " Community"
}
MouseArea {
anchors.fill: parent
onClicked: doubleFlickable.flip1Folding()
}
}
}
delegate: Rectangle {
width: grid1.cellWidth
height: grid1.cellHeight
border.color: "black"
color: "lightblue"
Text {
anchors.centerIn: parent
text: index
}
}
Rectangle {
border.color: "green"
border.width: 5
anchors.fill: parent
color: "transparent"
}
}
flickable2: GridView {
id: grid2
width: frame.width
model: secondModel
cellWidth: 100
cellHeight: 100
header: Item {
id: header2
height: root.header2Size
width: GridView.view.width
Rectangle {
parent: doubleFlickable.contentItem
y: doubleFlickable.gridHeader2YInContentItem
z: 1
width: header2.width
height: header2.height
color: "red"
Label {
anchors.centerIn: parent
font.bold: true
text: (doubleFlickable.flickable2Folded ? "➡️" : "⬇")
+ " Others"
}
MouseArea {
anchors.fill: parent
onClicked: doubleFlickable.flip2Folding()
}
}
}
delegate: Rectangle {
width: grid2.cellWidth
height: grid2.cellHeight
border.color: "black"
Text {
anchors.centerIn: parent
text: index
}
}
Rectangle {
border.color: "blue"
border.width: 5
anchors.fill: parent
color: "transparent"
}
}
}
}
}
LogsAndControlsPanel {
SplitView.minimumHeight: 100
SplitView.preferredHeight: 200
SplitView.fillWidth: true
Column {
CheckBox {
id: clipCheckBox
text: "clip"
checked: true
}
RowLayout {
Label {
text: "first model:"
}
Slider {
id: firstSlider
from: 0
to: 200
stepSize: 1
value: 160
onValueChanged: adjustModel(firstModel, value)
}
RoundButton {
text: "-"
onClicked: firstSlider.decrease()
}
RoundButton {
text: "+"
onClicked: firstSlider.increase()
}
Label {
text: firstSlider.value
}
}
RowLayout {
Label {
text: "second model:"
}
Slider {
id: secondSlider
from: 0
to: 100
stepSize: 1
value: 90
onValueChanged: adjustModel(secondModel, value)
}
RoundButton {
text: "-"
onClicked: secondSlider.decrease()
}
RoundButton {
text: "+"
onClicked: secondSlider.increase()
}
Label {
text: secondSlider.value
}
}
}
}
}
// category: Components

View File

@ -0,0 +1,112 @@
import QtQuick 2.15
import QtQml 2.15
DoubleFlickable {
readonly property real gridHeader1YInContentItem: contentY
readonly property real gridHeader2YInContentItem: contentY + d.grid2HeaderOffset
readonly property bool flickable1Folded: d.grid1ConentInViewport
readonly property bool flickable2Folded: d.grid2HeaderAtEnd || d.model2Blocked
function flip1Folding() {
if (d.grid1ConentInViewport) {
if (d.grid2FullyFilling)
contentY = flickable1ContentHeight - d.header1Size
else
d.model1Blocked = true
} else {
d.model1Blocked = false
contentY = 0
}
}
function flip2Folding() {
// header at the end (always folded)
if (d.grid2HeaderAtEnd) {
contentY = flickable1ContentHeight - d.header1Size -
Math.max(0, height - flickable2ContentHeight - d.header1Size)
return
}
// header on top, unfolded
// explicitly folding both sections
if (d.grid2HeaderAtTop && !d.model2Blocked) {
d.model1Blocked = true
d.model2Blocked = true
return
}
// header on top, folded
if (d.grid2HeaderAtTop && d.model2Blocked) {
d.model2Blocked = false
return
}
// header in the middle
if (d.grid1FullyFilling) { // top section long enough to fill the whole view
contentY = flickable1ContentHeight + d.header2Size - height
} else {
d.model2Blocked = !d.model2Blocked
}
}
QtObject {
id: d
readonly property real header1Size: flickable1.headerItem.height
readonly property real header2Size: flickable2.headerItem.height
readonly property bool grid1ConentInViewport:
flickable1.y > contentY - Math.min(height, flickable1ContentHeight) + header1Size
readonly property real grid2HeaderOffset:
Math.min(Math.max(flickable2.y - contentY, header1Size), height - header2Size)
readonly property bool grid2HeaderAtTop:
grid2HeaderOffset === header1Size
readonly property bool grid2HeaderAtEnd:
grid2HeaderOffset === height - header2Size
property bool model1Blocked: false
property bool model2Blocked: false
readonly property bool grid1FullyFilling:
flickable1ContentHeight >= height - header2Size
readonly property bool grid2FullyFilling:
flickable2ContentHeight >= height - header1Size
onGrid1FullyFillingChanged: {
if (grid1FullyFilling) {
model2Blocked = false
contentY = 0
}
}
onGrid2FullyFillingChanged: {
if (grid2FullyFilling && model1Blocked) {
model1Blocked = false
contentY = flickable1ContentHeight - header1Size
}
}
}
Binding {
when: d.model1Blocked
target: flickable1
property: "model"
value: null
restoreMode: Binding.RestoreBinding
}
Binding {
when: d.model2Blocked
target: flickable2
property: "model"
value: null
restoreMode: Binding.RestoreBinding
}
}

View File

@ -2,6 +2,7 @@ module StatusQ.Core.Utils
ClippingWrapper 0.1 ClippingWrapper.qml ClippingWrapper 0.1 ClippingWrapper.qml
DoubleFlickable 0.1 DoubleFlickable.qml DoubleFlickable 0.1 DoubleFlickable.qml
DoubleFlickableWithFolding 0.1 DoubleFlickableWithFolding.qml
EmojiJSON 1.0 emojiList.js EmojiJSON 1.0 emojiList.js
JSONListModel 0.1 JSONListModel.qml JSONListModel 0.1 JSONListModel.qml
ModelChangeGuard 0.1 ModelChangeGuard.qml ModelChangeGuard 0.1 ModelChangeGuard.qml

View File

@ -209,6 +209,7 @@
<file>StatusQ/Core/Utils/ModelChangeTracker.qml</file> <file>StatusQ/Core/Utils/ModelChangeTracker.qml</file>
<file>StatusQ/Core/Utils/StringUtils.qml</file> <file>StatusQ/Core/Utils/StringUtils.qml</file>
<file>StatusQ/Core/Utils/DoubleFlickable.qml</file> <file>StatusQ/Core/Utils/DoubleFlickable.qml</file>
<file>StatusQ/Core/Utils/DoubleFlickableWithFolding.qml</file>
<file>StatusQ/Components/StatusPageIndicator.qml</file> <file>StatusQ/Components/StatusPageIndicator.qml</file>
<file>StatusQ/Components/StatusQrCodeScanner.qml</file> <file>StatusQ/Components/StatusQrCodeScanner.qml</file>
<file>StatusQ/Components/StatusOnlineBadge.qml</file> <file>StatusQ/Components/StatusOnlineBadge.qml</file>