diff --git a/kdevplatform/vcs/CMakeLists.txt b/kdevplatform/vcs/CMakeLists.txt --- a/kdevplatform/vcs/CMakeLists.txt +++ b/kdevplatform/vcs/CMakeLists.txt @@ -46,6 +46,12 @@ 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" diff --git a/kdevplatform/vcs/models/vcsannotationmodel.h b/kdevplatform/vcs/models/vcsannotationmodel.h --- a/kdevplatform/vcs/models/vcsannotationmodel.h +++ b/kdevplatform/vcs/models/vcsannotationmodel.h @@ -35,6 +35,7 @@ { class VcsJob; +class VcsAnnotationLine; class KDEVPLATFORMVCS_EXPORT VcsAnnotationModel : public KTextEditor::AnnotationModel { @@ -48,6 +49,10 @@ 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; diff --git a/kdevplatform/vcs/models/vcsannotationmodel.cpp b/kdevplatform/vcs/models/vcsannotationmodel.cpp --- a/kdevplatform/vcs/models/vcsannotationmodel.cpp +++ b/kdevplatform/vcs/models/vcsannotationmodel.cpp @@ -182,6 +182,15 @@ 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/vcspluginhelper.cpp b/kdevplatform/vcs/vcspluginhelper.cpp --- a/kdevplatform/vcs/vcspluginhelper.cpp +++ b/kdevplatform/vcs/vcspluginhelper.cpp @@ -1,6 +1,7 @@ /*************************************************************************** * 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 * @@ -28,6 +29,7 @@ #include #include #include +#include #include #include @@ -45,6 +47,9 @@ #include #include #include +#if KTEXTEDITOR_VERSION >= QT_VERSION_CHECK(5,53,0) +#include +#endif #include #include #include @@ -362,8 +367,9 @@ 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; @@ -379,7 +385,7 @@ 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)) { @@ -392,12 +398,21 @@ 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.")); @@ -411,6 +426,13 @@ 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()); diff --git a/kdevplatform/vcs/widgets/vcsannotationitemdelegate.h b/kdevplatform/vcs/widgets/vcsannotationitemdelegate.h new file mode 100644 --- /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 diff --git a/kdevplatform/vcs/widgets/vcsannotationitemdelegate.cpp b/kdevplatform/vcs/widgets/vcsannotationitemdelegate.cpp new file mode 100644 --- /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; +}