Refactoring
This commit is contained in:
parent
24ac5dc0e6
commit
1355eafd48
@ -1,18 +1,15 @@
|
||||
#include "AppListModel.hpp"
|
||||
#include "DesktopAppListItem.hpp"
|
||||
#include <QDebug>
|
||||
#include <QProcess>
|
||||
#include <QRegularExpression>
|
||||
#include <QIcon>
|
||||
#include <QPixmap>
|
||||
#include <QUrl>
|
||||
#include <QFile>
|
||||
#include <QStandardPaths>
|
||||
#include <QDir>
|
||||
|
||||
#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<int>(m_filteredIndexes.size()))
|
||||
if (!index.isValid() || index.row() >= static_cast<int>(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<int>(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<int, QByteArray> AppListModel::roleNames() const
|
||||
{
|
||||
QHash<int, QByteArray> 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<int>(m_filteredIndexes.size()))
|
||||
if (index < 0 || index >= static_cast<int>(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<int>(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<int>(i));
|
||||
}
|
||||
for (const auto &entry : *apps) {
|
||||
auto item = std::make_shared<DesktopAppListItem>(entry);
|
||||
m_items.push_back(item);
|
||||
}
|
||||
}
|
||||
|
||||
QUrl AppListModel::getIconUrl(const dmenu::DesktopEntry &app) const
|
||||
void AppListModel::addItems(const std::vector<ListItemPtr> &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<int>(i));
|
||||
}
|
||||
}
|
||||
|
||||
return QUrl();
|
||||
}
|
@ -2,10 +2,8 @@
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QQmlEngine>
|
||||
|
||||
#undef signals
|
||||
#include "../dmenu.hpp"
|
||||
#define signals public
|
||||
#include "ListItem.hpp"
|
||||
#include <vector>
|
||||
|
||||
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<int, QByteArray> 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<ListItemPtr> &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<ListItemPtr> m_items;
|
||||
std::vector<int> m_filteredIndexes;
|
||||
QString m_searchText;
|
||||
};
|
97
lib/ui/DesktopAppListItem.cpp
Normal file
97
lib/ui/DesktopAppListItem.cpp
Normal file
@ -0,0 +1,97 @@
|
||||
#include "DesktopAppListItem.hpp"
|
||||
#include <QProcess>
|
||||
#include <QRegularExpression>
|
||||
#include <QIcon>
|
||||
#include <QStandardPaths>
|
||||
#include <QFile>
|
||||
#include <QDebug>
|
||||
|
||||
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();
|
||||
}
|
25
lib/ui/DesktopAppListItem.hpp
Normal file
25
lib/ui/DesktopAppListItem.hpp
Normal file
@ -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;
|
||||
};
|
15
lib/ui/ListItem.cpp
Normal file
15
lib/ui/ListItem.cpp
Normal file
@ -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);
|
||||
}
|
31
lib/ui/ListItem.hpp
Normal file
31
lib/ui/ListItem.hpp
Normal file
@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
#include <memory>
|
||||
|
||||
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<ListItem>;
|
@ -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"
|
||||
|
@ -3,9 +3,13 @@
|
||||
#include <QIcon>
|
||||
#include <QWindow>
|
||||
#include <LayerShellQt/window.h>
|
||||
#include "ui/AppListModel.hpp"
|
||||
|
||||
#undef signals
|
||||
#include "dmenu.hpp"
|
||||
#include "files.hpp"
|
||||
#define signals public
|
||||
|
||||
#include "ui/AppListModel.hpp"
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
#include <iostream>
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user