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"