Kirigami2

ScrollablePage.qml
1/*
2 * SPDX-FileCopyrightText: 2015 Marco Martin <mart@kde.org>
3 *
4 * SPDX-License-Identifier: LGPL-2.0-or-later
5 */
6
7import QtQuick
8import QtQml
9import QtQuick.Controls as QQC2
10import org.kde.kirigami as Kirigami
11import org.kde.kirigami.templates as KT
12import "private"
13
14
15// TODO KF6: undo many workarounds to make existing code work?
16
17/**
18 * @brief ScrollablePage is a Page that holds scrollable content, such as a ListView.
19 *
20 * Scrolling and scrolling indicators will be automatically managed.
21 *
22 * Example usage:
23 * @code
24 * ScrollablePage {
25 * id: root
26 * // The page will automatically be scrollable
27 * Rectangle {
28 * width: root.width
29 * height: 99999
30 * }
31 * }
32 * @endcode
33 *
34 * @warning Do not put a ScrollView inside of a ScrollablePage; children of a ScrollablePage are already inside a ScrollView.
35 *
36 * Another behavior added by this class is a "scroll down to refresh" behavior
37 * It also can give the contents of the flickable to have more top margins in order
38 * to make possible to scroll down the list to reach it with the thumb while using the
39 * phone with a single hand.
40 *
41 * Implementations should handle the refresh themselves as follows
42 *
43 * Example usage:
44 * @code
45 * Kirigami.ScrollablePage {
46 * id: view
47 * supportsRefreshing: true
48 * onRefreshingChanged: {
49 * if (refreshing) {
50 * myModel.refresh();
51 * }
52 * }
53 * ListView {
54 * // NOTE: MyModel doesn't come from the components,
55 * // it's purely an example on how it can be used together
56 * // some application logic that can update the list model
57 * // and signals when it's done.
58 * model: MyModel {
59 * onRefreshDone: view.refreshing = false;
60 * }
61 * delegate: ItemDelegate {}
62 * }
63 * }
64 * [...]
65 * @endcode
66 */
67Kirigami.Page {
68 id: root
69
70//BEGIN properties
71 /**
72 * @brief This property tells whether the list is asking for a refresh.
73 *
74 * This property will automatically be set to true when the user pulls the list down enough,
75 * which in return, shows a loading spinner. When this is set to true, it signals
76 * the application logic to start its refresh procedure.
77 *
78 * default: ``false``
79 *
80 * @note The application itself will have to set back this property to false when done.
81 */
82 property bool refreshing: false
83
84 /**
85 * @brief This property sets whether scrollable page supports "pull down to refresh" behaviour.
86 *
87 * default: ``false``
88 */
89 property bool supportsRefreshing: false
90
91 /**
92 * @brief This property holds the main Flickable item of this page.
93 * @deprecated here for compatibility; will be removed in KF6.
94 */
95 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
96 onFlickableChanged: scrollView.contentItem = flickable;
97
98 /**
99 * @brief This property sets the vertical scrollbar policy.
100 * @property Qt::ScrollBarPolicy verticalScrollBarPolicy
101 */
102 property int verticalScrollBarPolicy
103
104 /**
105 * @brief Set if the vertical scrollbar should be interactable.
106 * @property bool verticalScrollBarInteractive
107 */
108 property bool verticalScrollBarInteractive: true
109
110 /**
111 * @brief This property sets the horizontal scrollbar policy.
112 * @property Qt::ScrollBarPolicy horizontalScrollBarPolicy
113 */
114 property int horizontalScrollBarPolicy: QQC2.ScrollBar.AlwaysOff
115
116 /**
117 * @brief Set if the horizontal scrollbar should be interactable.
118 * @property bool horizontalScrollBarInteractive
119 */
120 property bool horizontalScrollBarInteractive: true
121
122 default property alias scrollablePageData: itemsParent.data
123 property alias scrollablePageChildren: itemsParent.children
124
125 /*
126 * @deprecated here for compatibility; will be removed in KF6.
127 */
128 property QtObject mainItem
129 onMainItemChanged: {
130 print("Warning: the mainItem property is deprecated");
131 scrollablePageData.push(mainItem);
132 }
134 /**
135 * @brief This property sets whether it is possible to navigate the items in a view that support it.
136 *
137 * If true, and if flickable is an item view (e.g. ListView, GridView), it will be possible
138 * to navigate the view current items with keyboard up/down arrow buttons.
139 * Also, any key event will be forwarded to the current list item.
140 *
141 * default: ``true``
142 */
143 property bool keyboardNavigationEnabled: true
144//END properties
145
146//BEGIN FUNCTIONS
147/**
148 * @brief This method checks whether a particular child item is in view, and scrolls
149 * the page to center the item if it is not.
150 *
151 * If the page is a View, the view should handle this by itself, but if the page is a
152 * manually handled layout, this needs to be done manually. Otherwise, if the user
153 * passes focus to an item with e.g. keyboard navigation, this may be outside the
154 * visible area.
155 *
156 * When called, this method will place the visible area such that the item at the
157 * center if any part of it is currently outside. If the item is larger than the viewable
158 * area in either direction, the area will be scrolled such that the top left corner
159 * is visible.
160 *
161 * @code
162 * Kirigami.ScrollablePage {
163 * id: page
164 * ColumnLayout {
165 * Repeater {
166 * model: 100
167 * delegate: QQC2.Button {
168 * text: modelData
169 * onFocusChanged: if (focus) page.ensureVisible(this)
170 * }
171 * }
172 * }
173 * }
174 * @endcode
175 *
176 * @param item The item that should be in the visible area of the flickable. Item coordinates need to be in the flickable's coordinate system.
177 * @param xOffset,yOffset (optional) Offsets to align the item's and the flickable's coordinate system<
178 */
179 function ensureVisible(item: Item, xOffset: int, yOffset: int) {
180 var actualItemX = item.x + (xOffset ?? 0)
181 var actualItemY = item.y + (yOffset ?? 0)
182 var viewXPosition = (item.width <= root.flickable.width)
183 ? Math.round(actualItemX + item.width / 2 - root.flickable.width / 2)
184 : actualItemX
185 var viewYPosition = (item.height <= root.flickable.height)
186 ? Math.round(actualItemY + item.height / 2 - root.flickable.height / 2)
187 : actualItemY
188 if (actualItemX < root.flickable.contentX) {
189 root.flickable.contentX = Math.max(0, viewXPosition)
190 } else if ((actualItemX + item.width) > (root.flickable.contentX + root.flickable.width)) {
191 root.flickable.contentX = Math.min(root.flickable.contentWidth - root.flickable.width, viewXPosition)
192 }
193 if (actualItemY < root.flickable.contentY) {
194 root.flickable.contentY = Math.max(0, viewYPosition)
195 } else if ((actualItemY + item.height) > (root.flickable.contentY + root.flickable.height)) {
196 root.flickable.contentY = Math.min(root.flickable.contentHeight - root.flickable.height, viewYPosition)
197 }
198 root.flickable.returnToBounds()
199 }
200//END FUNCTIONS
201
202 implicitWidth: flickable?.contentItem?.implicitWidth
203 ?? Math.max(implicitBackgroundWidth + leftInset + rightInset,
204 contentWidth + leftPadding + rightPadding,
205 implicitHeaderWidth,
206 implicitFooterWidth)
207
208 implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset,
209 contentHeight + topPadding + bottomPadding
210 + (implicitHeaderHeight > 0 ? implicitHeaderHeight + spacing : 0)
211 + (implicitFooterHeight > 0 ? implicitFooterHeight + spacing : 0))
212
213 contentHeight: flickable?.contentHeight ?? 0
214
215 Kirigami.Theme.inherit: false
216 Kirigami.Theme.colorSet: flickable?.hasOwnProperty("model") ? Kirigami.Theme.View : Kirigami.Theme.Window
217
218 Keys.forwardTo: {
219 if (root.keyboardNavigationEnabled && root.flickable) {
220 if (("currentItem" in root.flickable) && root.flickable.currentItem) {
221 return [ root.flickable.currentItem, root.flickable ];
222 } else {
223 return [ root.flickable ];
224 }
225 } else {
226 return [];
227 }
228 }
229
230 contentItem: QQC2.ScrollView {
231 id: scrollView
232 anchors {
233 top: root.header?.visible
234 ? root.header.bottom
235 : parent.top
236 bottom: root.footer?.visible ? root.footer.top : parent.bottom
237 left: parent.left
238 right: parent.right
239 }
240 clip: true
241 QQC2.ScrollBar.horizontal.policy: root.horizontalScrollBarPolicy
242 QQC2.ScrollBar.horizontal.interactive: root.horizontalScrollBarInteractive
243 QQC2.ScrollBar.vertical.policy: root.verticalScrollBarPolicy
244 QQC2.ScrollBar.vertical.interactive: root.verticalScrollBarInteractive
245 }
246
247 data: [
248 // Has to be a MouseArea that accepts events otherwise touch events on Wayland will get lost
249 MouseArea {
250 id: scrollingArea
251 width: root.horizontalScrollBarPolicy === QQC2.ScrollBar.AlwaysOff ? root.flickable.width : Math.max(root.flickable.width, implicitWidth)
252 height: Math.max(root.flickable.height, implicitHeight)
253 implicitWidth: {
254 let implicit = 0;
255 for (const child of itemsParent.visibleChildren) {
256 if (child.implicitWidth > 0) {
257 implicit = Math.max(implicit, child.implicitWidth);
258 }
259 }
260 return implicit + itemsParent.anchors.leftMargin + itemsParent.anchors.rightMargin;
261 }
262 implicitHeight: {
263 let implicit = 0;
264 for (const child of itemsParent.visibleChildren) {
265 if (child.implicitHeight > 0) {
266 implicit = Math.max(implicit, child.implicitHeight);
267 }
268 }
269 return implicit + itemsParent.anchors.topMargin + itemsParent.anchors.bottomMargin;
270 }
271 Item {
272 id: itemsParent
273 property Flickable flickable
274 anchors {
275 fill: parent
276 topMargin: root.topPadding
277 leftMargin: root.leftPadding
278 rightMargin: root.rightPadding
279 bottomMargin: root.bottomPadding
280 }
281 onChildrenChanged: {
282 const child = children[children.length - 1];
283 if (child instanceof QQC2.ScrollView) {
284 print("Warning: it's not supported to have ScrollViews inside a ScrollablePage")
285 }
286 }
287 }
288 Binding {
289 target: root.flickable
290 property: "bottomMargin"
291 value: root.bottomPadding
292 restoreMode: Binding.RestoreBinding
293 }
294 },
295
296 Loader {
297 id: busyIndicatorLoader
298 active: root.supportsRefreshing
299 sourceComponent: PullDownIndicator {
300 parent: root
301 active: root.refreshing
302 onTriggered: root.refreshing = true
303 }
304 }
305 ]
306
307 Component.onCompleted: {
308 let flickableFound = false;
309 for (const child of itemsParent.data) {
310 if (child instanceof Flickable) {
311 // If there were more flickable children, take the last one, as behavior compatibility
312 // with old internal ScrollView
313 child.activeFocusOnTab = true;
314 root.flickable = child;
315 flickableFound = true;
316 if (child instanceof ListView) {
317 child.keyNavigationEnabled = true;
318 child.keyNavigationWraps = false;
319 }
320 } else if (child instanceof Item) {
321 child.anchors.left = itemsParent.left;
322 child.anchors.right = itemsParent.right;
323 } else if (child instanceof KT.OverlaySheet) {
324 // Reparent sheets, needs to be done before Component.onCompleted
325 if (child.parent === itemsParent || child.parent === null) {
326 child.parent = root;
327 }
328 }
329 }
330
331 if (flickableFound) {
332 scrollView.contentItem = root.flickable;
333 root.flickable.parent = scrollView;
334 // 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)
335 Qt.callLater(() => {
336 if (root.activeFocus) {
337 root.flickable.forceActiveFocus();
338 }
339 });
340 // Some existing code incorrectly uses anchors
341 root.flickable.anchors.fill = undefined;
342 root.flickable.anchors.top = undefined;
343 root.flickable.anchors.left = undefined;
344 root.flickable.anchors.right = undefined;
345 root.flickable.anchors.bottom = undefined;
346 scrollingArea.visible = false;
347 } else {
348 scrollView.contentItem = root.flickable;
349 scrollingArea.parent = root.flickable.contentItem;
350 scrollingArea.visible = true;
351 root.flickable.contentHeight = Qt.binding(() => scrollingArea.implicitHeight - root.flickable.topMargin - root.flickable.bottomMargin);
352 root.flickable.contentWidth = Qt.binding(() => scrollingArea.implicitWidth);
353 scrollView.forceActiveFocus(Qt.TabFocusReason); // QTBUG-44043 : Focus on currentItem instead of pageStack itself
354 }
355 root.flickable.flickableDirection = Flickable.VerticalFlick;
356
357 // HACK: Qt's default flick deceleration is too high, and we can't change it from plasma-integration, see QTBUG-121500
358 root.flickable.flickDeceleration = 1500;
359 root.flickable.maximumFlickVelocity = 5000;
360 }
361}
A pull-down to refresh indicator that can be added to any Flickable or ScrollablePage.
void ensureVisible(Item item, int xOffset, int yOffset)
This method checks whether a particular child item is in view, and scrolls the page to center the ite...
int horizontalScrollBarPolicy
This property sets the horizontal scrollbar policy.
int verticalScrollBarPolicy
This property sets the vertical scrollbar policy.
bool keyboardNavigationEnabled
This property sets whether it is possible to navigate the items in a view that support it.
bool verticalScrollBarInteractive
Set if the vertical scrollbar should be interactable.
bool refreshing
This property tells whether the list is asking for a refresh.
Flickable flickable
This property holds the main Flickable item of this page.
bool supportsRefreshing
This property sets whether scrollable page supports "pull down to refresh" behaviour.
bool horizontalScrollBarInteractive
Set if the horizontal scrollbar should be interactable.
QAction * print(const QObject *recvr, const char *slot, QObject *parent)
QTextStream & left(QTextStream &stream)
QTextStream & right(QTextStream &stream)
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri Mar 7 2025 11:49:05 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.