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