Commit 4afadec2 authored by Nicolas Fella's avatar Nicolas Fella Committed by Nicolas Fella
Browse files

Rework the plasmoid configuration dialog

We put a ScrollView/Flickable around the whole content. When we embed a KCM (like we do e.g. in plasma-pa) that comes with its own flickable this creates problems.
This can be seen in the plasma-pa case where it's currently impossible to scroll and the scrollbar placement is wrong.

Instead of having a StackView wrapped by a ScrollView we now have a Loader that can either load a KCM or an applet config (this includes shortcuts and about page).
The applet config is wrapped in a Kirigami.ScrollablePage to allow scrolling if necessary.

BUG: 426998
parent 262ec0eb
/* /*
* Copyright 2013 Marco Martin <mart@kde.org> * Copyright 2013 Marco Martin <mart@kde.org>
* Copyright 2020 Nicolas Fella <nicolas.fella@gmx.de>
* *
* This program is free software; you can redistribute it and/or modify * This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
...@@ -26,8 +27,6 @@ import org.kde.kirigami 2.5 as Kirigami ...@@ -26,8 +27,6 @@ import org.kde.kirigami 2.5 as Kirigami
import org.kde.plasma.core 2.1 as PlasmaCore import org.kde.plasma.core 2.1 as PlasmaCore
import org.kde.plasma.configuration 2.0 import org.kde.plasma.configuration 2.0
//TODO: all of this will be done with desktop components
Rectangle { Rectangle {
id: root id: root
Layout.minimumWidth: PlasmaCore.Units.gridUnit * 30 Layout.minimumWidth: PlasmaCore.Units.gridUnit * 30
...@@ -36,15 +35,12 @@ Rectangle { ...@@ -36,15 +35,12 @@ Rectangle {
LayoutMirroring.enabled: Qt.application.layoutDirection === Qt.RightToLeft LayoutMirroring.enabled: Qt.application.layoutDirection === Qt.RightToLeft
LayoutMirroring.childrenInherit: true LayoutMirroring.childrenInherit: true
//BEGIN properties
color: Kirigami.Theme.backgroundColor color: Kirigami.Theme.backgroundColor
width: PlasmaCore.Units.gridUnit * 40 width: PlasmaCore.Units.gridUnit * 40
height: PlasmaCore.Units.gridUnit * 30 height: PlasmaCore.Units.gridUnit * 30
property bool isContainment: false property bool isContainment: false
//END properties
//BEGIN model
property ConfigModel globalConfigModel: globalAppletConfigModel property ConfigModel globalConfigModel: globalAppletConfigModel
ConfigModel { ConfigModel {
...@@ -62,46 +58,47 @@ Rectangle { ...@@ -62,46 +58,47 @@ Rectangle {
filterRole: "visible" filterRole: "visible"
filterCallback: function(source_row, value) { return value; } filterCallback: function(source_row, value) { return value; }
} }
//END model
//BEGIN functions
function saveConfig() {
if (pageStack.currentItem.saveConfig) {
pageStack.currentItem.saveConfig()
}
for (var key in plasmoid.configuration) {
if (pageStack.currentItem["cfg_"+key] !== undefined) {
plasmoid.configuration[key] = pageStack.currentItem["cfg_"+key]
}
}
}
function settingValueChanged() { function settingValueChanged() {
applyButton.enabled = true; applyButton.enabled = true;
} }
//END functions
function open(item) {
//BEGIN connections if (item.source) {
Component.onCompleted: { if (item.source === "ConfigurationContainmentAppearance.qml") {
if (!isContainment && configDialog.configModel && configDialog.configModel.count > 0) { mainLoader.source = item.source
if (configDialog.configModel.get(0).source) {
pageStack.sourceFile = configDialog.configModel.get(0).source
} else if (configDialog.configModel.get(0).kcm) {
pageStack.sourceFile = Qt.resolvedUrl("ConfigurationKcmPage.qml");
pageStack.currentItem.kcm = configDialog.configModel.get(0).kcm;
} else { } else {
pageStack.sourceFile = ""; mainLoader.setSource(Qt.resolvedUrl("ConfigurationAppletPage.qml"), {configItem: item})
} }
pageStack.title = configDialog.configModel.get(0).name } else if (item.kcm) {
mainLoader.setSource(Qt.resolvedUrl("ConfigurationKcmPage.qml"), {kcm: item.kcm})
} else { } else {
pageStack.sourceFile = globalConfigModel.get(0).source mainLoader.setSource("")
pageStack.title = globalConfigModel.get(0).name }
pageTitle.text = item.name
applyButton.enabled = false
}
Connections {
target: mainLoader.item
function onSettingValueChanged() {
applyButton.enabled = true
}
}
Component.onCompleted: {
// if we are a containment then the first item will be ConfigurationContainmentAppearance
// if the applet does not have own configs then the first item will be Shortcuts
if (isContainment || !configDialog.configModel || configDialog.configModel.count === 0) {
open(root.globalConfigModel.get(0))
} else {
open(configDialog.configModel.get(0))
} }
} }
//END connections
//BEGIN UI components
Rectangle { Rectangle {
id: sidebar id: sidebar
anchors.left: root.left anchors.left: root.left
...@@ -115,6 +112,7 @@ Rectangle { ...@@ -115,6 +112,7 @@ Rectangle {
Kirigami.Separator { Kirigami.Separator {
anchors.left: sidebar.right anchors.left: sidebar.right
height: root.height height: root.height
z: 100
} }
Kirigami.Separator { Kirigami.Separator {
...@@ -126,24 +124,21 @@ Rectangle { ...@@ -126,24 +124,21 @@ Rectangle {
MessageDialog { MessageDialog {
id: messageDialog id: messageDialog
icon: StandardIcon.Warning icon: StandardIcon.Warning
property Item delegate property var item
title: i18nd("plasma_shell_org.kde.plasma.desktop", "Apply Settings") title: i18nd("plasma_shell_org.kde.plasma.desktop", "Apply Settings")
text: i18nd("plasma_shell_org.kde.plasma.desktop", "The settings of the current module have changed. Do you want to apply the changes or discard them?") text: i18nd("plasma_shell_org.kde.plasma.desktop", "The settings of the current module have changed. Do you want to apply the changes or discard them?")
standardButtons: StandardButton.Apply | StandardButton.Discard | StandardButton.Cancel standardButtons: StandardButton.Apply | StandardButton.Discard | StandardButton.Cancel
onApply: { onApply: {
applyAction.trigger() applyAction.trigger()
delegate.openCategory() root.open(item)
} }
onDiscard: { onDiscard: {
delegate.openCategory() root.open(item)
} }
} }
RowLayout { RowLayout {
anchors { anchors.fill: parent
topMargin: topSeparator.height
fill: parent
}
spacing: 0 spacing: 0
QtControls.ScrollView { QtControls.ScrollView {
...@@ -166,7 +161,7 @@ Rectangle { ...@@ -166,7 +161,7 @@ Rectangle {
} }
if (foundPrevious) { if (foundPrevious) {
button.openCategory() categories.openCategory(button.item)
return return
} else if (button.current) { } else if (button.current) {
foundPrevious = true foundPrevious = true
...@@ -180,13 +175,12 @@ Rectangle { ...@@ -180,13 +175,12 @@ Rectangle {
var foundNext = false var foundNext = false
for (var i = 0, length = buttons.length; i < length; ++i) { for (var i = 0, length = buttons.length; i < length; ++i) {
var button = buttons[i]; var button = buttons[i];
console.log(button)
if (!button.hasOwnProperty("current")) { if (!button.hasOwnProperty("current")) {
continue; continue;
} }
if (foundNext) { if (foundNext) {
button.openCategory() categories.openCategory(button.item)
return return
} else if (button.current) { } else if (button.current) {
foundNext = true foundNext = true
...@@ -201,19 +195,47 @@ Rectangle { ...@@ -201,19 +195,47 @@ Rectangle {
property Item currentItem: children[1] property Item currentItem: children[1]
function openCategory(item) {
if (applyButton.enabled) {
messageDialog.item = item;
messageDialog.open();
return;
}
open(item)
}
Component {
id: categoryDelegate
ConfigCategoryDelegate {
onActivated: categories.openCategory(model)
current: {
if (model.kcm && mainLoader.item.kcm) {
return model.kcm == mainLoader.item.kcm
}
if (mainLoader.item.configItem) {
return model.source == mainLoader.item.configItem.source
}
return mainLoader.source == Qt.resolvedUrl(model.source)
}
item: model
}
}
Repeater { Repeater {
model: root.isContainment ? globalConfigModel : undefined model: root.isContainment ? globalConfigModel : undefined
delegate: ConfigCategoryDelegate {} delegate: categoryDelegate
} }
Repeater { Repeater {
model: configDialogFilterModel model: configDialogFilterModel
delegate: ConfigCategoryDelegate {} delegate: categoryDelegate
} }
Repeater { Repeater {
model: !root.isContainment ? globalConfigModel : undefined model: !root.isContainment ? globalConfigModel : undefined
delegate: ConfigCategoryDelegate {} delegate: categoryDelegate
} }
Repeater { Repeater {
model: ConfigModel { model: ConfigModel {
ConfigCategory{ ConfigCategory{
name: i18nd("plasma_shell_org.kde.plasma.desktop", "About") name: i18nd("plasma_shell_org.kde.plasma.desktop", "About")
...@@ -221,7 +243,7 @@ Rectangle { ...@@ -221,7 +243,7 @@ Rectangle {
source: "AboutPlugin.qml" source: "AboutPlugin.qml"
} }
} }
delegate: ConfigCategoryDelegate {} delegate: categoryDelegate
} }
} }
} }
...@@ -232,170 +254,18 @@ Rectangle { ...@@ -232,170 +254,18 @@ Rectangle {
Layout.topMargin: topSeparator.height Layout.topMargin: topSeparator.height
Layout.bottomMargin: PlasmaCore.Units.smallSpacing * 2 Layout.bottomMargin: PlasmaCore.Units.smallSpacing * 2
// Configuration scroll area Kirigami.Heading {
QtControls.ScrollView { id: pageTitle
id: scroll
Layout.fillHeight: true
Layout.fillWidth: true Layout.fillWidth: true
// we want to focus the controls in the settings page right away, don't focus the ScrollView topPadding: Kirigami.Units.smallSpacing
activeFocusOnTab: false leftPadding: Kirigami.Units.largeSpacing
// Avoid scrollbar flashing on/off when decrease the window height, that is created by the content matching the scroll height. level: 1
// Even if scrollbar does not appear in the UI, modifies the availableWidth causing other issues. }
QtControls.ScrollBar.vertical.policy: pageStack.maxHeight > pageStack.contentHeight ? QtControls.ScrollBar.AlwaysOff : QtControls.ScrollBar.AlwaysOn
property Item flickableItem: pageFlickable
// this horrible code below ensures the control with active focus stays visible in the window
// by scrolling the view up or down as needed when tabbing through the window
Window.onActiveFocusItemChanged: {
var flickable = scroll.flickableItem;
var item = Window.activeFocusItem;
if (!item) {
return;
}
// when an item within ScrollView has active focus the ScrollView,
// as FocusScope, also has it, so we only scroll in this case
if (!scroll.activeFocus) {
return;
}
var padding = PlasmaCore.Units.gridUnit * 2 // some padding to the top/bottom when we scroll
var yPos = item.mapToItem(scroll.contentItem, 0, 0).y;
if (yPos < flickable.contentY) {
flickable.contentY = Math.max(0, yPos - padding);
// The "Math.min(padding, item.height)" ensures that we only scroll the item into view
// when it's barely visible. The logic was mostly meant for keyboard navigating through
// a list of CheckBoxes, so this check keeps us from trying to scroll an inner ScrollView
// into view when it implicitly gains focus (like plasma-pa config dialog has).
} else if (yPos + Math.min(padding, item.height) > flickable.contentY + flickable.height) {
flickable.contentY = Math.min(flickable.contentHeight - flickable.height,
yPos - flickable.height + item.height + padding);
}
}
Flickable {
id: pageFlickable
anchors {
top: scroll.top
bottom: scroll.bottom
left: scroll.left
}
width: scroll.availableWidth
contentHeight: pageColumn.height
contentWidth: width
Column {
id: pageColumn
spacing: PlasmaCore.Units.largeSpacing / 2
anchors {
left: parent.left
right: parent.right
leftMargin: PlasmaCore.Units.smallSpacing * 2
rightMargin: PlasmaCore.Units.smallSpacing * 2
}
Kirigami.Heading {
id: pageTitle
width: pageColumn.width
topPadding: PlasmaCore.Units.smallSpacing
level: 1
text: pageStack.title
}
QtControls.StackView {
id: pageStack
property string title: ""
property bool invertAnimations: false
property var maxHeight: scroll.availableHeight - pageTitle.height - parent.spacing
property var contentHeight: pageStack.currentItem ? (pageStack.currentItem.implicitHeight
? pageStack.currentItem.implicitHeight
: pageStack.currentItem.childrenRect.height) : 0
height: Math.max(maxHeight, contentHeight)
width: pageColumn.width
property string sourceFile
onSourceFileChanged: {
if (!sourceFile) {
return;
}
//in a StackView pages need to be initialized with stackviews size, or have none
var props = {"width": width, "height": height}
var plasmoidConfig = plasmoid.configuration
for (var key in plasmoidConfig) {
props["cfg_" + key] = plasmoid.configuration[key]
}
var newItem = replace(Qt.resolvedUrl(sourceFile), props)
for (var key in plasmoidConfig) {
var changedSignal = newItem["cfg_" + key + "Changed"]
if (changedSignal) {
changedSignal.connect(root.settingValueChanged)
}
}
var configurationChangedSignal = newItem.configurationChanged
if (configurationChangedSignal) {
configurationChangedSignal.connect(root.settingValueChanged)
}
applyButton.enabled = false;
scroll.flickableItem.contentY = 0
/*
* This is not needed on a desktop shell that has ok/apply/cancel buttons, i'll leave it here only for future reference until we have a prototype for the active shell.
* root.pageChanged will start a timer, that in turn will call saveConfig() when triggered
for (var prop in currentItem) {
if (prop.indexOf("cfg_") === 0) {
currentItem[prop+"Changed"].connect(root.pageChanged)
}
}*/
}
replaceEnter: Transition { Loader {
ParallelAnimation { id: mainLoader
//OpacityAnimator when starting from 0 is buggy (it shows one frame with opacity 1) Layout.fillHeight: true
NumberAnimation { Layout.fillWidth: true
property: "opacity"
from: 0
to: 1
duration: PlasmaCore.Units.longDuration
easing.type: Easing.InOutQuad
}
XAnimator {
from: pageStack.invertAnimations ? -pageColumn.width/3: pageColumn.width/3
to: 0
duration: PlasmaCore.Units.longDuration
easing.type: Easing.InOutQuad
}
}
}
replaceExit: Transition {
ParallelAnimation {
OpacityAnimator {
from: 1
to: 0
duration: PlasmaCore.Units.longDuration
easing.type: Easing.InOutQuad
}
XAnimator {
from: 0
to: pageStack.invertAnimations ? pageColumn.width/3 : -pageColumn.width/3
duration: PlasmaCore.Units.longDuration
easing.type: Easing.InOutQuad
}
}
}
}
}
}
} }
QtControls.Action { QtControls.Action {
...@@ -410,7 +280,7 @@ Rectangle { ...@@ -410,7 +280,7 @@ Rectangle {
QtControls.Action { QtControls.Action {
id: applyAction id: applyAction
onTriggered: { onTriggered: {
root.saveConfig(); mainLoader.item.saveConfig()
applyButton.enabled = false; applyButton.enabled = false;
} }
...@@ -438,7 +308,7 @@ Rectangle { ...@@ -438,7 +308,7 @@ Rectangle {
enabled: false enabled: false
icon.name: "dialog-ok-apply" icon.name: "dialog-ok-apply"
text: i18nd("plasma_shell_org.kde.plasma.desktop", "Apply") text: i18nd("plasma_shell_org.kde.plasma.desktop", "Apply")
visible: pageStack.currentItem && (!pageStack.currentItem.kcm || pageStack.currentItem.kcm.buttons & 4) // 4 = Apply button visible: mainLoader.item && (!mainLoader.item.kcm || mainLoader.item.kcm.buttons & 4) // 4 = Apply button
onClicked: applyAction.trigger() onClicked: applyAction.trigger()
} }
QtControls.Button { QtControls.Button {
...@@ -449,5 +319,4 @@ Rectangle { ...@@ -449,5 +319,4 @@ Rectangle {
} }
} }
} }
//END UI components
} }
...@@ -28,37 +28,18 @@ import org.kde.kirigami 2.5 as Kirigami ...@@ -28,37 +28,18 @@ import org.kde.kirigami 2.5 as Kirigami
MouseArea { MouseArea {
id: delegate id: delegate
signal activated()
//BEGIN properties //BEGIN properties
implicitWidth: delegateContents.implicitWidth + 4 * PlasmaCore.Units.smallSpacing implicitWidth: delegateContents.implicitWidth + 4 * PlasmaCore.Units.smallSpacing
implicitHeight: delegateContents.height + PlasmaCore.Units.smallSpacing * 4 implicitHeight: delegateContents.height + PlasmaCore.Units.smallSpacing * 4
Layout.fillWidth: true Layout.fillWidth: true
hoverEnabled: true hoverEnabled: true
property bool current: (model.kcm && pageStack.currentItem.kcm && model.kcm == pageStack.currentItem.kcm) || (model.source == pageStack.sourceFile) property var item
property bool current: false
//END properties //END properties
//BEGIN functions
function openCategory() {
if (current) {
return;
}
if (typeof(categories.currentItem) !== "undefined") {
pageStack.invertAnimations = (categories.currentItem.y > delegate.y);
categories.currentItem = delegate;
}
if (model.source) {
pageStack.sourceFile = model.source;
} else if (model.kcm) {
pageStack.sourceFile = "";
pageStack.sourceFile = Qt.resolvedUrl("ConfigurationKcmPage.qml");
pageStack.currentItem.kcm = model.kcm;
} else {
pageStack.sourceFile = "";
}
pageStack.title = model.name
}
//END functions
//BEGIN connections //BEGIN connections
onPressed: { onPressed: {
categoriesScroll.forceActiveFocus() categoriesScroll.forceActiveFocus()
...@@ -67,14 +48,7 @@ MouseArea { ...@@ -67,14 +48,7 @@ MouseArea {
return; return;
} }
//print("model source: " + model.source + " " + pageStack.sourceFile); activated()
if (applyButton.enabled) {
messageDialog.delegate = delegate;
messageDialog.open();
return;