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

KDE's Doxygen guidelines are available online.