diff --git a/kcms/autostart/CMakeLists.txt b/kcms/autostart/CMakeLists.txt --- a/kcms/autostart/CMakeLists.txt +++ b/kcms/autostart/CMakeLists.txt @@ -5,6 +5,7 @@ autostartitem.cpp addscriptdialog.cpp advanceddialog.cpp + autostartmodel.cpp autostart.cpp ) ki18n_wrap_ui(kcm_autostart_PART_SRCS autostartconfig.ui ) diff --git a/kcms/autostart/autostart.h b/kcms/autostart/autostart.h --- a/kcms/autostart/autostart.h +++ b/kcms/autostart/autostart.h @@ -39,36 +39,36 @@ public: explicit Autostart( QWidget* parent, const QVariantList& ); ~Autostart() override; - enum COL_TYPE { COL_NAME = 0, COL_COMMAND=1, COL_STATUS=2,COL_RUN=3 }; + enum COL_TYPE { COL_NAME = 0, COL_COMMAND=1, COL_STATUS=2, COL_RUN=3 }; void load() override; void save() override; void defaults() override; - QStringList listPathName() const { return m_pathName;} - public Q_SLOTS: - void slotChangeStartup( ScriptStartItem* item, int index ); + void slotChangeStartup( ScriptStartItem *item, int index ); protected: - void addItem(DesktopStartItem *item, const QString& name, const QString& run, const QString& command, bool disabled ); - void addItem(ScriptStartItem *item, const QString& name, const QString& command, ScriptStartItem::ENV type ); + void updateDesktopStartItem(DesktopStartItem *item, const QString &name, const QString &command, bool disabled, const QString &fileName); + void updateScriptStartItem(ScriptStartItem *item, const QString &name, const QString &command, AutostartEntrySource type, const QString &fileName); private Q_SLOTS: void slotAddProgram(); void slotAddScript(); void slotRemoveCMD(); void slotEditCMD(QTreeWidgetItem*); - bool slotEditCMD(const KFileItem&); void slotEditCMD(); void slotSelectionChanged(); void slotItemClicked( QTreeWidgetItem *, int); void slotAdvanced(); + void slotRowInserted(const QModelIndex &parent, int first, int last); + void slotDatachanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles); private: - QTreeWidgetItem *m_programItem, *m_scriptItem; - QString m_desktopPath; - QStringList m_paths; - QStringList m_pathName; + QModelIndex indexFromWidget(QTreeWidgetItem *widget) const; + + AutostartModel *m_model; + QTreeWidgetItem *m_programItem; + QTreeWidgetItem *m_scriptItem; Ui_AutostartConfig *widget; }; diff --git a/kcms/autostart/autostart.cpp b/kcms/autostart/autostart.cpp --- a/kcms/autostart/autostart.cpp +++ b/kcms/autostart/autostart.cpp @@ -23,6 +23,7 @@ #include "autostartitem.h" #include "addscriptdialog.h" #include "advanceddialog.h" +#include "autostartmodel.h" #include #include @@ -39,12 +40,14 @@ #include #include #include +#include #include K_PLUGIN_FACTORY(AutostartFactory, registerPlugin();) Autostart::Autostart( QWidget* parent, const QVariantList& ) : KCModule(parent ) + , m_model(new AutostartModel(this)) { widget = new Ui_AutostartConfig(); widget->setupUi(this); @@ -62,14 +65,15 @@ connect( widget->btnProperties, SIGNAL(clicked()), SLOT(slotEditCMD()) ); connect( widget->listCMD, SIGNAL(itemDoubleClicked(QTreeWidgetItem*,int)), SLOT(slotEditCMD(QTreeWidgetItem*)) ); - connect(widget->btnAddScript, &QPushButton::clicked, this, &Autostart::slotAddScript); connect(widget->btnAddProgram, &QPushButton::clicked, this, &Autostart::slotAddProgram); connect(widget->btnRemove, &QPushButton::clicked, this, &Autostart::slotRemoveCMD); connect(widget->btnAdvanced, &QPushButton::clicked, this, &Autostart::slotAdvanced); connect(widget->listCMD, &QTreeWidget::itemClicked, this, &Autostart::slotItemClicked); connect(widget->listCMD, &QTreeWidget::itemSelectionChanged, this, &Autostart::slotSelectionChanged); + connect(m_model, &QAbstractItemModel::rowsInserted, this, &Autostart::slotRowInserted); + connect(m_model, &QAbstractItemModel::dataChanged, this, &Autostart::slotDatachanged); KAboutData* about = new KAboutData(QStringLiteral("Autostart"), i18n("Session Autostart Manager"), @@ -93,70 +97,40 @@ { if ( item && col == COL_STATUS ) { DesktopStartItem *entry = dynamic_cast( item ); - if ( entry ) { - bool disable = ( item->checkState( col ) == Qt::Unchecked ); - KDesktopFile kc(entry->fileName().path()); - KConfigGroup grp = kc.desktopGroup(); - if ( grp.hasKey( "Hidden" ) && !disable) { - grp.deleteEntry( "Hidden" ); - } - else - grp.writeEntry("Hidden", disable); - - kc.sync(); - if ( disable ) - item->setText( COL_STATUS, i18nc( "The program won't be run", "Disabled" ) ); - else + if (entry) { + const bool enabled = ( item->checkState( col ) == Qt::Checked ); + m_model->setData(indexFromWidget(item), enabled, AutostartModel::Roles::Enabled); + if (enabled) { item->setText( COL_STATUS, i18nc( "The program will be run", "Enabled" ) ); + } else { + item->setText( COL_STATUS, i18nc( "The program won't be run", "Disabled" ) ); + } } } } -void Autostart::addItem( DesktopStartItem* item, const QString& name, const QString& run, const QString& command, bool disabled ) +void Autostart::updateDesktopStartItem(DesktopStartItem *item, const QString &name, const QString &command, bool disabled, const QString &fileName) { Q_ASSERT( item ); item->setText( COL_NAME, name ); - item->setText( COL_RUN, run ); + item->setToolTip(COL_NAME, KShell::tildeCollapse(fileName)); + item->setText( COL_RUN, AutostartModel::listPathName()[0] /* Startup */ ); item->setText( COL_COMMAND, command ); item->setCheckState( COL_STATUS, disabled ? Qt::Unchecked : Qt::Checked ); item->setText( COL_STATUS, disabled ? i18nc( "The program won't be run", "Disabled" ) : i18nc( "The program will be run", "Enabled" )); } -void Autostart::addItem(ScriptStartItem* item, const QString& name, const QString& command, ScriptStartItem::ENV type ) +void Autostart::updateScriptStartItem(ScriptStartItem *item, const QString &name, const QString &command, AutostartEntrySource type, const QString &fileName) { Q_ASSERT( item ); item->setText( COL_NAME, name ); + item->setToolTip(COL_NAME, KShell::tildeCollapse(fileName)); item->setText( COL_COMMAND, command ); item->changeStartup( type ); } - void Autostart::load() { - // FDO user autostart directories are - // .config/autostart which has .desktop files executed by klaunch - - //Then we have Plasma-specific locations which run scripts - // .config/autostart-scripts which has scripts executed by ksmserver - // .config/plasma-workspace/shutdown which has scripts executed by startkde - // .config/plasma-workspace/env which has scripts executed by startkde - - //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 - - m_desktopPath = QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QStringLiteral("/autostart/"); - - m_paths << QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QStringLiteral("/autostart-scripts/") - << QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QStringLiteral("/plasma-workspace/shutdown/") - << QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QStringLiteral("/plasma-workspace/env/"); - // share/autostart shouldn't be an option as this should be reserved for global autostart entries - - m_pathName << i18n("Startup") - << i18n("Logout") - << i18n("Before session startup") - ; widget->listCMD->clear(); m_programItem = new QTreeWidgetItem( widget->listCMD ); @@ -175,278 +149,195 @@ widget->listCMD->expandItem( m_programItem ); widget->listCMD->expandItem( m_scriptItem ); - //add programs - { - QDir d(m_desktopPath); - if (!d.exists()) - d.mkpath(m_desktopPath); - QDir autostartdir( m_desktopPath ); - autostartdir.setFilter( QDir::Files ); - const QFileInfoList list = autostartdir.entryInfoList(); - - for (int i = 0; i < list.size(); ++i) { - QFileInfo fi = list.at(i); - QString filename = fi.fileName(); - bool desktopFile = filename.endsWith(QLatin1String(".desktop")); - if ( desktopFile ) { - KDesktopFile config(fi.absoluteFilePath()); - KService service(&config); - //kDebug() << fi.absoluteFilePath() << "trying" << service.exec(); - QStringList commandLine = KShell::splitArgs(service.exec()); - if (commandLine.isEmpty()) { - continue; - } - - const QString exe = commandLine.first(); - if (exe.isEmpty() || QStandardPaths::findExecutable(exe).isEmpty()) { - continue; - } - - DesktopStartItem *item = new DesktopStartItem( fi.absoluteFilePath(), m_programItem, this ); - - const KConfigGroup grp = config.desktopGroup(); - const bool hidden = service.isDeleted(); - const QStringList notShowList = grp.readXdgListEntry("NotShowIn"); - const QStringList onlyShowList = grp.readXdgListEntry("OnlyShowIn"); - - const bool disabled = hidden || - notShowList.contains(QLatin1String("KDE")) || - (!onlyShowList.isEmpty() && !onlyShowList.contains(QLatin1String("KDE"))); - - int indexPath = m_paths.indexOf((item->fileName().adjusted(QUrl::RemoveFilename).toString() ) ); - if ( indexPath > 2 ) - indexPath = 0; //.kde/share/autostart and .config/autostart load desktop at startup - addItem(item, service.name(), m_pathName.value(indexPath), service.exec(), disabled ); - } - } - } + m_model->load(); - //add scripts - - for (const QString& path : qAsConst(m_paths)) { - QDir d(path); - if (!d.exists()) - d.mkpath(path); - - QDir autostartdir( path ); - autostartdir.setFilter( QDir::Files ); - const QFileInfoList list = autostartdir.entryInfoList(); - - for (int i = 0; i < list.size(); ++i) { - QFileInfo fi = list.at(i); - - ScriptStartItem *item = new ScriptStartItem( fi.absoluteFilePath(), m_scriptItem,this ); - int typeOfStartup = m_paths.indexOf((item->fileName().adjusted(QUrl::RemoveScheme | QUrl::RemoveFilename).toString()) ); - ScriptStartItem::ENV type = ScriptStartItem::START; - switch( typeOfStartup ) - { - case 0: - type =ScriptStartItem::START; - break; - case 1: - type = ScriptStartItem::SHUTDOWN; - break; - case 2: - type = ScriptStartItem::PRE_START; - break; - default: - qDebug()<<" type is not defined :"<rowCount(); i++) { + slotRowInserted(QModelIndex(), i, i); } //Update button slotSelectionChanged(); widget->listCMD->resizeColumnToContents(COL_NAME); - //widget->listCMD->resizeColumnToContents(COL_COMMAND); widget->listCMD->resizeColumnToContents(COL_STATUS); widget->listCMD->resizeColumnToContents(COL_RUN); } void Autostart::slotAddProgram() { - KOpenWithDialog owdlg( this ); - if (owdlg.exec() != QDialog::Accepted) - return; + KOpenWithDialog *owdlg = new KOpenWithDialog(this); + connect(owdlg, &QDialog::finished, this, [this, owdlg] (int result) { + if (result == QDialog::Accepted) { - KService::Ptr service = owdlg.service(); + KService::Ptr service = owdlg->service(); - Q_ASSERT(service); - if (!service) { - return; // Don't crash if KOpenWith wasn't able to create service. - } + Q_ASSERT(service); + if (!service) { + return; // Don't crash if KOpenWith wasn't able to create service. + } - // 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 - QString desktopPath; - QUrl desktopTemplate; - if ( service->desktopEntryName().isEmpty() || service->entryPath().isEmpty()) { - // Build custom desktop file (e.g. when the user entered an executable - // name in the OpenWithDialog). - desktopPath = m_desktopPath + service->name() + QStringLiteral(".desktop"); - desktopTemplate = QUrl::fromLocalFile( desktopPath ); - KConfig kc(desktopTemplate.path(), KConfig::SimpleConfig); - KConfigGroup kcg = kc.group("Desktop Entry"); - kcg.writeEntry("Exec",service->exec()); - kcg.writeEntry("Icon","system-run"); - kcg.writeEntry("Path",""); - kcg.writeEntry("Terminal",false); - kcg.writeEntry("Type","Application"); - kc.sync(); - - KPropertiesDialog dlg( desktopTemplate, this ); - if ( dlg.exec() != QDialog::Accepted ) - { - return; + m_model->addEntry(service); } - } - else - { - // Use existing desktop file and use same file name to enable overrides. - desktopPath = m_desktopPath + service->desktopEntryName() + QStringLiteral(".desktop"); - desktopTemplate = QUrl::fromLocalFile( QStandardPaths::locate(QStandardPaths::ApplicationsLocation, service->entryPath()) ); - - KPropertiesDialog dlg( QUrl::fromLocalFile(service->entryPath()), QUrl::fromLocalFile(m_desktopPath), service->desktopEntryName() + QStringLiteral(".desktop"), this ); - if ( dlg.exec() != QDialog::Accepted ) - return; - } - KDesktopFile newConf(desktopTemplate.path()); - DesktopStartItem * item = new DesktopStartItem( desktopPath, m_programItem,this ); - addItem( item, service->name(), m_pathName[0], newConf.desktopGroup().readEntry("Exec") , false); + }); + owdlg->open(); } void Autostart::slotAddScript() { - AddScriptDialog * addDialog = new AddScriptDialog(this); - int result = addDialog->exec(); - if (result == QDialog::Accepted) { - if (addDialog->symLink()) - KIO::link(addDialog->importUrl(), QUrl::fromLocalFile(m_paths[0])); - else - KIO::copy(addDialog->importUrl(), QUrl::fromLocalFile(m_paths[0])); - - ScriptStartItem * item = new ScriptStartItem( m_paths[0] + addDialog->importUrl().fileName(), m_scriptItem,this ); - addItem( item, addDialog->importUrl().fileName(), addDialog->importUrl().fileName(),ScriptStartItem::START ); - } - delete addDialog; + AddScriptDialog *addDialog = new AddScriptDialog(this); + connect(addDialog, &QDialog::finished, this, [this, addDialog] (int result) { + if (result == QDialog::Accepted) { + m_model->addEntry(addDialog->importUrl(), addDialog->symLink()); + } + }); + addDialog->open(); } void Autostart::slotRemoveCMD() { - QTreeWidgetItem* item = widget->listCMD->currentItem(); - if (!item) + QTreeWidgetItem *widgetItem = widget->listCMD->currentItem(); + if (!widgetItem) { return; - DesktopStartItem *startItem = dynamic_cast( item ); - if ( startItem ) - { - QUrl path(startItem->fileName()); - path.setScheme(QStringLiteral("file")); - m_programItem->takeChild( m_programItem->indexOfChild( startItem ) ); - KIO::del(path); - delete item; } - else - { - ScriptStartItem * scriptItem = dynamic_cast( item ); - if ( scriptItem ) - { - QUrl path(scriptItem->fileName()); - path.setScheme(QStringLiteral("file")); - m_scriptItem->takeChild( m_scriptItem->indexOfChild( scriptItem ) ); - KIO::del(path); - delete item; + + if (m_model->removeEntry(indexFromWidget(widgetItem))) { + if (m_scriptItem->indexOfChild(widgetItem) != -1) { + m_scriptItem->removeChild(widgetItem); + } else { + m_programItem->removeChild(widgetItem); } + delete widgetItem; } } -void Autostart::slotEditCMD(QTreeWidgetItem* ent) +void Autostart::slotRowInserted(const QModelIndex &parent, int first, int last) { + Q_ASSERT(!parent.isValid()); - if (!ent) return; - DesktopStartItem *desktopEntry = dynamic_cast( ent ); - if ( desktopEntry ) - { - KFileItem kfi = KFileItem(QUrl(desktopEntry->fileName())); - kfi.setDelayedMimeTypes(true); - if (! slotEditCMD( kfi )) - return; - if (desktopEntry) { - KService service(desktopEntry->fileName().path()); - addItem( desktopEntry, service.name(), m_pathName.value(m_paths.indexOf(desktopEntry->fileName().adjusted(QUrl::RemoveFilename).toString())), service.exec(),false ); + for (int i = first; i <= last; i++) { + QModelIndex idx = m_model->index(i, 0); + + const QString &name = m_model->data(idx, Qt::DisplayRole).toString(); + const QString &fileName = m_model->data(idx, AutostartModel::Roles::FileName).toString(); + const AutostartEntrySource source = AutostartModel::sourceFromInt(m_model->data(idx, AutostartModel::Roles::Source).toInt()); + const QString &command = KShell::tildeCollapse(m_model->data(idx, AutostartModel::Roles::Command).toString()); + + if (source == AutostartEntrySource::XdgAutoStart) { + const bool enabled = m_model->data(idx, AutostartModel::Roles::Enabled).toBool(); + + DesktopStartItem *item = new DesktopStartItem(m_programItem); + updateDesktopStartItem(item, name, command, !enabled, fileName); + } else { + ScriptStartItem *item = new ScriptStartItem(m_scriptItem, this); + updateScriptStartItem(item, name, command, source, fileName); } } } -bool Autostart::slotEditCMD( const KFileItem &item) +void Autostart::slotDatachanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles) +{ + Q_UNUSED(roles) + + for (int row = topLeft.row(); row <= bottomRight.row(); row++) { + + QModelIndex idx = m_model->index(row); + + const QString &name = m_model->data(idx, Qt::DisplayRole).toString(); + const QString &fileName = m_model->data(idx, AutostartModel::Roles::FileName).toString(); + const AutostartEntrySource source = AutostartModel::sourceFromInt(m_model->data(idx, AutostartModel::Roles::Source).toInt()); + const QString &command = KShell::tildeCollapse(m_model->data(idx, AutostartModel::Roles::Command).toString()); + + if (row > (m_programItem->childCount() -1)) { + // scriptItem + QTreeWidgetItem *item = m_scriptItem->child(row - m_programItem->childCount()); + ScriptStartItem *scriptEntry = dynamic_cast(item); + updateScriptStartItem(scriptEntry, name, command, source, fileName); + } else { + // desktopItem + const bool enabled = m_model->data(idx, AutostartModel::Roles::Enabled).toBool(); + + QTreeWidgetItem *item = m_programItem->child(row); + DesktopStartItem *desktopItem = dynamic_cast(item); + updateDesktopStartItem(desktopItem, name, command, !enabled, fileName); + } + } +} + +void Autostart::slotEditCMD(QTreeWidgetItem* ent) { - KPropertiesDialog dlg( item, this ); - bool c = ( dlg.exec() == QDialog::Accepted ); - return c; + if (!ent) return; + DesktopStartItem *desktopItem = dynamic_cast( ent ); + if (desktopItem) { + const QModelIndex index = indexFromWidget(ent); + const QString fileName = m_model->data(index, AutostartModel::Roles::FileName).toString(); + KFileItem kfi(QUrl::fromLocalFile(fileName)); + kfi.setDelayedMimeTypes(true); + + KPropertiesDialog *dlg = new KPropertiesDialog(kfi, this); + connect(dlg, &QDialog::finished, this, [this, index, fileName, desktopItem, dlg] (int result) { + if (result == QDialog::Accepted) { + + // Entry may have change of file + m_model->reloadEntry(index, dlg->item().localPath()); + + const QString name = m_model->data(index, AutostartModel::Roles::DisplayRole).toString(); + const QString command = m_model->data(index, AutostartModel::Roles::Command).toString(); + const bool enabled = m_model->data(index, AutostartModel::Roles::Enabled).toBool(); + + updateDesktopStartItem( desktopItem, name, command, !enabled, fileName); + } + }); + dlg->open(); + } } void Autostart::slotEditCMD() { - if ( widget->listCMD->currentItem() == nullptr ) + if (widget->listCMD->currentItem() == nullptr) { return; - slotEditCMD( (AutoStartItem*)widget->listCMD->currentItem() ); + } + slotEditCMD(dynamic_cast(widget->listCMD->currentItem())); } void Autostart::slotAdvanced() { - if ( widget->listCMD->currentItem() == nullptr ) + if (widget->listCMD->currentItem() == nullptr) { return; - - DesktopStartItem *entry = static_cast( widget->listCMD->currentItem() ); - KDesktopFile kc(entry->fileName().path()); - KConfigGroup grp = kc.desktopGroup(); - bool status = false; - QStringList lstEntry; - if (grp.hasKey("OnlyShowIn")) - { - lstEntry = grp.readXdgListEntry("OnlyShowIn"); - status = lstEntry.contains(QLatin1String("KDE")); } - AdvancedDialog *dlg = new AdvancedDialog( this,status ); - if ( dlg->exec() ) - { - status = dlg->onlyInKde(); - if ( lstEntry.contains(QLatin1String("KDE") ) && !status ) - { - lstEntry.removeAll( QStringLiteral("KDE") ); - grp.writeXdgListEntry( "OnlyShowIn", lstEntry ); - } - else if ( !lstEntry.contains(QLatin1String("KDE") ) && status ) - { - lstEntry.append( QStringLiteral("KDE") ); - grp.writeXdgListEntry( "OnlyShowIn", lstEntry ); - } + const QModelIndex index = indexFromWidget(widget->listCMD->currentItem()); + const bool onlyInPlasma = m_model->data(index, AutostartModel::Roles::OnlyInPlasma).toBool(); + + AdvancedDialog *dlg = new AdvancedDialog(this, onlyInPlasma); + connect(dlg, &QDialog::finished, this, [this, index, dlg] (int result) { + if (result == QDialog::Accepted) { + const bool dialogOnlyInKde = dlg->onlyInKde(); + m_model->setData(index, dialogOnlyInKde, AutostartModel::Roles::OnlyInPlasma); + }; + }); + dlg->open(); +} + +QModelIndex Autostart::indexFromWidget(QTreeWidgetItem *widget) const +{ + int index = m_programItem->indexOfChild(widget); + if (index != -1) { + // widget is part of m_programItem children + return m_model->index(index); + } else { + // widget is part of m_scriptItem children + return m_model->index(m_programItem->childCount() + m_scriptItem->indexOfChild(widget)); } - delete dlg; } -void Autostart::slotChangeStartup( ScriptStartItem* item, int index ) +void Autostart::slotChangeStartup( ScriptStartItem *item, int comboData ) { Q_ASSERT(item); - if ( item ) - { - item->setPath(m_paths.value(index)); - widget->listCMD->setCurrentItem( item ); - if ( ( index == 2 ) && !item->fileName().path().endsWith( QLatin1String(".sh") )) - KMessageBox::information( this, i18n( "Only files with “.sh” extensions are allowed for setting up the environment." ) ); + const QModelIndex index = indexFromWidget(item); + if (!m_model->setData(index, comboData, AutostartModel::Roles::Source)) { + // the action was cancelled restore the previously selected source + item->changeStartup(AutostartModel::sourceFromInt(m_model->data(index, AutostartModel::Roles::Source).toInt())); } } diff --git a/kcms/autostart/autostartitem.h b/kcms/autostart/autostartitem.h --- a/kcms/autostart/autostartitem.h +++ b/kcms/autostart/autostartitem.h @@ -20,8 +20,9 @@ #ifndef AUTOSTARTITEM_H #define AUTOSTARTITEM_H +#include "autostartmodel.h" + #include -#include #include class QComboBox; @@ -31,21 +32,15 @@ class AutoStartItem : public QTreeWidgetItem, public QObject { public: - AutoStartItem( const QString &service, QTreeWidgetItem *parent, Autostart* ); + AutoStartItem(QTreeWidgetItem *parent); ~AutoStartItem() override; - QUrl fileName() const; - - void setPath(const QString &path); - -private: - QUrl m_fileName; }; class DesktopStartItem : public AutoStartItem { public: - DesktopStartItem( const QString &service, QTreeWidgetItem *parent, Autostart* ); + DesktopStartItem(QTreeWidgetItem *parent ); ~DesktopStartItem() override; }; @@ -55,17 +50,15 @@ Q_OBJECT public: - enum ENV { START=0, SHUTDOWN=1, PRE_START=2}; //rename - ScriptStartItem( const QString &service, QTreeWidgetItem *parent, Autostart* ); + ScriptStartItem(QTreeWidgetItem *parent, Autostart* autostart ); ~ScriptStartItem() override; - void changeStartup( ScriptStartItem::ENV type ); + void changeStartup( AutostartEntrySource type ); Q_SIGNALS: void askChangeStartup(ScriptStartItem* item, int index); private Q_SLOTS: - void slotStartupChanged(int index); private: diff --git a/kcms/autostart/autostartitem.cpp b/kcms/autostart/autostartitem.cpp --- a/kcms/autostart/autostartitem.cpp +++ b/kcms/autostart/autostartitem.cpp @@ -29,77 +29,63 @@ #include #include -AutoStartItem::AutoStartItem( const QString &service, QTreeWidgetItem *parent, Autostart* ) +AutoStartItem::AutoStartItem(QTreeWidgetItem *parent) : QTreeWidgetItem( parent ) - , m_fileName(QUrl::fromLocalFile(service)) { } AutoStartItem::~AutoStartItem() { } -QUrl AutoStartItem::fileName() const +DesktopStartItem::DesktopStartItem(QTreeWidgetItem *parent ) + : AutoStartItem(parent ) { - return m_fileName; -} - -void AutoStartItem::setPath(const QString &path) -{ - Q_ASSERT( path.endsWith(QDir::separator()) ); - - if (QUrl(path) == m_fileName.adjusted(QUrl::RemoveFilename)) - return; - - const QString& newFileName = path + m_fileName.fileName(); - KIO::move(m_fileName, QUrl::fromLocalFile(newFileName)); - - m_fileName = QUrl::fromLocalFile(newFileName); -} - -DesktopStartItem::DesktopStartItem( const QString &service, QTreeWidgetItem *parent, Autostart*autostart ) - : AutoStartItem( service, parent,autostart ) -{ - setCheckState ( Autostart::COL_STATUS,Qt::Checked ); + setCheckState( Autostart::COL_STATUS, Qt::Checked ); } DesktopStartItem::~DesktopStartItem() { } -ScriptStartItem::ScriptStartItem( const QString &service, QTreeWidgetItem *parent, Autostart* autostart ) - : AutoStartItem( service, parent,autostart ) +ScriptStartItem::ScriptStartItem( QTreeWidgetItem *parent, Autostart* autostart ) + : AutoStartItem( parent ) { m_comboBoxStartup = new QComboBox; - m_comboBoxStartup->addItems( autostart->listPathName() ); - - setText( 2, i18nc( "The program will be run", "Enabled" ) ); - QObject::connect(m_comboBoxStartup, static_cast(&QComboBox::activated), this, &ScriptStartItem::slotStartupChanged); - QObject::connect( this,&ScriptStartItem::askChangeStartup,autostart,&Autostart::slotChangeStartup ); - treeWidget()->setItemWidget ( this, Autostart::COL_RUN, m_comboBoxStartup ); + // make the folder PathName match the AutoStartPaths that match AutostartEntrySource + // skipping XdgAutoStart that is not available for script items + m_comboBoxStartup->addItem(AutostartModel::listPathName()[0], AutostartEntrySource::XdgScripts); + m_comboBoxStartup->addItem(AutostartModel::listPathName()[1], AutostartEntrySource::PlasmaShutdown); + m_comboBoxStartup->addItem(AutostartModel::listPathName()[2], AutostartEntrySource::PlasmaStart); + + setText(Autostart::COL_STATUS, i18nc( "The program will be run", "Enabled" ) ); + connect(m_comboBoxStartup, static_cast(&QComboBox::activated), this, &ScriptStartItem::slotStartupChanged); + connect( this, &ScriptStartItem::askChangeStartup, autostart, &Autostart::slotChangeStartup ); + treeWidget()->setItemWidget( this, Autostart::COL_RUN, m_comboBoxStartup ); } ScriptStartItem::~ScriptStartItem() { } void ScriptStartItem::slotStartupChanged(int index) { - emit askChangeStartup(this, index); + Q_UNUSED(index) + emit askChangeStartup(this, m_comboBoxStartup->currentData().toInt()); } -void ScriptStartItem::changeStartup(ScriptStartItem::ENV type ) +void ScriptStartItem::changeStartup(AutostartEntrySource type ) { switch( type ) { - case ScriptStartItem::START: + case AutostartEntrySource::XdgScripts: m_comboBoxStartup->setCurrentIndex( 0 ); break; - case ScriptStartItem::SHUTDOWN: + case AutostartEntrySource::PlasmaShutdown: m_comboBoxStartup->setCurrentIndex( 1 ); break; - case ScriptStartItem::PRE_START: + case AutostartEntrySource::PlasmaStart: m_comboBoxStartup->setCurrentIndex( 2 ); break; default: diff --git a/kcms/autostart/autostartmodel.h b/kcms/autostart/autostartmodel.h new file mode 100644 --- /dev/null +++ b/kcms/autostart/autostartmodel.h @@ -0,0 +1,84 @@ +/*************************************************************************** + * Copyright (C) 2020 by Méven Car * + * * + * 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 2 of the License, or * + * (at your option) 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, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA * + ***************************************************************************/ +#ifndef AUTOSTARTMODEL_H +#define AUTOSTARTMODEL_H + +#include + +#include + +enum AutostartEntrySource { + XdgAutoStart = 0, + XdgScripts = 1, + PlasmaShutdown = 2, + PlasmaStart = 3, +}; + +struct AutostartEntry +{ + QString name; // Human readable name or local script name + QString command; // exec or original .sh file + AutostartEntrySource source; + bool enabled; + QString fileName; // the file backing the entry + bool onlyInPlasma; +}; +Q_DECLARE_TYPEINFO(AutostartEntry, Q_MOVABLE_TYPE); + +class AutostartModel : public QAbstractListModel +{ + Q_OBJECT + +public: + explicit AutostartModel(QWidget *parent = nullptr); + + enum Roles { + DisplayRole = Qt::DisplayRole, + Command = Qt::UserRole + 1, + Enabled, + Source, + FileName, + OnlyInPlasma + }; + + static AutostartEntrySource sourceFromInt(int index); + static QString XdgAutoStartPath(); + static QStringList listPath(); + static QStringList listPathName(); + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; + QHash roleNames() const override; + + bool addEntry(const KService::Ptr &service); + + bool addEntry(const QUrl &path, const bool symlink); + + bool reloadEntry(const QModelIndex &index, const QString &fileName); + bool removeEntry(const QModelIndex &index); + + void load(); + +private: + QVector m_entries; + QWidget *m_window; +}; + +#endif // AUTOSTARTMODEL_H diff --git a/kcms/autostart/autostartmodel.cpp b/kcms/autostart/autostartmodel.cpp new file mode 100644 --- /dev/null +++ b/kcms/autostart/autostartmodel.cpp @@ -0,0 +1,492 @@ +/*************************************************************************** + * Copyright (C) 2020 by Méven Car * + * * + * 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 2 of the License, or * + * (at your option) 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, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA * + ***************************************************************************/ +#include "autostartmodel.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + + +namespace { + // FDO user autostart directories are + // .config/autostart which has .desktop files executed by klaunch + + //Then we have Plasma-specific locations which run scripts + // .config/autostart-scripts which has scripts executed by ksmserver + // .config/plasma-workspace/shutdown which has scripts executed by startkde + // .config/plasma-workspace/env which has scripts executed by startkde + + //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 + +// must match enum AutostartEntrySource +QStringList autostartPaths() +{ + return { + QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QStringLiteral("/autostart/"), + QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QStringLiteral("/autostart-scripts/"), + QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QStringLiteral("/plasma-workspace/shutdown/"), + QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QStringLiteral("/plasma-workspace/env/") + }; +} +Q_GLOBAL_STATIC_WITH_ARGS(QStringList, s_paths, (autostartPaths())) + +QStringList autoStartPathNames() +{ + return { + i18n("Startup"), + i18n("Logout"), + i18n("Before session startup") + }; +} +Q_GLOBAL_STATIC_WITH_ARGS(QStringList, s_pathName, (autoStartPathNames())) + +static bool checkEntry(const AutostartEntry &entry) +{ + QStringList commandLine = KShell::splitArgs(entry.command); + if (commandLine.isEmpty()) { + return false; + } + + const QString exe = commandLine.first(); + if (exe.isEmpty() || QStandardPaths::findExecutable(exe).isEmpty()) { + return false; + } + return true; +} + +static AutostartEntry loadDesktopEntry(const QString &fileName) +{ + KDesktopFile config(fileName); + const KConfigGroup grp = config.desktopGroup(); + const auto name = config.readName(); + const auto command = grp.readEntry("Exec"); + + const bool hidden = grp.readEntry("Hidden", false); + const QStringList notShowList = grp.readXdgListEntry("NotShowIn"); + const QStringList onlyShowList = grp.readXdgListEntry("OnlyShowIn"); + const bool enabled = !(hidden || + notShowList.contains(QLatin1String("KDE")) || + (!onlyShowList.isEmpty() && !onlyShowList.contains(QLatin1String("KDE")))); + + const auto lstEntry = grp.readXdgListEntry("OnlyShowIn"); + const bool onlyInPlasma = lstEntry.contains(QLatin1String("KDE")); + + return { + name, + command, + AutostartEntrySource::XdgAutoStart, // .config/autostart load desktop at startup + enabled, + fileName, + onlyInPlasma + }; +} +} + +AutostartEntrySource AutostartModel::sourceFromInt(int index) +{ + switch (index) { + case XdgAutoStart: + case XdgScripts: + case PlasmaShutdown: + case PlasmaStart: + return static_cast(index); + default: + Q_UNREACHABLE(); + } + return XdgAutoStart; +} + +AutostartModel::AutostartModel(QWidget *parent) + : QAbstractListModel(parent) +{ + m_window = parent; +} + +QString AutostartModel::XdgAutoStartPath() +{ + return s_paths->at(0); +} + +QStringList AutostartModel::listPath() +{ + return *s_paths; +} + +QStringList AutostartModel::listPathName() +{ + return *s_pathName; +} + +void AutostartModel::load() +{ + beginResetModel(); + + m_entries.clear(); + + QDir autostartdir(XdgAutoStartPath()); + if (!autostartdir.exists()) { + autostartdir.mkpath(XdgAutoStartPath()); + } + + autostartdir.setFilter(QDir::Files); + + const auto filesInfo = autostartdir.entryInfoList(); + for (const QFileInfo &fi : filesInfo) { + QString filename = fi.fileName(); + bool desktopFile = filename.endsWith(QLatin1String(".desktop")); + if (desktopFile) { + AutostartEntry entry = loadDesktopEntry(fi.absoluteFilePath()); + + if (!checkEntry(entry)) { + continue; + } + + m_entries.push_back(entry); + } + } + + // add scripts, skips first value of listPath + // .config/autostart that is not compatible with scripts + for (int i = 1; i < listPath().length(); i++) { + const auto path = listPath().at(i); + const AutostartEntrySource kind = AutostartModel::sourceFromInt(i); + + QDir dir(path); + if (!dir.exists()) { + dir.mkpath(path); + } + + dir.setFilter(QDir::Files); + + const auto autostartDirFilesInfo = dir.entryInfoList(); + for (const QFileInfo &fi : autostartDirFilesInfo) { + + QString fileName = fi.absoluteFilePath(); + const bool isSymlink = fi.isSymLink(); + if (isSymlink) { + fileName = fi.symLinkTarget(); + } + + m_entries.push_back({ + fi.fileName(), + isSymlink ? fileName : "", + kind, + true, + fi.absoluteFilePath(), + false + }); + } + } + + endResetModel(); +} + +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; + } + + AutostartEntry newEntry = loadDesktopEntry(fileName); + if (!checkEntry(newEntry)) { + return false; + } + + m_entries.replace(index.row(), newEntry); + 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 DisplayRole: return entry.name; + case Command: return entry.command; + case Enabled: return entry.enabled; + case Source: return entry.source; + case FileName: return entry.fileName; + case OnlyInPlasma: return entry.onlyInPlasma; + } + + return QVariant(); +} + +bool AutostartModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (!checkIndex(index)) { + return false; + } + + bool dirty = false; + AutostartEntry &entry = m_entries[index.row()]; + + switch (role) { + case Qt::EditRole: { + if (entry.name == value.toString()) { + return false; + } + entry.name = value.toString(); + dirty = true; + break; + } + case Command: { + if (entry.command == value.toString()) { + return false; + } + entry.command = value.toString(); + dirty = true; + break; + } + case Enabled: { + if (entry.enabled == value.toBool()) { + return false; + } + entry.enabled = value.toBool(); + + KDesktopFile kc(entry.fileName); + KConfigGroup grp = kc.desktopGroup(); + if ( grp.hasKey( "Hidden" ) && entry.enabled) { + grp.deleteEntry( "Hidden" ); + } else { + grp.writeEntry("Hidden", !entry.enabled); + } + kc.sync(); + + dirty = true; + break; + } + case OnlyInPlasma: { + if (value == entry.onlyInPlasma) { + return false; + } + entry.onlyInPlasma = value.toBool(); + + KDesktopFile kc(entry.fileName); + KConfigGroup grp = kc.desktopGroup(); + auto lstEntry = grp.readXdgListEntry("OnlyShowIn"); + if (lstEntry.contains(QLatin1String("KDE") ) && !entry.onlyInPlasma ) { + lstEntry.removeAll( QStringLiteral("KDE") ); + } else if (!lstEntry.contains(QLatin1String("KDE") ) && entry.onlyInPlasma ) { + lstEntry.append( QStringLiteral("KDE") ); + } + grp.writeXdgListEntry( "OnlyShowIn", lstEntry ); + kc.sync(); + + dirty = true; + break; + } + case Source: { + if (entry.source == XdgAutoStart || (entry.source == value.toInt())) { + return false; + } + + AutostartEntrySource newSource = AutostartModel::sourceFromInt(value.toInt()); + auto path = listPath().at(newSource); + + QUrl newFileName = QUrl::fromLocalFile(entry.fileName); + newFileName.setPath( path + newFileName.fileName()); + KIO::CopyJob *job = KIO::move(QUrl::fromLocalFile(entry.fileName), newFileName, KIO::HideProgressInfo); + KJobWidgets::setWindow(job, m_window); + connect(job, &KIO::CopyJob::renamed, this, [&newFileName](KIO::Job * job, const QUrl & from, const QUrl & to) { + Q_UNUSED(job) + Q_UNUSED(from) + + // in case the destination filename had to be renamed + newFileName = to; + }); + + if (job->exec()) { + + entry.source = newSource; + + entry.name = newFileName.fileName(); + entry.fileName = newFileName.path(); + dirty = true; + } + break; + } + } + + if (dirty) { + if (role == Qt::EditRole) { + emit dataChanged(index, index, {role, DisplayRole}); + } else { + emit dataChanged(index, index, {role}); + } + } + + return dirty; +} + +bool AutostartModel::addEntry(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 = XdgAutoStartPath() + service->name() + QStringLiteral(".desktop"); + + KDesktopFile desktopFile(desktopPath); + KConfigGroup kcg = desktopFile.desktopGroup(); + kcg.writeEntry("Name", service->name()); + kcg.writeEntry("Exec", service->exec()); + kcg.writeEntry("Icon", "system-run"); + kcg.writeEntry("Path", ""); + kcg.writeEntry("Terminal", service->terminal() ? "True": "False"); + kcg.writeEntry("Type", "Application"); + desktopFile.sync(); + + } else { + desktopPath = XdgAutoStartPath() + service->desktopEntryName() + QStringLiteral(".desktop"); + + // copy original desktop file to new path + KDesktopFile desktopFile(service->entryPath()); + auto newDeskTopFile = desktopFile.copyTo(desktopPath); + newDeskTopFile->sync(); + } + + const auto entry = AutostartEntry{ + service->name(), + service->exec(), + AutostartEntrySource:: XdgAutoStart, // .config/autostart load desktop at startup + true, + desktopPath, + false + }; + + // push before the script items + int index = 0; + for (const AutostartEntry &e : qAsConst(m_entries)) { + if (e.source == XdgAutoStart) { + index++; + } else { + break; + } + } + + beginInsertRows(QModelIndex(), index, index); + + m_entries.insert(index, entry); + + endInsertRows(); + + return true; +} + +bool AutostartModel::addEntry(const QUrl &path, const bool isSymlink) +{ + beginInsertRows(QModelIndex(), m_entries.count(), m_entries.count()); + + QString fileName = path.fileName(); + QUrl destinationScript = QUrl::fromLocalFile(listPath()[AutostartEntrySource::XdgScripts] + fileName); + KIO::CopyJob *job; + if (isSymlink) { + job = KIO::link(path, destinationScript, KIO::HideProgressInfo); + } else { + job = KIO::copy(path, destinationScript, KIO::HideProgressInfo); + } + KJobWidgets::setWindow(job, m_window); + connect(job, &KIO::CopyJob::renamed, this, [&destinationScript](KIO::Job * job, const QUrl & from, const QUrl & to) { + Q_UNUSED(job) + Q_UNUSED(from) + + // in case the destination filename had to be renamed + destinationScript = to; + }); + + if (!job->exec()) { + return false; + } + + AutostartEntry entry = AutostartEntry{ + destinationScript.fileName(), + isSymlink ? path.path() : "", + AutostartEntrySource::XdgScripts, // .config/autostart load desktop at startup + true, + destinationScript.path(), + false + }; + + m_entries.push_back(entry); + endInsertRows(); + + return true; +} + +bool AutostartModel::removeEntry(const QModelIndex &index) +{ + auto entry = m_entries.at(index.row()); + + KIO::DeleteJob* job = KIO::del(QUrl::fromLocalFile(entry.fileName), KIO::HideProgressInfo); + + if (job->exec()) { + beginRemoveRows(QModelIndex(), index.row(), index.row()); + + m_entries.remove(index.row()); + + endRemoveRows(); + return true; + } + return false; +} + +QHash AutostartModel::roleNames() const +{ + QHash roleNames = QAbstractListModel::roleNames(); + + roleNames.insert(Command, QByteArrayLiteral("command")); + roleNames.insert(Enabled, QByteArrayLiteral("enabled")); + roleNames.insert(Source, QByteArrayLiteral("source")); + roleNames.insert(FileName, QByteArrayLiteral("fileName")); + roleNames.insert(OnlyInPlasma, QByteArrayLiteral("onlyInPlasma")); + + return roleNames; +}