File indexing completed on 2024-04-28 05:48:35
0001 /* 0002 SPDX-FileCopyrightText: 2021 Waqar Ahmed <waqar.17a@gmail.com> 0003 SPDX-FileCopyrightText: 2021 Kåre Särs <kare.sars@iki.fi> 0004 0005 SPDX-License-Identifier: MIT 0006 */ 0007 #include "gitblametooltip.h" 0008 #include "kategitblameplugin.h" 0009 0010 #include <QDebug> 0011 #include <QEvent> 0012 #include <QFontMetrics> 0013 #include <QMouseEvent> 0014 #include <QScrollBar> 0015 #include <QString> 0016 #include <QTextBrowser> 0017 #include <QTimer> 0018 0019 #include <KTextEditor/Editor> 0020 #include <KTextEditor/View> 0021 0022 #include <KSyntaxHighlighting/AbstractHighlighter> 0023 #include <KSyntaxHighlighting/Definition> 0024 #include <KSyntaxHighlighting/Format> 0025 #include <KSyntaxHighlighting/Repository> 0026 #include <KSyntaxHighlighting/State> 0027 0028 #include <ktexteditor_utils.h> 0029 0030 using KSyntaxHighlighting::AbstractHighlighter; 0031 using KSyntaxHighlighting::Format; 0032 0033 static QString toHtmlRgbaString(const QColor &color) 0034 { 0035 if (color.alpha() == 0xFF) 0036 return color.name(); 0037 0038 QString rgba = QStringLiteral("rgba("); 0039 rgba.append(QString::number(color.red())); 0040 rgba.append(QLatin1Char(',')); 0041 rgba.append(QString::number(color.green())); 0042 rgba.append(QLatin1Char(',')); 0043 rgba.append(QString::number(color.blue())); 0044 rgba.append(QLatin1Char(',')); 0045 // this must be alphaF 0046 rgba.append(QString::number(color.alphaF())); 0047 rgba.append(QLatin1Char(')')); 0048 return rgba; 0049 } 0050 0051 class HtmlHl : public AbstractHighlighter 0052 { 0053 public: 0054 HtmlHl() 0055 : out(&outputString) 0056 { 0057 } 0058 0059 void setText(const QString &txt) 0060 { 0061 text = txt; 0062 QTextStream in(&text); 0063 0064 out.reset(); 0065 outputString.clear(); 0066 0067 bool inDiff = false; 0068 0069 KSyntaxHighlighting::State state; 0070 out << "<pre>"; 0071 while (!in.atEnd()) { 0072 currentLine = in.readLine(); 0073 0074 // Link to open the tree view, insert as is 0075 if (currentLine.startsWith(QStringLiteral("<a href"))) { 0076 out << currentLine; 0077 continue; 0078 } 0079 0080 // allow empty lines in code blocks, no ruler here 0081 if (!inDiff && currentLine.isEmpty()) { 0082 out << "<hr>"; 0083 continue; 0084 } 0085 0086 // diff block 0087 if (!inDiff && currentLine.startsWith(QLatin1String("diff"))) { 0088 inDiff = true; 0089 } 0090 0091 state = highlightLine(currentLine, state); 0092 out << "\n"; 0093 } 0094 out << "</pre>"; 0095 } 0096 0097 QString html() const 0098 { 0099 // while (!out.atEnd()) 0100 // qWarning() << out.readLine(); 0101 return outputString; 0102 } 0103 0104 protected: 0105 void applyFormat(int offset, int length, const Format &format) override 0106 { 0107 if (!length) 0108 return; 0109 0110 QString formatOutput; 0111 0112 if (format.hasTextColor(theme())) { 0113 formatOutput = toHtmlRgbaString(format.textColor(theme())); 0114 } 0115 0116 if (!formatOutput.isEmpty()) { 0117 out << "<span style=\"color:" << formatOutput << "\">"; 0118 } 0119 0120 out << currentLine.mid(offset, length).toHtmlEscaped(); 0121 0122 if (!formatOutput.isEmpty()) { 0123 out << "</span>"; 0124 } 0125 } 0126 0127 private: 0128 QString text; 0129 QString currentLine; 0130 QString outputString; 0131 QTextStream out; 0132 }; 0133 0134 class GitBlameTooltip::Private : public QTextBrowser 0135 { 0136 Q_OBJECT 0137 0138 public: 0139 QKeySequence m_ignoreKeySequence; 0140 0141 explicit Private(KateGitBlamePluginView *pluginView) 0142 : QTextBrowser(nullptr) 0143 { 0144 setWindowFlags(Qt::FramelessWindowHint | Qt::BypassGraphicsProxyWidget | Qt::ToolTip); 0145 setWordWrapMode(QTextOption::NoWrap); 0146 document()->setDocumentMargin(10); 0147 setFrameStyle(QFrame::Box | QFrame::Raised); 0148 setOpenLinks(false); 0149 connect(&m_hideTimer, &QTimer::timeout, this, &Private::hideTooltip); 0150 0151 setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded); 0152 setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); 0153 0154 m_htmlHl.setDefinition(m_syntaxHlRepo.definitionForName(QStringLiteral("Diff"))); 0155 0156 auto updateColors = [this](KTextEditor::Editor *e) { 0157 auto theme = e->theme(); 0158 m_htmlHl.setTheme(theme); 0159 0160 auto pal = palette(); 0161 const QColor bg = theme.editorColor(KSyntaxHighlighting::Theme::BackgroundColor); 0162 pal.setColor(QPalette::Base, bg); 0163 const QColor normal = theme.textColor(KSyntaxHighlighting::Theme::Normal); 0164 pal.setColor(QPalette::Text, normal); 0165 setPalette(pal); 0166 0167 setFont(Utils::editorFont()); 0168 }; 0169 updateColors(KTextEditor::Editor::instance()); 0170 connect(KTextEditor::Editor::instance(), &KTextEditor::Editor::configChanged, this, updateColors); 0171 // Kinda ugly, but we are deep in the pimpl class wrapped by a normal cpp class... 0172 connect(this, &QTextBrowser::anchorClicked, pluginView, [pluginView, this](const QUrl &url) { 0173 hideTooltip(); 0174 pluginView->showCommitTreeView(url); 0175 }); 0176 } 0177 0178 bool eventFilter(QObject *, QEvent *event) override 0179 { 0180 switch (event->type()) { 0181 case QEvent::KeyPress: 0182 case QEvent::ShortcutOverride: { 0183 QKeyEvent *ke = static_cast<QKeyEvent *>(event); 0184 if (ke->matches(QKeySequence::Copy)) { 0185 copy(); 0186 } else if (ke->matches(QKeySequence::SelectAll)) { 0187 selectAll(); 0188 } 0189 event->accept(); 0190 return true; 0191 } 0192 case QEvent::KeyRelease: { 0193 QKeyEvent *ke = static_cast<QKeyEvent *>(event); 0194 if (ke->matches(QKeySequence::Copy) || ke->matches(QKeySequence::SelectAll) 0195 || (m_ignoreKeySequence.matches(QKeySequence(ke->key()) != QKeySequence::NoMatch)) || ke->key() == Qt::Key_Control || ke->key() == Qt::Key_Alt 0196 || ke->key() == Qt::Key_Shift || ke->key() == Qt::Key_AltGr || ke->key() == Qt::Key_Meta) { 0197 event->accept(); 0198 return true; 0199 } 0200 } // fall through 0201 case QEvent::WindowActivate: 0202 case QEvent::WindowDeactivate: 0203 hideTooltip(); 0204 break; 0205 default: 0206 break; 0207 } 0208 return false; 0209 } 0210 0211 void showTooltip(const QString &text, KTextEditor::View *view) 0212 { 0213 if (text.isEmpty() || !view) { 0214 return; 0215 } 0216 0217 m_htmlHl.setText(text); 0218 setHtml(m_htmlHl.html()); 0219 // view changed? 0220 // => update definition 0221 // => update font 0222 if (view != m_view) { 0223 if (m_view && m_view->focusProxy()) { 0224 m_view->focusProxy()->removeEventFilter(this); 0225 } 0226 m_view = view; 0227 m_view->focusProxy()->installEventFilter(this); 0228 } 0229 0230 const int scrollBarHeight = horizontalScrollBar()->height(); 0231 QFontMetrics fm(font()); 0232 QSize size = fm.size(Qt::TextSingleLine, QStringLiteral("m")); 0233 int fontHeight = size.height(); 0234 size.setHeight(m_view->height() - fontHeight * 2 - scrollBarHeight); 0235 size.setWidth(qRound(m_view->width() * 0.7)); 0236 resize(size); 0237 0238 QPoint p = m_view->mapToGlobal(m_view->pos()); 0239 p.setY(p.y() + fontHeight); 0240 p.setX(p.x() + m_view->textAreaRect().left() + m_view->textAreaRect().width() - size.width() - fontHeight); 0241 this->move(p); 0242 0243 show(); 0244 } 0245 0246 Q_SLOT void hideTooltip() 0247 { 0248 if (m_view && m_view->focusProxy()) { 0249 m_view->focusProxy()->removeEventFilter(this); 0250 m_view.clear(); 0251 } 0252 close(); 0253 setText(QString()); 0254 m_inContextMenu = false; 0255 } 0256 0257 protected: 0258 void showEvent(QShowEvent *event) override 0259 { 0260 m_hideTimer.start(3000); 0261 return QTextBrowser::showEvent(event); 0262 } 0263 0264 void enterEvent(QEnterEvent *event) override 0265 { 0266 m_inContextMenu = false; 0267 m_hideTimer.stop(); 0268 return QTextBrowser::enterEvent(event); 0269 } 0270 0271 void leaveEvent(QEvent *event) override 0272 { 0273 if (!m_hideTimer.isActive() && !m_inContextMenu && textCursor().selectionStart() == textCursor().selectionEnd()) { 0274 hideTooltip(); 0275 } 0276 return QTextBrowser::leaveEvent(event); 0277 } 0278 0279 void mouseMoveEvent(QMouseEvent *event) override 0280 { 0281 auto pos = event->pos(); 0282 if (rect().contains(pos) || m_inContextMenu || textCursor().selectionStart() != textCursor().selectionEnd()) { 0283 return QTextBrowser::mouseMoveEvent(event); 0284 } 0285 hideTooltip(); 0286 } 0287 0288 void contextMenuEvent(QContextMenuEvent *event) override 0289 { 0290 m_inContextMenu = true; 0291 return QTextBrowser::contextMenuEvent(event); 0292 } 0293 0294 private: 0295 bool m_inContextMenu = false; 0296 QPointer<KTextEditor::View> m_view; 0297 QTimer m_hideTimer; 0298 HtmlHl m_htmlHl; 0299 KSyntaxHighlighting::Repository m_syntaxHlRepo; 0300 }; 0301 0302 GitBlameTooltip::GitBlameTooltip(KateGitBlamePluginView *pv) 0303 : m_pluginView(pv) 0304 0305 { 0306 } 0307 0308 GitBlameTooltip::~GitBlameTooltip() = default; 0309 0310 void GitBlameTooltip::show(const QString &text, KTextEditor::View *view) 0311 { 0312 if (text.isEmpty() || !view || !view->document()) { 0313 return; 0314 } 0315 0316 if (!d) { 0317 d = std::make_unique<GitBlameTooltip::Private>(m_pluginView); 0318 } 0319 0320 d->showTooltip(text, view); 0321 } 0322 0323 void GitBlameTooltip::setIgnoreKeySequence(const QKeySequence &sequence) 0324 { 0325 if (!d) { 0326 d = std::make_unique<GitBlameTooltip::Private>(m_pluginView); 0327 } 0328 d->m_ignoreKeySequence = sequence; 0329 } 0330 0331 #include "gitblametooltip.moc"