1#include "RepositoryViewDelegate.h"
2
3#include <Colors.h>
4#include <CommitHistoryColumns.h>
5#include <CommitHistoryModel.h>
6#include <CommitHistoryView.h>
7#include <CommitInfo.h>
8#include <GitBase.h>
9#include <GitCache.h>
10#include <GitLocal.h>
11#include <GitQlientStyles.h>
12#include <GitServerCache.h>
13#include <Lane.h>
14#include <LaneType.h>
15#include <PullRequest.h>
16
17#include <QApplication>
18#include <QClipboard>
19#include <QDesktopServices>
20#include <QEvent>
21#include <QPainter>
22#include <QPainterPath>
23#include <QSortFilterProxyModel>
24#include <QToolTip>
25#include <QUrl>
26
27using namespace GitServer;
28
29static const int MIN_VIEW_WIDTH_PX = 480;
30
31RepositoryViewDelegate::RepositoryViewDelegate(const QSharedPointer<GitCache> &cache,
32 const QSharedPointer<GitBase> &git,
33 const QSharedPointer<GitServerCache> &gitServerCache,
34 CommitHistoryView *view)
35 : mCache(cache)
36 , mGit(git)
37 , mGitServerCache(gitServerCache)
38 , mView(view)
39{
40}
41
42void RepositoryViewDelegate::paint(QPainter *p, const QStyleOptionViewItem &opt, const QModelIndex &index) const
43{
44 p->setRenderHints(QPainter::Antialiasing);
45
46 QStyleOptionViewItem newOpt(opt);
47 newOpt.font.setPointSize(9);
48
49 if (newOpt.state & QStyle::State_Selected)
50 p->fillRect(newOpt.rect, GitQlientStyles::getGraphSelectionColor());
51 else if (newOpt.state & QStyle::State_MouseOver)
52 p->fillRect(newOpt.rect, GitQlientStyles::getGraphHoverColor());
53
54 const auto row = mView->hasActiveFilter()
55 ? dynamic_cast<QSortFilterProxyModel *>(mView->model())->mapToSource(index).row()
56 : index.row();
57
58 const auto commit = mCache->commitInfo(row);
59
60 if (commit.sha.isEmpty())
61 return;
62
63 if (index.column() == static_cast<int>(CommitHistoryColumns::Graph))
64 {
65 newOpt.rect.setX(newOpt.rect.x() + 10);
66 paintGraph(p, newOpt, commit);
67 }
68 else if (index.column() == static_cast<int>(CommitHistoryColumns::Log))
69 paintLog(p, newOpt, commit, index.data().toString());
70 else
71 {
72
73 p->setPen(GitQlientStyles::getTextColor());
74 newOpt.rect.setX(newOpt.rect.x() + 10);
75
76 QTextOption textalignment(Qt::AlignLeft | Qt::AlignVCenter);
77 auto text = index.data().toString();
78
79 if (index.column() == static_cast<int>(CommitHistoryColumns::Date))
80 {
81 textalignment = QTextOption(Qt::AlignRight | Qt::AlignVCenter);
82 const auto prev = QDateTime::fromString(mView->indexAbove(index).data().toString(), "dd MMM yyyy hh:mm");
83 const auto current = QDateTime::fromString(text, "dd MMM yyyy hh:mm");
84
85 if (current.date() == prev.date())
86 text = current.toString("hh:mm");
87 else
88 text = current.toString("dd MMM yyyy - hh:mm");
89
90 newOpt.rect.setWidth(newOpt.rect.width() - 5);
91 }
92 else if (index.column() == static_cast<int>(CommitHistoryColumns::Sha))
93 {
94 newOpt.font.setPointSize(8);
95 newOpt.font.setFamily("DejaVu Sans Mono");
96
97 text = commit.sha != CommitInfo::ZERO_SHA ? text.left(8) : "";
98 }
99 else if (index.column() == static_cast<int>(CommitHistoryColumns::Author) && commit.isSigned())
100 {
101 static const auto size = 15;
102 static const auto offset = 5;
103 QPixmap pic(QString::fromUtf8(commit.verifiedSignature() ? ":/icons/signed" : ":/icons/unsigned"));
104 pic = pic.scaled(size, size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
105
106 const auto inc = (newOpt.rect.height() - size) / 2;
107
108 p->drawPixmap(QRect(newOpt.rect.x(), newOpt.rect.y() + inc, size, size), pic);
109
110 newOpt.rect.setX(newOpt.rect.x() + size + offset);
111 }
112
113 QFontMetrics fm(newOpt.font);
114 p->setFont(newOpt.font);
115
116 if (const auto cursorColumn = mView->indexAt(mView->mapFromGlobal(QCursor::pos())).column();
117 newOpt.state & QStyle::State_MouseOver && cursorColumn == index.column()
118 && cursorColumn == static_cast<int>(CommitHistoryColumns::Sha))
119 {
120 p->setPen(gitQlientOrange);
121 }
122
123 p->drawText(newOpt.rect, fm.elidedText(text, Qt::ElideRight, newOpt.rect.width()), textalignment);
124 }
125}
126
127QSize RepositoryViewDelegate::sizeHint(const QStyleOptionViewItem &, const QModelIndex &) const
128{
129 return QSize(LANE_WIDTH, ROW_HEIGHT);
130}
131
132bool RepositoryViewDelegate::editorEvent(QEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option,
133 const QModelIndex &index)
134{
135 const auto cursorColumn = mView->indexAt(mView->mapFromGlobal(QCursor::pos())).column();
136
137 if (event->type() == QEvent::MouseButtonPress && cursorColumn == index.column()
138 && cursorColumn == static_cast<int>(CommitHistoryColumns::Sha))
139 {
140 mColumnPressed = cursorColumn;
141 return true;
142 }
143 else if (event->type() == QEvent::MouseButtonRelease && cursorColumn == index.column() && mColumnPressed != -1)
144 {
145 const auto text = index.data().toString();
146 if (cursorColumn == static_cast<int>(CommitHistoryColumns::Sha) && text != CommitInfo::ZERO_SHA)
147 {
148 QApplication::clipboard()->setText(text);
149 QToolTip::showText(QCursor::pos(), tr("Copied!"), mView);
150 }
151
152 mColumnPressed = -1;
153 return true;
154 }
155
156 return QStyledItemDelegate::editorEvent(event, model, option, index);
157}
158
159void RepositoryViewDelegate::paintGraphLane(QPainter *p, const Lane &lane, bool laneHeadPresent, int x1, int x2,
160 const QColor &col, const QColor &activeCol, const QColor &mergeColor,
161 bool isWip, bool hasChilds) const
162{
163 const auto padding = 2;
164 x1 += padding;
165 x2 += padding;
166
167 const auto h = ROW_HEIGHT / 2;
168 const auto m = (x1 + x2) / 2;
169 const auto r = (x2 - x1) * 1 / 3;
170 const auto spanAngle = 90 * 16;
171 const auto angleWidthRight = 2 * (x1 - m);
172 const auto angleWidthLeft = 2 * (x2 - m);
173 const auto angleHeightUp = 2 * h;
174 const auto angleHeightDown = 2 * -h;
175
176 static QPen lanePen(GitQlientStyles::getTextColor(), 2); // fast path here
177
178 // arc
179 lanePen.setBrush(col);
180 p->setPen(lanePen);
181
182 switch (lane.getType())
183 {
184 case LaneType::JOIN:
185 case LaneType::JOIN_R:
186 case LaneType::HEAD:
187 case LaneType::HEAD_R: {
188 p->drawArc(m, h, angleWidthRight, angleHeightUp, 0 * 16, spanAngle);
189 break;
190 }
191 case LaneType::JOIN_L: {
192 p->drawArc(m, h, angleWidthLeft, angleHeightUp, 90 * 16, spanAngle);
193 break;
194 }
195 case LaneType::TAIL:
196 case LaneType::TAIL_R: {
197 p->drawArc(m, h, angleWidthRight, angleHeightDown, 270 * 16, spanAngle);
198 break;
199 }
200 default:
201 break;
202 }
203
204 if (isWip)
205 {
206 lanePen.setColor(activeCol);
207 p->setPen(lanePen);
208 }
209
210 // vertical line
211 if (!(isWip && !hasChilds))
212 {
213 if (!isWip && !hasChilds
214 && (lane.getType() == LaneType::HEAD || lane.getType() == LaneType::INITIAL
215 || lane.getType() == LaneType::BRANCH || lane.getType() == LaneType::MERGE_FORK
216 || lane.getType() == LaneType::MERGE_FORK_R || lane.getType() == LaneType::MERGE_FORK_L
217 || lane.getType() == LaneType::ACTIVE))
218 p->drawLine(m, h, m, 2 * h);
219 else
220 {
221 switch (lane.getType())
222 {
223 case LaneType::ACTIVE:
224 case LaneType::NOT_ACTIVE:
225 case LaneType::MERGE_FORK:
226 case LaneType::MERGE_FORK_R:
227 case LaneType::MERGE_FORK_L:
228 case LaneType::JOIN:
229 case LaneType::JOIN_R:
230 case LaneType::JOIN_L:
231 case LaneType::CROSS:
232 p->drawLine(m, 0, m, 2 * h);
233 break;
234 case LaneType::HEAD_L:
235 case LaneType::BRANCH:
236 p->drawLine(m, h, m, 2 * h);
237 break;
238 case LaneType::TAIL_L:
239 case LaneType::INITIAL:
240 p->drawLine(m, 0, m, h);
241 break;
242 default:
243 break;
244 }
245 }
246 }
247
248 // center symbol
249 auto isCommit = false;
250
251 if (isWip)
252 {
253 isCommit = true;
254 p->setPen(QPen(col, 2));
255 p->setBrush(col);
256 p->drawEllipse(m - r + 2, h - r + 2, 8, 8);
257 }
258 else
259 {
260 switch (lane.getType())
261 {
262 case LaneType::HEAD:
263 case LaneType::INITIAL:
264 case LaneType::BRANCH:
265 case LaneType::MERGE_FORK:
266 case LaneType::MERGE_FORK_R:
267 isCommit = true;
268 p->setPen(QPen(mergeColor, 2));
269 p->setBrush(col);
270 p->drawEllipse(m - r + 2, h - r + 2, 8, 8);
271 break;
272 case LaneType::MERGE_FORK_L:
273 isCommit = true;
274 p->setPen(QPen(laneHeadPresent ? mergeColor : col, 2));
275 p->setBrush(col);
276 p->drawEllipse(m - r + 2, h - r + 2, 8, 8);
277 break;
278 case LaneType::ACTIVE: {
279 isCommit = true;
280 p->setPen(QPen(col, 2));
281 p->setBrush(QColor(isWip ? col : GitQlientStyles::getBackgroundColor()));
282 p->drawEllipse(m - r + 2, h - r + 2, 8, 8);
283 }
284 break;
285 default:
286 break;
287 }
288 }
289
290 lanePen.setColor(mergeColor);
291 p->setPen(lanePen);
292
293 // horizontal line
294 switch (lane.getType())
295 {
296 case LaneType::MERGE_FORK:
297 case LaneType::JOIN:
298 case LaneType::HEAD:
299 case LaneType::TAIL:
300 case LaneType::CROSS:
301 case LaneType::CROSS_EMPTY:
302 p->drawLine(x1 + (isCommit ? 10 : 0), h, x2, h);
303 break;
304 case LaneType::MERGE_FORK_R:
305 p->drawLine(x1 + (isCommit ? 0 : 10), h, m - (isCommit ? 6 : 0), h);
306 break;
307 case LaneType::MERGE_FORK_L:
308 case LaneType::HEAD_L:
309 case LaneType::TAIL_L:
310 p->drawLine(m + (isCommit ? 6 : 0), h, x2, h);
311 break;
312 default:
313 break;
314 }
315}
316
317QColor RepositoryViewDelegate::getMergeColor(const Lane &currentLane, const CommitInfo &commit, int currentLaneIndex,
318 const QColor &defaultColor, bool &isSet) const
319{
320 auto mergeColor = defaultColor;
321 //= GitQlientStyles::getBranchColorAt((commit.getLanesCount() - 1) % GitQlientStyles::getTotalBranchColors());
322
323 switch (currentLane.getType())
324 {
325 case LaneType::HEAD_L:
326 case LaneType::HEAD_R:
327 case LaneType::TAIL_L:
328 case LaneType::TAIL_R:
329 case LaneType::MERGE_FORK_L:
330 case LaneType::JOIN_R:
331 isSet = true;
332 mergeColor = defaultColor;
333 break;
334 case LaneType::MERGE_FORK_R:
335 case LaneType::JOIN_L:
336 for (auto laneCount = 0; laneCount < currentLaneIndex; ++laneCount)
337 {
338 if (commit.laneAt(laneCount).equals(LaneType::JOIN_L))
339 {
340 mergeColor = GitQlientStyles::getBranchColorAt(laneCount % GitQlientStyles::getTotalBranchColors());
341 isSet = true;
342 break;
343 }
344 }
345 break;
346 default:
347 break;
348 }
349
350 return mergeColor;
351}
352
353void RepositoryViewDelegate::paintGraph(QPainter *p, const QStyleOptionViewItem &opt, const CommitInfo &commit) const
354{
355 p->save();
356 p->setClipRect(opt.rect, Qt::IntersectClip);
357 p->translate(opt.rect.topLeft());
358
359 if (mView->hasActiveFilter())
360 {
361 const auto activeColor = GitQlientStyles::getBranchColorAt(0);
362 paintGraphLane(p, LaneType::ACTIVE, false, 0, LANE_WIDTH, activeColor, activeColor, activeColor, false,
363 commit.hasChilds());
364 }
365 else
366 {
367 if (commit.sha == CommitInfo::ZERO_SHA)
368 {
369 const auto activeColor = GitQlientStyles::getBranchColorAt(0);
370 QColor color = activeColor;
371
372 if (mCache->pendingLocalChanges())
373 color = gitQlientOrange;
374
375 paintGraphLane(p, LaneType::BRANCH, false, 0, LANE_WIDTH, color, activeColor, activeColor, true,
376 commit.parentsCount() != 0);
377 }
378 else
379 {
380 const auto laneNum = commit.lanesCount();
381 const auto activeLane = commit.getActiveLane();
382 const auto activeColor
383 = GitQlientStyles::getBranchColorAt(activeLane % GitQlientStyles::getTotalBranchColors());
384 auto x1 = 0;
385 auto isSet = false;
386 auto laneHeadPresent = false;
387 auto mergeColor = GitQlientStyles::getBranchColorAt((laneNum - 1) % GitQlientStyles::getTotalBranchColors());
388
389 for (auto i = laneNum - 1, x2 = LANE_WIDTH * laneNum; i >= 0; --i, x2 -= LANE_WIDTH)
390 {
391 x1 = x2 - LANE_WIDTH;
392
393 auto currentLane = commit.laneAt(i);
394
395 if (!laneHeadPresent && i < laneNum - 1)
396 {
397 auto prevLane = commit.laneAt(i + 1);
398 laneHeadPresent
399 = prevLane.isHead() || prevLane.equals(LaneType::JOIN_R) || prevLane.equals(LaneType::JOIN_L);
400 }
401
402 if (!currentLane.equals(LaneType::EMPTY))
403 {
404 auto color = activeColor;
405
406 if (i != activeLane)
407 color = GitQlientStyles::getBranchColorAt(i % GitQlientStyles::getTotalBranchColors());
408
409 if (!isSet)
410 mergeColor = getMergeColor(currentLane, commit, i, color, isSet);
411
412 paintGraphLane(p, currentLane, laneHeadPresent, x1, x2, color, activeColor, mergeColor, false,
413 commit.hasChilds());
414
415 if (mView->hasActiveFilter())
416 break;
417 }
418 }
419 }
420 }
421 p->restore();
422}
423
424void RepositoryViewDelegate::paintLog(QPainter *p, const QStyleOptionViewItem &opt, const CommitInfo &commit,
425 const QString &text) const
426{
427 const auto sha = commit.sha;
428
429 if (sha.isEmpty())
430 return;
431
432 auto offset = 0;
433
434 if (mGitServerCache)
435 {
436 if (const auto pr = mGitServerCache->getPullRequest(commit.sha); pr.isValid())
437 {
438 offset = 5;
439 paintPrStatus(p, opt, offset, pr);
440 }
441 }
442
443 paintTagBranch(p, opt, offset, sha);
444
445 auto newOpt = opt;
446 newOpt.rect.setX(opt.rect.x() + offset + 5);
447
448 QFontMetrics fm(newOpt.font);
449
450 p->setFont(newOpt.font);
451 p->setPen(GitQlientStyles::getTextColor());
452 p->drawText(newOpt.rect, fm.elidedText(text, Qt::ElideRight, newOpt.rect.width()),
453 QTextOption(Qt::AlignLeft | Qt::AlignVCenter));
454}
455
456void RepositoryViewDelegate::paintTagBranch(QPainter *painter, QStyleOptionViewItem o, int &startPoint,
457 const QString &sha) const
458{
459 if (mCache->hasReferences(sha) && !mView->hasActiveFilter())
460 {
461 QVector<QString> marks;
462 QVector<QColor> colors;
463 const auto currentBranch = mGit->getCurrentBranch();
464
465 if (startPoint == 0)
466 startPoint = 5;
467
468 if ((currentBranch.isEmpty() || currentBranch == "HEAD"))
469 {
470 if (const auto ret = mGit->getLastCommit(); ret.success && sha == ret.output.trimmed())
471 {
472 marks.append("detached");
473 colors.append(graphDetached);
474 }
475 }
476
477 const auto localBranches = mCache->getReferences(sha, References::Type::LocalBranch);
478 for (const auto &branch : localBranches)
479 {
480 if (branch == currentBranch)
481 {
482 marks.prepend(branch);
483 colors.prepend(graphCurrentBranch);
484 }
485 else
486 {
487 marks.append(branch);
488 colors.append(graphLocalBranch);
489 }
490 }
491
492 const auto tags = mCache->getReferences(sha, References::Type::LocalTag);
493 for (const auto &tag : tags)
494 {
495 marks.append(tag);
496 colors.append(graphTag);
497 }
498
499 const auto remoteBranches = mCache->getReferences(sha, References::Type::RemoteBranches);
500 for (const auto &branch : remoteBranches)
501 {
502 marks.append(branch);
503 colors.append(graphRemoteBranch);
504 }
505
506 const auto showMinimal = o.rect.width() <= MIN_VIEW_WIDTH_PX;
507 const auto mark_spacing = 5; // Space between markers in pixels
508 const auto mapEnd = marks.constEnd();
509
510 auto mapIt = marks.constBegin();
511 auto colorIter = colors.constBegin();
512 for (; mapIt != mapEnd; ++mapIt, ++colorIter)
513 {
514 const auto isCurrentSpot = *mapIt == "detached" || *mapIt == currentBranch;
515 o.font.setBold(isCurrentSpot);
516
517 const auto nameToDisplay = showMinimal ? QString(". . .") : *mapIt;
518 const QFontMetrics fm(o.font);
519 const auto textBoundingRect = fm.boundingRect(nameToDisplay);
520 const int textPadding = 3;
521 const auto rectWidth = textBoundingRect.width() + 2 * textPadding;
522 const QRectF markerRect(o.rect.x() + startPoint, o.rect.y() + 2, rectWidth, ROW_HEIGHT - 4);
523
524 painter->save();
525 painter->setRenderHint(QPainter::Antialiasing);
526 painter->setPen(QPen(*colorIter, 2));
527 QPainterPath path;
528 path.addRoundedRect(markerRect, 1, 1);
529 painter->fillPath(path, *colorIter);
530 painter->drawPath(path);
531
532 // TODO: Fix this with a nicer way
533 painter->setPen(QColor(*colorIter == graphTag ? textColorBright : textColorDark));
534
535 painter->setFont(o.font);
536 painter->drawText(markerRect, Qt::AlignCenter, nameToDisplay);
537 painter->restore();
538
539 startPoint += rectWidth + mark_spacing;
540 }
541 }
542}
543
544void RepositoryViewDelegate::paintPrStatus(QPainter *painter, QStyleOptionViewItem opt, int &startPoint,
545 const PullRequest &pr) const
546{
547 QColor c;
548
549 switch (pr.state.eState)
550 {
551 case PullRequest::HeadState::State::Failure:
552 c = GitQlientStyles::getRed();
553 break;
554 case PullRequest::HeadState::State::Success:
555 c = GitQlientStyles::getGreen();
556 break;
557 default:
558 case PullRequest::HeadState::State::Pending:
559 c = GitQlientStyles::getOrange();
560 break;
561 }
562
563 painter->save();
564 painter->setRenderHint(QPainter::Antialiasing);
565 painter->setPen(c);
566 painter->setBrush(c);
567 painter->drawEllipse(opt.rect.x() + startPoint, opt.rect.y() + (opt.rect.height() / 2) - 5, 10, 10);
568 painter->restore();
569
570 startPoint += 10 + 5;
571}
572