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:
+6
-1
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
cmake_minimum_required(VERSION 3.16)
|
cmake_minimum_required(VERSION 3.16)
|
||||||
|
|
||||||
project(flatkontrol VERSION 0.1.0 LANGUAGES CXX)
|
project(flatkontrol VERSION 0.2.0 LANGUAGES CXX)
|
||||||
|
|
||||||
set(REQUIRED_QT_VERSION 6.6.0)
|
set(REQUIRED_QT_VERSION 6.6.0)
|
||||||
set(REQUIRED_KF_VERSION 6.5.0)
|
set(REQUIRED_KF_VERSION 6.5.0)
|
||||||
@@ -30,6 +30,7 @@ find_package(Qt6 ${REQUIRED_QT_VERSION} REQUIRED COMPONENTS
|
|||||||
Quick
|
Quick
|
||||||
QuickControls2
|
QuickControls2
|
||||||
DBus
|
DBus
|
||||||
|
Test
|
||||||
)
|
)
|
||||||
|
|
||||||
find_package(KF6 ${REQUIRED_KF_VERSION} REQUIRED COMPONENTS
|
find_package(KF6 ${REQUIRED_KF_VERSION} REQUIRED COMPONENTS
|
||||||
@@ -47,6 +48,10 @@ ecm_setup_version(${PROJECT_VERSION}
|
|||||||
|
|
||||||
add_subdirectory(src)
|
add_subdirectory(src)
|
||||||
|
|
||||||
|
if(BUILD_TESTING)
|
||||||
|
add_subdirectory(autotests)
|
||||||
|
endif()
|
||||||
|
|
||||||
install(PROGRAMS io.github.toservetheking.FlatKontrol.desktop
|
install(PROGRAMS io.github.toservetheking.FlatKontrol.desktop
|
||||||
DESTINATION ${KDE_INSTALL_APPDIR})
|
DESTINATION ${KDE_INSTALL_APPDIR})
|
||||||
install(FILES io.github.toservetheking.FlatKontrol.metainfo.xml
|
install(FILES io.github.toservetheking.FlatKontrol.metainfo.xml
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
add_executable(resolutiontest
|
||||||
|
resolutiontest.cpp
|
||||||
|
../src/keyfile.cpp
|
||||||
|
../src/flatpakinstallations.cpp
|
||||||
|
../src/permissioncatalog.cpp
|
||||||
|
../src/portalsbackend.cpp
|
||||||
|
../src/permissionscontroller.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(resolutiontest PRIVATE ../src)
|
||||||
|
|
||||||
|
target_link_libraries(resolutiontest PRIVATE
|
||||||
|
Qt6::Core
|
||||||
|
Qt6::Qml
|
||||||
|
Qt6::DBus
|
||||||
|
Qt6::Test
|
||||||
|
KF6::I18n
|
||||||
|
)
|
||||||
|
|
||||||
|
add_test(NAME resolutiontest COMMAND resolutiontest)
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#include "keyfile.h"
|
||||||
|
#include "permissionscontroller.h"
|
||||||
|
|
||||||
|
#include <QDir>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QTemporaryDir>
|
||||||
|
#include <QtTest>
|
||||||
|
|
||||||
|
using PC = PermissionsController;
|
||||||
|
|
||||||
|
class ResolutionTest : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
private:
|
||||||
|
QTemporaryDir m_dir;
|
||||||
|
|
||||||
|
QString userBase() const
|
||||||
|
{
|
||||||
|
return m_dir.path() + QStringLiteral("/user");
|
||||||
|
}
|
||||||
|
|
||||||
|
void makeApp(const QString &appId, const QString &metadata)
|
||||||
|
{
|
||||||
|
const QString d = userBase() + QStringLiteral("/app/") + appId + QStringLiteral("/current/active");
|
||||||
|
QVERIFY(QDir().mkpath(d));
|
||||||
|
QFile f(d + QStringLiteral("/metadata"));
|
||||||
|
QVERIFY(f.open(QIODevice::WriteOnly | QIODevice::Text));
|
||||||
|
f.write(metadata.toUtf8());
|
||||||
|
}
|
||||||
|
|
||||||
|
void writeOverride(const QString &appId, const QString &content)
|
||||||
|
{
|
||||||
|
const QString d = userBase() + QStringLiteral("/overrides");
|
||||||
|
QVERIFY(QDir().mkpath(d));
|
||||||
|
QFile f(d + QLatin1Char('/') + appId);
|
||||||
|
QVERIFY(f.open(QIODevice::WriteOnly | QIODevice::Text));
|
||||||
|
f.write(content.toUtf8());
|
||||||
|
}
|
||||||
|
|
||||||
|
KeyFile readOverride(const QString &appId)
|
||||||
|
{
|
||||||
|
KeyFile kf;
|
||||||
|
kf.load(userBase() + QStringLiteral("/overrides/") + appId);
|
||||||
|
return kf;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int findToggle(PC &c, const QString &catId, const QString &option)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < c.rowCount(); ++i) {
|
||||||
|
const QModelIndex idx = c.index(i);
|
||||||
|
if (c.data(idx, PC::RowTypeRole).toInt() == PC::Toggle && c.data(idx, PC::CategoryIdRole).toString() == catId
|
||||||
|
&& c.data(idx, PC::OptionKeyRole).toString() == option) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Q_SLOTS:
|
||||||
|
void initTestCase()
|
||||||
|
{
|
||||||
|
QVERIFY(m_dir.isValid());
|
||||||
|
qputenv("FLATPAK_USER_DIR", userBase().toUtf8());
|
||||||
|
qputenv("FLATPAK_SYSTEM_DIR", (m_dir.path() + QStringLiteral("/system")).toUtf8());
|
||||||
|
// Point config at an empty dir so host custom installations don't interfere.
|
||||||
|
qputenv("FLATPAK_CONFIG_DIR", (m_dir.path() + QStringLiteral("/config")).toUtf8());
|
||||||
|
}
|
||||||
|
|
||||||
|
void toggleRenderAndSave()
|
||||||
|
{
|
||||||
|
makeApp(QStringLiteral("org.test.Toggle"), QStringLiteral("[Context]\nshared=network;ipc\n"));
|
||||||
|
PC c;
|
||||||
|
c.setAppId(QStringLiteral("org.test.Toggle"));
|
||||||
|
|
||||||
|
const int net = findToggle(c, QStringLiteral("share"), QStringLiteral("network"));
|
||||||
|
QVERIFY(net >= 0);
|
||||||
|
QCOMPARE(c.data(c.index(net), PC::ValueRole).toBool(), true);
|
||||||
|
QCOMPARE(c.data(c.index(net), PC::StatusRole).toInt(), int(PC::Original));
|
||||||
|
QVERIFY(!c.modified());
|
||||||
|
|
||||||
|
c.setToggleValue(net, false);
|
||||||
|
QVERIFY(c.modified());
|
||||||
|
QCOMPARE(c.data(c.index(net), PC::StatusRole).toInt(), int(PC::User));
|
||||||
|
|
||||||
|
c.save();
|
||||||
|
QVERIFY(!c.modified());
|
||||||
|
|
||||||
|
const QStringList shared = readOverride(QStringLiteral("org.test.Toggle")).value(QStringLiteral("Context"), QStringLiteral("shared")).split(QLatin1Char(';'), Qt::SkipEmptyParts);
|
||||||
|
QVERIFY(shared.contains(QStringLiteral("!network")));
|
||||||
|
QVERIFY(!shared.contains(QStringLiteral("!ipc"))); // unchanged options are not written
|
||||||
|
}
|
||||||
|
|
||||||
|
void filesystemsSplitByMode()
|
||||||
|
{
|
||||||
|
makeApp(QStringLiteral("org.test.Fs"), QStringLiteral("[Context]\nfilesystems=home;~/Docs:ro;xdg-download\n"));
|
||||||
|
PC c;
|
||||||
|
c.setAppId(QStringLiteral("org.test.Fs"));
|
||||||
|
|
||||||
|
// "home" is a preset toggle, on.
|
||||||
|
const int home = findToggle(c, QStringLiteral("filesystems-presets"), QStringLiteral("home"));
|
||||||
|
QVERIFY(home >= 0);
|
||||||
|
QCOMPARE(c.data(c.index(home), PC::ValueRole).toBool(), true);
|
||||||
|
|
||||||
|
// The non-preset paths become entries split into path + access mode.
|
||||||
|
QMap<QString, QString> paths;
|
||||||
|
for (int i = 0; i < c.rowCount(); ++i) {
|
||||||
|
const QModelIndex idx = c.index(i);
|
||||||
|
if (c.data(idx, PC::RowTypeRole).toInt() == PC::PathEntry && c.data(idx, PC::CategoryIdRole).toString() == QStringLiteral("filesystems-other")) {
|
||||||
|
paths.insert(c.data(idx, PC::ValueRole).toString(), c.data(idx, PC::SecondaryRole).toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QCOMPARE(paths.value(QStringLiteral("~/Docs")), QStringLiteral("ro"));
|
||||||
|
QCOMPARE(paths.value(QStringLiteral("xdg-download")), QStringLiteral("rw"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void unknownKeysPreserved()
|
||||||
|
{
|
||||||
|
makeApp(QStringLiteral("org.test.Unknown"), QStringLiteral("[Context]\nshared=network\n"));
|
||||||
|
writeOverride(QStringLiteral("org.test.Unknown"),
|
||||||
|
QStringLiteral("[Context]\nshared=!network\nunknownkey=foo\n\n[Weird Group]\na=b\n"));
|
||||||
|
|
||||||
|
PC c;
|
||||||
|
c.setAppId(QStringLiteral("org.test.Unknown"));
|
||||||
|
// Loading an existing file must be idempotent (no spurious "modified").
|
||||||
|
QVERIFY(!c.modified());
|
||||||
|
|
||||||
|
const int net = findToggle(c, QStringLiteral("share"), QStringLiteral("network"));
|
||||||
|
QVERIFY(net >= 0);
|
||||||
|
QCOMPARE(c.data(c.index(net), PC::ValueRole).toBool(), false); // overridden off
|
||||||
|
|
||||||
|
c.setToggleValue(net, true); // back to the metadata default -> override removed
|
||||||
|
QVERIFY(c.modified());
|
||||||
|
c.save();
|
||||||
|
|
||||||
|
const KeyFile kf = readOverride(QStringLiteral("org.test.Unknown"));
|
||||||
|
// Keys we do not model must survive the round-trip.
|
||||||
|
QCOMPARE(kf.value(QStringLiteral("Context"), QStringLiteral("unknownkey")), QStringLiteral("foo"));
|
||||||
|
QCOMPARE(kf.value(QStringLiteral("Weird Group"), QStringLiteral("a")), QStringLiteral("b"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
QTEST_GUILESS_MAIN(ResolutionTest)
|
||||||
|
#include "resolutiontest.moc"
|
||||||
@@ -61,6 +61,16 @@
|
|||||||
<content_rating type="oars-1.1"/>
|
<content_rating type="oars-1.1"/>
|
||||||
|
|
||||||
<releases>
|
<releases>
|
||||||
|
<release version="0.2.0" date="2026-07-01">
|
||||||
|
<description>
|
||||||
|
<ul>
|
||||||
|
<li>Preserve override keys that are not modelled, so saving never drops data</li>
|
||||||
|
<li>Ask before discarding unsaved changes when switching applications</li>
|
||||||
|
<li>Native FormCard layout for the permission list</li>
|
||||||
|
<li>Filesystem entries now have an access-mode selector and a folder browser</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
<release version="0.1.0" date="2026-06-30">
|
<release version="0.1.0" date="2026-06-30">
|
||||||
<description>
|
<description>
|
||||||
<p>Initial release.</p>
|
<p>Initial release.</p>
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ modules:
|
|||||||
# For Flathub / release builds, pin to a tag AND commit:
|
# For Flathub / release builds, pin to a tag AND commit:
|
||||||
- type: git
|
- type: git
|
||||||
url: https://github.com/toservetheking/FlatKontrol.git
|
url: https://github.com/toservetheking/FlatKontrol.git
|
||||||
tag: v0.1.0
|
tag: v0.2.0
|
||||||
# commit: <fill in the exact commit SHA the tag points at>
|
# commit: <fill in the exact commit SHA the tag points at>
|
||||||
#
|
#
|
||||||
# For local testing before the repo is pushed, replace the source above with:
|
# For local testing before the repo is pushed, replace the source above with:
|
||||||
|
|||||||
@@ -29,6 +29,33 @@ QString joinSorted(const QStringList &tokens)
|
|||||||
return copy.join(QLatin1Char(';'));
|
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
|
// Normalise a KeyFile to a canonical string so two override sets can be
|
||||||
// compared regardless of group/key/token ordering.
|
// compared regardless of group/key/token ordering.
|
||||||
QString normalize(const KeyFile &kf)
|
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)
|
bool PermissionsController::isFilesystemPreset(const QString &bareToken)
|
||||||
{
|
{
|
||||||
static const QSet<QString> presets = {
|
static const QSet<QString> presets = {
|
||||||
@@ -319,7 +363,8 @@ void PermissionsController::rebuild()
|
|||||||
Row r;
|
Row r;
|
||||||
r.catIndex = i;
|
r.catIndex = i;
|
||||||
r.kind = PathEntry;
|
r.kind = PathEntry;
|
||||||
r.primary = t;
|
r.primary = fsPathOf(t);
|
||||||
|
r.secondary = fsModeOf(t);
|
||||||
r.removable = true;
|
r.removable = true;
|
||||||
r.entryId = m_nextEntryId++;
|
r.entryId = m_nextEntryId++;
|
||||||
m_rows.append(r);
|
m_rows.append(r);
|
||||||
@@ -730,7 +775,7 @@ KeyFile PermissionsController::buildOverrides() const
|
|||||||
QSet<QString> desired;
|
QSet<QString> desired;
|
||||||
for (const Row &r : m_rows) {
|
for (const Row &r : m_rows) {
|
||||||
if (r.catIndex == i && !r.primary.trimmed().isEmpty()) {
|
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).
|
// Port of filesystemsOther.updateFromProxyProperty (presets separated).
|
||||||
@@ -834,6 +879,33 @@ KeyFile PermissionsController::buildOverrides() const
|
|||||||
kf.setValue(group, key, tokens.join(QLatin1Char(';')));
|
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;
|
return kf;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
#include <QQmlEngine>
|
#include <QQmlEngine>
|
||||||
#include <QSet>
|
#include <QSet>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
#include <QVariantList>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Owns the editable permission state for one application (or the global
|
* 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 isGlobal READ isGlobal NOTIFY appIdChanged)
|
||||||
Q_PROPERTY(bool modified READ modified NOTIFY modifiedChanged)
|
Q_PROPERTY(bool modified READ modified NOTIFY modifiedChanged)
|
||||||
Q_PROPERTY(bool canUndo READ canUndo NOTIFY canUndoChanged)
|
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:
|
public:
|
||||||
enum RowKind {
|
enum RowKind {
|
||||||
@@ -119,6 +122,7 @@ public:
|
|||||||
{
|
{
|
||||||
return m_canUndo;
|
return m_canUndo;
|
||||||
}
|
}
|
||||||
|
QVariantList categories() const;
|
||||||
|
|
||||||
// --- Editing API, called from QML ---
|
// --- Editing API, called from QML ---
|
||||||
Q_INVOKABLE void setToggleValue(int row, bool value);
|
Q_INVOKABLE void setToggleValue(int row, bool value);
|
||||||
|
|||||||
+26
-4
@@ -45,12 +45,37 @@ Kirigami.ApplicationWindow {
|
|||||||
detailsDialog.open();
|
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 {
|
function selectApp(appId: string): void {
|
||||||
controller.appId = appId;
|
controller.appId = appId;
|
||||||
// On narrow layouts, reveal the permissions column.
|
// On narrow layouts, reveal the permissions column.
|
||||||
root.pageStack.currentIndex = 1;
|
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 {
|
Component {
|
||||||
id: sidebarComponent
|
id: sidebarComponent
|
||||||
|
|
||||||
@@ -85,10 +110,7 @@ Kirigami.ApplicationWindow {
|
|||||||
subtitle: appDelegate.isGlobal ? i18n("Default settings for all apps") : appDelegate.appId
|
subtitle: appDelegate.isGlobal ? i18n("Default settings for all apps") : appDelegate.appId
|
||||||
}
|
}
|
||||||
|
|
||||||
onClicked: {
|
onClicked: root.requestSelect(appId)
|
||||||
appList.currentIndex = index;
|
|
||||||
root.selectApp(appId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Kirigami.PlaceholderMessage {
|
Kirigami.PlaceholderMessage {
|
||||||
|
|||||||
+271
-257
@@ -4,8 +4,11 @@ pragma ComponentBehavior: Bound
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls as QQC2
|
import QtQuick.Controls as QQC2
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
|
import QtQuick.Dialogs
|
||||||
import Qt.labs.qmlmodels
|
import Qt.labs.qmlmodels
|
||||||
import org.kde.kirigami as Kirigami
|
import org.kde.kirigami as Kirigami
|
||||||
|
import org.kde.kitemmodels as KItemModels
|
||||||
|
import org.kde.kirigamiaddons.formcard as FormCard
|
||||||
import io.github.toservetheking.FlatKontrol
|
import io.github.toservetheking.FlatKontrol
|
||||||
|
|
||||||
Kirigami.ScrollablePage {
|
Kirigami.ScrollablePage {
|
||||||
@@ -17,20 +20,23 @@ Kirigami.ScrollablePage {
|
|||||||
|
|
||||||
title: hasSelection ? controller.appName : i18nc("@title", "Permissions")
|
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 {
|
component StatusBadge: Kirigami.Icon {
|
||||||
property int badgeStatus: 0
|
property int state: 0
|
||||||
visible: badgeStatus > 0
|
visible: state > 0
|
||||||
implicitWidth: Kirigami.Units.iconSizes.small
|
implicitWidth: Kirigami.Units.iconSizes.small
|
||||||
implicitHeight: Kirigami.Units.iconSizes.small
|
implicitHeight: Kirigami.Units.iconSizes.small
|
||||||
source: badgeStatus === 2 ? "document-edit" : "globe"
|
source: state === 2 ? "document-edit-symbolic" : "globe-symbolic"
|
||||||
|
|
||||||
HoverHandler {
|
HoverHandler {
|
||||||
id: hover
|
id: badgeHover
|
||||||
}
|
}
|
||||||
QQC2.ToolTip.visible: hover.hovered
|
QQC2.ToolTip.visible: badgeHover.hovered
|
||||||
QQC2.ToolTip.text: badgeStatus === 2 ? i18n("Set by you") : i18n("Set by a global override")
|
QQC2.ToolTip.text: state === 2 ? i18n("Set by you") : i18n("Set by a global override")
|
||||||
}
|
}
|
||||||
|
|
||||||
actions: [
|
actions: [
|
||||||
@@ -70,259 +76,267 @@ Kirigami.ScrollablePage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- the list ------------------------------------------------------------
|
Kirigami.PlaceholderMessage {
|
||||||
|
anchors.centerIn: parent
|
||||||
ListView {
|
width: parent.width - Kirigami.Units.gridUnit * 4
|
||||||
id: permsView
|
visible: !page.hasSelection
|
||||||
model: page.controller
|
icon.name: "preferences-desktop"
|
||||||
|
text: i18n("Select an application")
|
||||||
Kirigami.PlaceholderMessage {
|
explanation: i18n("Choose an application to review and adjust its permissions.")
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shared delegate for path / relative-path entries.
|
ColumnLayout {
|
||||||
property Component entryDelegate: Component {
|
spacing: Kirigami.Units.largeSpacing
|
||||||
QQC2.ItemDelegate {
|
visible: page.hasSelection
|
||||||
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
|
|
||||||
|
|
||||||
contentItem: RowLayout {
|
Repeater {
|
||||||
spacing: Kirigami.Units.smallSpacing
|
model: page.controller.categories
|
||||||
QQC2.TextField {
|
|
||||||
text: pathDelegate.value
|
delegate: ColumnLayout {
|
||||||
placeholderText: pathDelegate.categoryId === "persistent" ? i18n("e.g. .thunderbird") : i18n("e.g. ~/games:ro")
|
id: section
|
||||||
Layout.fillWidth: true
|
|
||||||
onTextEdited: page.controller.setEntryPrimary(pathDelegate.index, text)
|
required property var modelData
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
FormCard.FormHeader {
|
||||||
|
title: section.modelData.title
|
||||||
}
|
}
|
||||||
StatusBadge {
|
|
||||||
badgeStatus: pathDelegate.status
|
FormCard.FormCard {
|
||||||
}
|
KItemModels.KSortFilterProxyModel {
|
||||||
QQC2.ToolButton {
|
id: catModel
|
||||||
icon.name: "edit-delete-remove"
|
sourceModel: page.controller
|
||||||
visible: pathDelegate.removable
|
filterRoleName: "categoryId"
|
||||||
onClicked: page.controller.removeEntry(pathDelegate.index)
|
// 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