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"