Commit 22950255 authored by Friedrich W. H. Kossebau's avatar Friedrich W. H. Kossebau
Browse files

Feature: on DnD of repo urls on window show Open Project dialog for url

Summary:
When browsing the web for some sources and finding the repo url,
this patch allows to drag and drop the url onto the KDevelop window
and automatically get the Open Project dialog opened, prefilled with
the matching vcs system and the url to use.

Future plans:
* extend for ProjectProvider plugins
* install protocol handlers and add app cmdline parameter
  -> KDevelop would be launched as handler for repo urls
* detect existing projects using a given repo url and ask if to open instead

Reviewers: #kdevelop, mwolff

Reviewed By: #kdevelop, mwolff

Subscribers: mwolff, kdevelop-devel

Differential Revision: https://phabricator.kde.org/D4330
parent e14b3987
......@@ -65,6 +65,16 @@ QString BazaarPlugin::name() const
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<QUrl>& localLocations, IBasicVersionControl::RecursionMode recursion)
{
DVcsJob* job = new DVcsJob(BazaarUtils::workingCopy(localLocations[0]), this);
......
......@@ -42,6 +42,8 @@ public:
QString name() const override;
bool isValidRemoteRepositoryUrl(const QUrl& remoteLocation) override;
KDevelop::VcsJob* add(const QList<QUrl>& localLocations, RecursionMode recursion=Recursive) override;
KDevelop::VcsJob* annotate(const QUrl& localLocation, const KDevelop::VcsRevision& rev) override;
KDevelop::VcsJob* commit(const QString& message, const QList<QUrl>& localLocations, RecursionMode recursion=Recursive) override;
......
......@@ -271,6 +271,13 @@ QString CvsPlugin::findWorkingDir(const QUrl& location)
// 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);
......
......@@ -52,6 +52,7 @@ public:
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<QUrl>& localLocations,
......
......@@ -280,6 +280,32 @@ bool GitPlugin::isValidDirectory(const QUrl & dirPath)
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());
......
......@@ -77,6 +77,7 @@ public:
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;
......
......@@ -145,6 +145,13 @@ KDevelop::VcsImportMetadataWidget* PerforcePlugin::createImportMetadataWidget(QW
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());
......
......@@ -58,6 +58,7 @@ public:
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;
......
......@@ -91,6 +91,17 @@ KDevSvnPlugin::~KDevSvnPlugin()
{
}
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?
......
......@@ -47,6 +47,7 @@ public:
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;
......
......@@ -59,6 +59,7 @@ Boston, MA 02110-1301, USA.
#include <interfaces/isession.h>
#include <interfaces/iprojectcontroller.h>
#include <vcs/interfaces/ibasicversioncontrol.h>
#include <sublime/view.h>
#include <sublime/document.h>
#include <sublime/urldocument.h>
......@@ -202,9 +203,15 @@ QAction* MainWindow::createCustomElement(QWidget* parent, int index, const QDomE
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 @@ void MainWindow::dropEvent( QDropEvent* ev )
if(dropToView)
activateView(dropToView);
foreach( const QUrl& u, ev->mimeData()->urls() )
{
Core::self()->documentController()->openDocument( u );
QList<QUrl> 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<IBasicVersionControl>();
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();
}
......
......@@ -69,7 +69,9 @@ URLInfo getUrlInfo(const QUrl& url)
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 @@ OpenProjectDialog::OpenProjectDialog( bool fetch, const QUrl& startUrl, QWidget*
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;
......
......@@ -28,13 +28,16 @@ namespace KDevelop
{
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.
......
......@@ -413,10 +413,11 @@ bool equalProjectFile( const QString& configPath, OpenProjectDialog* dlg )
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::openProject( const QUrl &projectFile )
}
}
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);
......
......@@ -60,7 +60,8 @@ public Q_SLOTS:
* @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 @@ public:
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 @@ public:
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;
};
......
......@@ -31,7 +31,8 @@ using namespace KDevelop;
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,10 +48,14 @@ ProjectSourcePage::ProjectSourcePage(const QUrl& initial, QWidget* parent)
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<IPlugin*> 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<IBasicVersionControl>()->name());
}
......@@ -58,22 +63,29 @@ ProjectSourcePage::ProjectSourcePage(const QUrl& initial, QWidget* parent)
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<IProjectProvider>()->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<void(QComboBox::*)(int)>(&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()
......@@ -85,6 +97,11 @@ ProjectSourcePage::~ProjectSourcePage()
}
void ProjectSourcePage::setSourceIndex(int index)
{
setSourceWidget(index, QUrl());
}
void ProjectSourcePage::setSourceWidget(int index, const QUrl& repoUrl)
{
m_locationWidget = nullptr;
m_providerWidget = nullptr;
......@@ -103,6 +120,10 @@ void ProjectSourcePage::setSourceIndex(int index)
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::setStatus(const QString& message)
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
......
......@@ -29,7 +29,8 @@ class ProjectSourcePage : public QWidget
{
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 @@ class ProjectSourcePage : public QWidget
void isCorrect(bool);
private:
void setSourceWidget(int index, const QUrl& repoUrl);
void setStatus(const QString& message);
void clearStatus();
......
......@@ -54,7 +54,9 @@ public:
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; }
};
......
......@@ -80,6 +80,19 @@ public:
*/
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.
*
......
......@@ -37,6 +37,16 @@ StandardVcsLocationWidget::StandardVcsLocationWidget(QWidget* parent, Qt::Window
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();
......
......@@ -38,7 +38,9 @@ class KDEVPLATFORMVCS_EXPORT StandardVcsLocationWidget : public VcsLocationWidge
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);
......
......@@ -50,7 +50,13 @@ class KDEVPLATFORMVCS_EXPORT VcsLocationWidget : public QWidget
* 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();
};
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment