Commit d03c06bf authored by Marco Martin's avatar Marco Martin
Browse files

Delete and port away from internal ScrollView

Refactor ScrollablePage to port it out of our internal implementation of ScrollView to upstream QQC2.ScrollView

port the other users as well, most notably OverlaySheet and Dialog.

Testing on various applications don't seem to produce any noticeable differences

BUG:448784
parent 36c69701
Pipeline #184685 passed with stage
in 6 minutes and 48 seconds
......@@ -219,7 +219,6 @@ ecm_target_qml_sources(KirigamiPlugin PRIVATE PATH private SOURCES
controls/private/GlobalDrawerActionItem.qml
controls/private/PageActionPropertyGroup.qml
controls/private/PrivateActionToolButton.qml
controls/private/RefreshableScrollView.qml
controls/private/SwipeItemEventFilter.qml
)
......@@ -268,7 +267,6 @@ ecm_target_qml_sources(KirigamiPlugin PRIVATE PATH templates/private SOURCES
controls/templates/private/IconPropertiesGroup.qml
controls/templates/private/MenuIcon.qml
controls/templates/private/PassiveNotification.qml
controls/templates/private/ScrollView.qml
)
ecm_target_qml_sources(KirigamiPlugin PRIVATE PATH styles/Material SOURCES
......
......@@ -6,6 +6,7 @@
import QtQuick 2.1
import QtQuick.Layouts 1.2
import QtQuick.Controls 2.2 as QQC2
import org.kde.kirigami 2.4
import "private"
......@@ -118,7 +119,7 @@ OverlayDrawer {
page.contextualActionsAboutToShow();
}
}
contentItem: ScrollView {
contentItem: QQC2.ScrollView {
//this just to create the attached property
Theme.inherit: true
implicitWidth: Units.gridUnit * 20
......
......@@ -109,12 +109,22 @@ T.Dialog {
id: root
/**
* The dialog's contents.
* @deprecated will be removed on next Frameworks major release
*/
property Item mainItem: contentControl.contentChildren.length > 0 ? contentControl.contentChildren[0] : null
/**
* The dialog's contents, includes Items and QtObjects.
*/
default property alias dialogData: contentControl.contentData
/**
* The content items of the dialog.
*
* The initial height and width of the dialog is calculated from the
* `implicitWidth` and `implicitHeight` of this item.
* `implicitWidth` and `implicitHeight` of the content.
*/
default property Item mainItem
property alias dialogChildren: contentControl.contentChildren
/**
* The absolute maximum height the dialog can be (including the header
......@@ -312,30 +322,26 @@ T.Dialog {
// dialog content
contentItem: ColumnLayout {
Private.ScrollView {
Controls.ScrollView {
id: contentControl
// we cannot have contentItem inside a sub control (allowing for content padding within the scroll area),
// because if the contentItem is a Flickable (ex. ListView), the ScrollView needs it to be top level in order
// to decorate it
contentItem: root.mainItem
canFlickWithMouse: true
// ensure view colour scheme, and background color
Kirigami.Theme.inherit: false
Kirigami.Theme.colorSet: Kirigami.Theme.View
// needs to explicitly be set for each side to work
leftPadding: 0; topPadding: 0
rightPadding: 0; bottomPadding: 0
Controls.ScrollBar.horizontal.policy: Controls.ScrollBar.AlwaysOff
// height of everything else in the dialog other than the content
property real otherHeights: root.header.height + root.footer.height + root.topPadding + root.bottomPadding;
property real calculatedMaximumWidth: Math.min(root.absoluteMaximumWidth, root.maximumWidth) - root.leftPadding - root.rightPadding
property real calculatedMaximumHeight: Math.min(root.absoluteMaximumHeight, root.maximumHeight) - root.topPadding - root.bottomPadding
property real calculatedImplicitWidth: (root.mainItem.implicitWidth ? root.mainItem.implicitWidth : root.mainItem.width) + contentControl.rightSpacing + leftPadding + rightPadding
property real calculatedImplicitHeight: (root.mainItem.implicitHeight ? root.mainItem.implicitHeight : root.mainItem.height) + topPadding + bottomPadding
property real calculatedImplicitWidth: (contentChildren.length === 1 && contentChildren[0].implicitWidth > 0
? contentChildren[0].implicitWidth
: (contentItem.implicitWidth > 0 ? contentItem.implicitWidth : contentItem.width)) + leftPadding + rightPadding
property real calculatedImplicitHeight: (contentChildren.length === 1 && contentChildren[0].implicitHeight > 0
? contentChildren[0].implicitHeight
: (contentItem.implicitHeight > 0 ? contentItem.implicitHeight : contentItem.height)) + topPadding + bottomPadding
// how do we deal with the scrollbar width?
// - case 1: the dialog itself has the preferredWidth set
......@@ -343,8 +349,6 @@ T.Dialog {
// - case 2: preferredWidth not set, so we are using the content's implicit width
// -> we expand the dialog's width to accommodate the scrollbar width (to respect the content's desired width)
// note: the scrollbar width is accessed through "contentControl.rightSpacing"
// don't enforce preferred width and height if not set
Layout.preferredWidth: (root.preferredWidth >= 0 ? root.preferredWidth : calculatedImplicitWidth)
Layout.preferredHeight: root.preferredHeight >= 0 ? root.preferredHeight - otherHeights : calculatedImplicitHeight
......@@ -356,21 +360,15 @@ T.Dialog {
// give an implied width and height to the contentItem so that features like word wrapping/eliding work
// cannot placed directly in contentControl as a child, so we must use a property
property var widthHint: Binding {
target: root.mainItem
target: contentControl.contentChildren[0]
when: contentControl.contentChildren.length === 1
property: "width"
// we want to avoid horizontal scrolling, so we apply maximumWidth as a hint if necessary
property real preferredWidthHint: contentControl.Layout.preferredWidth - contentControl.leftPadding - contentControl.rightPadding
- (root.preferredWidth >= 0 ? contentControl.rightSpacing : 0) // hint the scrollbar width with conditions stated above
property real maximumWidthHint: contentControl.calculatedMaximumWidth - contentControl.leftPadding - contentControl.rightPadding - contentControl.rightSpacing
property real preferredWidthHint: contentControl.contentItem.width
property real maximumWidthHint: contentControl.calculatedMaximumWidth - contentControl.leftPadding - contentControl.rightPadding
value: maximumWidthHint < preferredWidthHint ? maximumWidthHint : preferredWidthHint
}
property var heightHint: Binding {
target: root.mainItem
property: "height"
// we are okay with overflow, if it exceeds maximumHeight we will allow scrolling
value: contentControl.Layout.preferredHeight - contentControl.topPadding - contentControl.bottomPadding
value: Math.min(maximumWidthHint,preferredWidthHint)
}
}
}
......
......@@ -6,7 +6,9 @@
import QtQuick 2.15
import QtQuick.Templates 2.15 as T
import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.15
import QtGraphicalEffects 1.0
import org.kde.kirigami 2.19
import org.kde.kirigami.templates 2.2 as KT
......@@ -70,40 +72,40 @@ Page {
* This signals the application logic to start its refresh procedure.
* The application itself will have to set back this property to false when done.
*/
property alias refreshing: scrollView.refreshing
property bool refreshing: false
/**
* \property bool ScrollablePage::supportsRefreshing
* If true the list supports the "pull down to refresh" behavior.
* By default it is false.
*/
property alias supportsRefreshing: scrollView.supportsRefreshing
property bool supportsRefreshing: false
/**
* \property QtQuick.Flickable ScrollablePage::flickable
* The main Flickable item of this page.
*/
property alias flickable: scrollView.flickableItem
readonly property Flickable flickable: itemsParent.flickable
/**
* \property Qt.ScrollBarPolicy ScrollablePage::verticalScrollBarPolicy
* The vertical scrollbar policy.
*/
property alias verticalScrollBarPolicy: scrollView.verticalScrollBarPolicy
property int verticalScrollBarPolicy
/**
* \property Qt.ScrollBarPolicy ScrollablePage::horizontalScrollBarPolicy
* The horizontal scrollbar policy.
*/
property alias horizontalScrollBarPolicy: scrollView.horizontalScrollBarPolicy
property int horizontalScrollBarPolicy: QQC2.ScrollBar.AlwaysOff
default property alias pageData: itemsParent.data
property alias pageChildren: itemsParent.children
/**
* The main content Item of this page.
* In the case of a ListView or GridView, both contentItem and flickable
* will be a pointer to the ListView (or GridView).
* @note This can't be contentItem as Page's contentItem is final.
* @deprecated here for compatibility, will be removed in next Frameworks release
*/
default property QtObject mainItem
property QtObject mainItem
/**
* If true, and if flickable is an item view, like a ListView or
......@@ -114,24 +116,17 @@ Page {
*/
property bool keyboardNavigationEnabled: true
contentHeight: root.flickable.contentHeight
contentHeight: flickable ? flickable.contentHeight : 0
implicitHeight: ((header && header.visible) ? header.implicitHeight : 0) + ((footer && footer.visible) ? footer.implicitHeight : 0) + contentHeight + topPadding + bottomPadding
implicitWidth: root.flickable.contentItem ? root.flickable.contentItem.implicitWidth : contentItem.implicitWidth + leftPadding + rightPadding
implicitWidth: flickable
? (flickable.contentItem ? flickable.contentItem.implicitWidth : contentItem.implicitWidth + leftPadding + rightPadding)
: 0
Theme.inherit: false
Theme.colorSet: flickable && flickable.hasOwnProperty("model") ? Theme.View : Theme.Window
clip: true
contentItem: RefreshableScrollView {
contentItem: QQC2.ScrollView {
id: scrollView
//NOTE: here to not expose it to public api
property QtObject oldMainItem
page: root
clip: true
topPadding: contentItem === flickableItem ? 0 : root.topPadding
leftPadding: root.leftPadding
rightPadding: root.rightPadding
bottomPadding: contentItem === flickableItem ? 0 : root.bottomPadding
anchors {
top: (root.header && root.header.visible)
? root.header.bottom
......@@ -142,38 +137,156 @@ Page {
bottom: (root.footer && root.footer.visible) ? root.footer.top : parent.bottom
left: parent.left
right: parent.right
topMargin: root.refreshing ? busyIndicatorFrame.height : 0
Behavior on topMargin {
NumberAnimation {
easing.type: Easing.InOutQuad
duration: Units.longDuration
}
}
}
QQC2.ScrollBar.horizontal.policy: root.horizontalScrollBarPolicy
QQC2.ScrollBar.vertical.policy: root.verticalScrollBarPolicy
}
anchors.topMargin: 0
Keys.forwardTo: root.keyboardNavigationEnabled && root.flickable
? (("currentItem" in root.flickable) && root.flickable.currentItem ?
[ root.flickable.currentItem, root.flickable ] : [ root.flickable ])
: []
//HACK to get the mainItem as the last one, all the other eventual items as an overlay
//no idea if is the way the user expects
onMainItemChanged: {
if (mainItem instanceof Item) {
scrollView.contentItem = mainItem
mainItem.focus = true
} else if (mainItem instanceof T.Drawer) {
//don't try to reparent drawers
return;
} else if (mainItem instanceof KT.OverlaySheet) {
//reparent sheets
if (mainItem.parent === root || mainItem.parent === null) {
mainItem.parent = root;
data: [
Item {
id: scrollingArea
width: itemsParent.flickable.width
height: Math.max(root.flickable.height, implicitHeight)
implicitHeight: {
let impl = 0;
for (let i in itemsParent.visibleChildren) {
let child = itemsParent.visibleChildren[i];
impl = Math.max(impl, child.implicitHeight);
}
return impl + itemsParent.anchors.topMargin + itemsParent.anchors.bottomMargin;
}
implicitWidth: {
let impl = 0;
for (let i in itemsParent.children) {
let child = itemsParent.children[i];
impl = Math.max(impl, child.implicitWidth);
}
return impl + itemsParent.anchors.leftMargin + itemsParent.anchors.rightMargin;
}
Item {
id: itemsParent
property Flickable flickable
anchors {
fill: parent
leftMargin: root.leftPadding
topMargin: root.topPadding
rightMargin: root.rightPadding
bottomMargin: root.bottomPadding
}
onChildrenChanged: {
let child = children[children.length - 1];
if (child instanceof KT.OverlaySheet) {
// Reparent sheets, needs to be done before Component.onCompleted
if (child.parent === itemsParent || child.parent === null) {
child.parent = root;
}
} else if (child instanceof QQC2.ScrollView) {
print("Warning: it's not supported to have ScrollViews inside a ScrollablePage")
}
}
}
},
Item {
id: busyIndicatorFrame
z: 99
y: root.flickable.verticalLayoutDirection === ListView.BottomToTop
? -root.flickable.contentY + root.flickable.originY + height
: -root.flickable.contentY + root.flickable.originY - height + scrollView.y
width: root.flickable.width
height: busyIndicator.height + Units.gridUnit * 2
QQC2.BusyIndicator {
id: busyIndicator
z: 1
anchors.centerIn: parent
running: root.refreshing
visible: root.refreshing
//Android busywidget QQC seems to be broken at custom sizes
}
Rectangle {
id: spinnerProgress
anchors {
fill: busyIndicator
margins: Math.ceil(Units.smallSpacing)
}
radius: width
visible: supportsRefreshing && !refreshing && progress > 0
color: "transparent"
opacity: 0.8
border.color: Theme.backgroundColor
border.width: Math.ceil(Units.smallSpacing)
property real progress: supportsRefreshing && !refreshing ? (parent.y/busyIndicatorFrame.height) : 0
}
ConicalGradient {
source: spinnerProgress
visible: spinnerProgress.visible
anchors.fill: spinnerProgress
gradient: Gradient {
GradientStop { position: 0.00; color: Theme.highlightColor }
GradientStop { position: spinnerProgress.progress; color: Theme.highlightColor }
GradientStop { position: spinnerProgress.progress + 0.01; color: "transparent" }
GradientStop { position: 1.00; color: "transparent" }
}
}
onYChanged: {
if (!supportsRefreshing) {
return;
}
if (!root.refreshing && y > busyIndicatorFrame.height/2 + topPadding) {
refreshTriggerTimer.running = true;
} else {
refreshTriggerTimer.running = false;
}
}
Timer {
id: refreshTriggerTimer
interval: 500
onTriggered: {
if (!root.refreshing && parent.y > busyIndicatorFrame.height/2 + topPadding) {
root.refreshing = true;
}
}
}
}
]
Component.onCompleted: {
for (let i in itemsParent.children) {
let child = itemsParent.children[i];
if (child instanceof Flickable) {
// If there were more flickable children, take the last one, as behavior compatibility
// with old internal ScrollView
itemsParent.flickable = child;
child.keyNavigationEnabled = true;
child.keyNavigationWraps = false;
} else if (!(child instanceof KT.OverlaySheet)) {
child.anchors.left = itemsParent.left;
child.anchors.right = itemsParent.right;
}
root.data.push(mainItem);
return;
}
if (scrollView.oldMainItem && scrollView.oldMainItem instanceof Item
&& (typeof applicationWindow === 'undefined'|| scrollView.oldMainItem.parent !== applicationWindow().overlay)) {
scrollView.oldMainItem.parent = overlay
if (itemsParent.flickable) {
scrollView.contentItem = flickable;
flickable.parent = scrollView;
// Some existing code incorrectly uses anchors
flickable.anchors.fill = undefined;
flickable.anchors.left = undefined;
flickable.anchors.right = undefined;
flickable.anchors.top = undefined;
flickable.anchors.bottom = undefined;
} else {
itemsParent.flickable = scrollView.contentItem;
scrollingArea.parent = scrollView.contentItem.contentItem;
}
scrollView.oldMainItem = mainItem
itemsParent.flickable.flickableDirection = Flickable.VerticalFlick;
}
}
/*
* SPDX-FileCopyrightText: 2015 Marco Martin <mart@kde.org>
*
* SPDX-License-Identifier: LGPL-2.0-or-later
*/
import QtQuick 2.15
import QtQuick.Window 2.15
import QtQuick.Controls 2.0 as QQC2
import QtGraphicalEffects 1.0
import QtQuick.Layouts 1.2
import QtQml 2.15
import org.kde.kirigami 2.4
import "../templates/private" as P
P.ScrollView {
id: root
/**
* type: bool
* If true the list is asking for refresh and will show a loading spinner.
* it will automatically be set to true when the user pulls down enough the list.
* This signals the application logic to start its refresh procedure.
* The application itself will have to set back this property to false when done.
*/
property bool refreshing: false
/**
* type: bool
* If true the list supports the "pull down to refresh" behavior.
*/
property bool supportsRefreshing: false
/**
* Warning: These duplicate the padding properties from P.ScrollView. This
* is apparently allowed by QML but very unexpected.
*
* TODO KF6: Fix this.
*/
/**
* leftPadding: int
* default contents padding at left
*/
property int leftPadding: Units.gridUnit
/**
* topPadding: int
* default contents padding at top
*/
property int topPadding: Units.gridUnit
/**
* rightPadding: int
* default contents padding at right
*/
property int rightPadding: Units.gridUnit
/**
* bottomPadding: int
* default contents padding at bottom
*/
property int bottomPadding: Units.gridUnit
/**
* Set when this scrollview manages a whole page
*/
property Page page
property Item _swipeFilter
onRefreshingChanged: flickableItem.topMargin = topPadding + (refreshing ? busyIndicatorFrame.height : 0);
children: [
Item {
id: busyIndicatorFrame
z: 99
y: root.flickableItem.verticalLayoutDirection === ListView.BottomToTop
? -root.flickableItem.contentY+root.flickableItem.originY+height
: -root.flickableItem.contentY+root.flickableItem.originY-height
width: root.flickableItem.width
height: busyIndicator.height + Units.gridUnit * 2
QQC2.BusyIndicator {
id: busyIndicator
anchors.centerIn: parent
running: root.refreshing
visible: root.refreshing
//Android busywidget QQC seems to be broken at custom sizes
}
Rectangle {
id: spinnerProgress
anchors {
fill: busyIndicator
margins: Math.ceil(Units.smallSpacing)
}
radius: width
visible: supportsRefreshing && !refreshing && progress > 0
color: "transparent"
opacity: 0.8
border.color: Theme.backgroundColor
border.width: Math.ceil(Units.smallSpacing)
property real progress: supportsRefreshing && !refreshing ? (parent.y/busyIndicatorFrame.height) : 0
}
ConicalGradient {
source: spinnerProgress
visible: spinnerProgress.visible
anchors.fill: spinnerProgress
gradient: Gradient {
GradientStop { position: 0.00; color: Theme.highlightColor }
GradientStop { position: spinnerProgress.progress; color: Theme.highlightColor }
GradientStop { position: spinnerProgress.progress + 0.01; color: "transparent" }
GradientStop { position: 1.00; color: "transparent" }
}
}
onYChanged: {
//it's overshooting enough and not reachable: start countdown for reachability
if (y > root.topPadding + Units.gridUnit && (typeof(applicationWindow) == "undefined" || !applicationWindow().reachableMode)) {
overshootResetTimer.running = true;
//not reachable and not overshooting enough, stop reachability countdown
} else if (typeof(applicationWindow) == "undefined" || !applicationWindow().reachableMode) {
//it's important it doesn't restart
overshootResetTimer.running = false;
}
if (!supportsRefreshing) {
return;
}
if (!root.refreshing && y > busyIndicatorFrame.height/2 + topPadding) {
refreshTriggerTimer.running = true;
} else {
refreshTriggerTimer.running = false;
}
}
Timer {
id: refreshTriggerTimer
interval: 500
onTriggered: {
if (!root.refreshing && parent.y > busyIndicatorFrame.height/2 + topPadding) {
root.refreshing = true;
}
}
}
Connections {
enabled: typeof applicationWindow !== "undefined"
target: typeof applicationWindow !== "undefined" ? applicationWindow() : null
function onReachableModeChanged() {
overshootResetTimer.running = applicationWindow().reachableMode;
}
}
Timer {
id: overshootResetTimer
interval: (typeof applicationWindow !== "undefined" && applicationWindow().reachableMode) ? 8000 : 2000
onTriggered: {
//put it there because widescreen may have changed since timer start
if (!Settings.isMobile || (typeof applicationWindow !== "undefined" && applicationWindow().wideScreen) || root.flickableItem.verticalLayoutDirection === ListView.BottomToTop) {
return;
}
applicationWindow().reachableMode = !applicationWindow().reachableMode;
}
}
Binding {
target: root.flickableItem
property: "flickableDirection"
value: Flickable.VerticalFlick
}
Binding {
target: root.flickableItem
property: "bottomMargin"
value: root.page.bottomPadding
}
Binding {
target: root.contentItem
property: "width"
restoreMode: Binding.RestoreBinding
value: root.flickableItem.width
when: root.horizontalScrollBarPolicy == Qt.ScrollBarAlwaysOff
}
}
]
Component.onCompleted: leftPaddingChanged()
onRightPaddingChanged: leftPaddingChanged()
onLeftPaddingChanged: {
//for gridviews do apply margins
if (root.contentItem == root.flickableItem) {
if (typeof root.flickableItem.cellWidth != "undefined") {
flickableItem