Kirigami2

Dialog.qml
1/*
2 SPDX-FileCopyrightText: 2021 Devin Lin <espidev@gmail.com>
3 SPDX-FileCopyrightText: 2021 Noah Davis <noahadvs@gmail.com>
4 SPDX-FileCopyrightText: 2022 ivan tkachenko <me@ratijas.tk>
5 SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
6*/
7
8import QtQuick
9import QtQml
10import QtQuick.Layouts
11import QtQuick.Templates as T
12import QtQuick.Controls as QQC2
13import org.kde.kirigami as Kirigami
14
15/**
16 * @brief Popup dialog that is used for short tasks and user interaction.
17 *
18 * Dialog consists of three components: the header, the content,
19 * and the footer.
20 *
21 * By default, the header is a heading with text specified by the
22 * `title` property.
23 *
24 * By default, the footer consists of a row of buttons specified by
25 * the `standardButtons` and `customFooterActions` properties.
26 *
27 * The `implicitHeight` and `implicitWidth` of the dialog contentItem is
28 * the primary hint used for the dialog size. The dialog will be the
29 * minimum size required for the header, footer and content unless
30 * it is larger than `maximumHeight` and `maximumWidth`. Use
31 * `preferredHeight` and `preferredWidth` in order to manually specify
32 * a size for the dialog.
33 *
34 * If the content height exceeds the maximum height of the dialog, the
35 * dialog's contents will become scrollable.
36 *
37 * If the contentItem is a <b>ListView</b>, the dialog will take care of the
38 * necessary scrollbars and scrolling behaviour. Do <b>not</b> attempt
39 * to nest ListViews (it must be the top level item), as the scrolling
40 * behaviour will not be handled. Use ListView's `header` and `footer` instead.
41 *
42 * Example for a selection dialog:
43 *
44 * @code{.qml}
45 * import QtQuick 2.15
46 * import QtQuick.Layouts 1.15
47 * import QtQuick.Controls 2.15 as Controls
48 * import org.kde.kirigami 2.19 as Kirigami
49 *
50 * Kirigami.Dialog {
51 * title: i18n("Dialog")
52 * padding: 0
53 * preferredWidth: Kirigami.Units.gridUnit * 16
54 *
55 * standardButtons: Kirigami.Dialog.Ok | Kirigami.Dialog.Cancel
56 *
57 * onAccepted: console.log("OK button pressed")
58 * onRejected: console.log("Rejected")
59 *
60 * ColumnLayout {
61 * spacing: 0
62 * Repeater {
63 * model: 5
64 * delegate: Controls.CheckDelegate {
65 * topPadding: Kirigami.Units.smallSpacing * 2
66 * bottomPadding: Kirigami.Units.smallSpacing * 2
67 * Layout.fillWidth: true
68 * text: modelData
69 * }
70 * }
71 * }
72 * }
73 * @endcode
74 *
75 * Example with scrolling (ListView scrolling behaviour is handled by the Dialog):
76 *
77 * @code{.qml}
78 * Kirigami.Dialog {
79 * id: scrollableDialog
80 * title: i18n("Select Number")
81 *
82 * ListView {
83 * id: listView
84 * // hints for the dialog dimensions
85 * implicitWidth: Kirigami.Units.gridUnit * 16
86 * implicitHeight: Kirigami.Units.gridUnit * 16
87 *
88 * model: 100
89 * delegate: Controls.RadioDelegate {
90 * topPadding: Kirigami.Units.smallSpacing * 2
91 * bottomPadding: Kirigami.Units.smallSpacing * 2
92 * implicitWidth: listView.width
93 * text: modelData
94 * }
95 * }
96 * }
97 * @endcode
98 *
99 * There are also sub-components of the Dialog that target specific usecases,
100 * and can reduce boilerplate code if used:
101 *
102 * @see PromptDialog
103 * @see MenuDialog
104 *
105 * @inherit QtQuick.QtObject
106 */
107T.Dialog {
108 id: root
109
110 /**
111 * @brief This property holds the dialog's contents; includes Items and QtObjects.
112 * @property list<QtObject> dialogData
113 */
114 default property alias dialogData: contentControl.contentData
115
116 /**
117 * @brief This property holds the content items of the dialog.
118 *
119 * The initial height and width of the dialog is calculated from the
120 * `implicitWidth` and `implicitHeight` of the content.
122 * @property list<Item> dialogChildren
123 */
124 property alias dialogChildren: contentControl.contentChildren
125
126 /**
127 * @brief This property sets the absolute maximum height the dialog can have.
128 *
129 * The height restriction is solely applied on the content, so if the
130 * maximum height given is not larger than the height of the header and
131 * footer, it will be ignored.
132 *
133 * This is the window height, subtracted by largeSpacing on both the top
134 * and bottom.
135 */
136 readonly property real absoluteMaximumHeight: parent.height - Kirigami.Units.largeSpacing * 2
137
138 /**
139 * @brief This property holds the absolute maximum width the dialog can have.
140 *
141 * By default, it is the window width, subtracted by largeSpacing on both
142 * the top and bottom.
143 */
144 readonly property real absoluteMaximumWidth: parent.width - Kirigami.Units.largeSpacing * 2
145
146 /**
147 * @brief This property holds the maximum height the dialog can have
148 * (including the header and footer).
149 *
150 * The height restriction is solely enforced on the content, so if the
151 * maximum height given is not larger than the height of the header and
152 * footer, it will be ignored.
153 *
154 * By default, this is `absoluteMaximumHeight`.
155 */
156 property real maximumHeight: absoluteMaximumHeight
157
158 /**
159 * @brief This property holds the maximum width the dialog can have.
160 *
161 * By default, this is `absoluteMaximumWidth`.
162 */
163 property real maximumWidth: absoluteMaximumWidth
164
165 /**
166 * @brief This property holds the preferred height of the dialog.
167 *
168 * The content will receive a hint for how tall it should be to have
169 * the dialog to be this height.
170 *
171 * If the content, header or footer require more space, then the height
172 * of the dialog will expand to the necessary amount of space.
173 */
174 property real preferredHeight: -1
175
176 /**
177 * @brief This property holds the preferred width of the dialog.
179 * The content will receive a hint for how wide it should be to have
180 * the dialog be this wide.
181 *
182 * If the content, header or footer require more space, then the width
183 * of the dialog will expand to the necessary amount of space.
184 */
185 property real preferredWidth: -1
187
188 /**
189 * @brief This property holds the component to the left of the footer buttons.
190 */
191 property Component footerLeadingComponent
192
193 /**
194 * @brief his property holds the component to the right of the footer buttons.
195 */
196 property Component footerTrailingComponent
197
198 /**
199 * @brief This property sets whether to show the close button in the header.
200 */
201 property bool showCloseButton: true
202
203 /**
204 * @brief This property sets whether the footer button style should be flat.
205 */
206 property bool flatFooterButtons: false
207
208 /**
209 * @brief This property holds the custom actions displayed in the footer.
210 *
211 * Example usage:
212 * @code{.qml}
213 * import QtQuick 2.15
214 * import QtQuick.Controls 2.15 as Controls
215 * import org.kde.kirigami 2.18 as Kirigami
216 *
217 * Kirigami.PromptDialog {
218 * id: dialog
219 * title: i18n("Confirm Playback")
220 * subtitle: i18n("Are you sure you want to play this song? It's really loud!")
221 *
222 * standardButtons: Kirigami.Dialog.Cancel
223 * customFooterActions: [
224 * Kirigami.Action {
225 * text: i18n("Play")
226 * icon.name: "media-playback-start"
227 * onTriggered: {
228 * //...
229 * dialog.close();
230 * }
231 * }
232 * ]
233 * }
234 * @endcode
235 *
236 * @see org::kde::kirigami::Action
237 */
238 property list<T.Action> customFooterActions
239
240 // DialogButtonBox should NOT contain invisible buttons, because in Qt 6
241 // ListView preserves space even for invisible items.
242 readonly property list<T.Action> __visibleCustomFooterActions: customFooterActions
243 .filter(action => !(action instanceof Kirigami.Action) || action?.visible)
244
245 // default standard button
246 standardButtons: QQC2.Dialog.Close
247
248 function standardButton(button): T.AbstractButton {
249 // in case a footer is redefined
250 if (footer instanceof T.DialogButtonBox) {
251 return footer.standardButton(button);
252 } else if (footer === footerToolBar) {
253 return dialogButtonBox.standardButton(button);
254 } else {
255 return null;
256 }
257 }
258
259 function customFooterButton(action: T.Action): T.AbstractButton {
260 if (!action) {
261 // Even if there's a null object in the list of actions, we should
262 // not return a button for it.
263 return null;
264 }
265 const index = __visibleCustomFooterActions.indexOf(action);
266 if (index < 0) {
267 return null;
268 }
269 return customFooterButtons.itemAt(index) as T.AbstractButton;
270 }
271
272 z: Kirigami.OverlayZStacking.z
273
274 // calculate dimensions
275 implicitWidth: contentItem.implicitWidth + leftPadding + rightPadding // maximum width enforced from our content (one source of truth) to avoid binding loops
276 implicitHeight: contentItem.implicitHeight + topPadding + bottomPadding
277 + (implicitHeaderHeight > 0 ? implicitHeaderHeight + spacing : 0)
278 + (implicitFooterHeight > 0 ? implicitFooterHeight + spacing : 0);
279
280 // misc. dialog settings
281 closePolicy: QQC2.Popup.CloseOnEscape | QQC2.Popup.CloseOnReleaseOutside
282 modal: true
283 clip: false
284 padding: 0
285
286 // determine parent so that popup knows which window to popup in
287 // we want to open the dialog in the center of the window, if possible
288 Component.onCompleted: {
289 if (typeof applicationWindow !== "undefined") {
290 parent = applicationWindow().overlay;
291 }
292 }
293
294 // center dialog
295 x: Math.round((parent.width - width) / 2)
296 y: Math.round((parent.height - height) / 2) + Kirigami.Units.gridUnit * 2 * (1 - opacity) // move animation
297
298 // dialog enter and exit transitions
299 enter: Transition {
300 NumberAnimation { property: "opacity"; from: 0; to: 1; easing.type: Easing.InOutQuad; duration: Kirigami.Units.longDuration }
301 }
302 exit: Transition {
303 NumberAnimation { property: "opacity"; from: 1; to: 0; easing.type: Easing.InOutQuad; duration: Kirigami.Units.longDuration }
304 }
305
306 // black background, fades in and out
307 QQC2.Overlay.modal: Rectangle {
308 color: Qt.rgba(0, 0, 0, 0.3)
309
310 // the opacity of the item is changed internally by QQuickPopup on open/close
311 Behavior on opacity {
312 OpacityAnimator {
313 duration: Kirigami.Units.longDuration
314 easing.type: Easing.InOutQuad
315 }
316 }
317 }
318
319 // dialog view background
320 background: Kirigami.ShadowedRectangle {
321 id: rect
322 Kirigami.Theme.colorSet: Kirigami.Theme.View
323 Kirigami.Theme.inherit: false
324 color: Kirigami.Theme.backgroundColor
325 radius: Kirigami.Units.smallSpacing
326 shadow {
327 size: radius * 2
328 color: Qt.rgba(0, 0, 0, 0.3)
329 yOffset: 1
330 }
331 }
332
333 // dialog content
334 contentItem: QQC2.ScrollView {
335 id: contentControl
336
337 // ensure view colour scheme, and background color
338 Kirigami.Theme.inherit: false
339 Kirigami.Theme.colorSet: Kirigami.Theme.View
340
341 QQC2.ScrollBar.horizontal.policy: QQC2.ScrollBar.AlwaysOff
342
343 // height of everything else in the dialog other than the content
344 property real otherHeights: root.header.height + root.footer.height + root.topPadding + root.bottomPadding;
345
346 property real calculatedMaximumWidth: Math.min(root.absoluteMaximumWidth, root.maximumWidth) - root.leftPadding - root.rightPadding
347 property real calculatedMaximumHeight: Math.min(root.absoluteMaximumHeight, root.maximumHeight) - root.topPadding - root.bottomPadding
348 property real calculatedImplicitWidth: (contentChildren.length === 1 && contentChildren[0].implicitWidth > 0
349 ? contentChildren[0].implicitWidth
350 : (contentItem.implicitWidth > 0 ? contentItem.implicitWidth : contentItem.width)) + leftPadding + rightPadding
351 property real calculatedImplicitHeight: (contentChildren.length === 1 && contentChildren[0].implicitHeight > 0
352 ? contentChildren[0].implicitHeight
353 : (contentItem.implicitHeight > 0 ? contentItem.implicitHeight : contentItem.height)) + topPadding + bottomPadding
354
355 onContentItemChanged: {
356 if (!contentItem) {
357 return;
358 }
359 /* Why this is necessary? A Flickable mainItem syncs its size with the conents only on startup,
360 and if the contents can change their size dinamically afterwards (wrapping text does that),
361 the contentsize will be wrong see BUG 477257
362 We also don't do this declaratively but only we are sure a contentItem is declared/created as just
363 accessing the property would create an internal flickable, making it impossible to assign custom
364 flickables/ listviews to the Dialog*/
365 contentItem.contentHeight = Qt.binding(()=>{return contentControl.calculatedImplicitHeight})
366 }
367
368 // how do we deal with the scrollbar width?
369 // - case 1: the dialog itself has the preferredWidth set
370 // -> we hint a width to the content so it shrinks to give space to the scrollbar
371 // - case 2: preferredWidth not set, so we are using the content's implicit width
372 // -> we expand the dialog's width to accommodate the scrollbar width (to respect the content's desired width)
373
374 // don't enforce preferred width and height if not set
375 property real preferredWidth: (root.preferredWidth >= 0 ? root.preferredWidth : calculatedImplicitWidth)
376 property real preferredHeight: root.preferredHeight >= 0 ? root.preferredHeight - otherHeights : calculatedImplicitHeight
377
378 property real maximumWidth: calculatedMaximumWidth
379 property real maximumHeight: calculatedMaximumHeight - otherHeights // we enforce maximum height solely from the content
380
381 implicitWidth: Math.min(preferredWidth, maximumWidth)
382 implicitHeight: Math.min(preferredHeight, maximumHeight)
383
384 // give an implied width and height to the contentItem so that features like word wrapping/eliding work
385 // cannot placed directly in contentControl as a child, so we must use a property
386 property var widthHint: Binding {
387 target: contentControl.contentChildren[0] || null
388 property: "width"
389
390 // we want to avoid horizontal scrolling, so we apply maximumWidth as a hint if necessary
391 property real preferredWidthHint: contentControl.contentItem.width
392 property real maximumWidthHint: contentControl.calculatedMaximumWidth - contentControl.leftPadding - contentControl.rightPadding
393
394 value: Math.min(maximumWidthHint, preferredWidthHint)
395
396 restoreMode: Binding.RestoreBinding
397 }
398 }
399
400 header: T.Control {
401 implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset,
402 implicitContentWidth + leftPadding + rightPadding)
403 implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset,
404 implicitContentHeight + topPadding + bottomPadding)
405
406 padding: Kirigami.Units.largeSpacing
407 bottomPadding: verticalPadding + headerSeparator.implicitHeight // add space for bottom separator
408
409 contentItem: RowLayout {
410 Kirigami.Heading {
411 id: heading
412 Layout.fillWidth: true
413 Layout.alignment: Qt.AlignVCenter
414 level: 2
415 text: root.title.length === 0 ? " " : root.title // always have text to ensure header height
416 elide: Text.ElideRight
417
418 // use tooltip for long text that is elided
419 QQC2.ToolTip.visible: truncated && titleHoverHandler.hovered
420 QQC2.ToolTip.text: root.title
421 HoverHandler { id: titleHoverHandler }
422 }
423 Kirigami.Icon {
424 id: closeIcon
425 visible: root.showCloseButton
426
427 // We want to position the close button in the top-right
428 // corner if the header is very tall, but we want to
429 // vertically center it in a short header
430 readonly property bool tallHeader: parent.height > (Kirigami.Units.iconSizes.smallMedium + Kirigami.Units.largeSpacing + Kirigami.Units.largeSpacing)
431 Layout.alignment: tallHeader ? Qt.AlignRight | Qt.AlignTop : Qt.AlignRight | Qt.AlignVCenter
432 Layout.topMargin: tallHeader ? Kirigami.Units.largeSpacing : 0
433 implicitHeight: Kirigami.Units.iconSizes.smallMedium
434 implicitWidth: implicitHeight
435
436 source: closeMouseArea.containsMouse ? "window-close" : "window-close-symbolic"
437 active: closeMouseArea.containsMouse
438 MouseArea {
439 id: closeMouseArea
440 hoverEnabled: Qt.styleHints.useHoverEffects
441 anchors.fill: parent
442 onClicked: mouse => root.reject()
443 }
444 }
445 }
446
447 // header background
448 background: Kirigami.ShadowedRectangle {
449 corners.topLeftRadius: Kirigami.Units.smallSpacing
450 corners.topRightRadius: Kirigami.Units.smallSpacing
451 Kirigami.Theme.colorSet: Kirigami.Theme.Header
452 Kirigami.Theme.inherit: false
453 color: Kirigami.Theme.backgroundColor
454 Kirigami.Separator {
455 id: headerSeparator
456 width: parent.width
457 anchors.bottom: parent.bottom
458 }
459 }
460 }
461
462 // use top level control rather than toolbar, since toolbar causes button rendering glitches
463 footer: T.Control {
464 id: footerToolBar
465
466 // if there is nothing in the footer, still maintain a height so that we can create a rounded bottom buffer for the dialog
467 property bool bufferMode: contentItem.implicitHeight === 0
468 implicitHeight: bufferMode ? Kirigami.Units.smallSpacing : contentItem.implicitHeight
469
470 leftPadding: 0; rightPadding: 0; bottomPadding: 0
471 topPadding: bufferMode ? 0 : footerSeparator.implicitHeight // add space for the separator above the footer
472
473 contentItem: RowLayout {
474 spacing: parent.spacing
475 // Don't let user interact with footer during transitions
476 enabled: root.opened
477
478 Loader {
479 id: leadingLoader
480 sourceComponent: root.footerLeadingComponent
481 }
482
483 // footer buttons
484 QQC2.DialogButtonBox {
485 // we don't explicitly set padding, to let the style choose the padding
486 id: dialogButtonBox
487 standardButtons: root.standardButtons
488 visible: count > 0
489
490 Layout.fillWidth: true
491 Layout.alignment: dialogButtonBox.alignment
492
493 position: QQC2.DialogButtonBox.Footer
494
495 // ensure themes don't add a background, since it can lead to visual inconsistencies
496 // with the rest of the dialog
497 background: null
498
499 // we need to hook all of the buttonbox events to the dialog events
500 onAccepted: root.accept()
501 onRejected: root.reject()
502 onApplied: root.applied()
503 onDiscarded: root.discarded()
504 onHelpRequested: root.helpRequested()
505 onReset: root.reset()
506
507 // add custom footer buttons
508 Repeater {
509 id: customFooterButtons
510 model: root.__visibleCustomFooterActions
511 // we have to use Button instead of ToolButton, because ToolButton has no visual distinction when disabled
512 delegate: QQC2.Button {
513 required property T.Action modelData
514
515 flat: flatFooterButtons
516 action: modelData
517 }
518 }
519 }
520
521 Loader {
522 id: trailingLoader
523 sourceComponent: root.footerTrailingComponent
524 }
525 }
526
527 background: Kirigami.ShadowedRectangle {
528 // curved footer bottom corners
529 corners.bottomLeftRadius: Kirigami.Units.smallSpacing
530 corners.bottomRightRadius: Kirigami.Units.smallSpacing
531
532 // we act as a content buffer if nothing is in the footer
533 Kirigami.Theme.colorSet: footerToolBar.bufferMode ? Kirigami.Theme.View : Kirigami.Theme.Window
534 Kirigami.Theme.inherit: false
535 color: Kirigami.Theme.backgroundColor
536
537 // separator above footer
538 Kirigami.Separator {
539 id: footerSeparator
540 visible: !footerToolBar.bufferMode
541 width: parent.width
542 anchors.top: parent.top
543 }
544 }
545 }
546}
An item that represents an abstract Action.
Definition Action.qml:15
AKONADI_MIME_EXPORT const char Header[]
QStringView level(QStringView ifopt)
QStringList filter(QStringView str, Qt::CaseSensitivity cs) const const
AlignVCenter
ElideRight
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Tue Mar 26 2024 11:18:46 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.