diff --git a/kdevplatform/vcs/CMakeLists.txt b/kdevplatform/vcs/CMakeLists.txt index 4d6ff930f2..072ab37032 100644 --- a/kdevplatform/vcs/CMakeLists.txt +++ b/kdevplatform/vcs/CMakeLists.txt @@ -1,120 +1,126 @@ add_definitions(-DTRANSLATION_DOMAIN=\"kdevplatform\") if(BUILD_TESTING) add_subdirectory(tests) add_subdirectory(dvcs/tests) add_subdirectory(models/tests) endif() set(KDevPlatformVcs_UIS widgets/vcscommitdialog.ui widgets/vcseventwidget.ui widgets/vcsdiffwidget.ui dvcs/ui/dvcsimportmetadatawidget.ui dvcs/ui/branchmanager.ui ) set(KDevPlatformVcs_LIB_SRCS vcsjob.cpp vcsrevision.cpp vcsannotation.cpp vcspluginhelper.cpp vcslocation.cpp vcsdiff.cpp vcsevent.cpp vcsstatusinfo.cpp widgets/vcsimportmetadatawidget.cpp widgets/vcseventwidget.cpp widgets/vcsdiffwidget.cpp widgets/vcscommitdialog.cpp widgets/vcsdiffpatchsources.cpp widgets/vcslocationwidget.cpp widgets/standardvcslocationwidget.cpp models/vcsannotationmodel.cpp models/vcseventmodel.cpp models/vcsfilechangesmodel.cpp models/vcsitemeventmodel.cpp models/brancheslistmodel.cpp dvcs/dvcsjob.cpp dvcs/dvcsplugin.cpp dvcs/dvcsevent.cpp dvcs/ui/dvcsimportmetadatawidget.cpp dvcs/ui/branchmanager.cpp interfaces/ibasicversioncontrol.cpp interfaces/icontentawareversioncontrol.cpp interfaces/ipatchdocument.cpp interfaces/ipatchsource.cpp ) +if(NOT KF5TextEditor_VERSION VERSION_LESS 5.53.0) + list(APPEND KDevPlatformVcs_LIB_SRCS + widgets/vcsannotationitemdelegate.cpp + ) +endif() + declare_qt_logging_category(KDevPlatformVcs_LIB_SRCS TYPE LIBRARY CATEGORY_BASENAME "vcs" ) ki18n_wrap_ui(KDevPlatformVcs_LIB_SRCS ${KDevPlatformVcs_UIS}) kdevplatform_add_library(KDevPlatformVcs SOURCES ${KDevPlatformVcs_LIB_SRCS}) target_link_libraries(KDevPlatformVcs PUBLIC KDev::OutputView KDev::Interfaces PRIVATE KDev::Util KF5::KIOWidgets KF5::Parts ) install(FILES vcsjob.h vcsrevision.h vcsannotation.h vcsdiff.h vcspluginhelper.h vcsevent.h vcsstatusinfo.h vcslocation.h DESTINATION ${KDE_INSTALL_INCLUDEDIR}/kdevplatform/vcs COMPONENT Devel ) install(FILES widgets/vcsimportmetadatawidget.h widgets/vcseventwidget.h widgets/vcsdiffwidget.h widgets/vcscommitdialog.h widgets/vcslocationwidget.h widgets/standardvcslocationwidget.h DESTINATION ${KDE_INSTALL_INCLUDEDIR}/kdevplatform/vcs/widgets COMPONENT Devel ) install(FILES models/vcsannotationmodel.h models/vcseventmodel.h models/vcsfilechangesmodel.h models/vcsitemeventmodel.h models/brancheslistmodel.h DESTINATION ${KDE_INSTALL_INCLUDEDIR}/kdevplatform/vcs/models COMPONENT Devel ) install(FILES interfaces/ibasicversioncontrol.h interfaces/icentralizedversioncontrol.h interfaces/idistributedversioncontrol.h interfaces/ibranchingversioncontrol.h interfaces/ibrowsableversioncontrol.h interfaces/irepositoryversioncontrol.h interfaces/ipatchdocument.h interfaces/ipatchsource.h DESTINATION ${KDE_INSTALL_INCLUDEDIR}/kdevplatform/vcs/interfaces COMPONENT Devel ) install(FILES dvcs/dvcsjob.h dvcs/dvcsplugin.h dvcs/dvcsevent.h DESTINATION ${KDE_INSTALL_INCLUDEDIR}/kdevplatform/vcs/dvcs COMPONENT Devel ) install(FILES dvcs/ui/dvcsimportmetadatawidget.h DESTINATION ${KDE_INSTALL_INCLUDEDIR}/kdevplatform/vcs/dvcs/ui COMPONENT Devel ) diff --git a/kdevplatform/vcs/models/vcsannotationmodel.cpp b/kdevplatform/vcs/models/vcsannotationmodel.cpp index 380e0ce2ec..869b692aad 100644 --- a/kdevplatform/vcs/models/vcsannotationmodel.cpp +++ b/kdevplatform/vcs/models/vcsannotationmodel.cpp @@ -1,187 +1,196 @@ /*************************************************************************** * This file is part of KDevelop * * Copyright 2007 Andreas Pakulat * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU Library General Public License as * * published by the Free Software Foundation; either version 2 of the * * License, or (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU Library General Public * * License along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * ***************************************************************************/ #include "vcsannotationmodel.h" #include "../vcsannotation.h" #include "../vcsrevision.h" #include "../vcsjob.h" #include #include #include #include #include #include #include #include #include #include namespace KDevelop { class VcsAnnotationModelPrivate { public: explicit VcsAnnotationModelPrivate( VcsAnnotationModel* q_ ) : q(q_) {} KDevelop::VcsAnnotation m_annotation; mutable QHash m_brushes; VcsAnnotationModel* q; VcsJob* job; QColor foreground; QColor background; const QBrush& brush(const VcsRevision& revision) const { auto brushIt = m_brushes.find(revision); if (brushIt == m_brushes.end()) { const int background_y = background.red()*0.299 + 0.587*background.green() + 0.114*background.blue(); // get random, but reproducable 8-bit values from last two bytes of the revision hash const uint revisionHash = qHash(revision); const int u = static_cast((0xFF & revisionHash)); const int v = static_cast((0xFF00 & revisionHash) >> 8); const int r = qRound(qMin(255.0, qMax(0.0, background_y + 1.402*(v-128)))); const int g = qRound(qMin(255.0, qMax(0.0, background_y - 0.344*(u-128) - 0.714*(v-128)))); const int b = qRound(qMin(255.0, qMax(0.0, background_y + 1.772*(u-128)))); brushIt = m_brushes.insert(revision, QBrush(QColor(r, g, b))); } return brushIt.value(); } void addLines( KDevelop::VcsJob* job ) { if( job == this->job ) { foreach( const QVariant& v, job->fetchResults().toList() ) { if( v.canConvert() ) { VcsAnnotationLine l = v.value(); m_annotation.insertLine( l.lineNumber(), l ); emit q->lineChanged( l.lineNumber() ); } } } } }; VcsAnnotationModel::VcsAnnotationModel(VcsJob *job, const QUrl& url, QObject* parent, const QColor &foreground, const QColor &background) : d( new VcsAnnotationModelPrivate( this ) ) { setParent( parent ); d->m_annotation.setLocation( url ); d->job = job; d->foreground = foreground; d->background = background; qsrand( QDateTime().toTime_t() ); connect( d->job, &VcsJob::resultsReady,this, [&] (VcsJob* job) { d->addLines(job); } ); ICore::self()->runController()->registerJob( d->job ); } VcsAnnotationModel::~VcsAnnotationModel() = default; static QString abbreviateLastName(const QString& author) { auto parts = author.split(QLatin1Char(' ')); bool onlyOneFragment = parts.size() == 1 || ( parts.size() == 2 && parts.at(1).isEmpty() ); return onlyOneFragment ? parts.first() : parts.first() + QStringLiteral(" %1.").arg(parts.last()[0]); } static QString annotationToolTip(const VcsAnnotationLine& aline) { const bool textIsLeftToRight = (QApplication::layoutDirection() == Qt::LeftToRight); const QString boldStyle = QStringLiteral(";font-weight:bold"); const QString one = QStringLiteral("1"); const QString two = QStringLiteral("2"); const QString line = QStringLiteral( "" "%%2" "%%4" "").arg( (textIsLeftToRight ? boldStyle : QString()), (textIsLeftToRight ? one : two), (textIsLeftToRight ? QString() : boldStyle), (textIsLeftToRight ? two : one) ); const QString authorLabel = i18n("Author:").toHtmlEscaped(); const QString dateLabel = i18n("Date:").toHtmlEscaped(); const QString messageLabel = i18n("Commit message:").toHtmlEscaped(); const QString author = aline.author().toHtmlEscaped(); const QString date = QLocale().toString(aline.date()).toHtmlEscaped(); const QString message = aline.commitMessage().toHtmlEscaped().replace(QLatin1Char('\n'), QLatin1String("
")); return QLatin1String("") + line.arg(authorLabel, author) + line.arg(dateLabel, date) + line.arg(messageLabel, message) + QLatin1String("
"); } QVariant VcsAnnotationModel::data( int line, Qt::ItemDataRole role ) const { if( line < 0 || !d->m_annotation.containsLine( line ) ) { return QVariant(); } KDevelop::VcsAnnotationLine aline = d->m_annotation.line( line ); if( role == Qt::ForegroundRole ) { return QVariant(QPen(d->foreground)); } if( role == Qt::BackgroundRole ) { return QVariant(d->brush(aline.revision())); } else if( role == Qt::DisplayRole ) { return QVariant( QStringLiteral("%1 ").arg(aline.date().date().year()) + abbreviateLastName(aline.author()) ); } else if( role == (int) KTextEditor::AnnotationModel::GroupIdentifierRole ) { return aline.revision().revisionValue(); } else if( role == Qt::ToolTipRole ) { return QVariant(annotationToolTip(aline)); } return QVariant(); } VcsRevision VcsAnnotationModel::revisionForLine( int line ) const { ///FIXME: update the annotation bar on edit/reload somehow ///BUG: https://bugs.kde.org/show_bug.cgi?id=269757 if (!d->m_annotation.containsLine(line)) { return VcsRevision(); } Q_ASSERT(line >= 0 && d->m_annotation.containsLine(line)); return d->m_annotation.line( line ).revision(); } +VcsAnnotationLine VcsAnnotationModel::annotationLine(int line) const +{ + if (line < 0 || !d->m_annotation.containsLine(line)) { + return VcsAnnotationLine(); + } + + return d->m_annotation.line(line); +} + } #include "moc_vcsannotationmodel.cpp" diff --git a/kdevplatform/vcs/models/vcsannotationmodel.h b/kdevplatform/vcs/models/vcsannotationmodel.h index 26758d0cc9..60ac6b4548 100644 --- a/kdevplatform/vcs/models/vcsannotationmodel.h +++ b/kdevplatform/vcs/models/vcsannotationmodel.h @@ -1,58 +1,63 @@ /*************************************************************************** * This file is part of KDevelop * * Copyright 2007 Andreas Pakulat * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU Library General Public License as * * published by the Free Software Foundation; either version 2 of the * * License, or (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU Library General Public * * License along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * ***************************************************************************/ #ifndef KDEVPLATFORM_VCSANNOTATIONMODEL_H #define KDEVPLATFORM_VCSANNOTATIONMODEL_H #include #include "../vcsrevision.h" #include #include class QUrl; template class QList; namespace KDevelop { class VcsJob; +class VcsAnnotationLine; class KDEVPLATFORMVCS_EXPORT VcsAnnotationModel : public KTextEditor::AnnotationModel { Q_OBJECT public: VcsAnnotationModel( VcsJob* job, const QUrl&, QObject*, const QColor& foreground = QColor(Qt::black), const QColor& background = QColor(Qt::white) ); ~VcsAnnotationModel() override; VcsRevision revisionForLine(int line) const; QVariant data( int line, Qt::ItemDataRole role = Qt::DisplayRole ) const override; + // given "role" argument is of type Qt::ItemDataRole and not int, we cannot use custom roles + // to access custom data, so providing a custom API instead + VcsAnnotationLine annotationLine(int line) const; + private: const QScopedPointer d; friend class VcsAnnotationModelPrivate; }; } #endif diff --git a/kdevplatform/vcs/vcspluginhelper.cpp b/kdevplatform/vcs/vcspluginhelper.cpp index 62d09f3007..0c3587339b 100644 --- a/kdevplatform/vcs/vcspluginhelper.cpp +++ b/kdevplatform/vcs/vcspluginhelper.cpp @@ -1,506 +1,528 @@ /*************************************************************************** * Copyright 2008 Andreas Pakulat * * Copyright 2010 Aleix Pol Gonzalez * + * Copyright 2017-2018 Friedrich W. H. Kossebau * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "vcspluginhelper.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include +#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include +#if KTEXTEDITOR_VERSION >= QT_VERSION_CHECK(5,53,0) +#include +#endif #include #include #include #include #include #include #include "interfaces/idistributedversioncontrol.h" #include "vcsevent.h" #include "debug.h" #include "widgets/vcsdiffpatchsources.h" namespace KDevelop { class VcsPluginHelperPrivate { public: IPlugin * plugin; IBasicVersionControl * vcs; QList ctxUrls; QAction* commitAction; QAction* addAction; QAction* updateAction; QAction* historyAction; QAction* annotationAction; QAction* diffToBaseAction; QAction* revertAction; QAction* diffForRevAction; QAction* diffForRevGlobalAction; QAction* pushAction; QAction* pullAction; void createActions(VcsPluginHelper* parent) { commitAction = new QAction(QIcon::fromTheme(QStringLiteral("svn-commit")), i18n("Commit..."), parent); updateAction = new QAction(QIcon::fromTheme(QStringLiteral("svn-update")), i18n("Update"), parent); addAction = new QAction(QIcon::fromTheme(QStringLiteral("list-add")), i18n("Add"), parent); diffToBaseAction = new QAction(QIcon::fromTheme(QStringLiteral("text-x-patch")), i18n("Show Differences..."), parent); revertAction = new QAction(QIcon::fromTheme(QStringLiteral("archive-remove")), i18n("Revert"), parent); historyAction = new QAction(QIcon::fromTheme(QStringLiteral("view-history")), i18n("History..."), parent); annotationAction = new QAction(QIcon::fromTheme(QStringLiteral("user-properties")), i18n("Annotation..."), parent); diffForRevAction = new QAction(QIcon::fromTheme(QStringLiteral("text-x-patch")), i18n("Show Diff..."), parent); diffForRevGlobalAction = new QAction(QIcon::fromTheme(QStringLiteral("text-x-patch")), i18n("Show Diff (all files)..."), parent); pushAction = new QAction(QIcon::fromTheme(QStringLiteral("arrow-up-double")), i18n("Push"), parent); pullAction = new QAction(QIcon::fromTheme(QStringLiteral("arrow-down-double")), i18n("Pull"), parent); QObject::connect(commitAction, &QAction::triggered, parent, &VcsPluginHelper::commit); QObject::connect(addAction, &QAction::triggered, parent, &VcsPluginHelper::add); QObject::connect(updateAction, &QAction::triggered, parent, &VcsPluginHelper::update); QObject::connect(diffToBaseAction, &QAction::triggered, parent, &VcsPluginHelper::diffToBase); QObject::connect(revertAction, &QAction::triggered, parent, &VcsPluginHelper::revert); QObject::connect(historyAction, &QAction::triggered, parent, [=] { parent->history(); }); QObject::connect(annotationAction, &QAction::triggered, parent, &VcsPluginHelper::annotation); QObject::connect(diffForRevAction, &QAction::triggered, parent, static_cast(&VcsPluginHelper::diffForRev)); QObject::connect(diffForRevGlobalAction, &QAction::triggered, parent, &VcsPluginHelper::diffForRevGlobal); QObject::connect(pullAction, &QAction::triggered, parent, &VcsPluginHelper::pull); QObject::connect(pushAction, &QAction::triggered, parent, &VcsPluginHelper::push); } bool allLocalFiles(const QList& urls) { bool ret=true; for (const QUrl& url : urls) { QFileInfo info(url.toLocalFile()); ret &= info.isFile(); } return ret; } QMenu* createMenu(QWidget* parent) { bool allVersioned=true; foreach(const QUrl &url, ctxUrls) { allVersioned=allVersioned && vcs->isVersionControlled(url); if(!allVersioned) break; } QMenu* menu = new QMenu(vcs->name(), parent); menu->setIcon(QIcon::fromTheme(ICore::self()->pluginController()->pluginInfo(plugin).iconName())); menu->addAction(commitAction); if(plugin->extension()) { menu->addAction(pushAction); menu->addAction(pullAction); } else { menu->addAction(updateAction); } menu->addSeparator(); menu->addAction(addAction); menu->addAction(revertAction); menu->addSeparator(); menu->addAction(historyAction); menu->addAction(annotationAction); menu->addAction(diffToBaseAction); const bool singleVersionedFile = ctxUrls.count() == 1 && allVersioned; historyAction->setEnabled(singleVersionedFile); annotationAction->setEnabled(singleVersionedFile && allLocalFiles(ctxUrls)); diffToBaseAction->setEnabled(singleVersionedFile); commitAction->setEnabled(singleVersionedFile); return menu; } }; VcsPluginHelper::VcsPluginHelper(KDevelop::IPlugin* parent, KDevelop::IBasicVersionControl* vcs) : QObject(parent) , d(new VcsPluginHelperPrivate()) { Q_ASSERT(vcs); Q_ASSERT(parent); d->plugin = parent; d->vcs = vcs; d->createActions(this); } VcsPluginHelper::~VcsPluginHelper() {} void VcsPluginHelper::addContextDocument(const QUrl &url) { d->ctxUrls.append(url); } void VcsPluginHelper::disposeEventually(KTextEditor::View *, bool dont) { if ( ! dont ) { deleteLater(); } } void VcsPluginHelper::disposeEventually(KTextEditor::Document *) { deleteLater(); } void VcsPluginHelper::setupFromContext(Context* context) { d->ctxUrls = context->urls(); } QList VcsPluginHelper::contextUrlList() const { return d->ctxUrls; } QMenu* VcsPluginHelper::commonActions(QWidget* parent) { /* TODO: the following logic to determine which actions need to be enabled * or disabled does not work properly. What needs to be implemented is that * project items that are vc-controlled enable all except add, project * items that are not vc-controlled enable add action. For urls that cannot * be made into a project item, or if the project has no associated VC * plugin we need to check whether a VC controls the parent dir, if we have * one we assume the urls can be added but are not currently controlled. If * the url is already version controlled then just enable all except add */ return d->createMenu(parent); } #define EXECUTE_VCS_METHOD( method ) \ d->plugin->core()->runController()->registerJob( d->vcs-> method ( d->ctxUrls ) ) #define SINGLEURL_SETUP_VARS \ KDevelop::IBasicVersionControl* iface = d->vcs;\ const QUrl &url = d->ctxUrls.front(); void VcsPluginHelper::revert() { VcsJob* job=d->vcs->revert(d->ctxUrls); connect(job, &VcsJob::finished, this, &VcsPluginHelper::revertDone); foreach(const QUrl &url, d->ctxUrls) { IDocument* doc=ICore::self()->documentController()->documentForUrl(url); if(doc && doc->textDocument()) { auto* modif = dynamic_cast(doc->textDocument()); if (modif) { modif->setModifiedOnDiskWarning(false); } doc->textDocument()->setModified(false); } } job->setProperty("urls", QVariant::fromValue(d->ctxUrls)); d->plugin->core()->runController()->registerJob(job); } void VcsPluginHelper::revertDone(KJob* job) { auto* modificationTimer = new QTimer; modificationTimer->setInterval(100); connect(modificationTimer, &QTimer::timeout, this, &VcsPluginHelper::delayedModificationWarningOn); connect(modificationTimer, &QTimer::timeout, modificationTimer, &QTimer::deleteLater); modificationTimer->setProperty("urls", job->property("urls")); modificationTimer->start(); } void VcsPluginHelper::delayedModificationWarningOn() { QObject* timer = sender(); const QList urls = timer->property("urls").value>(); for (const QUrl& url : urls) { IDocument* doc=ICore::self()->documentController()->documentForUrl(url); if(doc) { doc->reload(); auto* modif=dynamic_cast(doc->textDocument()); modif->setModifiedOnDiskWarning(true); } } } void VcsPluginHelper::diffJobFinished(KJob* job) { auto* vcsjob = qobject_cast(job); Q_ASSERT(vcsjob); if (vcsjob->status() == KDevelop::VcsJob::JobSucceeded) { KDevelop::VcsDiff d = vcsjob->fetchResults().value(); if(d.isEmpty()) KMessageBox::information(ICore::self()->uiController()->activeMainWindow(), i18n("There are no differences."), i18n("VCS support")); else { auto* patch=new VCSDiffPatchSource(d); showVcsDiff(patch); } } else { KMessageBox::error(ICore::self()->uiController()->activeMainWindow(), vcsjob->errorString(), i18n("Unable to get difference.")); } } void VcsPluginHelper::diffToBase() { SINGLEURL_SETUP_VARS ICore::self()->documentController()->saveAllDocuments(); auto* patch =new VCSDiffPatchSource(new VCSStandardDiffUpdater(iface, url)); showVcsDiff(patch); } void VcsPluginHelper::diffForRev() { if (d->ctxUrls.isEmpty()) { return; } diffForRev(d->ctxUrls.first()); } void VcsPluginHelper::diffForRevGlobal() { if (d->ctxUrls.isEmpty()) { return; } QUrl url = d->ctxUrls.first(); IProject* project = ICore::self()->projectController()->findProjectForUrl( url ); if( project ) { url = project->path().toUrl(); } diffForRev(url); } void VcsPluginHelper::diffForRev(const QUrl& url) { auto* action = qobject_cast( sender() ); Q_ASSERT(action); Q_ASSERT(action->data().canConvert()); VcsRevision rev = action->data().value(); ICore::self()->documentController()->saveAllDocuments(); VcsRevision prev = KDevelop::VcsRevision::createSpecialRevision(KDevelop::VcsRevision::Previous); KDevelop::VcsJob* job = d->vcs->diff(url, prev, rev ); connect(job, &VcsJob::finished, this, &VcsPluginHelper::diffJobFinished); d->plugin->core()->runController()->registerJob(job); } void VcsPluginHelper::history(const VcsRevision& rev) { SINGLEURL_SETUP_VARS QDialog* dlg = new QDialog(ICore::self()->uiController()->activeMainWindow()); dlg->setAttribute(Qt::WA_DeleteOnClose); dlg->setWindowTitle(i18nc("%1: path or URL, %2: name of a version control system", "%2 History (%1)", url.toDisplayString(QUrl::PreferLocalFile), iface->name())); auto *mainLayout = new QVBoxLayout(dlg); auto* logWidget = new KDevelop::VcsEventWidget(url, rev, iface, dlg); mainLayout->addWidget(logWidget); auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Close); dlg->connect(buttonBox, &QDialogButtonBox::accepted, dlg, &QDialog::accept); dlg->connect(buttonBox, &QDialogButtonBox::rejected, dlg, &QDialog::reject); mainLayout->addWidget(buttonBox); dlg->show(); } void VcsPluginHelper::annotation() { SINGLEURL_SETUP_VARS KDevelop::IDocument* doc = ICore::self()->documentController()->documentForUrl(url); if (!doc) doc = ICore::self()->documentController()->openDocument(url); + KTextEditor::View* view = doc ? doc->activeTextView() : nullptr; KTextEditor::AnnotationInterface* annotateiface = qobject_cast(doc->textDocument()); - KTextEditor::AnnotationViewInterface* viewiface = qobject_cast(doc->activeTextView()); + auto viewiface = qobject_cast(view); if (viewiface && viewiface->isAnnotationBorderVisible()) { viewiface->setAnnotationBorderVisible(false); return; } if (doc && doc->textDocument() && iface) { KDevelop::VcsJob* job = iface->annotate(url); if( !job ) { qCWarning(VCS) << "Couldn't create annotate job for:" << url << "with iface:" << iface << dynamic_cast( iface ); return; } QColor foreground(Qt::black); QColor background(Qt::white); - if (KTextEditor::View* view = doc->activeTextView()) { + if (view) { KTextEditor::Attribute::Ptr style = view->defaultStyleAttribute(KTextEditor::dsNormal); foreground = style->foreground().color(); if (style->hasProperty(QTextFormat::BackgroundBrush)) { background = style->background().color(); } } if (annotateiface && viewiface) { // TODO: only create model if there is none yet (e.g. from another view) auto* model = new KDevelop::VcsAnnotationModel(job, url, doc->textDocument(), foreground, background); annotateiface->setAnnotationModel(model); + +#if KTEXTEDITOR_VERSION >= QT_VERSION_CHECK(5,53,0) + auto viewifaceV2 = qobject_cast(view); + if (viewifaceV2) { + // TODO: only create delegate if there is none yet + auto delegate = new VcsAnnotationItemDelegate(view, model, view); + viewifaceV2->setAnnotationItemDelegate(delegate); + viewifaceV2->setAnnotationUniformItemSizes(true); + } +#endif viewiface->setAnnotationBorderVisible(true); // can't use new signal slot syntax here, AnnotationInterface is not a QObject - connect(doc->activeTextView(), - SIGNAL(annotationContextMenuAboutToShow(KTextEditor::View*,QMenu*,int)), + connect(view, SIGNAL(annotationContextMenuAboutToShow(KTextEditor::View*,QMenu*,int)), this, SLOT(annotationContextMenuAboutToShow(KTextEditor::View*,QMenu*,int))); - connect(doc->activeTextView(), SIGNAL(annotationBorderVisibilityChanged(View*,bool)), + connect(view, SIGNAL(annotationBorderVisibilityChanged(View*,bool)), this, SLOT(handleAnnotationBorderVisibilityChanged(View*,bool))); } else { KMessageBox::error(nullptr, i18n("Cannot display annotations, missing interface KTextEditor::AnnotationInterface for the editor.")); delete job; } } else { KMessageBox::error(nullptr, i18n("Cannot execute annotate action because the " "document was not found, or was not a text document:\n%1", url.toDisplayString(QUrl::PreferLocalFile))); } } void VcsPluginHelper::annotationContextMenuAboutToShow( KTextEditor::View* view, QMenu* menu, int line ) { +#if KTEXTEDITOR_VERSION >= QT_VERSION_CHECK(5,53,0) + auto viewifaceV2 = qobject_cast(view); + if (viewifaceV2) { + viewifaceV2->annotationItemDelegate()->hideTooltip(view); + } +#endif + KTextEditor::AnnotationInterface* annotateiface = qobject_cast(view->document()); auto* model = qobject_cast( annotateiface->annotationModel() ); Q_ASSERT(model); VcsRevision rev = model->revisionForLine(line); // check if the user clicked on a row without revision information if (rev.revisionType() == VcsRevision::Invalid) { // in this case, do not action depending on revision information return; } d->diffForRevAction->setData(QVariant::fromValue(rev)); d->diffForRevGlobalAction->setData(QVariant::fromValue(rev)); menu->addSeparator(); menu->addAction(d->diffForRevAction); menu->addAction(d->diffForRevGlobalAction); QAction* copyAction = menu->addAction(QIcon::fromTheme(QStringLiteral("edit-copy")), i18n("Copy Revision Id")); connect(copyAction, &QAction::triggered, this, [rev]() { QApplication::clipboard()->setText(rev.revisionValue().toString()); }); QAction* historyAction = menu->addAction(QIcon::fromTheme(QStringLiteral("view-history")), i18n("History...")); connect(historyAction, &QAction::triggered, this, [this, rev]() { history(rev); }); } void VcsPluginHelper::handleAnnotationBorderVisibilityChanged(View* view, bool visible) { if (visible) { return; } disconnect(view, SIGNAL(annotationContextMenuAboutToShow(KTextEditor::View*,QMenu*,int)), this, SLOT(annotationContextMenuAboutToShow(KTextEditor::View*,QMenu*,int))); disconnect(view, SIGNAL(annotationBorderVisibilityChanged(View*,bool)), this, SLOT(handleAnnotationBorderVisibilityChanged(View*,bool))); // TODO: remove the model if last user of it } void VcsPluginHelper::update() { EXECUTE_VCS_METHOD(update); } void VcsPluginHelper::add() { EXECUTE_VCS_METHOD(add); } void VcsPluginHelper::commit() { Q_ASSERT(!d->ctxUrls.isEmpty()); ICore::self()->documentController()->saveAllDocuments(); QUrl url = d->ctxUrls.first(); // We start the commit UI no matter whether there is real differences, as it can also be used to commit untracked files auto* patchSource = new VCSCommitDiffPatchSource(new VCSStandardDiffUpdater(d->vcs, url)); bool ret = showVcsDiff(patchSource); if(!ret) { ScopedDialog commitDialog(patchSource); commitDialog->setCommitCandidates(patchSource->infos()); commitDialog->exec(); } } void VcsPluginHelper::push() { foreach(const QUrl &url, d->ctxUrls) { VcsJob* job = d->plugin->extension()->push(url, VcsLocation()); ICore::self()->runController()->registerJob(job); } } void VcsPluginHelper::pull() { foreach(const QUrl &url, d->ctxUrls) { VcsJob* job = d->plugin->extension()->pull(VcsLocation(), url); ICore::self()->runController()->registerJob(job); } } } diff --git a/kdevplatform/vcs/widgets/vcsannotationitemdelegate.cpp b/kdevplatform/vcs/widgets/vcsannotationitemdelegate.cpp new file mode 100644 index 0000000000..fb08347f52 --- /dev/null +++ b/kdevplatform/vcs/widgets/vcsannotationitemdelegate.cpp @@ -0,0 +1,389 @@ +/* This file is part of KDevelop + * + * Copyright 2017-2018 Friedrich W. H. Kossebau + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +#include "vcsannotationitemdelegate.h" + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +using namespace KDevelop; + +VcsAnnotationItemDelegate::VcsAnnotationItemDelegate(KTextEditor::View* view, KTextEditor::AnnotationModel* model, + QObject* parent) + : KTextEditor::AbstractAnnotationItemDelegate(parent) + , m_model(model) +{ + // dump background brushes on schema change + Q_ASSERT(qobject_cast(view)); + connect(view, SIGNAL(configChanged()), this, SLOT(resetBackgrounds())); + + view->installEventFilter(this); +} + +VcsAnnotationItemDelegate::~VcsAnnotationItemDelegate() = default; + +static QString ageOfDate(const QDate& date) +{ + const auto now = QDate::currentDate(); + int ageInYears = now.year() - date.year(); + if (now < date.addYears(ageInYears)) { + --ageInYears; + } + if (ageInYears > 0) { + return i18ncp("age", "%1 year", "%1 years", ageInYears); + } + int ageInMonths = now.month() - date.month(); + if (ageInMonths < 0) { + ageInMonths += 12; + } + if (ageInMonths > 0) { + return i18ncp("age", "%1 month", "%1 months", ageInMonths); + } + const int ageInDays = date.daysTo(now); + if (ageInDays > 0) { + return i18ncp("age", "%1 day", "%1 days", ageInDays); + } + return i18n("Today"); +} + +void VcsAnnotationItemDelegate::doMessageLineLayout(const KTextEditor::StyleOptionAnnotationItem& option, + QRect* messageRect, QRect* ageRect) const +{ + Q_ASSERT(messageRect && messageRect->isValid()); + Q_ASSERT(ageRect); + + const QWidget* const widget = option.view; + QStyle* const style = widget ? widget->style() : QApplication::style(); + const bool hasAge = ageRect->isValid(); + // "+ 1" as used in QItemDelegate + const int textMargin = style->pixelMetric(QStyle::PM_FocusFrameHMargin, nullptr, widget) + 1; + const int ageMargin = hasAge ? textMargin : 0; + + const int x = option.rect.left(); + const int y = option.rect.top(); + const int w = option.rect.width(); + const int h = option.rect.height(); + + // add margins for fixed elements + QSize ageSize(0, 0); // ageRect could be invalid, so use separate object for calculation + if (hasAge) { + ageSize = ageRect->size(); + ageSize.rwidth() += 2 * ageMargin; + } + + // distribute space among layout items + QRect message; + QRect age; + if (option.direction == Qt::LeftToRight) { + message.setRect(x, y, w - ageSize.width(), h); + age.setRect(message.right() + 1, y, ageSize.width(), h); + } else { + age.setRect(x, y, ageSize.width(), h); + message.setRect(age.right() + 1, y, w - ageSize.width(), h); + } + // remove margins here, so renderMessageAndAge does not have to + message.adjust(textMargin, 0, -textMargin, 0); + age.adjust(ageMargin, 0, -ageMargin, 0); + + // return result + *ageRect = age; + *messageRect = QStyle::alignedRect(option.direction, Qt::AlignLeading, + messageRect->size().boundedTo(message.size()), message); +} + +void VcsAnnotationItemDelegate::doAuthorLineLayout(const KTextEditor::StyleOptionAnnotationItem& option, + QRect* authorRect) const +{ + Q_ASSERT(authorRect); + + // if invalid, nothing to be done, keep as is + if (!authorRect->isValid()) { + return; + } + + const QWidget* const widget = option.view; + QStyle* const style = widget ? widget->style() : QApplication::style(); + // "+ 1" as used in QItemDelegate + const int authorMargin = style->pixelMetric(QStyle::PM_FocusFrameHMargin, nullptr, widget) + 1; + + QRect author = option.rect; + // remove margins here, so renderAuthor does not have to + author.adjust(authorMargin, 0, -authorMargin, 0); + + // return result + *authorRect = QStyle::alignedRect(option.direction, Qt::AlignLeading, + authorRect->size().boundedTo(author.size()), author); +} + +void VcsAnnotationItemDelegate::renderBackground(QPainter* painter, + const KTextEditor::StyleOptionAnnotationItem& option, + const VcsAnnotationLine& annotationLine) const +{ + Q_UNUSED(option); + + const auto revision = annotationLine.revision(); + auto brushIt = m_backgrounds.find(revision); + if (brushIt == m_backgrounds.end()) { + KTextEditor::Attribute::Ptr normalStyle = option.view->defaultStyleAttribute(KTextEditor::dsNormal); + const auto background = (normalStyle->hasProperty(QTextFormat::BackgroundBrush)) ? normalStyle->background().color() : QColor(Qt::white); + const int background_y = background.red()*0.299 + 0.587*background.green() + + 0.114*background.blue(); + // get random, but reproducable 8-bit values from last two bytes of the revision hash + const uint revisionHash = qHash(revision); + const int u = static_cast((0xFF & revisionHash)); + const int v = static_cast((0xFF00 & revisionHash) >> 8); + const int r = qRound(qMin(255.0, qMax(0.0, background_y + 1.402*(v-128)))); + const int g = qRound(qMin(255.0, qMax(0.0, background_y - 0.344*(u-128) - 0.714*(v-128)))); + const int b = qRound(qMin(255.0, qMax(0.0, background_y + 1.772*(u-128)))); + brushIt = m_backgrounds.insert(revision, QBrush(QColor(r, g, b))); + } + + painter->fillRect(option.rect, brushIt.value()); +} + +void VcsAnnotationItemDelegate::renderMessageAndAge(QPainter* painter, + const KTextEditor::StyleOptionAnnotationItem& option, + const QRect& messageRect, const QString& messageText, + const QRect& ageRect, const QString& ageText) const +{ + Q_UNUSED(option); + + painter->save(); + + KTextEditor::Attribute::Ptr normalStyle = option.view->defaultStyleAttribute(KTextEditor::dsNormal); + painter->setPen(normalStyle->foreground().color()); + painter->drawText(messageRect, Qt::AlignLeading | Qt::AlignVCenter, + painter->fontMetrics().elidedText(messageText, Qt::ElideRight, messageRect.width())); + + // TODO: defaultStyleAttribute only returns relyably for dsNormal, so what to do for a comment-like color? + KTextEditor::Attribute::Ptr commentStyle = option.view->defaultStyleAttribute(KTextEditor::dsNormal); + painter->setPen(commentStyle->foreground().color()); + painter->drawText(ageRect, Qt::AlignTrailing | Qt::AlignVCenter, ageText); + + painter->restore(); +} + +void VcsAnnotationItemDelegate::renderAuthor(QPainter* painter, + const KTextEditor::StyleOptionAnnotationItem& option, + const QRect& authorRect, const QString& authorText) const +{ + Q_UNUSED(option); + + painter->save(); + + // TODO: defaultStyleAttribute only returns relyably for dsNormal, so what to do for a comment-like color? + KTextEditor::Attribute::Ptr commentStyle = option.view->defaultStyleAttribute(KTextEditor::dsNormal); + painter->setPen(commentStyle->foreground().color()); + painter->drawText(authorRect, Qt::AlignLeading | Qt::AlignVCenter, + painter->fontMetrics().elidedText(authorText, Qt::ElideRight, authorRect.width())); + + painter->restore(); +} + +void VcsAnnotationItemDelegate::renderHighlight(QPainter* painter, + const KTextEditor::StyleOptionAnnotationItem& option) const +{ + // Draw a border around all adjacent entries that have the same text as the currently hovered one + if ((option.state & QStyle::State_MouseOver) && + (option.annotationItemGroupingPosition & KTextEditor::StyleOptionAnnotationItem::InGroup)) { + KTextEditor::Attribute::Ptr style = option.view->defaultStyleAttribute(KTextEditor::dsNormal); + painter->setPen(style->foreground().color()); + // Use floating point coordinates to support scaled rendering + QRectF rect(option.rect); + rect.adjust(0.5, 0.5, -0.5, -0.5); + // draw left and right highlight borders + painter->drawLine(rect.topLeft(), rect.bottomLeft()); + painter->drawLine(rect.topRight(), rect.bottomRight()); + + if ((option.annotationItemGroupingPosition & KTextEditor::StyleOptionAnnotationItem::GroupBegin) && + (option.wrappedLine == 0)) { + painter->drawLine(rect.topLeft(), rect.topRight()); + } + + if ((option.annotationItemGroupingPosition & KTextEditor::StyleOptionAnnotationItem::GroupEnd) && + (option.wrappedLine == (option.wrappedLineCount-1))) { + painter->drawLine(rect.bottomLeft(), rect.bottomRight()); + } + } +} + +void VcsAnnotationItemDelegate::paint(QPainter* painter, const KTextEditor::StyleOptionAnnotationItem& option, + KTextEditor::AnnotationModel* model, int line) const +{ + Q_ASSERT(painter); + // we cannot use custom roles and data() API (cmp. VcsAnnotationModel dox), so accessing custom API instead + VcsAnnotationModel* vcsModel = qobject_cast(model); + Q_ASSERT(vcsModel); + if (!painter || !vcsModel) { + return; + } + // test of line just for sake of completeness skipped here + + // Fetch data from the model + const VcsAnnotationLine annotationLine = vcsModel->annotationLine(line); + + if (annotationLine.revision().revisionType() == VcsRevision::Invalid) { + return; + } + + // prepare + painter->save(); + + renderBackground(painter, option, annotationLine); + + // We use the normal UI font here, which usually is a proportimal one, + // so more text fits into the available space. + // Though we do this at the cost of not adapting to any scaled content font size, + // as there is no zooming state info available, so we cannot adapt. + // Tooltip font also is not scaled, and annotations could be considered to fall into + // that category, so might be fine. + painter->setFont(option.view->font()); + + if (option.visibleWrappedLineInGroup == 0) { + QRect ageRect; + QString ageText; + const auto date = annotationLine.date(); + if (date.isValid()) { + ageText = ageOfDate(date.date()); + ageRect = QRect(QPoint(0, 0), + QSize(option.fontMetrics.width(ageText), option.rect.height())); + } + const auto messageText = annotationLine.commitMessage(); + auto messageRect = QRect(QPoint(0, 0), + QSize(option.fontMetrics.width(messageText), option.rect.height())); + + doMessageLineLayout(option, &messageRect, &ageRect); + + renderMessageAndAge(painter, option, messageRect, messageText, ageRect, ageText); + } else if (option.visibleWrappedLineInGroup == 1) { + const auto author = annotationLine.author(); + if (!author.isEmpty()) { + const auto authorText = i18nc("By: commit author", "By: %1", author); + auto authorRect = QRect(QPoint(0, 0), + QSize(option.fontMetrics.width(authorText), option.rect.height())); + + doAuthorLineLayout(option, &authorRect); + + renderAuthor(painter, option, authorRect, authorText); + } + } + + renderHighlight(painter, option); + + // done + painter->restore(); +} + +bool VcsAnnotationItemDelegate::helpEvent(QHelpEvent* event, KTextEditor::View* view, + const KTextEditor::StyleOptionAnnotationItem& option, + KTextEditor::AnnotationModel* model, int line) +{ + Q_UNUSED(option); + if (!model || event->type() != QEvent::ToolTip) { + return false; + } + + const QString annotationGroupId = model->data(line, (Qt::ItemDataRole)KTextEditor::AnnotationModel::GroupIdentifierRole).toString(); + + const QVariant data = model->data(line, Qt::ToolTipRole); + if (!data.isValid()) { + return false; + } + + const QString toolTipText = data.toString(); + if (toolTipText.isEmpty()) { + return false; + } + + QToolTip::showText(event->globalPos(), toolTipText, view, option.rect); + + return true; +} + +void VcsAnnotationItemDelegate::hideTooltip(KTextEditor::View *view) +{ + Q_UNUSED(view); + QToolTip::hideText(); +} + +QSize VcsAnnotationItemDelegate::sizeHint(const KTextEditor::StyleOptionAnnotationItem& option, + KTextEditor::AnnotationModel* model, int line) const +{ + Q_UNUSED(line); + Q_ASSERT(model); + if (!model) { + return QSize(0, 0); + } + + // Ideally the user could configure the width of the annotations, best interactively. + // Until this is possible, the sizehint is: roughly 40 chars, but maximal 25 % of the view + // See eventFilter for making sure we adapt the max 25 % to a changed width. + + const QFontMetricsF& fm(option.fontMetrics); + // if only averageCharWidth would yield sane values, + // multiply by 40 in average seemed okayish at least with english, showing enough of message + m_lastCharBasedWidthHint = ceil(40 * fm.averageCharWidth()); + m_lastViewBasedWidthHint = widthHintFromViewWidth(option.view->width()); + return QSize(qMin(m_lastCharBasedWidthHint, m_lastViewBasedWidthHint), fm.height()); +} + +bool VcsAnnotationItemDelegate::eventFilter(QObject* object, QEvent* event) +{ + if (event->type() == QEvent::Resize) { + auto resizeEvent = static_cast(event); + const int viewBasedWidthHint = widthHintFromViewWidth(resizeEvent->size().width()); + if ((viewBasedWidthHint < m_lastCharBasedWidthHint) && + (viewBasedWidthHint != m_lastViewBasedWidthHint)) { + // emit for first line only, assuming uniformAnnotationItemSizes is set to true + emit sizeHintChanged(m_model, 0); + } + } + + return KTextEditor::AbstractAnnotationItemDelegate::eventFilter(object, event); +} + +void VcsAnnotationItemDelegate::resetBackgrounds() +{ + m_backgrounds.clear(); +} + +int VcsAnnotationItemDelegate::widthHintFromViewWidth(int viewWidth) const +{ + return viewWidth * m_maxWidthViewPercent / 100; +} diff --git a/kdevplatform/vcs/widgets/vcsannotationitemdelegate.h b/kdevplatform/vcs/widgets/vcsannotationitemdelegate.h new file mode 100644 index 0000000000..e90b49c82a --- /dev/null +++ b/kdevplatform/vcs/widgets/vcsannotationitemdelegate.h @@ -0,0 +1,95 @@ +/* This file is part of KDevelop + * + * Copyright 2017-2018 Friedrich W. H. Kossebau + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +#ifndef KDEVPLATFORM_VCSANNITATIONITEMDELEGATE_H +#define KDEVPLATFORM_VCSANNITATIONITEMDELEGATE_H + +// KDev +#include +// KF +#include +// Qt +#include +#include + +namespace KDevelop +{ +class VcsAnnotationLine; + +class VcsAnnotationItemDelegate : public KTextEditor::AbstractAnnotationItemDelegate +{ + Q_OBJECT + +public: + VcsAnnotationItemDelegate(KTextEditor::View* view, KTextEditor::AnnotationModel* model, QObject* parent); + ~VcsAnnotationItemDelegate() override; + +public: // AbstractAnnotationItemDelegate APO + void paint(QPainter* painter, const KTextEditor::StyleOptionAnnotationItem& option, + KTextEditor::AnnotationModel* model, int line) const override; + QSize sizeHint(const KTextEditor::StyleOptionAnnotationItem& option, + KTextEditor::AnnotationModel* model, int line) const override; + bool helpEvent(QHelpEvent* event, KTextEditor::View* view, + const KTextEditor::StyleOptionAnnotationItem& option, + KTextEditor::AnnotationModel *model, int line) override; + void hideTooltip(KTextEditor::View *view) override; + +private: + void renderBackground(QPainter* painter, + const KTextEditor::StyleOptionAnnotationItem& option, + const VcsAnnotationLine& annotationLine) const; + void renderMessageAndAge(QPainter* painter, + const KTextEditor::StyleOptionAnnotationItem& option, + const QRect& messageRect, const QString& messageText, + const QRect& ageRect, const QString& ageText) const; + void renderAuthor(QPainter* painter, + const KTextEditor::StyleOptionAnnotationItem& option, + const QRect& authorRect, const QString& authorText) const; + void renderHighlight(QPainter* painter, + const KTextEditor::StyleOptionAnnotationItem& option) const; + void doMessageLineLayout(const KTextEditor::StyleOptionAnnotationItem& option, + QRect* messageRect, QRect* ageRect) const; + void doAuthorLineLayout(const KTextEditor::StyleOptionAnnotationItem& option, + QRect* authorRect) const; + +protected: // QObject API + bool eventFilter(QObject* object, QEvent* event) override; + +private Q_SLOTS: + void resetBackgrounds(); + +private: + int widthHintFromViewWidth(int viewWidth) const; + +private: + KTextEditor::AnnotationModel* const m_model; + + // TODO: make this configurable + const int m_maxWidthViewPercent = 25; + + mutable QHash m_backgrounds; + + mutable int m_lastCharBasedWidthHint = 0; + mutable int m_lastViewBasedWidthHint = 0; +}; + +} + +#endif