From 1355eafd483ed16af54ca1b02241b180110cf064 Mon Sep 17 00:00:00 2001 From: Javier Feliz Date: Thu, 28 Aug 2025 14:50:26 -0400 Subject: [PATCH] Refactoring --- lib/ui/AppListModel.cpp | 149 ++++++++++++---------------------- lib/ui/AppListModel.hpp | 27 +++--- lib/ui/DesktopAppListItem.cpp | 97 ++++++++++++++++++++++ lib/ui/DesktopAppListItem.hpp | 25 ++++++ lib/ui/ListItem.cpp | 15 ++++ lib/ui/ListItem.hpp | 31 +++++++ shell.nix | 9 +- src/main.cpp | 6 +- ui/Main.qml | 4 +- 9 files changed, 250 insertions(+), 113 deletions(-) create mode 100644 lib/ui/DesktopAppListItem.cpp create mode 100644 lib/ui/DesktopAppListItem.hpp create mode 100644 lib/ui/ListItem.cpp create mode 100644 lib/ui/ListItem.hpp diff --git a/lib/ui/AppListModel.cpp b/lib/ui/AppListModel.cpp index 8d397f5..c57e0a8 100644 --- a/lib/ui/AppListModel.cpp +++ b/lib/ui/AppListModel.cpp @@ -1,18 +1,15 @@ #include "AppListModel.hpp" +#include "DesktopAppListItem.hpp" #include -#include -#include -#include -#include -#include -#include -#include -#include + +#undef signals +#include "../dmenu.hpp" +#define signals public AppListModel::AppListModel(QObject *parent) : QAbstractListModel(parent) { - loadApps(); + loadItems(); } int AppListModel::rowCount(const QModelIndex &parent) const @@ -23,21 +20,24 @@ int AppListModel::rowCount(const QModelIndex &parent) const QVariant AppListModel::data(const QModelIndex &index, int role) const { - if (!index.isValid() || !m_apps || index.row() >= static_cast(m_filteredIndexes.size())) + if (!index.isValid() || index.row() >= static_cast(m_filteredIndexes.size())) return QVariant(); - int appIndex = m_filteredIndexes[index.row()]; - const dmenu::DesktopEntry &app = (*m_apps)[appIndex]; + int itemIndex = m_filteredIndexes[index.row()]; + if (itemIndex >= static_cast(m_items.size())) + return QVariant(); + + const ListItemPtr &item = m_items[itemIndex]; switch (role) { case NameRole: - return QString::fromStdString(app.name); - case ExecRole: - return QString::fromStdString(app.exec); - case IdRole: - return QString::fromStdString(app.id); + return item->name(); + case DescriptionRole: + return item->description(); case IconRole: - return getIconUrl(app); + return item->iconUrl(); + case ItemTypeRole: + return item->itemType(); default: return QVariant(); } @@ -47,38 +47,34 @@ QHash AppListModel::roleNames() const { QHash roles; roles[NameRole] = "name"; - roles[ExecRole] = "exec"; - roles[IdRole] = "id"; + roles[DescriptionRole] = "description"; roles[IconRole] = "icon"; + roles[ItemTypeRole] = "itemType"; return roles; } -void AppListModel::loadApps() +void AppListModel::loadItems() { beginResetModel(); - m_apps = dmenu::get_dmenu_app_data(); - updateFilteredApps(); + m_items.clear(); + + // Add desktop applications + addDesktopApps(); + + updateFilteredItems(); endResetModel(); } -void AppListModel::launchApp(int index) +void AppListModel::executeItem(int index) { - if (!m_apps || index < 0 || index >= static_cast(m_filteredIndexes.size())) + if (index < 0 || index >= static_cast(m_filteredIndexes.size())) return; - int appIndex = m_filteredIndexes[index]; - const dmenu::DesktopEntry &app = (*m_apps)[appIndex]; - - // Parse exec command (remove %f, %u, %F, %U field codes if present) - QString command = QString::fromStdString(app.exec); - QRegularExpression fieldCodes("%[fuFU]"); - command = command.replace(fieldCodes, "").trimmed(); - - qDebug() << "Launching:" << command; - - // Use nohup and redirect output to /dev/null for proper detachment - QString detachedCommand = QString("nohup %1 >/dev/null 2>&1 &").arg(command); - QProcess::startDetached("/bin/sh", QStringList() << "-c" << detachedCommand); + int itemIndex = m_filteredIndexes[index]; + if (itemIndex >= static_cast(m_items.size())) + return; + + m_items[itemIndex]->execute(); } QString AppListModel::searchText() const @@ -95,75 +91,38 @@ void AppListModel::setSearchText(const QString &searchText) emit searchTextChanged(); beginResetModel(); - updateFilteredApps(); + updateFilteredItems(); endResetModel(); } -void AppListModel::updateFilteredApps() +void AppListModel::addDesktopApps() { - m_filteredIndexes.clear(); - - if (!m_apps) + dmenu::DEVec apps = dmenu::get_dmenu_app_data(); + if (!apps) return; - - for (size_t i = 0; i < m_apps->size(); ++i) { - const dmenu::DesktopEntry &app = (*m_apps)[i]; - if (m_searchText.isEmpty() || - QString::fromStdString(app.name).contains(m_searchText, Qt::CaseInsensitive)) { - m_filteredIndexes.push_back(static_cast(i)); - } + for (const auto &entry : *apps) { + auto item = std::make_shared(entry); + m_items.push_back(item); } } -QUrl AppListModel::getIconUrl(const dmenu::DesktopEntry &app) const +void AppListModel::addItems(const std::vector &items) { - QString iconName = QString::fromStdString(app.icon_path); - - if (iconName.isEmpty()) - return QUrl(); - - // If it's already a full path, use it directly - if (iconName.startsWith('/')) { - if (QFile::exists(iconName)) { - return QUrl::fromLocalFile(iconName); - } - return QUrl(); + for (const auto &item : items) { + m_items.push_back(item); } +} + +void AppListModel::updateFilteredItems() +{ + m_filteredIndexes.clear(); - // Use Qt's proper icon theme search which follows XDG spec - QIcon icon = QIcon::fromTheme(iconName); - bool themeFound = !icon.isNull(); - - // Qt doesn't expose the resolved file path directly, so let's use QStandardPaths - // to search in the proper system directories - QStringList dataDirs = QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation); - - // Common icon subdirectories and sizes (prioritized) - QStringList iconSubDirs = { - "icons/hicolor/scalable/apps", - "icons/hicolor/48x48/apps", - "icons/hicolor/64x64/apps", - "icons/hicolor/32x32/apps", - "icons/hicolor/128x128/apps", - "icons/Adwaita/scalable/apps", - "icons/Adwaita/48x48/apps", - "pixmaps" // This should search /path/to/share/pixmaps/ - }; - - QStringList extensions = {"", ".png", ".svg", ".xpm"}; - - for (const QString &dataDir : dataDirs) { - for (const QString &iconSubDir : iconSubDirs) { - QString basePath = dataDir + "/" + iconSubDir + "/"; - for (const QString &ext : extensions) { - QString fullPath = basePath + iconName + ext; - if (QFile::exists(fullPath)) { - return QUrl::fromLocalFile(fullPath); - } - } + for (size_t i = 0; i < m_items.size(); ++i) { + const ListItemPtr &item = m_items[i]; + + if (item->matches(m_searchText)) { + m_filteredIndexes.push_back(static_cast(i)); } } - - return QUrl(); } \ No newline at end of file diff --git a/lib/ui/AppListModel.hpp b/lib/ui/AppListModel.hpp index aaf719a..06dfdba 100644 --- a/lib/ui/AppListModel.hpp +++ b/lib/ui/AppListModel.hpp @@ -2,10 +2,8 @@ #include #include - -#undef signals -#include "../dmenu.hpp" -#define signals public +#include "ListItem.hpp" +#include class AppListModel : public QAbstractListModel { @@ -13,11 +11,11 @@ class AppListModel : public QAbstractListModel Q_PROPERTY(QString searchText READ searchText WRITE setSearchText NOTIFY searchTextChanged) public: - enum AppRoles { + enum ItemRoles { NameRole = Qt::UserRole + 1, - ExecRole, - IdRole, - IconRole + DescriptionRole, + IconRole, + ItemTypeRole }; explicit AppListModel(QObject *parent = nullptr); @@ -26,8 +24,12 @@ public: QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; QHash roleNames() const override; - Q_INVOKABLE void loadApps(); - Q_INVOKABLE void launchApp(int index); + Q_INVOKABLE void loadItems(); + Q_INVOKABLE void executeItem(int index); + + // Add items from different sources + void addDesktopApps(); + void addItems(const std::vector &items); QString searchText() const; void setSearchText(const QString &searchText); @@ -36,10 +38,9 @@ signals: void searchTextChanged(); private: - void updateFilteredApps(); - QUrl getIconUrl(const dmenu::DesktopEntry &app) const; + void updateFilteredItems(); - dmenu::DEVec m_apps; + std::vector m_items; std::vector m_filteredIndexes; QString m_searchText; }; \ No newline at end of file diff --git a/lib/ui/DesktopAppListItem.cpp b/lib/ui/DesktopAppListItem.cpp new file mode 100644 index 0000000..2dd2576 --- /dev/null +++ b/lib/ui/DesktopAppListItem.cpp @@ -0,0 +1,97 @@ +#include "DesktopAppListItem.hpp" +#include +#include +#include +#include +#include +#include + +DesktopAppListItem::DesktopAppListItem(const dmenu::DesktopEntry &entry) + : m_entry(entry) +{ +} + +QString DesktopAppListItem::name() const +{ + return QString::fromStdString(m_entry.name); +} + +QString DesktopAppListItem::description() const +{ + // Could add support for Comment field from desktop entry later + return QString::fromStdString(m_entry.exec); +} + +QUrl DesktopAppListItem::iconUrl() const +{ + return resolveIconUrl(m_entry.icon_path); +} + +void DesktopAppListItem::execute() +{ + // Parse exec command (remove %f, %u, %F, %U field codes if present) + QString command = QString::fromStdString(m_entry.exec); + QRegularExpression fieldCodes("%[fuFU]"); + command = command.replace(fieldCodes, "").trimmed(); + + // Use nohup and redirect output to /dev/null for proper detachment + QString detachedCommand = QString("nohup %1 >/dev/null 2>&1 &").arg(command); + QProcess::startDetached("/bin/sh", QStringList() << "-c" << detachedCommand); +} + +QString DesktopAppListItem::itemType() const +{ + return "app"; +} + +QUrl DesktopAppListItem::resolveIconUrl(const std::string &iconPath) const +{ + QString iconName = QString::fromStdString(iconPath); + + if (iconName.isEmpty()) + return QUrl(); + + // If it's already a full path, use it directly + if (iconName.startsWith('/')) { + if (QFile::exists(iconName)) { + return QUrl::fromLocalFile(iconName); + } + return QUrl(); + } + + // Use Qt's proper icon theme search which follows XDG spec + QIcon icon = QIcon::fromTheme(iconName); + bool themeFound = !icon.isNull(); + + // Qt doesn't expose the resolved file path directly, so let's use QStandardPaths + // to search in the proper system directories + QStringList dataDirs = QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation); + + // Common icon subdirectories and sizes (prioritized) + QStringList iconSubDirs = { + "icons/hicolor/scalable/apps", + "icons/hicolor/48x48/apps", + "icons/hicolor/64x64/apps", + "icons/hicolor/32x32/apps", + "icons/hicolor/128x128/apps", + "icons/Adwaita/scalable/apps", + "icons/Adwaita/48x48/apps", + "pixmaps" // This should search /path/to/share/pixmaps/ + }; + + QStringList extensions = {"", ".png", ".svg", ".xpm"}; + + for (const QString &dataDir : dataDirs) { + for (const QString &iconSubDir : iconSubDirs) { + QString basePath = dataDir + "/" + iconSubDir + "/"; + for (const QString &ext : extensions) { + QString fullPath = basePath + iconName + ext; + if (QFile::exists(fullPath)) { + return QUrl::fromLocalFile(fullPath); + } + } + } + } + + return QUrl(); +} \ No newline at end of file diff --git a/lib/ui/DesktopAppListItem.hpp b/lib/ui/DesktopAppListItem.hpp new file mode 100644 index 0000000..d84813f --- /dev/null +++ b/lib/ui/DesktopAppListItem.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include "ListItem.hpp" + +#undef signals +#include "../dmenu.hpp" +#define signals public + +class DesktopAppListItem : public ListItem +{ +public: + explicit DesktopAppListItem(const dmenu::DesktopEntry &entry); + + // ListItem interface + QString name() const override; + QString description() const override; + QUrl iconUrl() const override; + void execute() override; + QString itemType() const override; + +private: + QUrl resolveIconUrl(const std::string &iconPath) const; + + dmenu::DesktopEntry m_entry; +}; \ No newline at end of file diff --git a/lib/ui/ListItem.cpp b/lib/ui/ListItem.cpp new file mode 100644 index 0000000..8c3635a --- /dev/null +++ b/lib/ui/ListItem.cpp @@ -0,0 +1,15 @@ +#include "ListItem.hpp" + +bool ListItem::matches(const QString &query) const +{ + return defaultMatches(query); +} + +bool ListItem::defaultMatches(const QString &query) const +{ + if (query.isEmpty()) + return true; + + return name().contains(query, Qt::CaseInsensitive) || + description().contains(query, Qt::CaseInsensitive); +} \ No newline at end of file diff --git a/lib/ui/ListItem.hpp b/lib/ui/ListItem.hpp new file mode 100644 index 0000000..79a3f36 --- /dev/null +++ b/lib/ui/ListItem.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include +#include +#include + +class ListItem +{ +public: + virtual ~ListItem() = default; + + // Display properties for the UI + virtual QString name() const = 0; + virtual QString description() const = 0; // Optional subtitle/description + virtual QUrl iconUrl() const = 0; + + // Action when item is selected + virtual void execute() = 0; + + // Search matching - return true if this item matches the search query + virtual bool matches(const QString &query) const; + + // Item type for extensibility (e.g., "app", "file", "bookmark") + virtual QString itemType() const = 0; + +protected: + // Helper for default search matching (case-insensitive name search) + bool defaultMatches(const QString &query) const; +}; + +using ListItemPtr = std::shared_ptr; \ No newline at end of file diff --git a/shell.nix b/shell.nix index a4c83a5..1eaac82 100644 --- a/shell.nix +++ b/shell.nix @@ -20,14 +20,19 @@ pkgs.mkShell { pkgs.kdePackages.layer-shell-qt # <- provides layer shell support pkgs.kdePackages.layer-shell-qt.dev # <- provides headers pkgs.pkg-config - # qt.qttools - # qt.qtshadertools + + # THEMING + pkgs.qt6ct + pkgs.kdePackages.qqc2-desktop-style + pkgs.kdePackages.breeze-icons + pkgs.hicolor-icon-theme ]; shellHook = '' export QT_PLUGIN_PATH='${qt.full}' export QML2_IMPORT_PATH='${qt.full}:${pkgs.kdePackages.layer-shell-qt}/lib/qt-6/qml' export QT_QPA_PLATFORM=wayland + export QT_QPA_PLATFORMTHEME=qt6ct echo "--------------------------------------------" echo "QT_PLUGIN_PATH: $QT_PLUGIN_PATH" echo "QML2_IMPORT_PATH: $QML2_IMPORT_PATH" diff --git a/src/main.cpp b/src/main.cpp index 59f340a..d2341e6 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -3,9 +3,13 @@ #include #include #include -#include "ui/AppListModel.hpp" + +#undef signals #include "dmenu.hpp" #include "files.hpp" +#define signals public + +#include "ui/AppListModel.hpp" #include #include #include diff --git a/ui/Main.qml b/ui/Main.qml index efbfd95..ddabbff 100644 --- a/ui/Main.qml +++ b/ui/Main.qml @@ -50,7 +50,7 @@ ApplicationWindow { Keys.onUpPressed: listView.decrementCurrentIndex() Keys.onReturnPressed: { if (listView.currentItem) { - appModel.launchApp(listView.currentIndex) + appModel.executeItem(listView.currentIndex) Qt.quit() } } @@ -119,7 +119,7 @@ ApplicationWindow { onClicked: { listView.currentIndex = index - appModel.launchApp(index) + appModel.executeItem(index) Qt.quit() } }