1 /*
2 SPDX-FileCopyrightText: 2001 Bernd Gehrmann <bernd@kdevelop.org>
3 SPDX-FileCopyrightText: 2004-2005 Sascha Cunz <sascha@kdevelop.org>
4 SPDX-FileCopyrightText: 2005 Ian Reinhart Geiser <ian@geiseri.com>
5 SPDX-FileCopyrightText: 2007 Alexander Dymo <adymo@kdevelop.org>
6 SPDX-FileCopyrightText: 2008 Evgeniy Ivanov <powerfox@kde.ru>
7
8 SPDX-License-Identifier: GPL-2.0-or-later
9 */
10
11 #include "appwizardplugin.h"
12
13 #include <QAction>
14 #include <QDir>
15 #include <QDirIterator>
16 #include <QFile>
17 #include <QFileInfo>
18 #include <QMimeType>
19 #include <QMimeDatabase>
20 #include <QStandardPaths>
21 #include <QTemporaryDir>
22 #include <QTextCodec>
23 #include <QTextStream>
24 #include <qplatformdefs.h>
25
26 #include <KActionCollection>
27 #include <KConfigGroup>
28 #include <KIO/CopyJob>
29 #include <KIO/DeleteJob>
30 #include <KLocalizedString>
31 #include <KMessageBox>
32 #include <KParts/MainWindow>
33 #include <KPluginFactory>
34 #include <KSharedConfig>
35 #include <KTar>
36 #include <KZip>
37 #include <KMacroExpander>
38
39 #include <interfaces/icore.h>
40 #include <interfaces/iprojectcontroller.h>
41 #include <interfaces/iplugincontroller.h>
42 #include <interfaces/iuicontroller.h>
43 #include <interfaces/idocumentcontroller.h>
44 #include <interfaces/context.h>
45 #include <interfaces/contextmenuextension.h>
46 #include <util/scopeddialog.h>
47 #include <sublime/message.h>
48 #include <vcs/vcsjob.h>
49 #include <vcs/interfaces/icentralizedversioncontrol.h>
50 #include <vcs/interfaces/idistributedversioncontrol.h>
51
52 #include "appwizarddialog.h"
53 #include "projectselectionpage.h"
54 #include "projectvcspage.h"
55 #include "projecttemplatesmodel.h"
56 #include "debug.h"
57
58 using namespace KDevelop;
59
60 K_PLUGIN_FACTORY_WITH_JSON(AppWizardFactory, "kdevappwizard.json", registerPlugin<AppWizardPlugin>();)
61
AppWizardPlugin(QObject * parent,const QVariantList &)62 AppWizardPlugin::AppWizardPlugin(QObject *parent, const QVariantList &)
63 : KDevelop::IPlugin(QStringLiteral("kdevappwizard"), parent)
64 {
65 setXMLFile(QStringLiteral("kdevappwizard.rc"));
66
67 m_newFromTemplate = actionCollection()->addAction(QStringLiteral("project_new"));
68 m_newFromTemplate->setIcon(QIcon::fromTheme(QStringLiteral("project-development-new-template")));
69 m_newFromTemplate->setText(i18nc("@action", "New from Template..."));
70 connect(m_newFromTemplate, &QAction::triggered, this, &AppWizardPlugin::slotNewProject);
71 m_newFromTemplate->setToolTip( i18nc("@info:tooltip", "Generate a new project from a template") );
72 m_newFromTemplate->setWhatsThis( i18nc("@info:whatsthis", "This starts KDevelop's application wizard. "
73 "It helps you to generate a skeleton for your "
74 "application from a set of templates.") );
75 }
76
~AppWizardPlugin()77 AppWizardPlugin::~AppWizardPlugin()
78 {
79 }
80
slotNewProject()81 void AppWizardPlugin::slotNewProject()
82 {
83 model()->refresh();
84
85 ScopedDialog<AppWizardDialog> dlg(core()->pluginController(), m_templatesModel);
86
87 if (dlg->exec() == QDialog::Accepted)
88 {
89 QString project = createProject( dlg->appInfo() );
90 if (!project.isEmpty())
91 {
92 core()->projectController()->openProject(QUrl::fromLocalFile(project));
93
94 KConfig templateConfig(dlg->appInfo().appTemplate);
95 KConfigGroup general(&templateConfig, "General");
96 #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
97 const QStringList fileArgs = general.readEntry("ShowFilesAfterGeneration").split(QLatin1Char(','), Qt::SkipEmptyParts);
98 #else
99 const QStringList fileArgs = general.readEntry("ShowFilesAfterGeneration").split(QLatin1Char(','), QString::SkipEmptyParts);
100 #endif
101 for (const auto& fileArg : fileArgs) {
102 QString file = KMacroExpander::expandMacros(fileArg.trimmed(), m_variables);
103 if (QDir::isRelativePath(file)) {
104 file = m_variables[QStringLiteral("PROJECTDIR")] + QLatin1Char('/') + file;
105 }
106 core()->documentController()->openDocument(QUrl::fromUserInput(file));
107 }
108 } else {
109 const QString messageText = i18n("Could not create project from template.");
110 auto* message = new Sublime::Message(messageText, Sublime::Message::Error);
111 ICore::self()->uiController()->postMessage(message);
112 }
113 }
114 }
115
116 namespace
117 {
118
toDVCS(IPlugin * plugin)119 IDistributedVersionControl* toDVCS(IPlugin* plugin)
120 {
121 Q_ASSERT(plugin);
122 return plugin->extension<IDistributedVersionControl>();
123 }
124
toCVCS(IPlugin * plugin)125 ICentralizedVersionControl* toCVCS(IPlugin* plugin)
126 {
127 Q_ASSERT(plugin);
128 return plugin->extension<ICentralizedVersionControl>();
129 }
130
131 /*! Trouble while initializing version control. Show failure message to user. */
vcsError(const QString & errorMsg,QTemporaryDir & tmpdir,const QUrl & dest,const QString & details=QString ())132 void vcsError(const QString &errorMsg, QTemporaryDir &tmpdir, const QUrl &dest, const QString &details = QString())
133 {
134 QString displayDetails = details;
135 if (displayDetails.isEmpty())
136 {
137 displayDetails = i18n("Please see the Version Control tool view.");
138 }
139 KMessageBox::detailedError(nullptr, errorMsg, displayDetails, i18nc("@title:window", "Version Control System Error"));
140 KIO::del(dest, KIO::HideProgressInfo)->exec();
141 tmpdir.remove();
142 }
143
144 /*! Setup distributed version control for a new project defined by @p info. Use @p scratchArea for temporary files */
initializeDVCS(IDistributedVersionControl * dvcs,const ApplicationInfo & info,QTemporaryDir & scratchArea)145 bool initializeDVCS(IDistributedVersionControl* dvcs, const ApplicationInfo& info, QTemporaryDir& scratchArea)
146 {
147 Q_ASSERT(dvcs);
148 qCDebug(PLUGIN_APPWIZARD) << "DVCS system is used, just initializing DVCS";
149
150 const QUrl& dest = info.location;
151 //TODO: check if we want to handle KDevelop project files (like now) or only SRC dir
152 VcsJob* job = dvcs->init(dest);
153 if (!job || !job->exec() || job->status() != VcsJob::JobSucceeded)
154 {
155 vcsError(i18n("Could not initialize DVCS repository"), scratchArea, dest);
156 return false;
157 }
158 qCDebug(PLUGIN_APPWIZARD) << "Initializing DVCS repository:" << dest;
159
160 qCDebug(PLUGIN_APPWIZARD) << "Checking for valid files in the DVCS repository:" << dest;
161 job = dvcs->status({dest}, KDevelop::IBasicVersionControl::Recursive);
162 if (!job || !job->exec() || job->status() != VcsJob::JobSucceeded)
163 {
164 vcsError(i18n("Could not check status of the DVCS repository"), scratchArea, dest);
165 return false;
166 }
167
168 if (job->fetchResults().toList().isEmpty())
169 {
170 qCDebug(PLUGIN_APPWIZARD) << "No files to add, skipping commit in the DVCS repository:" << dest;
171 return true;
172 }
173
174 job = dvcs->add({dest}, KDevelop::IBasicVersionControl::Recursive);
175 if (!job || !job->exec() || job->status() != VcsJob::JobSucceeded)
176 {
177 vcsError(i18n("Could not add files to the DVCS repository"), scratchArea, dest);
178 return false;
179 }
180
181 job = dvcs->commit(info.importCommitMessage, {dest},
182 KDevelop::IBasicVersionControl::Recursive);
183 if (!job || !job->exec() || job->status() != VcsJob::JobSucceeded)
184 {
185 vcsError(i18n("Could not import project into %1.", dvcs->name()), scratchArea, dest, job ? job->errorString() : QString());
186 return false;
187 }
188
189 return true; // We're good
190 }
191
192 /*! Setup version control for a new project defined by @p info. Use @p scratchArea for temporary files */
initializeCVCS(ICentralizedVersionControl * cvcs,const ApplicationInfo & info,QTemporaryDir & scratchArea)193 bool initializeCVCS(ICentralizedVersionControl* cvcs, const ApplicationInfo& info, QTemporaryDir& scratchArea)
194 {
195 Q_ASSERT(cvcs);
196
197 qCDebug(PLUGIN_APPWIZARD) << "Importing" << info.sourceLocation << "to"
198 << info.repository.repositoryServer();
199 VcsJob* job = cvcs->import( info.importCommitMessage, QUrl::fromLocalFile(scratchArea.path()), info.repository);
200 if (!job || !job->exec() || job->status() != VcsJob::JobSucceeded )
201 {
202 vcsError(i18n("Could not import project"), scratchArea, QUrl::fromUserInput(info.repository.repositoryServer()));
203 return false;
204 }
205
206 qCDebug(PLUGIN_APPWIZARD) << "Checking out";
207 job = cvcs->createWorkingCopy( info.repository, info.location, IBasicVersionControl::Recursive);
208 if (!job || !job->exec() || job->status() != VcsJob::JobSucceeded )
209 {
210 vcsError(i18n("Could not checkout imported project"), scratchArea, QUrl::fromUserInput(info.repository.repositoryServer()));
211 return false;
212 }
213
214 return true; // initialization phase complete
215 }
216
generateIdentifier(const QString & appname)217 QString generateIdentifier( const QString& appname )
218 {
219 QString tmp = appname;
220 QRegExp re(QStringLiteral("[^a-zA-Z0-9_]"));
221 return tmp.replace(re, QStringLiteral("_"));
222 }
223
224 } // end anonymous namespace
225
createProject(const ApplicationInfo & info)226 QString AppWizardPlugin::createProject(const ApplicationInfo& info)
227 {
228 QFileInfo templateInfo(info.appTemplate);
229 if (!templateInfo.exists()) {
230 qCWarning(PLUGIN_APPWIZARD) << "Project app template does not exist:" << info.appTemplate;
231 return QString();
232 }
233
234 QString templateName = templateInfo.baseName();
235 qCDebug(PLUGIN_APPWIZARD) << "Searching archive for template name:" << templateName;
236
237 QString templateArchive;
238 const QStringList filters = {templateName + QStringLiteral(".*")};
239 const QStringList matchesPaths = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("kdevappwizard/templates/"), QStandardPaths::LocateDirectory);
240 for (const QString& matchesPath : matchesPaths) {
241 const QStringList files = QDir(matchesPath).entryList(filters);
242 if(!files.isEmpty()) {
243 templateArchive = matchesPath + files.first();
244 }
245 }
246
247 if(templateArchive.isEmpty()) {
248 qCWarning(PLUGIN_APPWIZARD) << "Template name does not exist in the template list";
249 return QString();
250 }
251
252 qCDebug(PLUGIN_APPWIZARD) << "Using template archive:" << templateArchive;
253
254 QUrl dest = info.location;
255
256 //prepare variable substitution hash
257 m_variables.clear();
258 m_variables[QStringLiteral("APPNAME")] = info.name;
259 m_variables[QStringLiteral("APPNAMEUC")] = generateIdentifier(info.name.toUpper());
260 m_variables[QStringLiteral("APPNAMELC")] = info.name.toLower();
261 m_variables[QStringLiteral("APPNAMEID")] = generateIdentifier(info.name);
262 m_variables[QStringLiteral("PROJECTDIR")] = dest.toLocalFile();
263 // backwards compatibility
264 m_variables[QStringLiteral("dest")] = m_variables[QStringLiteral("PROJECTDIR")];
265 m_variables[QStringLiteral("PROJECTDIRNAME")] = dest.fileName();
266 m_variables[QStringLiteral("VERSIONCONTROLPLUGIN")] = info.vcsPluginName;
267
268 KArchive* arch = nullptr;
269 if( templateArchive.endsWith(QLatin1String(".zip")) )
270 {
271 arch = new KZip(templateArchive);
272 }
273 else
274 {
275 arch = new KTar(templateArchive, QStringLiteral("application/x-bzip"));
276 }
277 if (arch->open(QIODevice::ReadOnly))
278 {
279 QTemporaryDir tmpdir;
280 QString unpackDir = tmpdir.path(); //the default value for all Centralized VCS
281 IPlugin* plugin = core()->pluginController()->loadPlugin( info.vcsPluginName );
282 if( info.vcsPluginName.isEmpty() || ( plugin && plugin->extension<KDevelop::IDistributedVersionControl>() ) )
283 {
284 if( !QFileInfo::exists( dest.toLocalFile() ) )
285 {
286 QDir::root().mkpath( dest.toLocalFile() );
287 }
288 unpackDir = dest.toLocalFile(); //in DVCS we unpack template directly to the project's directory
289 }
290 else
291 {
292 QUrl url = KIO::upUrl(dest);
293 if(!QFileInfo::exists(url.toLocalFile())) {
294 QDir::root().mkpath(url.toLocalFile());
295 }
296 }
297
298 // estimate metadata files which should not be copied
299 QStringList metaDataFileNames;
300
301 // try by same name
302 const KArchiveEntry *templateEntry =
303 arch->directory()->entry(templateName + QLatin1String(".kdevtemplate"));
304
305 // but could be different name, if e.g. downloaded, so make a guess
306 if (!templateEntry || !templateEntry->isFile()) {
307 const auto& entries = arch->directory()->entries();
308 for (const auto& entryName : entries) {
309 if (entryName.endsWith(QLatin1String(".kdevtemplate"))) {
310 templateEntry = arch->directory()->entry(entryName);
311 break;
312 }
313 }
314 }
315
316 if (templateEntry && templateEntry->isFile()) {
317 metaDataFileNames << templateEntry->name();
318
319 // check if a preview file is to be ignored
320 const auto *templateFile = static_cast<const KArchiveFile*>(templateEntry);
321 QTemporaryDir temporaryDir;
322 templateFile->copyTo(temporaryDir.path());
323
324 KConfig config(temporaryDir.path() + QLatin1Char('/') + templateEntry->name());
325 KConfigGroup group(&config, "General");
326 if (group.hasKey("Icon")) {
327 const KArchiveEntry* iconEntry = arch->directory()->entry(group.readEntry("Icon"));
328 if (iconEntry && iconEntry->isFile()) {
329 metaDataFileNames << iconEntry->name();
330 }
331 }
332 }
333
334 if (!unpackArchive(arch->directory(), unpackDir, metaDataFileNames)) {
335 QString errorMsg = i18n("Could not create new project");
336 vcsError(errorMsg, tmpdir, QUrl::fromLocalFile(unpackDir));
337 return QString();
338 }
339
340 if( !info.vcsPluginName.isEmpty() )
341 {
342 if (!plugin)
343 {
344 // Red Alert, serious program corruption.
345 // This should never happen, the vcs dialog presented a list of vcs
346 // systems and now the chosen system doesn't exist anymore??
347 tmpdir.remove();
348 return QString();
349 }
350
351 IDistributedVersionControl* dvcs = toDVCS(plugin);
352 ICentralizedVersionControl* cvcs = toCVCS(plugin);
353 bool success = false;
354 if (dvcs)
355 {
356 success = initializeDVCS(dvcs, info, tmpdir);
357 }
358 else if (cvcs)
359 {
360 success = initializeCVCS(cvcs, info, tmpdir);
361 }
362 else
363 {
364 if (KMessageBox::Continue ==
365 KMessageBox::warningContinueCancel(nullptr,
366 QStringLiteral("Failed to initialize version control system, "
367 "plugin is neither VCS nor DVCS.")))
368 success = true;
369 }
370 if (!success) return QString();
371 }
372 tmpdir.remove();
373 }else
374 {
375 qCDebug(PLUGIN_APPWIZARD) << "failed to open template archive";
376 return QString();
377 }
378
379 QString projectFileName = QDir::cleanPath(dest.toLocalFile() + QLatin1Char('/') + info.name + QLatin1String(".kdev4"));
380
381 // Loop through the new project directory and try to detect the first .kdev4 file.
382 // If one is found this file will be used. So .kdev4 file can be stored in any subdirectory and the
383 // project templates can be more complex.
384 QDirIterator it(QDir::cleanPath( dest.toLocalFile()), QStringList() << QStringLiteral("*.kdev4"), QDir::NoFilter, QDirIterator::Subdirectories);
385 if(it.hasNext() == true)
386 {
387 projectFileName = it.next();
388 }
389
390 qCDebug(PLUGIN_APPWIZARD) << "Returning" << projectFileName << QFileInfo::exists( projectFileName ) ;
391
392 const QFileInfo projectFileInfo(projectFileName);
393 if (!projectFileInfo.exists()) {
394 qCDebug(PLUGIN_APPWIZARD) << "creating .kdev4 file";
395 KSharedConfigPtr cfg = KSharedConfig::openConfig( projectFileName, KConfig::SimpleConfig );
396 KConfigGroup project = cfg->group( "Project" );
397 project.writeEntry( "Name", info.name );
398 QString manager = QStringLiteral("KDevGenericManager");
399
400 QDir d( dest.toLocalFile() );
401 const auto data = ICore::self()->pluginController()->queryExtensionPlugins(QStringLiteral("org.kdevelop.IProjectFileManager"));
402 for (const KPluginMetaData& info : data) {
403 QStringList filter = KPluginMetaData::readStringList(info.rawData(), QStringLiteral("X-KDevelop-ProjectFilesFilter"));
404 if (!filter.isEmpty()) {
405 if (!d.entryList(filter).isEmpty()) {
406 manager = info.pluginId();
407 break;
408 }
409 }
410 }
411 project.writeEntry( "Manager", manager );
412 project.sync();
413 cfg->sync();
414 KConfigGroup project2 = cfg->group( "Project" );
415 qCDebug(PLUGIN_APPWIZARD) << "kdev4 file contents:" << project2.readEntry("Name", "") << project2.readEntry("Manager", "" );
416 }
417
418 // create developer .kde4 file
419 const QString developerProjectFileName = projectFileInfo.canonicalPath() + QLatin1String("/.kdev4/") + projectFileInfo.fileName();
420
421 qCDebug(PLUGIN_APPWIZARD) << "creating developer .kdev4 file:" << developerProjectFileName;
422 KSharedConfigPtr developerCfg = KSharedConfig::openConfig(developerProjectFileName, KConfig::SimpleConfig);
423 KConfigGroup developerProjectGroup = developerCfg->group("Project");
424 developerProjectGroup.writeEntry("VersionControlSupport", info.vcsPluginName);
425 developerProjectGroup.sync();
426
427 developerCfg->sync();
428
429 return projectFileName;
430 }
431
unpackArchive(const KArchiveDirectory * dir,const QString & dest,const QStringList & skipList)432 bool AppWizardPlugin::unpackArchive(const KArchiveDirectory* dir, const QString& dest, const QStringList& skipList)
433 {
434 qCDebug(PLUGIN_APPWIZARD) << "unpacking dir:" << dir->name() << "to" << dest;
435 const QStringList entries = dir->entries();
436 qCDebug(PLUGIN_APPWIZARD) << "entries:" << entries.join(QLatin1Char(','));
437
438 //This extra tempdir is needed just for the files that have special names,
439 //which may contain macros also files contain content with macros. So the
440 //easiest way to extract the files from the archive and then rename them
441 //and replace the macros is to use a tempdir and copy the file (and
442 //replacing while copying). This also allows one to easily remove all files,
443 //by just unlinking the tempdir
444 QTemporaryDir tdir;
445
446 bool ret = true;
447
448 for (const auto& entryName : entries) {
449 if (skipList.contains(entryName)) {
450 continue;
451 }
452
453 const auto entry = dir->entry(entryName);
454 if (entry->isDirectory()) {
455 const auto* subdir = static_cast<const KArchiveDirectory*>(entry);
456 const QString newdest = dest + QLatin1Char('/') + KMacroExpander::expandMacros(subdir->name(), m_variables);
457 if( !QFileInfo::exists( newdest ) )
458 {
459 QDir::root().mkdir( newdest );
460 }
461 ret |= unpackArchive(subdir, newdest);
462 }
463 else if (entry->isFile()) {
464 const auto* file = static_cast<const KArchiveFile*>(entry);
465 file->copyTo(tdir.path());
466 const QString destName = dest + QLatin1Char('/') + file->name();
467 if (!copyFileAndExpandMacros(QDir::cleanPath(tdir.path() + QLatin1Char('/') + file->name()),
468 KMacroExpander::expandMacros(destName, m_variables)))
469 {
470 KMessageBox::sorry(nullptr, i18n("The file %1 cannot be created.", dest));
471 return false;
472 }
473 }
474 }
475 tdir.remove();
476 return ret;
477 }
478
copyFileAndExpandMacros(const QString & source,const QString & dest)479 bool AppWizardPlugin::copyFileAndExpandMacros(const QString &source, const QString &dest)
480 {
481 qCDebug(PLUGIN_APPWIZARD) << "copy:" << source << "to" << dest;
482 QMimeDatabase db;
483 QMimeType mime = db.mimeTypeForFile(source);
484 if( !mime.inherits(QStringLiteral("text/plain")) )
485 {
486 KIO::CopyJob* job = KIO::copy( QUrl::fromUserInput(source), QUrl::fromUserInput(dest), KIO::HideProgressInfo );
487 if( !job->exec() )
488 {
489 return false;
490 }
491 return true;
492 } else
493 {
494 QFile inputFile(source);
495 QFile outputFile(dest);
496
497
498 if (inputFile.open(QFile::ReadOnly) && outputFile.open(QFile::WriteOnly))
499 {
500 QTextStream input(&inputFile);
501 input.setCodec(QTextCodec::codecForName("UTF-8"));
502 QTextStream output(&outputFile);
503 output.setCodec(QTextCodec::codecForName("UTF-8"));
504 while(!input.atEnd())
505 {
506 QString line = input.readLine();
507 output << KMacroExpander::expandMacros(line, m_variables) << "\n";
508 }
509 #ifndef Q_OS_WIN
510 // Preserve file mode...
511 QT_STATBUF statBuf;
512 QT_FSTAT(inputFile.handle(), &statBuf);
513 // Unix only, won't work in Windows, maybe KIO::chmod could be used
514 ::fchmod(outputFile.handle(), statBuf.st_mode);
515 #endif
516 return true;
517 }
518 else
519 {
520 inputFile.close();
521 outputFile.close();
522 return false;
523 }
524 }
525 }
contextMenuExtension(KDevelop::Context * context,QWidget * parent)526 KDevelop::ContextMenuExtension AppWizardPlugin::contextMenuExtension(KDevelop::Context* context, QWidget* parent)
527 {
528 Q_UNUSED(parent);
529 KDevelop::ContextMenuExtension ext;
530 if ( context->type() != KDevelop::Context::ProjectItemContext || !static_cast<KDevelop::ProjectItemContext*>(context)->items().isEmpty() ) {
531 return ext;
532 }
533 ext.addAction(KDevelop::ContextMenuExtension::ProjectGroup, m_newFromTemplate);
534 return ext;
535 }
536
model() const537 ProjectTemplatesModel* AppWizardPlugin::model() const
538 {
539 if(!m_templatesModel) {
540 auto* self = const_cast<AppWizardPlugin*>(this);
541 m_templatesModel = new ProjectTemplatesModel(self);
542 }
543 return m_templatesModel;
544 }
545
templatesModel() const546 QAbstractItemModel* AppWizardPlugin::templatesModel() const
547 {
548 return model();
549 }
550
knsConfigurationFile() const551 QString AppWizardPlugin::knsConfigurationFile() const
552 {
553 return QStringLiteral("kdevappwizard.knsrc");
554 }
555
supportedMimeTypes() const556 QStringList AppWizardPlugin::supportedMimeTypes() const
557 {
558 const QStringList types{
559 QStringLiteral("application/x-desktop"),
560 QStringLiteral("application/x-bzip-compressed-tar"),
561 QStringLiteral("application/zip"),
562 };
563 return types;
564 }
565
icon() const566 QIcon AppWizardPlugin::icon() const
567 {
568 return QIcon::fromTheme(QStringLiteral("project-development-new-template"));
569 }
570
name() const571 QString AppWizardPlugin::name() const
572 {
573 return i18n("Project Templates");
574 }
575
loadTemplate(const QString & fileName)576 void AppWizardPlugin::loadTemplate(const QString& fileName)
577 {
578 model()->loadTemplateFile(fileName);
579 }
580
reload()581 void AppWizardPlugin::reload()
582 {
583 model()->refresh();
584 }
585
586
587 #include "appwizardplugin.moc"
588