Kirigami2

templates/OverlaySheet.qml
1/*
2 * SPDX-FileCopyrightText: 2016-2023 Marco Martin <notmart@gmail.com>
3 *
4 * SPDX-License-Identifier: LGPL-2.0-or-later
5 */
6
7import QtQuick
8import QtQuick.Layouts
9import QtQuick.Controls as QQC2
10import QtQuick.Templates as T
11import org.kde.kirigami as Kirigami
12
13/**
14 * @brief An overlay sheet that covers the current Page content.
15 *
16 * Its contents can be scrolled up or down, scrolling all the way up or
17 * all the way down, dismisses it.
18 * Use this for big, modal dialogs or information display, that can't be
19 * logically done as a new separate Page, even if potentially
20 * are taller than the screen space.
21 *
22 * Example usage:
23 * @code
24 * Kirigami.OverlaySheet {
25 * ColumnLayout { ... }
26 * }
27 * Kirigami.OverlaySheet {
28 * ListView { ... }
29 * }
30 * @endcode
31 *
32 * It needs a single element declared inseide, do *not* override its contentItem
33 *
34 * @inherit QtQuick.Templates.Popup
35 */
36T.Popup {
37 id: root
38
39 Kirigami.OverlayZStacking.layer: Kirigami.OverlayZStacking.FullScreen
40 z: Kirigami.OverlayZStacking.z
41
42 Kirigami.Theme.colorSet: Kirigami.Theme.View
43 Kirigami.Theme.inherit: false
44
45//BEGIN Own Properties
46
47 /**
48 * @brief A title to be displayed in the header of this Sheet
49 */
50 property string title
51
52 /**
53 * @brief This property sets the visibility of the close button in the top-right corner.
54 *
55 * default: `Only shown in desktop mode`
56 *
57 */
58 property bool showCloseButton: !Kirigami.Settings.isMobile
60 /**
61 * @brief This property holds an optional item which will be used as the sheet's header,
62 * and will always be displayed.
63 */
64 property Item header: Kirigami.Heading {
65 level: 2
66 text: root.title
67 elide: Text.ElideRight
68
69 // use tooltip for long text that is elided
70 T.ToolTip.visible: truncated && titleHoverHandler.hovered
71 T.ToolTip.text: root.title
72 HoverHandler {
73 id: titleHoverHandler
74 }
75 }
76
77 /**
78 * @brief An optional item which will be used as the sheet's footer,
79 * always kept on screen.
80 */
81 property Item footer
82
83 default property alias flickableContentData: scrollView.contentData
84//END Own Properties
85
86//BEGIN Reimplemented Properties
87 T.Overlay.modeless: Item {
88 id: overlay
89 Rectangle {
90 x: sheetHandler.visualParent?.Kirigami.ScenePosition.x ?? 0
91 y: sheetHandler.visualParent?.Kirigami.ScenePosition.y ?? 0
92 width: sheetHandler.visualParent?.width ?? 0
93 height: sheetHandler.visualParent?.height ?? 0
94 color: Qt.rgba(0, 0, 0, 0.2)
95 }
96 Behavior on opacity {
97 NumberAnimation {
98 property: "opacity"
99 easing.type: Easing.InOutQuad
100 duration: Kirigami.Units.longDuration
101 }
102 }
103 }
104
105 modal: false
106 dim: true
107
108 leftInset: -1
109 rightInset: -1
110 topInset: -1
111 bottomInset: -1
112
113 closePolicy: T.Popup.CloseOnEscape
114 x: parent ? Math.round(parent.width / 2 - width / 2) : 0
115 y: {
116 if (!parent) {
117 return 0;
118 }
119 const visualParentAdjust = sheetHandler.visualParent?.y ?? 0;
120 const wantedPosition = parent.height / 2 - implicitHeight / 2;
121 return Math.round(Math.max(visualParentAdjust, wantedPosition, Kirigami.Units.gridUnit * 3));
122 }
123
124 implicitWidth: {
125 let width = parent?.width ?? 0;
126 if (!scrollView.itemForSizeHints) {
127 return width;
128 } else if (scrollView.itemForSizeHints.Layout.preferredWidth > 0) {
129 return Math.min(width, scrollView.itemForSizeHints.Layout.preferredWidth);
130 } else if (scrollView.itemForSizeHints.implicitWidth > 0) {
131 return Math.min(width, scrollView.itemForSizeHints.implicitWidth);
132 } else {
133 return width;
134 }
135 }
136 implicitHeight: {
137 let h = parent?.height ?? 0;
138 if (!scrollView.itemForSizeHints) {
139 return h - y;
140 } else if (scrollView.itemForSizeHints.Layout.preferredHeight > 0) {
141 h = scrollView.itemForSizeHints.Layout.preferredHeight;
142 } else if (scrollView.itemForSizeHints.implicitHeight > 0) {
143 h = scrollView.itemForSizeHints.implicitHeight + Kirigami.Units.largeSpacing * 2;
144 } else if (scrollView.itemForSizeHints instanceof Flickable && scrollView.itemForSizeHints.contentHeight > 0) {
145 h = scrollView.itemForSizeHints.contentHeight + Kirigami.Units.largeSpacing * 2;
146 } else {
147 h = scrollView.itemForSizeHints.height;
148 }
149 h += headerItem.implicitHeight + footerParent.implicitHeight + topPadding + bottomPadding;
150 return Math.min(h, parent.height - y)
151 }
152//END Reimplemented Properties
153
154//BEGIN Signal handlers
155 onVisibleChanged: {
156 const flickable = scrollView.contentItem;
157 flickable.contentY = flickable.originY - flickable.topMargin;
158 }
159
160 Component.onCompleted: {
161 Qt.callLater(() => {
162 if (!root.parent && typeof applicationWindow !== "undefined") {
163 root.parent = applicationWindow().overlay
164 }
165 });
166 }
167
168 Connections {
169 target: parent
170 function onVisibleChanged() {
171 if (!parent.visible) {
172 root.close();
173 }
174 }
175 }
176//END Signal handlers
177
178//BEGIN UI
179 contentItem: MouseArea {
180 implicitWidth: mainLayout.implicitWidth
181 implicitHeight: mainLayout.implicitHeight
182 Kirigami.Theme.colorSet: root.Kirigami.Theme.colorSet
183 Kirigami.Theme.inherit: false
184
185 property real scenePressY
186 property real lastY
187 property bool dragStarted
188 drag.filterChildren: true
189 DragHandler {
190 id: mouseDragBlocker
191 target: null
192 dragThreshold: 0
193 acceptedDevices: PointerDevice.Mouse
194 onActiveChanged: {
195 if (active) {
196 parent.dragStarted = false;
197 }
198 }
199 }
200
201 onPressed: mouse => {
202 scenePressY = mapToItem(null, mouse.x, mouse.y).y;
203 lastY = scenePressY;
204 dragStarted = false;
205 }
206 onPositionChanged: mouse => {
207 if (mouseDragBlocker.active) {
208 return;
209 }
210 const currentY = mapToItem(null, mouse.x, mouse.y).y;
211
212 if (dragStarted && currentY !== lastY) {
213 translation.y += currentY - lastY;
214 }
215 if (Math.abs(currentY - scenePressY) > Qt.styleHints.startDragDistance) {
216 dragStarted = true;
217 }
218 lastY = currentY;
219 }
220 onCanceled: restoreAnim.restart();
221 onReleased: mouse => {
222 if (mouseDragBlocker.active) {
223 return;
224 }
225 if (Math.abs(mapToItem(null, mouse.x, mouse.y).y - scenePressY) > Kirigami.Units.gridUnit * 5) {
226 root.close();
227 } else {
228 restoreAnim.restart();
229 }
230 }
231
232 ColumnLayout {
233 id: mainLayout
234 anchors.fill: parent
235 spacing: 0
236
237 // Even though we're not actually using any shadows here,
238 // we're using a ShadowedRectangle instead of a regular
239 // rectangle because it allows fine-grained control over which
240 // corners to round, which we need here
241 Kirigami.ShadowedRectangle {
242 id: headerItem
243 Layout.fillWidth: true
244 Layout.alignment: Qt.AlignTop
245 //Layout.margins: 1
246 visible: root.header || root.showCloseButton
247 implicitHeight: Math.max(headerParent.implicitHeight, closeIcon.height) + Kirigami.Units.smallSpacing * 2
248 z: 2
249 corners.topLeftRadius: Kirigami.Units.smallSpacing
250 corners.topRightRadius: Kirigami.Units.smallSpacing
251 Kirigami.Theme.colorSet: Kirigami.Theme.Header
252 Kirigami.Theme.inherit: false
253 color: Kirigami.Theme.backgroundColor
254
255 Kirigami.Padding {
256 id: headerParent
257
258 readonly property real leadingPadding: Kirigami.Units.largeSpacing
259 readonly property real trailingPadding: (root.showCloseButton ? closeIcon.width : 0) + Kirigami.Units.smallSpacing
260
261 anchors.fill: parent
262 verticalPadding: Kirigami.Units.smallSpacing
263 leftPadding: root.mirrored ? trailingPadding : leadingPadding
264 rightPadding: root.mirrored ? leadingPadding : trailingPadding
265
266 contentItem: root.header
267 }
268 Kirigami.Icon {
269 id: closeIcon
270
271 readonly property bool tallHeader: headerItem.height > (Kirigami.Units.iconSizes.smallMedium + Kirigami.Units.largeSpacing + Kirigami.Units.largeSpacing)
272
273 anchors {
274 right: parent.right
275 rightMargin: Kirigami.Units.largeSpacing
276 verticalCenter: headerItem.verticalCenter
277 margins: Kirigami.Units.smallSpacing
278 }
279
280 // Apply the changes to the anchors imperatively, to first disable an anchor point
281 // before setting the new one, so the icon don't grow unexpectedly
282 onTallHeaderChanged: {
283 if (tallHeader) {
284 // We want to position the close button in the top-right corner if the header is very tall
285 anchors.verticalCenter = undefined
286 anchors.topMargin = Kirigami.Units.largeSpacing
287 anchors.top = headerItem.top
288 } else {
289 // but we want to vertically center it in a short header
290 anchors.top = undefined
291 anchors.topMargin = undefined
292 anchors.verticalCenter = headerItem.verticalCenter
293 }
294 }
295 Component.onCompleted: tallHeaderChanged()
296
297 z: 3
298 visible: root.showCloseButton
299 width: Kirigami.Units.iconSizes.smallMedium
300 height: width
301 source: closeMouseArea.containsMouse ? "window-close" : "window-close-symbolic"
302 active: closeMouseArea.containsMouse
303 MouseArea {
304 id: closeMouseArea
305 hoverEnabled: true
306 anchors.fill: parent
307 onClicked: root.close();
308 }
309 }
310 Kirigami.Separator {
311 anchors {
312 right: parent.right
313 left: parent.left
314 top: parent.bottom
315 }
316 }
317 }
318
319 // Here goes the main Sheet content
320 QQC2.ScrollView {
321 id: scrollView
322 Layout.fillWidth: true
323 Layout.fillHeight: true
324 clip: true
325 T.ScrollBar.horizontal.policy: T.ScrollBar.AlwaysOff
326
327 property bool initialized: false
328 property Item itemForSizeHints
329
330 // Important to not even access contentItem before it has been spontaneously created
331 contentWidth: initialized ? contentItem.width : width
332 contentHeight: itemForSizeHints?.implicitHeight ?? 0
333
334 onContentItemChanged: {
335 initialized = true;
336 const flickable = contentItem as Flickable;
337 flickable.boundsBehavior = Flickable.StopAtBounds;
338 if ((flickable instanceof ListView) || (flickable instanceof GridView)) {
339 itemForSizeHints = flickable;
340 return;
341 }
342 const content = flickable.contentItem;
343 content.childrenChanged.connect(() => {
344 for (const item of content.children) {
345 item.anchors.margins = Kirigami.Units.largeSpacing;
346 item.anchors.top = content.top;
347 item.anchors.left = content.left;
348 item.anchors.right = content.right;
349 }
350 itemForSizeHints = content.children?.[0] ?? null;
351 });
352 }
353 }
354
355 // Optional footer
356 Kirigami.Separator {
357 Layout.fillWidth: true
358 visible: footerParent.visible
359 }
360 Kirigami.Padding {
361 id: footerParent
362 Layout.fillWidth: true
363 padding: Kirigami.Units.smallSpacing
364 contentItem: root.footer
365 visible: contentItem !== null
366 }
367 }
368 Translate {
369 id: translation
370 }
371 MouseArea {
372 id: sheetHandler
373 readonly property Item visualParent: root.parent?.contentItem ?? root.parent
374 x: -root.x
375 y: -root.y
376 z: -1
377 width: visualParent?.width ?? 0
378 height: (visualParent?.height ?? 0) * 2
379
380 property var pressPos
381 onPressed: mouse => {
382 pressPos = mapToItem(null, mouse.x, mouse.y)
383 }
384 onReleased: mouse => {
385 // onClicked is emitted even if the mouse was dragged a lot, so we have to check the Manhattan length by hand
386 // https://en.wikipedia.org/wiki/Taxicab_geometry
387 let pos = mapToItem(null, mouse.x, mouse.y)
388 if (Math.abs(pos.x - pressPos.x) + Math.abs(pos.y - pressPos.y) < Qt.styleHints.startDragDistance) {
389 root.close();
390 }
391 }
392
393 NumberAnimation {
394 id: restoreAnim
395 target: translation
396 property: "y"
397 from: translation.y
398 to: 0
399 easing.type: Easing.InOutQuad
400 duration: Kirigami.Units.longDuration
401 }
402 Component.onCompleted: {
403 root.contentItem.parent.transform = translation
404 root.contentItem.parent.clip = false
405 }
406 }
407 }
408//END UI
409
410//BEGIN Transitions
411 enter: Transition {
412 ParallelAnimation {
413 NumberAnimation {
414 property: "opacity"
415 from: 0
416 to: 1
417 easing.type: Easing.InOutQuad
418 duration: Kirigami.Units.longDuration
419 }
420 NumberAnimation {
421 target: translation
422 property: "y"
423 from: Kirigami.Units.gridUnit * 5
424 to: 0
425 easing.type: Easing.InOutQuad
426 duration: Kirigami.Units.longDuration
427 }
428 }
429 }
430
431 exit: Transition {
432 ParallelAnimation {
433 NumberAnimation {
434 property: "opacity"
435 from: 1
436 to: 0
437 easing.type: Easing.InOutQuad
438 duration: Kirigami.Units.longDuration
439 }
440 NumberAnimation {
441 target: translation
442 property: "y"
443 from: translation.y
444 to: translation.y >= 0 ? translation.y + Kirigami.Units.gridUnit * 5 : translation.y - Kirigami.Units.gridUnit * 5
445 easing.type: Easing.InOutQuad
446 duration: Kirigami.Units.longDuration
447 }
448 }
449 }
450//END Transitions
451}
452
QTextStream & left(QTextStream &stream)
QTextStream & right(QTextStream &stream)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Sat Apr 27 2024 22:13:10 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.