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
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.
139 * This is the window height, subtracted by largeSpacing on both the top
140 * and bottom.
141 */
142 readonly property real absoluteMaximumHeight: parent ? (parent.height - Kirigami.Units.largeSpacing * 2) : Infinity
143
144 /**
145 * @brief This property holds the absolute maximum width the dialog can have.
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 ? (parent.width - Kirigami.Units.largeSpacing * 2) : Infinity
151
152 readonly property real __borderWidth: 1
153
154 /**
155 * @brief This property holds the maximum height the dialog can have
156 * (including the header and footer).
158 * The height restriction is solely enforced on the content, so if the
159 * maximum height given is not larger than the height of the header and
160 * footer, it will be ignored.
161 *
162 * By default, this is `absoluteMaximumHeight`.
163 */
164 property real maximumHeight: absoluteMaximumHeight
165
166 /**
167 * @brief This property holds the maximum width the dialog can have.
168 *
169 * By default, this is `absoluteMaximumWidth`.
170 */
171 property real maximumWidth: absoluteMaximumWidth
172
173 /**
174 * @brief This property holds the preferred height of the dialog.
175 *
176 * The content will receive a hint for how tall it should be to have
177 * the dialog to be this height.
178 *
179 * If the content, header or footer require more space, then the height
180 * of the dialog will expand to the necessary amount of space.
181 */
182 property real preferredHeight: -1
184 /**
185 * @brief This property holds the preferred width of the dialog.
186 *
187 * The content will receive a hint for how wide it should be to have
188 * the dialog be this wide.
189 *
190 * If the content, header or footer require more space, then the width
191 * of the dialog will expand to the necessary amount of space.
192 */
193 property real preferredWidth: -1
194
196 /**
197 * @brief This property holds the component to the left of the footer buttons.
198 */
199 property Component footerLeadingComponent
200
201 /**
202 * @brief his property holds the component to the right of the footer buttons.
203 */
204 property Component footerTrailingComponent
205
206 /**
207 * @brief This property sets whether to show the close button in the header.
208 */
209 property bool showCloseButton: true
210
211 /**
212 * @brief This property sets whether the footer button style should be flat.
213 */
214 property bool flatFooterButtons: false
215
216 /**
217 * @brief This property holds the custom actions displayed in the footer.
218 *
219 * Example usage:
220 * @code{.qml}
221 * import QtQuick
222 * import org.kde.kirigami as Kirigami
223 *
224 * Kirigami.PromptDialog {
225 * id: dialog
226 * title: i18n("Confirm Playback")
227 * subtitle: i18n("Are you sure you want to play this song? It's really loud!")
228 *
229 * standardButtons: Kirigami.Dialog.Cancel
230 * customFooterActions: [
231 * Kirigami.Action {
232 * text: i18n("Play")
233 * icon.name: "media-playback-start"
234 * onTriggered: {
235 * //...
236 * dialog.close();
237 * }
238 * }
239 * ]
240 * }
241 * @endcode
242 *
243 * @see org::kde::kirigami::Action
244 */
245 property list<T.Action> customFooterActions
246
247 // DialogButtonBox should NOT contain invisible buttons, because in Qt 6
248 // ListView preserves space even for invisible items.
249 readonly property list<T.Action> __visibleCustomFooterActions: customFooterActions
250 .filter(action => !(action instanceof Kirigami.Action) || action?.visible)
251
252 function standardButton(button): T.AbstractButton {
253 // in case a footer is redefined
254 if (footer instanceof T.DialogButtonBox) {
255 return footer.standardButton(button);
256 } else if (footer === footerToolBar) {
257 return dialogButtonBox.standardButton(button);
258 } else {
259 return null;
260 }
261 }
262
263 function customFooterButton(action: T.Action): T.AbstractButton {
264 if (!action) {
265 // Even if there's a null object in the list of actions, we should
266 // not return a button for it.
267 return null;
268 }
269 const index = __visibleCustomFooterActions.indexOf(action);
270 if (index < 0) {
271 return null;
272 }
273 return customFooterButtons.itemAt(index) as T.AbstractButton;
274 }
275
276 z: Kirigami.OverlayZStacking.z
277
278 // calculate dimensions and in case footer is wider than content, use that
279 implicitWidth: Math.max(contentItem.implicitWidth, footerToolBar.implicitWidth, heading.implicitWidth) + leftPadding + rightPadding // maximum width enforced from our content (one source of truth) to avoid binding loops
280 implicitHeight: contentItem.implicitHeight + topPadding + bottomPadding
281 + (implicitHeaderHeight > 0 ? implicitHeaderHeight + spacing : 0)
282 + (implicitFooterHeight > 0 ? implicitFooterHeight + spacing : 0);
283
284 // misc. dialog settings
285 closePolicy: QQC2.Popup.CloseOnEscape | QQC2.Popup.CloseOnReleaseOutside
286 modal: true
287 clip: false
288 padding: 0
289 horizontalPadding: __borderWidth + padding
290
291 // determine parent so that popup knows which window to popup in
292 // we want to open the dialog in the center of the window, if possible
293 Component.onCompleted: {
294 if (typeof applicationWindow !== "undefined") {
295 parent = applicationWindow().overlay;
296 }
297 }
298
299 // center dialog
300 x: parent ? Math.round((parent.width - width) / 2) : 0
301 y: parent ? Math.round((parent.height - height) / 2) + Kirigami.Units.gridUnit * 2 * (1 - opacity) : 0 // move animation
302
303 // dialog enter and exit transitions
304 enter: Transition {
305 NumberAnimation { property: "opacity"; from: 0; to: 1; easing.type: Easing.InOutQuad; duration: Kirigami.Units.longDuration }
306 }
307 exit: Transition {
308 NumberAnimation { property: "opacity"; from: 1; to: 0; easing.type: Easing.InOutQuad; duration: Kirigami.Units.longDuration }
309 }
310
311 // black background, fades in and out
312 QQC2.Overlay.modal: Rectangle {
313 color: Qt.rgba(0, 0, 0, 0.3)
314
315 // the opacity of the item is changed internally by QQuickPopup on open/close
316 Behavior on opacity {
317 OpacityAnimator {
318 duration: Kirigami.Units.longDuration
319 easing.type: Easing.InOutQuad
320 }
321 }
322 }
323
324 // dialog view background
325 background: Kirigami.ShadowedRectangle {
326 id: rect
327 Kirigami.Theme.colorSet: Kirigami.Theme.View
328 Kirigami.Theme.inherit: false
329 color: Kirigami.Theme.backgroundColor
330 radius: Kirigami.Units.cornerRadius
331 shadow {
332 size: radius * 2
333 color: Qt.rgba(0, 0, 0, 0.3)
334 yOffset: 1
335 }
336
337 border {
338 width: root.__borderWidth
339 color: Kirigami.ColorUtils.linearInterpolation(Kirigami.Theme.backgroundColor, Kirigami.Theme.textColor, Kirigami.Theme.frameContrast);
340 }
341 }
342
343 // dialog content
344 contentItem: QQC2.ScrollView {
345 id: contentControl
346
347 // ensure view colour scheme, and background color
348 Kirigami.Theme.inherit: false
349 Kirigami.Theme.colorSet: Kirigami.Theme.View
350
351 QQC2.ScrollBar.horizontal.policy: QQC2.ScrollBar.AlwaysOff
352
353 // height of everything else in the dialog other than the content
354 property real otherHeights: (root.header?.height ?? 0) + (root.footer?.height ?? 0) + root.topPadding + root.bottomPadding;
355
356 property real calculatedMaximumWidth: Math.min(root.absoluteMaximumWidth, root.maximumWidth) - root.leftPadding - root.rightPadding
357 property real calculatedMaximumHeight: Math.min(root.absoluteMaximumHeight, root.maximumHeight) - root.topPadding - root.bottomPadding
358 property real calculatedImplicitWidth: (contentChildren.length === 1 && contentChildren[0].implicitWidth > 0 ? contentChildren[0].implicitWidth : contentItem.implicitWidth) + leftPadding + rightPadding
359 property real calculatedImplicitHeight: (contentChildren.length === 1 && contentChildren[0].implicitHeight > 0? contentChildren[0].implicitHeight: contentItem.implicitHeight) + topPadding + bottomPadding
360
361 onContentItemChanged: {
362 const contentFlickable = contentItem as Flickable;
363 if (contentFlickable) {
364 /*
365 Why this is necessary? A Flickable mainItem syncs its size with the contents only on startup,
366 and if the contents can change their size dinamically afterwards (wrapping text does that),
367 the contentsize will be wrong see BUG 477257.
368
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 */
373 contentFlickable.contentHeight = Qt.binding(() => calculatedImplicitHeight);
374
375 contentFlickable.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 value: contentControl.width + contentControl.leftPadding + contentControl.rightPadding
401 restoreMode: Binding.RestoreBinding
402 }
403 }
404
405 header: T.Control {
406 implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset,
407 implicitContentWidth + leftPadding + rightPadding)
408 implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset,
409 implicitContentHeight + topPadding + bottomPadding)
410
411 padding: Kirigami.Units.largeSpacing
412 bottomPadding: verticalPadding + headerSeparator.implicitHeight // add space for bottom separator
413
414 contentItem: RowLayout {
415 spacing: Kirigami.Units.smallSpacing
416
417 Kirigami.Heading {
418 id: heading
419 Layout.fillWidth: true
420 Layout.alignment: Qt.AlignVCenter
421 text: root.title.length === 0 ? " " : root.title // always have text to ensure header height
422 elide: Text.ElideRight
423
424 // use tooltip for long text that is elided
425 QQC2.ToolTip.visible: truncated && titleHoverHandler.hovered
426 QQC2.ToolTip.text: root.title
427 HoverHandler { id: titleHoverHandler }
428 }
429
430 QQC2.ToolButton {
431 id: closeIcon
432
433 // We want to position the close button in the top-right
434 // corner if the header is very tall, but we want to
435 // vertically center it in a short header
436 readonly property bool tallHeader: parent.height > (Kirigami.Units.iconSizes.smallMedium + Kirigami.Units.largeSpacing * 2)
437 Layout.alignment: tallHeader ? Qt.AlignRight | Qt.AlignTop : Qt.AlignRight | Qt.AlignVCenter
438 Layout.topMargin: tallHeader ? Kirigami.Units.largeSpacing : 0
439
440 visible: root.showCloseButton
441 icon.name: closeIcon.hovered ? "window-close" : "window-close-symbolic"
442 text: qsTr("Close", "@action:button close dialog")
443 onClicked: root.reject()
444 display: QQC2.AbstractButton.IconOnly
445 }
446 }
447
448 // header background
449 background: Item {
450 Kirigami.Separator {
451 id: headerSeparator
452 width: parent.width
453 anchors.bottom: parent.bottom
454 visible: contentControl.contentHeight > contentControl.implicitHeight
455 }
456 }
457 }
458
459 // use top level control rather than toolbar, since toolbar causes button rendering glitches
460 footer: T.Control {
461 id: footerToolBar
462
463 // if there is nothing in the footer, still maintain a height so that we can create a rounded bottom buffer for the dialog
464 property bool bufferMode: !root.footerLeadingComponent && !dialogButtonBox.visible
465 implicitHeight: bufferMode ? Math.round(Kirigami.Units.smallSpacing / 2) : contentItem.implicitHeight + topPadding + bottomPadding
466 implicitWidth: dialogButtonBox.implicitWidth + leftPadding + rightPadding
467
468 padding: !bufferMode ? Kirigami.Units.largeSpacing : 0
469
470 contentItem: RowLayout {
471 spacing: footerToolBar.spacing
472 // Don't let user interact with footer during transitions
473 enabled: root.opened
474
475 Loader {
476 id: leadingLoader
477 sourceComponent: root.footerLeadingComponent
478 }
479
480 // footer buttons
481 QQC2.DialogButtonBox {
482 // we don't explicitly set padding, to let the style choose the padding
483 id: dialogButtonBox
484 standardButtons: root.standardButtons
485 visible: count > 0
486 padding: 0
487
488 Layout.fillWidth: true
489 Layout.alignment: dialogButtonBox.alignment
490
491 position: QQC2.DialogButtonBox.Footer
492
493 // ensure themes don't add a background, since it can lead to visual inconsistencies
494 // with the rest of the dialog
495 background: null
496
497 // we need to hook all of the buttonbox events to the dialog events
498 onAccepted: root.accept()
499 onRejected: root.reject()
500 onApplied: root.applied()
501 onDiscarded: root.discarded()
502 onHelpRequested: root.helpRequested()
503 onReset: root.reset()
504
505 // add custom footer buttons
506 Repeater {
507 id: customFooterButtons
508 model: root.__visibleCustomFooterActions
509 // we have to use Button instead of ToolButton, because ToolButton has no visual distinction when disabled
510 delegate: QQC2.Button {
511 required property T.Action modelData
512
513 flat: root.flatFooterButtons
514 action: modelData
515 }
516 }
517 }
518
519 Loader {
520 id: trailingLoader
521 sourceComponent: root.footerTrailingComponent
522 }
523 }
524
525 background: Item {
526 Kirigami.Separator {
527 id: footerSeparator
528 visible: contentControl.contentHeight > contentControl.implicitHeight && footerToolBar.padding !== 0
529 width: parent.width
530 anchors.top: parent.top
531 }
532 }
533 }
534}
An item that represents an abstract Action.
Definition Action.qml:17
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 Sat Dec 21 2024 16:56:52 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.