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.width);
258 } else {
259 implicit = Math.max(implicit, child.implicitWidth);
260 }
261 }
262 return implicit + itemsParent.anchors.leftMargin + itemsParent.anchors.rightMargin;
263 }
264 implicitHeight: {
265 let implicit = 0;
266 for (const child of itemsParent.visibleChildren) {
267 if (child.implicitHeight <= 0) {
268 implicit = Math.max(implicit, child.height);
269 } else {
270 implicit = Math.max(implicit, child.implicitHeight);
271 }
272 }
273 return implicit + itemsParent.anchors.topMargin + itemsParent.anchors.bottomMargin;
274 }
275 Item {
276 id: itemsParent
277 property Flickable flickable
278 anchors {
279 fill: parent
280 topMargin: root.topPadding
281 leftMargin: root.leftPadding
282 rightMargin: root.rightPadding
283 bottomMargin: root.bottomPadding
284 }
285 onChildrenChanged: {
286 const child = children[children.length - 1];
287 if (child instanceof QQC2.ScrollView) {
288 print("Warning: it's not supported to have ScrollViews inside a ScrollablePage")
289 }
290 }
291 }
292 Binding {
293 target: root.flickable
294 property: "bottomMargin"
295 value: root.bottomPadding
296 restoreMode: Binding.RestoreBinding
297 }
298 },
299
300 Loader {
301 id: busyIndicatorLoader
302 active: root.supportsRefreshing
303 sourceComponent: PullDownIndicator {
304 parent: root
305 active: root.refreshing
306 onTriggered: root.refreshing = true
307 }
308 }
309 ]
310
311 Component.onCompleted: {
312 let flickableFound = false;
313 for (const child of itemsParent.data) {
314 if (child instanceof Flickable) {
315 // If there were more flickable children, take the last one, as behavior compatibility
316 // with old internal ScrollView
317 child.activeFocusOnTab = true;
318 root.flickable = child;
319 flickableFound = true;
320 if (child instanceof ListView) {
321 child.keyNavigationEnabled = true;
322 child.keyNavigationWraps = false;
323 }
324 } else if (child instanceof Item) {
325 child.anchors.left = itemsParent.left;
326 child.anchors.right = itemsParent.right;
327 } else if (child instanceof KT.OverlaySheet) {
328 // Reparent sheets, needs to be done before Component.onCompleted
329 if (child.parent === itemsParent || child.parent === null) {
330 child.parent = root;
331 }
332 }
333 }
334
335 if (flickableFound) {
336 scrollView.contentItem = root.flickable;
337 root.flickable.parent = scrollView;
338 // 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)
339 Qt.callLater(() => {
340 if (root.activeFocus) {
341 root.flickable.forceActiveFocus();
342 }
343 });
344 // Some existing code incorrectly uses anchors
345 root.flickable.anchors.fill = undefined;
346 root.flickable.anchors.top = undefined;
347 root.flickable.anchors.left = undefined;
348 root.flickable.anchors.right = undefined;
349 root.flickable.anchors.bottom = undefined;
350 scrollingArea.visible = false;
351 } else {
352 scrollView.contentItem = root.flickable;
353 scrollingArea.parent = root.flickable.contentItem;
354 scrollingArea.visible = true;
355 root.flickable.contentHeight = Qt.binding(() => scrollingArea.implicitHeight - root.flickable.topMargin - root.flickable.bottomMargin);
356 root.flickable.contentWidth = Qt.binding(() => scrollingArea.implicitWidth);
357 scrollView.forceActiveFocus(Qt.TabFocusReason); // QTBUG-44043 : Focus on currentItem instead of pageStack itself
358 }
359 root.flickable.flickableDirection = Flickable.VerticalFlick;
360
361 // HACK: Qt's default flick deceleration is too high, and we can't change it from plasma-integration, see QTBUG-121500
362 root.flickable.flickDeceleration = 1500;
363 root.flickable.maximumFlickVelocity = 5000;
364 }
365}
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 Feb 21 2025 11:47:53 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.