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 | |
27 | using namespace GitServer; |
28 | |
29 | static const int MIN_VIEW_WIDTH_PX = 480; |
30 | |
31 | RepositoryViewDelegate::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 | |
42 | void 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 | |
127 | QSize RepositoryViewDelegate::sizeHint(const QStyleOptionViewItem &, const QModelIndex &) const |
128 | { |
129 | return QSize(LANE_WIDTH, ROW_HEIGHT); |
130 | } |
131 | |
132 | bool 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 | |
159 | void 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 | |
317 | QColor RepositoryViewDelegate::getMergeColor(const Lane ¤tLane, 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 | |
353 | void 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 | |
424 | void 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 | |
456 | void 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 | |
544 | void 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 | |