1#include "PrCommentsList.h"
2
3#include <GitServerCache.h>
4#include <GitHubRestApi.h>
5#include <GitLabRestApi.h>
6#include <CircularPixmap.h>
7#include <SourceCodeReview.h>
8#include <AvatarHelper.h>
9#include <CodeReviewComment.h>
10#include <ButtonLink.hpp>
11#include <Colors.h>
12#include <previewpage.h>
13#include <GitQlientSettings.h>
14
15#include <QNetworkAccessManager>
16#include <QVBoxLayout>
17#include <QLabel>
18#include <QScrollArea>
19#include <QDir>
20#include <QFile>
21#include <QStandardPaths>
22#include <QNetworkReply>
23#include <QTextEdit>
24#include <QPropertyAnimation>
25#include <QSequentialAnimationGroup>
26#include <QPushButton>
27#include <QIcon>
28#include <QScrollBar>
29//#include <QWebChannel>
30//#include <QWebEngineView>
31
32using namespace GitServer;
33
34PrCommentsList::PrCommentsList(const QSharedPointer<GitServerCache> &gitServerCache, QWidget *parent)
35 : QFrame(parent)
36 , mMutex(QMutex::Recursive)
37 , mGitServerCache(gitServerCache)
38 , mManager(new QNetworkAccessManager())
39{
40 setObjectName("IssuesViewFrame");
41}
42
43PrCommentsList::~PrCommentsList()
44{
45 delete mManager;
46}
47
48void PrCommentsList::loadData(PrCommentsList::Config config, int issueNumber)
49{
50 QMutexLocker lock(&mMutex);
51
52 connect(mGitServerCache.get(), &GitServerCache::issueUpdated, this, &PrCommentsList::processComments,
53 Qt::UniqueConnection);
54 connect(mGitServerCache.get(), &GitServerCache::prReviewsReceived, this, &PrCommentsList::onReviewsReceived,
55 Qt::UniqueConnection);
56
57 mConfig = config;
58 mIssueNumber = issueNumber;
59
60 auto issue = config == Config::Issues ? mGitServerCache->getIssue(mIssueNumber)
61 : mGitServerCache->getPullRequest(mIssueNumber);
62
63 delete mIssuesFrame;
64 delete mScroll;
65 delete layout();
66
67 mCommentsFrame = nullptr;
68
69 mIssuesLayout = new QVBoxLayout();
70 mIssuesLayout->setContentsMargins(QMargins());
71 mIssuesLayout->setSpacing(30);
72
73 mInputTextEdit = new QTextEdit();
74 mInputTextEdit->setPlaceholderText(tr("Add your comment..."));
75 mInputTextEdit->setObjectName("AddReviewInput");
76
77 const auto cancel = new QPushButton(tr("Cancel"));
78 const auto add = new QPushButton(tr("Comment"));
79 connect(cancel, &QPushButton::clicked, this, [this]() {
80 mInputTextEdit->clear();
81 mInputFrame->setVisible(false);
82 });
83
84 connect(add, &QPushButton::clicked, this, [issue, this]() {
85 connect(mGitServerCache.get(), &GitServerCache::issueUpdated, this, &PrCommentsList::processComments,
86 Qt::UniqueConnection);
87#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
88 mGitServerCache->getApi()->addIssueComment(issue, mInputTextEdit->toMarkdown());
89#else
90 mGitServerCache->getApi()->addIssueComment(issue, mInputTextEdit->toPlainText());
91#endif
92 mInputTextEdit->clear();
93 mInputFrame->setVisible(false);
94 });
95
96 const auto btnsLayout = new QHBoxLayout();
97 btnsLayout->setContentsMargins(QMargins());
98 btnsLayout->setSpacing(0);
99 btnsLayout->addWidget(cancel);
100 btnsLayout->addStretch();
101 btnsLayout->addWidget(add);
102
103 const auto inputLayout = new QVBoxLayout();
104 inputLayout->setContentsMargins(20, 20, 20, 10);
105 inputLayout->setSpacing(10);
106 inputLayout->addWidget(mInputTextEdit);
107 inputLayout->addLayout(btnsLayout);
108
109 mInputFrame = new QFrame();
110 mInputFrame->setFixedHeight(200);
111 mInputFrame->setLayout(inputLayout);
112 mInputFrame->setVisible(false);
113
114 const auto descriptionFrame = new QFrame();
115 descriptionFrame->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum);
116
117 const auto bodyLayout = new QVBoxLayout();
118 bodyLayout->setContentsMargins(20, 20, 20, 20);
119 bodyLayout->setAlignment(Qt::AlignTop);
120 bodyLayout->setSpacing(30);
121 bodyLayout->addWidget(descriptionFrame);
122 bodyLayout->addLayout(mIssuesLayout);
123 bodyLayout->addStretch();
124
125 mIssuesFrame = new QFrame();
126 mIssuesFrame->setObjectName("IssuesViewFrame");
127 mIssuesFrame->setLayout(bodyLayout);
128
129 mScroll = new QScrollArea();
130 mScroll->setWidgetResizable(true);
131 mScroll->setWidget(mIssuesFrame);
132
133 const auto aLayout = new QVBoxLayout(this);
134 aLayout->setContentsMargins(QMargins());
135 aLayout->setSpacing(0);
136 aLayout->addWidget(mScroll);
137 aLayout->addWidget(mInputFrame);
138
139 const auto creationLayout = new QHBoxLayout();
140 creationLayout->setContentsMargins(QMargins());
141 creationLayout->setSpacing(0);
142
143 const auto days = issue.creation.daysTo(QDateTime::currentDateTime());
144 const auto whenText = days <= 30
145 ? days != 0 ? tr(" %1 days ago").arg(days) : tr(" today")
146 : tr(" on %1").arg(issue.creation.date().toString(QLocale().dateFormat(QLocale::ShortFormat)));
147
148 const auto creationLabel = new QLabel();
149 creationLabel->setText(tr("<i>Created by <b>%1</b>%2</i> - ").arg(issue.creator.name, whenText));
150 creationLabel->setToolTip(issue.creation.toString(QLocale().dateTimeFormat(QLocale::ShortFormat)));
151
152 creationLayout->addWidget(creationLabel);
153
154 if (!issue.assignees.isEmpty())
155 {
156 const auto assignedLayout = new QHBoxLayout();
157 assignedLayout->setContentsMargins(QMargins());
158 assignedLayout->setSpacing(0);
159
160 assignedLayout->addWidget(new QLabel(tr("<i>Assigned to </i>")));
161
162 auto count = 0;
163 const auto totalAssignees = issue.assignees.count();
164 for (auto &assignee : issue.assignees)
165 {
166 const auto assigneLabel = new QLabel(QString("<i><b>%1</b></i>").arg(assignee.name));
167 assigneLabel->setObjectName("CreatorLink");
168 assignedLayout->addWidget(assigneLabel);
169
170 if (count++ < totalAssignees - 1)
171 assignedLayout->addWidget(new QLabel(", "));
172 }
173
174 creationLayout->addLayout(assignedLayout);
175 }
176 else
177 creationLayout->addWidget(new QLabel(tr("<i>Unassigned</i>")));
178
179 creationLayout->addStretch();
180
181 for (auto &label : issue.labels)
182 {
183 auto labelWidget = new QLabel();
184 labelWidget->setStyleSheet(QString("QLabel {"
185 "background-color: #%1;"
186 "border-radius: 7px;"
187 "min-height: 15px;"
188 "max-height: 15px;"
189 "min-width: 15px;"
190 "max-width: 15px;}")
191 .arg(label.colorHex));
192 labelWidget->setToolTip(label.name);
193 creationLayout->addWidget(labelWidget);
194 creationLayout->addItem(new QSpacerItem(10, 1, QSizePolicy::Fixed, QSizePolicy::Fixed));
195 }
196
197 if (!issue.milestone.title.isEmpty())
198 {
199 const auto milestone = new QLabel(QString("%1").arg(issue.milestone.title));
200 milestone->setObjectName("IssueLabel");
201 creationLayout->addWidget(milestone);
202 }
203
204 const auto layout = new QVBoxLayout(descriptionFrame);
205 layout->setContentsMargins(QMargins());
206 layout->setSpacing(10);
207 layout->addLayout(creationLayout);
208
209 const auto bodyDescFrame = new QFrame();
210 bodyDescFrame->setObjectName("IssueDescription");
211
212 const auto bodyDescLayout = new QVBoxLayout(bodyDescFrame);
213 bodyDescLayout->setContentsMargins(10, 10, 10, 10);
214
215 GitQlientSettings settings("");
216 const auto colorSchema = settings.globalValue("colorSchema", "dark").toString();
217 const auto style = colorSchema == "dark" ? QString::fromUtf8("dark") : QString::fromUtf8("bright");
218
219// QPointer<QWebEngineView> body = new QWebEngineView();
220
221// PreviewPage *page = new PreviewPage(this);
222// body->setPage(page);
223
224// QWebChannel *channel = new QWebChannel(this);
225// channel->registerObject(QStringLiteral("content"), &m_content);
226// page->setWebChannel(channel);
227
228// body->setUrl(QUrl(QString("qrc:/resources/index_%1.html").arg(style)));
229// body->setFixedHeight(20);
230
231// connect(page, &PreviewPage::contentsSizeChanged, this, [body](const QSizeF size) {
232// if (body)
233// body->setFixedHeight(size.height());
234// });
235
236 m_content.setText(QString::fromUtf8(issue.body));
237
238// bodyDescLayout->addWidget(body);
239 layout->addWidget(bodyDescFrame);
240
241 descriptionFrame->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
242
243 const auto separator = new QFrame();
244 separator->setObjectName("orangeHSeparator");
245
246 mIssuesLayout->addWidget(separator);
247
248 if (mConfig == Config::Issues)
249 mGitServerCache->getApi()->requestComments(issue.number);
250 else
251 {
252 mGitServerCache->getApi()->requestComments(mIssueNumber);
253 mGitServerCache->getApi()->requestReviews(mIssueNumber);
254 }
255}
256
257void PrCommentsList::highlightComment(int frameId)
258{
259 const auto daFrame = mComments.value(frameId);
260
261 mScroll->ensureWidgetVisible(daFrame);
262
263 const auto animationGoup = new QSequentialAnimationGroup();
264 auto animation = new QPropertyAnimation(daFrame, "color");
265 animation->setDuration(500);
266 animation->setStartValue(highlightCommentStart);
267 animation->setEndValue(highlightCommentEnd);
268 animationGoup->addAnimation(animation);
269
270 animation = new QPropertyAnimation(daFrame, "color");
271 animation->setDuration(500);
272 animation->setStartValue(highlightCommentEnd);
273 animation->setEndValue(highlightCommentStart);
274 animationGoup->addAnimation(animation);
275
276 animationGoup->start();
277}
278
279void PrCommentsList::addGlobalComment()
280{
281 mInputFrame->setVisible(true);
282 mInputTextEdit->setFocus();
283}
284
285void PrCommentsList::processComments(const Issue &issue)
286{
287 QMutexLocker lock(&mMutex);
288
289 disconnect(mGitServerCache.get(), &GitServerCache::issueUpdated, this, &PrCommentsList::processComments);
290
291 if (mIssueNumber != issue.number)
292 return;
293
294 delete mCommentsFrame;
295
296 mCommentsFrame = new QFrame();
297
298 mIssuesLayout->addWidget(mCommentsFrame);
299
300 const auto commentsLayout = new QVBoxLayout(mCommentsFrame);
301 commentsLayout->setContentsMargins(QMargins());
302 commentsLayout->setSpacing(30);
303
304 for (auto &comment : issue.comments)
305 {
306 const auto layout = createBubbleForComment(comment);
307 commentsLayout->addLayout(layout);
308 }
309
310 commentsLayout->addStretch();
311}
312
313QLabel *PrCommentsList::createHeadline(const QDateTime &dt, const QString &prefix)
314{
315 const auto days = dt.daysTo(QDateTime::currentDateTime());
316 const auto whenText = days <= 30 ? days != 0 ? tr(" %1 days ago").arg(days) : tr(" today")
317 : tr(" on %1").arg(dt.date().toString(QLocale().dateFormat(QLocale::ShortFormat)));
318
319 const auto label = prefix.isEmpty() ? new QLabel(whenText) : new QLabel(prefix + whenText);
320 label->setToolTip(dt.toString(QLocale().dateFormat(QLocale::ShortFormat)));
321
322 return label;
323}
324
325void PrCommentsList::onReviewsReceived()
326{
327 QMutexLocker lock(&mMutex);
328
329 auto pr = mGitServerCache->getPullRequest(mIssueNumber);
330
331 mFrameLinks.clear();
332 mComments.clear();
333
334 const auto originalPr = pr;
335
336 QMultiMap<QDateTime, QLayout *> bubblesMap;
337
338 for (const auto &comment : qAsConst(pr.comments))
339 {
340 const auto layout = createBubbleForComment(comment);
341 bubblesMap.insert(comment.creation, layout);
342 }
343
344 for (const auto &review : qAsConst(pr.reviews))
345 {
346 const auto layouts = new QVBoxLayout();
347
348 const auto reviewLayout = createBubbleForReview(review);
349
350 if (reviewLayout)
351 layouts->addLayout(reviewLayout);
352
353 auto codeReviewsLayouts = createBubbleForCodeReview(review.id, pr.reviewComment);
354
355 for (auto layout : codeReviewsLayouts)
356 layouts->addLayout(layout);
357
358 bubblesMap.insert(review.creation, layouts);
359 }
360
361 delete mCommentsFrame;
362 mCommentsFrame = new QFrame();
363
364 const auto reviewsLayout = new QVBoxLayout(mCommentsFrame);
365 reviewsLayout->setContentsMargins(QMargins());
366 reviewsLayout->setSpacing(30);
367
368 for (auto layout : bubblesMap)
369 {
370 if (layout)
371 reviewsLayout->addLayout(layout);
372 }
373
374 reviewsLayout->addStretch();
375 reviewsLayout->addItem(new QSpacerItem(1, 1, QSizePolicy::Fixed, QSizePolicy::Expanding));
376
377 emit frameReviewLink(originalPr, mFrameLinks);
378
379 mIssuesLayout->addWidget(mCommentsFrame);
380}
381
382QLayout *PrCommentsList::createBubbleForComment(const Comment &comment)
383{
384 const auto creationLayout = new QHBoxLayout();
385 creationLayout->setContentsMargins(QMargins());
386 creationLayout->setSpacing(0);
387 creationLayout->addWidget(new QLabel(tr("Comment by ")));
388
389 const auto creator = new QLabel(QString("<b>%1</b>").arg(comment.creator.name));
390 creator->setObjectName("CreatorLink");
391
392 creationLayout->addWidget(creator);
393 creationLayout->addWidget(createHeadline(comment.creation));
394 creationLayout->addStretch();
395 creationLayout->addWidget(new QLabel(comment.association));
396
397 GitQlientSettings settings("");
398 const auto colorSchema = settings.globalValue("colorSchema", "dark").toString();
399 const auto style = colorSchema == "dark" ? QString::fromUtf8("dark") : QString::fromUtf8("bright");
400
401 const auto doc = new Document(this);
402 m_commentContents.append(doc);
403
404// const auto channel = new QWebChannel(this);
405// channel->registerObject(QStringLiteral("content"), doc);
406
407// const auto page = new PreviewPage(this);
408// page->setWebChannel(channel);
409
410// QPointer<QWebEngineView> body = new QWebEngineView();
411
412// connect(page, &PreviewPage::contentsSizeChanged, this, [body](const QSizeF size) {
413// if (body)
414// body->setFixedHeight(size.height());
415// });
416
417// body->setPage(page);
418// body->setUrl(QUrl(QString("qrc:/resources/index_%1.html").arg(style)));
419// body->setFixedHeight(20);
420
421 doc->setText(comment.body.trimmed());
422
423 const auto frame = new QFrame();
424 frame->setObjectName("IssueIntro");
425
426 const auto innerLayout = new QVBoxLayout(frame);
427 innerLayout->setContentsMargins(10, 10, 10, 10);
428 innerLayout->setSpacing(5);
429 innerLayout->addLayout(creationLayout);
430 innerLayout->addSpacing(20);
431// innerLayout->addWidget(body);
432 innerLayout->addStretch();
433
434 const auto layout = new QHBoxLayout();
435 layout->setContentsMargins(QMargins());
436 layout->setSpacing(30);
437 layout->addSpacing(30);
438 layout->addWidget(createAvatar(comment.creator.name, comment.creator.avatar));
439 layout->addWidget(frame);
440
441 return layout;
442}
443
444QLayout *PrCommentsList::createBubbleForReview(const Review &review)
445{
446 const auto frame = new QFrame();
447 QString header;
448 QLabel *label = nullptr;
449
450 if (review.state == QString::fromUtf8("CHANGES_REQUESTED"))
451 {
452 frame->setObjectName("IssueIntroChangesRequested");
453
454 header = tr("<b>%1</b> (%2) requested changes to the PR ").arg(review.creator.name, review.association.toLower());
455 }
456 else if (review.state == QString::fromUtf8("APPROVED"))
457 {
458 frame->setObjectName("IssueIntroApproved");
459
460 header = tr("<b>%1</b> (%2) approved the PR ").arg(review.creator.name, review.association.toLower());
461 }
462 else if (review.state == QString::fromUtf8("COMMENTED"))
463 {
464 if (review.body.isEmpty())
465 {
466 delete frame;
467 return nullptr;
468 }
469
470 frame->setObjectName("IssueIntroCommented");
471
472 header = tr("<b>%1</b> (%2) reviewed the PR and added some comments ")
473 .arg(review.creator.name, review.association.toLower());
474
475 label = createHeadline(review.creation, header);
476 label->setText(label->text().append(" <p>%1</p>").arg(review.body));
477 }
478
479 if (!label)
480 label = createHeadline(review.creation, header);
481
482 const auto creationLayout = new QHBoxLayout();
483 creationLayout->setContentsMargins(QMargins());
484 creationLayout->setSpacing(0);
485 creationLayout->addWidget(label);
486 creationLayout->addStretch();
487
488 const auto innerLayout = new QVBoxLayout(frame);
489 innerLayout->setContentsMargins(10, 10, 10, 10);
490 innerLayout->setSpacing(20);
491 innerLayout->addLayout(creationLayout);
492
493 const auto layout = new QHBoxLayout();
494 layout->setContentsMargins(QMargins());
495 layout->setSpacing(30);
496 layout->addSpacing(30);
497 layout->addWidget(createAvatar(review.creator.name, review.creator.avatar));
498 layout->addWidget(frame);
499
500 return layout;
501}
502
503QVector<QLayout *> PrCommentsList::createBubbleForCodeReview(int reviewId, QVector<CodeReview> &comments)
504{
505 QMap<int, QVector<CodeReview>> reviews;
506 QVector<int> codeReviewIds;
507 QVector<QLayout *> listOfCodeReviews;
508
509 auto iter = comments.begin();
510
511 while (iter != comments.end())
512 {
513 if (iter->reviewId == reviewId)
514 {
515 codeReviewIds.append(iter->id);
516 reviews[iter->id].append(*iter);
517 comments.erase(iter);
518 }
519 else if (codeReviewIds.contains(iter->replyToId))
520 {
521 reviews[iter->replyToId].append(*iter);
522 comments.erase(iter);
523 }
524 else
525 ++iter;
526 }
527
528 if (!reviews.isEmpty())
529 {
530 for (auto &codeReviews : reviews)
531 {
532 std::sort(codeReviews.begin(), codeReviews.end(),
533 [](const CodeReview &r1, const CodeReview &r2) { return r1.creation < r2.creation; });
534
535 const auto review = codeReviews.constFirst();
536
537 const auto creationLayout = new QHBoxLayout();
538 creationLayout->setContentsMargins(QMargins());
539 creationLayout->setSpacing(0);
540
541 const auto header
542 = QString("<b>%1</b> (%2) started a review ").arg(review.creator.name, review.association.toLower());
543
544 creationLayout->addWidget(createHeadline(review.creation, header));
545 creationLayout->addStretch();
546
547 const auto codeReviewFrame = new QFrame();
548 const auto frame = new HighlightningFrame();
549
550 const auto innerLayout = new QVBoxLayout(frame);
551 innerLayout->setContentsMargins(10, 10, 10, 10);
552 innerLayout->setSpacing(20);
553 innerLayout->addLayout(creationLayout);
554
555 const auto codeReviewLayout = new QVBoxLayout(codeReviewFrame);
556
557 innerLayout->addWidget(codeReviewFrame);
558
559 const auto code = new SourceCodeReview(review.diff.file, review.diff.diff, review.diff.line);
560
561 codeReviewLayout->addWidget(code);
562 codeReviewLayout->addSpacing(20);
563
564 const auto commentsLayout = new QVBoxLayout();
565 commentsLayout->setAlignment(Qt::AlignLeft | Qt::AlignVCenter);
566 commentsLayout->setContentsMargins(QMargins());
567 commentsLayout->setSpacing(20);
568
569 for (auto &comment : codeReviews)
570 commentsLayout->addWidget(new CodeReviewComment(comment));
571
572 codeReviewLayout->addLayout(commentsLayout);
573
574 if (review.outdated)
575 {
576 const auto outdatedLabel = new ButtonLink(tr("Outdated"));
577 outdatedLabel->setObjectName("OutdatedLabel");
578 creationLayout->addWidget(outdatedLabel);
579
580 codeReviewFrame->setVisible(false);
581
582 connect(outdatedLabel, &ButtonLink::clicked, this,
583 [codeReviewFrame]() { codeReviewFrame->setVisible(!codeReviewFrame->isVisible()); });
584 }
585 else
586 {
587 const auto addComment = new QPushButton();
588 addComment->setCheckable(true);
589 addComment->setChecked(false);
590 addComment->setIcon(QIcon(":/icons/add_comment"));
591 addComment->setToolTip(tr("Add new comment"));
592
593 creationLayout->addWidget(addComment);
594
595 const auto inputTextEdit = new QTextEdit();
596 inputTextEdit->setPlaceholderText(tr("Add your comment..."));
597 inputTextEdit->setObjectName("AddReviewInput");
598
599 const auto cancel = new QPushButton(tr("Cancel"));
600 const auto add = new QPushButton(tr("Comment"));
601 const auto btnsLayout = new QHBoxLayout();
602 btnsLayout->setContentsMargins(QMargins());
603 btnsLayout->setSpacing(0);
604 btnsLayout->addWidget(cancel);
605 btnsLayout->addStretch();
606 btnsLayout->addWidget(add);
607
608 const auto inputFrame = new QFrame();
609 inputFrame->setVisible(false);
610 const auto inputLayout = new QVBoxLayout(inputFrame);
611 inputLayout->setContentsMargins(QMargins());
612 inputLayout->addSpacing(20);
613 inputLayout->setSpacing(10);
614 inputLayout->addWidget(inputTextEdit);
615 inputLayout->addLayout(btnsLayout);
616
617 codeReviewLayout->addWidget(inputFrame);
618
619 connect(add, &QPushButton::clicked, this, [this, inputTextEdit, commentId = review.id]() {
620#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
621 addReplyToCodeReview(commentId, inputTextEdit->toMarkdown().trimmed());
622#else
623 addReplyToCodeReview(commentId, inputTextEdit->toPlainText().trimmed());
624#endif
625 });
626 connect(cancel, &QPushButton::clicked, this, [inputTextEdit, addComment]() {
627 inputTextEdit->clear();
628 addComment->toggle();
629 });
630 connect(addComment, &QPushButton::toggled, this, [inputFrame, inputTextEdit](bool checked) {
631 inputFrame->setVisible(checked);
632 inputTextEdit->setFocus();
633 });
634 }
635
636 mFrameLinks.insert(mCommentId, review.id);
637 mComments.insert(mCommentId, frame);
638
639 ++mCommentId;
640
641 const auto layout = new QHBoxLayout();
642 layout->setContentsMargins(QMargins());
643 layout->setSpacing(30);
644 layout->addSpacing(30);
645 layout->addWidget(createAvatar(review.creator.name, review.creator.avatar));
646 layout->addWidget(frame);
647
648 listOfCodeReviews.append(layout);
649 }
650 }
651
652 return listOfCodeReviews;
653}
654
655void PrCommentsList::addReplyToCodeReview(int commentId, const QString &message)
656{
657 mGitServerCache->getApi()->replyCodeReview(mIssueNumber, commentId, message);
658}
659
660HighlightningFrame::HighlightningFrame(QWidget *parent)
661 : QFrame(parent)
662{
663 setObjectName("IssueIntro");
664}
665
666void HighlightningFrame::setColor(QColor color)
667{
668 setStyleSheet(QString("#IssueIntro { background-color: rgb(%1, %2, %3); }")
669 .arg(color.red())
670 .arg(color.green())
671 .arg(color.blue()));
672}
673
674QColor HighlightningFrame::color()
675{
676 return Qt::black; // getter is not really needed for now
677}
678