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:
parent
62857410e6
commit
cbc15b368e
|
@ -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
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ module StatusQ.Core.Utils
|
|||
|
||||
ClippingWrapper 0.1 ClippingWrapper.qml
|
||||
DoubleFlickable 0.1 DoubleFlickable.qml
|
||||
DoubleFlickableWithFolding 0.1 DoubleFlickableWithFolding.qml
|
||||
EmojiJSON 1.0 emojiList.js
|
||||
JSONListModel 0.1 JSONListModel.qml
|
||||
ModelChangeGuard 0.1 ModelChangeGuard.qml
|
||||
|
|
|
@ -209,6 +209,7 @@
|
|||
<file>StatusQ/Core/Utils/ModelChangeTracker.qml</file>
|
||||
<file>StatusQ/Core/Utils/StringUtils.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/StatusQrCodeScanner.qml</file>
|
||||
<file>StatusQ/Components/StatusOnlineBadge.qml</file>
|
||||
|
|
Loading…
Reference in New Issue