File indexing completed on 2024-04-28 05:49:32
0001 /* 0002 SPDX-FileCopyrightText: 2021 Christoph Cullmann <cullmann@kde.org> 0003 0004 SPDX-License-Identifier: LGPL-2.0-or-later 0005 */ 0006 0007 #include "kateoutputview.h" 0008 #include "kateapp.h" 0009 #include "katemainwindow.h" 0010 0011 #include <KColorScheme> 0012 #include <KConfigGroup> 0013 #include <KLocalizedString> 0014 #include <KSharedConfig> 0015 #include <KTextEditor/Editor> 0016 0017 #include <QApplication> 0018 #include <QClipboard> 0019 #include <QDateTime> 0020 #include <QMenu> 0021 #include <QPainter> 0022 #include <QScrollBar> 0023 #include <QTextBlock> 0024 #include <QTimeLine> 0025 #include <QToolButton> 0026 #include <QVBoxLayout> 0027 #include <QWindow> 0028 0029 #include <ktexteditor_utils.h> 0030 0031 #define GENERIC_ICON_NAME QLatin1String("dialog-scripts") 0032 0033 class BlockData : public QTextBlockUserData 0034 { 0035 public: 0036 explicit BlockData(const QString &t) 0037 : token(t) 0038 { 0039 } 0040 const QString token; 0041 }; 0042 0043 class NewMsgIndicator : public QWidget 0044 { 0045 Q_OBJECT 0046 public: 0047 explicit NewMsgIndicator(QWidget *parent) 0048 : QWidget(parent) 0049 , m_timeline(1000, this) 0050 { 0051 setAttribute(Qt::WA_TransparentForMouseEvents, true); 0052 setGeometry(parent->geometry().adjusted(-2, -2, 2, 2)); 0053 0054 m_timeline.setDirection(QTimeLine::Forward); 0055 m_timeline.setEasingCurve(QEasingCurve::SineCurve); 0056 m_timeline.setFrameRange(20, 150); 0057 auto update = QOverload<>::of(&QWidget::update); 0058 connect(&m_timeline, &QTimeLine::valueChanged, this, update); 0059 connect(&m_timeline, &QTimeLine::finished, this, &QObject::deleteLater); 0060 connect(window()->windowHandle(), &QWindow::activeChanged, this, [this]() { 0061 if (window()->windowHandle()->isActive() && m_timeline.state() == QTimeLine::Paused) { 0062 m_timeline.setPaused(false); 0063 } else if (!window()->windowHandle()->isActive()) { 0064 m_timeline.setPaused(true); 0065 } 0066 }); 0067 } 0068 0069 void run(int loopCount, QColor c) 0070 { 0071 // If parent is not visible, do nothing 0072 if (auto p = qobject_cast<QWidget *>(parent()); p && !p->isVisible()) { 0073 return; 0074 } 0075 m_flashColor = c; 0076 show(); 0077 raise(); 0078 m_timeline.setLoopCount(loopCount); 0079 m_timeline.start(); 0080 if (!isVisible() || !window()->windowHandle()->isActive()) { 0081 m_timeline.setPaused(true); 0082 } 0083 } 0084 0085 Q_SLOT void stop() 0086 { 0087 m_timeline.stop(); 0088 deleteLater(); 0089 } 0090 0091 protected: 0092 void paintEvent(QPaintEvent *) override 0093 { 0094 if (m_timeline.state() == QTimeLine::Running) { 0095 QPainter p(this); 0096 p.setRenderHint(QPainter::Antialiasing); 0097 m_flashColor.setAlpha(m_timeline.currentFrame()); 0098 p.setBrush(m_flashColor); 0099 p.setPen(Qt::NoPen); 0100 p.drawRoundedRect(rect(), 15, 15); 0101 } 0102 } 0103 0104 void hideEvent(QHideEvent *) override 0105 { 0106 m_timeline.setPaused(true); 0107 } 0108 0109 void showEvent(QShowEvent *) override 0110 { 0111 if (m_timeline.state() == QTimeLine::Paused) { 0112 m_timeline.setPaused(false); 0113 } 0114 } 0115 0116 private: 0117 QColor m_flashColor; 0118 QTimeLine m_timeline; 0119 }; 0120 0121 class KateOutputEdit : public QTextBrowser 0122 { 0123 Q_OBJECT 0124 public: 0125 explicit KateOutputEdit(QWidget *parent) 0126 : QTextBrowser(parent) 0127 { 0128 setOpenExternalLinks(true); 0129 } 0130 0131 QVariant loadResource(int type, const QUrl &name) override 0132 { 0133 if (type == QTextDocument::ImageResource) { 0134 const QPixmap icon = m_iconCache[name.toString()]; 0135 if (!icon.isNull()) { 0136 return icon; 0137 } 0138 } 0139 return QTextBrowser::loadResource(type, name); 0140 } 0141 0142 void addIcon(const QString &cat, const QIcon &icon) 0143 { 0144 const auto it = m_iconCache.constFind(cat); 0145 if (it == m_iconCache.constEnd()) { 0146 if (cat == GENERIC_ICON_NAME) { 0147 m_iconCache[GENERIC_ICON_NAME] = QIcon::fromTheme(GENERIC_ICON_NAME).pixmap(16, 16); 0148 } else { 0149 m_iconCache[cat] = icon.pixmap(16, 16); 0150 } 0151 } 0152 } 0153 0154 private: 0155 QHash<QString, QPixmap> m_iconCache; 0156 }; 0157 0158 KateOutputView::KateOutputView(KateMainWindow *mainWindow, QWidget *parent) 0159 : QWidget(parent) 0160 , m_mainWindow(mainWindow) 0161 , m_textEdit(new KateOutputEdit(this)) 0162 { 0163 setFocusPolicy(Qt::NoFocus); 0164 0165 QVBoxLayout *layout = new QVBoxLayout(this); 0166 layout->setContentsMargins(0, 0, 0, 0); 0167 layout->setSpacing(0); 0168 0169 m_searchTimer.setInterval(400); 0170 m_searchTimer.setSingleShot(true); 0171 m_searchTimer.callOnTimeout(this, &KateOutputView::search); 0172 0173 // filter line edit 0174 m_filterLine.setPlaceholderText(i18n("Search...")); 0175 m_filterLine.setClearButtonEnabled(true); 0176 m_filterLine.setProperty("_breeze_borders_sides", QVariant::fromValue(QFlags{Qt::RightEdge})); 0177 connect(&m_filterLine, &QLineEdit::textChanged, this, [this]() { 0178 m_searchTimer.start(); 0179 }); 0180 0181 // copy button 0182 auto copy = new QToolButton(this); 0183 connect(copy, &QToolButton::clicked, this, [this] { 0184 const QString text = m_textEdit->toPlainText(); 0185 if (!text.isEmpty()) { 0186 qApp->clipboard()->setText(text); 0187 } 0188 }); 0189 copy->setIcon(QIcon::fromTheme(QStringLiteral("edit-copy"))); 0190 copy->setToolButtonStyle(Qt::ToolButtonIconOnly); 0191 copy->setToolTip(i18n("Copy all text to clipboard")); 0192 0193 // clear button 0194 auto clear = new QToolButton(this); 0195 clear->setIcon(QIcon::fromTheme(QStringLiteral("edit-clear-history"))); 0196 clear->setToolTip(i18n("Clear all messages")); 0197 connect(clear, &QPushButton::clicked, this, [this] { 0198 m_textEdit->clear(); 0199 }); 0200 0201 // setup top horizontal layout 0202 // tried toolbar, has bad spacing 0203 QHBoxLayout *hLayout = new QHBoxLayout(); 0204 hLayout->setSpacing(3); 0205 hLayout->addWidget(&m_filterLine); 0206 hLayout->addWidget(copy); 0207 hLayout->addWidget(clear); 0208 hLayout->setStretch(0, 1); 0209 0210 // Vertical separator 0211 auto separator = new QFrame(this); 0212 separator->setFrameShape(QFrame::HLine); 0213 separator->setEnabled(false); 0214 separator->setFixedHeight(1); 0215 0216 // tree view 0217 layout->addLayout(hLayout); 0218 layout->addWidget(separator); 0219 layout->addWidget(m_textEdit); 0220 0221 // handle tab button creation 0222 connect(mainWindow, &KateMainWindow::tabForToolViewAdded, this, &KateOutputView::tabForToolViewAdded); 0223 0224 // handle config changes & apply initial configuration 0225 connect(KateApp::self(), &KateApp::configurationChanged, this, &KateOutputView::readConfig); 0226 connect(KTextEditor::Editor::instance(), &KTextEditor::Editor::configChanged, this, &KateOutputView::readConfig); 0227 readConfig(); 0228 } 0229 0230 void KateOutputView::tabForToolViewAdded(QWidget *toolView, QWidget *tab) 0231 { 0232 if (parent() == toolView) { 0233 tabButton = tab; 0234 } 0235 } 0236 0237 void KateOutputView::search() 0238 { 0239 const QString text = m_filterLine.text(); 0240 if (text.isEmpty()) { 0241 m_textEdit->setExtraSelections({}); 0242 return; 0243 } 0244 0245 const auto theme = KTextEditor::Editor::instance()->theme(); 0246 QTextCharFormat f; 0247 f.setBackground(QColor::fromRgba(theme.editorColor(KSyntaxHighlighting::Theme::SearchHighlight))); 0248 0249 QList<QTextEdit::ExtraSelection> sels; 0250 const auto *doc = m_textEdit->document(); 0251 QTextCursor cursor = doc->find(text, 0); 0252 while (!cursor.isNull()) { 0253 QTextEdit::ExtraSelection s; 0254 s.cursor = cursor; 0255 s.format = f; 0256 sels.append(s); 0257 cursor = doc->find(text, cursor); 0258 } 0259 0260 if (!sels.isEmpty()) { 0261 m_textEdit->setExtraSelections(sels); 0262 if (auto scroll = m_textEdit->verticalScrollBar()) { 0263 scroll->setValue(sels.constFirst().cursor.blockNumber()); 0264 } 0265 } 0266 } 0267 0268 void KateOutputView::readConfig() 0269 { 0270 KSharedConfig::Ptr config = KSharedConfig::openConfig(); 0271 KConfigGroup cgGeneral = KConfigGroup(config, QStringLiteral("General")); 0272 m_showOutputViewForMessageType = cgGeneral.readEntry("Show output view for message type", 1); 0273 const int historyLimit = cgGeneral.readEntry("Output History Limit", 100); 0274 0275 if (historyLimit != m_historyLimit) { 0276 m_historyLimit = historyLimit; 0277 m_textEdit->document()->setMaximumBlockCount(m_historyLimit); 0278 } 0279 0280 // use editor fonts 0281 const auto theme = KTextEditor::Editor::instance()->theme(); 0282 auto pal = m_textEdit->palette(); 0283 pal.setColor(QPalette::Base, QColor::fromRgba(theme.editorColor(KSyntaxHighlighting::Theme::BackgroundColor))); 0284 pal.setColor(QPalette::Highlight, QColor::fromRgba(theme.editorColor(KSyntaxHighlighting::Theme::TextSelection))); 0285 pal.setColor(QPalette::Text, QColor::fromRgba(theme.textColor(KSyntaxHighlighting::Theme::Normal))); 0286 m_textEdit->setPalette(pal); 0287 m_textEdit->setFont(Utils::editorFont()); 0288 m_textEdit->document()->setIndentWidth(m_textEdit->fontMetrics().horizontalAdvance(QLatin1Char(' '))); 0289 0290 auto brighten = [](QColor &c) { 0291 c = c.toHsv(); 0292 c.setHsv(c.hue(), qMin(c.saturation() + 35, 255), qMin(c.value() + 10, 255)); 0293 }; 0294 0295 KColorScheme c; 0296 m_msgIndicatorColors[0] = c.background(KColorScheme::NegativeBackground).color(); 0297 brighten(m_msgIndicatorColors[0]); 0298 m_msgIndicatorColors[1] = c.background(KColorScheme::NeutralBackground).color(); 0299 brighten(m_msgIndicatorColors[1]); 0300 m_msgIndicatorColors[2] = c.background(KColorScheme::PositiveBackground).color(); 0301 brighten(m_msgIndicatorColors[2]); 0302 0303 m_infoColor = QColor::fromRgba(theme.textColor(KSyntaxHighlighting::Theme::Information)).name(); 0304 m_warnColor = QColor::fromRgba(theme.textColor(KSyntaxHighlighting::Theme::Warning)).name(); 0305 m_errColor = QColor::fromRgba(theme.textColor(KSyntaxHighlighting::Theme::Error)).name(); 0306 m_keywordColor = QColor::fromRgba(theme.textColor(KSyntaxHighlighting::Theme::DataType)).name(); 0307 } 0308 0309 static void wrapLinksWithHref(QString &text) 0310 { 0311 static const QRegularExpression re( 0312 QStringLiteral(R"re((https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)))re")); 0313 text.replace(re, QStringLiteral("<a href=\"\\1\" >\\1</a>")); 0314 } 0315 0316 void KateOutputView::appendLines(const QStringList &lines, const QString &token, const QTextCursor &pos) 0317 { 0318 QTextCursor cursor = pos; 0319 if (cursor.isNull()) { 0320 cursor = m_textEdit->textCursor(); 0321 if (!cursor.atEnd()) { 0322 cursor.movePosition(QTextCursor::End); 0323 m_textEdit->setTextCursor(cursor); 0324 } 0325 } 0326 bool atStart = cursor.atStart(); 0327 0328 int i = 0; 0329 for (const auto &l : lines) { 0330 if (!atStart) { 0331 cursor.insertBlock(); 0332 cursor.setBlockFormat({}); 0333 } 0334 cursor.insertHtml(QLatin1String("<span style=\"white-space:pre\">") + l + QLatin1String("</span>")); 0335 if (i >= 1) { 0336 QTextBlockFormat fmt; 0337 fmt.setIndent(8); 0338 cursor.setBlockFormat(fmt); 0339 } 0340 atStart = false; 0341 i++; 0342 } 0343 0344 if (!token.isEmpty()) { 0345 cursor.block().setUserData(new BlockData(token)); 0346 } 0347 } 0348 0349 void KateOutputView::slotMessage(const QVariantMap &message) 0350 { 0351 /** 0352 * discard all messages without any real text 0353 */ 0354 auto text = message.value(QStringLiteral("text")).toString().trimmed() /*.replace(QStringLiteral("\n"), QStringLiteral("<br>"))*/; 0355 if (text.isEmpty()) { 0356 return; 0357 } 0358 0359 /* 0360 * subsequent message might replace a former one (e.g. for progress) 0361 */ 0362 const auto token = message.value(QStringLiteral("token")).toString(); 0363 0364 QString meta = QStringLiteral("["); 0365 0366 /** 0367 * date time column: we want to know when a message arrived 0368 * TODO: perhaps store full date time for more stuff later 0369 */ 0370 const QDateTime current = QDateTime::currentDateTime(); 0371 meta += current.time().toString(Qt::TextDate); 0372 0373 /** 0374 * category 0375 * provided by sender to better categorize the output into stuff like: lsp, git, ... 0376 * optional icon support 0377 */ 0378 const QString category = message.value(QStringLiteral("category")).toString().trimmed(); 0379 const auto categoryIcon = message.value(QStringLiteral("categoryIcon")).value<QIcon>(); 0380 if (categoryIcon.isNull()) { 0381 m_textEdit->addIcon(GENERIC_ICON_NAME, {}); 0382 meta += QStringLiteral(" <img style=\"vertical-align:middle\" src=\"") + GENERIC_ICON_NAME + QStringLiteral("\"/> "); 0383 } else { 0384 m_textEdit->addIcon(category, categoryIcon); 0385 meta += QStringLiteral(" <img style=\"vertical-align:middle\" src=\"") + category + QStringLiteral("\"/> "); 0386 } 0387 0388 meta += QStringLiteral("<span style=\"color:%1\">").arg(m_keywordColor) + category + QStringLiteral("</span> "); 0389 0390 /** 0391 * type column: shows the type, icons for some types only 0392 */ 0393 bool shouldShowOutputToolView = false; 0394 int indicatorLoopCount = 0; // for warning/error infinite loop 0395 QColor color; 0396 const auto typeString = message.value(QStringLiteral("type")).toString(); 0397 if (typeString == QLatin1String("Error")) { 0398 shouldShowOutputToolView = (m_showOutputViewForMessageType >= 1); 0399 meta += QStringLiteral("<span style=\"color:%1\">").arg(m_errColor) + i18nc("@info", "Error") + QStringLiteral("</span>"); 0400 color = m_msgIndicatorColors[0]; 0401 } else if (typeString == QLatin1String("Warning")) { 0402 shouldShowOutputToolView = (m_showOutputViewForMessageType >= 2); 0403 meta += QStringLiteral("<span style=\"color:%1\">").arg(m_warnColor) + i18nc("@info", "Warning") + QStringLiteral("</span>"); 0404 color = m_msgIndicatorColors[1]; 0405 } else if (typeString == QLatin1String("Info")) { 0406 shouldShowOutputToolView = (m_showOutputViewForMessageType >= 3); 0407 meta += QStringLiteral("<span style=\"color:%1\">").arg(m_infoColor) + i18nc("@info", "Info") + QStringLiteral("</span>"); 0408 indicatorLoopCount = 2; 0409 color = m_msgIndicatorColors[2]; 0410 } else { 0411 shouldShowOutputToolView = (m_showOutputViewForMessageType >= 4); 0412 meta += i18nc("@info", "Log"); 0413 indicatorLoopCount = -1; // no FadingIndicator for log messages 0414 } 0415 0416 meta += QStringLiteral("] "); 0417 0418 if (shouldShowOutputToolView || isVisible()) { 0419 // if we are going to show the output toolview afterwards 0420 indicatorLoopCount = 1; 0421 } 0422 0423 if (!m_fadingIndicator && indicatorLoopCount >= 0 && tabButton) { 0424 m_fadingIndicator = new NewMsgIndicator(tabButton); 0425 m_fadingIndicator->run(indicatorLoopCount, color); 0426 connect(tabButton, SIGNAL(clicked()), m_fadingIndicator, SLOT(stop())); 0427 } 0428 0429 /** 0430 * actual message text 0431 */ 0432 wrapLinksWithHref(text); 0433 auto lines = text.split(QLatin1Char('\n'), Qt::SkipEmptyParts); 0434 if (lines.isEmpty()) { 0435 return; 0436 } 0437 0438 if (!token.isEmpty()) { 0439 const auto doc = m_textEdit->document(); 0440 auto block = doc->lastBlock(); 0441 bool found = false; 0442 while (block.isValid()) { 0443 auto data = static_cast<BlockData *>(block.userData()); 0444 if (data && data->token == token) { 0445 found = true; 0446 break; 0447 } 0448 block = block.previous(); 0449 } 0450 0451 if (!found) { 0452 lines.first().prepend(meta); 0453 appendLines(lines, token); 0454 } else if (block.isValid()) { 0455 QTextCursor c(block); 0456 0457 c.select(QTextCursor::BlockUnderCursor); 0458 c.removeSelectedText(); 0459 if (lines.size() == 1) { 0460 c.insertBlock(); 0461 c.insertHtml(meta + lines.first()); 0462 } else { 0463 lines.first().prepend(meta); 0464 appendLines(lines, token, c); 0465 } 0466 c.block().setUserData(new BlockData(token)); 0467 } else { 0468 qWarning() << Q_FUNC_INFO << "unable to find valid block!"; 0469 m_textEdit->append(meta); 0470 auto data = new BlockData(token); 0471 m_textEdit->document()->lastBlock().setUserData(data); 0472 } 0473 } else { 0474 lines.first().prepend(meta); 0475 appendLines(lines, /*token=*/QString()); 0476 } 0477 0478 /** 0479 * ensure last item is visible 0480 */ 0481 if (auto scroll = m_textEdit->verticalScrollBar()) { 0482 scroll->setValue(scroll->maximum()); 0483 } 0484 0485 /** 0486 * if message requires it => show the tool view if hidden 0487 */ 0488 if (shouldShowOutputToolView) { 0489 QPointer<QWidget> focusWidget = qApp->focusWidget(); 0490 m_mainWindow->showToolView(qobject_cast<QWidget *>(parent())); 0491 if (focusWidget) { 0492 focusWidget->setFocus(); 0493 } 0494 } 0495 } 0496 0497 #include "kateoutputview.moc" 0498 #include "moc_kateoutputview.cpp"