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

KDE's Doxygen guidelines are available online.