Kirigami2

ScrollablePage.qml
1 /*
2  * SPDX-FileCopyrightText: 2015 Marco Martin <[email protected]>
3  *
4  * SPDX-License-Identifier: LGPL-2.0-or-later
5  */
6 
7 import QtQuick 2.15
8 import QtQml 2.15
9 import QtQuick.Controls 2.15 as QQC2
10 import QtGraphicalEffects 1.0 as GE
11 import org.kde.kirigami 2.19 as Kirigami
12 import org.kde.kirigami.templates 2.2 as KT
13 import "private"
14 
15 
16 // TODO KF6: undo many workarounds to make existing code work?
17 
18 /**
19  * @brief ScrollablePage is a Page that holds scrollable content, such as a ListView.
20  *
21  * Scrolling and scrolling indicators will be automatically managed.
22  *
23  * Example usage:
24  * @code
25  * ScrollablePage {
26  * id: root
27  * // The page will automatically be scrollable
28  * Rectangle {
29  * width: root.width
30  * height: 99999
31  * }
32  * }
33  * @endcode
34  *
35  * @warning Do not put a ScrollView inside of a ScrollablePage; children of a ScrollablePage are already inside a ScrollView.
36  *
37  * Another behavior added by this class is a "scroll down to refresh" behavior
38  * It also can give the contents of the flickable to have more top margins in order
39  * to make possible to scroll down the list to reach it with the thumb while using the
40  * phone with a single hand.
41  *
42  * Implementations should handle the refresh themselves as follows
43  *
44  * Example usage:
45  * @code
46  * Kirigami.ScrollablePage {
47  * id: view
48  * supportsRefreshing: true
49  * onRefreshingChanged: {
50  * if (refreshing) {
51  * myModel.refresh();
52  * }
53  * }
54  * ListView {
55  * // NOTE: MyModel doesn't come from the components,
56  * // it's purely an example on how it can be used together
57  * // some application logic that can update the list model
58  * // and signals when it's done.
59  * model: MyModel {
60  * onRefreshDone: view.refreshing = false;
61  * }
62  * delegate: BasicListItem {}
63  * }
64  * }
65  * [...]
66  * @endcode
67  */
68 Kirigami.Page {
69  id: root
70 
71 //BEGIN properties
72  /**
73  * @brief This property tells whether the list is asking for a refresh.
74  *
75  * This property will automatically be set to true when the user pulls the list down enough,
76  * which in return, shows a loading spinner. When this is set to true, it signals
77  * the application logic to start its refresh procedure.
78  *
79  * default: ``false``
80  *
81  * @note The application itself will have to set back this property to false when done.
82  */
83  property bool refreshing: false
84 
85  /**
86  * @brief This property sets whether scrollable page supports "pull down to refresh" behaviour.
87  *
88  * default: ``false``
89  */
90  property bool supportsRefreshing: false
91 
92  /**
93  * @brief This property holds the main Flickable item of this page.
94  * @deprecated here for compatibility; will be removed in KF6.
95  */
96  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
97  onFlickableChanged: scrollView.contentItem = flickable;
98 
99  /**
100  * @brief This property sets the vertical scrollbar policy.
101  * @property Qt::ScrollBarPolicy verticalScrollBarPolicy
102  */
103  property int verticalScrollBarPolicy
104 
105  /**
106  * @brief This property sets the horizontal scrollbar policy.
107  * @property Qt::ScrollBarPolicy horizontalScrollBarPolicy
108  */
109  property int horizontalScrollBarPolicy: QQC2.ScrollBar.AlwaysOff
110 
111  default property alias scrollablePageData: itemsParent.data
112  property alias scrollablePageChildren: itemsParent.children
113 
114  /*
115  * @deprecated here for compatibility; will be removed in KF6.
116  */
117  property QtObject mainItem
118  onMainItemChanged: {
119  print("Warning: the mainItem property is deprecated");
120  scrollablePageData.push(mainItem);
121  }
122 
123  /**
124  * @brief This property sets whether it is possible to navigate the items in a view that support it.
125  *
126  * If true, and if flickable is an item view (e.g. ListView, GridView), it will be possible
127  * to navigate the view current items with keyboard up/down arrow buttons.
128  * Also, any key event will be forwarded to the current list item.
129  *
130  * default: ``true``
131  */
132  property bool keyboardNavigationEnabled: true
133 //END properties
134 
135  contentHeight: flickable ? flickable.contentHeight : 0
136  implicitHeight: {
137  let height = contentHeight + topPadding + bottomPadding;
138  if (header && header.visible) {
139  height += header.implicitHeight;
140  }
141  if (footer && footer.visible) {
142  height += footer.implicitHeight;
143  }
144  return height;
145  }
146 
147  implicitWidth: {
148  if (flickable) {
149  if (flickable.contentItem) {
150  return flickable.contentItem.implicitWidth;
151  } else {
152  return contentItem.implicitWidth + leftPadding + rightPadding;
153  }
154  } else {
155  return 0;
156  }
157  }
158 
159  Kirigami.Theme.inherit: false
160  Kirigami.Theme.colorSet: flickable && flickable.hasOwnProperty("model") ? Kirigami.Theme.View : Kirigami.Theme.Window
161 
162  Keys.forwardTo: {
163  if (root.keyboardNavigationEnabled && root.flickable) {
164  if (("currentItem" in root.flickable) && root.flickable.currentItem) {
165  return [ root.flickable.currentItem, root.flickable ];
166  } else {
167  return [ root.flickable ];
168  }
169  } else {
170  return [];
171  }
172  }
173 
174  contentItem: QQC2.ScrollView {
175  id: scrollView
176  anchors {
177  top: (root.header && root.header.visible)
178  ? root.header.bottom
179  // FIXME: for now assuming globalToolBarItem is in a Loader, which needs to be get rid of
180  : (globalToolBarItem && globalToolBarItem.parent && globalToolBarItem.visible
181  ? globalToolBarItem.parent.bottom
182  : parent.top)
183  bottom: (root.footer && root.footer.visible) ? root.footer.top : parent.bottom
184  left: parent.left
185  right: parent.right
186  topMargin: root.refreshing ? busyIndicatorLoader.height : 0
187  Behavior on topMargin {
188  NumberAnimation {
189  easing.type: Easing.InOutQuad
190  duration: Kirigami.Units.longDuration
191  }
192  }
193  }
194  QQC2.ScrollBar.horizontal.policy: root.horizontalScrollBarPolicy
195  QQC2.ScrollBar.vertical.policy: root.verticalScrollBarPolicy
196  }
197 
198  data: [
199  // Has to be a MouseArea that accepts events otherwise touch events on Wayland will get lost
200  MouseArea {
201  id: scrollingArea
202  width: root.flickable.width
203  height: Math.max(root.flickable.height, implicitHeight)
204  implicitHeight: {
205  let impl = 0;
206  for (const i in itemsParent.visibleChildren) {
207  const child = itemsParent.visibleChildren[i];
208  if (child.implicitHeight <= 0) {
209  impl = Math.max(impl, child.height);
210  } else {
211  impl = Math.max(impl, child.implicitHeight);
212  }
213  }
214  return impl + itemsParent.anchors.topMargin + itemsParent.anchors.bottomMargin;
215  }
216  Item {
217  id: itemsParent
218  property Flickable flickable
219  anchors {
220  fill: parent
221  leftMargin: root.leftPadding
222  topMargin: root.topPadding
223  rightMargin: root.rightPadding
224  bottomMargin: root.bottomPadding
225  }
226  onChildrenChanged: {
227  const child = children[children.length - 1];
228  if (child instanceof QQC2.ScrollView) {
229  print("Warning: it's not supported to have ScrollViews inside a ScrollablePage")
230  }
231  }
232  }
233  Binding {
234  target: root.flickable
235  property: "bottomMargin"
236  value: root.bottomPadding
237  restoreMode: Binding.RestoreBinding
238  }
239  },
240 
241  Loader {
242  id: busyIndicatorLoader
243  z: 99
244  y: root.flickable.verticalLayoutDirection === ListView.BottomToTop
245  ? -root.flickable.contentY + root.flickable.originY + height
246  : -root.flickable.contentY + root.flickable.originY - height
247  width: root.flickable.width
248  height: Kirigami.Units.gridUnit * 4
249  active: root.supportsRefreshing
250 
251  sourceComponent: Item {
252  id: busyIndicatorFrame
253 
254  QQC2.BusyIndicator {
255  id: busyIndicator
256  z: 1
257  anchors.centerIn: parent
258  running: root.refreshing
259  visible: root.refreshing
260  // Android busywidget QQC seems to be broken at custom sizes
261  }
262  Rectangle {
263  id: spinnerProgress
264  anchors {
265  fill: busyIndicator
266  margins: Kirigami.Units.smallSpacing
267  }
268  radius: width
269  visible: supportsRefreshing && !refreshing && progress > 0
270  color: "transparent"
271  opacity: 0.8
272  border.color: Kirigami.Theme.backgroundColor
273  border.width: Kirigami.Units.smallSpacing
274  property real progress: supportsRefreshing && !refreshing ? (busyIndicatorLoader.y / busyIndicatorFrame.height) : 0
275  }
276  GE.ConicalGradient {
277  source: spinnerProgress
278  visible: spinnerProgress.visible
279  anchors.fill: spinnerProgress
280  gradient: Gradient {
281  GradientStop { position: 0.00; color: Kirigami.Theme.highlightColor }
282  GradientStop { position: spinnerProgress.progress; color: Kirigami.Theme.highlightColor }
283  GradientStop { position: spinnerProgress.progress + 0.01; color: "transparent" }
284  GradientStop { position: 1.00; color: "transparent" }
285  }
286  }
287 
288  Connections {
289  target: busyIndicatorLoader
290  function onYChanged() {
291  if (!supportsRefreshing) {
292  return;
293  }
294 
295  if (!root.refreshing && busyIndicatorLoader.y > busyIndicatorFrame.height / 2 + topPadding) {
296  refreshTriggerTimer.running = true;
297  } else {
298  refreshTriggerTimer.running = false;
299  }
300  }
301  }
302  Timer {
303  id: refreshTriggerTimer
304  interval: 500
305  onTriggered: {
306  if (!root.refreshing && busyIndicatorLoader.y > busyIndicatorFrame.height / 2 + topPadding) {
307  root.refreshing = true;
308  }
309  }
310  }
311  }
312  }
313  ]
314 
315  Component.onCompleted: {
316  let flickableFound = false;
317  for (const i in itemsParent.data) {
318  const child = itemsParent.data[i];
319  if (child instanceof Flickable) {
320  // If there were more flickable children, take the last one, as behavior compatibility
321  // with old internal ScrollView
322  child.activeFocusOnTab = true;
323  root.flickable = child;
324  flickableFound = true;
325  if (child instanceof ListView) {
326  child.keyNavigationEnabled = true;
327  child.keyNavigationWraps = false;
328  }
329  } else if (child instanceof Item) {
330  child.anchors.left = itemsParent.left;
331  child.anchors.right = itemsParent.right;
332  } else if (child instanceof KT.OverlaySheet) {
333  // Reparent sheets, needs to be done before Component.onCompleted
334  if (child.parent === itemsParent || child.parent === null) {
335  child.parent = root;
336  }
337  }
338  }
339 
340  if (flickableFound) {
341  scrollView.contentItem = root.flickable;
342  root.flickable.parent = scrollView;
343  // 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)
344  Qt.callLater(() => {
345  if (root.activeFocus) {
346  root.flickable.forceActiveFocus();
347  }
348  });
349  // Some existing code incorrectly uses anchors
350  root.flickable.anchors.fill = undefined;
351  root.flickable.anchors.left = undefined;
352  root.flickable.anchors.right = undefined;
353  root.flickable.anchors.top = undefined;
354  root.flickable.anchors.bottom = undefined;
355  } else {
356  scrollView.contentItem = root.flickable;
357  scrollingArea.parent = root.flickable.contentItem;
358  root.flickable.contentHeight = Qt.binding(() => scrollingArea.implicitHeight - root.flickable.topMargin - root.flickable.bottomMargin);
359  root.flickable.contentWidth = Qt.binding(() => scrollingArea.implicitWidth);
360  }
361  root.flickable.flickableDirection = Flickable.VerticalFlick;
362  }
363 }
const QList< QKeySequence > & print()
QObject * parent() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2023 The KDE developers.
Generated on Sat Jan 28 2023 04:15:07 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.