diff options
| -rw-r--r-- | README.md | 1 | ||||
| -rw-r--r-- | config/Config.qml | 1 | ||||
| -rw-r--r-- | config/LauncherConfig.qml | 1 | ||||
| -rw-r--r-- | modules/controlcenter/launcher/LauncherPane.qml | 92 | ||||
| -rw-r--r-- | modules/drawers/Drawers.qml | 16 | ||||
| -rw-r--r-- | modules/launcher/items/AppItem.qml | 19 | ||||
| -rw-r--r-- | modules/launcher/services/Apps.qml | 3 | ||||
| -rw-r--r-- | plugin/src/Caelestia/appdb.cpp | 49 | ||||
| -rw-r--r-- | plugin/src/Caelestia/appdb.hpp | 10 | ||||
| -rw-r--r-- | utils/Strings.qml | 20 |
10 files changed, 180 insertions, 32 deletions
@@ -562,6 +562,7 @@ default, you must create it manually. "wallpapers": false }, "showOnHover": false, + "favouriteApps": [], "hiddenApps": [] }, "lock": { diff --git a/config/Config.qml b/config/Config.qml index 74e3f45..7851c3b 100644 --- a/config/Config.qml +++ b/config/Config.qml @@ -297,6 +297,7 @@ Singleton { enableDangerousActions: launcher.enableDangerousActions, dragThreshold: launcher.dragThreshold, vimKeybinds: launcher.vimKeybinds, + favouriteApps: launcher.favouriteApps, hiddenApps: launcher.hiddenApps, useFuzzy: { apps: launcher.useFuzzy.apps, diff --git a/config/LauncherConfig.qml b/config/LauncherConfig.qml index 7f9c788..d9e3a73 100644 --- a/config/LauncherConfig.qml +++ b/config/LauncherConfig.qml @@ -10,6 +10,7 @@ JsonObject { property bool enableDangerousActions: false // Allow actions that can cause losing data, like shutdown, reboot and logout property int dragThreshold: 50 property bool vimKeybinds: false + property list<string> favouriteApps: [] property list<string> hiddenApps: [] property UseFuzzy useFuzzy: UseFuzzy {} property Sizes sizes: Sizes {} diff --git a/modules/controlcenter/launcher/LauncherPane.qml b/modules/controlcenter/launcher/LauncherPane.qml index 0dd464f..b236cf9 100644 --- a/modules/controlcenter/launcher/LauncherPane.qml +++ b/modules/controlcenter/launcher/LauncherPane.qml @@ -24,6 +24,7 @@ Item { property var selectedApp: root.session.launcher.active property bool hideFromLauncherChecked: false + property bool favouriteChecked: false anchors.fill: parent @@ -43,16 +44,14 @@ Item { function updateToggleState() { if (!root.selectedApp) { root.hideFromLauncherChecked = false; + root.favouriteChecked = false; return; } const appId = root.selectedApp.id || root.selectedApp.entry?.id; - if (Config.launcher.hiddenApps && Config.launcher.hiddenApps.length > 0) { - root.hideFromLauncherChecked = Config.launcher.hiddenApps.includes(appId); - } else { - root.hideFromLauncherChecked = false; - } + root.hideFromLauncherChecked = Config.launcher.hiddenApps && Config.launcher.hiddenApps.length > 0 && Strings.testRegexList(Config.launcher.hiddenApps, appId); + root.favouriteChecked = Config.launcher.favouriteApps && Config.launcher.favouriteApps.length > 0 && Strings.testRegexList(Config.launcher.favouriteApps, appId); } function saveHiddenApps(isHidden) { @@ -83,6 +82,7 @@ Item { id: allAppsDb path: `${Paths.state}/apps.sqlite` + favouriteApps: Config.launcher.favouriteApps entries: DesktopEntries.applications.values } @@ -286,6 +286,7 @@ Item { id: appsListLoader Layout.fillWidth: true Layout.fillHeight: true + asynchronous: true active: true sourceComponent: StyledListView { @@ -305,7 +306,8 @@ Item { delegate: StyledRect { required property var modelData - width: parent ? parent.width : 0 + width: parent ? parent.width : 0 + implicitHeight: 40 readonly property bool isSelected: root.selectedApp === modelData @@ -353,9 +355,34 @@ Item { text: modelData.name || modelData.entry?.name || qsTr("Unknown") font.pointSize: Appearance.font.size.normal } - } - implicitHeight: 40 + Loader { + Layout.alignment: Qt.AlignVCenter + readonly property bool isHidden: modelData ? Strings.testRegexList(Config.launcher.hiddenApps, modelData.id) : false + readonly property bool isFav: modelData ? Strings.testRegexList(Config.launcher.favouriteApps, modelData.id) : false + active: isHidden || isFav + + sourceComponent: isHidden ? hiddenIcon : (isFav ? favouriteIcon : null) + } + + Component { + id: hiddenIcon + MaterialIcon { + text: "visibility_off" + fill: 1 + color: Colours.palette.m3primary + } + } + + Component { + id: favouriteIcon + MaterialIcon { + text: "favorite" + fill: 1 + color: Colours.palette.m3primary + } + } + } } } } @@ -440,13 +467,11 @@ Item { onDisplayedAppChanged: { if (displayedApp) { const appId = displayedApp.id || displayedApp.entry?.id; - if (Config.launcher.hiddenApps && Config.launcher.hiddenApps.length > 0) { - root.hideFromLauncherChecked = Config.launcher.hiddenApps.includes(appId); - } else { - root.hideFromLauncherChecked = false; - } + root.hideFromLauncherChecked = Config.launcher.hiddenApps && Config.launcher.hiddenApps.length > 0 && Strings.testRegexList(Config.launcher.hiddenApps, appId); + root.favouriteChecked = Config.launcher.favouriteApps && Config.launcher.favouriteApps.length > 0 && Strings.testRegexList(Config.launcher.favouriteApps, appId); } else { root.hideFromLauncherChecked = false; + root.favouriteChecked = false; } } } @@ -562,9 +587,48 @@ Item { SwitchRow { Layout.topMargin: Appearance.spacing.normal visible: appDetailsLayout.displayedApp !== null + label: qsTr("Mark as favourite") + checked: root.favouriteChecked + // disabled if: + // * app is hidden + // * app isn't in favouriteApps array but marked as favourite anyway + // ^^^ This means that this app is favourited because of a regex check + // this button can not toggle regexed apps + enabled: appDetailsLayout.displayedApp !== null && !root.hideFromLauncherChecked && (Config.launcher.favouriteApps.indexOf(appDetailsLayout.displayedApp.id || appDetailsLayout.displayedApp.entry?.id) !== -1 || !root.favouriteChecked) + opacity: enabled ? 1 : 0.6 + onToggled: checked => { + root.favouriteChecked = checked; + const app = appDetailsLayout.displayedApp; + if (app) { + const appId = app.id || app.entry?.id; + const favouriteApps = Config.launcher.favouriteApps ? [...Config.launcher.favouriteApps] : []; + if (checked) { + if (!favouriteApps.includes(appId)) { + favouriteApps.push(appId); + } + } else { + const index = favouriteApps.indexOf(appId); + if (index !== -1) { + favouriteApps.splice(index, 1); + } + } + Config.launcher.favouriteApps = favouriteApps; + Config.save(); + } + } + } + SwitchRow { + Layout.topMargin: Appearance.spacing.normal + visible: appDetailsLayout.displayedApp !== null label: qsTr("Hide from launcher") checked: root.hideFromLauncherChecked - enabled: appDetailsLayout.displayedApp !== null + // disabled if: + // * app is favourited + // * app isn't in hiddenApps array but marked as hidden anyway + // ^^^ This means that this app is hidden because of a regex check + // this button can not toggle regexed apps + enabled: appDetailsLayout.displayedApp !== null && !root.favouriteChecked && (Config.launcher.hiddenApps.indexOf(appDetailsLayout.displayedApp.id || appDetailsLayout.displayedApp.entry?.id) !== -1 || !root.hideFromLauncherChecked) + opacity: enabled ? 1 : 0.6 onToggled: checked => { root.hideFromLauncherChecked = checked; const app = appDetailsLayout.displayedApp; diff --git a/modules/drawers/Drawers.qml b/modules/drawers/Drawers.qml index 00f9596..93534ec 100644 --- a/modules/drawers/Drawers.qml +++ b/modules/drawers/Drawers.qml @@ -4,6 +4,7 @@ import qs.components import qs.components.containers import qs.services import qs.config +import qs.utils import qs.modules.bar import Quickshell import Quickshell.Wayland @@ -18,20 +19,7 @@ Variants { id: scope required property ShellScreen modelData - readonly property bool barDisabled: { - const regexChecker = /^\^.*\$$/; - for (const filter of Config.bar.excludedScreens) { - // If filter is a regex - if (regexChecker.test(filter)) { - if ((new RegExp(filter)).test(modelData.name)) - return true; - } else { - if (filter === modelData.name) - return true; - } - } - return false; - } + readonly property bool barDisabled: Strings.testRegexList(Config.bar.excludedScreens, modelData.name) Exclusions { screen: scope.modelData diff --git a/modules/launcher/items/AppItem.qml b/modules/launcher/items/AppItem.qml index 48aace7..2bd818d 100644 --- a/modules/launcher/items/AppItem.qml +++ b/modules/launcher/items/AppItem.qml @@ -2,6 +2,7 @@ import "../services" import qs.components import qs.services import qs.config +import qs.utils import Quickshell import Quickshell.Widgets import QtQuick @@ -46,7 +47,7 @@ Item { anchors.leftMargin: Appearance.spacing.normal anchors.verticalCenter: icon.verticalCenter - implicitWidth: parent.width - icon.width + implicitWidth: parent.width - icon.width - favouriteIcon.width implicitHeight: name.implicitHeight + comment.implicitHeight StyledText { @@ -64,10 +65,24 @@ Item { color: Colours.palette.m3outline elide: Text.ElideRight - width: root.width - icon.width - Appearance.rounding.normal * 2 + width: root.width - icon.width - favouriteIcon.width - Appearance.rounding.normal * 2 anchors.top: name.bottom } } + + Loader { + id: favouriteIcon + + anchors.verticalCenter: parent.verticalCenter + anchors.right: parent.right + active: modelData && Strings.testRegexList(Config.launcher.favouriteApps, modelData.id) + + sourceComponent: MaterialIcon { + text: "favorite" + fill: 1 + color: Colours.palette.m3primary + } + } } } diff --git a/modules/launcher/services/Apps.qml b/modules/launcher/services/Apps.qml index c409a7b..7f2d645 100644 --- a/modules/launcher/services/Apps.qml +++ b/modules/launcher/services/Apps.qml @@ -72,6 +72,7 @@ Searcher { id: appDb path: `${Paths.state}/apps.sqlite` - entries: DesktopEntries.applications.values.filter(a => !Config.launcher.hiddenApps.includes(a.id)) + favouriteApps: Config.launcher.favouriteApps + entries: DesktopEntries.applications.values.filter(a => !Strings.testRegexList(Config.launcher.hiddenApps, a.id)) } } diff --git a/plugin/src/Caelestia/appdb.cpp b/plugin/src/Caelestia/appdb.cpp index 6e37e16..b074cf4 100644 --- a/plugin/src/Caelestia/appdb.cpp +++ b/plugin/src/Caelestia/appdb.cpp @@ -162,6 +162,39 @@ void AppDb::setEntries(const QObjectList& entries) { m_timer->start(); } +QStringList AppDb::favouriteApps() const { + return m_favouriteApps; +} + +void AppDb::setFavouriteApps(const QStringList& favApps) { + if (m_favouriteApps == favApps) { + return; + } + + m_favouriteApps = favApps; + emit favouriteAppsChanged(); + m_favouriteAppsRegex.clear(); + m_favouriteAppsRegex.reserve(m_favouriteApps.size()); + for (const QString& item : std::as_const(m_favouriteApps)) { + const QRegularExpression re(regexifyString(item)); + if (re.isValid()) { + m_favouriteAppsRegex << re; + } else { + qWarning() << "AppDb::setFavouriteApps: Regular expression is not valid: " << re.pattern(); + } + } + + emit appsChanged(); +} + +QString AppDb::regexifyString(const QString& original) const { + if (original.startsWith('^') && original.endsWith('$')) + return original; + + const QString escaped = QRegularExpression::escape(original); + return QStringLiteral("^%1$").arg(escaped); +} + QQmlListProperty<AppEntry> AppDb::apps() { return QQmlListProperty<AppEntry>(this, &getSortedApps()); } @@ -192,7 +225,12 @@ void AppDb::incrementFrequency(const QString& id) { QList<AppEntry*>& AppDb::getSortedApps() const { m_sortedApps = m_apps.values(); - std::sort(m_sortedApps.begin(), m_sortedApps.end(), [](AppEntry* a, AppEntry* b) { + std::sort(m_sortedApps.begin(), m_sortedApps.end(), [this](AppEntry* a, AppEntry* b) { + bool aIsFav = isFavourite(a); + bool bIsFav = isFavourite(b); + if (aIsFav != bIsFav) { + return aIsFav; + } if (a->frequency() != b->frequency()) { return a->frequency() > b->frequency(); } @@ -201,6 +239,15 @@ QList<AppEntry*>& AppDb::getSortedApps() const { return m_sortedApps; } +bool AppDb::isFavourite(const AppEntry* app) const { + for (const QRegularExpression& re : m_favouriteAppsRegex) { + if (re.match(app->id()).hasMatch()) { + return true; + } + } + return false; +} + quint32 AppDb::getFrequency(const QString& id) const { auto db = QSqlDatabase::database(m_uuid); QSqlQuery query(db); diff --git a/plugin/src/Caelestia/appdb.hpp b/plugin/src/Caelestia/appdb.hpp index 5f9b960..ce5f270 100644 --- a/plugin/src/Caelestia/appdb.hpp +++ b/plugin/src/Caelestia/appdb.hpp @@ -4,6 +4,7 @@ #include <qobject.h> #include <qqmlintegration.h> #include <qqmllist.h> +#include <qregularexpression.h> #include <qtimer.h> namespace caelestia { @@ -66,6 +67,7 @@ class AppDb : public QObject { Q_PROPERTY(QString uuid READ uuid CONSTANT) Q_PROPERTY(QString path READ path WRITE setPath NOTIFY pathChanged REQUIRED) Q_PROPERTY(QObjectList entries READ entries WRITE setEntries NOTIFY entriesChanged REQUIRED) + Q_PROPERTY(QStringList favouriteApps READ favouriteApps WRITE setFavouriteApps NOTIFY favouriteAppsChanged REQUIRED) Q_PROPERTY(QQmlListProperty<caelestia::AppEntry> apps READ apps NOTIFY appsChanged) public: @@ -79,6 +81,9 @@ public: [[nodiscard]] QObjectList entries() const; void setEntries(const QObjectList& entries); + [[nodiscard]] QStringList favouriteApps() const; + void setFavouriteApps(const QStringList& favApps); + [[nodiscard]] QQmlListProperty<AppEntry> apps(); Q_INVOKABLE void incrementFrequency(const QString& id); @@ -86,6 +91,7 @@ public: signals: void pathChanged(); void entriesChanged(); + void favouriteAppsChanged(); void appsChanged(); private: @@ -94,10 +100,14 @@ private: const QString m_uuid; QString m_path; QObjectList m_entries; + QStringList m_favouriteApps; // unedited string list from qml + QList<QRegularExpression> m_favouriteAppsRegex; // pre-regexified m_favouriteApps list QHash<QString, AppEntry*> m_apps; mutable QList<AppEntry*> m_sortedApps; + QString regexifyString(const QString& original) const; QList<AppEntry*>& getSortedApps() const; + bool isFavourite(const AppEntry* app) const; quint32 getFrequency(const QString& id) const; void updateAppFrequencies(); void updateApps(); diff --git a/utils/Strings.qml b/utils/Strings.qml new file mode 100644 index 0000000..1d0cc76 --- /dev/null +++ b/utils/Strings.qml @@ -0,0 +1,20 @@ +pragma Singleton + +import Quickshell + +Singleton { + function testRegexList(filterList: list<string>, target: string): bool { + const regexChecker = /^\^.*\$$/; + for (const filter of filterList) { + // If filter is a regex + if (regexChecker.test(filter)) { + if ((new RegExp(filter)).test(target)) + return true; + } else { + if (filter === target) + return true; + } + } + return false; + } +} |