Warning, /frameworks/kirigami/src/controls/ScrollablePage.qml is written in an unsupported language. File is not indexed.

0001 /*
0002  *  SPDX-FileCopyrightText: 2015 Marco Martin <mart@kde.org>
0003  *
0004  *  SPDX-License-Identifier: LGPL-2.0-or-later
0005  */
0006 
0007 import QtQuick 2.15
0008 import QtQml 2.15
0009 import QtQuick.Controls 2.15 as QQC2
0010 import QtGraphicalEffects 1.0 as GE
0011 import org.kde.kirigami 2.19 as Kirigami
0012 import org.kde.kirigami.templates 2.2 as KT
0013 import "private"
0014 
0015 
0016 // TODO KF6: undo many workarounds to make existing code work?
0017 
0018 /**
0019  * @brief ScrollablePage is a Page that holds scrollable content, such as a QtQuick.ListView.
0020  *
0021  * Scrolling and scrolling indicators will be automatically managed.
0022  *
0023  * Example usage:
0024  * @code{.qml}
0025  * ScrollablePage {
0026  *     id: root
0027  *     // The page will automatically be scrollable
0028  *     Rectangle {
0029  *         width: root.width
0030  *         height: 99999
0031  *     }
0032  * }
0033  * @endcode
0034  * @warning Do not put a QtQuick.Controls.ScrollView inside of a ScrollablePage;
0035  * children of a ScrollablePage are already inside a QtQuick.Controls.ScrollView.
0036  *
0037  * Another behavior added by this class is a "scroll down to refresh" behavior
0038  * It also can give the contents of the flickable to have more top margins in order
0039  * to make possible to scroll down the list to reach it with the thumb while using the
0040  * phone with a single hand.
0041  *
0042  * Implementations should handle the refresh themselves as follows
0043  *
0044  * Example usage:
0045  * @code{.qml}
0046  * Kirigami.ScrollablePage {
0047  *     id: view
0048  *     supportsRefreshing: true
0049  *     onRefreshingChanged: {
0050  *         if (refreshing) {
0051  *             myModel.refresh();
0052  *         }
0053  *     }
0054  *     ListView {
0055  *         // NOTE: MyModel doesn't come from the components,
0056  *         // it's purely an example on how it can be used together
0057  *         // some application logic that can update the list model
0058  *         // and signals when it's done.
0059  *         model: MyModel {
0060  *             onRefreshDone: view.refreshing = false;
0061  *         }
0062  *         delegate: BasicListItem {}
0063  *     }
0064  * }
0065  * [...]
0066  * @endcode
0067  */
0068 Kirigami.Page {
0069     id: root
0070 
0071 //BEGIN properties
0072     /**
0073      * @brief This property specifies whether the list is asking for a refresh.
0074      *
0075      * This property will automatically be set to @c true when the user pulls the list down enough,
0076      * which in return, shows a loading spinner. When this is set to true, it signals
0077      * the application logic to start its refresh procedure.
0078      *
0079      * default: ``false``
0080      *
0081      * @note The application itself will have to set back this property to @c false when done.
0082      */
0083     property bool refreshing: false
0084 
0085     /**
0086      * @brief This property sets whether scrollable page supports "pull down to refresh" behaviour.
0087      *
0088      * default: ``false``
0089      */
0090     property bool supportsRefreshing: false
0091 
0092     /**
0093      * @brief This property holds the main Flickable item of this page.
0094      * @deprecated here for compatibility; will be removed in KF6.
0095      */
0096     property Flickable flickable: Flickable {} // FIXME KF6: this empty flickable exists for compatibility reasons. some apps assume flickable exists right from the beginning but ScrollView internally assumes it does not
0097     onFlickableChanged: scrollView.contentItem = flickable;
0098 
0099     /**
0100      * @brief This property sets the vertical scrollbar policy.
0101      * @property Qt::ScrollBarPolicy verticalScrollBarPolicy
0102      */
0103     property int verticalScrollBarPolicy
0104 
0105     /**
0106      * @brief This property sets the horizontal scrollbar policy.
0107      * @property Qt::ScrollBarPolicy horizontalScrollBarPolicy
0108      */
0109     property int horizontalScrollBarPolicy: QQC2.ScrollBar.AlwaysOff
0110 
0111     default property alias scrollablePageData: itemsParent.data
0112     property alias scrollablePageChildren: itemsParent.children
0113 
0114     /*
0115      * @deprecated here for compatibility; will be removed in KF6.
0116      */
0117     property QtObject mainItem
0118     onMainItemChanged: {
0119         print("Warning: the mainItem property is deprecated");
0120         scrollablePageData.push(mainItem);
0121     }
0122 
0123     /**
0124      * @brief This property sets whether it is possible to navigate the items in a view that support it.
0125      *
0126      * If true, and if the QtQuick.Flickable is in an item view (e.g. QtQuick.ListView, QtQuick.GridView),
0127      * it will be possible to navigate the view current items with keyboard up/down arrow buttons.
0128      * Also, any key event will be forwarded to the current list item.
0129      *
0130      * default: ``true``
0131      */
0132     property bool keyboardNavigationEnabled: true
0133 //END properties
0134 
0135     contentHeight: flickable ? flickable.contentHeight : 0
0136     implicitHeight: {
0137         let height = contentHeight + topPadding + bottomPadding;
0138         if (header && header.visible) {
0139             height += header.implicitHeight;
0140         }
0141         if (footer && footer.visible) {
0142             height += footer.implicitHeight;
0143         }
0144         return height;
0145     }
0146 
0147     implicitWidth: {
0148         if (flickable) {
0149             if (flickable.contentItem) {
0150                 return flickable.contentItem.implicitWidth;
0151             } else {
0152                 return contentItem.implicitWidth + leftPadding + rightPadding;
0153             }
0154         } else {
0155             return 0;
0156         }
0157     }
0158 
0159     Kirigami.Theme.inherit: false
0160     Kirigami.Theme.colorSet: flickable && flickable.hasOwnProperty("model") ? Kirigami.Theme.View : Kirigami.Theme.Window
0161 
0162     Keys.forwardTo: {
0163         if (root.keyboardNavigationEnabled && root.flickable) {
0164             if (("currentItem" in root.flickable) && root.flickable.currentItem) {
0165                 return [ root.flickable.currentItem, root.flickable ];
0166             } else {
0167                 return [ root.flickable ];
0168             }
0169         } else {
0170             return [];
0171         }
0172     }
0173 
0174     contentItem: QQC2.ScrollView {
0175         id: scrollView
0176         anchors {
0177             top: (root.header && root.header.visible)
0178                     ? root.header.bottom
0179                     // FIXME: for now assuming globalToolBarItem is in a Loader, which needs to be get rid of
0180                     : (globalToolBarItem && globalToolBarItem.parent && globalToolBarItem.visible
0181                         ? globalToolBarItem.parent.bottom
0182                         : parent.top)
0183             bottom: (root.footer && root.footer.visible) ? root.footer.top : parent.bottom
0184             left: parent.left
0185             right: parent.right
0186             topMargin: root.refreshing ? busyIndicatorLoader.height : 0
0187             Behavior on topMargin {
0188                 NumberAnimation {
0189                     easing.type: Easing.InOutQuad
0190                     duration: Kirigami.Units.longDuration
0191                 }
0192             }
0193         }
0194         QQC2.ScrollBar.horizontal.policy: root.horizontalScrollBarPolicy
0195         QQC2.ScrollBar.vertical.policy: root.verticalScrollBarPolicy
0196     }
0197 
0198     data: [
0199         // Has to be a MouseArea that accepts events otherwise touch events on Wayland will get lost
0200         MouseArea {
0201             id: scrollingArea
0202             width: root.flickable.width
0203             height: Math.max(root.flickable.height, implicitHeight)
0204             implicitHeight: {
0205                 let impl = 0;
0206                 for (const i in itemsParent.visibleChildren) {
0207                     const child = itemsParent.visibleChildren[i];
0208                     if (child.implicitHeight <= 0) {
0209                         impl = Math.max(impl, child.height);
0210                     } else {
0211                         impl = Math.max(impl, child.implicitHeight);
0212                     }
0213                 }
0214                 return impl + itemsParent.anchors.topMargin + itemsParent.anchors.bottomMargin;
0215             }
0216             Item {
0217                 id: itemsParent
0218                 property Flickable flickable
0219                 anchors {
0220                     fill: parent
0221                     leftMargin: root.leftPadding
0222                     topMargin: root.topPadding
0223                     rightMargin: root.rightPadding
0224                     bottomMargin: root.bottomPadding
0225                 }
0226                 onChildrenChanged: {
0227                     const child = children[children.length - 1];
0228                     if (child instanceof QQC2.ScrollView) {
0229                         print("Warning: it's not supported to have ScrollViews inside a ScrollablePage")
0230                     }
0231                 }
0232             }
0233             Binding {
0234                 target: root.flickable
0235                 property: "bottomMargin"
0236                 value: root.bottomPadding
0237                 restoreMode: Binding.RestoreBinding
0238             }
0239         },
0240 
0241         Loader {
0242             id: busyIndicatorLoader
0243             z: 99
0244             y: root.flickable.verticalLayoutDirection === ListView.BottomToTop
0245                 ? -root.flickable.contentY + root.flickable.originY + height
0246                 : -root.flickable.contentY + root.flickable.originY - height
0247             width: root.flickable.width
0248             height: Kirigami.Units.gridUnit * 4
0249             active: root.supportsRefreshing
0250 
0251             sourceComponent: Item {
0252                 id: busyIndicatorFrame
0253 
0254                 QQC2.BusyIndicator {
0255                     id: busyIndicator
0256                     z: 1
0257                     anchors.centerIn: parent
0258                     running: root.refreshing
0259                     visible: root.refreshing
0260                     // Android busywidget QQC seems to be broken at custom sizes
0261                 }
0262                 Rectangle {
0263                     id: spinnerProgress
0264                     anchors {
0265                         fill: busyIndicator
0266                         margins: Kirigami.Units.smallSpacing
0267                     }
0268                     radius: width
0269                     visible: supportsRefreshing && !refreshing && progress > 0
0270                     color: "transparent"
0271                     opacity: 0.8
0272                     border.color: Kirigami.Theme.backgroundColor
0273                     border.width: Kirigami.Units.smallSpacing
0274                     property real progress: supportsRefreshing && !refreshing ? (busyIndicatorLoader.y / busyIndicatorFrame.height) : 0
0275                 }
0276                 GE.ConicalGradient {
0277                     source: spinnerProgress
0278                     visible: spinnerProgress.visible
0279                     anchors.fill: spinnerProgress
0280                     gradient: Gradient {
0281                         GradientStop { position: 0.00; color: Kirigami.Theme.highlightColor }
0282                         GradientStop { position: spinnerProgress.progress; color: Kirigami.Theme.highlightColor }
0283                         GradientStop { position: spinnerProgress.progress + 0.01; color: "transparent" }
0284                         GradientStop { position: 1.00; color: "transparent" }
0285                     }
0286                 }
0287 
0288                 Connections {
0289                     target: busyIndicatorLoader
0290                     function onYChanged() {
0291                         if (!supportsRefreshing) {
0292                             return;
0293                         }
0294 
0295                         if (!root.refreshing && busyIndicatorLoader.y > busyIndicatorFrame.height / 2 + topPadding) {
0296                             refreshTriggerTimer.running = true;
0297                         } else {
0298                             refreshTriggerTimer.running = false;
0299                         }
0300                     }
0301                 }
0302                 Timer {
0303                     id: refreshTriggerTimer
0304                     interval: 500
0305                     onTriggered: {
0306                         if (!root.refreshing && busyIndicatorLoader.y > busyIndicatorFrame.height / 2 + topPadding) {
0307                             root.refreshing = true;
0308                         }
0309                     }
0310                 }
0311             }
0312         }
0313     ]
0314 
0315     Component.onCompleted: {
0316         let flickableFound = false;
0317         for (const i in itemsParent.data) {
0318             const child = itemsParent.data[i];
0319             if (child instanceof Flickable) {
0320                 // If there were more flickable children, take the last one, as behavior compatibility
0321                 // with old internal ScrollView
0322                 child.activeFocusOnTab = true;
0323                 root.flickable = child;
0324                 flickableFound = true;
0325                 if (child instanceof ListView) {
0326                     child.keyNavigationEnabled = true;
0327                     child.keyNavigationWraps = false;
0328                 }
0329             } else if (child instanceof Item) {
0330                 child.anchors.left = itemsParent.left;
0331                 child.anchors.right = itemsParent.right;
0332             } else if (child instanceof KT.OverlaySheet) {
0333                 // Reparent sheets, needs to be done before Component.onCompleted
0334                 if (child.parent === itemsParent || child.parent === null) {
0335                     child.parent = root;
0336                 }
0337             }
0338         }
0339 
0340         if (flickableFound) {
0341             scrollView.contentItem = root.flickable;
0342             root.flickable.parent = scrollView;
0343             // The flickable needs focus only if the page didn't already explicitly set focus to some other control (eg a text field in the header)
0344             Qt.callLater(() => {
0345                 if (root.activeFocus) {
0346                     root.flickable.forceActiveFocus();
0347                 }
0348             });
0349             // Some existing code incorrectly uses anchors
0350             root.flickable.anchors.fill = undefined;
0351             root.flickable.anchors.left = undefined;
0352             root.flickable.anchors.right = undefined;
0353             root.flickable.anchors.top = undefined;
0354             root.flickable.anchors.bottom = undefined;
0355             scrollingArea.visible = false;
0356         } else {
0357             scrollView.contentItem = root.flickable;
0358             scrollingArea.parent = root.flickable.contentItem;
0359             scrollingArea.visible = true;
0360             root.flickable.contentHeight = Qt.binding(() => scrollingArea.implicitHeight - root.flickable.topMargin - root.flickable.bottomMargin);
0361             root.flickable.contentWidth = Qt.binding(() => scrollingArea.implicitWidth);
0362         }
0363         root.flickable.flickableDirection = Flickable.VerticalFlick;
0364     }
0365 }