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 inside, 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 verticalAlignment: Text.AlignVCenter
68 elide: Text.ElideRight
69
70 // use tooltip for long text that is elided
71 T.ToolTip.visible: truncated && titleHoverHandler.hovered
72 T.ToolTip.text: root.title
73 HoverHandler {
74 id: titleHoverHandler
75 }
76 }
77
78 /**
79 * @brief An optional item which will be used as the sheet's footer,
80 * always kept on screen.
81 */
82 property Item footer
83
84 default property alias flickableContentData: scrollView.contentData
85//END Own Properties
86
87//BEGIN Reimplemented Properties
88 QQC2.Overlay.modal: Rectangle {
89 color: Qt.rgba(0, 0, 0, 0.3)
90
91 // the opacity of the item is changed internally by QQuickPopup on open/close
92 Behavior on opacity {
93 OpacityAnimator {
94 duration: Kirigami.Units.longDuration
95 easing.type: Easing.InOutQuad
96 }
97 }
98 }
99
100 modal: true
101 dim: true
102
103 leftInset: -1
104 rightInset: -1
105 topInset: -1
106 bottomInset: -1
107
108 closePolicy: T.Popup.CloseOnEscape
109 x: parent ? Math.round(parent.width / 2 - width / 2) : 0
110 y: {
111 if (!parent) {
112 return 0;
113 }
114 const visualParentAdjust = sheetHandler.visualParent?.y ?? 0;
115 const wantedPosition = parent.height / 2 - implicitHeight / 2;
116 return Math.round(Math.max(visualParentAdjust, wantedPosition, Kirigami.Units.gridUnit * 3));
117 }
118
119 implicitWidth: {
120 let width = parent?.width ?? 0;
121 if (!scrollView.itemForSizeHints) {
122 return width;
123 } else if (scrollView.itemForSizeHints.Layout.preferredWidth > 0) {
124 return Math.min(width, scrollView.itemForSizeHints.Layout.preferredWidth);
125 } else if (scrollView.itemForSizeHints.implicitWidth > 0) {
126 return Math.min(width, scrollView.itemForSizeHints.implicitWidth);
127 } else {
128 return width;
129 }
130 }
131 implicitHeight: {
132 let h = parent?.height ?? 0;
133 if (!scrollView.itemForSizeHints) {
134 return h - y;
135 } else if (scrollView.itemForSizeHints.Layout.preferredHeight > 0) {
136 h = scrollView.itemForSizeHints.Layout.preferredHeight;
137 } else if (scrollView.itemForSizeHints.implicitHeight > 0) {
138 h = scrollView.itemForSizeHints.implicitHeight + Kirigami.Units.largeSpacing * 2;
139 } else if (scrollView.itemForSizeHints instanceof Flickable && scrollView.itemForSizeHints.contentHeight > 0) {
140 h = scrollView.itemForSizeHints.contentHeight + Kirigami.Units.largeSpacing * 2;
141 } else {
142 h = scrollView.itemForSizeHints.height;
143 }
144 h += headerItem.implicitHeight + footerParent.implicitHeight + topPadding + bottomPadding;
145 return parent ? Math.min(h, parent.height - y) : h
146 }
147//END Reimplemented Properties
148
149//BEGIN Signal handlers
150 onVisibleChanged: {
151 const flickable = scrollView.contentItem;
152 flickable.contentY = flickable.originY - flickable.topMargin;
153 }
154
155 Component.onCompleted: {
156 Qt.callLater(() => {
157 if (!root.parent && typeof applicationWindow !== "undefined") {
158 root.parent = applicationWindow().overlay
159 }
160 });
161 }
162
163 Connections {
164 target: parent
165 function onVisibleChanged() {
166 if (!parent.visible) {
167 root.close();
168 }
169 }
170 }
171//END Signal handlers
172
173//BEGIN UI
174 contentItem: MouseArea {
175 implicitWidth: mainLayout.implicitWidth
176 implicitHeight: mainLayout.implicitHeight
177 Kirigami.Theme.colorSet: root.Kirigami.Theme.colorSet
178 Kirigami.Theme.inherit: false
179
180 property real scenePressY
181 property real lastY
182 property bool dragStarted
183 drag.filterChildren: true
184 DragHandler {
185 id: mouseDragBlocker
186 target: null
187 dragThreshold: 0
188 acceptedDevices: PointerDevice.Mouse
189 onActiveChanged: {
190 if (active) {
191 parent.dragStarted = false;
192 }
193 }
194 }
195
196 onPressed: mouse => {
197 scenePressY = mapToItem(null, mouse.x, mouse.y).y;
198 lastY = scenePressY;
199 dragStarted = false;
200 }
201 onPositionChanged: mouse => {
202 if (mouseDragBlocker.active) {
203 return;
204 }
205 const currentY = mapToItem(null, mouse.x, mouse.y).y;
206
207 if (dragStarted && currentY !== lastY) {
208 translation.y += currentY - lastY;
209 }
210 if (Math.abs(currentY - scenePressY) > Qt.styleHints.startDragDistance) {
211 dragStarted = true;
212 }
213 lastY = currentY;
214 }
215 onCanceled: restoreAnim.restart();
216 onReleased: mouse => {
217 if (mouseDragBlocker.active) {
218 return;
219 }
220 if (Math.abs(mapToItem(null, mouse.x, mouse.y).y - scenePressY) > Kirigami.Units.gridUnit * 5) {
221 root.close();
222 } else {
223 restoreAnim.restart();
224 }
225 }
226
227 ColumnLayout {
228 id: mainLayout
229 anchors.fill: parent
230 spacing: 0
231
232 // Even though we're not actually using any shadows here,
233 // we're using a ShadowedRectangle instead of a regular
234 // rectangle because it allows fine-grained control over which
235 // corners to round, which we need here
236 Item {
237 id: headerItem
238 Layout.fillWidth: true
239 Layout.alignment: Qt.AlignTop
240 //Layout.margins: 1
241 visible: root.header || root.showCloseButton
242 implicitHeight: Math.max(headerParent.implicitHeight, closeIcon.height)// + Kirigami.Units.smallSpacing * 2
243 z: 2
244
245 Rectangle {
246 anchors {
247 top: parent.top
248 horizontalCenter: parent.horizontalCenter
249 topMargin: Kirigami.Units.smallSpacing
250 }
251 width: Math.round(Kirigami.Units.gridUnit * 3)
252 height: Math.round(Kirigami.Units.gridUnit / 4)
253 radius: height
254 color: Kirigami.Theme.textColor
255 opacity: 0.4
256 visible: Kirigami.Settings.hasTransientTouchInput
257 }
258 Kirigami.Padding {
259 id: headerParent
260
261 readonly property real leadingPadding: Kirigami.Units.largeSpacing
262 readonly property real trailingPadding: (root.showCloseButton ? closeIcon.width : 0) + Kirigami.Units.smallSpacing
263
264 anchors.fill: parent
265 verticalPadding: Kirigami.Units.largeSpacing
266 leftPadding: root.mirrored ? trailingPadding : leadingPadding
267 rightPadding: root.mirrored ? leadingPadding : trailingPadding
268
269 contentItem: root.header
270 }
271 QQC2.ToolButton {
272 id: closeIcon
273
274 // We want to position the close button in the top-right
275 // corner if the header is very tall, but we want to
276 // vertically center it in a short header
277 readonly property bool tallHeader: parent.height > (Kirigami.Units.iconSizes.smallMedium + Kirigami.Units.largeSpacing * 2)
278 Layout.alignment: tallHeader ? Qt.AlignRight | Qt.AlignTop : Qt.AlignRight | Qt.AlignVCenter
279 Layout.topMargin: tallHeader ? Kirigami.Units.largeSpacing : 0
280 anchors {
281 verticalCenter: !tallHeader ? undefined : parent.verticalCenter
282 right: parent.right
283 margins: Kirigami.Units.largeSpacing
284 }
285 z: 3
286
287 visible: root.showCloseButton
288 icon.name: closeIcon.hovered ? "window-close" : "window-close-symbolic"
289 text: qsTr("Close", "@action:button close dialog")
290 onClicked: root.close()
291 display: QQC2.AbstractButton.IconOnly
292 }
293 Kirigami.Separator {
294 anchors {
295 right: parent.right
296 left: parent.left
297 top: parent.bottom
298 }
299 visible: scrollView.T.ScrollBar.vertical.visible
300 }
301 }
302
303 // Here goes the main Sheet content
304 QQC2.ScrollView {
305 id: scrollView
306 Layout.fillWidth: true
307 Layout.fillHeight: true
308 clip: true
309 T.ScrollBar.horizontal.policy: T.ScrollBar.AlwaysOff
310
311 property bool initialized: false
312 property Item itemForSizeHints
313
314 // Important to not even access contentItem before it has been spontaneously created
315 contentWidth: initialized ? contentItem.width : width
316 contentHeight: itemForSizeHints?.implicitHeight ?? 0
317
318 onContentItemChanged: {
319 initialized = true;
320 const flickable = contentItem as Flickable;
321 flickable.boundsBehavior = Flickable.StopAtBounds;
322 if ((flickable instanceof ListView) || (flickable instanceof GridView)) {
323 itemForSizeHints = flickable;
324 return;
325 }
326 const content = flickable.contentItem;
327 content.childrenChanged.connect(() => {
328 for (const item of content.children) {
329 item.anchors.margins = Kirigami.Units.largeSpacing;
330 item.anchors.top = content.top;
331 item.anchors.left = content.left;
332 item.anchors.right = content.right;
333 }
334 itemForSizeHints = content.children?.[0] ?? null;
335 });
336 }
337 }
338
339 // Optional footer
340 Kirigami.Separator {
341 Layout.fillWidth: true
342 visible: footerParent.visible
343 }
344 Kirigami.Padding {
345 id: footerParent
346 Layout.fillWidth: true
347 padding: Kirigami.Units.smallSpacing
348 contentItem: root.footer
349 visible: contentItem !== null
350 }
351 }
352 Translate {
353 id: translation
354 }
355 MouseArea {
356 id: sheetHandler
357 readonly property Item visualParent: root.parent?.contentItem ?? root.parent
358 x: -root.x
359 y: -root.y
360 z: -1
361 width: visualParent?.width ?? 0
362 height: (visualParent?.height ?? 0) * 2
363
364 property var pressPos
365 onPressed: mouse => {
366 pressPos = mapToItem(null, mouse.x, mouse.y)
367 }
368 onReleased: mouse => {
369 // onClicked is emitted even if the mouse was dragged a lot, so we have to check the Manhattan length by hand
370 // https://en.wikipedia.org/wiki/Taxicab_geometry
371 let pos = mapToItem(null, mouse.x, mouse.y)
372 if (Math.abs(pos.x - pressPos.x) + Math.abs(pos.y - pressPos.y) < Qt.styleHints.startDragDistance) {
373 root.close();
374 }
375 }
376
377 NumberAnimation {
378 id: restoreAnim
379 target: translation
380 property: "y"
381 from: translation.y
382 to: 0
383 easing.type: Easing.InOutQuad
384 duration: Kirigami.Units.longDuration
385 }
386 Component.onCompleted: {
387 root.contentItem.parent.transform = translation
388 root.contentItem.parent.clip = false
389 }
390 }
391 }
392//END UI
393
394//BEGIN Transitions
395 enter: Transition {
396 ParallelAnimation {
397 NumberAnimation {
398 property: "opacity"
399 from: 0
400 to: 1
401 easing.type: Easing.InOutQuad
402 duration: Kirigami.Units.longDuration
403 }
404 NumberAnimation {
405 target: translation
406 property: "y"
407 from: Kirigami.Units.gridUnit * 5
408 to: 0
409 easing.type: Easing.InOutQuad
410 duration: Kirigami.Units.longDuration
411 }
412 }
413 }
414
415 exit: Transition {
416 ParallelAnimation {
417 NumberAnimation {
418 property: "opacity"
419 from: 1
420 to: 0
421 easing.type: Easing.InOutQuad
422 duration: Kirigami.Units.longDuration
423 }
424 NumberAnimation {
425 target: translation
426 property: "y"
427 from: translation.y
428 to: translation.y >= 0 ? translation.y + Kirigami.Units.gridUnit * 5 : translation.y - Kirigami.Units.gridUnit * 5
429 easing.type: Easing.InOutQuad
430 duration: Kirigami.Units.longDuration
431 }
432 }
433 }
434//END Transitions
435}
436
A visual separator.
Definition Separator.qml:16
QString name(StandardAction id)
KGuiItem close()
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
AlignRight
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 Fri Oct 11 2024 12:13:25 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.