1 /**
2  * UGENE - Integrated Bioinformatics Tools.
3  * Copyright (C) 2008-2021 UniPro <ugene@unipro.ru>
4  * http://ugene.net
5  *
6  * This program is free software; you can redistribute it and/or
7  * modify it under the terms of the GNU General Public License
8  * as published by the Free Software Foundation; either version 2
9  * of the License, or (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with this program; if not, write to the Free Software
18  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
19  * MA 02110-1301, USA.
20  */
21 
22 #include "ExternalToolsDashboardWidget.h"
23 
24 #include <QApplication>
25 #include <QClipboard>
26 #include <QHBoxLayout>
27 #include <QPainter>
28 
29 #include <U2Core/U2SafePoints.h>
30 
31 #include <U2Gui/HoverQLabel.h>
32 
33 #include "DomUtils.h"
34 
35 namespace U2 {
36 
37 #define NODE_KIND_ACTOR 1
38 #define NODE_KIND_TOOL 2
39 #define NODE_KIND_RUN 3
40 #define NODE_KIND_COMMAND 4
41 #define NODE_KIND_OUTPUT 5
42 #define NODE_KIND_LOG_CONTENT 6
43 
44 #define NODE_CLASS_ACTOR QString("actor-node")
45 #define NODE_CLASS_TOOL QString("actor-tick-node")
46 #define NODE_CLASS_RUN QString("tool-run-node")
47 #define NODE_CLASS_COMMAND QString("command")
48 #define NODE_CLASS_CONTENT QString("content")
49 #define NODE_CLASS_IMPORTANT QString("badge-important")
50 #define NODE_CLASS_BADGE QString("badge")
51 
52 const QString ExternalToolsDashboardWidget::TREE_ID("treeRoot");
53 
fixOldStyleOpenFileJs(const QString & html)54 static QString fixOldStyleOpenFileJs(const QString &html) {
55     return QString(html).replace("onclick=\"openLog('", "href=\"file://").replace("/logs')", "/logs").replace("_log.txt')", "_log.txt");
56 }
57 
ExternalToolsDashboardWidget(const QDomElement & dom,const WorkflowMonitor * monitor)58 ExternalToolsDashboardWidget::ExternalToolsDashboardWidget(const QDomElement &dom, const WorkflowMonitor *monitor)
59     : monitor(monitor) {
60     setMinimumWidth(1100);  // TODO: make it expanding.
61 
62     // A frame with rounded borders around the content.
63     auto frameLayout = new QVBoxLayout();
64     frameLayout->setContentsMargins(12, 12, 12, 12);
65     setLayout(frameLayout);
66     auto frameWidget = new QWidget();
67     frameWidget->setObjectName("frameWidget");
68     frameWidget->setStyleSheet("#frameWidget {border: 1px solid #ddd; padding: 8px; border-radius: 6px;}");
69     frameLayout->addWidget(frameWidget);
70 
71     // Vertical layout with all nodes.
72     layout = new QVBoxLayout();
73     layout->setMargin(12);
74     layout->setSpacing(0);
75     frameWidget->setLayout(layout);
76 
77     QList<QDomElement> actorElementList = DomUtils::findChildElementsByClass(DomUtils::findElementById(dom, TREE_ID), NODE_CLASS_ACTOR, 2);
78     for (auto actorSpan : qAsConst(actorElementList)) {
79         auto actorNode = new ExternalToolsTreeNode(NODE_KIND_ACTOR, actorSpan.attribute("id"), actorSpan.text(), nullptr);
80         layout->addWidget(actorNode);
81         topLevelNodes << actorNode;
82 
83         QList<QDomElement> toolElementList = DomUtils::findChildElementsByClass(actorSpan.nextSiblingElement("ul"), NODE_CLASS_TOOL, 2);
84         for (auto toolSpan : qAsConst(toolElementList)) {
85             auto toolNode = new ExternalToolsTreeNode(NODE_KIND_TOOL, toolSpan.attribute("id"), toolSpan.text(), actorNode);
86             layout->addWidget(toolNode);
87             addLimitationWarningIfNeeded(toolNode, DomUtils::findParentByTag(toolSpan, "ul"));
88 
89             QList<QDomElement> runElementList = DomUtils::findChildElementsByClass(toolSpan.nextSiblingElement("ul"), NODE_CLASS_RUN, 2);
90             for (auto runSpan : qAsConst(runElementList)) {
91                 auto runNode = new ExternalToolsTreeNode(NODE_KIND_RUN, runSpan.attribute("id"), runSpan.text(), toolNode, DomUtils::hasClass(runSpan, NODE_CLASS_IMPORTANT));
92                 layout->addWidget(runNode);
93                 addLimitationWarningIfNeeded(runNode, DomUtils::findParentByTag(runSpan, "ul"));
94 
95                 QDomElement commandSpan = runSpan.nextSiblingElement("ul").firstChildElement("li").firstChildElement("span");
96                 if (!commandSpan.isNull()) {
97                     auto commandNode = new ExternalToolsTreeNode(NODE_KIND_COMMAND, commandSpan.attribute("id"), commandSpan.text(), runNode);
98                     layout->addWidget(commandNode);
99 
100                     QDomElement commandContentSpan = commandSpan.nextSiblingElement("ul").firstChildElement("li").firstChildElement("span");
101                     if (!commandContentSpan.isNull()) {
102                         auto commandContentNode = new ExternalToolsTreeNode(NODE_KIND_LOG_CONTENT, commandContentSpan.attribute("id"), commandContentSpan.text(), commandNode);
103                         layout->addWidget(commandContentNode);
104                     }
105 
106                     QDomNode outputLi = commandSpan.parentNode();  // previous node for the real outputLi.
107                     while (true) {
108                         outputLi = outputLi.nextSiblingElement("li");
109                         QDomElement outputSpan = outputLi.firstChildElement("span");
110                         if (outputLi.isNull() || outputSpan.isNull()) {
111                             break;
112                         }
113                         auto outputNode = new ExternalToolsTreeNode(NODE_KIND_OUTPUT, outputSpan.attribute("id"), outputSpan.text(), runNode, DomUtils::hasClass(outputSpan, NODE_CLASS_IMPORTANT));
114                         layout->addWidget(outputNode);
115 
116                         QDomElement outputContentSpan = outputSpan.nextSiblingElement("ul").firstChildElement("li").firstChildElement("span");
117                         if (!outputContentSpan.isNull()) {
118                             QString outputHtml = fixOldStyleOpenFileJs(DomUtils::toString(outputContentSpan, false));
119                             layout->addWidget(new ExternalToolsTreeNode(NODE_KIND_LOG_CONTENT, outputContentSpan.attribute("id"), outputHtml, outputNode));
120                         }
121                     }
122                 }
123             }
124         }
125     }
126     if (!actorElementList.isEmpty()) {
127         addLimitationWarningIfNeeded(nullptr, DomUtils::findParentByTag(actorElementList.first(), "ul"));
128     }
129 }
130 
isValidDom(const QDomElement & dom)131 bool ExternalToolsDashboardWidget::isValidDom(const QDomElement &dom) {
132     return !DomUtils::findElementById(dom, TREE_ID).isNull();
133 }
134 
addLimitationWarningIfNeeded(ExternalToolsTreeNode * parentNode,const QDomElement & listHeadElement)135 void ExternalToolsDashboardWidget::addLimitationWarningIfNeeded(ExternalToolsTreeNode *parentNode, const QDomElement &listHeadElement) {
136     QDomElement span = listHeadElement.lastChildElement("li").firstChildElement("span");
137     if (!DomUtils::hasClass(span, "limitation-message")) {
138         return;
139     }
140     QString text = fixOldStyleOpenFileJs(DomUtils::toString(span, false));
141     addLimitationWarning(parentNode, text);
142 }
143 
addLimitationWarning(ExternalToolsTreeNode * parentNode,const QString & limitationMessage)144 void ExternalToolsDashboardWidget::addLimitationWarning(ExternalToolsTreeNode *parentNode, const QString &limitationMessage) {
145     QString message = limitationMessage;
146     if (message.isEmpty()) {
147         SAFE_POINT(monitor != nullptr, "WorkflowMonitor is null!", );
148         message = "Messages limit on the dashboard exceeded. See <a href=\"" + monitor->getLogsDir() + "\">log files</a>.";
149     }
150     auto limitationLabel = new QLabel("<code>" + message + "</code>");
151     limitationLabel->setStyleSheet("font-size: 16px; background-color: #F0F0F0; color: black; padding: 5px;");
152     limitationLabel->setOpenExternalLinks(true);
153     if (parentNode == nullptr) {
154         if (!limitationWarningHtml.isEmpty()) {
155             return;
156         }
157         layout->addSpacing(20);
158         layout->addWidget(limitationLabel);
159         limitationWarningHtml = message;
160     } else {
161         if (!parentNode->limitationWarningHtml.isEmpty()) {
162             return;
163         }
164         parentNode->limitationWarningHtml = message;
165         int lastChildIndex = parentNode->children.isEmpty() ? 0 : layout->indexOf(parentNode->children.last());
166         layout->insertSpacing(lastChildIndex, 20);
167         layout->insertWidget(lastChildIndex, limitationLabel);
168     }
169 }
170 
toHtml() const171 QString ExternalToolsDashboardWidget::toHtml() const {
172     CHECK(!topLevelNodes.isEmpty(), "");
173     QString html = "<ul id=\"" + TREE_ID + "\">";
174     for (auto node : qAsConst(topLevelNodes)) {
175         html += node->toHtml();
176     }
177     if (!limitationWarningHtml.isEmpty()) {
178         html += "<li><span class=\"badge limitation-message\">" + limitationWarningHtml + "</span></li>";
179     }
180     html += "</ul>";
181     return html;
182 }
183 
findNode(const QList<ExternalToolsTreeNode * > & nodeList,const QString & objectName)184 static ExternalToolsTreeNode *findNode(const QList<ExternalToolsTreeNode *> &nodeList, const QString &objectName) {
185     for (const auto &node : qAsConst(nodeList)) {
186         if (node->objectName() == objectName) {
187             return node;
188         }
189     }
190     return nullptr;
191 }
192 
193 #define MAX_SAME_LEVEL_NODES 100
194 #define MAX_OUTPUT_CONTENT_SIZE 100000
195 
addLogEntry(const Monitor::LogEntry & entry)196 void ExternalToolsDashboardWidget::addLogEntry(const Monitor::LogEntry &entry) {
197     SAFE_POINT(monitor != nullptr, "WorkflowMonitor instance is null!", );
198     QString newLine = QString(entry.lastLine)
199                           .replace("<", "&lt;")
200                           .replace(">", "&gt;")
201                           .replace("\n", "<br/>")
202                           .replace("\r", "");
203 
204     QString actorNodeId = "actor_" + entry.actorId;
205     ExternalToolsTreeNode *actorNode = findNode(topLevelNodes, actorNodeId);
206     if (actorNode == nullptr) {
207         if (topLevelNodes.size() >= MAX_SAME_LEVEL_NODES) {
208             addLimitationWarning();
209             return;
210         }
211         actorNode = addNodeToLayout(new ExternalToolsTreeNode(NODE_KIND_ACTOR, actorNodeId, entry.actorName, nullptr));
212         topLevelNodes << actorNode;
213     }
214 
215     QString toolNodeId = actorNodeId + "_run_" + QString::number(entry.actorRunNumber);
216     ExternalToolsTreeNode *toolNode = findNode(actorNode->children, toolNodeId);
217     if (toolNode == nullptr) {
218         if (actorNode->children.size() > MAX_SAME_LEVEL_NODES) {
219             addLimitationWarning(actorNode);
220             return;
221         }
222         QString toolNodeText = entry.actorName + " run " + QString::number(entry.actorRunNumber);
223         toolNode = addNodeToLayout(new ExternalToolsTreeNode(NODE_KIND_TOOL, toolNodeId, toolNodeText, actorNode));
224     }
225 
226     bool isImportant = entry.contentType == 0;
227     QString runNodeId = toolNodeId + "_tool_" + entry.toolName + "_run_" + QString::number(entry.toolRunNumber);
228     ExternalToolsTreeNode *runNode = findNode(toolNode->children, runNodeId);
229     if (runNode == nullptr) {
230         if (toolNode->children.size() > MAX_SAME_LEVEL_NODES) {
231             addLimitationWarning(toolNode);
232             return;
233         }
234         QString runNodeText = entry.toolName + " run" + (entry.toolRunNumber > 1 ? " " + QString::number(entry.toolRunNumber) : "");
235         runNode = addNodeToLayout(new ExternalToolsTreeNode(NODE_KIND_RUN, runNodeId, runNodeText, toolNode, isImportant));
236     } else if (!runNode->isImportant && isImportant) {
237         runNode->isImportant = true;
238         runNode->badgeLabel->switchToImportantStyle();
239     }
240 
241     QString outputNodeId = toolNodeId + (entry.contentType == 0 ? "_stderr" : (entry.contentType == 1 ? "_stdout" : "_command"));
242     int outputNodeKind = entry.contentType == 2 ? NODE_KIND_COMMAND : NODE_KIND_OUTPUT;
243     ExternalToolsTreeNode *outputNode = findNode(runNode->children, outputNodeId);
244     QString outputNodeText = entry.contentType == 0 ? "Output log (stderr)" : (entry.contentType == 1 ? "Output log (stdout)" : "Command");
245     if (outputNode == nullptr) {
246         outputNode = addNodeToLayout(new ExternalToolsTreeNode(outputNodeKind, outputNodeId, outputNodeText, runNode, isImportant));
247     }
248 
249     QString outputContentNodeId = outputNodeId + "_content";
250     ExternalToolsTreeNode *outputContentNode = findNode(outputNode->children, outputContentNodeId);
251     if (outputContentNode == nullptr) {
252         outputContentNode = addNodeToLayout(new ExternalToolsTreeNode(NODE_KIND_LOG_CONTENT, outputContentNodeId, "", outputNode));
253     }
254 
255     if (!outputContentNode->isLogFull) {
256         QString logLine = newLine;
257         if (outputContentNode->content.length() + newLine.length() > MAX_OUTPUT_CONTENT_SIZE) {
258             outputContentNode->isLogFull = true;
259             QString logUrl = monitor->getLogUrl(entry.actorId, entry.actorRunNumber, entry.toolName, entry.toolRunNumber, entry.contentType);
260             logLine = "<br/><br/>The external tool output is too large and can't be visualized on the dashboard. Find full output in <a href=\"" + logUrl + "\">log file</a>.";
261         }
262         outputContentNode->content.append(logLine);
263         outputContentNode->badgeLabel->logView->setHtml("<code>" + outputContentNode->content + "</code>");
264     }
265 }
266 
addNodeToLayout(ExternalToolsTreeNode * node)267 ExternalToolsTreeNode *ExternalToolsDashboardWidget::addNodeToLayout(ExternalToolsTreeNode *node) {
268     if (node->parent == nullptr) {
269         layout->addWidget(node);
270     } else {
271         auto prevNode = node->parent->getLastChildInHierarchyOrSelf();
272         int prevIndex = layout->indexOf(prevNode);
273         layout->insertWidget(prevIndex, node);
274     }
275     return node;
276 }
277 
278 #define TREE_NODE_X_OFFSET 50
279 
getLevelByNodeKind(int kind)280 static int getLevelByNodeKind(int kind) {
281     switch (kind) {
282         case NODE_KIND_ACTOR:
283             return 0;
284         case NODE_KIND_TOOL:
285             return 1;
286         case NODE_KIND_RUN:
287             return 2;
288         case NODE_KIND_COMMAND:
289         case NODE_KIND_OUTPUT:
290             return 3;
291         case NODE_KIND_LOG_CONTENT:
292             return 4;
293     }
294     SAFE_POINT(false, "Unknown kind: " + QString::number(kind), 0);
295 }
296 
ExternalToolsTreeNode(int kind,const QString & objectName,const QString & content,ExternalToolsTreeNode * parent,bool isImportant)297 ExternalToolsTreeNode::ExternalToolsTreeNode(int kind, const QString &objectName, const QString &content, ExternalToolsTreeNode *parent, bool isImportant)
298     : kind(kind), parent(parent), content(content), isImportant(isImportant), isLogFull(false), badgeLabel(nullptr) {
299     Q_ASSERT(!objectName.isEmpty());
300     setObjectName(objectName);
301     if (parent != nullptr) {
302         parent->children << this;
303     }
304     setContentsMargins(0, 5, 0, 5);
305     auto layout = new QHBoxLayout();
306     layout->setMargin(0);
307     layout->setSpacing(0);
308     setLayout(layout);
309 
310     int level = getLevelByNodeKind(kind);
311     layout->addSpacing(level * TREE_NODE_X_OFFSET);
312     badgeLabel = new BadgeLabel(kind, content, isImportant);
313     layout->addWidget(badgeLabel);
314 
315     if (badgeLabel->titleLabel) {
316         connect(badgeLabel->titleLabel, SIGNAL(clicked()), SLOT(sl_toggle()));
317     }
318 
319     if (badgeLabel->copyButton && kind == NODE_KIND_RUN) {
320         connect(badgeLabel->copyButton, SIGNAL(clicked()), SLOT(sl_copyRunCommand()));
321     }
322 
323     setVisible(level <= getLevelByNodeKind(NODE_KIND_RUN));
324 }
325 
isExpanded() const326 bool ExternalToolsTreeNode::isExpanded() const {
327     return children.size() > 0 && children.first()->isVisible();
328 }
329 
getLastChildInHierarchyOrSelf()330 ExternalToolsTreeNode *ExternalToolsTreeNode::getLastChildInHierarchyOrSelf() {
331     return children.isEmpty() ? this : children.last()->getLastChildInHierarchyOrSelf();
332 }
333 
sl_toggle()334 void ExternalToolsTreeNode::sl_toggle() {
335     bool isExpandedBefore = isExpanded();
336     bool isExpandedAfter = !isExpandedBefore;
337 
338     // Auto-expand command & output nodes when RUN node is clicked.
339     bool expandAllChildren = isExpandedAfter && getLevelByNodeKind(kind) >= getLevelByNodeKind(NODE_KIND_RUN);
340 
341     for (auto child : qAsConst(children)) {
342         child->updateExpandCollapseState(isExpandedAfter, expandAllChildren);
343     }
344 }
345 
updateExpandCollapseState(bool isParentExpanded,bool isApplyToAllLevelOfChildren)346 void ExternalToolsTreeNode::updateExpandCollapseState(bool isParentExpanded, bool isApplyToAllLevelOfChildren) {
347     this->setVisible(isParentExpanded);
348     if (!isParentExpanded) {  // make all children invisible (we use flat VBOX layout model for the tree, so parent must hide children manually).
349         for (auto child : qAsConst(children)) {
350             child->updateExpandCollapseState(false);
351         }
352     } else if (isApplyToAllLevelOfChildren) {  // make children on all levels visible.
353         for (auto child : qAsConst(children)) {
354             child->updateExpandCollapseState(true, true);
355         }
356     }
357 }
358 
sl_copyRunCommand()359 void ExternalToolsTreeNode::sl_copyRunCommand() {
360     if (kind == NODE_KIND_RUN && !children.isEmpty() && !children[0]->children.isEmpty()) {
361         QApplication::clipboard()->setText(children[0]->children[0]->content);
362     }
363 }
364 
isLastChild(const ExternalToolsTreeNode * node)365 static bool isLastChild(const ExternalToolsTreeNode *node) {
366     return node != nullptr && node->parent != nullptr && node->parent->children.last() == node;
367 }
368 
369 #define BRANCH_X_PADDING 15
370 
paintEvent(QPaintEvent * event)371 void ExternalToolsTreeNode::paintEvent(QPaintEvent *event) {
372     QWidget::paintEvent(event);
373     if (width() == 0 || height() == 0) {
374         return;
375     }
376 
377     QPainter painter(this);
378     painter.setPen(QPen(QBrush(QColor("#999999")), 1));
379 
380     for (const ExternalToolsTreeNode *node = this; node != nullptr; node = node->parent) {
381         int level = getLevelByNodeKind(node->kind);
382         int x = (level - 1) * TREE_NODE_X_OFFSET + BRANCH_X_PADDING;
383         if (node == this) {
384             int horizontalLineY = height() / 2;
385             if (node->kind != NODE_KIND_ACTOR) {
386                 painter.drawLine(x, 0, x, isLastChild(node) ? horizontalLineY : height());  // vertical line from from the parent to the Y-center.
387                 painter.drawLine(x, horizontalLineY, x + TREE_NODE_X_OFFSET - 5, horizontalLineY);  // horizontal line to the node.
388             }
389             if (!children.isEmpty() && isExpanded()) {  // part of the link to the first child.
390                 int childX = level * TREE_NODE_X_OFFSET + BRANCH_X_PADDING;
391                 painter.drawLine(childX, horizontalLineY, childX, height());
392             }
393         } else if (!isLastChild(node)) {
394             painter.drawLine(x, 0, x, height());
395         }
396     }
397 }
398 
getSpanClass() const399 QString ExternalToolsTreeNode::getSpanClass() const {
400     QString result = NODE_CLASS_BADGE + (isImportant ? " " + NODE_CLASS_IMPORTANT : "");
401     switch (kind) {
402         case NODE_KIND_ACTOR:
403             return result + " " + NODE_CLASS_ACTOR;
404         case NODE_KIND_TOOL:
405             return result + " " + NODE_CLASS_TOOL;
406         case NODE_KIND_RUN:
407             return result + " " + NODE_CLASS_RUN;
408         case NODE_KIND_LOG_CONTENT:
409             return result + " " + NODE_CLASS_CONTENT;
410     }
411     return result;
412 }
413 
toHtml() const414 QString ExternalToolsTreeNode::toHtml() const {
415     QString html = "<li>\n";
416     html += "<span id=\"" + objectName() + "\" class=\"" + getSpanClass() + "\">" + content + "</span>\n";
417     if (!children.isEmpty()) {
418         html += "<ul>\n";
419         for (auto child : qAsConst(children)) {
420             html += child->toHtml();
421         }
422         html += "</ul>\n";
423     }
424     if (!limitationWarningHtml.isEmpty()) {
425         html += "<li><span class=\"badge limitation-message\">" + limitationWarningHtml + "</span></li>\n";
426     }
427     html += "</li>\n";
428     return html;
429 }
430 
431 #define RUN_NODE_NORMAL_COLOR "#50A976"
432 #define RUN_NODE_IMPORTANT_COLOR "#CC6666"
433 
getBadgeLabelStyle(int kind,bool isImportant)434 static QString getBadgeLabelStyle(int kind, bool isImportant) {
435     QString style = "border-radius: 6px; padding: 2px 4px; color: white;";
436     switch (kind) {
437         case NODE_KIND_ACTOR:
438             return style + "background-color: #92939E;";
439         case NODE_KIND_TOOL:
440             return style + "background-color: #bdb0a0;";
441         case NODE_KIND_RUN:
442             return style + QString("background-color: ") + (isImportant ? RUN_NODE_IMPORTANT_COLOR : RUN_NODE_NORMAL_COLOR) + ";";
443         case NODE_KIND_COMMAND:
444             return style + "background-color: #79ACAC;";
445         case NODE_KIND_OUTPUT:
446             return style + QString("background-color: ") + (isImportant ? RUN_NODE_IMPORTANT_COLOR : "#6699CC") + ";";
447         case NODE_KIND_LOG_CONTENT:
448             return style + "font-size: 16px; background-color: #F0F0F0; color: black;";
449     }
450     return style;
451 };
452 
BadgeLabel(int kind,const QString & text,bool isImportant)453 BadgeLabel::BadgeLabel(int kind, const QString &text, bool isImportant)
454     : kind(kind), titleLabel(nullptr), copyButton(nullptr), logView(nullptr) {
455     auto layout = new QHBoxLayout();
456     layout->setMargin(0);
457     layout->setSpacing(0);
458     setLayout(layout);
459 
460     QString style = getBadgeLabelStyle(kind, isImportant);
461     bool isCopyButtonVisible = kind == NODE_KIND_RUN;
462     QString finalStyle = isCopyButtonVisible ? style + ";border-top-right-radius: 0; border-bottom-right-radius: 0;" : style;
463     if (kind == NODE_KIND_LOG_CONTENT) {
464         logView = new QTextBrowser();
465         logView->setStyleSheet("QTextBrowser {" + finalStyle + "}");
466         logView->setTextInteractionFlags(Qt::TextBrowserInteraction);
467         logView->setContextMenuPolicy(Qt::DefaultContextMenu);
468         logView->setOpenExternalLinks(true);
469         logView->setMinimumHeight(qBound(56, 30 * qMax(text.count("\n"), text.size() / 84), 400));
470         logView->setMaximumHeight(800);
471         logView->setHtml("<code>" + text + "</code>");
472         layout->addWidget(logView);
473     } else {
474         titleLabel = new HoverQLabel(text, "QLabel {" + finalStyle + "}", "QLabel {" + finalStyle + "; color: black;}");
475         layout->addWidget(titleLabel);
476     }
477 
478     if (isCopyButtonVisible) {
479         QString copyButtonStyle = style + ";border-top-left-radius: 0; border-bottom-left-radius: 0; border-left: 1px solid #eee;";
480         copyButton = new HoverQLabel("", "QLabel {" + copyButtonStyle + "}", "QLabel {" + copyButtonStyle + "; color: black; background: #777;}");
481         copyButton->setPixmap(QPixmap(":U2Designer/images/copy.png"));
482         copyButton->setObjectName("copyButton");
483         copyButton->setToolTip(tr("Copy command line"));
484         layout->addWidget(copyButton);
485     }
486     if (kind != NODE_KIND_LOG_CONTENT) {
487         layout->addStretch(1);
488     }
489 }
490 
switchToImportantStyle()491 void BadgeLabel::switchToImportantStyle() {
492     CHECK(kind == NODE_KIND_RUN, );
493     titleLabel->normalStyle = titleLabel->normalStyle.replace(RUN_NODE_NORMAL_COLOR, RUN_NODE_IMPORTANT_COLOR);
494     titleLabel->hoveredStyle = titleLabel->hoveredStyle.replace(RUN_NODE_NORMAL_COLOR, RUN_NODE_IMPORTANT_COLOR);
495     titleLabel->setStyleSheet(titleLabel->normalStyle);
496     copyButton->setStyleSheet(copyButton->styleSheet().replace(RUN_NODE_NORMAL_COLOR, RUN_NODE_IMPORTANT_COLOR));
497 }
498 
499 }  // namespace U2
500