v0.2.0: data-safety, unsaved-changes guard, FormCard UI, filesystem UX
- Preserve override keys we do not model so Save never drops data - Prompt before discarding unsaved changes when switching applications - Native FormCard layout for the permission list (grouped cards) - Filesystem entries gain an access-mode selector and a folder browser - Add resolution autotests (toggle render/save, filesystem split, key preservation)
This commit is contained in:
@@ -29,6 +29,33 @@ QString joinSorted(const QStringList &tokens)
|
||||
return copy.join(QLatin1Char(';'));
|
||||
}
|
||||
|
||||
// Split a filesystem token like "~/games:ro" into its access mode and path.
|
||||
// A missing/unknown suffix means read-write (flatpak's default).
|
||||
bool isFsMode(const QString &s)
|
||||
{
|
||||
return s == QLatin1String("ro") || s == QLatin1String("rw") || s == QLatin1String("create");
|
||||
}
|
||||
QString fsModeOf(const QString &token)
|
||||
{
|
||||
const int idx = token.lastIndexOf(QLatin1Char(':'));
|
||||
if (idx > 0 && isFsMode(token.mid(idx + 1))) {
|
||||
return token.mid(idx + 1);
|
||||
}
|
||||
return QStringLiteral("rw");
|
||||
}
|
||||
QString fsPathOf(const QString &token)
|
||||
{
|
||||
const int idx = token.lastIndexOf(QLatin1Char(':'));
|
||||
if (idx > 0 && isFsMode(token.mid(idx + 1))) {
|
||||
return token.left(idx);
|
||||
}
|
||||
return token;
|
||||
}
|
||||
QString fsToken(const QString &path, const QString &mode)
|
||||
{
|
||||
return (mode.isEmpty() || mode == QLatin1String("rw")) ? path : path + QLatin1Char(':') + mode;
|
||||
}
|
||||
|
||||
// Normalise a KeyFile to a canonical string so two override sets can be
|
||||
// compared regardless of group/key/token ordering.
|
||||
QString normalize(const KeyFile &kf)
|
||||
@@ -54,6 +81,23 @@ PermissionsController::PermissionsController(QObject *parent)
|
||||
{
|
||||
}
|
||||
|
||||
QVariantList PermissionsController::categories() const
|
||||
{
|
||||
QVariantList list;
|
||||
for (const CategoryDef &cat : catalog()) {
|
||||
if (cat.type == RowType::Portal && isGlobal()) {
|
||||
continue; // portals are not applicable to the global override file
|
||||
}
|
||||
list.append(QVariantMap{
|
||||
{QStringLiteral("id"), cat.id},
|
||||
{QStringLiteral("title"), cat.title},
|
||||
{QStringLiteral("description"), cat.description},
|
||||
{QStringLiteral("type"), static_cast<int>(cat.type)},
|
||||
});
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
bool PermissionsController::isFilesystemPreset(const QString &bareToken)
|
||||
{
|
||||
static const QSet<QString> presets = {
|
||||
@@ -319,7 +363,8 @@ void PermissionsController::rebuild()
|
||||
Row r;
|
||||
r.catIndex = i;
|
||||
r.kind = PathEntry;
|
||||
r.primary = t;
|
||||
r.primary = fsPathOf(t);
|
||||
r.secondary = fsModeOf(t);
|
||||
r.removable = true;
|
||||
r.entryId = m_nextEntryId++;
|
||||
m_rows.append(r);
|
||||
@@ -730,7 +775,7 @@ KeyFile PermissionsController::buildOverrides() const
|
||||
QSet<QString> desired;
|
||||
for (const Row &r : m_rows) {
|
||||
if (r.catIndex == i && !r.primary.trimmed().isEmpty()) {
|
||||
desired.insert(r.primary.trimmed());
|
||||
desired.insert(fsToken(r.primary.trimmed(), r.secondary));
|
||||
}
|
||||
}
|
||||
// Port of filesystemsOther.updateFromProxyProperty (presets separated).
|
||||
@@ -834,6 +879,33 @@ KeyFile PermissionsController::buildOverrides() const
|
||||
kf.setValue(group, key, tokens.join(QLatin1Char(';')));
|
||||
}
|
||||
|
||||
// Preserve any keys we do not model (e.g. newer flatpak options, or entries
|
||||
// written by other tools) so saving never drops data. We claim the six
|
||||
// Context keys and the whole Environment / bus-policy groups; everything else
|
||||
// is copied through verbatim from what was on disk.
|
||||
static const QSet<QString> claimedContextKeys = {
|
||||
QStringLiteral("shared"),
|
||||
QStringLiteral("sockets"),
|
||||
QStringLiteral("devices"),
|
||||
QStringLiteral("features"),
|
||||
QStringLiteral("filesystems"),
|
||||
QStringLiteral("persistent"),
|
||||
};
|
||||
static const QSet<QString> claimedGroups = {
|
||||
QStringLiteral("Environment"),
|
||||
QStringLiteral("Session Bus Policy"),
|
||||
QStringLiteral("System Bus Policy"),
|
||||
};
|
||||
for (const QString &group : m_savedSnapshot.groups()) {
|
||||
const bool wholeGroupClaimed = claimedGroups.contains(group);
|
||||
for (const QString &key : m_savedSnapshot.keys(group)) {
|
||||
const bool claimed = wholeGroupClaimed || (group == QStringLiteral("Context") && claimedContextKeys.contains(key));
|
||||
if (!claimed) {
|
||||
kf.setValue(group, key, m_savedSnapshot.value(group, key));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return kf;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <QQmlEngine>
|
||||
#include <QSet>
|
||||
#include <QString>
|
||||
#include <QVariantList>
|
||||
|
||||
/**
|
||||
* Owns the editable permission state for one application (or the global
|
||||
@@ -34,6 +35,8 @@ class PermissionsController : public QAbstractListModel
|
||||
Q_PROPERTY(bool isGlobal READ isGlobal NOTIFY appIdChanged)
|
||||
Q_PROPERTY(bool modified READ modified NOTIFY modifiedChanged)
|
||||
Q_PROPERTY(bool canUndo READ canUndo NOTIFY canUndoChanged)
|
||||
// One entry per visible category: {id, title, description, type}.
|
||||
Q_PROPERTY(QVariantList categories READ categories NOTIFY appIdChanged)
|
||||
|
||||
public:
|
||||
enum RowKind {
|
||||
@@ -119,6 +122,7 @@ public:
|
||||
{
|
||||
return m_canUndo;
|
||||
}
|
||||
QVariantList categories() const;
|
||||
|
||||
// --- Editing API, called from QML ---
|
||||
Q_INVOKABLE void setToggleValue(int row, bool value);
|
||||
|
||||
+26
-4
@@ -45,12 +45,37 @@ Kirigami.ApplicationWindow {
|
||||
detailsDialog.open();
|
||||
}
|
||||
|
||||
function requestSelect(appId: string): void {
|
||||
if (appId === controller.appId) {
|
||||
root.pageStack.currentIndex = 1;
|
||||
return;
|
||||
}
|
||||
if (controller.modified) {
|
||||
discardDialog.pendingAppId = appId;
|
||||
discardDialog.open();
|
||||
} else {
|
||||
selectApp(appId);
|
||||
}
|
||||
}
|
||||
|
||||
function selectApp(appId: string): void {
|
||||
controller.appId = appId;
|
||||
// On narrow layouts, reveal the permissions column.
|
||||
root.pageStack.currentIndex = 1;
|
||||
}
|
||||
|
||||
Kirigami.PromptDialog {
|
||||
id: discardDialog
|
||||
property string pendingAppId
|
||||
title: i18nc("@title", "Unsaved changes")
|
||||
subtitle: i18n("You have unsaved changes. Discard them and switch applications?")
|
||||
standardButtons: QQC2.Dialog.Discard | QQC2.Dialog.Cancel
|
||||
onDiscarded: {
|
||||
close();
|
||||
root.selectApp(discardDialog.pendingAppId);
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: sidebarComponent
|
||||
|
||||
@@ -85,10 +110,7 @@ Kirigami.ApplicationWindow {
|
||||
subtitle: appDelegate.isGlobal ? i18n("Default settings for all apps") : appDelegate.appId
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
appList.currentIndex = index;
|
||||
root.selectApp(appId);
|
||||
}
|
||||
onClicked: root.requestSelect(appId)
|
||||
}
|
||||
|
||||
Kirigami.PlaceholderMessage {
|
||||
|
||||
+271
-257
@@ -4,8 +4,11 @@ pragma ComponentBehavior: Bound
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Dialogs
|
||||
import Qt.labs.qmlmodels
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.kitemmodels as KItemModels
|
||||
import org.kde.kirigamiaddons.formcard as FormCard
|
||||
import io.github.toservetheking.FlatKontrol
|
||||
|
||||
Kirigami.ScrollablePage {
|
||||
@@ -17,20 +20,23 @@ Kirigami.ScrollablePage {
|
||||
|
||||
title: hasSelection ? controller.appName : i18nc("@title", "Permissions")
|
||||
|
||||
// --- shared bits ---------------------------------------------------------
|
||||
leftPadding: 0
|
||||
rightPadding: 0
|
||||
topPadding: Kirigami.Units.gridUnit
|
||||
bottomPadding: Kirigami.Units.gridUnit
|
||||
|
||||
// A small status marker used across delegates.
|
||||
component StatusBadge: Kirigami.Icon {
|
||||
property int badgeStatus: 0
|
||||
visible: badgeStatus > 0
|
||||
property int state: 0
|
||||
visible: state > 0
|
||||
implicitWidth: Kirigami.Units.iconSizes.small
|
||||
implicitHeight: Kirigami.Units.iconSizes.small
|
||||
source: badgeStatus === 2 ? "document-edit" : "globe"
|
||||
|
||||
source: state === 2 ? "document-edit-symbolic" : "globe-symbolic"
|
||||
HoverHandler {
|
||||
id: hover
|
||||
id: badgeHover
|
||||
}
|
||||
QQC2.ToolTip.visible: hover.hovered
|
||||
QQC2.ToolTip.text: badgeStatus === 2 ? i18n("Set by you") : i18n("Set by a global override")
|
||||
QQC2.ToolTip.visible: badgeHover.hovered
|
||||
QQC2.ToolTip.text: state === 2 ? i18n("Set by you") : i18n("Set by a global override")
|
||||
}
|
||||
|
||||
actions: [
|
||||
@@ -70,259 +76,267 @@ Kirigami.ScrollablePage {
|
||||
}
|
||||
}
|
||||
|
||||
// --- the list ------------------------------------------------------------
|
||||
|
||||
ListView {
|
||||
id: permsView
|
||||
model: page.controller
|
||||
|
||||
Kirigami.PlaceholderMessage {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - Kirigami.Units.gridUnit * 4
|
||||
visible: !page.hasSelection
|
||||
icon.name: "preferences-desktop"
|
||||
text: i18n("Select an application")
|
||||
explanation: i18n("Choose an application to review and adjust its permissions.")
|
||||
}
|
||||
|
||||
section.property: "categoryTitle"
|
||||
section.delegate: Kirigami.ListSectionHeader {
|
||||
required property string section
|
||||
width: ListView.view.width
|
||||
text: section
|
||||
}
|
||||
|
||||
delegate: DelegateChooser {
|
||||
role: "rowType"
|
||||
|
||||
// Toggle (share/sockets/devices/features/filesystem presets)
|
||||
DelegateChoice {
|
||||
roleValue: 0
|
||||
delegate: QQC2.ItemDelegate {
|
||||
id: toggleDelegate
|
||||
required property int index
|
||||
required property string label
|
||||
required property string example
|
||||
required property bool value
|
||||
required property int status
|
||||
|
||||
width: ListView.view.width
|
||||
hoverEnabled: true
|
||||
onClicked: toggleSwitch.toggle()
|
||||
|
||||
contentItem: RowLayout {
|
||||
spacing: Kirigami.Units.largeSpacing
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
QQC2.Label {
|
||||
text: toggleDelegate.label
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
QQC2.Label {
|
||||
text: toggleDelegate.example
|
||||
visible: text.length > 0
|
||||
opacity: 0.6
|
||||
font: Kirigami.Theme.smallFont
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
StatusBadge {
|
||||
badgeStatus: toggleDelegate.status
|
||||
}
|
||||
QQC2.Switch {
|
||||
id: toggleSwitch
|
||||
checked: toggleDelegate.value
|
||||
onToggled: page.controller.setToggleValue(toggleDelegate.index, checked)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Filesystem path entry
|
||||
DelegateChoice {
|
||||
roleValue: 1
|
||||
delegate: page.entryDelegate
|
||||
}
|
||||
// Persistent (relative path) entry
|
||||
DelegateChoice {
|
||||
roleValue: 2
|
||||
delegate: page.entryDelegate
|
||||
}
|
||||
|
||||
// Environment variable
|
||||
DelegateChoice {
|
||||
roleValue: 3
|
||||
delegate: QQC2.ItemDelegate {
|
||||
id: varDelegate
|
||||
required property int index
|
||||
required property string value
|
||||
required property string secondary
|
||||
required property int status
|
||||
required property bool removable
|
||||
width: ListView.view.width
|
||||
hoverEnabled: true
|
||||
background: null
|
||||
|
||||
contentItem: RowLayout {
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
QQC2.TextField {
|
||||
text: varDelegate.value
|
||||
placeholderText: i18n("VARIABLE")
|
||||
Layout.preferredWidth: Kirigami.Units.gridUnit * 10
|
||||
onTextEdited: page.controller.setEntryPrimary(varDelegate.index, text)
|
||||
}
|
||||
QQC2.Label {
|
||||
text: "="
|
||||
}
|
||||
QQC2.TextField {
|
||||
text: varDelegate.secondary
|
||||
placeholderText: i18n("value")
|
||||
Layout.fillWidth: true
|
||||
onTextEdited: page.controller.setEntrySecondary(varDelegate.index, text)
|
||||
}
|
||||
StatusBadge {
|
||||
badgeStatus: varDelegate.status
|
||||
}
|
||||
QQC2.ToolButton {
|
||||
icon.name: "edit-delete-remove"
|
||||
visible: varDelegate.removable
|
||||
onClicked: page.controller.removeEntry(varDelegate.index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// D-Bus name policy
|
||||
DelegateChoice {
|
||||
roleValue: 4
|
||||
delegate: QQC2.ItemDelegate {
|
||||
id: busDelegate
|
||||
required property int index
|
||||
required property string value
|
||||
required property string secondary
|
||||
required property int status
|
||||
required property bool removable
|
||||
width: ListView.view.width
|
||||
hoverEnabled: true
|
||||
background: null
|
||||
|
||||
contentItem: RowLayout {
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
QQC2.TextField {
|
||||
text: busDelegate.value
|
||||
placeholderText: i18n("e.g. org.freedesktop.Notifications")
|
||||
Layout.fillWidth: true
|
||||
onTextEdited: page.controller.setEntryPrimary(busDelegate.index, text)
|
||||
}
|
||||
QQC2.ComboBox {
|
||||
model: ["talk", "own"]
|
||||
currentIndex: busDelegate.secondary === "own" ? 1 : 0
|
||||
onActivated: page.controller.setEntrySecondary(busDelegate.index, currentValue)
|
||||
}
|
||||
StatusBadge {
|
||||
badgeStatus: busDelegate.status
|
||||
}
|
||||
QQC2.ToolButton {
|
||||
icon.name: "edit-delete-remove"
|
||||
visible: busDelegate.removable
|
||||
onClicked: page.controller.removeEntry(busDelegate.index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Portal (tri-state)
|
||||
DelegateChoice {
|
||||
roleValue: 5
|
||||
delegate: QQC2.ItemDelegate {
|
||||
id: portalDelegate
|
||||
required property int index
|
||||
required property string label
|
||||
required property string example
|
||||
required property int value
|
||||
required property bool supported
|
||||
required property string unsupportedReason
|
||||
width: ListView.view.width
|
||||
hoverEnabled: true
|
||||
background: null
|
||||
|
||||
readonly property var states: [2, 4, 3] // Unset, Allowed, Disallowed
|
||||
|
||||
contentItem: RowLayout {
|
||||
spacing: Kirigami.Units.largeSpacing
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
QQC2.Label {
|
||||
text: portalDelegate.label
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
QQC2.Label {
|
||||
text: portalDelegate.supported ? portalDelegate.example : portalDelegate.unsupportedReason
|
||||
visible: text.length > 0
|
||||
opacity: 0.6
|
||||
font: Kirigami.Theme.smallFont
|
||||
Layout.fillWidth: true
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
QQC2.ComboBox {
|
||||
enabled: portalDelegate.supported
|
||||
model: [i18n("Unset"), i18n("Allowed"), i18n("Disallowed")]
|
||||
currentIndex: portalDelegate.value === 4 ? 1 : (portalDelegate.value === 3 ? 2 : 0)
|
||||
onActivated: page.controller.setPortalValue(portalDelegate.index, portalDelegate.states[currentIndex])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// "Add entry" affordance
|
||||
DelegateChoice {
|
||||
roleValue: 6
|
||||
delegate: QQC2.ItemDelegate {
|
||||
id: addDelegate
|
||||
required property string categoryId
|
||||
width: ListView.view.width
|
||||
icon.name: "list-add"
|
||||
text: i18nc("@action", "Add…")
|
||||
onClicked: page.controller.addEntry(categoryId)
|
||||
}
|
||||
}
|
||||
}
|
||||
Kirigami.PlaceholderMessage {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - Kirigami.Units.gridUnit * 4
|
||||
visible: !page.hasSelection
|
||||
icon.name: "preferences-desktop"
|
||||
text: i18n("Select an application")
|
||||
explanation: i18n("Choose an application to review and adjust its permissions.")
|
||||
}
|
||||
|
||||
// Shared delegate for path / relative-path entries.
|
||||
property Component entryDelegate: Component {
|
||||
QQC2.ItemDelegate {
|
||||
id: pathDelegate
|
||||
required property int index
|
||||
required property string value
|
||||
required property string categoryId
|
||||
required property int status
|
||||
required property bool removable
|
||||
width: ListView.view.width
|
||||
hoverEnabled: true
|
||||
background: null
|
||||
ColumnLayout {
|
||||
spacing: Kirigami.Units.largeSpacing
|
||||
visible: page.hasSelection
|
||||
|
||||
contentItem: RowLayout {
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
QQC2.TextField {
|
||||
text: pathDelegate.value
|
||||
placeholderText: pathDelegate.categoryId === "persistent" ? i18n("e.g. .thunderbird") : i18n("e.g. ~/games:ro")
|
||||
Layout.fillWidth: true
|
||||
onTextEdited: page.controller.setEntryPrimary(pathDelegate.index, text)
|
||||
Repeater {
|
||||
model: page.controller.categories
|
||||
|
||||
delegate: ColumnLayout {
|
||||
id: section
|
||||
|
||||
required property var modelData
|
||||
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
|
||||
FormCard.FormHeader {
|
||||
title: section.modelData.title
|
||||
}
|
||||
StatusBadge {
|
||||
badgeStatus: pathDelegate.status
|
||||
}
|
||||
QQC2.ToolButton {
|
||||
icon.name: "edit-delete-remove"
|
||||
visible: pathDelegate.removable
|
||||
onClicked: page.controller.removeEntry(pathDelegate.index)
|
||||
|
||||
FormCard.FormCard {
|
||||
KItemModels.KSortFilterProxyModel {
|
||||
id: catModel
|
||||
sourceModel: page.controller
|
||||
filterRoleName: "categoryId"
|
||||
// No category id is a substring of another, so this is an exact match.
|
||||
filterString: section.modelData.id
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: catModel
|
||||
|
||||
delegate: DelegateChooser {
|
||||
role: "rowType"
|
||||
|
||||
// Toggle
|
||||
DelegateChoice {
|
||||
roleValue: 0
|
||||
delegate: FormCard.FormSwitchDelegate {
|
||||
id: toggleD
|
||||
required property int index
|
||||
required property string label
|
||||
required property string example
|
||||
required property bool value
|
||||
required property int status
|
||||
readonly property int sourceRow: catModel.mapToSource(catModel.index(index, 0)).row
|
||||
text: label
|
||||
description: example
|
||||
checked: value
|
||||
icon.name: status === 2 ? "document-edit-symbolic" : (status === 1 ? "globe-symbolic" : "")
|
||||
onToggled: page.controller.setToggleValue(sourceRow, checked)
|
||||
}
|
||||
}
|
||||
|
||||
// Filesystem path (with access mode + Browse)
|
||||
DelegateChoice {
|
||||
roleValue: 1
|
||||
delegate: FormCard.AbstractFormDelegate {
|
||||
id: fsD
|
||||
required property int index
|
||||
required property string value
|
||||
required property string secondary
|
||||
required property int status
|
||||
required property bool removable
|
||||
readonly property int sourceRow: catModel.mapToSource(catModel.index(index, 0)).row
|
||||
background: null
|
||||
|
||||
FolderDialog {
|
||||
id: folderDialog
|
||||
onAccepted: {
|
||||
const p = decodeURIComponent(selectedFolder.toString().replace(/^file:\/\//, ""));
|
||||
fsField.text = p;
|
||||
page.controller.setEntryPrimary(fsD.sourceRow, p);
|
||||
}
|
||||
}
|
||||
|
||||
contentItem: RowLayout {
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
QQC2.TextField {
|
||||
id: fsField
|
||||
text: fsD.value
|
||||
placeholderText: i18n("e.g. ~/Downloads")
|
||||
Layout.fillWidth: true
|
||||
onTextEdited: page.controller.setEntryPrimary(fsD.sourceRow, text)
|
||||
}
|
||||
QQC2.ComboBox {
|
||||
readonly property var modes: ["ro", "rw", "create"]
|
||||
model: [i18n("Read-only"), i18n("Read/write"), i18n("Create")]
|
||||
currentIndex: Math.max(0, modes.indexOf(fsD.secondary))
|
||||
onActivated: page.controller.setEntrySecondary(fsD.sourceRow, modes[currentIndex])
|
||||
}
|
||||
QQC2.ToolButton {
|
||||
icon.name: "document-open-folder"
|
||||
QQC2.ToolTip.text: i18n("Browse…")
|
||||
QQC2.ToolTip.visible: hovered
|
||||
onClicked: folderDialog.open()
|
||||
}
|
||||
StatusBadge {
|
||||
state: fsD.status
|
||||
}
|
||||
QQC2.ToolButton {
|
||||
icon.name: "edit-delete-remove"
|
||||
visible: fsD.removable
|
||||
onClicked: page.controller.removeEntry(fsD.sourceRow)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Persistent (home-relative) path
|
||||
DelegateChoice {
|
||||
roleValue: 2
|
||||
delegate: FormCard.AbstractFormDelegate {
|
||||
id: relD
|
||||
required property int index
|
||||
required property string value
|
||||
required property int status
|
||||
required property bool removable
|
||||
readonly property int sourceRow: catModel.mapToSource(catModel.index(index, 0)).row
|
||||
background: null
|
||||
contentItem: RowLayout {
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
QQC2.TextField {
|
||||
text: relD.value
|
||||
placeholderText: i18n("e.g. .thunderbird")
|
||||
Layout.fillWidth: true
|
||||
onTextEdited: page.controller.setEntryPrimary(relD.sourceRow, text)
|
||||
}
|
||||
StatusBadge {
|
||||
state: relD.status
|
||||
}
|
||||
QQC2.ToolButton {
|
||||
icon.name: "edit-delete-remove"
|
||||
visible: relD.removable
|
||||
onClicked: page.controller.removeEntry(relD.sourceRow)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Environment variable
|
||||
DelegateChoice {
|
||||
roleValue: 3
|
||||
delegate: FormCard.AbstractFormDelegate {
|
||||
id: varD
|
||||
required property int index
|
||||
required property string value
|
||||
required property string secondary
|
||||
required property int status
|
||||
required property bool removable
|
||||
readonly property int sourceRow: catModel.mapToSource(catModel.index(index, 0)).row
|
||||
background: null
|
||||
contentItem: RowLayout {
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
QQC2.TextField {
|
||||
text: varD.value
|
||||
placeholderText: i18n("VARIABLE")
|
||||
Layout.preferredWidth: Kirigami.Units.gridUnit * 10
|
||||
onTextEdited: page.controller.setEntryPrimary(varD.sourceRow, text)
|
||||
}
|
||||
QQC2.Label {
|
||||
text: "="
|
||||
}
|
||||
QQC2.TextField {
|
||||
text: varD.secondary
|
||||
placeholderText: i18n("value")
|
||||
Layout.fillWidth: true
|
||||
onTextEdited: page.controller.setEntrySecondary(varD.sourceRow, text)
|
||||
}
|
||||
StatusBadge {
|
||||
state: varD.status
|
||||
}
|
||||
QQC2.ToolButton {
|
||||
icon.name: "edit-delete-remove"
|
||||
visible: varD.removable
|
||||
onClicked: page.controller.removeEntry(varD.sourceRow)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// D-Bus name policy
|
||||
DelegateChoice {
|
||||
roleValue: 4
|
||||
delegate: FormCard.AbstractFormDelegate {
|
||||
id: busD
|
||||
required property int index
|
||||
required property string value
|
||||
required property string secondary
|
||||
required property int status
|
||||
required property bool removable
|
||||
readonly property int sourceRow: catModel.mapToSource(catModel.index(index, 0)).row
|
||||
background: null
|
||||
contentItem: RowLayout {
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
QQC2.TextField {
|
||||
text: busD.value
|
||||
placeholderText: i18n("e.g. org.freedesktop.Notifications")
|
||||
Layout.fillWidth: true
|
||||
onTextEdited: page.controller.setEntryPrimary(busD.sourceRow, text)
|
||||
}
|
||||
QQC2.ComboBox {
|
||||
model: ["talk", "own"]
|
||||
currentIndex: busD.secondary === "own" ? 1 : 0
|
||||
onActivated: page.controller.setEntrySecondary(busD.sourceRow, currentValue)
|
||||
}
|
||||
StatusBadge {
|
||||
state: busD.status
|
||||
}
|
||||
QQC2.ToolButton {
|
||||
icon.name: "edit-delete-remove"
|
||||
visible: busD.removable
|
||||
onClicked: page.controller.removeEntry(busD.sourceRow)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Portal (tri-state)
|
||||
DelegateChoice {
|
||||
roleValue: 5
|
||||
delegate: FormCard.FormComboBoxDelegate {
|
||||
id: portalD
|
||||
required property int index
|
||||
required property string label
|
||||
required property string example
|
||||
required property int value
|
||||
required property bool supported
|
||||
required property string unsupportedReason
|
||||
readonly property int sourceRow: catModel.mapToSource(catModel.index(index, 0)).row
|
||||
readonly property var states: [2, 4, 3]
|
||||
text: label
|
||||
description: supported ? example : unsupportedReason
|
||||
enabled: supported
|
||||
model: [i18n("Unset"), i18n("Allowed"), i18n("Disallowed")]
|
||||
currentIndex: value === 4 ? 1 : (value === 3 ? 2 : 0)
|
||||
onActivated: page.controller.setPortalValue(sourceRow, states[currentIndex])
|
||||
}
|
||||
}
|
||||
|
||||
// "Add entry" affordance
|
||||
DelegateChoice {
|
||||
roleValue: 6
|
||||
delegate: FormCard.FormButtonDelegate {
|
||||
id: addD
|
||||
required property string categoryId
|
||||
text: i18nc("@action", "Add…")
|
||||
icon.name: "list-add"
|
||||
onClicked: page.controller.addEntry(categoryId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user