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

KDE's Doxygen guidelines are available online.