You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
412 lines
14 KiB
412 lines
14 KiB
/* |
|
SPDX-FileCopyrightText: 2020 Méven Car <meven.car@enioka.com> |
|
|
|
SPDX-License-Identifier: GPL-2.0-or-later |
|
*/ |
|
|
|
#include "autostartmodel.h" |
|
#include "kcm_autostart_debug.h" |
|
|
|
#include <KConfigGroup> |
|
#include <KDesktopFile> |
|
#include <KShell> |
|
#include <QDebug> |
|
#include <QQuickItem> |
|
#include <QQuickRenderControl> |
|
#include <QStandardPaths> |
|
#include <QWindow> |
|
|
|
#include <KFileItem> |
|
#include <KIO/CopyJob> |
|
#include <KIO/DeleteJob> |
|
#include <KLocalizedString> |
|
#include <KOpenWithDialog> |
|
#include <KPropertiesDialog> |
|
#include <autostartscriptdesktopfile.h> |
|
|
|
// FDO user autostart directories are |
|
// .config/autostart which has .desktop files executed by klaunch or systemd, some of which might be scripts |
|
|
|
// Then we have Plasma-specific locations which run scripts |
|
// .config/autostart-scripts which has scripts executed by plasma_session (now migrated to .desktop files) |
|
// .config/plasma-workspace/shutdown which has scripts executed by plasma-shutdown |
|
// .config/plasma-workspace/env which has scripts executed by startplasma |
|
|
|
// in the case of pre-startup they have to end in .sh |
|
// everywhere else it doesn't matter |
|
|
|
// the comment above describes how autostart *currently* works, it is not definitive documentation on how autostart *should* work |
|
|
|
// share/autostart shouldn't be an option as this should be reserved for global autostart entries |
|
|
|
std::optional<AutostartEntry> AutostartModel::loadDesktopEntry(const QString &fileName) |
|
{ |
|
KDesktopFile config(fileName); |
|
const KConfigGroup grp = config.desktopGroup(); |
|
const auto name = config.readName(); |
|
|
|
const bool hidden = grp.readEntry("Hidden", false); |
|
|
|
if (hidden) { |
|
return {}; |
|
} |
|
|
|
const QStringList notShowList = grp.readXdgListEntry("NotShowIn"); |
|
const QStringList onlyShowList = grp.readXdgListEntry("OnlyShowIn"); |
|
const bool enabled = !(notShowList.contains(QLatin1String("KDE")) || (!onlyShowList.isEmpty() && !onlyShowList.contains(QLatin1String("KDE")))); |
|
|
|
if (!enabled) { |
|
return {}; |
|
} |
|
|
|
const auto lstEntry = grp.readXdgListEntry("OnlyShowIn"); |
|
const bool onlyInPlasma = lstEntry.contains(QLatin1String("KDE")); |
|
const QString iconName = !config.readIcon().isEmpty() ? config.readIcon() : QStringLiteral("dialog-scripts"); |
|
const auto kind = AutostartScriptDesktopFile::isAutostartScript(config) ? XdgScripts : XdgAutoStart; // .config/autostart load desktop at startup |
|
const QString tryCommand = grp.readEntry("TryExec"); |
|
|
|
// Try to filter out entries that point to nonexistant programs |
|
// If TryExec is either found in $PATH or is an absolute file path that exists |
|
// This doesn't detect uninstalled Flatpaks for example though |
|
if (!tryCommand.isEmpty() && QStandardPaths::findExecutable(tryCommand).isEmpty() && !QFile::exists(tryCommand)) { |
|
return {}; |
|
} |
|
|
|
return AutostartEntry{name, kind, enabled, fileName, onlyInPlasma, iconName}; |
|
} |
|
|
|
AutostartModel::AutostartModel(QObject *parent) |
|
: QAbstractListModel(parent) |
|
, m_xdgConfigPath(QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation)) |
|
, m_xdgAutoStartPath(m_xdgConfigPath.filePath(QStringLiteral("autostart"))) |
|
{ |
|
} |
|
|
|
void AutostartModel::load() |
|
{ |
|
beginResetModel(); |
|
|
|
m_entries.clear(); |
|
|
|
// Creates if doesn't already exist |
|
m_xdgAutoStartPath.mkpath(QStringLiteral(".")); |
|
|
|
// Needed to add all script entries after application entries |
|
QVector<AutostartEntry> scriptEntries; |
|
const auto filesInfo = m_xdgAutoStartPath.entryInfoList(QDir::Files); |
|
for (const QFileInfo &fi : filesInfo) { |
|
if (!KDesktopFile::isDesktopFile(fi.fileName())) { |
|
continue; |
|
} |
|
|
|
const std::optional<AutostartEntry> entry = loadDesktopEntry(fi.absoluteFilePath()); |
|
|
|
if (!entry) { |
|
continue; |
|
} |
|
|
|
if (entry->source == XdgScripts) { |
|
scriptEntries.push_back(entry.value()); |
|
} else { |
|
m_entries.push_back(entry.value()); |
|
} |
|
} |
|
|
|
m_entries.append(scriptEntries); |
|
|
|
loadScriptsFromDir(QStringLiteral("plasma-workspace/env/"), AutostartModel::AutostartEntrySource::PlasmaEnvScripts); |
|
|
|
loadScriptsFromDir(QStringLiteral("plasma-workspace/shutdown/"), AutostartModel::AutostartEntrySource::PlasmaShutdown); |
|
|
|
endResetModel(); |
|
} |
|
|
|
void AutostartModel::loadScriptsFromDir(const QString &subDir, AutostartModel::AutostartEntrySource kind) |
|
{ |
|
QDir dir(m_xdgConfigPath.filePath(subDir)); |
|
// Creates if doesn't already exist |
|
dir.mkpath(QStringLiteral(".")); |
|
|
|
const auto autostartDirFilesInfo = dir.entryInfoList(QDir::Files); |
|
for (const QFileInfo &fi : autostartDirFilesInfo) { |
|
QString fileName = fi.absoluteFilePath(); |
|
const bool isSymlink = fi.isSymLink(); |
|
if (isSymlink) { |
|
fileName = fi.symLinkTarget(); |
|
} |
|
|
|
m_entries.push_back({fileName, kind, true, fi.absoluteFilePath(), false, QStringLiteral("dialog-scripts")}); |
|
} |
|
} |
|
|
|
int AutostartModel::rowCount(const QModelIndex &parent) const |
|
{ |
|
if (parent.isValid()) { |
|
return 0; |
|
} |
|
|
|
return m_entries.count(); |
|
} |
|
|
|
bool AutostartModel::reloadEntry(const QModelIndex &index, const QString &fileName) |
|
{ |
|
if (!checkIndex(index)) { |
|
return false; |
|
} |
|
|
|
const std::optional<AutostartEntry> newEntry = loadDesktopEntry(fileName); |
|
|
|
if (!newEntry) { |
|
return false; |
|
} |
|
|
|
m_entries.replace(index.row(), newEntry.value()); |
|
Q_EMIT dataChanged(index, index); |
|
return true; |
|
} |
|
|
|
QVariant AutostartModel::data(const QModelIndex &index, int role) const |
|
{ |
|
if (!checkIndex(index)) { |
|
return QVariant(); |
|
} |
|
|
|
const auto &entry = m_entries.at(index.row()); |
|
|
|
switch (role) { |
|
case Qt::DisplayRole: |
|
return entry.name; |
|
case Enabled: |
|
return entry.enabled; |
|
case Source: |
|
return entry.source; |
|
case FileName: |
|
return entry.fileName; |
|
case OnlyInPlasma: |
|
return entry.onlyInPlasma; |
|
case IconName: |
|
return entry.iconName; |
|
} |
|
|
|
return QVariant(); |
|
} |
|
|
|
void AutostartModel::addApplication(const KService::Ptr &service) |
|
{ |
|
QString desktopPath; |
|
// It is important to ensure that we make an exact copy of an existing |
|
// desktop file (if selected) to enable users to override global autostarts. |
|
// Also see |
|
// https://bugs.launchpad.net/ubuntu/+source/kde-workspace/+bug/923360 |
|
if (service->desktopEntryName().isEmpty() || service->entryPath().isEmpty()) { |
|
// create a new desktop file in s_desktopPath |
|
desktopPath = m_xdgAutoStartPath.filePath(service->name() + QStringLiteral(".desktop")); |
|
|
|
KDesktopFile desktopFile(desktopPath); |
|
KConfigGroup kcg = desktopFile.desktopGroup(); |
|
kcg.writeEntry("Name", service->name()); |
|
kcg.writeEntry("Exec", service->exec()); |
|
kcg.writeEntry("Icon", service->icon()); |
|
kcg.writeEntry("Path", ""); |
|
kcg.writeEntry("Terminal", service->terminal() ? "True" : "False"); |
|
kcg.writeEntry("Type", "Application"); |
|
desktopFile.sync(); |
|
|
|
} else { |
|
desktopPath = m_xdgAutoStartPath.filePath(service->storageId()); |
|
|
|
QFile::remove(desktopPath); |
|
|
|
// copy original desktop file to new path |
|
KDesktopFile desktopFile(service->entryPath()); |
|
auto newDeskTopFile = desktopFile.copyTo(desktopPath); |
|
newDeskTopFile->sync(); |
|
} |
|
|
|
const QString iconName = !service->icon().isEmpty() ? service->icon() : QStringLiteral("dialog-scripts"); |
|
|
|
const auto entry = AutostartEntry{service->name(), |
|
AutostartModel::AutostartEntrySource::XdgAutoStart, // .config/autostart load desktop at startup |
|
true, |
|
desktopPath, |
|
false, |
|
iconName}; |
|
|
|
int lastApplication = -1; |
|
for (const AutostartEntry &e : qAsConst(m_entries)) { |
|
if (e.source == AutostartModel::AutostartEntrySource::XdgScripts) { |
|
break; |
|
} |
|
++lastApplication; |
|
} |
|
|
|
// push before the script items |
|
const int index = lastApplication + 1; |
|
|
|
beginInsertRows(QModelIndex(), index, index); |
|
|
|
m_entries.insert(index, entry); |
|
|
|
endInsertRows(); |
|
} |
|
|
|
void AutostartModel::showApplicationDialog(QQuickItem *context) |
|
{ |
|
KOpenWithDialog *owdlg = new KOpenWithDialog(); |
|
owdlg->setAttribute(Qt::WA_DeleteOnClose); |
|
|
|
if (context && context->window()) { |
|
if (QWindow *actualWindow = QQuickRenderControl::renderWindowFor(context->window())) { |
|
owdlg->winId(); // so it creates windowHandle |
|
owdlg->windowHandle()->setTransientParent(actualWindow); |
|
owdlg->setModal(true); |
|
} |
|
} |
|
|
|
connect(owdlg, &QDialog::finished, this, [this, owdlg](int result) { |
|
if (result != QDialog::Accepted) { |
|
return; |
|
} |
|
|
|
const KService::Ptr service = owdlg->service(); |
|
|
|
Q_ASSERT(service); |
|
if (!service) { |
|
return; // Don't crash if KOpenWith wasn't able to create service. |
|
} |
|
|
|
addApplication(service); |
|
}); |
|
owdlg->open(); |
|
} |
|
|
|
void AutostartModel::addScript(const QUrl &url, AutostartModel::AutostartEntrySource kind) |
|
{ |
|
const QFileInfo file(url.toLocalFile()); |
|
|
|
if (!file.isAbsolute()) { |
|
Q_EMIT error(i18n("\"%1\" is not an absolute url.", url.toLocalFile())); |
|
return; |
|
} else if (!file.exists()) { |
|
Q_EMIT error(i18n("\"%1\" does not exist.", url.toLocalFile())); |
|
return; |
|
} else if (!file.isFile()) { |
|
Q_EMIT error(i18n("\"%1\" is not a file.", url.toLocalFile())); |
|
return; |
|
} else if (!file.isReadable()) { |
|
Q_EMIT error(i18n("\"%1\" is not readable.", url.toLocalFile())); |
|
return; |
|
} |
|
|
|
const QString fileName = url.fileName(); |
|
|
|
if (kind == AutostartModel::AutostartEntrySource::XdgScripts) { |
|
int lastLoginScript = -1; |
|
for (const AutostartEntry &e : qAsConst(m_entries)) { |
|
if (e.source == AutostartModel::AutostartEntrySource::PlasmaShutdown) { |
|
break; |
|
} |
|
++lastLoginScript; |
|
} |
|
|
|
AutostartScriptDesktopFile desktopFile(fileName, file.filePath()); |
|
insertScriptEntry(lastLoginScript + 1, fileName, desktopFile.fileName(), kind); |
|
} else if (kind == AutostartModel::AutostartEntrySource::PlasmaShutdown) { |
|
const QUrl destinationScript = QUrl::fromLocalFile(QDir(m_xdgConfigPath.filePath(QStringLiteral("plasma-workspace/shutdown/"))).filePath(fileName)); |
|
KIO::CopyJob *job = KIO::link(url, destinationScript, KIO::HideProgressInfo); |
|
job->setAutoRename(true); |
|
job->setProperty("finalUrl", destinationScript); |
|
|
|
connect(job, &KIO::CopyJob::renamed, this, [](KIO::Job *job, const QUrl &from, const QUrl &to) { |
|
Q_UNUSED(from) |
|
// in case the destination filename had to be renamed |
|
job->setProperty("finalUrl", to); |
|
}); |
|
|
|
connect(job, &KJob::finished, this, [this, url, kind](KJob *theJob) { |
|
if (theJob->error()) { |
|
qCWarning(KCM_AUTOSTART_DEBUG) << "Could not add script entry" << theJob->errorString(); |
|
return; |
|
} |
|
const QUrl dest = theJob->property("finalUrl").toUrl(); |
|
insertScriptEntry(m_entries.size(), dest.fileName(), dest.path(), kind); |
|
}); |
|
|
|
job->start(); |
|
} else { |
|
Q_ASSERT(0); |
|
} |
|
} |
|
|
|
void AutostartModel::insertScriptEntry(int index, const QString &name, const QString &path, AutostartEntrySource kind) |
|
{ |
|
beginInsertRows(QModelIndex(), index, index); |
|
|
|
AutostartEntry entry = AutostartEntry{name, kind, true, path, false, QStringLiteral("dialog-scripts")}; |
|
|
|
m_entries.insert(index, entry); |
|
|
|
endInsertRows(); |
|
} |
|
|
|
void AutostartModel::removeEntry(int row) |
|
{ |
|
const auto entry = m_entries.at(row); |
|
|
|
KIO::DeleteJob *job = KIO::del(QUrl::fromLocalFile(entry.fileName), KIO::HideProgressInfo); |
|
|
|
connect(job, &KJob::finished, this, [this, row, entry](KJob *theJob) { |
|
if (theJob->error()) { |
|
qCWarning(KCM_AUTOSTART_DEBUG) << "Could not remove entry" << theJob->errorString(); |
|
return; |
|
} |
|
|
|
beginRemoveRows(QModelIndex(), row, row); |
|
m_entries.remove(row); |
|
|
|
endRemoveRows(); |
|
}); |
|
|
|
job->start(); |
|
} |
|
|
|
QHash<int, QByteArray> AutostartModel::roleNames() const |
|
{ |
|
QHash<int, QByteArray> roleNames = QAbstractListModel::roleNames(); |
|
|
|
roleNames.insert(Name, QByteArrayLiteral("name")); |
|
roleNames.insert(Enabled, QByteArrayLiteral("enabled")); |
|
roleNames.insert(Source, QByteArrayLiteral("source")); |
|
roleNames.insert(FileName, QByteArrayLiteral("fileName")); |
|
roleNames.insert(OnlyInPlasma, QByteArrayLiteral("onlyInPlasma")); |
|
roleNames.insert(IconName, QByteArrayLiteral("iconName")); |
|
|
|
return roleNames; |
|
} |
|
|
|
void AutostartModel::editApplication(int row, QQuickItem *context) |
|
{ |
|
const QModelIndex idx = index(row, 0); |
|
|
|
const QString fileName = data(idx, AutostartModel::Roles::FileName).toString(); |
|
KFileItem kfi(QUrl::fromLocalFile(fileName)); |
|
kfi.setDelayedMimeTypes(true); |
|
|
|
KPropertiesDialog *dlg = new KPropertiesDialog(kfi, nullptr); |
|
dlg->setAttribute(Qt::WA_DeleteOnClose); |
|
|
|
if (context && context->window()) { |
|
if (QWindow *actualWindow = QQuickRenderControl::renderWindowFor(context->window())) { |
|
dlg->winId(); // so it creates windowHandle |
|
dlg->windowHandle()->setTransientParent(actualWindow); |
|
dlg->setModal(true); |
|
} |
|
} |
|
|
|
connect(dlg, &QDialog::finished, this, [this, idx, dlg](int result) { |
|
if (result == QDialog::Accepted) { |
|
reloadEntry(idx, dlg->item().localPath()); |
|
} |
|
}); |
|
dlg->open(); |
|
}
|
|
|