File indexing completed on 2024-05-12 04:38:55

0001 /*
0002     SPDX-FileCopyrightText: 2017-2018 Friedrich W. H. Kossebau <kossebau@kde.org>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "vcsannotationitemdelegate.h"
0008 
0009 #include <models/vcsannotationmodel.h>
0010 #include <vcsannotation.h>
0011 #include <debug.h>
0012 
0013 #include <KTextEditor/AnnotationInterface>
0014 #include <KTextEditor/View>
0015 #include <KTextEditor/ConfigInterface>
0016 #include <KTextEditor/Attribute>
0017 #include <KLocalizedString>
0018 
0019 #include <QHelpEvent>
0020 #include <QPainter>
0021 #include <QAction>
0022 #include <QMenu>
0023 #include <QToolTip>
0024 #include <QFontMetricsF>
0025 #include <QDate>
0026 #include <QStyle>
0027 #include <QApplication>
0028 
0029 #include <cmath>
0030 
0031 using namespace KDevelop;
0032 
0033 VcsAnnotationItemDelegate::VcsAnnotationItemDelegate(KTextEditor::View* view, KTextEditor::AnnotationModel* model,
0034                                                      QObject* parent)
0035     : KTextEditor::AbstractAnnotationItemDelegate(parent)
0036     , m_model(model)
0037 {
0038     // dump background brushes on schema change
0039     Q_ASSERT(qobject_cast<KTextEditor::ConfigInterface*>(view));
0040     connect(view, &KTextEditor::View::configChanged, this, &VcsAnnotationItemDelegate::resetBackgrounds);
0041 
0042     view->installEventFilter(this);
0043 }
0044 
0045 VcsAnnotationItemDelegate::~VcsAnnotationItemDelegate() = default;
0046 
0047 static QString ageOfDate(const QDate& date)
0048 {
0049     const auto now = QDate::currentDate();
0050     int ageInYears = now.year() - date.year();
0051     if (now < date.addYears(ageInYears)) {
0052         --ageInYears;
0053     }
0054     if (ageInYears > 0) {
0055         return i18ncp("@item age", "%1 year", "%1 years", ageInYears);
0056     }
0057     int ageInMonths = now.month() - date.month();
0058     if (now.day() < date.day()) {
0059         --ageInMonths;
0060     }
0061     if (ageInMonths < 0) {
0062         ageInMonths += 12;
0063     }
0064     if (ageInMonths > 0) {
0065         return i18ncp("@item age", "%1 month", "%1 months", ageInMonths);
0066     }
0067     const int ageInDays = date.daysTo(now);
0068     if (ageInDays > 0) {
0069         return i18ncp("@item age", "%1 day", "%1 days", ageInDays);
0070     }
0071     return i18nc("@item age", "Today");
0072 }
0073 
0074 void VcsAnnotationItemDelegate::doMessageLineLayout(const KTextEditor::StyleOptionAnnotationItem& option,
0075                                                     QRect* messageRect, QRect* ageRect) const
0076 {
0077     Q_ASSERT(messageRect && messageRect->isValid());
0078     Q_ASSERT(ageRect);
0079 
0080     const QWidget* const widget = option.view;
0081     QStyle* const style = widget ? widget->style() : QApplication::style();
0082     const bool hasAge = ageRect->isValid();
0083     // "+ 1" as used in QItemDelegate
0084     const int textMargin = style->pixelMetric(QStyle::PM_FocusFrameHMargin, nullptr, widget) + 1;
0085     const int ageMargin = hasAge ? textMargin : 0;
0086 
0087     const int x = option.rect.left();
0088     const int y = option.rect.top();
0089     const int w = option.rect.width();
0090     const int h = option.rect.height();
0091 
0092     // add margins for fixed elements
0093     QSize ageSize(0, 0); // ageRect could be invalid, so use separate object for calculation
0094     if (hasAge) {
0095         ageSize = ageRect->size();
0096         ageSize.rwidth() += 2 * ageMargin;
0097     }
0098 
0099     // distribute space among layout items
0100     QRect message;
0101     QRect age;
0102     if (option.direction == Qt::LeftToRight) {
0103         message.setRect(x, y, w - ageSize.width(), h);
0104         age.setRect(message.right() + 1, y, ageSize.width(), h);
0105     } else {
0106         age.setRect(x, y, ageSize.width(), h);
0107         message.setRect(age.right() + 1, y, w - ageSize.width(), h);
0108     }
0109     // remove margins here, so renderMessageAndAge does not have to
0110     message.adjust(textMargin, 0, -textMargin, 0);
0111     age.adjust(ageMargin, 0, -ageMargin, 0);
0112 
0113     // return result
0114     *ageRect = age;
0115     *messageRect = QStyle::alignedRect(option.direction, Qt::AlignLeading,
0116                                        messageRect->size().boundedTo(message.size()), message);
0117 }
0118 
0119 void VcsAnnotationItemDelegate::doAuthorLineLayout(const KTextEditor::StyleOptionAnnotationItem& option,
0120                                                    QRect* authorRect) const
0121 {
0122     Q_ASSERT(authorRect);
0123 
0124     // if invalid, nothing to be done, keep as is
0125     if (!authorRect->isValid()) {
0126         return;
0127     }
0128 
0129     const QWidget* const widget = option.view;
0130     QStyle* const style = widget ? widget->style() : QApplication::style();
0131     // "+ 1" as used in QItemDelegate
0132     const int authorMargin = style->pixelMetric(QStyle::PM_FocusFrameHMargin, nullptr, widget) + 1;
0133 
0134     QRect author = option.rect;
0135     // remove margins here, so renderAuthor does not have to
0136     author.adjust(authorMargin, 0, -authorMargin, 0);
0137 
0138     // return result
0139     *authorRect = QStyle::alignedRect(option.direction, Qt::AlignLeading,
0140                                       authorRect->size().boundedTo(author.size()), author);
0141 }
0142 
0143 void VcsAnnotationItemDelegate::renderBackground(QPainter* painter,
0144                                                  const KTextEditor::StyleOptionAnnotationItem& option,
0145                                                  const VcsAnnotationLine& annotationLine) const
0146 {
0147     Q_UNUSED(option);
0148 
0149     const auto revision = annotationLine.revision();
0150     auto brushIt = m_backgrounds.find(revision);
0151     if (brushIt == m_backgrounds.end()) {
0152         KTextEditor::Attribute::Ptr normalStyle = option.view->defaultStyleAttribute(KTextEditor::dsNormal);
0153         const auto background = (normalStyle->hasProperty(QTextFormat::BackgroundBrush)) ? normalStyle->background().color() : QColor(Qt::white);
0154         const int background_y = background.red()*0.299 + 0.587*background.green()
0155                                                         + 0.114*background.blue();
0156         // get random, but reproducible 8-bit values from last two bytes of the revision hash
0157         const uint revisionHash = qHash(revision);
0158         const int u = static_cast<int>((0xFF & revisionHash));
0159         const int v = static_cast<int>((0xFF00 & revisionHash) >> 8);
0160         const int r = qRound(qMin(255.0, qMax(0.0, background_y + 1.402*(v-128))));
0161         const int g = qRound(qMin(255.0, qMax(0.0, background_y - 0.344*(u-128) - 0.714*(v-128))));
0162         const int b = qRound(qMin(255.0, qMax(0.0, background_y + 1.772*(u-128))));
0163         brushIt = m_backgrounds.insert(revision, QBrush(QColor(r, g, b)));
0164     }
0165 
0166     painter->fillRect(option.rect, brushIt.value());
0167 }
0168 
0169 void VcsAnnotationItemDelegate::renderMessageAndAge(QPainter* painter,
0170                                                     const KTextEditor::StyleOptionAnnotationItem& option,
0171                                                     const QRect& messageRect, const QString& messageText,
0172                                                     const QRect& ageRect, const QString& ageText) const
0173 {
0174     Q_UNUSED(option);
0175 
0176     painter->save();
0177 
0178     KTextEditor::Attribute::Ptr normalStyle = option.view->defaultStyleAttribute(KTextEditor::dsNormal);
0179     painter->setPen(normalStyle->foreground().color());
0180     painter->drawText(messageRect, Qt::AlignLeading | Qt::AlignVCenter,
0181                       painter->fontMetrics().elidedText(messageText, Qt::ElideRight, messageRect.width()));
0182 
0183     // TODO: defaultStyleAttribute only returns reliably for dsNormal, so what to do for a comment-like color?
0184     KTextEditor::Attribute::Ptr commentStyle = option.view->defaultStyleAttribute(KTextEditor::dsNormal);
0185     painter->setPen(commentStyle->foreground().color());
0186     painter->drawText(ageRect, Qt::AlignTrailing | Qt::AlignVCenter, ageText);
0187 
0188     painter->restore();
0189 }
0190 
0191 void VcsAnnotationItemDelegate::renderAuthor(QPainter* painter,
0192                                              const KTextEditor::StyleOptionAnnotationItem& option,
0193                                              const QRect& authorRect, const QString& authorText) const
0194 {
0195     Q_UNUSED(option);
0196 
0197     painter->save();
0198 
0199     // TODO: defaultStyleAttribute only returns reliably for dsNormal, so what to do for a comment-like color?
0200     KTextEditor::Attribute::Ptr commentStyle = option.view->defaultStyleAttribute(KTextEditor::dsNormal);
0201     painter->setPen(commentStyle->foreground().color());
0202     painter->drawText(authorRect, Qt::AlignLeading | Qt::AlignVCenter,
0203                       painter->fontMetrics().elidedText(authorText, Qt::ElideRight, authorRect.width()));
0204 
0205     painter->restore();
0206 }
0207 
0208 void VcsAnnotationItemDelegate::renderHighlight(QPainter* painter,
0209                                                 const KTextEditor::StyleOptionAnnotationItem& option) const
0210 {
0211     // Draw a border around all adjacent entries that have the same text as the currently hovered one
0212     if ((option.state & QStyle::State_MouseOver) &&
0213         (option.annotationItemGroupingPosition & KTextEditor::StyleOptionAnnotationItem::InGroup)) {
0214         KTextEditor::Attribute::Ptr style = option.view->defaultStyleAttribute(KTextEditor::dsNormal);
0215         painter->setPen(style->foreground().color());
0216         // Use floating point coordinates to support scaled rendering
0217         QRectF rect(option.rect);
0218         rect.adjust(0.5, 0.5, -0.5, -0.5);
0219         // draw left and right highlight borders
0220         painter->drawLine(rect.topLeft(), rect.bottomLeft());
0221         painter->drawLine(rect.topRight(), rect.bottomRight());
0222 
0223         if ((option.annotationItemGroupingPosition & KTextEditor::StyleOptionAnnotationItem::GroupBegin) &&
0224             (option.wrappedLine == 0)) {
0225             painter->drawLine(rect.topLeft(), rect.topRight());
0226         }
0227 
0228         if ((option.annotationItemGroupingPosition & KTextEditor::StyleOptionAnnotationItem::GroupEnd) &&
0229             (option.wrappedLine == (option.wrappedLineCount-1))) {
0230             painter->drawLine(rect.bottomLeft(), rect.bottomRight());
0231         }
0232     }
0233 }
0234 
0235 void VcsAnnotationItemDelegate::paint(QPainter* painter, const KTextEditor::StyleOptionAnnotationItem& option,
0236                                       KTextEditor::AnnotationModel* model, int line) const
0237 {
0238     Q_ASSERT(painter);
0239     // we cannot use custom roles and data() API (cmp. VcsAnnotationModel dox), so accessing custom API instead
0240     auto* vcsModel = qobject_cast<VcsAnnotationModel*>(model);
0241     Q_ASSERT(vcsModel);
0242     if (!painter || !vcsModel) {
0243         return;
0244     }
0245     // test of line just for sake of completeness skipped here
0246 
0247     // Fetch data from the model
0248     const VcsAnnotationLine annotationLine = vcsModel->annotationLine(line);
0249 
0250     if (annotationLine.revision().revisionType() == VcsRevision::Invalid) {
0251         return;
0252     }
0253 
0254     // prepare
0255     painter->save();
0256 
0257     renderBackground(painter, option, annotationLine);
0258 
0259     // We use the normal UI font here, which usually is a proportimal one,
0260     // so more text fits into the available space.
0261     // Though we do this at the cost of not adapting to any scaled content font size,
0262     // as there is no zooming state info available, so we cannot adapt.
0263     // Tooltip font also is not scaled, and annotations could be considered to fall into
0264     // that category, so might be fine.
0265     painter->setFont(option.view->font());
0266 
0267     if (option.visibleWrappedLineInGroup == 0) {
0268         QRect ageRect;
0269         QString ageText;
0270         const auto date = annotationLine.date();
0271         if (date.isValid()) {
0272             ageText = ageOfDate(date.date());
0273             ageRect = QRect(QPoint(0, 0), QSize(option.fontMetrics.horizontalAdvance(ageText), option.rect.height()));
0274         }
0275         const auto messageText = annotationLine.commitMessage();
0276         auto messageRect =
0277             QRect(QPoint(0, 0), QSize(option.fontMetrics.horizontalAdvance(messageText), option.rect.height()));
0278 
0279         doMessageLineLayout(option, &messageRect, &ageRect);
0280 
0281         renderMessageAndAge(painter, option, messageRect, messageText, ageRect, ageText);
0282     } else if (option.visibleWrappedLineInGroup == 1) {
0283         const auto author = annotationLine.author();
0284         if (!author.isEmpty()) {
0285             const auto authorText = i18nc("By: commit author", "By: %1", author);
0286             auto authorRect =
0287                 QRect(QPoint(0, 0), QSize(option.fontMetrics.horizontalAdvance(authorText), option.rect.height()));
0288 
0289             doAuthorLineLayout(option, &authorRect);
0290 
0291             renderAuthor(painter, option, authorRect, authorText);
0292         }
0293     }
0294 
0295     renderHighlight(painter, option);
0296 
0297     // done
0298     painter->restore();
0299 }
0300 
0301 bool VcsAnnotationItemDelegate::helpEvent(QHelpEvent* event, KTextEditor::View* view,
0302                                           const KTextEditor::StyleOptionAnnotationItem& option,
0303                                           KTextEditor::AnnotationModel* model, int line)
0304 {
0305     Q_UNUSED(option);
0306     if (!model || event->type() != QEvent::ToolTip) {
0307         return false;
0308     }
0309 
0310     const QVariant data = model->data(line, Qt::ToolTipRole);
0311     if (!data.isValid()) {
0312         return false;
0313     }
0314 
0315     const QString toolTipText = data.toString();
0316     if (toolTipText.isEmpty()) {
0317         return false;
0318     }
0319 
0320     QToolTip::showText(event->globalPos(), toolTipText, view, option.rect);
0321 
0322     return true;
0323 }
0324 
0325 void VcsAnnotationItemDelegate::hideTooltip(KTextEditor::View *view)
0326 {
0327     Q_UNUSED(view);
0328     QToolTip::hideText();
0329 }
0330 
0331 QSize VcsAnnotationItemDelegate::sizeHint(const KTextEditor::StyleOptionAnnotationItem& option,
0332                                           KTextEditor::AnnotationModel* model, int line) const
0333 {
0334     Q_UNUSED(line);
0335     Q_ASSERT(model);
0336     if (!model) {
0337         return QSize(0, 0);
0338     }
0339 
0340     // Ideally the user could configure the width of the annotations, best interactively.
0341     // Until this is possible, the sizehint is: roughly 40 chars, but maximal 25 % of the view
0342     // See eventFilter for making sure we adapt the max 25 % to a changed width.
0343 
0344     const QFontMetricsF& fm(option.fontMetrics);
0345     // if only averageCharWidth would yield sane values,
0346     // multiply by 40 in average seemed okayish at least with english, showing enough of message
0347     m_lastCharBasedWidthHint = ceil(40 * fm.averageCharWidth());
0348     m_lastViewBasedWidthHint = widthHintFromViewWidth(option.view->width());
0349     return QSize(qMin(m_lastCharBasedWidthHint, m_lastViewBasedWidthHint), fm.height());
0350 }
0351 
0352 bool VcsAnnotationItemDelegate::eventFilter(QObject* object, QEvent* event)
0353 {
0354     if (event->type() == QEvent::Resize) {
0355         auto resizeEvent = static_cast<QResizeEvent*>(event);
0356         const int viewBasedWidthHint = widthHintFromViewWidth(resizeEvent->size().width());
0357         if ((viewBasedWidthHint < m_lastCharBasedWidthHint) &&
0358             (viewBasedWidthHint != m_lastViewBasedWidthHint)) {
0359             // emit for first line only, assuming uniformAnnotationItemSizes is set to true
0360             emit sizeHintChanged(m_model, 0);
0361         }
0362     }
0363 
0364     return KTextEditor::AbstractAnnotationItemDelegate::eventFilter(object, event);
0365 }
0366 
0367 void VcsAnnotationItemDelegate::resetBackgrounds()
0368 {
0369     m_backgrounds.clear();
0370 }
0371 
0372 int VcsAnnotationItemDelegate::widthHintFromViewWidth(int viewWidth) const
0373 {
0374     return viewWidth * m_maxWidthViewPercent / 100;
0375 }
0376 
0377 #include "moc_vcsannotationitemdelegate.cpp"