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 2020 Nicolas Fella <nicolas.fella@gmx.de>
*
* 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
......@@ -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.configuration 2.0
//TODO: all of this will be done with desktop components
Rectangle {
id: root
Layout.minimumWidth: PlasmaCore.Units.gridUnit * 30
......@@ -36,15 +35,12 @@ Rectangle {
LayoutMirroring.enabled: Qt.application.layoutDirection === Qt.RightToLeft
LayoutMirroring.childrenInherit: true
//BEGIN properties
color: Kirigami.Theme.backgroundColor
width: PlasmaCore.Units.gridUnit * 40
height: PlasmaCore.Units.gridUnit * 30
property bool isContainment: false
//END properties
//BEGIN model
property ConfigModel globalConfigModel: globalAppletConfigModel
ConfigModel {
......@@ -62,46 +58,47 @@ Rectangle {
filterRole: "visible"
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() {
applyButton.enabled = true;
}
function open(item) {
if (item.source) {
if (item.source === "ConfigurationContainmentAppearance.qml") {
mainLoader.source = item.source
} else {
mainLoader.setSource(Qt.resolvedUrl("ConfigurationAppletPage.qml"), {configItem: item})
}
} else if (item.kcm) {
mainLoader.setSource(Qt.resolvedUrl("ConfigurationKcmPage.qml"), {kcm: item.kcm})
} else {
mainLoader.setSource("")
}
pageTitle.text = item.name
function settingValueChanged() {
applyButton.enabled = true;
applyButton.enabled = false
}
//END functions
Connections {
target: mainLoader.item
//BEGIN connections
Component.onCompleted: {
if (!isContainment && configDialog.configModel && configDialog.configModel.count > 0) {
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 {
pageStack.sourceFile = "";
function onSettingValueChanged() {
applyButton.enabled = true
}
}
pageStack.title = configDialog.configModel.get(0).name
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 {
pageStack.sourceFile = globalConfigModel.get(0).source
pageStack.title = globalConfigModel.get(0).name
open(configDialog.configModel.get(0))
}
}
//END connections
//BEGIN UI components
Rectangle {
id: sidebar
anchors.left: root.left
......@@ -115,6 +112,7 @@ Rectangle {
Kirigami.Separator {
anchors.left: sidebar.right
height: root.height
z: 100
}
Kirigami.Separator {
......@@ -126,24 +124,21 @@ Rectangle {
MessageDialog {
id: messageDialog
icon: StandardIcon.Warning
property Item delegate
property var item
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?")
standardButtons: StandardButton.Apply | StandardButton.Discard | StandardButton.Cancel
onApply: {
applyAction.trigger()
delegate.openCategory()
root.open(item)
}
onDiscard: {
delegate.openCategory()
root.open(item)
}
}
RowLayout {
anchors {
topMargin: topSeparator.height
fill: parent
}
anchors.fill: parent
spacing: 0
QtControls.ScrollView {
......@@ -166,7 +161,7 @@ Rectangle {
}
if (foundPrevious) {
button.openCategory()
categories.openCategory(button.item)
return
} else if (button.current) {
foundPrevious = true
......@@ -180,13 +175,12 @@ Rectangle {
var foundNext = false
for (var i = 0, length = buttons.length; i < length; ++i) {
var button = buttons[i];
console.log(button)
if (!button.hasOwnProperty("current")) {
continue;
}
if (foundNext) {
button.openCategory()
categories.openCategory(button.item)
return
} else if (button.current) {
foundNext = true
......@@ -201,17 +195,45 @@ Rectangle {
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 {
model: root.isContainment ? globalConfigModel : undefined
delegate: ConfigCategoryDelegate {}
delegate: categoryDelegate
}
Repeater {
model: configDialogFilterModel
delegate: ConfigCategoryDelegate {}
delegate: categoryDelegate
}
Repeater {
model: !root.isContainment ? globalConfigModel : undefined
delegate: ConfigCategoryDelegate {}
delegate: categoryDelegate
}
Repeater {
model: ConfigModel {
......@@ -221,7 +243,7 @@ Rectangle {
source: "AboutPlugin.qml"
}
}
delegate: ConfigCategoryDelegate {}
delegate: categoryDelegate
}
}
}
......@@ -232,170 +254,18 @@ Rectangle {
Layout.topMargin: topSeparator.height
Layout.bottomMargin: PlasmaCore.Units.smallSpacing * 2
// Configuration scroll area
QtControls.ScrollView {
id: scroll
Layout.fillHeight: true
Layout.fillWidth: true
// we want to focus the controls in the settings page right away, don't focus the ScrollView
activeFocusOnTab: false
// Avoid scrollbar flashing on/off when decrease the window height, that is created by the content matching the scroll height.
// 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
Layout.fillWidth: true
topPadding: Kirigami.Units.smallSpacing
leftPadding: Kirigami.Units.largeSpacing
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 {
ParallelAnimation {
//OpacityAnimator when starting from 0 is buggy (it shows one frame with opacity 1)
NumberAnimation {
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
}
}
}
}
}
}
Loader {
id: mainLoader
Layout.fillHeight: true
Layout.fillWidth: true
}
QtControls.Action {
......@@ -410,7 +280,7 @@ Rectangle {
QtControls.Action {
id: applyAction
onTriggered: {
root.saveConfig();
mainLoader.item.saveConfig()
applyButton.enabled = false;
}
......@@ -438,7 +308,7 @@ Rectangle {
enabled: false
icon.name: "dialog-ok-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()
}
QtControls.Button {
......@@ -449,5 +319,4 @@ Rectangle {
}
}
}
//END UI components
}
......@@ -28,37 +28,18 @@ import org.kde.kirigami 2.5 as Kirigami
MouseArea {
id: delegate
signal activated()
//BEGIN properties
implicitWidth: delegateContents.implicitWidth + 4 * PlasmaCore.Units.smallSpacing
implicitHeight: delegateContents.height + PlasmaCore.Units.smallSpacing * 4
Layout.fillWidth: 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
//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
onPressed: {
categoriesScroll.forceActiveFocus()
......@@ -67,14 +48,7 @@ MouseArea {
return;
}
//print("model source: " + model.source + " " + pageStack.sourceFile);
if (applyButton.enabled) {
messageDialog.delegate = delegate;
messageDialog.open();
return;
}
openCategory();
activated()
}
onCurrentChanged: {
if (current) {
......
/*
* Copyright 2020 Nicolas Fella <nicolas.fella@gmx.de>
*
* 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
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 2.010-1301, USA.
*/
import QtQuick 2.0
import org.kde.kirigami 2.10 as Kirigami
Kirigami.ScrollablePage {
id: root
property var configItem
signal settingValueChanged()
function saveConfig() {
for (var key in plasmoid.configuration) {
if (loader.item["cfg_" + key] != undefined) {
plasmoid.configuration[key] = loader.item["cfg_" + key]
}
}
// For ConfigurationContainmentActions.qml
if (loader.item.hasOwnProperty("saveConfig")) {
loader.item.saveConfig()
}
}
Loader {
id: loader
width: parent.width
// HACK the height of the loader is based on the implicitHeight of the content
// If the content item doesn't set one fall back to the height of its children
height: item.implicitHeight ? item.implicitHeight : item.childrenRect.height
Component.onCompleted: {
var plasmoidConfig = plasmoid.configuration
var props = {}
for (var key in plasmoidConfig) {
props["cfg_" + key] = plasmoid.configuration[key]
}
setSource(configItem.source, props)
for (var key in plasmoidConfig) {
var changedSignal = item["cfg_" + key + "Changed"]
if (changedSignal) {
changedSignal.connect(root.settingValueChanged)
}
}
var configurationChangedSignal = item.configurationChanged
if (configurationChangedSignal) {
configurationChangedSignal.connect(root.settingValueChanged)
}
}
}
}
......@@ -26,17 +26,16 @@ import org.kde.kconfig 1.0 // for KAuthorized
import org.kde.plasma.private.shell 2.0 as ShellPrivate // for WallpaperPlugin
import org.kde.plasma.core 2.0 as PlasmaCore
import org.kde.kirigami 2.5 as Kirigami
import org.kde.kcm 1.4
ColumnLayout {
AbstractKCM {
id: root
spacing: 0 // unless it's 0 there will be an additional gap between two FormLayouts
signal settingValueChanged
property int formAlignment: wallpaperComboBox.Kirigami.ScenePosition.x - root.Kirigami.ScenePosition.x + (PlasmaCore.Units.largeSpacing/2)
property string currentWallpaper: ""
property string containmentPlugin: ""
signal configurationChanged
//BEGIN functions
function saveConfig() {
if (main.currentItem.saveConfig) {
main.currentItem.saveConfig()
......@@ -50,7 +49,10 @@ ColumnLayout {
configDialog.applyWallpaper()
configDialog.containmentPlugin = root.containmentPlugin
}
//END functions
ColumnLayout {
anchors.fill: parent
spacing: 0 // unless it's 0 there will be an additional gap between two FormLayouts
Component.onCompleted: {
for (var i = 0; i < configDialog.containmentPluginsConfigModel.count; ++i) {
......@@ -97,7 +99,7 @@ ColumnLayout {
onActivated: {