lsptooltip.cpp 9.11 KB
Newer Older
Waqar Ahmed's avatar
Waqar Ahmed committed
1
2
3
4
5
6
/*  SPDX-License-Identifier: MIT

    SPDX-FileCopyrightText: 2021 Waqar Ahmed <waqar.17a@gmail.com>

    SPDX-License-Identifier: MIT
*/
Waqar Ahmed's avatar
Waqar Ahmed committed
7
8
#include "lsptooltip.h"

9
#include <QApplication>
Waqar Ahmed's avatar
Waqar Ahmed committed
10
11
12
13
14
#include <QDebug>
#include <QEvent>
#include <QFontMetrics>
#include <QLabel>
#include <QMouseEvent>
15
#include <QScreen>
Waqar Ahmed's avatar
Waqar Ahmed committed
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include <QString>
#include <QTextBrowser>
#include <QTimer>

#include <KTextEditor/ConfigInterface>
#include <KTextEditor/Editor>
#include <KTextEditor/View>

#include <KSyntaxHighlighting/AbstractHighlighter>
#include <KSyntaxHighlighting/Definition>
#include <KSyntaxHighlighting/Format>
#include <KSyntaxHighlighting/Repository>
#include <KSyntaxHighlighting/State>

using KSyntaxHighlighting::AbstractHighlighter;
using KSyntaxHighlighting::Format;

static QString toHtmlRgbaString(const QColor &color)
{
    if (color.alpha() == 0xFF)
        return color.name();

    QString rgba = QStringLiteral("rgba(");
    rgba.append(QString::number(color.red()));
    rgba.append(QLatin1Char(','));
    rgba.append(QString::number(color.green()));
    rgba.append(QLatin1Char(','));
    rgba.append(QString::number(color.blue()));
    rgba.append(QLatin1Char(','));
    // this must be alphaF
    rgba.append(QString::number(color.alphaF()));
    rgba.append(QLatin1Char(')'));
    return rgba;
}

class HtmlHl : public AbstractHighlighter
{
public:
    HtmlHl()
55
        : out(&outputString)
Waqar Ahmed's avatar
Waqar Ahmed committed
56
57
58
59
60
61
62
    {
    }

    void setText(const QString &txt)
    {
        text = txt;
        QTextStream in(&text);
63

Waqar Ahmed's avatar
Waqar Ahmed committed
64
        out.reset();
65
66
        outputString.clear();

Waqar Ahmed's avatar
Waqar Ahmed committed
67
68
        bool inCodeBlock = false;

69
        KSyntaxHighlighting::State state;
Waqar Ahmed's avatar
Waqar Ahmed committed
70
        bool li = false;
Waqar Ahmed's avatar
Waqar Ahmed committed
71
        // World's smallest markdown parser :)
Waqar Ahmed's avatar
Waqar Ahmed committed
72
73
        while (!in.atEnd()) {
            currentLine = in.readLine();
Waqar Ahmed's avatar
Waqar Ahmed committed
74
75
76

            // allow empty lines in code blocks, no ruler here
            if (!inCodeBlock && currentLine.isEmpty()) {
Waqar Ahmed's avatar
Waqar Ahmed committed
77
78
79
                out << "<hr>";
                continue;
            }
Waqar Ahmed's avatar
Waqar Ahmed committed
80
81

            // list
Waqar Ahmed's avatar
Waqar Ahmed committed
82
83
84
85
86
87
88
89
90
91
92
            if (!li && currentLine.startsWith(QLatin1String("- "))) {
                currentLine.remove(0, 2);
                out << "<ul><li>";
                li = true;
            } else if (li && currentLine.startsWith(QLatin1String("- "))) {
                currentLine.remove(0, 2);
                out << "<li>";
            } else if (li) {
                out << "</li></ul>";
                li = false;
            }
Waqar Ahmed's avatar
Waqar Ahmed committed
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110

            // code block
            if (!inCodeBlock && currentLine.startsWith(QLatin1String("```"))) {
                inCodeBlock = true;
                continue;
            } else if (inCodeBlock && currentLine.startsWith(QLatin1String("```"))) {
                inCodeBlock = false;
                continue;
            }

            // ATX heading
            if (currentLine.startsWith(QStringLiteral("# "))) {
                currentLine.remove(0, 2);
                currentLine = QStringLiteral("<h3>") + currentLine + QStringLiteral("</h3>");
                out << currentLine;
                continue;
            }

Waqar Ahmed's avatar
Waqar Ahmed committed
111
112
113
114
115
116
117
118
119
            state = highlightLine(currentLine, state);
            if (li) {
                out << "</li>";
                continue;
            }
            out << "\n<br>";
        }
    }

120
    QString html() const
Waqar Ahmed's avatar
Waqar Ahmed committed
121
122
123
    {
        //        while (!out.atEnd())
        //            qWarning() << out.readLine();
124
        return outputString;
Waqar Ahmed's avatar
Waqar Ahmed committed
125
126
127
128
129
130
131
132
    }

protected:
    void applyFormat(int offset, int length, const Format &format) override
    {
        if (!length)
            return;

133
        QString formatOutput;
Waqar Ahmed's avatar
Waqar Ahmed committed
134
135

        if (format.hasTextColor(theme())) {
136
            formatOutput = toHtmlRgbaString(format.textColor(theme()));
Waqar Ahmed's avatar
Waqar Ahmed committed
137
138
139
        }

        if (!formatOutput.isEmpty()) {
140
            out << "<span style=\"color:" << formatOutput << "\">";
Waqar Ahmed's avatar
Waqar Ahmed committed
141
142
143
144
145
146
147
148
149
150
151
152
        }

        out << currentLine.mid(offset, length).toHtmlEscaped();

        if (!formatOutput.isEmpty()) {
            out << "</span>";
        }
    }

private:
    QString text;
    QString currentLine;
153
    QString outputString;
Waqar Ahmed's avatar
Waqar Ahmed committed
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
    QTextStream out;
};

class Tooltip : public QTextBrowser
{
    Q_OBJECT

public:
    static Tooltip *self()
    {
        static Tooltip instance;
        return &instance;
    }

    void setTooltipText(const QString &text)
    {
Waqar Ahmed's avatar
Waqar Ahmed committed
170
171
172
        if (text.isEmpty())
            return;

Waqar Ahmed's avatar
Waqar Ahmed committed
173
174
        hl.setText(text);
        setHtml(hl.html());
Waqar Ahmed's avatar
Waqar Ahmed committed
175
        resizeTip(text);
Waqar Ahmed's avatar
Waqar Ahmed committed
176
177
178
179
180
181
182
183
    }

    void setView(KTextEditor::View *view)
    {
        // view changed?
        // => update definition
        // => update font
        if (view != m_view) {
Waqar Ahmed's avatar
Waqar Ahmed committed
184
185
186
187
            if (m_view) {
                m_view->focusProxy()->removeEventFilter(this);
            }

Waqar Ahmed's avatar
Waqar Ahmed committed
188
189
190
            m_view = view;
            hl.setDefinition(r.definitionForFileName(view->document()->url().toString()));
            updateFont();
Waqar Ahmed's avatar
Waqar Ahmed committed
191
192

            m_view->focusProxy()->installEventFilter(this);
Waqar Ahmed's avatar
Waqar Ahmed committed
193
194
195
196
197
198
199
        }
    }

    Tooltip(QWidget *parent = nullptr)
        : QTextBrowser(parent)
    {
        setWindowFlags(Qt::FramelessWindowHint | Qt::BypassGraphicsProxyWidget | Qt::ToolTip);
Waqar Ahmed's avatar
Waqar Ahmed committed
200
        document()->setDocumentMargin(5);
Waqar Ahmed's avatar
Waqar Ahmed committed
201
202
203
        setFrameStyle(QFrame::Box);
        connect(&m_hideTimer, &QTimer::timeout, this, &Tooltip::hideTooltip);

Waqar Ahmed's avatar
Waqar Ahmed committed
204
205
206
        setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
        setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);

Waqar Ahmed's avatar
Waqar Ahmed committed
207
208
209
210
211
212
213
214
215
216
217
218
219
220
        auto updateColors = [this](KTextEditor::Editor *e) {
            auto theme = e->theme();
            hl.setTheme(theme);

            auto pal = palette();
            pal.setColor(QPalette::Base, theme.editorColor(KSyntaxHighlighting::Theme::BackgroundColor));
            setPalette(pal);

            updateFont();
        };
        updateColors(KTextEditor::Editor::instance());
        connect(KTextEditor::Editor::instance(), &KTextEditor::Editor::configChanged, this, updateColors);
    }

Waqar Ahmed's avatar
Waqar Ahmed committed
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
    bool eventFilter(QObject *o, QEvent *e) override
    {
        switch (e->type()) {
        case QEvent::KeyPress:
        case QEvent::KeyRelease:
            hideTooltip();
            break;
        case QEvent::WindowActivate:
        case QEvent::WindowDeactivate:
        case QEvent::MouseButtonPress:
        case QEvent::MouseButtonRelease:
        case QEvent::MouseButtonDblClick:
        case QEvent::Wheel:
            if (!rect().contains(static_cast<QMouseEvent *>(e)->pos())) {
                hideTooltip();
            }
        default:
            break;
        }
        return false;
    }

Waqar Ahmed's avatar
Waqar Ahmed committed
243
244
245
246
247
248
249
250
251
252
253
254
    void updateFont()
    {
        if (!m_view)
            return;
        auto ciface = qobject_cast<KTextEditor::ConfigInterface *>(m_view);
        auto font = ciface->configValue(QStringLiteral("font")).value<QFont>();
        setFont(font);
    }

    Q_SLOT void hideTooltip()
    {
        close();
Waqar Ahmed's avatar
Waqar Ahmed committed
255
        setText(QString());
Waqar Ahmed's avatar
Waqar Ahmed committed
256
257
258
259
260
261
    }

    void resizeTip(const QString &text)
    {
        QFontMetrics fm(font());
        QSize size = fm.size(0, text);
Waqar Ahmed's avatar
Waqar Ahmed committed
262
263
264
265
266
267
268
269
270
271
272

        // make sure we have the correct height
        // size above gives us correct width but not
        // correct height
        qreal totalHeight = document()->size().height();
        // add +1 line height to prevent scrollbar from appearing with small
        // tooltips
        int lineHeight = totalHeight / document()->lineCount();
        const int height = totalHeight + lineHeight;

        size.setHeight(std::min(height, m_view->height() / 3));
Waqar Ahmed's avatar
Waqar Ahmed committed
273
        size.setWidth(std::min(size.width(), m_view->width() / 2));
Waqar Ahmed's avatar
Waqar Ahmed committed
274
275
276
        resize(size);
    }

Waqar Ahmed's avatar
Waqar Ahmed committed
277
278
    void place(QPoint p)
    {
279
280
281
        QRect screen = QApplication::screenAt(p)->availableGeometry();

        if (p.x() + width() > screen.x() + screen.width())
Waqar Ahmed's avatar
Waqar Ahmed committed
282
            p.rx() -= 4 + width();
283
        if (p.y() + this->height() > screen.y() + screen.height())
Waqar Ahmed's avatar
Waqar Ahmed committed
284
            p.ry() -= 24 + this->height();
285
286
287
288
289
290
291
292
293
        if (p.y() < screen.y())
            p.setY(screen.y());
        if (p.x() + this->width() > screen.x() + screen.width())
            p.setX(screen.x() + screen.width() - this->width());
        if (p.x() < screen.x())
            p.setX(screen.x());
        if (p.y() + this->height() > screen.y() + screen.height())
            p.setY(screen.y() + screen.height() - this->height());

Waqar Ahmed's avatar
Waqar Ahmed committed
294
295
296
        this->move(p);
    }

Waqar Ahmed's avatar
Waqar Ahmed committed
297
298
299
protected:
    void showEvent(QShowEvent *event) override
    {
Waqar Ahmed's avatar
Waqar Ahmed committed
300
        m_hideTimer.start(1000);
Waqar Ahmed's avatar
Waqar Ahmed committed
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
        return QTextBrowser::showEvent(event);
    }

    void enterEvent(QEvent *event) override
    {
        m_hideTimer.stop();
        return QTextBrowser::enterEvent(event);
    }

    void leaveEvent(QEvent *event) override
    {
        if (!m_hideTimer.isActive()) {
            hideTooltip();
        }
        return QTextBrowser::leaveEvent(event);
    }

    void mouseMoveEvent(QMouseEvent *event) override
    {
        auto pos = event->pos();
        if (rect().contains(pos)) {
            return QTextBrowser::mouseMoveEvent(event);
        }
        hideTooltip();
    }

private:
    KTextEditor::View *m_view;
    QTimer m_hideTimer;
    HtmlHl hl;
    KSyntaxHighlighting::Repository r;
};

void LspTooltip::show(const QString &text, QPoint pos, KTextEditor::View *v)
{
Waqar Ahmed's avatar
Waqar Ahmed committed
336
337
338
    if (text.isEmpty())
        return;

Waqar Ahmed's avatar
Waqar Ahmed committed
339
340
    Tooltip::self()->setView(v);
    Tooltip::self()->setTooltipText(text);
Waqar Ahmed's avatar
Waqar Ahmed committed
341
    Tooltip::self()->place(pos);
Waqar Ahmed's avatar
Waqar Ahmed committed
342
343
344
345
    Tooltip::self()->show();
}

#include "lsptooltip.moc"