mirror of https://github.com/cutefishos/launcher
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
319 lines
8.6 KiB
C++
319 lines
8.6 KiB
C++
/*
|
|
* Copyright (C) 2021 CutefishOS.
|
|
*
|
|
* Author: revenmartin <revenmartin@gmail.com>
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include "launchermodel.h"
|
|
#include "desktopproperties.h"
|
|
#include "launcheritem.h"
|
|
|
|
#include <QtConcurrent/QtConcurrentRun>
|
|
#include <QRegularExpression>
|
|
#include <QFileSystemWatcher>
|
|
#include <QScopedPointer>
|
|
#include <QDirIterator>
|
|
#include <QProcess>
|
|
#include <QDebug>
|
|
#include <QIcon>
|
|
#include <QDir>
|
|
|
|
static QByteArray detectDesktopEnvironment()
|
|
{
|
|
const QByteArray desktop = qgetenv("XDG_CURRENT_DESKTOP");
|
|
|
|
if (!desktop.isEmpty())
|
|
return desktop.toUpper();
|
|
|
|
return QByteArray("UNKNOWN");
|
|
}
|
|
|
|
LauncherModel::LauncherModel(QObject *parent)
|
|
: QAbstractListModel(parent),
|
|
m_mode(NormalMode)
|
|
{
|
|
QtConcurrent::run(LauncherModel::refresh, this);
|
|
|
|
QFileSystemWatcher *watcher = new QFileSystemWatcher(this);
|
|
watcher->addPath("/usr/share/applications");
|
|
connect(watcher, &QFileSystemWatcher::directoryChanged, this, [this](const QString &) {
|
|
QtConcurrent::run(LauncherModel::refresh, this);
|
|
});
|
|
|
|
connect(this, &QAbstractItemModel::rowsInserted, this, &LauncherModel::countChanged);
|
|
connect(this, &QAbstractItemModel::rowsRemoved, this, &LauncherModel::countChanged);
|
|
connect(this, &QAbstractItemModel::modelReset, this, &LauncherModel::countChanged);
|
|
connect(this, &QAbstractItemModel::layoutChanged, this, &LauncherModel::countChanged);
|
|
}
|
|
|
|
LauncherModel::~LauncherModel()
|
|
{
|
|
while (!m_items.isEmpty())
|
|
delete m_items.takeFirst();
|
|
}
|
|
|
|
int LauncherModel::count() const
|
|
{
|
|
return rowCount();
|
|
}
|
|
|
|
int LauncherModel::rowCount(const QModelIndex &parent) const
|
|
{
|
|
Q_UNUSED(parent);
|
|
|
|
if (m_mode == SearchMode)
|
|
return m_searchItems.size();
|
|
|
|
return m_items.size();
|
|
}
|
|
|
|
QHash<int, QByteArray> LauncherModel::roleNames() const
|
|
{
|
|
QHash<int, QByteArray> roles;
|
|
roles.insert(AppIdRole, "appId");
|
|
roles.insert(ApplicationRole, "application");
|
|
roles.insert(NameRole, "name");
|
|
roles.insert(GenericNameRole, "genericName");
|
|
roles.insert(CommentRole, "comment");
|
|
roles.insert(IconNameRole, "iconName");
|
|
roles.insert(CategoriesRole, "categories");
|
|
roles.insert(FilterInfoRole, "filterInfo");
|
|
roles.insert(PinnedRole, "pinned");
|
|
roles.insert(PinnedIndexRole, "pinnedIndex");
|
|
return roles;
|
|
}
|
|
|
|
QVariant LauncherModel::data(const QModelIndex &index, int role) const
|
|
{
|
|
if (!index.isValid())
|
|
return QVariant();
|
|
|
|
LauncherItem *item = m_mode == NormalMode
|
|
? m_items.at(index.row())
|
|
: m_searchItems.at(index.row());
|
|
|
|
if (!item)
|
|
return QVariant();
|
|
|
|
switch (role) {
|
|
case AppIdRole:
|
|
return item->id;
|
|
case NameRole:
|
|
return item->name;
|
|
case IconNameRole:
|
|
return item->iconName;
|
|
case FilterInfoRole:
|
|
return QString(item->name + QStringLiteral(" ") + item->genericName + QStringLiteral(" ") + item->comment);
|
|
default:
|
|
return QVariant();
|
|
}
|
|
}
|
|
|
|
void LauncherModel::search(const QString &key)
|
|
{
|
|
m_mode = key.isEmpty() ? NormalMode : SearchMode;
|
|
m_searchItems.clear();
|
|
|
|
for (LauncherItem *item : qAsConst(m_items)) {
|
|
const QString &name = item->name;
|
|
const QString &fileName = item->id;
|
|
|
|
if (name.contains(key, Qt::CaseInsensitive) ||
|
|
fileName.contains(key, Qt::CaseInsensitive)) {
|
|
m_searchItems.append(item);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
emit layoutChanged();
|
|
}
|
|
|
|
LauncherItem *LauncherModel::findApplication(const QString &appId)
|
|
{
|
|
for (LauncherItem *item : qAsConst(m_items)) {
|
|
if (item->id == appId)
|
|
return item;
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
void LauncherModel::refresh(LauncherModel *manager)
|
|
{
|
|
QStringList addedEntries;
|
|
for (LauncherItem *item : qAsConst(manager->m_items))
|
|
addedEntries.append(item->id);
|
|
|
|
QStringList allEntries;
|
|
QDirIterator it("/usr/share/applications", { "*.desktop" }, QDir::NoFilter, QDirIterator::Subdirectories);
|
|
|
|
while (it.hasNext()) {
|
|
const auto fileName = it.next();
|
|
if (!QFile::exists(fileName))
|
|
continue;
|
|
|
|
allEntries.append(fileName);
|
|
}
|
|
|
|
for (const QString &fileName : allEntries) {
|
|
if (!addedEntries.contains(fileName))
|
|
QMetaObject::invokeMethod(manager, "addApp", Q_ARG(QString, fileName));
|
|
}
|
|
|
|
for (LauncherItem *item : qAsConst(manager->m_items)) {
|
|
if (!allEntries.contains(item->id))
|
|
QMetaObject::invokeMethod(manager, "removeApp", Q_ARG(QObject*, qobject_cast<QObject*>(item)));
|
|
}
|
|
|
|
// Signal the model was refreshed
|
|
QMetaObject::invokeMethod(manager, "refreshed");
|
|
}
|
|
|
|
LauncherItem *LauncherModel::get(int index) const
|
|
{
|
|
if (index < 0 || index >= m_items.size())
|
|
return nullptr;
|
|
|
|
return m_items.at(index);
|
|
}
|
|
|
|
QString LauncherModel::getIconName(const QString &appId)
|
|
{
|
|
LauncherItem *item = get(indexFromAppId(appId));
|
|
if (item)
|
|
return item->iconName;
|
|
|
|
return QString();
|
|
}
|
|
|
|
int LauncherModel::indexFromAppId(const QString &appId) const
|
|
{
|
|
for (int i = 0; i < m_items.size(); i++) {
|
|
if (m_items.at(i)->id == appId)
|
|
return i;
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
void LauncherModel::move(int from, int to)
|
|
{
|
|
if (from == to)
|
|
return;
|
|
|
|
m_items.move(from, to);
|
|
|
|
if (from < to)
|
|
beginMoveRows(QModelIndex(), from, from, QModelIndex(), to + 1);
|
|
else
|
|
beginMoveRows(QModelIndex(), from, from, QModelIndex(), to);
|
|
|
|
endMoveRows();
|
|
}
|
|
|
|
bool LauncherModel::launch(const QString &path)
|
|
{
|
|
LauncherItem *app = findApplication(path);
|
|
if (app) {
|
|
QStringList args = app->args;
|
|
QScopedPointer<QProcess> p(new QProcess);
|
|
p->setStandardInputFile(QProcess::nullDevice());
|
|
p->setProcessChannelMode(QProcess::ForwardedChannels);
|
|
|
|
QString cmd = args.takeFirst();
|
|
p->setProgram(cmd);
|
|
p->setArguments(args);
|
|
|
|
// Because launcher has hidden animation,
|
|
// cutefish-screenshot needs to be processed.
|
|
if (cmd == "cutefish-screenshot") {
|
|
p->setArguments(QStringList() << "-d" << "200");
|
|
}
|
|
|
|
Q_EMIT applicationLaunched();
|
|
|
|
return p->startDetached();
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void LauncherModel::addApp(const QString &fileName)
|
|
{
|
|
if (findApplication(fileName))
|
|
return;
|
|
|
|
DesktopProperties desktop(fileName, "Desktop Entry");
|
|
|
|
if (desktop.contains("Terminal") && desktop.value("Terminal").toBool())
|
|
return;
|
|
|
|
if (desktop.contains("OnlyShowIn")) {
|
|
const QString &value = desktop.value("OnlyShowIn").toString();
|
|
if (!value.contains(detectDesktopEnvironment(), Qt::CaseInsensitive)) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (desktop.value("NoDisplay").toBool() ||
|
|
desktop.value("Hidden").toBool())
|
|
return;
|
|
|
|
QString appName = desktop.value(QString("Name[%1]").arg(QLocale::system().name())).toString();
|
|
QString appExec = desktop.value("Exec").toString();
|
|
|
|
if (appName.isEmpty())
|
|
appName = desktop.value("Name").toString();
|
|
|
|
appExec.remove(QRegularExpression("%."));
|
|
appExec.remove(QRegularExpression("^\""));
|
|
// appExec.remove(QRegularExpression(" *$"));
|
|
appExec = appExec.simplified();
|
|
|
|
LauncherItem *item = new LauncherItem;
|
|
item->id = fileName;
|
|
item->name = appName;
|
|
item->genericName = desktop.value("Comment").toString();
|
|
item->comment = desktop.value("Comment").toString();
|
|
item->iconName = desktop.value("Icon").toString();
|
|
item->args = appExec.split(" ");
|
|
|
|
beginInsertRows(QModelIndex(), m_items.count(), m_items.count());
|
|
m_items.append(item);
|
|
qDebug() << "added: " << item->name;
|
|
Q_EMIT applicationAdded(item);
|
|
endInsertRows();
|
|
}
|
|
|
|
void LauncherModel::removeApp(QObject *object)
|
|
{
|
|
LauncherItem *item = qobject_cast<LauncherItem *>(object);
|
|
if (!item)
|
|
return;
|
|
|
|
int index = m_items.indexOf(item);
|
|
if (index < 0)
|
|
return;
|
|
|
|
beginRemoveRows(QModelIndex(), index, index);
|
|
qDebug() << "removed: " << item->name;
|
|
m_items.removeAt(index);
|
|
endRemoveRows();
|
|
|
|
Q_EMIT applicationRemoved(item);
|
|
item->deleteLater();
|
|
}
|