Kirigami2

templates/OverlaySheet.qml
1 /*
2  * SPDX-FileCopyrightText: 2016-2020 Marco Martin <[email protected]>
3  *
4  * SPDX-License-Identifier: LGPL-2.0-or-later
5  */
6 
7 
8 import QtQuick 2.11
9 import QtQuick.Layouts 1.2
10 import QtQuick.Window 2.2
11 import org.kde.kirigami 2.11
12 import QtGraphicalEffects 1.0
13 import QtQuick.Templates 2.0 as T2
14 import "private"
15 import "../private"
16 
26 QtObject {
27  id: root
28 
29  Theme.colorSet: Theme.View
30  Theme.inherit: false
31 
42  default property Item contentItem
43 
49  property bool sheetOpen
50 
55  property int leftPadding: Units.largeSpacing
56 
61  property int topPadding: Units.largeSpacing
62 
67  property int rightPadding: Units.largeSpacing
68 
73  property int bottomPadding: Units.largeSpacing
74 
80  property real leftInset: 0
81 
87  property real topInset: 0
88 
94  property real rightInset: 0
95 
101  property real bottomInset: 0
102 
109  property Item header
117  property Item footer
127  property Item background
128 
134  property bool showCloseButton: !Settings.isMobile
135 
136  property Item parent
137 
138 
139  function open() {
140  openAnimation.running = true;
141  root.sheetOpen = true;
142  mainItem.visible = true;
143  }
144 
145  function close() {
146  if (root.sheetOpen) {
147  closeAnimation.running = true;
148  }
149  }
150 
151  onBackgroundChanged: {
152  background.parent = contentLayout.parent;
153  background.anchors.fill = contentLayout;
154  background.anchors.margins = -1
155  background.z = -1;
156  }
157  onContentItemChanged: {
158  if (contentItem instanceof Flickable) {
159  scrollView.flickableItem = contentItem;
160  contentItem.parent = scrollView;
161  contentItem.anchors.fill = scrollView;
162  scrollView.contentItem = contentItem;
163  } else {
164  contentItem.parent = contentItemParent;
165  scrollView.contentItem = flickableContents;
166  contentItem.anchors.left = contentItemParent.left;
167  contentItem.anchors.right = contentItemParent.right;
168  }
169  scrollView.flickableItem.interactive = false;
170  scrollView.flickableItem.flickableDirection = Flickable.VerticalFlick;
171  }
172  onSheetOpenChanged: {
173  if (sheetOpen) {
174  open();
175  } else {
176  closeAnimation.running = true;
177  Qt.inputMethod.hide();
178  }
179  }
180  onHeaderChanged: {
181  header.parent = headerParent;
182  header.anchors.fill = headerParent;
183 
184  //TODO: special case for actual ListViews
185  }
186  onFooterChanged: {
187  footer.parent = footerParent;
188  footer.anchors.fill = footerParent;
189  }
190 
191  Component.onCompleted: {
192  if (!root.parent && typeof applicationWindow !== "undefined") {
193  root.parent = applicationWindow().overlay
194  }
195  }
196 
197  readonly property Item rootItem: MouseArea {
198  id: mainItem
199  Theme.colorSet: root.Theme.colorSet
200  Theme.inherit: root.Theme.inherit
201  //we want to be over any possible OverlayDrawers, including handles
202  parent: {
203  if (root.parent && root.parent.ColumnView.view && (root.parent.ColumnView.view == root.parent || root.parent.ColumnView.view == root.parent.parent)) {
204  return root.parent.ColumnView.view.parent;
205  } else if (root.parent && root.parent.overlay) {
206  root.parent.overlay;
207  } else {
208  return root.parent;
209  }
210  }
211 
212  anchors.fill: parent
213 
214  visible: false
215  drag.filterChildren: true
216  hoverEnabled: true
217  clip: true
218 
219  onClicked: {
220  var pos = mapToItem(scrollView.contentItem, mouse.x, mouse.y);
221  if (!scrollView.contentItem.contains(pos)) {
222  root.close();
223  }
224  }
225 
226  readonly property int contentItemPreferredWidth: root.contentItem.Layout.preferredWidth > 0 ? root.contentItem.Layout.preferredWidth : root.contentItem.implicitWidth
227 
228  readonly property int contentItemMaximumWidth: width > Units.gridUnit * 30 ? width * 0.95 : width
229 
230  onHeightChanged: {
231  var focusItem;
232 
233  focusItem = Window.activeFocusItem;
234 
235  if (!focusItem) {
236  return;
237  }
238 
239  //NOTE: there is no function to know if an item is descended from another,
240  //so we have to walk the parent hierarchy by hand
241  var isDescendent = false;
242  var candidate = focusItem.parent;
243  while (candidate) {
244  if (candidate === root) {
245  isDescendent = true;
246  break;
247  }
248  candidate = candidate.parent;
249  }
250  if (!isDescendent) {
251  return;
252  }
253 
254  var cursorY = 0;
255  if (focusItem.cursorPosition !== undefined) {
256  cursorY = focusItem.positionToRectangle(focusItem.cursorPosition).y;
257  }
258 
259 
260  var pos = focusItem.mapToItem(flickableContents, 0, cursorY - Units.gridUnit*3);
261  //focused item already visible? add some margin for the space of the action buttons
262  if (pos.y >= scrollView.flickableItem.contentY && pos.y <= scrollView.flickableItem.contentY + scrollView.flickableItem.height - Units.gridUnit * 8) {
263  return;
264  }
265  scrollView.flickableItem.contentY = pos.y;
266  }
267 
268  ParallelAnimation {
269  id: openAnimation
270  property int margins: Units.gridUnit * 5
271  NumberAnimation {
272  target: outerFlickable
273  properties: "contentY"
274  from: -outerFlickable.height
275  to: Math.max(0, outerFlickable.height - outerFlickable.contentHeight + headerItem.height + footerItem.height) + outerFlickable.topMargin/2 - contentLayout.height/2
276  duration: Units.longDuration
277  easing.type: Easing.OutQuad
278  }
279  OpacityAnimator {
280  target: mainItem
281  from: 0
282  to: 1
283  duration: Units.longDuration
284  easing.type: Easing.InQuad
285  }
286  }
287 
288  NumberAnimation {
289  id: resetAnimation
290  target: outerFlickable
291  properties: "contentY"
292  from: outerFlickable.contentY
293  to: outerFlickable.visibleArea.yPosition < (1 - outerFlickable.visibleArea.heightRatio)/2 || scrollView.flickableItem.contentHeight < outerFlickable.height
294  ? Math.max(0, outerFlickable.height - outerFlickable.contentHeight + headerItem.height + footerItem.height) + outerFlickable.topMargin/2 - contentLayout.height/2
295  : outerFlickable.contentHeight - outerFlickable.height + outerFlickable.topEmptyArea + headerItem.height + footerItem.height
296  duration: Units.longDuration
297  easing.type: Easing.OutQuad
298  }
299 
300  SequentialAnimation {
301  id: closeAnimation
302  ParallelAnimation {
303  NumberAnimation {
304  target: outerFlickable
305  properties: "contentY"
306  to: outerFlickable.visibleArea.yPosition < (1 - outerFlickable.visibleArea.heightRatio)/2 ? -mainItem.height : outerFlickable.contentHeight
307  duration: Units.longDuration
308  easing.type: Easing.InQuad
309  }
310  OpacityAnimator {
311  target: mainItem
312  from: 1
313  to: 0
314  duration: Units.longDuration
315  easing.type: Easing.InQuad
316  }
317  }
318  ScriptAction {
319  script: {
320  scrollView.flickableItem.contentY = -mainItem.height;
321  mainItem.visible = root.sheetOpen = false;
322  }
323  }
324  }
325  Rectangle {
326  anchors.fill: parent
327  color: "black"
328  opacity: 0.3 * Math.min(
329  (Math.min(outerFlickable.contentY + outerFlickable.height, outerFlickable.height) / outerFlickable.height),
330  (2 + (outerFlickable.contentHeight - outerFlickable.contentY - outerFlickable.topMargin - outerFlickable.bottomMargin)/outerFlickable.height))
331  }
332 
333  FocusScope {
334  id: flickableContents
335 
336  readonly property real listHeaderHeight: scrollView.flickableItem ? -scrollView.flickableItem.originY : 0
337 
338  y: (scrollView.contentItem != flickableContents ? -scrollView.flickableItem.contentY - listHeaderHeight - (headerItem.visible ? headerItem.height : 0): 0)
339 
340  width: mainItem.contentItemPreferredWidth <= 0 ? mainItem.width : Math.max(mainItem.width/2, Math.min(mainItem.contentItemMaximumWidth, mainItem.contentItemPreferredWidth))
341 
342  height: scrollView.contentItem == flickableContents ? (root.contentItem.height + topPadding + bottomPadding) + (headerItem.visible ? headerItem.height : 0) + (footerItem.visible ? footerItem.height : 0) : 0
343  Connections {
344  target: enabled ? flickableContents.Window.activeFocusItem : null
345  enabled: flickableContents.focus && flickableContents.Window.activeFocusItem && flickableContents.Window.activeFocusItem.hasOwnProperty("text")
346  onTextChanged: {
347  if (Qt.inputMethod.cursorRectangle.y + Qt.inputMethod.cursorRectangle.height > mainItem.Window.height) {
348  scrollView.flickableItem.contentY += (Qt.inputMethod.cursorRectangle.y + Qt.inputMethod.cursorRectangle.height) - mainItem.Window.height
349  }
350  }
351  }
352 
353  Item {
354  id: contentItemParent
355  anchors {
356  fill: parent
357  leftMargin: leftPadding
358  topMargin: topPadding
359  rightMargin: rightPadding
360  bottomMargin: bottomPadding
361  }
362  }
363  }
364 
365  Connections {
366  target: scrollView.flickableItem
367  onContentHeightChanged: {
368  if (openAnimation.running) {
369  openAnimation.running = false;
370  open();
371  }
372  }
373  }
374 
375  Flickable {
376  id: outerFlickable
377  anchors.fill: parent
378  contentWidth: width
379  topMargin: height
380  bottomMargin: height
381  // +1: we need the flickable to be always interactive
382  contentHeight: Math.max(height+1, scrollView.flickableItem.contentHeight + topEmptyArea)
383 
384  readonly property int topEmptyArea: Math.max(height-scrollView.flickableItem.contentHeight, Units.gridUnit * 3)
385 
386  property int oldContentY: NaN
387  property bool lastMovementWasDown: false
388  property real startDraggingPos
389  WheelHandler {
390  target: outerFlickable
391  scrollFlickableTarget: false
392  }
393  onContentYChanged: {
394  if (scrollView.userInteracting) {
395  return;
396  }
397 
398  let startPos = -scrollView.flickableItem.topMargin - flickableContents.listHeaderHeight;
399  let pos = contentY - topEmptyArea - flickableContents.listHeaderHeight;
400  let endPos = scrollView.flickableItem.contentHeight - scrollView.flickableItem.height + scrollView.flickableItem.bottomMargin - flickableContents.listHeaderHeight;
401 
402  if (endPos - pos > 0) {
403  contentLayout.y = Math.max(root.topInset, scrollView.flickableItem.topMargin - pos - flickableContents.listHeaderHeight);
404  } else if (scrollView.flickableItem.topMargin - pos < 0) {
405  contentLayout.y = endPos - pos + root.topInset;
406  }
407 
408  scrollView.flickableItem.contentY = Math.max(
409  startPos, Math.min(pos, endPos));
410 
411  lastMovementWasDown = contentY < oldContentY;
412  oldContentY = contentY;
413  }
414 
415  onFlickEnded: {
416  if (openAnimation.running || closeAnimation.running) {
417  return;
418  }
419  if (scrollView.flickableItem.atYBeginning ||scrollView.flickableItem.atYEnd) {
420  resetAnimation.restart();
421  }
422  }
423 
424  onDraggingChanged: {
425  if (dragging) {
426  startDraggingPos = contentY;
427  return;
428  }
429 
430  let shouldClose = false;
431 
432  // close
433  if (scrollView.flickableItem.atYBeginning) {
434  if (startDraggingPos - contentY > Units.gridUnit * 4 &&
435  contentY < -Units.gridUnit * 4 &&
436  lastMovementWasDown) {
437  shouldClose = true;
438  }
439  }
440 
441  if (scrollView.flickableItem.atYEnd) {
442  if (contentY - startDraggingPos > Units.gridUnit * 4 &&
443  contentY > contentHeight - height + Units.gridUnit * 4 &&
444  !lastMovementWasDown) {
445  shouldClose = true;
446  }
447  }
448 
449  if (shouldClose) {
450  closeAnimation.restart();
451  } else if (scrollView.flickableItem.atYBeginning || scrollView.flickableItem.atYEnd) {
452  resetAnimation.restart();
453  }
454  }
455 
456  onHeightChanged: {
457  if (scrollView.flickableItem.contentHeight < height) {
458  contentYChanged();
459  }
460  }
461 
462  ColumnLayout {
463  id: contentLayout
464  spacing: 0
465  // Its events should be filtered but not scrolled
466  parent: outerFlickable
467  anchors.horizontalCenter: parent.horizontalCenter
468  width: mainItem.contentItemPreferredWidth <= 0 ? mainItem.width : Math.max(mainItem.width/2, Math.min(mainItem.contentItemMaximumWidth, mainItem.contentItemPreferredWidth)) - root.leftInset - root.rightInset
469  height: Math.min(implicitHeight, parent.height) - root.topInset - root.bottomInset
470 
471  Item {
472  id: headerItem
473  Layout.fillWidth: true
474  //Layout.margins: 1
475  visible: root.header || root.showCloseButton
476  implicitHeight: Math.max(headerParent.implicitHeight, closeIcon.height) + Units.smallSpacing * 2
477  z: 2
478  Item {
479  id: headerParent
480  implicitHeight: header ? header.implicitHeight : 0
481  anchors {
482  fill: parent
483  leftMargin: root.leftPadding
484  margins: Units.smallSpacing
485  rightMargin: closeIcon.width + Units.smallSpacing
486  }
487  }
488  Icon {
489  id: closeIcon
490  anchors {
491  right: parent.right
492  top: parent.top
493  margins: Units.smallSpacing
494  rightMargin: root.rightPadding
495  verticalCenter: headerItem.verticalCenter
496  }
497  z: 3
498  visible: root.showCloseButton
499  width: Units.iconSizes.smallMedium
500  height: width
501  source: closeMouseArea.containsMouse ? "window-close" : "window-close-symbolic"
502  active: closeMouseArea.containsMouse
503  MouseArea {
504  id: closeMouseArea
505  hoverEnabled: true
506  anchors.fill: parent
507  onClicked: root.close();
508  }
509  }
510  Separator {
511  anchors {
512  right: parent.right
513  left: parent.left
514  top: parent.bottom
515  }
516  }
517  }
518 
519  ScrollView {
520  id: scrollView
521 
522  property bool userInteracting: false
523  Layout.fillWidth: true
524  Layout.fillHeight: true
525 
526  implicitHeight: flickableItem.contentHeight
527  Layout.maximumHeight: flickableItem.contentHeight
528  }
529 
530  Connections {
531  target: scrollView.flickableItem
532  property real oldContentY: 0
533  onContentYChanged: {
534  if (outerFlickable.moving) {
535  oldContentY = scrollView.flickableItem.contentY;
536  return;
537  }
538  scrollView.userInteracting = true;
539 
540  let diff = scrollView.flickableItem.contentY - oldContentY
541 
542  outerFlickable.contentY = outerFlickable.contentY + diff;
543 
544  if (diff > 0) {
545  contentLayout.y = Math.max(root.topInset, contentLayout.y - diff);
546  } else if (scrollView.flickableItem.contentY < outerFlickable.topEmptyArea + headerItem.height) {
547  contentLayout.y = Math.min(outerFlickable.topEmptyArea, contentLayout.y + (contentLayout.y - diff)) + root.topInset;
548  }
549 
550  oldContentY = scrollView.flickableItem.contentY;
551  scrollView.userInteracting = false;
552  }
553  }
554  Item {
555  visible: footerItem.visible
556  implicitHeight: footerItem.height
557  }
558  }
559 
560  // footer item is outside the layout as it should never scroll away
561  Rectangle {
562  id: footerItem
563  width: contentLayout.width - 2
564  radius: Units.smallSpacing
565  parent: outerFlickable
566  x: contentLayout.x + 1
567  y: Math.min(parent.height, contentLayout.y + contentLayout.height -1) - height
568  visible: root.footer
569  implicitHeight: footerParent.implicitHeight + Units.smallSpacing * 2 + extraMargin
570  color: Theme.backgroundColor
571 
572  //Show an extra margin when:
573  //* the application is in mobile mode (no toolbarapplicationheader)
574  //* the bottom screen controls are visible
575  //* the sheet is displayed *under* the controls
576  property int extraMargin: (!root.parent ||
577  typeof applicationWindow === "undefined" ||
578  (root.parent === applicationWindow().overlay) ||
579  !applicationWindow().controlsVisible ||
580  (applicationWindow().pageStack && applicationWindow().pageStack.globalToolBar && applicationWindow().pageStack.globalToolBar.actualStyle === ApplicationHeaderStyle.ToolBar) ||
581  (applicationWindow().header && applicationWindow().header.toString().indexOf("ToolBarApplicationHeader") === 0))
582  ? 0 : Units.gridUnit * 3
583 
584  z: 2
585  Item {
586  id: footerParent
587  implicitHeight: footer ? footer.implicitHeight : 0
588  anchors {
589  top: parent.top
590  left: parent.left
591  right: parent.right
592  margins: Units.smallSpacing
593  }
594  }
595 
596  Separator {
597  anchors {
598  right: parent.right
599  left: parent.left
600  bottom: parent.top
601  leftMargin: -1
602  rightMargin: -1
603  }
604  }
605  }
606  }
607  }
608 }
int largeSpacing
units.largeSpacing is the amount of spacing that should be used inside bigger UI elements, for example between an icon and the corresponding text.
QString from() const
int gridUnit
The fundamental unit of space that should be used for sizes, expressed in pixels. ...
Class for rendering an icon in UI.
Definition: icon.h:26
bool isMobile
True if we are running on a small mobile device such as a mobile phone This is used when we want to d...
Definition: settings.h:32
QString to() const
This class contains global kirigami settings about the current device setup It is exposed to QML as t...
Definition: settings.h:16
int smallSpacing
units.smallSpacing is the amount of spacing that should be used around smaller UI elements...
QtObject iconSizes
units.iconSizes provides access to platform-dependent icon sizing
QTextStream & right(QTextStream &stream)
A set of named colors for the application.
int longDuration
units.longDuration should be used for longer, screen-covering animations, for opening and closing of ...
This class intercepts the mouse wheel events of its target, and gives them to the user code as a sign...
Definition: wheelhandler.h:164
A set of values to define semantically sizes and durations.
QTextStream & left(QTextStream &stream)
A visual separator.
Definition: Separator.qml:18
This file is part of the KDE documentation.
Documentation copyright © 1996-2020 The KDE developers.
Generated on Fri Jun 5 2020 22:39:02 by doxygen 1.8.11 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.