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-2024 Devin Lin <devin@kde.org>
* SPDX-FileCopyrightText: 2021 Devin Lin <espidev@gmail.com> // SPDX-License-Identifier: LGPL-2.0-or-later
*
* SPDX-License-Identifier: LGPL-2.0-or-later
*/
import QtQuick 2.15 import QtQuick
import QtQuick.Controls 2.15 import QtQuick.Controls as QQC2
import QtQuick.Templates as T import QtQuick.Layouts
import QtQuick.Layouts 1.15 import QtQuick.Window
import QtQuick.Window 2.15
import Qt5Compat.GraphicalEffects import Qt5Compat.GraphicalEffects
import org.kde.kirigami 2.18 as Kirigami import QtQuick.Templates as T
import "private" as Private import org.kde.kirigami as Kirigami
Item { Item {
id: root id: root
// -- public API: should match plasma-workspace implementation --
default property Item mainItem default property Item mainItem
/**
* Title of the dialog.
*/
property string mainText: "" property string mainText: ""
/**
* Subtitle of the dialog.
*/
property string subtitle: "" 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 string iconName
property list<T.Action> actions
readonly property alias dialogButtonBox: footerButtonBox
implicitWidth: loader.implicitWidth required property Window window
implicitHeight: loader.implicitHeight
readonly property real minimumHeight: implicitWidth readonly property real minimumHeight: implicitWidth
readonly property real minimumWidth: implicitHeight readonly property real minimumWidth: implicitHeight
readonly property int flags: Qt.FramelessWindowHint | Qt.Dialog
required property Kirigami.AbstractApplicationWindow window property var standardButtons // footerButtonBox standardButtons
readonly property int spacing: Kirigami.Units.gridUnit
function present() { function present() {
window.showFullScreen() window.showMaximized();
} }
onWindowChanged: { onWindowChanged: {
@ -77,250 +38,128 @@ Item {
} }
} }
// load in async to speed up load times (especially on embedded devices) Item {
Loader { id: windowItem
id: loader
anchors.centerIn: parent 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 { // shadow
// margins for shadow RectangularGlow {
implicitWidth: Math.min(Screen.width, control.implicitWidth + 2 * Kirigami.Units.gridUnit) id: glow
implicitHeight: Math.min(Screen.height, control.implicitHeight + 2 * Kirigami.Units.gridUnit) anchors.topMargin: 1
anchors.fill: control
// shadow cached: true
RectangularGlow { glowRadius: 2
id: glow cornerRadius: Kirigami.Units.gridUnit
anchors.topMargin: 1 spread: 0.1
anchors.fill: control color: Qt.rgba(0, 0, 0, 0.4)
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
}
} }
}
readonly property var contents: ColumnLayout { // actual window
id: column QQC2.Control {
spacing: 0 id: control
anchors.fill: parent
anchors.margins: glow.cornerRadius
topPadding: root.spacing
bottomPadding: root.spacing
rightPadding: root.spacing
leftPadding: root.spacing
// header implicitWidth: Kirigami.Units.gridUnit * 22
Control {
id: headerControl
Layout.fillWidth: true background: Item {
Layout.maximumWidth: root.window.maximumWidth 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 contentItem: ColumnLayout {
leftPadding: 0 id: column
rightPadding: 0 spacing: 0
bottomPadding: 0
background: Item {} // header
contentItem: RowLayout {
Kirigami.Heading { Kirigami.Heading {
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: Kirigami.Units.largeSpacing Layout.maximumWidth: root.window.maximumWidth
Layout.bottomMargin: Kirigami.Units.largeSpacing level: 3
Layout.leftMargin: Kirigami.Units.largeSpacing font.weight: Font.Bold
Layout.rightMargin: Kirigami.Units.largeSpacing
Layout.alignment: Qt.AlignVCenter
level: 2
text: root.mainText text: root.mainText
wrapMode: Text.Wrap wrapMode: Text.Wrap
elide: Text.ElideRight elide: Text.ElideRight
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
} }
}
}
// content QQC2.Label {
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
Layout.topMargin: Kirigami.Units.largeSpacing Layout.topMargin: Kirigami.Units.largeSpacing
Layout.bottomMargin: Kirigami.Units.largeSpacing Layout.bottomMargin: Kirigami.Units.largeSpacing
Layout.leftMargin: Kirigami.Units.gridUnit * 3 Layout.maximumWidth: root.window.maximumWidth
Layout.rightMargin: Kirigami.Units.gridUnit * 3 Layout.fillWidth: true
visible: root.subtitle !== ""
horizontalAlignment: Text.AlignHCenter
text: root.subtitle text: root.subtitle
wrapMode: Label.Wrap visible: text.length > 0
wrapMode: Text.Wrap
horizontalAlignment: Text.AlignHCenter
} }
// separator when scrolling Kirigami.Icon {
Kirigami.Separator { Layout.topMargin: Kirigami.Units.largeSpacing
Layout.fillWidth: true Layout.alignment: Qt.AlignCenter
opacity: root.mainItem && contentControl.flickableItem && contentControl.flickableItem.contentY !== 0 ? 1 : 0 // always maintain same height (as opposed to visible) source: root.iconName
implicitWidth: Kirigami.Units.iconSizes.large
implicitHeight: Kirigami.Units.iconSizes.large
} }
// mainItem is in scrollview, in case of overflow // content
Private.ScrollView { QQC2.Control {
id: contentControl id: content
clip: true
// 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.fillHeight: true
Layout.maximumWidth: calculatedMaximumWidth Layout.fillWidth: true
Layout.maximumHeight: calculatedMaximumHeight >= otherHeights ? calculatedMaximumHeight - otherHeights : 0 // we enforce maximum height solely from the content 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 leftPadding: 0
// cannot placed directly in contentControl as a child, so we must use a property rightPadding: 0
property var widthHint: Binding { topPadding: 0
target: root.mainItem bottomPadding: 0
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
}
// give explicit warnings since the maximumHeight is ignored when negative, so developers aren't confused contentItem: root.mainItem
Component.onCompleted: { background: Item {}
if (contentControl.Layout.maximumHeight < 0 || contentControl.Layout.maximumHeight === Infinity) { }
console.log("Dialog Warning: the calculated maximumHeight for the content is " + contentControl.Layout.maximumHeight + ", ignoring...");
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
}
}
}
}