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

second attempt of Delete and port away from internal ScrollView

This reintroduces the refactor of ScrollablePage which was reverted due to last minute regressions.

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 13b0f558
Pipeline #204948 passed with stage
in 4 minutes and 13 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,44 @@ 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 scrollablePageData: itemsParent.data
property alias scrollablePageChildren: 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
onMainItemChanged: {
print("Warning: the mainItem property is deprecated");
scrollablePageData.push(mainItem);
}
/**
* If true, and if flickable is an item view, like a ListView or
......@@ -114,24 +120,48 @@ Page {
*/
property bool keyboardNavigationEnabled: true
contentHeight: root.flickable.contentHeight
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
contentHeight: flickable ? flickable.contentHeight : 0
implicitHeight: {
let height = contentHeight + topPadding + bottomPadding;
if (header && header.visible) {
height += header.implicitHeight;
}
if (footer && footer.visible) {
height += footer.implicitHeight;
}
return height;
}
implicitWidth: {
let width = 0;
if (flickable) {
if (flickable.contentItem) {
return flickable.contentItem.implicitWidth;
} else {
return contentItem.implicitWidth + leftPadding + rightPadding;
}
} else {
return 0;
}
}
Theme.inherit: false
Theme.colorSet: flickable && flickable.hasOwnProperty("model") ? Theme.View : Theme.Window
clip: true
contentItem: RefreshableScrollView {
Keys.forwardTo: {
if (root.keyboardNavigationEnabled && root.flickable) {
if (("currentItem" in root.flickable) && root.flickable.currentItem) {
return [ root.flickable.currentItem, root.flickable ];
} else {
return [ root.flickable ];
}
} else {
return [];
}
}
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 +172,168 @@ Page {
bottom: (root.footer && root.footer.visible) ? root.footer.top : parent.bottom
left: parent.left
right: parent.right
topMargin: root.refreshing ? busyIndicatorLoader.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
data: [
// Has to be a MouseArea that accepts events otherwise touch events on Wayland will get lost
MouseArea {
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;
}
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 QQC2.ScrollView) {
print("Warning: it's not supported to have ScrollViews inside a ScrollablePage")
}
}
}
Binding {
target: root.flickable
property: "bottomMargin"
value: root.bottomPadding
}
},
Keys.forwardTo: root.keyboardNavigationEnabled && root.flickable
? (("currentItem" in root.flickable) && root.flickable.currentItem ?
[ root.flickable.currentItem, root.flickable ] : [ root.flickable ])
: []
Loader {
id: busyIndicatorLoader
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: Units.gridUnit * 4
active: root.supportsRefreshing
//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;
sourceComponent: Item {
id: busyIndicatorFrame
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 ? (busyIndicatorLoader.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" }
}
}
Connections {
target: busyIndicatorLoader
function onYChanged() {
if (!supportsRefreshing) {
return;
}
if (!root.refreshing && busyIndicatorLoader.y > busyIndicatorFrame.height/2 + topPadding) {
refreshTriggerTimer.running = true;
} else {
refreshTriggerTimer.running = false;
}
}
}
Timer {
id: refreshTriggerTimer
interval: 500
onTriggered: {
if (!root.refreshing && busyIndicatorLoader.y > busyIndicatorFrame.height/2 + topPadding) {
root.refreshing = true;
}
}
}
}
}
]
Component.onCompleted: {
for (let i in itemsParent.data) {
let child = itemsParent.data[i];
if (child instanceof Flickable) {
// If there were more flickable children, take the last one, as behavior compatibility
// with old internal ScrollView
child.activeFocusOnTab = true;
itemsParent.flickable = child;
child.keyNavigationEnabled = true;
child.keyNavigationWraps = false;
} else if (child instanceof Item) {
child.anchors.left = itemsParent.left;
child.anchors.right = itemsParent.right;
} else if (child instanceof KT.OverlaySheet) {
// Reparent sheets, needs to be done before Component.onCompleted
if (child.parent === itemsParent || child.parent === null) {
child.parent = root;
}
}
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;
// 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)
Qt.callLater( () => {if (root.activeFocus) itemsParent.flickable.forceActiveFocus()});
// 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 = itemsParent.flickable.contentItem;
itemsParent.flickable.contentHeight = Qt.binding(() => { return scrollingArea.implicitHeight - itemsParent.flickable.topMargin - itemsParent.flickable.bottomMargin });
itemsParent.flickable.contentWidth = Qt.binding(() => { return scrollingArea.implicitWidth });
}
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() :</