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

KDE's Doxygen guidelines are available online.