File indexing completed on 2024-04-28 04:37:20

0001 /*
0002     SPDX-FileCopyrightText: 2008 Andreas Pakulat <apaku@gmx.de>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "openprojectdialog.h"
0008 #include "openprojectpage.h"
0009 #include "projectinfopage.h"
0010 
0011 #include <QPushButton>
0012 #include <QFileInfo>
0013 #include <QFileDialog>
0014 
0015 #include <KColorScheme>
0016 #include <KIO/StatJob>
0017 #include <KIO/ListJob>
0018 #include <KJobWidgets>
0019 #include <KLocalizedString>
0020 
0021 #include "core.h"
0022 #include "uicontroller.h"
0023 #include "plugincontroller.h"
0024 #include "mainwindow.h"
0025 #include "shellextension.h"
0026 #include "projectsourcepage.h"
0027 #include <debug.h>
0028 #include <interfaces/iprojectcontroller.h>
0029 
0030 namespace
0031 {
0032 struct URLInfo
0033 {
0034     bool isValid;
0035     bool isDir;
0036     QString extension;
0037 };
0038 
0039 URLInfo urlInfo(const QUrl& url)
0040 {
0041     URLInfo ret;
0042     ret.isValid = false;
0043 
0044     if (url.isLocalFile()) {
0045         QFileInfo info(url.toLocalFile());
0046         ret.isValid = info.exists();
0047         if (ret.isValid) {
0048             ret.isDir = info.isDir();
0049             ret.extension = info.suffix();
0050         }
0051     } else if (url.isValid()) {
0052         KIO::StatJob* statJob = KIO::stat(url, KIO::HideProgressInfo);
0053         KJobWidgets::setWindow(statJob, KDevelop::Core::self()->uiControllerInternal()->defaultMainWindow());
0054         ret.isValid = statJob->exec(); // TODO: do this asynchronously so that the user isn't blocked while typing every letter of the hostname in sftp://hostname
0055         if (ret.isValid) {
0056             KIO::UDSEntry entry = statJob->statResult();
0057             ret.isDir = entry.isDir();
0058             ret.extension = QFileInfo(entry.stringValue(KIO::UDSEntry::UDS_NAME)).suffix();
0059         }
0060     }
0061     return ret;
0062 }
0063 }
0064 
0065 namespace KDevelop
0066 {
0067 
0068 OpenProjectDialog::OpenProjectDialog(bool fetch, const QUrl& startUrl,
0069                                      const QUrl& repoUrl, IPlugin* vcsOrProviderPlugin,
0070                                      QWidget* parent)
0071     : KAssistantDialog( parent )
0072     , m_urlIsDirectory(false)
0073     , sourcePage(nullptr)
0074     , openPage(nullptr)
0075     , projectInfoPage(nullptr)
0076 {
0077     resize(QSize(700, 500));
0078 
0079     // KAssistantDialog creates a help button by default, no option to prevent that
0080     auto helpButton = button(QDialogButtonBox::Help);
0081     if (helpButton) {
0082         buttonBox()->removeButton(helpButton);
0083         delete helpButton;
0084     }
0085 
0086     const bool useKdeFileDialog = qEnvironmentVariableIsSet("KDE_FULL_SESSION");
0087     QStringList filters, allEntry;
0088     QString filterFormat = useKdeFileDialog
0089                          ? QStringLiteral("%1|%2 (%1)")
0090                          : QStringLiteral("%2 (%1)");
0091     allEntry << QLatin1String("*.") + ShellExtension::getInstance()->projectFileExtension();
0092     filters << filterFormat.arg(QLatin1String("*.") + ShellExtension::getInstance()->projectFileExtension(), ShellExtension::getInstance()->projectFileDescription());
0093     const QVector<KPluginMetaData> plugins = ICore::self()->pluginController()->queryExtensionPlugins(QStringLiteral("org.kdevelop.IProjectFileManager"));
0094     for (const KPluginMetaData& info : plugins) {
0095         m_projectPlugins.insert(info.name(), info);
0096 
0097         const auto filter = info.value(QStringLiteral("X-KDevelop-ProjectFilesFilter"), QStringList());
0098 
0099         // some project file manager plugins like KDevGenericManager have no file filter set
0100         if (filter.isEmpty()) {
0101             m_genericProjectPlugins << info.name();
0102             continue;
0103         }
0104         QString desc = info.value(QStringLiteral("X-KDevelop-ProjectFilesFilterDescription"));
0105 
0106         m_projectFilters.insert(info.name(), filter);
0107         allEntry += filter;
0108         filters << filterFormat.arg(filter.join(QLatin1Char(' ')), desc);
0109     }
0110 
0111     if (useKdeFileDialog)
0112         filters.prepend(i18n("%1|All Project Files (%1)", allEntry.join(QLatin1Char(' '))));
0113 
0114     QUrl start = startUrl.isValid() ? startUrl : Core::self()->projectController()->projectsBaseDirectory();
0115     start = start.adjusted(QUrl::NormalizePathSegments);
0116     KPageWidgetItem* currentPage = nullptr;
0117 
0118     if( fetch ) {
0119         sourcePageWidget = new ProjectSourcePage(start, repoUrl, vcsOrProviderPlugin, this);
0120         connect( sourcePageWidget, &ProjectSourcePage::isCorrect, this, &OpenProjectDialog::validateSourcePage );
0121         sourcePage = addPage( sourcePageWidget, i18nc("@title:tab", "Select Source") );
0122         currentPage = sourcePage;
0123     }
0124 
0125     if (useKdeFileDialog) {
0126         openPageWidget = new OpenProjectPage( start, filters, this );
0127         connect( openPageWidget, &OpenProjectPage::urlSelected, this, &OpenProjectDialog::validateOpenUrl );
0128         connect( openPageWidget, &OpenProjectPage::accepted, this, &OpenProjectDialog::openPageAccepted );
0129         openPage = addPage( openPageWidget, i18n("Select a build system setup file, existing KDevelop project, "
0130                                                  "or any folder to open as a project") );
0131 
0132         if (!currentPage) {
0133             currentPage = openPage;
0134         }
0135     } else {
0136         nativeDialog.assign(parent, i18nc("@title:window", "Open Project"));
0137         nativeDialog->setDirectoryUrl(start);
0138         nativeDialog->setFileMode(QFileDialog::Directory);
0139     }
0140 
0141     auto* page = new ProjectInfoPage( this );
0142     connect( page, &ProjectInfoPage::projectNameChanged, this, &OpenProjectDialog::validateProjectName );
0143     connect( page, &ProjectInfoPage::projectManagerChanged, this, &OpenProjectDialog::validateProjectManager );
0144     projectInfoPage = addPage( page, i18nc("@title:tab", "Project Information") );
0145 
0146     if (!currentPage) {
0147         currentPage = projectInfoPage;
0148     }
0149 
0150     setValid( sourcePage, false );
0151     setValid( openPage, false );
0152     setValid( projectInfoPage, false);
0153     setAppropriate( projectInfoPage, false );
0154 
0155     setCurrentPage( currentPage );
0156     setWindowTitle(i18nc("@title:window", "Open Project"));
0157 }
0158 
0159 OpenProjectDialog::~OpenProjectDialog() = default;
0160 
0161 bool OpenProjectDialog::execNativeDialog()
0162 {
0163     while (true)
0164     {
0165         if (nativeDialog->exec()) {
0166             QUrl selectedUrl = nativeDialog->selectedUrls().at(0);
0167             if (urlInfo(selectedUrl).isValid) {
0168                 // validate directory first to populate m_projectName and m_projectManager
0169                 validateOpenUrl(selectedUrl.adjusted(QUrl::RemoveFilename));
0170                 validateOpenUrl(selectedUrl);
0171                 return true;
0172             }
0173         }
0174         else {
0175             return false;
0176         }
0177     }
0178 }
0179 
0180 int OpenProjectDialog::exec()
0181 {
0182     if (nativeDialog && !execNativeDialog()) {
0183         reject();
0184         return QDialog::Rejected;
0185     }
0186     return KAssistantDialog::exec();
0187 }
0188 
0189 void OpenProjectDialog::validateSourcePage(bool valid)
0190 {
0191     setValid(sourcePage, valid);
0192     if (!nativeDialog) {
0193         openPageWidget->setUrl(sourcePageWidget->workingDir());
0194     }
0195 }
0196 
0197 void OpenProjectDialog::validateOpenUrl( const QUrl& url_ )
0198 {
0199     URLInfo urlInfo = ::urlInfo(url_);
0200 
0201     const QUrl url = url_.adjusted(QUrl::StripTrailingSlash);
0202 
0203     // openPage is used only in KDE
0204     if (openPage) {
0205         if ( urlInfo.isValid ) {
0206             // reset header
0207             openPage->setHeader(i18n("Open \"%1\" as project", url.fileName()));
0208         } else {
0209             // report error
0210             KColorScheme scheme(palette().currentColorGroup());
0211             const QString errorMsg = i18n("Selected URL is invalid");
0212             openPage->setHeader(QStringLiteral("<font color='%1'>%2</font>")
0213                 .arg(scheme.foreground(KColorScheme::NegativeText).color().name(), errorMsg)
0214             );
0215             setAppropriate( projectInfoPage, false );
0216             setAppropriate( openPage, true );
0217             setValid( openPage, false );
0218             return;
0219         }
0220     }
0221 
0222     m_selected = url;
0223 
0224     if( urlInfo.isDir || urlInfo.extension != ShellExtension::getInstance()->projectFileExtension() )
0225     {
0226         m_urlIsDirectory = urlInfo.isDir;
0227         setAppropriate( projectInfoPage, true );
0228         m_url = url;
0229         if( !urlInfo.isDir ) {
0230             m_url = m_url.adjusted(QUrl::StripTrailingSlash | QUrl::RemoveFilename);
0231         }
0232         auto* page = qobject_cast<ProjectInfoPage*>( projectInfoPage->widget() );
0233         if( page )
0234         {
0235             page->setProjectName( m_url.fileName() );
0236             // clear the filelist
0237             m_fileList.clear();
0238 
0239             if( urlInfo.isDir ) {
0240                 // If a dir was selected fetch all files in it
0241                 KIO::ListJob* job = KIO::listDir( m_url );
0242                 connect( job, &KIO::ListJob::entries,
0243                                 this, &OpenProjectDialog::storeFileList);
0244                 KJobWidgets::setWindow(job, Core::self()->uiController()->activeMainWindow());
0245                 job->exec();
0246             } else {
0247                 // Else we'll just take the given file
0248                 m_fileList << url.fileName();
0249             }
0250             // Now find a manager for the file(s) in our filelist.
0251             QVector<ProjectFileChoice> choices;
0252             for (const auto& file : qAsConst(m_fileList)) {
0253                 auto plugins = projectManagerForFile(file);
0254                 if ( plugins.contains(QStringLiteral("<built-in>")) ) {
0255                     plugins.removeAll(QStringLiteral("<built-in>"));
0256                     choices.append({i18nc("@item:inlistbox", "Open existing file \"%1\"", file), QStringLiteral("<built-in>"), QString(), QString()});
0257                 }
0258                 choices.reserve(choices.size() + plugins.size());
0259                 for (const auto& plugin : qAsConst(plugins)) {
0260                     auto meta = m_projectPlugins.value(plugin);
0261                     choices.append({file + QLatin1String(" (") + plugin + QLatin1Char(')'), meta.pluginId(), meta.iconName(), file});
0262                 }
0263             }
0264             // add managers that work in any case (e.g. KDevGenericManager)
0265                 choices.reserve(choices.size() + m_genericProjectPlugins.size());
0266             for (const auto& plugin : qAsConst(m_genericProjectPlugins)) {
0267                 qCDebug(SHELL) << plugin;
0268                 auto meta = m_projectPlugins.value(plugin);
0269                 choices.append({plugin, meta.pluginId(), meta.iconName(), QString()});
0270             }
0271             page->populateProjectFileCombo(choices);
0272         }
0273         m_url.setPath( m_url.path() + QLatin1Char('/') + m_url.fileName() + QLatin1Char('.') + ShellExtension::getInstance()->projectFileExtension() );
0274     } else {
0275         setAppropriate( projectInfoPage, false );
0276         m_url = url;
0277         m_urlIsDirectory = false;
0278     }
0279     validateProjectInfo();
0280     setValid( openPage, true );
0281 }
0282 
0283 QStringList OpenProjectDialog::projectManagerForFile(const QString& file) const
0284 {
0285     QStringList ret;
0286     for (auto it = m_projectFilters.begin(), end = m_projectFilters.end(); it != end; ++it) {
0287         const QString& manager = it.key();
0288         for (const QString& filterexp : it.value()) {
0289             QRegExp exp( filterexp, Qt::CaseSensitive, QRegExp::Wildcard );
0290             if ( exp.exactMatch(file) ) {
0291                 ret.append(manager);
0292             }
0293         }
0294     }
0295     if ( file.endsWith(ShellExtension::getInstance()->projectFileExtension()) ) {
0296         ret.append(QStringLiteral("<built-in>"));
0297     }
0298     return ret;
0299 }
0300 
0301 void OpenProjectDialog::openPageAccepted()
0302 {
0303     if ( isValid( openPage ) ) {
0304         next();
0305     }
0306 }
0307 
0308 void OpenProjectDialog::validateProjectName( const QString& name )
0309 {
0310     m_projectName = name;
0311     validateProjectInfo();
0312 }
0313 
0314 void OpenProjectDialog::validateProjectInfo()
0315 {
0316     setValid( projectInfoPage, (!projectName().isEmpty() && !projectManager().isEmpty()) );
0317 }
0318 
0319 void OpenProjectDialog::validateProjectManager( const QString& manager, const QString & fileName )
0320 {
0321     m_projectManager = manager;
0322     
0323     if ( m_urlIsDirectory )
0324     {
0325         m_selected = m_url.resolved( QUrl(QLatin1String("./") + fileName) );
0326     }
0327     
0328     validateProjectInfo();
0329 }
0330 
0331 QUrl OpenProjectDialog::projectFileUrl() const
0332 {
0333     return m_url;
0334 }
0335 
0336 QUrl OpenProjectDialog::selectedUrl() const
0337 {
0338     return m_selected;
0339 }
0340 
0341 QString OpenProjectDialog::projectName() const
0342 {
0343     return m_projectName;
0344 }
0345 
0346 QString OpenProjectDialog::projectManager() const
0347 {
0348     return m_projectManager;
0349 }
0350 
0351 void OpenProjectDialog::storeFileList(KIO::Job*, const KIO::UDSEntryList& list)
0352 {
0353     for (const KIO::UDSEntry& entry : list) {
0354         QString name = entry.stringValue( KIO::UDSEntry::UDS_NAME );
0355         if( name != QLatin1String(".") && name != QLatin1String("..") && !entry.isDir() )
0356         {
0357             m_fileList << name;
0358         }
0359     }
0360 }
0361 
0362 }
0363 
0364 #include "moc_openprojectdialog.cpp"