/* SPDX-FileCopyrightText: 2021 Devin Lin SPDX-FileCopyrightText: 2021 Noah Davis SPDX-FileCopyrightText: 2022 ivan tkachenko SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL */ pragma ComponentBehavior: Bound import QtQuick import QtQml import QtQuick.Layouts import QtQuick.Templates as T import QtQuick.Controls as QQC2 import org.kde.kirigami as Kirigami import org.kde.kirigami.dialogs as KDialogs /*! \qmltype Dialog \inqmlmodule org.kde.kirigami.dialogs \inherits Dialog \brief Popup dialog that is used for short tasks and user interaction. Dialog consists of three components: the header, the content, and the footer. By default, the header is a heading with text specified by the title property. By default, the footer consists of a row of buttons specified by the standardButtons and customFooterActions properties. The implicitHeight and implicitWidth of the dialog contentItem is the primary hint used for the dialog size. The dialog will be the minimum size required for the header, footer and content unless it is larger than maximumHeight and maximumWidth. Use preferredHeight and preferredWidth in order to manually specify a size for the dialog. If the content height exceeds the maximum height of the dialog, the dialog's contents will become scrollable. If the contentItem is a ListView, the dialog will take care of the necessary scrollbars and scrolling behaviour. Do not attempt to nest ListViews (it must be the top level item), as the scrolling behaviour will not be handled. Use ListView's header and footer instead. Example for a selection dialog: \qml import QtQuick import QtQuick.Layouts import QtQuick.Controls as QQC2 import org.kde.kirigami as Kirigami Kirigami.Dialog { title: i18n("Dialog") padding: 0 preferredWidth: Kirigami.Units.gridUnit * 16 standardButtons: Kirigami.Dialog.Ok | Kirigami.Dialog.Cancel onAccepted: console.log("OK button pressed") onRejected: console.log("Rejected") ColumnLayout { spacing: 0 Repeater { model: 5 delegate: QQC2.CheckDelegate { topPadding: Kirigami.Units.smallSpacing * 2 bottomPadding: Kirigami.Units.smallSpacing * 2 Layout.fillWidth: true text: modelData } } } } \endqml Example with scrolling (ListView scrolling behaviour is handled by the Dialog): \qml import QtQuick import QtQuick.Layouts import QtQuick.Controls as QQC2 import org.kde.kirigami as Kirigami Kirigami.Dialog { id: scrollableDialog title: i18n("Select Number") ListView { id: listView // hints for the dialog dimensions implicitWidth: Kirigami.Units.gridUnit * 16 implicitHeight: Kirigami.Units.gridUnit * 16 model: 100 delegate: QQC2.RadioDelegate { topPadding: Kirigami.Units.smallSpacing * 2 bottomPadding: Kirigami.Units.smallSpacing * 2 implicitWidth: listView.width text: modelData } } } \endqml There are also sub-components of the Dialog that target specific usecases, and can reduce boilerplate code if used. \sa PromptDialog \sa MenuDialog */ T.Dialog { id: root /*! \qmlproperty list Dialog::dialogData \brief This property holds the dialog's contents; includes Items and QtObjects. */ default property alias dialogData: contentControl.contentData /*! \qmlproperty list Dialog::dialogChildren \brief This property holds the content items of the dialog. The initial height and width of the dialog is calculated from the implicitWidth and implicitHeight of the content. */ property alias dialogChildren: contentControl.contentChildren /*! \brief This property sets the absolute maximum height the dialog can have. The height restriction is solely applied on the content, so if the maximum height given is not larger than the height of the header and footer, it will be ignored. This is the window height, subtracted by largeSpacing on both the top and bottom. */ readonly property real absoluteMaximumHeight: ((parent && parent.height) || Infinity) - Kirigami.Units.largeSpacing * 2 /*! \brief This property holds the absolute maximum width the dialog can have. By default, it is the window width, subtracted by largeSpacing on both the top and bottom. */ readonly property real absoluteMaximumWidth: ((parent && parent.width) || Infinity) - Kirigami.Units.largeSpacing * 2 readonly property real __borderWidth: !root.hasOwnProperty("popupType") || popupType === T.Popup.Item ? 1 : 0 /*! \brief This property holds the maximum height the dialog can have (including the header and footer). The height restriction is solely enforced on the content, so if the maximum height given is not larger than the height of the header and footer, it will be ignored. By default, this is absoluteMaximumHeight. */ property real maximumHeight: absoluteMaximumHeight /*! \brief This property holds the maximum width the dialog can have. By default, this is absoluteMaximumWidth. */ property real maximumWidth: absoluteMaximumWidth /*! \brief This property holds the preferred height of the dialog. The content will receive a hint for how tall it should be to have the dialog to be this height. If the content, header or footer require more space, then the height of the dialog will expand to the necessary amount of space. */ property real preferredHeight: -1 /*! \brief This property holds the preferred width of the dialog. The content will receive a hint for how wide it should be to have the dialog be this wide. If the content, header or footer require more space, then the width of the dialog will expand to the necessary amount of space. */ property real preferredWidth: -1 /*! \brief This property holds the component to the left of the footer buttons. */ property Component footerLeadingComponent /*! \brief his property holds the component to the right of the footer buttons. */ property Component footerTrailingComponent /*! \brief This property sets whether to show the close button in the header. */ property bool showCloseButton: true /*! \brief This property sets whether the footer button style should be flat. */ property bool flatFooterButtons: false /*! \brief This property holds the custom actions displayed in the footer. Example usage: \code import QtQuick import org.kde.kirigami as Kirigami Kirigami.PromptDialog { id: dialog title: i18n("Confirm Playback") subtitle: i18n("Are you sure you want to play this song? It's really loud!") standardButtons: Kirigami.Dialog.Cancel customFooterActions: [ Kirigami.Action { text: i18n("Play") icon.name: "media-playback-start" onTriggered: { //... dialog.close(); } } ] } \endcode \sa Action */ property list customFooterActions // DialogButtonBox should NOT contain invisible buttons, because in Qt 6 // ListView preserves space even for invisible items. readonly property list __visibleCustomFooterActions: customFooterActions .filter(action => !(action instanceof Kirigami.Action) || action?.visible) function standardButton(button): T.AbstractButton { // in case a footer is redefined if (footer instanceof T.DialogButtonBox) { return (footer as T.DialogButtonBox).standardButton(button); } else if (footer === footerToolBar) { return dialogButtonBox.standardButton(button); } else { return null; } } function customFooterButton(action: T.Action): T.AbstractButton { if (!action) { // Even if there's a null object in the list of actions, we should // not return a button for it. return null; } const index = __visibleCustomFooterActions.indexOf(action); if (index < 0) { return null; } return customFooterButtons.itemAt(index) as T.AbstractButton; } z: Kirigami.OverlayZStacking.z // calculate dimensions and in case footer is wider than content, use that implicitWidth: Math.max(implicitContentWidth, implicitFooterWidth, implicitHeaderWidth) + leftPadding + rightPadding // maximum width enforced from our content (one source of truth) to avoid binding loops implicitHeight: implicitContentHeight + topPadding + bottomPadding + (implicitHeaderHeight > 0 ? implicitHeaderHeight + spacing : 0) + (implicitFooterHeight > 0 ? implicitFooterHeight + spacing : 0); // misc. dialog settings closePolicy: QQC2.Popup.CloseOnEscape | QQC2.Popup.CloseOnReleaseOutside modal: true clip: false padding: 0 horizontalPadding: __borderWidth + padding // determine parent so that popup knows which window to popup in // we want to open the dialog in the center of the window, if possible parent: typeof applicationWindow !== "undefined" ? applicationWindow().overlay : undefined // center dialog x: parent ? Math.round(((parent && parent.width) - width) / 2) : 0 y: parent ? Math.round(((parent && parent.height) - height) / 2) + Kirigami.Units.gridUnit * 2 * (1 - opacity) : 0 // move animation // dialog enter and exit transitions enter: Transition { NumberAnimation { property: "opacity"; from: 0; to: 1; easing.type: Easing.InOutQuad; duration: Kirigami.Units.longDuration } } exit: Transition { NumberAnimation { property: "opacity"; from: 1; to: 0; easing.type: Easing.InOutQuad; duration: Kirigami.Units.longDuration } } // black background, fades in and out QQC2.Overlay.modal: Rectangle { color: Qt.rgba(0, 0, 0, 0.3) // the opacity of the item is changed internally by QQuickPopup on open/close Behavior on opacity { OpacityAnimator { duration: Kirigami.Units.longDuration easing.type: Easing.InOutQuad } } } // dialog view background background: Kirigami.ShadowedRectangle { id: rect Kirigami.Theme.colorSet: Kirigami.Theme.View Kirigami.Theme.inherit: false color: Kirigami.Theme.backgroundColor radius: !root.hasOwnProperty("popupType") || root.popupType === T.Popup.Item ? Kirigami.Units.cornerRadius : 0 shadow { size: radius * 2 color: Qt.rgba(0, 0, 0, 0.3) yOffset: 1 } border { width: root.__borderWidth color: Kirigami.ColorUtils.linearInterpolation(Kirigami.Theme.backgroundColor, Kirigami.Theme.textColor, Kirigami.Theme.frameContrast); } } // dialog content contentItem: QQC2.ScrollView { id: contentControl // ensure view colour scheme, and background color Kirigami.Theme.inherit: false Kirigami.Theme.colorSet: Kirigami.Theme.View QQC2.ScrollBar.horizontal.policy: QQC2.ScrollBar.AlwaysOff // height of everything else in the dialog other than the content property real otherHeights: (root.header?.height ?? 0) + (root.footer?.height ?? 0) + root.topPadding + root.bottomPadding; property real calculatedMaximumWidth: Math.min(root.absoluteMaximumWidth, root.maximumWidth) - root.leftPadding - root.rightPadding property real calculatedMaximumHeight: Math.min(root.absoluteMaximumHeight, root.maximumHeight) - root.topPadding - root.bottomPadding property real calculatedImplicitWidth: implicitContentWidth + leftPadding + rightPadding property real calculatedImplicitHeight: implicitContentHeight + topPadding + bottomPadding onContentItemChanged: { const contentFlickable = contentItem as Flickable; if (contentFlickable) { /* Why this is necessary? A Flickable mainItem syncs its size with the contents only on startup, and if the contents can change their size dynamically afterwards (wrapping text does that), the contentsize will be wrong see BUG 477257. We also don't do this declaratively but only we are sure a contentItem is declared/created as just accessing the property would create an internal Flickable, making it impossible to assign custom flickables/listviews to the Dialog. */ contentFlickable.contentHeight = Qt.binding(() => calculatedImplicitHeight); contentFlickable.clip = true; } } // how do we deal with the scrollbar width? // - case 1: the dialog itself has the preferredWidth set // -> we hint a width to the content so it shrinks to give space to the scrollbar // - case 2: preferredWidth not set, so we are using the content's implicit width // -> we expand the dialog's width to accommodate the scrollbar width (to respect the content's desired width) // don't enforce preferred width and height if not set (-1), and expand to a larger implicit size property real preferredWidth: Math.max(root.preferredWidth, calculatedImplicitWidth) property real preferredHeight: Math.max(root.preferredHeight - otherHeights, calculatedImplicitHeight) property real maximumWidth: calculatedMaximumWidth property real maximumHeight: calculatedMaximumHeight - otherHeights // we enforce maximum height solely from the content implicitWidth: Math.min(preferredWidth, maximumWidth) implicitHeight: Math.min(preferredHeight, maximumHeight) // give an implied width and height to the contentItem so that features like word wrapping/eliding work // cannot placed directly in contentControl as a child, so we must use a property property var widthHint: Binding { target: contentControl.contentChildren[0] || null property: "width" value: contentControl.width - contentControl.leftPadding - contentControl.rightPadding restoreMode: Binding.RestoreBinding } } header: KDialogs.DialogHeader { visible: !root.hasOwnProperty("popupType") || root.popupType === T.Popup.Item dialog: root contentItem: KDialogs.DialogHeaderTopContent { dialog: root } } // use top level control rather than toolbar, since toolbar causes button rendering glitches footer: T.Control { id: footerToolBar // if there is nothing in the footer, still maintain a height so that we can create a rounded bottom buffer for the dialog property bool bufferMode: !root.footerLeadingComponent && !dialogButtonBox.visible implicitHeight: bufferMode ? Math.round(Kirigami.Units.smallSpacing / 2) : implicitContentHeight + topPadding + bottomPadding implicitWidth: footerLayout.implicitWidth + leftPadding + rightPadding padding: !bufferMode ? Kirigami.Units.largeSpacing : 0 contentItem: RowLayout { id: footerLayout spacing: footerToolBar.spacing // Don't let user interact with footer during transitions enabled: root.opened Loader { id: leadingLoader sourceComponent: root.footerLeadingComponent } // footer buttons QQC2.DialogButtonBox { // we don't explicitly set padding, to let the style choose the padding id: dialogButtonBox standardButtons: root.standardButtons visible: count > 0 padding: 0 Layout.fillWidth: true Layout.alignment: dialogButtonBox.alignment position: QQC2.DialogButtonBox.Footer // ensure themes don't add a background, since it can lead to visual inconsistencies // with the rest of the dialog background: null // we need to hook all of the buttonbox events to the dialog events onAccepted: root.accept() onRejected: root.reject() onApplied: root.applied() onDiscarded: root.discarded() onHelpRequested: root.helpRequested() onReset: root.reset() // add custom footer buttons Repeater { id: customFooterButtons model: root.__visibleCustomFooterActions // we have to use Button instead of ToolButton, because ToolButton has no visual distinction when disabled delegate: QQC2.Button { required property T.Action modelData flat: root.flatFooterButtons action: modelData } } } Loader { id: trailingLoader sourceComponent: root.footerTrailingComponent } } background: Item { Kirigami.Separator { id: footerSeparator visible: if (root.contentItem instanceof T.Pane || root.contentItem instanceof Flickable) { const itemContentHeight = (root.contentItem as T.Pane)?.contentHeight ?? (root.contentItem as Flickable)?.contentHeight return root.contentItem.height < itemContentHeight; } else { return false; } width: parent.width anchors.top: parent.top } } } }