systemdialog: Update to match plasma-workspace implementation, and simplify UI

SystemDialog was added in 2021 and did not receive any testing while the API was in flux when portals started using it.

Update the implementation to match plasma-workspace now that the API has settled.

Also simplify the UI, remove automatic scrollview and other loaders that make initialization very complicated.

Fixes https://invent.kde.org/plasma/plasma-mobile/-/issues/301
This commit is contained in:
Devin Lin 2024-03-05 21:51:18 -05:00
parent 98f925b5f9
commit 8f39d156f1
3 changed files with 109 additions and 515 deletions

View file

@ -1,74 +1,35 @@
/*
* SPDX-FileCopyrightText: 2021 Devin Lin <espidev@gmail.com>
*
* SPDX-License-Identifier: LGPL-2.0-or-later
*/
// SPDX-FileCopyrightText: 2021-2024 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Templates as T
import QtQuick.Layouts 1.15
import QtQuick.Window 2.15
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import QtQuick.Window
import Qt5Compat.GraphicalEffects
import org.kde.kirigami 2.18 as Kirigami
import "private" as Private
import QtQuick.Templates as T
import org.kde.kirigami as Kirigami
Item {
id: root
// -- public API: should match plasma-workspace implementation --
default property Item mainItem
/**
* Title of the dialog.
*/
property string mainText: ""
/**
* Subtitle of the dialog.
*/
property string subtitle: ""
/**
* This property holds the default padding of the content.
*/
property real padding: Kirigami.Units.smallSpacing
/**
* This property holds the left padding of the content. If not specified, it uses `padding`.
*/
property real leftPadding: padding
/**
* This property holds the right padding of the content. If not specified, it uses `padding`.
*/
property real rightPadding: padding
/**
* This property holds the top padding of the content. If not specified, it uses `padding`.
*/
property real topPadding: padding
/**
* This property holds the bottom padding of the content. If not specified, it uses `padding`.
*/
property real bottomPadding: padding
property alias standardButtons: footerButtonBox.standardButtons
readonly property int flags: Qt.FramelessWindowHint | Qt.Dialog
readonly property real dialogCornerRadius: Kirigami.Units.smallSpacing * 2
property list<T.Action> actions
property string iconName
property list<T.Action> actions
readonly property alias dialogButtonBox: footerButtonBox
implicitWidth: loader.implicitWidth
implicitHeight: loader.implicitHeight
required property Window window
readonly property real minimumHeight: implicitWidth
readonly property real minimumWidth: implicitHeight
required property Kirigami.AbstractApplicationWindow window
readonly property int flags: Qt.FramelessWindowHint | Qt.Dialog
property var standardButtons // footerButtonBox standardButtons
readonly property int spacing: Kirigami.Units.gridUnit
function present() {
window.showFullScreen()
window.showMaximized();
}
onWindowChanged: {
@ -77,250 +38,128 @@ Item {
}
}
// load in async to speed up load times (especially on embedded devices)
Loader {
id: loader
Item {
id: windowItem
anchors.centerIn: parent
asynchronous: true
// margins for shadow
implicitWidth: Math.min(Screen.width, control.implicitWidth + 2 * Kirigami.Units.gridUnit)
implicitHeight: Math.min(Screen.height, control.implicitHeight + 2 * Kirigami.Units.gridUnit)
sourceComponent: Item {
// margins for shadow
implicitWidth: Math.min(Screen.width, control.implicitWidth + 2 * Kirigami.Units.gridUnit)
implicitHeight: Math.min(Screen.height, control.implicitHeight + 2 * Kirigami.Units.gridUnit)
// shadow
RectangularGlow {
id: glow
anchors.topMargin: 1
anchors.fill: control
cached: true
glowRadius: 2
cornerRadius: Kirigami.Units.gridUnit
spread: 0.1
color: Qt.rgba(0, 0, 0, 0.4)
}
// actual window
Control {
id: control
anchors.fill: parent
anchors.margins: glow.cornerRadius
topPadding: 0
bottomPadding: 0
rightPadding: 0
leftPadding: 0
background: Item {
Rectangle { // border
anchors.fill: parent
anchors.margins: -1
radius: dialogCornerRadius + 1
color: Qt.darker(Kirigami.Theme.backgroundColor, 1.5)
}
Rectangle { // background colour
anchors.fill: parent
radius: dialogCornerRadius
color: Kirigami.Theme.backgroundColor
}
}
contentItem: column
}
// shadow
RectangularGlow {
id: glow
anchors.topMargin: 1
anchors.fill: control
cached: true
glowRadius: 2
cornerRadius: Kirigami.Units.gridUnit
spread: 0.1
color: Qt.rgba(0, 0, 0, 0.4)
}
}
readonly property var contents: ColumnLayout {
id: column
spacing: 0
// actual window
QQC2.Control {
id: control
anchors.fill: parent
anchors.margins: glow.cornerRadius
topPadding: root.spacing
bottomPadding: root.spacing
rightPadding: root.spacing
leftPadding: root.spacing
// header
Control {
id: headerControl
implicitWidth: Kirigami.Units.gridUnit * 22
Layout.fillWidth: true
Layout.maximumWidth: root.window.maximumWidth
background: Item {
Rectangle { // border
anchors.fill: parent
anchors.margins: -1
radius: Kirigami.Units.largeSpacing + 1
color: Qt.darker(Kirigami.Theme.backgroundColor, 1.5)
}
Rectangle { // background colour
anchors.fill: parent
radius: Kirigami.Units.largeSpacing
color: Kirigami.Theme.backgroundColor
}
}
topPadding: 0
leftPadding: 0
rightPadding: 0
bottomPadding: 0
contentItem: ColumnLayout {
id: column
spacing: 0
background: Item {}
contentItem: RowLayout {
// header
Kirigami.Heading {
Layout.fillWidth: true
Layout.topMargin: Kirigami.Units.largeSpacing
Layout.bottomMargin: Kirigami.Units.largeSpacing
Layout.leftMargin: Kirigami.Units.largeSpacing
Layout.rightMargin: Kirigami.Units.largeSpacing
Layout.alignment: Qt.AlignVCenter
level: 2
Layout.maximumWidth: root.window.maximumWidth
level: 3
font.weight: Font.Bold
text: root.mainText
wrapMode: Text.Wrap
elide: Text.ElideRight
horizontalAlignment: Text.AlignHCenter
}
}
}
// content
Control {
id: content
Layout.fillHeight: true
Layout.fillWidth: true
Layout.maximumWidth: root.window.maximumWidth
leftPadding: 0
rightPadding: 0
topPadding: 0
bottomPadding: 0
background: Item {}
contentItem: ColumnLayout {
spacing: 0
clip: true
Label {
id: subtitleLabel
Layout.fillWidth: true
QQC2.Label {
Layout.topMargin: Kirigami.Units.largeSpacing
Layout.bottomMargin: Kirigami.Units.largeSpacing
Layout.leftMargin: Kirigami.Units.gridUnit * 3
Layout.rightMargin: Kirigami.Units.gridUnit * 3
visible: root.subtitle !== ""
horizontalAlignment: Text.AlignHCenter
Layout.maximumWidth: root.window.maximumWidth
Layout.fillWidth: true
text: root.subtitle
wrapMode: Label.Wrap
visible: text.length > 0
wrapMode: Text.Wrap
horizontalAlignment: Text.AlignHCenter
}
// separator when scrolling
Kirigami.Separator {
Layout.fillWidth: true
opacity: root.mainItem && contentControl.flickableItem && contentControl.flickableItem.contentY !== 0 ? 1 : 0 // always maintain same height (as opposed to visible)
Kirigami.Icon {
Layout.topMargin: Kirigami.Units.largeSpacing
Layout.alignment: Qt.AlignCenter
source: root.iconName
implicitWidth: Kirigami.Units.iconSizes.large
implicitHeight: Kirigami.Units.iconSizes.large
}
// mainItem is in scrollview, in case of overflow
Private.ScrollView {
id: contentControl
clip: true
// content
QQC2.Control {
id: content
// we cannot have contentItem inside a sub control (allowing for content padding within the scroll area),
// because if the contentItem is a Flickable (ex. ListView), the ScrollView needs it to be top level in order
// to decorate it
contentItem: root.mainItem
canFlickWithMouse: true
// ensure window colour scheme, and background color
Kirigami.Theme.inherit: false
Kirigami.Theme.colorSet: Kirigami.Theme.Window
// needs to explicitly be set for each side to work
leftPadding: root.leftPadding; topPadding: root.topPadding
rightPadding: root.rightPadding; bottomPadding: root.bottomPadding
// height of everything else in the dialog other than the content
property real otherHeights: headerControl.height + subtitleLabel.height + footerButtonBox.height + root.topPadding + root.bottomPadding;
property real calculatedMaximumWidth: root.window.maximumWidth > root.absoluteMaximumWidth ? root.absoluteMaximumWidth : root.window.maximumWidth
property real calculatedMaximumHeight: root.window.maximumHeight > root.absoluteMaximumHeight ? root.absoluteMaximumHeight : root.window.maximumHeight
property real calculatedImplicitWidth: root.mainItem ? (root.mainItem.implicitWidth ? root.mainItem.implicitWidth : root.mainItem.width) + root.leftPadding + root.rightPadding : 0
property real calculatedImplicitHeight: root.mainItem ? (root.mainItem.implicitHeight ? root.mainItem.implicitHeight : root.mainItem.height) + root.topPadding + root.bottomPadding : 0
// don't enforce preferred width and height if not set
Layout.preferredWidth: root.preferredWidth >= 0 ? root.preferredWidth : calculatedImplicitWidth + contentControl.rightSpacing
Layout.preferredHeight: root.preferredHeight >= 0 ? root.preferredHeight - otherHeights : calculatedImplicitHeight + contentControl.bottomSpacing
Layout.fillWidth: true
Layout.fillHeight: true
Layout.maximumWidth: calculatedMaximumWidth
Layout.maximumHeight: calculatedMaximumHeight >= otherHeights ? calculatedMaximumHeight - otherHeights : 0 // we enforce maximum height solely from the content
Layout.fillWidth: true
Layout.topMargin: Kirigami.Units.gridUnit
Layout.maximumWidth: root.window.maximumWidth
// give an implied width and height to the contentItem so that features like word wrapping/eliding work
// cannot placed directly in contentControl as a child, so we must use a property
property var widthHint: Binding {
target: root.mainItem
property: "width"
// we want to avoid horizontal scrolling, so we apply maximumWidth as a hint if necessary
property real preferredWidthHint: contentControl.width - root.leftPadding - root.rightPadding - contentControl.rightSpacing
property real maximumWidthHint: contentControl.calculatedMaximumWidth - root.leftPadding - root.rightPadding - contentControl.rightSpacing
value: maximumWidthHint < preferredWidthHint ? maximumWidthHint : preferredWidthHint
}
property var heightHint: Binding {
target: root.mainItem
property: "height"
// we are okay with overflow, if it exceeds maximumHeight we will allow scrolling
value: contentControl.Layout.preferredHeight - root.topPadding - root.bottomPadding - contentControl.bottomSpacing
}
leftPadding: 0
rightPadding: 0
topPadding: 0
bottomPadding: 0
// give explicit warnings since the maximumHeight is ignored when negative, so developers aren't confused
Component.onCompleted: {
if (contentControl.Layout.maximumHeight < 0 || contentControl.Layout.maximumHeight === Infinity) {
console.log("Dialog Warning: the calculated maximumHeight for the content is " + contentControl.Layout.maximumHeight + ", ignoring...");
contentItem: root.mainItem
background: Item {}
}
QQC2.DialogButtonBox {
id: footerButtonBox
// ensure we never have no buttons, we always must have the cancel button available
standardButtons: (root.standardButtons === QQC2.DialogButtonBox.NoButton) ? QQC2.DialogButtonBox.Cancel : root.standardButtons
Layout.topMargin: Kirigami.Units.largeSpacing
Layout.fillWidth: true
Layout.maximumWidth: root.window.maximumWidth
leftPadding: 0
rightPadding: 0
topPadding: 0
bottomPadding: 0
onAccepted: root.window.accept()
onRejected: root.window.reject()
Repeater {
model: root.actions
delegate: QQC2.Button {
action: modelData
}
}
}
}
}
Control {
Layout.fillWidth: true
leftPadding: 0
rightPadding: 0
topPadding: 0
bottomPadding: 0
contentItem: footerButtonBox
}
}
readonly property DialogButtonBox dialogButtonBox: DialogButtonBox {
//We want to report the same width on all so the button area is split equally
id: footerButtonBox
Layout.fillWidth: true
onAccepted: root.window.accept()
onRejected: root.window.reject()
implicitHeight: contentItem.implicitHeight
alignment: undefined
readonly property real sameWidth: 50
delegate: Private.MobileSystemDialogButton {
Layout.fillWidth: true
Layout.preferredWidth: footerButtonBox.sameWidth
readonly property point globalPos: root.window.visible ? mapToItem(footerButtonBox, Qt.point(x, y)) : Qt.point(0, 0)
verticalSeparator: globalPos.x > 0 && root.window.layout === Qt.Horizontal
horizontalSeparator: true
corners.bottomLeftRadius: verticalSeparator ? 0 : root.dialogCornerRadius
corners.bottomRightRadius: globalPos.x >= footerButtonBox.width ? root.dialogCornerRadius : 0
}
leftPadding: 0
rightPadding: 0
topPadding: 0
bottomPadding: 0
contentItem: GridLayout {
Layout.fillWidth: true
rowSpacing: 0
columnSpacing: 0
rows: root.window.layout === Qt.Vertical ? Number.MAX_VALUE : 1
columns: root.window.layout !== Qt.Vertical ? Number.MAX_VALUE : 1
Repeater {
model: root.actions
delegate: Private.MobileSystemDialogButton {
Layout.fillWidth: true
Layout.preferredWidth: footerButtonBox.sameWidth
readonly property point globalPos: root.window.visible ? mapToItem(footerButtonBox, Qt.point(x, y)) : Qt.point(0, 0)
verticalSeparator: globalPos.x > 0 && root.window.layout === Qt.Horizontal
horizontalSeparator: true
corners.bottomLeftRadius: model.index === root.actions.length - 1 ? root.dialogCornerRadius : 0
corners.bottomRightRadius: model.index === root.actions.length - 1 && footerButtonBox.standardButtons === 0 ? root.dialogCornerRadius : 0
action: modelData
}
}
}
}
}

View file

@ -1,83 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021 Devin Lin <espidev@gmail.com>
*
* SPDX-License-Identifier: LGPL-2.0-or-later
*/
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import QtQuick.Window 2.2
import Qt5Compat.GraphicalEffects
import org.kde.kirigami 2.18 as Kirigami
AbstractButton {
id: root
property alias corners: background.corners
property bool verticalSeparator: false
property bool horizontalSeparator: false
background: Kirigami.ShadowedRectangle {
id: background
Kirigami.Theme.colorSet: Kirigami.Theme.Button
Kirigami.Theme.inherit: false
color: {
if (root.down) {
let avg = (Kirigami.Theme.backgroundColor.r + Kirigami.Theme.backgroundColor.g + Kirigami.Theme.backgroundColor.b) / 3;
// sample down
avg = Math.max(0, (avg - 0.8) * 5);
return Qt.darker(Kirigami.Theme.backgroundColor, 1.1 + 0.4 * (1 - avg));
} else if (hoverHandler.hovered) {
return Qt.darker(Kirigami.Theme.backgroundColor, 1.05)
} else {
return Kirigami.Theme.backgroundColor
}
}
Kirigami.Separator {
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
visible: root.verticalSeparator
}
Kirigami.Separator {
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
visible: root.horizontalSeparator
}
}
leftPadding: Kirigami.Units.largeSpacing
rightPadding: Kirigami.Units.largeSpacing
topPadding: Kirigami.Units.largeSpacing
bottomPadding: Kirigami.Units.largeSpacing
contentItem: Item {
implicitHeight: row.height + Kirigami.Units.smallSpacing
implicitWidth: row.implicitWidth
RowLayout {
id: row
anchors.centerIn: parent
spacing: 0
Kirigami.Icon {
Layout.preferredHeight: label.height
Layout.preferredWidth: height
Layout.rightMargin: Kirigami.Units.smallSpacing
visible: root.icon.name
source: root.icon.name
}
Label {
id: label
text: root.text
}
}
HoverHandler {
id: hoverHandler
}
}
}

View file

@ -1,162 +0,0 @@
/*
* SPDX-FileCopyrightText: 2016 Marco Martin <mart@kde.org>
*
* SPDX-License-Identifier: LGPL-2.0-or-later
*/
import QtQuick 2.7
import QtQuick.Controls 2.0
import QtQml 2.15
import org.kde.kirigami 2.9 as Kirigami
// taken from Kirigami
MouseArea {
id: root
default property Item contentItem
property Flickable flickableItem
clip: true
property int horizontalScrollBarPolicy: Qt.ScrollBarAlwaysOff
property int verticalScrollBarPolicy: Qt.ScrollBarAsNeeded
property int topPadding: 0
property int leftPadding: 0
property int bottomPadding: 0
property int rightPadding: 0
property bool canFlickWithMouse: false
// Note: These are used because RefreshableScrollView overrides right and
// bottom padding properties.
property int rightSpacing: !Kirigami.Settings.hasTransientTouchInput && flickableItem && flickableItem.ScrollBar.vertical && flickableItem.ScrollBar.vertical.visible ? flickableItem.ScrollBar.vertical.width : 0
property int bottomSpacing: !Kirigami.Settings.hasTransientTouchInput && flickableItem && flickableItem.ScrollBar.horizontal && flickableItem.ScrollBar.horizontal.visible ? flickableItem.ScrollBar.horizontal.height : 0
Accessible.onScrollDownAction: flickableItem.Accessible.onScrollDownAction
Accessible.onScrollUpAction: flickableItem.Accessible.onScrollUpAction
Keys.onUpPressed: scroll(Kirigami.Units.gridUnit * 2)
Keys.onDownPressed: scroll(-Kirigami.Units.gridUnit * 2)
function scroll(y) {
// Don't scroll if the view isn't scrollable!
if (flickableItem.contentHeight < flickableItem.height + flickableItem.contentY) {
return;
}
const minYExtent = flickableItem.topMargin - flickableItem.originY;
const maxYExtent = flickableItem.height - (flickableItem.contentHeight + flickableItem.bottomMargin + flickableItem.originY);
flickableItem.contentY = Math.min(-maxYExtent, Math.max(-minYExtent, flickableItem.contentY - y));
}
focus: true
onPressed: mouse => { mouse.accepted = mouse.source !== Qt.MouseEventNotSynthesized; }
onVerticalScrollBarPolicyChanged: {
scrollBarCreationTimer.restart();
}
onHorizontalScrollBarPolicyChanged: {
scrollBarCreationTimer.restart();
}
onContentItemChanged: {
if (contentItem.hasOwnProperty("contentY")) {
flickableItem = contentItem;
if (typeof(flickableItem.keyNavigationEnabled) != "undefined") {
flickableItem.keyNavigationEnabled = true;
flickableItem.keyNavigationWraps = false;
}
contentItem.parent = flickableParent;
} else {
flickableItem = flickableComponent.createObject(flickableParent);
contentItem.parent = flickableItem.contentItem;
}
flickableItem.anchors.fill = flickableParent;
scrollBarCreationTimer.restart();
}
Binding {
when: !root.canFlickWithMouse
target: root.flickableItem
property: "interactive"
value: Kirigami.Settings.hasTransientTouchInput
restoreMode: Binding.RestoreBinding
}
Timer {
id: scrollBarCreationTimer
interval: 0
onTriggered: {
//create or destroy the vertical scrollbar
if ((!flickableItem.ScrollBar.vertical) &&
verticalScrollBarPolicy != Qt.ScrollBarAlwaysOff) {
flickableItem.ScrollBar.vertical = verticalScrollComponent.createObject(root);
} else if (flickableItem.ScrollBar.vertical &&
verticalScrollBarPolicy == Qt.ScrollBarAlwaysOff) {
flickableItem.ScrollBar.vertical.destroy();
}
//create or destroy the horizontal scrollbar
if ((!flickableItem.ScrollBar.horizontal) &&
horizontalScrollBarPolicy != Qt.ScrollBarAlwaysOff) {
flickableItem.ScrollBar.horizontal = horizontalScrollComponent.createObject(root);
} else if (flickableItem.ScrollBar.horizontal &&
horizontalScrollBarPolicy == Qt.ScrollBarAlwaysOff) {
flickableItem.ScrollBar.horizontal.destroy();
}
}
}
Kirigami.WheelHandler {
id: wheelHandler
target: root.flickableItem
}
Item {
id: flickableParent
clip: true
anchors {
fill: parent
leftMargin: root.leftPadding
topMargin: root.topPadding
rightMargin: root.rightPadding + root.rightSpacing
bottomMargin: root.bottomPadding + root.bottomSpacing
}
}
Component {
id: flickableComponent
Flickable {
anchors {
fill: parent
}
contentWidth: root.contentItem ? root.contentItem.width : 0
contentHeight: root.contentItem ? root.contentItem.height : 0
}
}
Component {
id: verticalScrollComponent
ScrollBar {
z: flickableParent.z + 1
visible: root.verticalScrollBarPolicy != Qt.ScrollBarAlwaysOff && root.contentItem.visible && size < 1
interactive: !Kirigami.Settings.hasTransientTouchInput
anchors {
right: parent.right
top: parent.top
bottom: parent.bottom
bottomMargin: root.bottomSpacing
}
}
}
Component {
id: horizontalScrollComponent
ScrollBar {
z: flickableParent.z + 1
visible: root.horizontalScrollBarPolicy != Qt.ScrollBarAlwaysOff && root.contentItem.visible && size < 1
interactive: !Kirigami.Settings.hasTransientTouchInput
anchors {
left: parent.left
right: parent.right
bottom: parent.bottom
rightMargin: root.rightSpacing
}
}
}
}