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
110 *
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.
138 *
139 * This is the window height, subtracted by largeSpacing on both the top
140 * and bottom.
141 */
142 readonly property real absoluteMaximumHeight: parent.height - Kirigami.Units.largeSpacing * 2
143
144 /**
145 * @brief This property holds the absolute maximum width the dialog can have.
146 *
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.width - Kirigami.Units.largeSpacing * 2
151
152 /**
153 * @brief This property holds the maximum height the dialog can have
154 * (including the header and footer).
155 *
156 * The height restriction is solely enforced on the content, so if the
157 * maximum height given is not larger than the height of the header and
158 * footer, it will be ignored.
159 *
160 * By default, this is `absoluteMaximumHeight`.
161 */
162 property real maximumHeight: absoluteMaximumHeight
163
164 /**
165 * @brief This property holds the maximum width the dialog can have.
166 *
167 * By default, this is `absoluteMaximumWidth`.
168 */
169 property real maximumWidth: absoluteMaximumWidth
170
171 /**
172 * @brief This property holds the preferred height of the dialog.
173 *
174 * The content will receive a hint for how tall it should be to have
175 * the dialog to be this height.
176 *
177 * If the content, header or footer require more space, then the height
178 * of the dialog will expand to the necessary amount of space.
179 */
180 property real preferredHeight: -1
181
182 /**
183 * @brief This property holds the preferred width of the dialog.
184 *
185 * The content will receive a hint for how wide it should be to have
186 * the dialog be this wide.
187 *
188 * If the content, header or footer require more space, then the width
189 * of the dialog will expand to the necessary amount of space.
190 */
191 property real preferredWidth: -1
192
193
194 /**
195 * @brief This property holds the component to the left of the footer buttons.
196 */
197 property Component footerLeadingComponent
198
199 /**
200 * @brief his property holds the component to the right of the footer buttons.
201 */
202 property Component footerTrailingComponent
203
204 /**
205 * @brief This property sets whether to show the close button in the header.
206 */
207 property bool showCloseButton: true
208
209 /**
210 * @brief This property sets whether the footer button style should be flat.
211 */
212 property bool flatFooterButtons: false
213
214 /**
215 * @brief This property holds the custom actions displayed in the footer.
216 *
217 * Example usage:
218 * @code{.qml}
219 * import QtQuick
220 * import org.kde.kirigami as Kirigami
221 *
222 * Kirigami.PromptDialog {
223 * id: dialog
224 * title: i18n("Confirm Playback")
225 * subtitle: i18n("Are you sure you want to play this song? It's really loud!")
226 *
227 * standardButtons: Kirigami.Dialog.Cancel
228 * customFooterActions: [
229 * Kirigami.Action {
230 * text: i18n("Play")
231 * icon.name: "media-playback-start"
232 * onTriggered: {
233 * //...
234 * dialog.close();
235 * }
236 * }
237 * ]
238 * }
239 * @endcode
240 *
241 * @see org::kde::kirigami::Action
242 */
243 property list<T.Action> customFooterActions
244
245 // DialogButtonBox should NOT contain invisible buttons, because in Qt 6
246 // ListView preserves space even for invisible items.
247 readonly property list<T.Action> __visibleCustomFooterActions: customFooterActions
248 .filter(action => !(action instanceof Kirigami.Action) || action?.visible)
249
250 function standardButton(button): T.AbstractButton {
251 // in case a footer is redefined
252 if (footer instanceof T.DialogButtonBox) {
253 return footer.standardButton(button);
254 } else if (footer === footerToolBar) {
255 return dialogButtonBox.standardButton(button);
256 } else {
257 return null;
258 }
259 }
260
261 function customFooterButton(action: T.Action): T.AbstractButton {
262 if (!action) {
263 // Even if there's a null object in the list of actions, we should
264 // not return a button for it.
265 return null;
266 }
267 const index = __visibleCustomFooterActions.indexOf(action);
268 if (index < 0) {
269 return null;
270 }
271 return customFooterButtons.itemAt(index) as T.AbstractButton;
272 }
273
274 z: Kirigami.OverlayZStacking.z
275
276 // calculate dimensions
277 implicitWidth: contentItem.implicitWidth + leftPadding + rightPadding // maximum width enforced from our content (one source of truth) to avoid binding loops
278 implicitHeight: contentItem.implicitHeight + topPadding + bottomPadding
279 + (implicitHeaderHeight > 0 ? implicitHeaderHeight + spacing : 0)
280 + (implicitFooterHeight > 0 ? implicitFooterHeight + spacing : 0);
281
282 // misc. dialog settings
283 closePolicy: QQC2.Popup.CloseOnEscape | QQC2.Popup.CloseOnReleaseOutside
284 modal: true
285 clip: false
286 padding: 0
287
288 // determine parent so that popup knows which window to popup in
289 // we want to open the dialog in the center of the window, if possible
290 Component.onCompleted: {
291 if (typeof applicationWindow !== "undefined") {
292 parent = applicationWindow().overlay;
293 }
294 }
295
296 // center dialog
297 x: Math.round((parent.width - width) / 2)
298 y: Math.round((parent.height - height) / 2) + Kirigami.Units.gridUnit * 2 * (1 - opacity) // 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: 1
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: (contentChildren.length === 1 && contentChildren[0].implicitWidth > 0
356 ? contentChildren[0].implicitWidth
357 : (contentItem.implicitWidth > 0 ? contentItem.implicitWidth : contentItem.width)) + leftPadding + rightPadding
358 property real calculatedImplicitHeight: (contentChildren.length === 1 && contentChildren[0].implicitHeight > 0
359 ? contentChildren[0].implicitHeight
360 : (contentItem.implicitHeight > 0 ? contentItem.implicitHeight : contentItem.height)) + topPadding + bottomPadding
361
362 onContentItemChanged: {
363 if (!contentItem) {
364 return;
365 }
366 /* Why this is necessary? A Flickable mainItem syncs its size with the contents only on startup,
367 and if the contents can change their size dinamically afterwards (wrapping text does that),
368 the contentsize will be wrong see BUG 477257
369 We also don't do this declaratively but only we are sure a contentItem is declared/created as just
370 accessing the property would create an internal flickable, making it impossible to assign custom
371 flickables/ listviews to the Dialog*/
372 contentItem.contentHeight = Qt.binding(()=>{return contentControl.calculatedImplicitHeight})
373
374 if (contentItem instanceof Flickable) {
375 contentItem.clip = true;
376 }
377 }
378
379 // how do we deal with the scrollbar width?
380 // - case 1: the dialog itself has the preferredWidth set
381 // -> we hint a width to the content so it shrinks to give space to the scrollbar
382 // - case 2: preferredWidth not set, so we are using the content's implicit width
383 // -> we expand the dialog's width to accommodate the scrollbar width (to respect the content's desired width)
384
385 // don't enforce preferred width and height if not set (-1), and expand to a larger implicit size
386 property real preferredWidth: Math.max(root.preferredWidth, calculatedImplicitWidth)
387 property real preferredHeight: Math.max(root.preferredHeight - otherHeights, calculatedImplicitHeight)
388
389 property real maximumWidth: calculatedMaximumWidth
390 property real maximumHeight: calculatedMaximumHeight - otherHeights // we enforce maximum height solely from the content
391
392 implicitWidth: Math.min(preferredWidth, maximumWidth)
393 implicitHeight: Math.min(preferredHeight, maximumHeight)
394
395 // give an implied width and height to the contentItem so that features like word wrapping/eliding work
396 // cannot placed directly in contentControl as a child, so we must use a property
397 property var widthHint: Binding {
398 target: contentControl.contentChildren[0] || null
399 property: "width"
400
401 // we want to avoid horizontal scrolling, so we apply maximumWidth as a hint if necessary
402 property real preferredWidthHint: contentControl.contentItem.width
403 property real maximumWidthHint: contentControl.calculatedMaximumWidth - contentControl.leftPadding - contentControl.rightPadding
404
405 value: Math.min(maximumWidthHint, preferredWidthHint)
406
407 restoreMode: Binding.RestoreBinding
408 }
409 }
410
411 header: T.Control {
412 implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset,
413 implicitContentWidth + leftPadding + rightPadding)
414 implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset,
415 implicitContentHeight + topPadding + bottomPadding)
416
417 padding: Kirigami.Units.largeSpacing
418 bottomPadding: verticalPadding + headerSeparator.implicitHeight // add space for bottom separator
419
420 Kirigami.Theme.colorSet: Kirigami.Theme.Header
421
422 contentItem: RowLayout {
423 spacing: Kirigami.Units.smallSpacing
424
425 Kirigami.Heading {
426 id: heading
427 Layout.fillWidth: true
428 Layout.alignment: Qt.AlignVCenter
429 text: root.title.length === 0 ? " " : root.title // always have text to ensure header height
430 elide: Text.ElideRight
431
432 // use tooltip for long text that is elided
433 QQC2.ToolTip.visible: truncated && titleHoverHandler.hovered
434 QQC2.ToolTip.text: root.title
435 HoverHandler { id: titleHoverHandler }
436 }
437
438 QQC2.ToolButton {
439 id: closeIcon
440
441 // We want to position the close button in the top-right
442 // corner if the header is very tall, but we want to
443 // vertically center it in a short header
444 readonly property bool tallHeader: parent.height > (Kirigami.Units.iconSizes.smallMedium + Kirigami.Units.largeSpacing * 2)
445 Layout.alignment: tallHeader ? Qt.AlignRight | Qt.AlignTop : Qt.AlignRight | Qt.AlignVCenter
446 Layout.topMargin: tallHeader ? Kirigami.Units.largeSpacing : 0
447
448 visible: root.showCloseButton
449 icon.name: closeIcon.hovered ? "window-close" : "window-close-symbolic"
450 text: qsTr("Close", "@action:button close dialog")
451 onClicked: root.reject()
452 display: QQC2.AbstractButton.IconOnly
453 }
454 }
455
456 // header background
457 background: Item {
458 Kirigami.Separator {
459 id: headerSeparator
460 width: parent.width
461 anchors.bottom: parent.bottom
462 visible: contentControl.contentHeight > contentControl.implicitHeight
463 }
464 }
465 }
466
467 // use top level control rather than toolbar, since toolbar causes button rendering glitches
468 footer: T.Control {
469 id: footerToolBar
470
471 // if there is nothing in the footer, still maintain a height so that we can create a rounded bottom buffer for the dialog
472 property bool bufferMode: !root.footerLeadingComponent && !dialogButtonBox.visible
473 implicitHeight: bufferMode ? Math.round(Kirigami.Units.smallSpacing / 2) : contentItem.implicitHeight + topPadding + bottomPadding
474
475 padding: !bufferMode ? Kirigami.Units.largeSpacing : 0
476
477 contentItem: RowLayout {
478 spacing: footerToolBar.spacing
479 // Don't let user interact with footer during transitions
480 enabled: root.opened
481
482 Loader {
483 id: leadingLoader
484 sourceComponent: root.footerLeadingComponent
485 }
486
487 // footer buttons
488 QQC2.DialogButtonBox {
489 // we don't explicitly set padding, to let the style choose the padding
490 id: dialogButtonBox
491 standardButtons: root.standardButtons
492 visible: count > 0
493 padding: 0
494
495 Layout.fillWidth: true
496 Layout.alignment: dialogButtonBox.alignment
497
498 position: QQC2.DialogButtonBox.Footer
499
500 // ensure themes don't add a background, since it can lead to visual inconsistencies
501 // with the rest of the dialog
502 background: null
503
504 // we need to hook all of the buttonbox events to the dialog events
505 onAccepted: root.accept()
506 onRejected: root.reject()
507 onApplied: root.applied()
508 onDiscarded: root.discarded()
509 onHelpRequested: root.helpRequested()
510 onReset: root.reset()
511
512 // add custom footer buttons
513 Repeater {
514 id: customFooterButtons
515 model: root.__visibleCustomFooterActions
516 // we have to use Button instead of ToolButton, because ToolButton has no visual distinction when disabled
517 delegate: QQC2.Button {
518 required property T.Action modelData
519
520 flat: root.flatFooterButtons
521 action: modelData
522 }
523 }
524 }
525
526 Loader {
527 id: trailingLoader
528 sourceComponent: root.footerTrailingComponent
529 }
530 }
531
532 background: Item {
533 Kirigami.Separator {
534 id: footerSeparator
535 visible: contentControl.contentHeight > contentControl.implicitHeight && footerToolBar.padding !== 0
536 width: parent.width
537 anchors.top: parent.top
538 }
539 }
540 }
541}
A visual separator.
Definition Separator.qml:16
KIOCORE_EXPORT QStringList list(const QString &fileClass)
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 Jun 14 2024 11:53:34 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.