launcher: add favorite apps (#946)
* launcher: add favorite apps Favorite apps always appear above non-favorite apps Accepts regex, same logic as #920 Added the same regex logic to hidden apps Added util file may need to be relocated * addressed requested changes * fix: Renamed newly added util singleton Also added a null check to favorite icon loader in AppItem.qml * controlCenter/launcherPane: added favorite apps added icons to the app list to indicate if they are favorited/hidden marking as favorite/hidden is desabled if the other is selected * favouriteApps: renamed from favorite to favourite Also disabled favorite/hidden switch for entries added as regex * appDb: added notify and emit to favoriteApps * controlCentre/Launcher: Fixed bug with favourite switch not enabling itself when no hiddenApps exist Added a comment to explain the enabled state of the switches icon loader is now a single loader rather than two, hidden icon has priority * spelling mistakes * fixed warning * formatting fixes
This commit is contained in:
parent
3a7309294c
commit
40a2552830
10 changed files with 180 additions and 32 deletions
|
|
@ -562,6 +562,7 @@ default, you must create it manually.
|
|||
"wallpapers": false
|
||||
},
|
||||
"showOnHover": false,
|
||||
"favouriteApps": [],
|
||||
"hiddenApps": []
|
||||
},
|
||||
"lock": {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 {}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
@ -306,6 +307,7 @@ Item {
|
|||
required property var modelData
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
implicitHeight: 40
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -559,12 +584,51 @@ Item {
|
|||
anchors.top: parent.top
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
20
utils/Strings.qml
Normal file
20
utils/Strings.qml
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue