File indexing completed on 2024-05-12 05:52:08

0001 /*
0002     SPDX-FileCopyrightText: 2021 Waqar Ahmed <waqar.17a@gmail.com>
0003     SPDX-License-Identifier: LGPL-2.0-or-later
0004 */
0005 #include "tooltip.h"
0006 
0007 #include <QApplication>
0008 #include <QEvent>
0009 #include <QFontMetrics>
0010 #include <QMouseEvent>
0011 #include <QPointer>
0012 #include <QScreen>
0013 #include <QScrollBar>
0014 #include <QString>
0015 #include <QTextBrowser>
0016 #include <QTimer>
0017 
0018 #include <KSyntaxHighlighting/Definition>
0019 #include <KSyntaxHighlighting/Repository>
0020 #include <KSyntaxHighlighting/SyntaxHighlighter>
0021 #include <KTextEditor/Document>
0022 #include <KTextEditor/Editor>
0023 #include <KTextEditor/View>
0024 #include <KWindowSystem>
0025 
0026 // #include <ktexteditor_utils.h>
0027 
0028 class TooltipPrivate : public QTextBrowser
0029 {
0030     Q_OBJECT
0031 
0032 public:
0033     void setTooltipText(const QString &text, TextHintMarkupKind kind)
0034     {
0035         if (text.isEmpty())
0036             return;
0037 
0038         m_kind = kind;
0039         // we have to do this to handle soft line
0040         if (kind == TextHintMarkupKind::PlainText) {
0041             setPlainText(text);
0042         } else {
0043             QString htext = text;
0044             htext.replace(QLatin1Char('\n'), QStringLiteral("  \n"));
0045             setMarkdown(htext);
0046         }
0047         resizeTip(text);
0048     }
0049 
0050     void appendMarkdown(const QString &text)
0051     {
0052         auto md = toMarkdown();
0053         // hbreak
0054         md += QStringLiteral("\n----\n");
0055         md += text;
0056         setMarkdown(md);
0057     }
0058 
0059     void appendTooltipText(const QString &text, TextHintMarkupKind kind)
0060     {
0061         auto cursor = textCursor();
0062         cursor.movePosition(QTextCursor::End);
0063         if (m_kind == TextHintMarkupKind::PlainText && kind == TextHintMarkupKind::PlainText) {
0064             cursor.insertText(QStringLiteral("\n"));
0065             cursor.insertText(text);
0066         } else if (m_kind == TextHintMarkupKind::MarkDown && kind == TextHintMarkupKind::MarkDown) {
0067             appendMarkdown(text);
0068         } else if (m_kind == TextHintMarkupKind::PlainText && kind == TextHintMarkupKind::MarkDown) {
0069             appendMarkdown(text);
0070         } else if (m_kind == TextHintMarkupKind::MarkDown && kind == TextHintMarkupKind::PlainText) {
0071             appendMarkdown(text);
0072         }
0073 
0074         // resize if too small
0075         if (height() < 300) {
0076             resize(width(), height() + 200);
0077         }
0078     }
0079 
0080     void setView(KTextEditor::View *view)
0081     {
0082         // view changed?
0083         // => update definition
0084         // => update font
0085         if (view != m_view) {
0086             if (m_view && m_view->focusProxy()) {
0087                 m_view->focusProxy()->removeEventFilter(this);
0088             }
0089 
0090             m_view = view;
0091 
0092             hl.setDefinition(KTextEditor::Editor::instance()->repository().definitionForFileName(m_view->document()->url().toString()));
0093 
0094             if (m_view && m_view->focusProxy()) {
0095                 m_view->focusProxy()->installEventFilter(this);
0096             }
0097         }
0098     }
0099 
0100     TooltipPrivate(QWidget *parent, bool manual)
0101         : QTextBrowser(parent)
0102         , hl(document())
0103         , m_manual(manual)
0104     {
0105         setWindowFlags(Qt::FramelessWindowHint | Qt::BypassGraphicsProxyWidget | Qt::ToolTip);
0106         setAttribute(Qt::WA_DeleteOnClose, true);
0107         document()->setDocumentMargin(5);
0108         setFrameStyle(QFrame::Box | QFrame::Raised);
0109         connect(&m_hideTimer, &QTimer::timeout, this, &TooltipPrivate::hideTooltip);
0110 
0111         setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
0112         setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
0113 
0114         // doc links
0115         setOpenExternalLinks(true);
0116 
0117         auto updateColors = [this](KTextEditor::Editor *e) {
0118             auto theme = e->theme();
0119             hl.setTheme(theme);
0120 
0121             auto pal = palette();
0122             const QColor bg = theme.editorColor(KSyntaxHighlighting::Theme::BackgroundColor);
0123             pal.setColor(QPalette::Base, bg);
0124             const QColor normal = theme.textColor(KSyntaxHighlighting::Theme::Normal);
0125             pal.setColor(QPalette::Text, normal);
0126             setPalette(pal);
0127 
0128             setFont(KTextEditor::Editor::instance()->font());
0129         };
0130         updateColors(KTextEditor::Editor::instance());
0131         connect(KTextEditor::Editor::instance(), &KTextEditor::Editor::configChanged, this, updateColors);
0132     }
0133 
0134     bool eventFilter(QObject *, QEvent *e) override
0135     {
0136         switch (e->type()) {
0137         // only consider KeyPress
0138         // a key release might get triggered by the trail of a shortcut key activation
0139         case QEvent::KeyPress:
0140             hideTooltip();
0141             break;
0142         case QEvent::WindowActivate:
0143         case QEvent::WindowDeactivate:
0144         case QEvent::FocusOut:
0145         case QEvent::FocusIn:
0146             if (!inContextMenu && !m_view->hasFocus()) {
0147                 hideTooltip();
0148             }
0149             break;
0150         case QEvent::MouseMove:
0151             if (!m_manual && !hasFocus())
0152                 hideTooltipWithDelay();
0153             break;
0154         case QEvent::MouseButtonPress:
0155         case QEvent::MouseButtonRelease:
0156         case QEvent::MouseButtonDblClick:
0157         case QEvent::Wheel:
0158             if (!rect().contains(static_cast<QSinglePointEvent *>(e)->position().toPoint())) {
0159                 hideTooltip();
0160             }
0161             break;
0162         default:
0163             break;
0164         }
0165         return false;
0166     }
0167 
0168     Q_SLOT void hideTooltip()
0169     {
0170         deleteLater();
0171     }
0172 
0173     Q_SLOT void hideTooltipWithDelay()
0174     {
0175         m_hideTimer.start(100);
0176     }
0177 
0178     void resizeTip(const QString &text)
0179     {
0180         QFontMetrics fm(font());
0181         QSize size = fm.size(0, text);
0182 
0183         const int height = fm.lineSpacing() * (document()->lineCount());
0184 
0185         size.setHeight(std::min(height, m_view->height() / 3));
0186         size.setWidth(std::min<int>(size.width(), m_view->width() / 2.5));
0187 
0188         const int contentsMarginsWidth = this->contentsMargins().left() + this->contentsMargins().right();
0189         const int contentsMarginsHeight = this->contentsMargins().top() + this->contentsMargins().top();
0190         const int docMargin = 2 * document()->documentMargin();
0191         const int wMargins = contentsMarginsWidth + docMargin + verticalScrollBar()->height();
0192         const int hMargins = contentsMarginsHeight + docMargin + horizontalScrollBar()->height();
0193 
0194         size.setWidth(size.width() + wMargins);
0195         size.setHeight(size.height() + hMargins);
0196         resize(size);
0197     }
0198 
0199     void place(QPoint p)
0200     {
0201         const auto offset = QPoint(3, 21);
0202         p += offset;
0203 
0204         // wayland automatically keeps popups on screen
0205         if (KWindowSystem::isPlatformWayland()) {
0206             move(p);
0207             return;
0208         }
0209 
0210         // try to get right screen, important: QApplication::screenAt(p) might return nullptr
0211         // see crash in bug 439804
0212         const QScreen *screenForTooltip = QApplication::screenAt(p);
0213         if (!screenForTooltip) {
0214             screenForTooltip = screen();
0215         }
0216         const QRect screen = screenForTooltip->availableGeometry();
0217 
0218         if (p.x() + width() > screen.x() + screen.width())
0219             p.rx() -= 4 + width();
0220         if (p.y() + this->height() > screen.y() + screen.height())
0221             p.ry() -= 24 + this->height();
0222         if (p.y() < screen.y())
0223             p.setY(screen.y());
0224         if (p.x() + this->width() > screen.x() + screen.width())
0225             p.setX(screen.x() + screen.width() - this->width());
0226         if (p.x() < screen.x())
0227             p.setX(screen.x());
0228         if (p.y() + this->height() > screen.y() + screen.height())
0229             p.setY(screen.y() + screen.height() - this->height());
0230 
0231         this->move(p);
0232     }
0233 
0234 protected:
0235     void enterEvent(QEnterEvent *event) override
0236     {
0237         inContextMenu = false;
0238         m_hideTimer.stop();
0239         QTextBrowser::enterEvent(event);
0240     }
0241 
0242     void leaveEvent(QEvent *event) override
0243     {
0244         if (!m_hideTimer.isActive() && !inContextMenu) {
0245             hideTooltip();
0246         }
0247         QTextBrowser::leaveEvent(event);
0248     }
0249 
0250     void mouseMoveEvent(QMouseEvent *event) override
0251     {
0252         auto pos = event->pos();
0253         if (rect().contains(pos)) {
0254             return QTextBrowser::mouseMoveEvent(event);
0255         }
0256     }
0257 
0258     void contextMenuEvent(QContextMenuEvent *e) override
0259     {
0260         inContextMenu = true;
0261         QTextBrowser::contextMenuEvent(e);
0262     }
0263 
0264 private:
0265     bool inContextMenu = false;
0266     QPointer<KTextEditor::View> m_view;
0267     QTimer m_hideTimer;
0268     KSyntaxHighlighting::SyntaxHighlighter hl;
0269     bool m_manual;
0270     TextHintMarkupKind m_kind = TextHintMarkupKind::PlainText;
0271 };
0272 
0273 void KateTooltip::show(const QString &text, TextHintMarkupKind kind, QPoint pos, KTextEditor::View *v, bool manual)
0274 {
0275     if (text.isEmpty())
0276         return;
0277 
0278     if (!v || !v->document()) {
0279         return;
0280     }
0281 
0282     static QPointer<TooltipPrivate> tooltip = nullptr;
0283     if (tooltip && tooltip->isVisible()) {
0284         tooltip->appendTooltipText(text, kind);
0285         return;
0286     }
0287     delete tooltip;
0288 
0289     tooltip = new TooltipPrivate(v, manual);
0290     tooltip->setView(v);
0291     tooltip->setTooltipText(text, kind);
0292     tooltip->place(pos);
0293     tooltip->show();
0294 }
0295 
0296 #include "tooltip.moc"