v0.2.2: Preferences page, About page, full icon set, redesigned permissions list

- Add a Preferences page (reachable from the hamburger menu) with a
  KColorSchemeManager-backed color scheme switcher
- Add an About page reading from KAboutData
- Ship a full hicolor icon set (16-128px + scalable) via ecm_install_icons
  instead of a single flat SVG
- Restyle the permissions list after KDE System Settings' Audio page:
  Kirigami.ListSectionHeader per category, flat separated rows, no card
  borders
- Explicitly declare Kirigami/KirigamiAddons/ColorScheme as CMake
  dependencies instead of relying on the QML import scanner alone
- Add KDEClangFormat/KDEGitCommitHooks, a CMakePresets.json, and a
  REUSE + clang-format lint CI workflow
- Reformat SPDX headers to include copyright, and tidy up a stray
  namespace inconsistency
This commit is contained in:
2026-07-03 16:25:29 -05:00
parent a657287bd1
commit 3a17433f53
42 changed files with 590 additions and 71 deletions
+7
View File
@@ -1,3 +1,5 @@
# SPDX-FileCopyrightText: 2026 ToServeTheKing <[email protected]>
#
# SPDX-License-Identifier: GPL-3.0-or-later
add_executable(flatkontrol
@@ -15,11 +17,14 @@ qt_add_qml_module(flatkontrol
QML_FILES
qml/Main.qml
qml/PermissionsPage.qml
qml/SettingsPage.qml
SOURCES
applicationsmodel.cpp
applicationsmodel.h
permissionscontroller.cpp
permissionscontroller.h
appcolorscheme.cpp
appcolorscheme.h
)
target_include_directories(flatkontrol PRIVATE ${CMAKE_BINARY_DIR})
@@ -38,6 +43,8 @@ target_link_libraries(flatkontrol PRIVATE
KF6::IconThemes
KF6::Crash
KF6::DBusAddons
KF6::ColorScheme
KF6::Kirigami
)
install(TARGETS flatkontrol ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
+34
View File
@@ -0,0 +1,34 @@
/*
SPDX-FileCopyrightText: 2026 ToServeTheKing <[email protected]>
SPDX-License-Identifier: GPL-3.0-or-later
*/
#include "appcolorscheme.h"
#include <KColorSchemeManager>
AppColorScheme::AppColorScheme(QObject *parent)
: QObject(parent)
, mSchemes(KColorSchemeManager::instance())
{
}
QAbstractItemModel *AppColorScheme::colorSchemesModel()
{
return mSchemes->model();
}
QString AppColorScheme::activeColorSchemeName() const
{
return mSchemes->activeSchemeName();
}
void AppColorScheme::setActiveColorSchemeName(const QString &name)
{
if (name == activeColorSchemeName()) {
return;
}
mSchemes->activateScheme(mSchemes->indexForScheme(name));
Q_EMIT activeColorSchemeNameChanged();
}
+40
View File
@@ -0,0 +1,40 @@
/*
SPDX-FileCopyrightText: 2026 ToServeTheKing <[email protected]>
SPDX-License-Identifier: GPL-3.0-or-later
*/
#pragma once
#include <QAbstractItemModel>
#include <QObject>
#include <QQmlEngine>
class KColorSchemeManager;
/**
* Thin QML-facing wrapper around KColorSchemeManager: exposes the list of
* installed color schemes and the currently active one. KColorSchemeManager
* autosaves the active scheme itself, so there is nothing else to persist.
*/
class AppColorScheme : public QObject
{
Q_OBJECT
QML_ELEMENT
QML_SINGLETON
Q_PROPERTY(QAbstractItemModel *colorSchemesModel READ colorSchemesModel CONSTANT)
Q_PROPERTY(QString activeColorSchemeName READ activeColorSchemeName WRITE setActiveColorSchemeName NOTIFY activeColorSchemeNameChanged)
public:
explicit AppColorScheme(QObject *parent = nullptr);
QAbstractItemModel *colorSchemesModel();
QString activeColorSchemeName() const;
void setActiveColorSchemeName(const QString &name);
Q_SIGNALS:
void activeColorSchemeNameChanged();
private:
KColorSchemeManager *mSchemes;
};
+6 -1
View File
@@ -1,4 +1,9 @@
// SPDX-License-Identifier: GPL-3.0-or-later
/*
SPDX-FileCopyrightText: 2026 ToServeTheKing <[email protected]>
SPDX-License-Identifier: GPL-3.0-or-later
*/
#include "applicationsmodel.h"
#include <KDirWatch>
+6 -1
View File
@@ -1,4 +1,9 @@
// SPDX-License-Identifier: GPL-3.0-or-later
/*
SPDX-FileCopyrightText: 2026 ToServeTheKing <[email protected]>
SPDX-License-Identifier: GPL-3.0-or-later
*/
#pragma once
#include "flatpakinstallations.h"
+6 -1
View File
@@ -1,4 +1,9 @@
// SPDX-License-Identifier: GPL-3.0-or-later
/*
SPDX-FileCopyrightText: 2026 ToServeTheKing <[email protected]>
SPDX-License-Identifier: GPL-3.0-or-later
*/
#include "flatpakinstallations.h"
#include "keyfile.h"
+6 -1
View File
@@ -1,4 +1,9 @@
// SPDX-License-Identifier: GPL-3.0-or-later
/*
SPDX-FileCopyrightText: 2026 ToServeTheKing <[email protected]>
SPDX-License-Identifier: GPL-3.0-or-later
*/
#pragma once
#include <QString>
+6 -1
View File
@@ -1,4 +1,9 @@
// SPDX-License-Identifier: GPL-3.0-or-later
/*
SPDX-FileCopyrightText: 2026 ToServeTheKing <[email protected]>
SPDX-License-Identifier: GPL-3.0-or-later
*/
#include "keyfile.h"
#include <QDir>
+6 -1
View File
@@ -1,4 +1,9 @@
// SPDX-License-Identifier: GPL-3.0-or-later
/*
SPDX-FileCopyrightText: 2026 ToServeTheKing <[email protected]>
SPDX-License-Identifier: GPL-3.0-or-later
*/
#pragma once
#include <QMap>
+6 -1
View File
@@ -1,4 +1,9 @@
// SPDX-License-Identifier: GPL-3.0-or-later
/*
SPDX-FileCopyrightText: 2026 ToServeTheKing <[email protected]>
SPDX-License-Identifier: GPL-3.0-or-later
*/
#include "flatkontrol-version.h"
#include "applicationsmodel.h"
+6 -1
View File
@@ -1,4 +1,9 @@
// SPDX-License-Identifier: GPL-3.0-or-later
/*
SPDX-FileCopyrightText: 2026 ToServeTheKing <[email protected]>
SPDX-License-Identifier: GPL-3.0-or-later
*/
#include "permissioncatalog.h"
#include <KLocalizedString>
+6 -1
View File
@@ -1,4 +1,9 @@
// SPDX-License-Identifier: GPL-3.0-or-later
/*
SPDX-FileCopyrightText: 2026 ToServeTheKing <[email protected]>
SPDX-License-Identifier: GPL-3.0-or-later
*/
#pragma once
#include <QList>
+6 -1
View File
@@ -1,4 +1,9 @@
// SPDX-License-Identifier: GPL-3.0-or-later
/*
SPDX-FileCopyrightText: 2026 ToServeTheKing <[email protected]>
SPDX-License-Identifier: GPL-3.0-or-later
*/
#include "permissionscontroller.h"
#include <KLocalizedString>
+7 -2
View File
@@ -1,4 +1,9 @@
// SPDX-License-Identifier: GPL-3.0-or-later
/*
SPDX-FileCopyrightText: 2026 ToServeTheKing <[email protected]>
SPDX-License-Identifier: GPL-3.0-or-later
*/
#pragma once
#include "flatpakinstallations.h"
@@ -187,7 +192,7 @@ private:
static bool isFilesystemPreset(const QString &bareToken);
FlatpakInstallations m_installations;
FlatKontrol::PortalsBackend m_portals;
PortalsBackend m_portals;
QString m_appId;
QString m_appName;
+6 -6
View File
@@ -1,4 +1,9 @@
// SPDX-License-Identifier: GPL-3.0-or-later
/*
SPDX-FileCopyrightText: 2026 ToServeTheKing <[email protected]>
SPDX-License-Identifier: GPL-3.0-or-later
*/
#include "portalsbackend.h"
#include <KLocalizedString>
@@ -9,9 +14,6 @@
#include <QDBusMessage>
#include <QDBusReply>
namespace FlatKontrol
{
static constexpr uint SUPPORTED_SERVICE_VERSION = 2;
static const char *DBUS_PATH = "/org/freedesktop/impl/portal/PermissionStore";
static const char *DBUS_IFACE = "org.freedesktop.impl.portal.PermissionStore";
@@ -253,5 +255,3 @@ void PortalsBackend::reload()
m_supported.clear();
m_reasons.clear();
}
} // namespace FlatKontrol
+6 -6
View File
@@ -1,4 +1,9 @@
// SPDX-License-Identifier: GPL-3.0-or-later
/*
SPDX-FileCopyrightText: 2026 ToServeTheKing <[email protected]>
SPDX-License-Identifier: GPL-3.0-or-later
*/
#pragma once
#include <QHash>
@@ -8,9 +13,6 @@
class QDBusInterface;
namespace FlatKontrol
{
/// Tri-state (plus availability) of a portal permission, matching Flatseal.
enum class PortalState {
Unknown = 0,
@@ -71,5 +73,3 @@ private:
QHash<QString, bool> m_supported; // property -> supported
QHash<QString, QString> m_reasons; // property -> reason
};
} // namespace FlatKontrol
+45 -1
View File
@@ -1,4 +1,9 @@
// SPDX-License-Identifier: GPL-3.0-or-later
/*
SPDX-FileCopyrightText: 2026 ToServeTheKing <[email protected]>
SPDX-License-Identifier: GPL-3.0-or-later
*/
pragma ComponentBehavior: Bound
import QtQuick
@@ -7,6 +12,7 @@ import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.kitemmodels as KItemModels
import org.kde.kirigamiaddons.delegates as Delegates
import org.kde.kirigamiaddons.formcard as FormCard
import io.github.toservetheking.FlatKontrol
Kirigami.ApplicationWindow {
@@ -19,6 +25,44 @@ Kirigami.ApplicationWindow {
width: Kirigami.Units.gridUnit * 48
height: Kirigami.Units.gridUnit * 34
globalDrawer: Kirigami.GlobalDrawer {
isMenu: true
actions: [
Kirigami.Action {
text: i18nc("@action:inmenu", "Preferences…")
icon.name: "configure"
onTriggered: root.pageStack.pushDialogLayer(settingsPageComponent, {
width: root.width
}, {
width: Kirigami.Units.gridUnit * 24,
height: Kirigami.Units.gridUnit * 20,
modality: Qt.NonModal
})
},
Kirigami.Action {
text: i18nc("@action:inmenu", "About %1", root.title)
icon.name: "help-about"
onTriggered: root.pageStack.pushDialogLayer(aboutPageComponent, {
width: root.width
}, {
width: Kirigami.Units.gridUnit * 30,
height: Kirigami.Units.gridUnit * 30,
modality: Qt.NonModal
})
}
]
}
Component {
id: aboutPageComponent
FormCard.AboutPage {}
}
Component {
id: settingsPageComponent
SettingsPage {}
}
ApplicationsModel {
id: appsModel
}
+159 -41
View File
@@ -1,6 +1,14 @@
// SPDX-License-Identifier: GPL-3.0-or-later
/*
SPDX-FileCopyrightText: 2026 ToServeTheKing <[email protected]>
SPDX-License-Identifier: GPL-3.0-or-later
*/
pragma ComponentBehavior: Bound
// Styled after KDE System Settings' Audio page (plasma-pa's kcm/ui/main.qml):
// Kirigami.ListSectionHeader per category, flat rows indented under the
// header and separated by Kirigami.Separator, no card/border around them.
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
@@ -8,7 +16,6 @@ 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 {
@@ -22,10 +29,12 @@ Kirigami.ScrollablePage {
leftPadding: 0
rightPadding: 0
topPadding: Kirigami.Units.gridUnit
// No topPadding: Kirigami.ListSectionHeader (the first thing on the
// page) already carries its own top padding, and stacking ours on top
// of that left an oversized gap above the first category.
bottomPadding: Kirigami.Units.gridUnit
// A small status marker used across delegates.
// A small status marker used across rows.
component StatusBadge: Kirigami.Icon {
property int state: 0
visible: state > 0
@@ -86,7 +95,7 @@ Kirigami.ScrollablePage {
}
ColumnLayout {
spacing: Kirigami.Units.largeSpacing
spacing: 0
visible: page.hasSelection
Repeater {
@@ -100,20 +109,29 @@ Kirigami.ScrollablePage {
Layout.fillWidth: true
spacing: 0
FormCard.FormHeader {
title: section.modelData.title
Kirigami.ListSectionHeader {
Layout.fillWidth: true
text: section.modelData.title
}
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
}
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
}
ColumnLayout {
Layout.fillWidth: true
Layout.leftMargin: Kirigami.Units.largeSpacing
Layout.rightMargin: Kirigami.Units.largeSpacing
Layout.topMargin: Kirigami.Units.smallSpacing
Layout.bottomMargin: Kirigami.Units.smallSpacing
spacing: Kirigami.Units.largeSpacing
Repeater {
id: rowRepeater
model: catModel
delegate: DelegateChooser {
@@ -122,7 +140,7 @@ Kirigami.ScrollablePage {
// Toggle
DelegateChoice {
roleValue: 0
delegate: FormCard.FormSwitchDelegate {
delegate: ColumnLayout {
id: toggleD
required property int index
required property string label
@@ -130,18 +148,51 @@ Kirigami.ScrollablePage {
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)
Layout.fillWidth: true
spacing: Kirigami.Units.smallSpacing
RowLayout {
Layout.fillWidth: true
spacing: Kirigami.Units.smallSpacing
ColumnLayout {
Layout.fillWidth: true
spacing: 0
QQC2.Label {
Layout.fillWidth: true
text: toggleD.label
wrapMode: Text.Wrap
}
QQC2.Label {
Layout.fillWidth: true
text: toggleD.example
visible: text.length > 0
wrapMode: Text.Wrap
font: Kirigami.Theme.smallFont
color: Kirigami.Theme.disabledTextColor
}
}
StatusBadge {
state: toggleD.status
}
QQC2.Switch {
checked: toggleD.value
onToggled: page.controller.setToggleValue(toggleD.sourceRow, checked)
}
}
Kirigami.Separator {
Layout.fillWidth: true
Layout.topMargin: Kirigami.Units.smallSpacing
visible: toggleD.index !== rowRepeater.count - 1
}
}
}
// Filesystem path (with access mode + Browse)
DelegateChoice {
roleValue: 1
delegate: FormCard.AbstractFormDelegate {
delegate: ColumnLayout {
id: fsD
required property int index
required property string value
@@ -149,7 +200,8 @@ Kirigami.ScrollablePage {
required property int status
required property bool removable
readonly property int sourceRow: catModel.mapToSource(catModel.index(index, 0)).row
background: null
Layout.fillWidth: true
spacing: Kirigami.Units.smallSpacing
FolderDialog {
id: folderDialog
@@ -160,7 +212,8 @@ Kirigami.ScrollablePage {
}
}
contentItem: RowLayout {
RowLayout {
Layout.fillWidth: true
spacing: Kirigami.Units.smallSpacing
QQC2.TextField {
id: fsField
@@ -190,21 +243,30 @@ Kirigami.ScrollablePage {
onClicked: page.controller.removeEntry(fsD.sourceRow)
}
}
Kirigami.Separator {
Layout.fillWidth: true
Layout.topMargin: Kirigami.Units.smallSpacing
visible: fsD.index !== rowRepeater.count - 1
}
}
}
// Persistent (home-relative) path
DelegateChoice {
roleValue: 2
delegate: FormCard.AbstractFormDelegate {
delegate: ColumnLayout {
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 {
Layout.fillWidth: true
spacing: Kirigami.Units.smallSpacing
RowLayout {
Layout.fillWidth: true
spacing: Kirigami.Units.smallSpacing
QQC2.TextField {
text: relD.value
@@ -221,13 +283,19 @@ Kirigami.ScrollablePage {
onClicked: page.controller.removeEntry(relD.sourceRow)
}
}
Kirigami.Separator {
Layout.fillWidth: true
Layout.topMargin: Kirigami.Units.smallSpacing
visible: relD.index !== rowRepeater.count - 1
}
}
}
// Environment variable
DelegateChoice {
roleValue: 3
delegate: FormCard.AbstractFormDelegate {
delegate: ColumnLayout {
id: varD
required property int index
required property string value
@@ -235,8 +303,11 @@ Kirigami.ScrollablePage {
required property int status
required property bool removable
readonly property int sourceRow: catModel.mapToSource(catModel.index(index, 0)).row
background: null
contentItem: RowLayout {
Layout.fillWidth: true
spacing: Kirigami.Units.smallSpacing
RowLayout {
Layout.fillWidth: true
spacing: Kirigami.Units.smallSpacing
QQC2.TextField {
text: varD.value
@@ -262,13 +333,19 @@ Kirigami.ScrollablePage {
onClicked: page.controller.removeEntry(varD.sourceRow)
}
}
Kirigami.Separator {
Layout.fillWidth: true
Layout.topMargin: Kirigami.Units.smallSpacing
visible: varD.index !== rowRepeater.count - 1
}
}
}
// D-Bus name policy
DelegateChoice {
roleValue: 4
delegate: FormCard.AbstractFormDelegate {
delegate: ColumnLayout {
id: busD
required property int index
required property string value
@@ -276,8 +353,11 @@ Kirigami.ScrollablePage {
required property int status
required property bool removable
readonly property int sourceRow: catModel.mapToSource(catModel.index(index, 0)).row
background: null
contentItem: RowLayout {
Layout.fillWidth: true
spacing: Kirigami.Units.smallSpacing
RowLayout {
Layout.fillWidth: true
spacing: Kirigami.Units.smallSpacing
QQC2.TextField {
text: busD.value
@@ -299,13 +379,19 @@ Kirigami.ScrollablePage {
onClicked: page.controller.removeEntry(busD.sourceRow)
}
}
Kirigami.Separator {
Layout.fillWidth: true
Layout.topMargin: Kirigami.Units.smallSpacing
visible: busD.index !== rowRepeater.count - 1
}
}
}
// Portal (tri-state)
DelegateChoice {
roleValue: 5
delegate: FormCard.FormComboBoxDelegate {
delegate: ColumnLayout {
id: portalD
required property int index
required property string label
@@ -315,19 +401,51 @@ Kirigami.ScrollablePage {
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])
Layout.fillWidth: true
spacing: Kirigami.Units.smallSpacing
RowLayout {
Layout.fillWidth: true
spacing: Kirigami.Units.smallSpacing
ColumnLayout {
Layout.fillWidth: true
spacing: 0
QQC2.Label {
Layout.fillWidth: true
text: portalD.label
wrapMode: Text.Wrap
enabled: portalD.supported
}
QQC2.Label {
Layout.fillWidth: true
text: portalD.supported ? portalD.example : portalD.unsupportedReason
visible: text.length > 0
wrapMode: Text.Wrap
font: Kirigami.Theme.smallFont
color: Kirigami.Theme.disabledTextColor
}
}
QQC2.ComboBox {
enabled: portalD.supported
model: [i18n("Unset"), i18n("Allowed"), i18n("Disallowed")]
currentIndex: portalD.value === 4 ? 1 : (portalD.value === 3 ? 2 : 0)
onActivated: page.controller.setPortalValue(portalD.sourceRow, portalD.states[currentIndex])
}
}
Kirigami.Separator {
Layout.fillWidth: true
Layout.topMargin: Kirigami.Units.smallSpacing
visible: portalD.index !== rowRepeater.count - 1
}
}
}
// "Add entry" affordance
DelegateChoice {
roleValue: 6
delegate: FormCard.FormButtonDelegate {
delegate: QQC2.Button {
id: addD
required property string categoryId
text: i18nc("@action", "Add…")
+79
View File
@@ -0,0 +1,79 @@
/*
SPDX-FileCopyrightText: 2026 ToServeTheKing <[email protected]>
SPDX-License-Identifier: GPL-3.0-or-later
*/
pragma ComponentBehavior: Bound
// Styled after KDE System Settings' Audio page (plasma-pa's kcm/ui/main.qml):
// Kirigami.ListSectionHeader per section, flat rows indented under the
// header, no card/border around them.
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import io.github.toservetheking.FlatKontrol
Kirigami.ScrollablePage {
id: root
title: i18nc("@title:window", "Preferences")
leftPadding: 0
rightPadding: 0
// No topPadding: Kirigami.ListSectionHeader (the first thing on the
// page) already carries its own top padding, and stacking ours on top
// of that left an oversized gap above "Appearance".
bottomPadding: Kirigami.Units.gridUnit
ColumnLayout {
width: root.width
spacing: 0
Kirigami.ListSectionHeader {
Layout.fillWidth: true
text: i18nc("@title:group", "Appearance")
}
ColumnLayout {
Layout.fillWidth: true
Layout.leftMargin: Kirigami.Units.largeSpacing
Layout.rightMargin: Kirigami.Units.largeSpacing
Layout.topMargin: Kirigami.Units.smallSpacing
Layout.bottomMargin: Kirigami.Units.smallSpacing
spacing: Kirigami.Units.smallSpacing
QQC2.Label {
Layout.fillWidth: true
text: i18nc("@label:listbox", "Color scheme")
}
QQC2.ComboBox {
id: colorSchemeCombo
Layout.fillWidth: true
model: AppColorScheme.colorSchemesModel
textRole: "display"
delegate: QQC2.ItemDelegate {
id: schemeDelegate
required property var model
width: colorSchemeCombo.width
icon.source: "image://colorScheme/" + schemeDelegate.model.display
icon.color: "transparent"
text: schemeDelegate.model.display
highlighted: schemeDelegate.model.display === AppColorScheme.activeColorSchemeName
onClicked: {
AppColorScheme.activeColorSchemeName = schemeDelegate.model.display;
colorSchemeCombo.popup.close();
}
}
// Keep the closed-box label in sync without fighting the popup's own selection state.
displayText: AppColorScheme.activeColorSchemeName
}
}
}
}