Warning, /frameworks/kirigami/src/controls/Dialog.qml is written in an unsupported language. File is not indexed.

0001 /*
0002     SPDX-FileCopyrightText: 2021 Devin Lin <espidev@gmail.com>
0003     SPDX-FileCopyrightText: 2021 Noah Davis <noahadvs@gmail.com>
0004     SPDX-FileCopyrightText: 2022 ivan tkachenko <me@ratijas.tk>
0005     SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
0006 */
0007 
0008 import QtQuick
0009 import QtQml
0010 import QtQuick.Layouts
0011 import QtQuick.Templates as T
0012 import QtQuick.Controls as QQC2
0013 import org.kde.kirigami as Kirigami
0014 
0015 /**
0016  * @brief Popup dialog that is used for short tasks and user interaction.
0017  *
0018  * Dialog consists of three components: the header, the content,
0019  * and the footer.
0020  *
0021  * By default, the header is a heading with text specified by the
0022  * `title` property.
0023  *
0024  * By default, the footer consists of a row of buttons specified by
0025  * the `standardButtons` and `customFooterActions` properties.
0026  *
0027  * The `implicitHeight` and `implicitWidth` of the dialog contentItem is
0028  * the primary hint used for the dialog size. The dialog will be the
0029  * minimum size required for the header, footer and content unless
0030  * it is larger than `maximumHeight` and `maximumWidth`. Use
0031  * `preferredHeight` and `preferredWidth` in order to manually specify
0032  * a size for the dialog.
0033  *
0034  * If the content height exceeds the maximum height of the dialog, the
0035  * dialog's contents will become scrollable.
0036  *
0037  * If the contentItem is a <b>ListView</b>, the dialog will take care of the
0038  * necessary scrollbars and scrolling behaviour. Do <b>not</b> attempt
0039  * to nest ListViews (it must be the top level item), as the scrolling
0040  * behaviour will not be handled. Use ListView's `header` and `footer` instead.
0041  *
0042  * Example for a selection dialog:
0043  *
0044  * @code{.qml}
0045  * import QtQuick 2.15
0046  * import QtQuick.Layouts 1.15
0047  * import QtQuick.Controls 2.15 as Controls
0048  * import org.kde.kirigami 2.19 as Kirigami
0049  *
0050  * Kirigami.Dialog {
0051  *     title: i18n("Dialog")
0052  *     padding: 0
0053  *     preferredWidth: Kirigami.Units.gridUnit * 16
0054  *
0055  *     standardButtons: Kirigami.Dialog.Ok | Kirigami.Dialog.Cancel
0056  *
0057  *     onAccepted: console.log("OK button pressed")
0058  *     onRejected: console.log("Rejected")
0059  *
0060  *     ColumnLayout {
0061  *         spacing: 0
0062  *         Repeater {
0063  *             model: 5
0064  *             delegate: Controls.CheckDelegate {
0065  *                 topPadding: Kirigami.Units.smallSpacing * 2
0066  *                 bottomPadding: Kirigami.Units.smallSpacing * 2
0067  *                 Layout.fillWidth: true
0068  *                 text: modelData
0069  *             }
0070  *         }
0071  *     }
0072  * }
0073  * @endcode
0074  *
0075  * Example with scrolling (ListView scrolling behaviour is handled by the Dialog):
0076  *
0077  * @code{.qml}
0078  * Kirigami.Dialog {
0079  *     id: scrollableDialog
0080  *     title: i18n("Select Number")
0081  *
0082  *     ListView {
0083  *         id: listView
0084  *         // hints for the dialog dimensions
0085  *         implicitWidth: Kirigami.Units.gridUnit * 16
0086  *         implicitHeight: Kirigami.Units.gridUnit * 16
0087  *
0088  *         model: 100
0089  *         delegate: Controls.RadioDelegate {
0090  *             topPadding: Kirigami.Units.smallSpacing * 2
0091  *             bottomPadding: Kirigami.Units.smallSpacing * 2
0092  *             implicitWidth: listView.width
0093  *             text: modelData
0094  *         }
0095  *     }
0096  * }
0097  * @endcode
0098  *
0099  * There are also sub-components of the Dialog that target specific usecases,
0100  * and can reduce boilerplate code if used:
0101  *
0102  * @see PromptDialog
0103  * @see MenuDialog
0104  *
0105  * @inherit QtQuick.QtObject
0106  */
0107 T.Dialog {
0108     id: root
0109 
0110     /**
0111      * @brief This property holds the dialog's contents; includes Items and QtObjects.
0112      * @property list<QtObject> dialogData
0113      */
0114     default property alias dialogData: contentControl.contentData
0115 
0116     /**
0117      * @brief This property holds the content items of the dialog.
0118      *
0119      * The initial height and width of the dialog is calculated from the
0120      * `implicitWidth` and `implicitHeight` of the content.
0121      *
0122      * @property list<Item> dialogChildren
0123      */
0124     property alias dialogChildren: contentControl.contentChildren
0125 
0126     /**
0127      * @brief This property sets the absolute maximum height the dialog can have.
0128      *
0129      * The height restriction is solely applied on the content, so if the
0130      * maximum height given is not larger than the height of the header and
0131      * footer, it will be ignored.
0132      *
0133      * This is the window height, subtracted by largeSpacing on both the top
0134      * and bottom.
0135      */
0136     readonly property real absoluteMaximumHeight: parent.height - Kirigami.Units.largeSpacing * 2
0137 
0138     /**
0139      * @brief This property holds the absolute maximum width the dialog can have.
0140      *
0141      * By default, it is the window width, subtracted by largeSpacing on both
0142      * the top and bottom.
0143      */
0144     readonly property real absoluteMaximumWidth: parent.width - Kirigami.Units.largeSpacing * 2
0145 
0146     /**
0147      * @brief This property holds the maximum height the dialog can have
0148      * (including the header and footer).
0149      *
0150      * The height restriction is solely enforced on the content, so if the
0151      * maximum height given is not larger than the height of the header and
0152      * footer, it will be ignored.
0153      *
0154      * By default, this is `absoluteMaximumHeight`.
0155      */
0156     property real maximumHeight: absoluteMaximumHeight
0157 
0158     /**
0159      * @brief This property holds the maximum width the dialog can have.
0160      *
0161      * By default, this is `absoluteMaximumWidth`.
0162      */
0163     property real maximumWidth: absoluteMaximumWidth
0164 
0165     /**
0166      * @brief This property holds the preferred height of the dialog.
0167      *
0168      * The content will receive a hint for how tall it should be to have
0169      * the dialog to be this height.
0170      *
0171      * If the content, header or footer require more space, then the height
0172      * of the dialog will expand to the necessary amount of space.
0173      */
0174     property real preferredHeight: -1
0175 
0176     /**
0177      * @brief This property holds the preferred width of the dialog.
0178      *
0179      * The content will receive a hint for how wide it should be to have
0180      * the dialog be this wide.
0181      *
0182      * If the content, header or footer require more space, then the width
0183      * of the dialog will expand to the necessary amount of space.
0184      */
0185     property real preferredWidth: -1
0186 
0187 
0188     /**
0189      * @brief This property holds the component to the left of the footer buttons.
0190      */
0191     property Component footerLeadingComponent
0192 
0193     /**
0194      * @brief his property holds the component to the right of the footer buttons.
0195      */
0196     property Component footerTrailingComponent
0197 
0198     /**
0199      * @brief This property sets whether to show the close button in the header.
0200      */
0201     property bool showCloseButton: true
0202 
0203     /**
0204      * @brief This property sets whether the footer button style should be flat.
0205      */
0206     property bool flatFooterButtons: false
0207 
0208     /**
0209      * @brief This property holds the custom actions displayed in the footer.
0210      *
0211      * Example usage:
0212      * @code{.qml}
0213      * import QtQuick 2.15
0214      * import QtQuick.Controls 2.15 as Controls
0215      * import org.kde.kirigami 2.18 as Kirigami
0216      *
0217      * Kirigami.PromptDialog {
0218      *     id: dialog
0219      *     title: i18n("Confirm Playback")
0220      *     subtitle: i18n("Are you sure you want to play this song? It's really loud!")
0221      *
0222      *     standardButtons: Kirigami.Dialog.Cancel
0223      *     customFooterActions: [
0224      *         Kirigami.Action {
0225      *             text: i18n("Play")
0226      *             icon.name: "media-playback-start"
0227      *             onTriggered: {
0228      *                 //...
0229      *                 dialog.close();
0230      *             }
0231      *         }
0232      *     ]
0233      * }
0234      * @endcode
0235      *
0236      * @see org::kde::kirigami::Action
0237      */
0238     property list<T.Action> customFooterActions
0239 
0240     // DialogButtonBox should NOT contain invisible buttons, because in Qt 6
0241     // ListView preserves space even for invisible items.
0242     readonly property list<T.Action> __visibleCustomFooterActions: customFooterActions
0243         .filter(action => !(action instanceof Kirigami.Action) || action?.visible)
0244 
0245     // default standard button
0246     standardButtons: QQC2.Dialog.Close
0247 
0248     function standardButton(button): T.AbstractButton {
0249         // in case a footer is redefined
0250         if (footer instanceof T.DialogButtonBox) {
0251             return footer.standardButton(button);
0252         } else if (footer === footerToolBar) {
0253             return dialogButtonBox.standardButton(button);
0254         } else {
0255             return null;
0256         }
0257     }
0258 
0259     function customFooterButton(action: T.Action): T.AbstractButton {
0260         if (!action) {
0261             // Even if there's a null object in the list of actions, we should
0262             // not return a button for it.
0263             return null;
0264         }
0265         const index = __visibleCustomFooterActions.indexOf(action);
0266         if (index < 0) {
0267             return null;
0268         }
0269         return customFooterButtons.itemAt(index) as T.AbstractButton;
0270     }
0271 
0272     z: Kirigami.OverlayZStacking.z
0273 
0274     // calculate dimensions
0275     implicitWidth: contentItem.implicitWidth + leftPadding + rightPadding // maximum width enforced from our content (one source of truth) to avoid binding loops
0276     implicitHeight: contentItem.implicitHeight + topPadding + bottomPadding
0277                     + (implicitHeaderHeight > 0 ? implicitHeaderHeight + spacing : 0)
0278                     + (implicitFooterHeight > 0 ? implicitFooterHeight + spacing : 0);
0279 
0280     // misc. dialog settings
0281     closePolicy: QQC2.Popup.CloseOnEscape | QQC2.Popup.CloseOnReleaseOutside
0282     modal: true
0283     clip: false
0284     padding: 0
0285 
0286     // determine parent so that popup knows which window to popup in
0287     // we want to open the dialog in the center of the window, if possible
0288     Component.onCompleted: {
0289         if (typeof applicationWindow !== "undefined") {
0290             parent = applicationWindow().overlay;
0291         }
0292     }
0293 
0294     // center dialog
0295     x: Math.round((parent.width - width) / 2)
0296     y: Math.round((parent.height - height) / 2) + Kirigami.Units.gridUnit * 2 * (1 - opacity) // move animation
0297 
0298     // dialog enter and exit transitions
0299     enter: Transition {
0300         NumberAnimation { property: "opacity"; from: 0; to: 1; easing.type: Easing.InOutQuad; duration: Kirigami.Units.longDuration }
0301     }
0302     exit: Transition {
0303         NumberAnimation { property: "opacity"; from: 1; to: 0; easing.type: Easing.InOutQuad; duration: Kirigami.Units.longDuration }
0304     }
0305 
0306     // black background, fades in and out
0307     QQC2.Overlay.modal: Rectangle {
0308         color: Qt.rgba(0, 0, 0, 0.3)
0309 
0310         // the opacity of the item is changed internally by QQuickPopup on open/close
0311         Behavior on opacity {
0312             OpacityAnimator {
0313                 duration: Kirigami.Units.longDuration
0314                 easing.type: Easing.InOutQuad
0315             }
0316         }
0317     }
0318 
0319     // dialog view background
0320     background: Kirigami.ShadowedRectangle {
0321         id: rect
0322         Kirigami.Theme.colorSet: Kirigami.Theme.View
0323         Kirigami.Theme.inherit: false
0324         color: Kirigami.Theme.backgroundColor
0325         radius: Kirigami.Units.smallSpacing
0326         shadow {
0327             size: radius * 2
0328             color: Qt.rgba(0, 0, 0, 0.3)
0329             yOffset: 1
0330         }
0331     }
0332 
0333     // dialog content
0334     contentItem: QQC2.ScrollView {
0335         id: contentControl
0336 
0337         // ensure view colour scheme, and background color
0338         Kirigami.Theme.inherit: false
0339         Kirigami.Theme.colorSet: Kirigami.Theme.View
0340 
0341         QQC2.ScrollBar.horizontal.policy: QQC2.ScrollBar.AlwaysOff
0342 
0343         // height of everything else in the dialog other than the content
0344         property real otherHeights: root.header.height + root.footer.height + root.topPadding + root.bottomPadding;
0345 
0346         property real calculatedMaximumWidth: Math.min(root.absoluteMaximumWidth, root.maximumWidth) - root.leftPadding - root.rightPadding
0347         property real calculatedMaximumHeight: Math.min(root.absoluteMaximumHeight, root.maximumHeight) - root.topPadding - root.bottomPadding
0348         property real calculatedImplicitWidth: (contentChildren.length === 1 && contentChildren[0].implicitWidth > 0
0349             ? contentChildren[0].implicitWidth
0350             : (contentItem.implicitWidth > 0 ? contentItem.implicitWidth : contentItem.width)) + leftPadding + rightPadding
0351         property real calculatedImplicitHeight: (contentChildren.length === 1 && contentChildren[0].implicitHeight > 0
0352                 ? contentChildren[0].implicitHeight
0353                 : (contentItem.implicitHeight > 0 ? contentItem.implicitHeight : contentItem.height)) + topPadding + bottomPadding
0354 
0355         onContentItemChanged: {
0356             if (!contentItem) {
0357                 return;
0358             }
0359             /* Why this is necessary? A Flickable mainItem syncs its size with the conents only on startup,
0360              and if the contents can change their size dinamically afterwards (wrapping text does that),
0361              the contentsize will be wrong see BUG 477257
0362              We also don't do this declaratively but only we are sure a contentItem is declared/created as just
0363              accessing the property would create an internal flickable, making it impossible to assign custom
0364              flickables/ listviews to the Dialog*/
0365             contentItem.contentHeight = Qt.binding(()=>{return contentControl.calculatedImplicitHeight})
0366         }
0367 
0368         // how do we deal with the scrollbar width?
0369         // - case 1: the dialog itself has the preferredWidth set
0370         //   -> we hint a width to the content so it shrinks to give space to the scrollbar
0371         // - case 2: preferredWidth not set, so we are using the content's implicit width
0372         //   -> we expand the dialog's width to accommodate the scrollbar width (to respect the content's desired width)
0373 
0374         // don't enforce preferred width and height if not set
0375         property real preferredWidth: (root.preferredWidth >= 0 ? root.preferredWidth : calculatedImplicitWidth)
0376         property real preferredHeight: root.preferredHeight >= 0 ? root.preferredHeight - otherHeights : calculatedImplicitHeight
0377 
0378         property real maximumWidth: calculatedMaximumWidth
0379         property real maximumHeight: calculatedMaximumHeight - otherHeights // we enforce maximum height solely from the content
0380 
0381         implicitWidth: Math.min(preferredWidth, maximumWidth)
0382         implicitHeight: Math.min(preferredHeight, maximumHeight)
0383 
0384         // give an implied width and height to the contentItem so that features like word wrapping/eliding work
0385         // cannot placed directly in contentControl as a child, so we must use a property
0386         property var widthHint: Binding {
0387             target: contentControl.contentChildren[0] || null
0388             property: "width"
0389 
0390             // we want to avoid horizontal scrolling, so we apply maximumWidth as a hint if necessary
0391             property real preferredWidthHint: contentControl.contentItem.width
0392             property real maximumWidthHint: contentControl.calculatedMaximumWidth - contentControl.leftPadding - contentControl.rightPadding
0393 
0394             value: Math.min(maximumWidthHint, preferredWidthHint)
0395 
0396             restoreMode: Binding.RestoreBinding
0397         }
0398     }
0399 
0400     header: T.Control {
0401         implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset,
0402                                 implicitContentWidth + leftPadding + rightPadding)
0403         implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset,
0404                                 implicitContentHeight + topPadding + bottomPadding)
0405 
0406         padding: Kirigami.Units.largeSpacing
0407         bottomPadding: verticalPadding + headerSeparator.implicitHeight // add space for bottom separator
0408 
0409         contentItem: RowLayout {
0410             Kirigami.Heading {
0411                 id: heading
0412                 Layout.fillWidth: true
0413                 Layout.alignment: Qt.AlignVCenter
0414                 level: 2
0415                 text: root.title.length === 0 ? " " : root.title // always have text to ensure header height
0416                 elide: Text.ElideRight
0417 
0418                 // use tooltip for long text that is elided
0419                 QQC2.ToolTip.visible: truncated && titleHoverHandler.hovered
0420                 QQC2.ToolTip.text: root.title
0421                 HoverHandler { id: titleHoverHandler }
0422             }
0423             Kirigami.Icon {
0424                 id: closeIcon
0425                 visible: root.showCloseButton
0426 
0427                 // We want to position the close button in the top-right
0428                 // corner if the header is very tall, but we want to
0429                 // vertically center it in a short header
0430                 readonly property bool tallHeader: parent.height > (Kirigami.Units.iconSizes.smallMedium + Kirigami.Units.largeSpacing + Kirigami.Units.largeSpacing)
0431                 Layout.alignment: tallHeader ? Qt.AlignRight | Qt.AlignTop : Qt.AlignRight | Qt.AlignVCenter
0432                 Layout.topMargin: tallHeader ? Kirigami.Units.largeSpacing : 0
0433                 implicitHeight: Kirigami.Units.iconSizes.smallMedium
0434                 implicitWidth: implicitHeight
0435 
0436                 source: closeMouseArea.containsMouse ? "window-close" : "window-close-symbolic"
0437                 active: closeMouseArea.containsMouse
0438                 MouseArea {
0439                     id: closeMouseArea
0440                     hoverEnabled: Qt.styleHints.useHoverEffects
0441                     anchors.fill: parent
0442                     onClicked: mouse => root.reject()
0443                 }
0444             }
0445         }
0446 
0447         // header background
0448         background: Kirigami.ShadowedRectangle {
0449             corners.topLeftRadius: Kirigami.Units.smallSpacing
0450             corners.topRightRadius: Kirigami.Units.smallSpacing
0451             Kirigami.Theme.colorSet: Kirigami.Theme.Header
0452             Kirigami.Theme.inherit: false
0453             color: Kirigami.Theme.backgroundColor
0454             Kirigami.Separator {
0455                 id: headerSeparator
0456                 width: parent.width
0457                 anchors.bottom: parent.bottom
0458             }
0459         }
0460     }
0461 
0462     // use top level control rather than toolbar, since toolbar causes button rendering glitches
0463     footer: T.Control {
0464         id: footerToolBar
0465 
0466         // if there is nothing in the footer, still maintain a height so that we can create a rounded bottom buffer for the dialog
0467         property bool bufferMode: contentItem.implicitHeight === 0
0468         implicitHeight: bufferMode ? Kirigami.Units.smallSpacing : contentItem.implicitHeight
0469 
0470         leftPadding: 0; rightPadding: 0; bottomPadding: 0
0471         topPadding: bufferMode ? 0 : footerSeparator.implicitHeight // add space for the separator above the footer
0472 
0473         contentItem: RowLayout {
0474             spacing: parent.spacing
0475             // Don't let user interact with footer during transitions
0476             enabled: root.opened
0477 
0478             Loader {
0479                 id: leadingLoader
0480                 sourceComponent: root.footerLeadingComponent
0481             }
0482 
0483             // footer buttons
0484             QQC2.DialogButtonBox {
0485                 // we don't explicitly set padding, to let the style choose the padding
0486                 id: dialogButtonBox
0487                 standardButtons: root.standardButtons
0488                 visible: count > 0
0489 
0490                 Layout.fillWidth: true
0491                 Layout.alignment: dialogButtonBox.alignment
0492 
0493                 position: QQC2.DialogButtonBox.Footer
0494 
0495                 // ensure themes don't add a background, since it can lead to visual inconsistencies
0496                 // with the rest of the dialog
0497                 background: null
0498 
0499                 // we need to hook all of the buttonbox events to the dialog events
0500                 onAccepted: root.accept()
0501                 onRejected: root.reject()
0502                 onApplied: root.applied()
0503                 onDiscarded: root.discarded()
0504                 onHelpRequested: root.helpRequested()
0505                 onReset: root.reset()
0506 
0507                 // add custom footer buttons
0508                 Repeater {
0509                     id: customFooterButtons
0510                     model: root.__visibleCustomFooterActions
0511                     // we have to use Button instead of ToolButton, because ToolButton has no visual distinction when disabled
0512                     delegate: QQC2.Button {
0513                         required property T.Action modelData
0514 
0515                         flat: flatFooterButtons
0516                         action: modelData
0517                     }
0518                 }
0519             }
0520 
0521             Loader {
0522                 id: trailingLoader
0523                 sourceComponent: root.footerTrailingComponent
0524             }
0525         }
0526 
0527         background: Kirigami.ShadowedRectangle {
0528             // curved footer bottom corners
0529             corners.bottomLeftRadius: Kirigami.Units.smallSpacing
0530             corners.bottomRightRadius: Kirigami.Units.smallSpacing
0531 
0532             // we act as a content buffer if nothing is in the footer
0533             Kirigami.Theme.colorSet: footerToolBar.bufferMode ? Kirigami.Theme.View : Kirigami.Theme.Window
0534             Kirigami.Theme.inherit: false
0535             color: Kirigami.Theme.backgroundColor
0536 
0537             // separator above footer
0538             Kirigami.Separator {
0539                 id: footerSeparator
0540                 visible: !footerToolBar.bufferMode
0541                 width: parent.width
0542                 anchors.top: parent.top
0543             }
0544         }
0545     }
0546 }