1 /*
2     SPDX-FileCopyrightText: 2009 Andreas Pakulat <apaku@gmx.de>
3     SPDX-FileCopyrightText: 2010 Aleix Pol Gonzalez <aleixpol@kde.org>
4 
5     SPDX-License-Identifier: LGPL-2.0-or-later
6 */
7 
8 #include "nativeappconfig.h"
9 
10 #include <interfaces/icore.h>
11 #include <interfaces/iprojectcontroller.h>
12 #include <interfaces/iruncontroller.h>
13 #include <interfaces/ilaunchconfiguration.h>
14 
15 #include <project/projectmodel.h>
16 
17 #include "nativeappjob.h"
18 #include <interfaces/iproject.h>
19 #include <project/interfaces/iprojectfilemanager.h>
20 #include <util/executecompositejob.h>
21 
22 #include <interfaces/iplugincontroller.h>
23 
24 #include "executeplugin.h"
25 #include "debug.h"
26 #include <util/kdevstringhandler.h>
27 #include "projecttargetscombobox.h"
28 
29 #include <QIcon>
30 #include <QMenu>
31 
32 #include <KConfigGroup>
33 #include <KLineEdit>
34 #include <KLocalizedString>
35 #include <KShell>
36 
37 
38 using namespace KDevelop;
39 
icon() const40 QIcon NativeAppConfigPage::icon() const
41 {
42     return QIcon::fromTheme(QStringLiteral("system-run"));
43 }
44 
itemForPath(const QStringList & path,KDevelop::ProjectModel * model)45 static KDevelop::ProjectBaseItem* itemForPath(const QStringList& path, KDevelop::ProjectModel* model)
46 {
47     return model->itemFromIndex(model->pathToIndex(path));
48 }
49 
50 //TODO: Make sure to auto-add the executable target to the dependencies when its used.
51 
loadFromConfiguration(const KConfigGroup & cfg,KDevelop::IProject * project)52 void NativeAppConfigPage::loadFromConfiguration(const KConfigGroup& cfg, KDevelop::IProject* project )
53 {
54     QSignalBlocker blocker(this);
55     projectTarget->setBaseItem( project ? project->projectItem() : nullptr, true);
56     projectTarget->setCurrentItemPath( cfg.readEntry( ExecutePlugin::projectTargetEntry, QStringList() ) );
57 
58     QUrl exe = cfg.readEntry( ExecutePlugin::executableEntry, QUrl());
59     if( !exe.isEmpty() || project ){
60         executablePath->setUrl( !exe.isEmpty() ? exe : project->path().toUrl() );
61     }else{
62         KDevelop::IProjectController* pc = KDevelop::ICore::self()->projectController();
63         if( pc ){
64             executablePath->setUrl( pc->projects().isEmpty() ? QUrl() : pc->projects().at(0)->path().toUrl() );
65         }
66     }
67     dependencies->setSuggestion(project);
68 
69     //executablePath->setFilter("application/x-executable");
70 
71     executableRadio->setChecked( true );
72     if ( !cfg.readEntry( ExecutePlugin::isExecutableEntry, false ) && projectTarget->count() ){
73         projectTargetRadio->setChecked( true );
74     }
75 
76     arguments->setClearButtonEnabled( true );
77     arguments->setText( cfg.readEntry( ExecutePlugin::argumentsEntry, "" ) );
78     workingDirectory->setUrl( cfg.readEntry( ExecutePlugin::workingDirEntry, QUrl() ) );
79     environment->setCurrentProfile(cfg.readEntry(ExecutePlugin::environmentProfileEntry, QString()));
80     runInTerminal->setChecked( cfg.readEntry( ExecutePlugin::useTerminalEntry, false ) );
81     terminal->setEditText( cfg.readEntry( ExecutePlugin::terminalEntry, terminal->itemText(0) ) );
82     dependencies->setDependencies(KDevelop::stringToQVariant( cfg.readEntry( ExecutePlugin::dependencyEntry, QString() ) ).toList());
83 
84     dependencyAction->setCurrentIndex( dependencyAction->findData( cfg.readEntry( ExecutePlugin::dependencyActionEntry, "Nothing" ) ) );
85 
86     if (cfg.readEntry<bool>(ExecutePlugin::configuredByCTest, false)) {
87         killBeforeStartingAgain->setCurrentIndex(killBeforeStartingAgain->findData(NativeAppJob::startAnother));
88         killBeforeStartingAgain->setDisabled(true);
89     } else {
90         killBeforeStartingAgain->setCurrentIndex(killBeforeStartingAgain->findData(cfg.readEntry<int>(ExecutePlugin::killBeforeExecutingAgain, NativeAppJob::askIfRunning)));
91     }
92 }
93 
NativeAppConfigPage(QWidget * parent)94 NativeAppConfigPage::NativeAppConfigPage( QWidget* parent )
95     : LaunchConfigurationPage( parent )
96 {
97     setupUi(this);
98     //Setup data info for combobox
99     dependencyAction->setItemData(0, QStringLiteral("Nothing"));
100     dependencyAction->setItemData(1, QStringLiteral("Build"));
101     dependencyAction->setItemData(2, QStringLiteral("Install"));
102     dependencyAction->setItemData(3, QStringLiteral("SudoInstall"));
103 
104     killBeforeStartingAgain->addItem(i18nc("@item:inlistbox", "Ask If Running"), NativeAppJob::askIfRunning);
105     killBeforeStartingAgain->addItem(i18nc("@item:inlistbox", "Kill All Instances"), NativeAppJob::killAllInstances);
106     killBeforeStartingAgain->addItem(i18nc("@item:inlistbox", "Start Another"), NativeAppJob::startAnother);
107 
108     //Set workingdirectory widget to ask for directories rather than files
109     workingDirectory->setMode(KFile::Directory | KFile::ExistingOnly | KFile::LocalOnly);
110 
111     configureEnvironment->setSelectionWidget(environment);
112 
113     //connect signals to changed signal
114     connect( projectTarget, &QComboBox::currentTextChanged, this, &NativeAppConfigPage::changed );
115     connect( projectTargetRadio, &QRadioButton::toggled, this, &NativeAppConfigPage::changed );
116     connect( executableRadio, &QRadioButton::toggled, this, &NativeAppConfigPage::changed );
117     connect( executablePath->lineEdit(), &KLineEdit::textEdited, this, &NativeAppConfigPage::changed );
118     connect( executablePath, &KUrlRequester::urlSelected, this, &NativeAppConfigPage::changed );
119     connect( arguments, &QLineEdit::textEdited, this, &NativeAppConfigPage::changed );
120     connect( workingDirectory, &KUrlRequester::urlSelected, this, &NativeAppConfigPage::changed );
121     connect( workingDirectory->lineEdit(), &KLineEdit::textEdited, this, &NativeAppConfigPage::changed );
122     connect( environment, &EnvironmentSelectionWidget::currentProfileChanged, this, &NativeAppConfigPage::changed );
123     connect( dependencyAction, QOverload<int>::of(&KComboBox::currentIndexChanged), this, &NativeAppConfigPage::changed );
124     connect( runInTerminal, &QCheckBox::toggled, this, &NativeAppConfigPage::changed );
125     connect( terminal, &KComboBox::editTextChanged, this, &NativeAppConfigPage::changed );
126     connect( terminal, QOverload<int>::of(&KComboBox::currentIndexChanged), this, &NativeAppConfigPage::changed );
127     connect( dependencyAction, QOverload<int>::of(&KComboBox::currentIndexChanged), this, &NativeAppConfigPage::activateDeps );
128     connect( killBeforeStartingAgain, QOverload<int>::of(&KComboBox::currentIndexChanged), this, &NativeAppConfigPage::changed );
129     connect( dependencies, &DependenciesWidget::changed, this, &NativeAppConfigPage::changed );
130 }
131 
activateDeps(int idx)132 void NativeAppConfigPage::activateDeps( int idx )
133 {
134     dependencies->setEnabled( dependencyAction->itemData( idx ).toString() != QLatin1String("Nothing") );
135 }
136 
saveToConfiguration(KConfigGroup cfg,KDevelop::IProject * project) const137 void NativeAppConfigPage::saveToConfiguration( KConfigGroup cfg, KDevelop::IProject* project ) const
138 {
139     Q_UNUSED( project );
140     cfg.writeEntry( ExecutePlugin::isExecutableEntry, executableRadio->isChecked() );
141     cfg.writeEntry( ExecutePlugin::executableEntry, executablePath->url() );
142     cfg.writeEntry( ExecutePlugin::projectTargetEntry, projectTarget->currentItemPath() );
143     cfg.writeEntry( ExecutePlugin::argumentsEntry, arguments->text() );
144     cfg.writeEntry( ExecutePlugin::workingDirEntry, workingDirectory->url() );
145     cfg.writeEntry( ExecutePlugin::environmentProfileEntry, environment->currentProfile() );
146     cfg.writeEntry( ExecutePlugin::useTerminalEntry, runInTerminal->isChecked() );
147     cfg.writeEntry( ExecutePlugin::terminalEntry, terminal->currentText() );
148     cfg.writeEntry( ExecutePlugin::dependencyActionEntry, dependencyAction->itemData( dependencyAction->currentIndex() ).toString() );
149     cfg.writeEntry( ExecutePlugin::killBeforeExecutingAgain, killBeforeStartingAgain->itemData( killBeforeStartingAgain->currentIndex() ).toInt() );
150     QVariantList deps = dependencies->dependencies();
151     cfg.writeEntry( ExecutePlugin::dependencyEntry, KDevelop::qvariantToString( QVariant( deps ) ) );
152 }
153 
title() const154 QString NativeAppConfigPage::title() const
155 {
156     return i18nc("@title:tab", "Configure Native Application");
157 }
158 
configPages() const159 QList< KDevelop::LaunchConfigurationPageFactory* > NativeAppLauncher::configPages() const
160 {
161     return QList<KDevelop::LaunchConfigurationPageFactory*>();
162 }
163 
description() const164 QString NativeAppLauncher::description() const
165 {
166     return i18n("Executes Native Applications");
167 }
168 
id()169 QString NativeAppLauncher::id()
170 {
171     return QStringLiteral("nativeAppLauncher");
172 }
173 
name() const174 QString NativeAppLauncher::name() const
175 {
176     return i18n("Native Application");
177 }
178 
NativeAppLauncher()179 NativeAppLauncher::NativeAppLauncher()
180 {
181 }
182 
start(const QString & launchMode,KDevelop::ILaunchConfiguration * cfg)183 KJob* NativeAppLauncher::start(const QString& launchMode, KDevelop::ILaunchConfiguration* cfg)
184 {
185     Q_ASSERT(cfg);
186     if( !cfg )
187     {
188         return nullptr;
189     }
190     if( launchMode == QLatin1String("execute") )
191     {
192         auto* iface = KDevelop::ICore::self()->pluginController()->pluginForExtension(QStringLiteral("org.kdevelop.IExecutePlugin"), QStringLiteral("kdevexecute"))->extension<IExecutePlugin>();
193         Q_ASSERT(iface);
194         KJob* depjob = iface->dependencyJob( cfg );
195         QList<KJob*> l;
196         if( depjob )
197         {
198             l << depjob;
199         }
200         auto nativeAppJob = new NativeAppJob( KDevelop::ICore::self()->runController(), cfg );
201         QObject::connect(nativeAppJob, &NativeAppJob::killBeforeExecutingAgainChanged, KDevelop::ICore::self()->runController(), [cfg] (int newValue) {
202             auto cfgGroup = cfg->config();
203             cfgGroup.writeEntry(ExecutePlugin::killBeforeExecutingAgain, newValue);
204         });
205         l << nativeAppJob;
206 
207         return new KDevelop::ExecuteCompositeJob( KDevelop::ICore::self()->runController(), l );
208 
209     }
210     qCWarning(PLUGIN_EXECUTE) << "Unknown launch mode " << launchMode << "for config:" << cfg->name();
211     return nullptr;
212 }
213 
supportedModes() const214 QStringList NativeAppLauncher::supportedModes() const
215 {
216     return QStringList() << QStringLiteral("execute");
217 }
218 
createWidget(QWidget * parent)219 KDevelop::LaunchConfigurationPage* NativeAppPageFactory::createWidget(QWidget* parent)
220 {
221     return new NativeAppConfigPage( parent );
222 }
223 
NativeAppPageFactory()224 NativeAppPageFactory::NativeAppPageFactory()
225 {
226 }
227 
NativeAppConfigType()228 NativeAppConfigType::NativeAppConfigType()
229 {
230     factoryList.append( new NativeAppPageFactory() );
231 }
232 
~NativeAppConfigType()233 NativeAppConfigType::~NativeAppConfigType()
234 {
235     qDeleteAll(factoryList);
236     factoryList.clear();
237 }
238 
name() const239 QString NativeAppConfigType::name() const
240 {
241     return i18n("Compiled Binary");
242 }
243 
244 
configPages() const245 QList<KDevelop::LaunchConfigurationPageFactory*> NativeAppConfigType::configPages() const
246 {
247     return factoryList;
248 }
249 
sharedId()250 QString NativeAppConfigType::sharedId()
251 {
252     return QStringLiteral("Native Application");
253 }
254 
id() const255 QString NativeAppConfigType::id() const
256 {
257     return sharedId();
258 }
259 
icon() const260 QIcon NativeAppConfigType::icon() const
261 {
262     return QIcon::fromTheme(QStringLiteral("application-x-executable"));
263 }
264 
canLaunch(KDevelop::ProjectBaseItem * item) const265 bool NativeAppConfigType::canLaunch ( KDevelop::ProjectBaseItem* item ) const
266 {
267     if( item->target() && item->target()->executable() ) {
268         return canLaunch( item->target()->executable()->builtUrl() );
269     }
270     return false;
271 }
272 
canLaunch(const QUrl & file) const273 bool NativeAppConfigType::canLaunch ( const QUrl& file ) const
274 {
275     return ( file.isLocalFile() && QFileInfo( file.toLocalFile() ).isExecutable() );
276 }
277 
configureLaunchFromItem(KConfigGroup cfg,KDevelop::ProjectBaseItem * item) const278 void NativeAppConfigType::configureLaunchFromItem ( KConfigGroup cfg, KDevelop::ProjectBaseItem* item ) const
279 {
280     cfg.writeEntry( ExecutePlugin::isExecutableEntry, false );
281     KDevelop::ProjectModel* model = KDevelop::ICore::self()->projectController()->projectModel();
282     cfg.writeEntry( ExecutePlugin::projectTargetEntry, model->pathFromIndex( model->indexFromItem( item ) ) );
283     cfg.writeEntry( ExecutePlugin::workingDirEntry, item->executable()->builtUrl().adjusted(QUrl::RemoveFilename) );
284     cfg.sync();
285 }
286 
configureLaunchFromCmdLineArguments(KConfigGroup cfg,const QStringList & args) const287 void NativeAppConfigType::configureLaunchFromCmdLineArguments ( KConfigGroup cfg, const QStringList& args ) const
288 {
289     cfg.writeEntry( ExecutePlugin::isExecutableEntry, true );
290 //  TODO: we probably want to flexibilize, but at least we won't be accepting wrong values anymore
291     cfg.writeEntry( ExecutePlugin::executableEntry, QUrl::fromLocalFile(args.first()) );
292     QStringList a(args);
293     a.removeFirst();
294     cfg.writeEntry( ExecutePlugin::argumentsEntry, KShell::joinArgs(a) );
295     cfg.sync();
296 }
297 
targetsInFolder(KDevelop::ProjectFolderItem * folder)298 QList<KDevelop::ProjectTargetItem*> targetsInFolder(KDevelop::ProjectFolderItem* folder)
299 {
300     QList<KDevelop::ProjectTargetItem*> ret;
301     const auto folders = folder->folderList();
302     for (KDevelop::ProjectFolderItem* f : folders) {
303         ret += targetsInFolder(f);
304     }
305 
306     ret += folder->targetList();
307     return ret;
308 }
309 
actionLess(QAction * a,QAction * b)310 bool actionLess(QAction* a, QAction* b)
311 {
312     return a->text() < b->text();
313 }
314 
menuLess(QMenu * a,QMenu * b)315 bool menuLess(QMenu* a, QMenu* b)
316 {
317     return a->title() < b->title();
318 }
319 
launcherSuggestions()320 QMenu* NativeAppConfigType::launcherSuggestions()
321 {
322     auto* ret = new QMenu(i18nc("@title:menu", "Project Executables"));
323 
324     KDevelop::ProjectModel* model = KDevelop::ICore::self()->projectController()->projectModel();
325     const QList<KDevelop::IProject*> projects = KDevelop::ICore::self()->projectController()->projects();
326 
327     for (KDevelop::IProject* project : projects) {
328         if(project->projectFileManager()->features() & KDevelop::IProjectFileManager::Targets) {
329             const QList<KDevelop::ProjectTargetItem*> targets = targetsInFolder(project->projectItem());
330             QHash<KDevelop::ProjectBaseItem*, QList<QAction*> > targetsContainer;
331             QMenu* projectMenu = ret->addMenu(QIcon::fromTheme(QStringLiteral("project-development")), project->name());
332             for (KDevelop::ProjectTargetItem* target : targets) {
333                 if(target->executable()) {
334                     QStringList path = model->pathFromIndex(target->index());
335                     if(!path.isEmpty()){
336                         auto* act = new QAction(projectMenu);
337                         act->setData(KDevelop::joinWithEscaping(path, QLatin1Char('/'), QLatin1Char('\\')));
338                         act->setProperty("name", target->text());
339                         path.removeFirst();
340                         act->setText(path.join(QLatin1Char('/')));
341                         act->setIcon(QIcon::fromTheme(QStringLiteral("system-run")));
342                         connect(act, &QAction::triggered, this, &NativeAppConfigType::suggestionTriggered);
343                         targetsContainer[target->parent()].append(act);
344                     }
345                 }
346             }
347 
348             QList<QAction*> separateActions;
349             QList<QMenu*> submenus;
350             for (auto it = targetsContainer.constBegin(), end = targetsContainer.constEnd(); it != end; ++it) {
351                 KDevelop::ProjectBaseItem* folder = it.key();
352                 QList<QAction*> actions = it.value();
353                 if(actions.size()==1 || !folder->parent()) {
354                     separateActions.append(actions);
355                 } else {
356                     for (QAction* a : qAsConst(actions)) {
357                         a->setText(a->property("name").toString());
358                     }
359                     QStringList path = model->pathFromIndex(folder->index());
360                     path.removeFirst();
361                     auto* submenu = new QMenu(path.join(QLatin1Char('/')), projectMenu);
362                     std::sort(actions.begin(), actions.end(), actionLess);
363                     submenu->addActions(actions);
364                     submenus += submenu;
365                 }
366             }
367             std::sort(separateActions.begin(), separateActions.end(), actionLess);
368             std::sort(submenus.begin(), submenus.end(), menuLess);
369             for (QMenu* m : qAsConst(submenus)) {
370                 projectMenu->addMenu(m);
371             }
372             projectMenu->addActions(separateActions);
373 
374             projectMenu->setEnabled(!projectMenu->isEmpty());
375         }
376     }
377 
378     return ret;
379 }
380 
suggestionTriggered()381 void NativeAppConfigType::suggestionTriggered()
382 {
383     auto* action = qobject_cast<QAction*>(sender());
384     KDevelop::ProjectModel* model = KDevelop::ICore::self()->projectController()->projectModel();
385     KDevelop::ProjectTargetItem* pitem = dynamic_cast<KDevelop::ProjectTargetItem*>(itemForPath(KDevelop::splitWithEscaping(action->data().toString(), QLatin1Char('/'), QLatin1Char('\\')), model));
386     if(pitem) {
387         QPair<QString,QString> launcher = qMakePair( launchers().at( 0 )->supportedModes().at(0), launchers().at( 0 )->id() );
388         KDevelop::IProject* p = pitem->project();
389 
390         KDevelop::ILaunchConfiguration* config = KDevelop::ICore::self()->runController()->createLaunchConfiguration(this, launcher, p, pitem->text());
391         KConfigGroup cfg = config->config();
392 
393         QStringList splitPath = model->pathFromIndex(pitem->index());
394 //         QString path = KDevelop::joinWithEscaping(splitPath,'/','\\');
395         cfg.writeEntry( ExecutePlugin::projectTargetEntry, splitPath );
396         cfg.writeEntry( ExecutePlugin::dependencyEntry, KDevelop::qvariantToString( QVariantList() << splitPath ) );
397         cfg.writeEntry( ExecutePlugin::dependencyActionEntry, "Build" );
398         cfg.sync();
399 
400         emit signalAddLaunchConfiguration(config);
401     }
402 }
403 
404