summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md1
-rw-r--r--config/Config.qml1
-rw-r--r--config/LauncherConfig.qml1
-rw-r--r--modules/controlcenter/launcher/LauncherPane.qml92
-rw-r--r--modules/drawers/Drawers.qml16
-rw-r--r--modules/launcher/items/AppItem.qml19
-rw-r--r--modules/launcher/services/Apps.qml3
-rw-r--r--plugin/src/Caelestia/appdb.cpp49
-rw-r--r--plugin/src/Caelestia/appdb.hpp10
-rw-r--r--utils/Strings.qml20
10 files changed, 180 insertions, 32 deletions
diff --git a/README.md b/README.md
index 25f5a27..0c3ca82 100644
--- a/README.md
+++ b/README.md
@@ -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;
+ }
+}