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

KDE's Doxygen guidelines are available online.