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

KDE's Doxygen guidelines are available online.