diff --git a/plugins/bazaar/bazaarplugin.h b/plugins/bazaar/bazaarplugin.h --- a/plugins/bazaar/bazaarplugin.h +++ b/plugins/bazaar/bazaarplugin.h @@ -42,6 +42,8 @@ QString name() const override; + bool isValidRemoteRepositoryUrl(const QUrl& remoteLocation) override; + KDevelop::VcsJob* add(const QList& localLocations, RecursionMode recursion=Recursive) override; KDevelop::VcsJob* annotate(const QUrl& localLocation, const KDevelop::VcsRevision& rev) override; KDevelop::VcsJob* commit(const QString& message, const QList& localLocations, RecursionMode recursion=Recursive) override; diff --git a/plugins/bazaar/bazaarplugin.cpp b/plugins/bazaar/bazaarplugin.cpp --- a/plugins/bazaar/bazaarplugin.cpp +++ b/plugins/bazaar/bazaarplugin.cpp @@ -65,6 +65,16 @@ return QStringLiteral("Bazaar"); } +bool BazaarPlugin::isValidRemoteRepositoryUrl(const QUrl& remoteLocation) +{ + const QString scheme = remoteLocation.scheme(); + if (scheme == QLatin1String("bzr") || + scheme == QLatin1String("bzr+ssh")) { + return true; + } + return false; +} + VcsJob* BazaarPlugin::add(const QList& localLocations, IBasicVersionControl::RecursionMode recursion) { DVcsJob* job = new DVcsJob(BazaarUtils::workingCopy(localLocations[0]), this); diff --git a/plugins/cvs/cvsplugin.h b/plugins/cvs/cvsplugin.h --- a/plugins/cvs/cvsplugin.h +++ b/plugins/cvs/cvsplugin.h @@ -52,6 +52,7 @@ KDevelop::ContextMenuExtension contextMenuExtension(KDevelop::Context*) override; // Begin: KDevelop::IBasicVersionControl + bool isValidRemoteRepositoryUrl(const QUrl& remoteLocation) override; bool isVersionControlled(const QUrl& localLocation) override; KDevelop::VcsJob* repositoryLocation(const QUrl& localLocation) override; KDevelop::VcsJob* add(const QList& localLocations, diff --git a/plugins/cvs/cvsplugin.cpp b/plugins/cvs/cvsplugin.cpp --- a/plugins/cvs/cvsplugin.cpp +++ b/plugins/cvs/cvsplugin.cpp @@ -271,6 +271,13 @@ // Begin: KDevelop::IBasicVersionControl +bool CvsPlugin::isValidRemoteRepositoryUrl(const QUrl& remoteLocation) +{ + Q_UNUSED(remoteLocation); + // TODO + return false; +} + bool CvsPlugin::isVersionControlled(const QUrl & localLocation) { return d->m_proxy->isVersionControlled(localLocation); diff --git a/plugins/git/gitplugin.h b/plugins/git/gitplugin.h --- a/plugins/git/gitplugin.h +++ b/plugins/git/gitplugin.h @@ -77,6 +77,7 @@ QString name() const override; + bool isValidRemoteRepositoryUrl(const QUrl& remoteLocation) override; bool isVersionControlled(const QUrl &path) override; KDevelop::VcsJob* copy(const QUrl& localLocationSrc, const QUrl& localLocationDstn) override; diff --git a/plugins/git/gitplugin.cpp b/plugins/git/gitplugin.cpp --- a/plugins/git/gitplugin.cpp +++ b/plugins/git/gitplugin.cpp @@ -280,6 +280,32 @@ return dir.exists(QStringLiteral(".git/HEAD")); } +bool GitPlugin::isValidRemoteRepositoryUrl(const QUrl& remoteLocation) +{ + if (remoteLocation.isLocalFile()) { + QFileInfo fileInfo(remoteLocation.toLocalFile()); + if (fileInfo.isDir()) { + QDir dir(fileInfo.filePath()); + if (dir.exists(QStringLiteral(".git/HEAD"))) { + return true; + } + // TODO: check also for bare repo + } + } else { + const QString scheme = remoteLocation.scheme(); + if (scheme == QLatin1String("git")) { + return true; + } + // heuristic check, anything better we can do here without talking to server? + if ((scheme == QLatin1String("http") || + scheme == QLatin1String("https")) && + remoteLocation.path().endsWith(QLatin1String(".git"))) { + return true; + } + } + return false; +} + bool GitPlugin::isVersionControlled(const QUrl &path) { QFileInfo fsObject(path.toLocalFile()); diff --git a/plugins/perforce/perforceplugin.h b/plugins/perforce/perforceplugin.h --- a/plugins/perforce/perforceplugin.h +++ b/plugins/perforce/perforceplugin.h @@ -58,6 +58,7 @@ KDevelop::VcsImportMetadataWidget* createImportMetadataWidget(QWidget* parent) override; + bool isValidRemoteRepositoryUrl(const QUrl& remoteLocation) override; bool isVersionControlled(const QUrl& localLocation) override; KDevelop::VcsJob* repositoryLocation(const QUrl& localLocation) override; diff --git a/plugins/perforce/perforceplugin.cpp b/plugins/perforce/perforceplugin.cpp --- a/plugins/perforce/perforceplugin.cpp +++ b/plugins/perforce/perforceplugin.cpp @@ -145,6 +145,13 @@ return nullptr; } +bool PerforcePlugin::isValidRemoteRepositoryUrl(const QUrl& remoteLocation) +{ + Q_UNUSED(remoteLocation); + // TODO + return false; +} + bool PerforcePlugin::isValidDirectory(const QUrl & dirPath) { const QFileInfo finfo(dirPath.toLocalFile()); diff --git a/plugins/subversion/kdevsvnplugin.h b/plugins/subversion/kdevsvnplugin.h --- a/plugins/subversion/kdevsvnplugin.h +++ b/plugins/subversion/kdevsvnplugin.h @@ -47,6 +47,7 @@ KDevelop::VcsImportMetadataWidget* createImportMetadataWidget(QWidget* parent) override; // Begin: KDevelop::IBasicVersionControl + bool isValidRemoteRepositoryUrl(const QUrl& remoteLocation) override; bool isVersionControlled(const QUrl &localLocation) override; KDevelop::VcsJob* repositoryLocation(const QUrl &localLocation) override; diff --git a/plugins/subversion/kdevsvnplugin.cpp b/plugins/subversion/kdevsvnplugin.cpp --- a/plugins/subversion/kdevsvnplugin.cpp +++ b/plugins/subversion/kdevsvnplugin.cpp @@ -91,6 +91,17 @@ { } +bool KDevSvnPlugin::isValidRemoteRepositoryUrl(const QUrl& remoteLocation) +{ + const QString scheme = remoteLocation.scheme(); + if (scheme == QLatin1String("svn") || + scheme == QLatin1String("svn+ssh")) { + return true; + } + return false; +} + + bool KDevSvnPlugin::isVersionControlled(const QUrl &localLocation) { ///TODO: also check this in the other functions? diff --git a/shell/mainwindow.cpp b/shell/mainwindow.cpp --- a/shell/mainwindow.cpp +++ b/shell/mainwindow.cpp @@ -59,6 +59,7 @@ #include #include +#include #include #include #include @@ -202,9 +203,15 @@ void MainWindow::dragEnterEvent( QDragEnterEvent* ev ) { - if( ev->mimeData()->hasFormat( QStringLiteral("text/uri-list") ) && ev->mimeData()->hasUrls() ) - { + const QMimeData* mimeData = ev->mimeData(); + if (mimeData->hasUrls()) { ev->acceptProposedAction(); + } else if (mimeData->hasText()) { + // also take text which contains a URL + const QUrl url = QUrl::fromUserInput(mimeData->text()); + if (url.isValid()) { + ev->acceptProposedAction(); + } } } @@ -214,10 +221,43 @@ if(dropToView) activateView(dropToView); - foreach( const QUrl& u, ev->mimeData()->urls() ) - { - Core::self()->documentController()->openDocument( u ); + QList urls; + + const QMimeData* mimeData = ev->mimeData(); + if (mimeData->hasUrls()) { + urls = mimeData->urls(); + } else if (mimeData->hasText()) { + const QUrl url = QUrl::fromUserInput(mimeData->text()); + if (url.isValid()) { + urls << url; + } } + + bool eventUsed = false; + if (urls.size() == 1) { + const QUrl& url = urls.at(0); + // TODO: query also projectprovider plugins, and that before plain vcs plugins + // e.g. KDE provider plugin could catch URLs from mirror or pickup kde:repo things + + auto* pluginController = Core::self()->pluginController(); + const auto& plugins = pluginController->allPluginsForExtension(QStringLiteral("org.kdevelop.IBasicVersionControl")); + + for (auto* plugin : plugins) { + auto* iface = plugin->extension(); + if (iface->isValidRemoteRepositoryUrl(url)) { + Core::self()->projectControllerInternal()->fetchProjectFromUrl(url, plugin); + eventUsed = true; + break; + } + } + } + + if (!eventUsed) { + for(const auto& url : urls) { + Core::self()->documentController()->openDocument(url); + } + } + ev->acceptProposedAction(); } diff --git a/shell/openprojectdialog.h b/shell/openprojectdialog.h --- a/shell/openprojectdialog.h +++ b/shell/openprojectdialog.h @@ -28,13 +28,16 @@ { class ProjectSourcePage; class OpenProjectPage; +class IPlugin; class OpenProjectDialog : public KAssistantDialog { Q_OBJECT public: - OpenProjectDialog( bool fetch, const QUrl& startUrl, QWidget* parent = nullptr ); + OpenProjectDialog(bool fetch, const QUrl& startUrl, + const QUrl& repoUrl = QUrl(), IPlugin* vcsOrProviderPlugin = nullptr, + QWidget* parent = nullptr); /** * Return a QUrl pointing to the project's .kdev file. diff --git a/shell/openprojectdialog.cpp b/shell/openprojectdialog.cpp --- a/shell/openprojectdialog.cpp +++ b/shell/openprojectdialog.cpp @@ -69,7 +69,9 @@ namespace KDevelop { -OpenProjectDialog::OpenProjectDialog( bool fetch, const QUrl& startUrl, QWidget* parent ) +OpenProjectDialog::OpenProjectDialog(bool fetch, const QUrl& startUrl, + const QUrl& repoUrl, IPlugin* vcsOrProviderPlugin, + QWidget* parent) : KAssistantDialog( parent ) , m_urlIsDirectory(false) , sourcePage(nullptr) @@ -112,7 +114,7 @@ KPageWidgetItem* currentPage = nullptr; if( fetch ) { - sourcePageWidget = new ProjectSourcePage( start, this ); + sourcePageWidget = new ProjectSourcePage(start, repoUrl, vcsOrProviderPlugin, this); connect( sourcePageWidget, &ProjectSourcePage::isCorrect, this, &OpenProjectDialog::validateSourcePage ); sourcePage = addPage( sourcePageWidget, i18n("Select Source") ); currentPage = sourcePage; diff --git a/shell/projectcontroller.h b/shell/projectcontroller.h --- a/shell/projectcontroller.h +++ b/shell/projectcontroller.h @@ -60,7 +60,8 @@ * @param fetch will tell the UI that the user might want to fetch the project first * @param startUrl tells where to look first */ - virtual QUrl askProjectConfigLocation(bool fetch, const QUrl& startUrl = QUrl()) = 0; + virtual QUrl askProjectConfigLocation(bool fetch, const QUrl& startUrl = QUrl(), + const QUrl& repoUrl = QUrl(), IPlugin* plugin = nullptr) = 0; virtual bool userWantsReopen() = 0; }; @@ -95,6 +96,8 @@ ContextMenuExtension contextMenuExtension( KDevelop::Context* ctx ); + void fetchProjectFromUrl(const QUrl& repoUrl, IPlugin* vcsOrProviderPlugin); + public Q_SLOTS: void openProjectForUrl( const QUrl &sourceUrl ) override; void fetchProject(); @@ -168,7 +171,8 @@ ProjectControllerPrivate* const d; public Q_SLOTS: - QUrl askProjectConfigLocation(bool fetch, const QUrl& sta) override; + QUrl askProjectConfigLocation(bool fetch, const QUrl& startUrl, + const QUrl& repoUrl, IPlugin* plugin) override; bool userWantsReopen() override; }; diff --git a/shell/projectcontroller.cpp b/shell/projectcontroller.cpp --- a/shell/projectcontroller.cpp +++ b/shell/projectcontroller.cpp @@ -413,10 +413,11 @@ grp.readEntry( "Manager", QString() ) == dlg->projectManager(); } -QUrl ProjectDialogProvider::askProjectConfigLocation(bool fetch, const QUrl& startUrl) +QUrl ProjectDialogProvider::askProjectConfigLocation(bool fetch, const QUrl& startUrl, + const QUrl& repoUrl, IPlugin* vcsOrProviderPlugin) { Q_ASSERT(d); - OpenProjectDialog dlg( fetch, startUrl, Core::self()->uiController()->activeMainWindow() ); + OpenProjectDialog dlg(fetch, startUrl, repoUrl, vcsOrProviderPlugin, Core::self()->uiController()->activeMainWindow()); if(dlg.exec() == QDialog::Rejected) return QUrl(); @@ -832,6 +833,15 @@ } } +void ProjectController::fetchProjectFromUrl(const QUrl& repoUrl, IPlugin* vcsOrProviderPlugin) +{ + const QUrl url = d->dialog->askProjectConfigLocation(true, QUrl(), repoUrl, vcsOrProviderPlugin); + + if (!url.isEmpty()) { + d->importProject(url); + } +} + void ProjectController::fetchProject() { QUrl url = d->dialog->askProjectConfigLocation(true); diff --git a/shell/projectsourcepage.h b/shell/projectsourcepage.h --- a/shell/projectsourcepage.h +++ b/shell/projectsourcepage.h @@ -29,7 +29,8 @@ { Q_OBJECT public: - explicit ProjectSourcePage(const QUrl& initial, QWidget* parent = nullptr); + explicit ProjectSourcePage(const QUrl& initial, const QUrl& repoUrl, IPlugin* preSelectPlugin, + QWidget* parent = nullptr); ~ProjectSourcePage() override; QUrl workingDir() const; @@ -48,6 +49,7 @@ void isCorrect(bool); private: + void setSourceWidget(int index, const QUrl& repoUrl); void setStatus(const QString& message); void clearStatus(); diff --git a/shell/projectsourcepage.cpp b/shell/projectsourcepage.cpp --- a/shell/projectsourcepage.cpp +++ b/shell/projectsourcepage.cpp @@ -31,7 +31,8 @@ static const int FROM_FILESYSTEM_SOURCE_INDEX = 0; -ProjectSourcePage::ProjectSourcePage(const QUrl& initial, QWidget* parent) +ProjectSourcePage::ProjectSourcePage(const QUrl& initial, const QUrl& repoUrl, IPlugin* preSelectPlugin, + QWidget* parent) : QWidget(parent) { m_ui = new Ui::ProjectSourcePage; @@ -47,33 +48,44 @@ m_ui->sources->addItem(QIcon::fromTheme(QStringLiteral("folder")), i18n("From File System")); m_plugins.append(nullptr); + int preselectIndex = -1; IPluginController* pluginManager = ICore::self()->pluginController(); QList plugins = pluginManager->allPluginsForExtension( QStringLiteral("org.kdevelop.IBasicVersionControl") ); foreach( IPlugin* p, plugins ) { + if (p == preSelectPlugin) { + preselectIndex = m_plugins.count(); + } m_plugins.append(p); m_ui->sources->addItem(QIcon::fromTheme(pluginManager->pluginInfo(p).iconName()), p->extension()->name()); } plugins = pluginManager->allPluginsForExtension( QStringLiteral("org.kdevelop.IProjectProvider") ); foreach( IPlugin* p, plugins ) { + if (p == preSelectPlugin) { + preselectIndex = m_plugins.count(); + } m_plugins.append(p); m_ui->sources->addItem(QIcon::fromTheme(pluginManager->pluginInfo(p).iconName()), p->extension()->name()); } + if (preselectIndex == -1) { + // "From File System" is quite unlikely to be what the user wants, so default to first real plugin... + const int defaultIndex = (m_plugins.count() > 1) ? 1 : 0; + KConfigGroup configGroup = KSharedConfig::openConfig()->group("Providers"); + preselectIndex = configGroup.readEntry("LastProviderIndex", defaultIndex); + } + preselectIndex = qBound(0, preselectIndex, m_ui->sources->count() - 1); + m_ui->sources->setCurrentIndex(preselectIndex); + setSourceWidget(preselectIndex, repoUrl); + + // connect as last step, otherwise KMessageWidget could get both animatedHide() and animatedShow() + // during setup and due to a bug will ignore any but the first call + // Patch proposed at https://phabricator.kde.org/D4329 connect(m_ui->workingDir, &KUrlRequester::textChanged, this, &ProjectSourcePage::reevaluateCorrection); connect(m_ui->sources, static_cast(&QComboBox::currentIndexChanged), this, &ProjectSourcePage::setSourceIndex); connect(m_ui->get, &QPushButton::clicked, this, &ProjectSourcePage::checkoutVcsProject); - - emit isCorrect(false); - - setSourceIndex(FROM_FILESYSTEM_SOURCE_INDEX); - - const int defaultIndex = m_plugins.isEmpty() ? 0 : 1; // "From File System" is quite unlikely to what you want... - KConfigGroup configGroup = KSharedConfig::openConfig()->group("Providers"); - const int lastCurrentIndex = configGroup.readEntry("LastProviderIndex", defaultIndex); - m_ui->sources->setCurrentIndex(qBound(0, lastCurrentIndex, m_ui->sources->count() - 1)); } ProjectSourcePage::~ProjectSourcePage() @@ -86,6 +98,11 @@ void ProjectSourcePage::setSourceIndex(int index) { + setSourceWidget(index, QUrl()); +} + +void ProjectSourcePage::setSourceWidget(int index, const QUrl& repoUrl) +{ m_locationWidget = nullptr; m_providerWidget = nullptr; QLayout* remoteWidgetLayout = m_ui->remoteWidget->layout(); @@ -103,6 +120,10 @@ m_locationWidget=vcIface->vcsLocation(m_ui->sourceBox); connect(m_locationWidget, &VcsLocationWidget::changed, this, &ProjectSourcePage::locationChanged); + // set after connect, to trigger handler + if (!repoUrl.isEmpty()) { + m_locationWidget->setLocation(repoUrl); + } remoteWidgetLayout->addWidget(m_locationWidget); } else { providerIface = providerPerIndex(index); @@ -271,7 +292,15 @@ void ProjectSourcePage::clearStatus() { - m_ui->status->animatedHide(); + // workaround for KMessageWidget bug: + // animatedHide will not explicitely hide the widget if it is not yet shown, + // so if show() is called on the parent later, the KMessageWidget + // Patch proposed at https://phabricator.kde.org/D4329 + if (!m_ui->status->isVisible()) { + m_ui->status->hide(); + } else { + m_ui->status->animatedHide(); + } } QUrl ProjectSourcePage::workingDir() const diff --git a/shell/tests/test_projectcontroller.cpp b/shell/tests/test_projectcontroller.cpp --- a/shell/tests/test_projectcontroller.cpp +++ b/shell/tests/test_projectcontroller.cpp @@ -54,7 +54,9 @@ bool m_reopen; public slots: - QUrl askProjectConfigLocation(bool /*fetch*/, const QUrl& /*startUrl*/ = QUrl()) override { return QUrl(); } + QUrl askProjectConfigLocation(bool /*fetch*/, const QUrl& /*startUrl*/, + const QUrl& /*repoUrl*/, IPlugin* /*plugin*/) override + { return QUrl(); } bool userWantsReopen() override { return m_reopen; } }; diff --git a/vcs/interfaces/ibasicversioncontrol.h b/vcs/interfaces/ibasicversioncontrol.h --- a/vcs/interfaces/ibasicversioncontrol.h +++ b/vcs/interfaces/ibasicversioncontrol.h @@ -81,6 +81,19 @@ virtual VcsImportMetadataWidget* createImportMetadataWidget( QWidget* parent ) = 0; /** + * Checks whether the given @p remoteLocation is a valid remote repository URL. + * + * If the URL is a local filesystem path, the folder will be checked + * if it contains proper repository content. + * For non-local filesystem URLs only the URL properties will be checked, + * no communication to any server is done. + * + * @param remoteLocation the URL used to access a remote repository + * @returns true if the the given @p remoteLocation seems valid for this version control system + */ + virtual bool isValidRemoteRepositoryUrl(const QUrl& remoteLocation) = 0; + + /** * These methods rely on a valid vcs-directory with vcs-metadata in it. * * revisions can contain a date in format parseable by QDate, a number, diff --git a/vcs/widgets/standardvcslocationwidget.h b/vcs/widgets/standardvcslocationwidget.h --- a/vcs/widgets/standardvcslocationwidget.h +++ b/vcs/widgets/standardvcslocationwidget.h @@ -38,7 +38,9 @@ bool isCorrect() const override; QUrl url() const; QString projectName() const override; - + void setLocation(const QUrl& remoteLocation) override; + void setUrl(const QUrl& url); + public slots: void textChanged(const QString& str); diff --git a/vcs/widgets/standardvcslocationwidget.cpp b/vcs/widgets/standardvcslocationwidget.cpp --- a/vcs/widgets/standardvcslocationwidget.cpp +++ b/vcs/widgets/standardvcslocationwidget.cpp @@ -37,6 +37,16 @@ connect(m_urlWidget, &KUrlRequester::textChanged, this, &StandardVcsLocationWidget::textChanged); } +void StandardVcsLocationWidget::setLocation(const QUrl& remoteLocation) +{ + setUrl(remoteLocation); +} + +void StandardVcsLocationWidget::setUrl(const QUrl& url) +{ + m_urlWidget->setUrl(url); +} + QUrl StandardVcsLocationWidget::url() const { return m_urlWidget->url(); diff --git a/vcs/widgets/vcslocationwidget.h b/vcs/widgets/vcslocationwidget.h --- a/vcs/widgets/vcslocationwidget.h +++ b/vcs/widgets/vcslocationwidget.h @@ -50,7 +50,13 @@ * for the VcsLocation. */ virtual QString projectName() const=0; - + + /** Sets the location by a respective URL + * @param remoteLocation the URL used to access a remote repository + * @see IBasicVersionControl::isValidRemoteRepositoryUrl + */ + virtual void setLocation(const QUrl& remoteLocation) = 0; + signals: void changed(); };