From 62857410e6d2be79f50da323a9b4f67403308a2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Cie=C5=9Blak?= Date: Mon, 22 Jan 2024 15:11:28 +0000 Subject: [PATCH] feat(StatusQ): DoubleFlickable utility making two Flickables operating line one The wrapper doesn't "unroll" the managed Flickables. There is no instantiation of delegats for all model entries up front. --- storybook/pages/DoubleFlickablePage.qml | 238 ++++++++++++++++++ .../StatusQ/Core/Utils/DoubleFlickable.qml | 114 +++++++++ ui/StatusQ/src/StatusQ/Core/Utils/qmldir | 1 + ui/StatusQ/src/statusq.qrc | 1 + 4 files changed, 354 insertions(+) create mode 100644 storybook/pages/DoubleFlickablePage.qml create mode 100644 ui/StatusQ/src/StatusQ/Core/Utils/DoubleFlickable.qml diff --git a/storybook/pages/DoubleFlickablePage.qml b/storybook/pages/DoubleFlickablePage.qml new file mode 100644 index 0000000000..002c6c489f --- /dev/null +++ b/storybook/pages/DoubleFlickablePage.qml @@ -0,0 +1,238 @@ +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 headerSize: 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" + + DoubleFlickable { + id: doubleFlickable + + anchors.fill: parent + clip: clipCheckBox.checked + z: -1 + + ScrollBar.vertical: ScrollBar { policy: ScrollBar.AlwaysOn } + + flickable1: GridView { + width: frame.width + interactive: false + model: firstModel + + cellWidth: 120 + cellHeight: 30 + + header: Rectangle { + height: root.headerSize + width: GridView.view.width + + color: "orange" + + Label { + anchors.centerIn: parent + font.bold: true + text: "Community" + } + } + + delegate: Rectangle { + width: GridView.view.cellWidth + height: GridView.view.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 { + width: frame.width + interactive: false + model: secondModel + + cellWidth: 100 + cellHeight: 100 + + header: Rectangle { + height: root.headerSize + width: GridView.view.width + + color: "red" + + Label { + anchors.centerIn: parent + font.bold: true + text: "Others" + } + } + + delegate: Rectangle { + width: GridView.view.cellWidth + height: GridView.view.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 diff --git a/ui/StatusQ/src/StatusQ/Core/Utils/DoubleFlickable.qml b/ui/StatusQ/src/StatusQ/Core/Utils/DoubleFlickable.qml new file mode 100644 index 0000000000..003f3ec52e --- /dev/null +++ b/ui/StatusQ/src/StatusQ/Core/Utils/DoubleFlickable.qml @@ -0,0 +1,114 @@ +import QtQuick 2.15 +import QtQml 2.15 + +Flickable { + id: root + + boundsBehavior: Flickable.StopAtBounds + maximumFlickVelocity: 2000 + synchronousDrag: true + + property Flickable flickable1: Flickable {} + property Flickable flickable2: Flickable {} + + readonly property real flickable1ContentHeight: flickable1.contentHeight + readonly property real flickable2ContentHeight: flickable2.contentHeight + + onWidthChanged: returnToBounds() + onHeightChanged: returnToBounds() + + contentWidth: root.width + contentHeight: flickable1ContentHeight + flickable2ContentHeight + + QtObject { + id: d + + property real offsetY1 + property real offsetY2 + + Binding on offsetY1 { + value: flickable1.originY + delayed: true + } + + Binding on offsetY2 { + value: flickable2.originY + delayed: true + } + } + + // First flickable + + Binding { + target: flickable1 + property: "parent" + value: contentItem + } + + Binding { + target: flickable1 + property: "interactive" + value: false + } + + Binding { + target: flickable1 + property: "height" + value: Math.min(root.height, flickable1ContentHeight) + delayed: true + } + + Binding { + target: flickable1 + property: "y" + value: Math.min(Math.max(0, root.contentY), + flickable1ContentHeight - flickable1.height) + } + + Binding { + target: flickable1 + property: "contentY" + value: Math.min(Math.max(root.contentY, 0), + flickable1ContentHeight - flickable1.height) + d.offsetY1 + + delayed: true + } + + // Second flickable + + Binding { + target: flickable2 + property: "parent" + value: contentItem + } + + Binding { + target: flickable2 + property: "interactive" + value: false + } + + Binding { + target: flickable2 + property: "height" + value: Math.min(root.height, flickable2ContentHeight) + + delayed: true + } + + Binding { + target: flickable2 + property: "y" + value: Math.min(Math.max(flickable1ContentHeight, root.contentY), + root.contentHeight - flickable2.height) + } + + Binding { + target: flickable2 + property: "contentY" + value: Math.min(Math.max(0, root.contentY - flickable1ContentHeight), + flickable2ContentHeight - flickable2.height) + d.offsetY2 + + delayed: true + } +} diff --git a/ui/StatusQ/src/StatusQ/Core/Utils/qmldir b/ui/StatusQ/src/StatusQ/Core/Utils/qmldir index ede0e58c2a..1017291888 100644 --- a/ui/StatusQ/src/StatusQ/Core/Utils/qmldir +++ b/ui/StatusQ/src/StatusQ/Core/Utils/qmldir @@ -1,6 +1,7 @@ module StatusQ.Core.Utils ClippingWrapper 0.1 ClippingWrapper.qml +DoubleFlickable 0.1 DoubleFlickable.qml EmojiJSON 1.0 emojiList.js JSONListModel 0.1 JSONListModel.qml ModelChangeGuard 0.1 ModelChangeGuard.qml diff --git a/ui/StatusQ/src/statusq.qrc b/ui/StatusQ/src/statusq.qrc index 7881b0c600..0d17a8380f 100644 --- a/ui/StatusQ/src/statusq.qrc +++ b/ui/StatusQ/src/statusq.qrc @@ -208,6 +208,7 @@ StatusQ/Core/Utils/ModelsComparator.qml StatusQ/Core/Utils/ModelChangeTracker.qml StatusQ/Core/Utils/StringUtils.qml + StatusQ/Core/Utils/DoubleFlickable.qml StatusQ/Components/StatusPageIndicator.qml StatusQ/Components/StatusQrCodeScanner.qml StatusQ/Components/StatusOnlineBadge.qml