/*
 * Copyright (C) Pedram Pourang (aka Tsu Jan) 2016-2020 <tsujan2000@gmail.com>
 *
 * FeatherNotes is free software: you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the
 * Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * FeatherNotes is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "textedit.h"
#include <QDesktopServices>
#include <QProcess>
#include <QToolTip>
#include <QTimer>
#include <QTextBlock>
#include <QRegularExpression>
#include <QBuffer>
#include <QMimeDatabase>
#include <QLocale>
#include <QTextDocumentFragment>
#include <QApplication>
#include <QClipboard>

#define SCROLL_FRAMES_PER_SEC 50
#define SCROLL_DURATION 200 // in ms
static const int scrollAnimFrames = SCROLL_FRAMES_PER_SEC * SCROLL_DURATION / 1000;

namespace FeatherNotes {

TextEdit::TextEdit (QWidget *parent) : QTextEdit (parent)
{
    autoIndentation = true;
    autoBracket = false;
    autoReplace = false;
    textTab_ = "    "; // the default text tab is four spaces
    pressPoint = QPoint();
    scrollTimer_ = nullptr;

    VScrollBar *vScrollBar = new VScrollBar;
    setVerticalScrollBar (vScrollBar);

#if (QT_VERSION == QT_VERSION_CHECK(5,14,0))
    /* workaround */
    HScrollBar *hScrollBar = new HScrollBar;
    setHorizontalScrollBar (hScrollBar);
#endif
}
/*************************/
TextEdit::~TextEdit()
{
    if (scrollTimer_)
    {
        disconnect (scrollTimer_, &QTimer::timeout, this, &TextEdit::scrollSmoothly);
        scrollTimer_->stop();
        delete scrollTimer_;
    }
}
/*************************/
// Finds the (remaining) spaces that should be inserted with Ctrl+Tab.
QString TextEdit::remainingSpaces (const QString& spaceTab, const QTextCursor& cursor) const
{
    QTextCursor tmp = cursor;
    QString txt = cursor.block().text().left (cursor.positionInBlock());
    QFontMetricsF fm = QFontMetricsF (document()->defaultFont());
#if (QT_VERSION >= QT_VERSION_CHECK(5,11,0))
    qreal spaceL = fm.horizontalAdvance (" ");
#else
    qreal spaceL = fm.width (" ");
#endif
    int n = 0, i = 0;
    while ((i = txt.indexOf("\t", i)) != -1)
    { // find tab widths in terms of spaces
        tmp.setPosition (tmp.block().position() + i);
        qreal x = static_cast<qreal>(cursorRect (tmp).right());
        tmp.setPosition (tmp.position() + 1);
        x = static_cast<qreal>(cursorRect (tmp).right()) - x;
        n += qMax (qRound (qAbs (x) / spaceL) - 1, 0); // x is negative for RTL
        ++i;
    }
    n += txt.count();
    n = spaceTab.count() - n % spaceTab.count();
    QString res;
    for (int i = 0 ; i < n; ++i)
        res += " ";
    return res;
}
/*************************/
// Returns a cursor that selects the spaces to be removed by a backtab.
// If "twoSpace" is true, a 2-space backtab will be applied as far as possible.
QTextCursor TextEdit::backTabCursor (const QTextCursor& cursor, bool twoSpace) const
{
    QTextCursor tmp = cursor;
    tmp.movePosition (QTextCursor::StartOfBlock);
    /* find the start of the real text */
    const QString blockText = cursor.block().text();
    int indx = 0;
    QRegularExpressionMatch match;
    if (blockText.indexOf (QRegularExpression (R"(^\s+)"), 0, &match) > -1)
        indx = match.capturedLength();
    else
        return tmp;
    int txtStart = cursor.block().position() + indx;

    QString txt = blockText.left (indx);
    QFontMetricsF fm = QFontMetricsF (document()->defaultFont());
#if (QT_VERSION >= QT_VERSION_CHECK(5,11,0))
    qreal spaceL = fm.horizontalAdvance (" ");
#else
    qreal spaceL = fm.width (" ");
#endif
    int n = 0, i = 0;
    while ((i = txt.indexOf("\t", i)) != -1)
    { // find tab widths in terms of spaces
        tmp.setPosition (tmp.block().position() + i);
        qreal x = static_cast<qreal>(cursorRect (tmp).right());
        tmp.setPosition (tmp.position() + 1);
        x = static_cast<qreal>(cursorRect (tmp).right()) - x;
        n += qMax (qRound (qAbs (x) / spaceL) - 1, 0);
        ++i;
    }
    n += txt.count();
    n = n % textTab_.count();
    if (n == 0) n = textTab_.count();

    if (twoSpace) n = qMin (n, 2);

    tmp.setPosition (txtStart);
    if (blockText.at (indx - 1) == QChar (QChar::Space))
        tmp.setPosition (txtStart - n, QTextCursor::KeepAnchor);
    else // the previous character is a tab
    {
        qreal x = static_cast<qreal>(cursorRect (tmp).right());
        tmp.setPosition (txtStart - 1, QTextCursor::KeepAnchor);
        x -= static_cast<qreal>(cursorRect (tmp).right());
        n -= qRound (qAbs (x) / spaceL);
        if (n < 0) n = 0; // impossible without "twoSpace"
        tmp.setPosition (tmp.position() - n, QTextCursor::KeepAnchor);
    }

    return tmp;
}
/*************************/
static inline bool isOnlySpaces (const QString &str)
{
    int i = 0;
    while (i < str.size())
    { // always skip the starting spaces
        QChar ch = str.at (i);
        if (ch == QChar (QChar::Space) || ch == QChar (QChar::Tabulation))
            ++i;
        else return false;
    }
    return true;
}

void TextEdit::keyPressEvent (QKeyEvent *event)
{
    if (event->modifiers() == Qt::ControlModifier && !isReadOnly() && event->key() == Qt::Key_Z)
    { // only for a workaround (see TextEdit::undo)
        if (document()->isUndoAvailable())
            undo();
        event->accept();
        return;
    }

    if (event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter)
    {
        QTextCursor cur = textCursor();
        QString selTxt = cur.selectedText();

        if (autoReplace && selTxt.isEmpty())
        {
            const int p = cur.positionInBlock();
            if (p > 1)
            {
                cur.beginEditBlock();
                cur.movePosition (QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor, 2);
                const QString sel = cur.selectedText();
                if (!sel.endsWith ("."))
                {
                    if (sel == "--")
                    {
                        QTextCursor prevCur = cur;
                        prevCur.setPosition (cur.position());
                        prevCur.movePosition (QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor);
                        if (prevCur.selectedText() != "-")
                            cur.insertText ("—");
                    }
                    else if (sel == "->")
                        cur.insertText ("→");
                    else if (sel == "<-")
                        cur.insertText ("←");
                    else if (sel == ">=")
                        cur.insertText ("≥");
                    else if (sel == "<=")
                        cur.insertText ("≤");
                }
                else if (p > 2)
                {
                    cur = textCursor();
                    cur.movePosition (QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor, 3);
                    const QString sel = cur.selectedText();
                    if (sel == "...")
                    {
                        QTextCursor prevCur = cur;
                        prevCur.setPosition (cur.position());
                        if (p > 3)
                        {
                            prevCur.movePosition (QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor);
                            if (prevCur.selectedText() != ".")
                                cur.insertText ("…");
                        }
                        else cur.insertText ("…");
                    }
                }
                cur.endEditBlock();
                cur = textCursor(); // reset the current cursor
            }
        }

        bool isBracketed (false);
        QString prefix, indent;
        bool withShift (event->modifiers() & Qt::ShiftModifier);

        /* with Shift+Enter, find the non-letter prefix */
        if (withShift)
        {
            cur.clearSelection();
            setTextCursor (cur);
            const QString blockText = cur.block().text();
            int i = 0;
            int curBlockPos = cur.position() - cur.block().position();
            while (i < curBlockPos)
            {
                QChar ch = blockText.at (i);
                if (!ch.isLetterOrNumber())
                {
                    prefix += ch;
                    ++i;
                }
                else break;
            }
            /* still check if a letter or number follows */
            if (i < curBlockPos)
            {
                QChar c = blockText.at (i);
                if (c.isLetter())
                {
                    if (i + 1 < curBlockPos
                        && !prefix.isEmpty() && !prefix.at (prefix.size() - 1).isSpace()
                        && blockText.at (i + 1).isSpace())
                    { // non-letter and non-space character -> singlle letter -> space
                        prefix = blockText.left (i + 2);
                        QChar cc = QChar (c.unicode() + 1);
                        if (cc.isLetter()) prefix.replace (c, cc);
                    }
                    else if (i + 2 < curBlockPos
                             && !blockText.at (i + 1).isLetterOrNumber() && !blockText.at (i + 1).isSpace()
                             && blockText.at (i + 2).isSpace())
                    { // singlle letter -> non-letter and non-space character -> space
                        prefix = blockText.left (i + 3);
                        QChar cc = QChar (c.unicode() + 1);
                        if (cc.isLetter()) prefix.replace (c, cc);
                    }
                }
                else if (c.isNumber())
                { // making lists with numbers
                    QString num;
                    while (i < curBlockPos)
                    {
                        QChar ch = blockText.at (i);
                        if (ch.isNumber())
                        {
                            num += ch;
                            ++i;
                        }
                        else
                        {
                            if (!num.isEmpty())
                            {
                                QChar ch = blockText.at (i);
                                if (ch.isSpace())
                                { // non-letter and non-space character -> number -> space
                                    if (!prefix.isEmpty() && !prefix.at (prefix.size() - 1).isSpace())
                                        num = locale().toString (locale().toInt (num) + 1) + ch;
                                    else num = QString();
                                }
                                else if (i + 1 < curBlockPos
                                         && !ch.isLetterOrNumber() && !ch.isSpace()
                                         && blockText.at (i + 1).isSpace())
                                { // number -> non-letter and non-space character -> space
                                    num = locale().toString (locale().toInt (num) + 1) + ch + blockText.at (i + 1);
                                }
                                else num = QString();
                            }
                            break;
                        }
                    }
                    if (i < curBlockPos) // otherwise, it'll be just a number
                        prefix += num;
                }
            }
        }
        else
        {
            /* find the indentation */
            if (autoIndentation)
                indent = computeIndentation (cur);
            /* check whether a bracketed text is selected
               so that the cursor position is at its start */
            QTextCursor anchorCur = cur;
            anchorCur.setPosition (cur.anchor());
            if (autoBracket
                && cur.position() == cur.selectionStart()
                && !cur.atBlockStart() && !anchorCur.atBlockEnd())
            {
                cur.setPosition (cur.position());
                cur.movePosition (QTextCursor::PreviousCharacter);
                cur.movePosition (QTextCursor::NextCharacter,
                                  QTextCursor::KeepAnchor,
                                  selTxt.size() + 2);
                QString selTxt1 = cur.selectedText();
                if (selTxt1 == "{" + selTxt + "}" || selTxt1 == "(" + selTxt + ")")
                    isBracketed = true;
                cur = textCursor(); // reset the current cursor
            }
        }

        if (withShift || autoIndentation || isBracketed)
        {
            cur.beginEditBlock();
            /* first press Enter normally... */
            cur.insertText (QChar (QChar::ParagraphSeparator));
            /* ... then, insert indentation... */
            cur.insertText (indent);
            /* ... and handle Shift+Enter or brackets */
            if (withShift)
                cur.insertText (prefix);
            else if (isBracketed)
            {
                cur.movePosition (QTextCursor::PreviousBlock);
                cur.movePosition (QTextCursor::EndOfBlock);
                int start = -1;
                QStringList lines = selTxt.split (QChar (QChar::ParagraphSeparator));
                if (lines.size() == 1)
                {
                    cur.insertText (QChar (QChar::ParagraphSeparator));
                    cur.insertText (indent);
                    start = cur.position();
                    if (!isOnlySpaces (lines. at (0)))
                        cur.insertText (lines. at (0));
                }
                else // multi-line
                {
                    for (int i = 0; i < lines.size(); ++i)
                    {
                        if (i == 0 && isOnlySpaces (lines. at (0)))
                            continue;
                        cur.insertText (QChar (QChar::ParagraphSeparator));
                        if (i == 0)
                        {
                            cur.insertText (indent);
                            start = cur.position();
                        }
                        else if (i == 1 && start == -1)
                            start = cur.position(); // the first line was only spaces
                        cur.insertText (lines. at (i));
                    }
                }
                cur.setPosition (start, start >= cur.block().position()
                                            ? QTextCursor::MoveAnchor
                                            : QTextCursor::KeepAnchor);
                setTextCursor (cur);
            }
            cur.endEditBlock();
            ensureCursorVisible();
            event->accept();
            return;
        }
    }
    else if (event->key() == Qt::Key_ParenLeft
             || event->key() == Qt::Key_BraceLeft
             || event->key() == Qt::Key_BracketLeft
             || event->key() == Qt::Key_QuoteDbl)
    {
        if (autoBracket)
        {
            QTextCursor cursor = textCursor();
            bool autoB (false);
            if (!cursor.hasSelection())
            {
                if (cursor.atBlockEnd())
                    autoB = true;
                else
                {
                    QTextCursor tmp = cursor;
                    tmp.movePosition (QTextCursor::NextCharacter, QTextCursor::KeepAnchor);
                    if (!tmp.selectedText().at (0).isLetterOrNumber())
                        autoB = true;
                }
            }
            else if (cursor.position() == cursor.selectionStart())
                autoB = true;
            if (autoB)
            {
                int pos = cursor.position();
                int anch = cursor.anchor();
                cursor.beginEditBlock();
                cursor.setPosition (anch);
                if (event->key() == Qt::Key_ParenLeft)
                {
                    cursor.insertText (")");
                    cursor.setPosition (pos);
                    cursor.insertText ("(");
                }
                else if (event->key() == Qt::Key_BraceLeft)
                {
                    cursor.insertText ("}");
                    cursor.setPosition (pos);
                    cursor.insertText ("{");
                }
                else if (event->key() == Qt::Key_BracketLeft)
                {
                    cursor.insertText ("]");
                    cursor.setPosition (pos);
                    cursor.insertText ("[");
                }
                else// if (event->key() == Qt::Key_QuoteDbl)
                {
                    cursor.insertText ("\"");
                    cursor.setPosition (pos);
                    cursor.insertText ("\"");
                }
                /* select the text and set the cursor at its start */
                cursor.setPosition (anch + 1, QTextCursor::MoveAnchor);
                cursor.setPosition (pos + 1, QTextCursor::KeepAnchor);
                cursor.endEditBlock();
                /* WARNING: Why does putting "setTextCursor()" before "endEditBlock()"
                            cause a crash with huge lines? Most probably, a Qt bug. */
                setTextCursor (cursor);
                event->accept();
                return;
            }
        }
    }
    else if (event->key() == Qt::Key_Left || event->key() == Qt::Key_Right)
    {
        /* when text is selected, use arrow keys
           to go to the start or end of the selection */
        QTextCursor cursor = textCursor();
        if (event->modifiers() == Qt::NoModifier && cursor.hasSelection())
        {
            QString str = cursor.selectedText();
            if (event->key() == Qt::Key_Left)
            {
                if (str.isRightToLeft())
                    cursor.setPosition (cursor.selectionEnd());
                else
                    cursor.setPosition (cursor.selectionStart());
            }
            else
            {
                if (str.isRightToLeft())
                    cursor.setPosition(cursor.selectionStart());
                else
                    cursor.setPosition(cursor.selectionEnd());
            }
            cursor.clearSelection();
            setTextCursor (cursor);
            event->accept();
            return;
        }
    }
    else if (event->key() == Qt::Key_Tab)
    {
        QTextCursor cursor = textCursor();
        int newLines = cursor.selectedText().count (QChar (QChar::ParagraphSeparator));
        if (newLines > 0)
        {
            cursor.beginEditBlock();
            cursor.setPosition (qMin (cursor.anchor(), cursor.position())); // go to the first block
            cursor.movePosition (QTextCursor::StartOfBlock);
            for (int i = 0; i <= newLines; ++i)
            {
                /* skip all spaces to align the real text */
                int indx = 0;
                QRegularExpressionMatch match;
                if (cursor.block().text().indexOf (QRegularExpression (R"(^\s+)"), 0, &match) > -1)
                    indx = match.capturedLength();
                cursor.setPosition (cursor.block().position() + indx);
                if (event->modifiers() & Qt::ControlModifier)
                {
                    cursor.insertText (remainingSpaces (event->modifiers() & Qt::MetaModifier
                                                        ? "  " : textTab_, cursor));
                }
                else
                    cursor.insertText ("\t");
                if (!cursor.movePosition (QTextCursor::NextBlock))
                    break; // not needed
            }
            cursor.endEditBlock();
            event->accept();
            return;
        }
        else if (event->modifiers() & Qt::ControlModifier)
        {
            QTextCursor tmp (cursor);
            tmp.setPosition (qMin (tmp.anchor(), tmp.position()));
            cursor.insertText (remainingSpaces (event->modifiers() & Qt::MetaModifier
                                                ? "  " : textTab_, tmp));
            event->accept();
            return;
        }
    }
    else if (event->key() == Qt::Key_Backtab)
    {
        QTextCursor cursor = textCursor();
        int newLines = cursor.selectedText().count (QChar (QChar::ParagraphSeparator));
        cursor.setPosition (qMin (cursor.anchor(), cursor.position()));
        cursor.beginEditBlock();
        cursor.movePosition (QTextCursor::StartOfBlock);
        for (int i = 0; i <= newLines; ++i)
        {
            if (cursor.atBlockEnd())
            {
                if (!cursor.movePosition (QTextCursor::NextBlock))
                    break; // not needed
                continue;
            }
            cursor = backTabCursor (cursor, event->modifiers() & Qt::MetaModifier
                                            ? true : false);
            cursor.removeSelectedText();
            if (!cursor.movePosition (QTextCursor::NextBlock))
                break; // not needed
        }
        cursor.endEditBlock();

        /* otherwise, do nothing with SHIFT+TAB */
        event->accept();
        return;
    }
    else if (event->key() == Qt::Key_Insert)
    {
        if (event->modifiers() == Qt::NoModifier || event->modifiers() == Qt::KeypadModifier)
        {
            setOverwriteMode (!overwriteMode());
            if (!overwriteMode())
            {
                setCursorWidth (1);
                update(); // otherwise, a part of the thick cursor might remain
            }
            else
                setCursorWidth (QFontMetrics(font()).averageCharWidth());
            event->accept();
            return;
        }

    }
    /* because of a bug in Qt5, the non-breaking space (ZWNJ) isn't inserted with SHIFT+SPACE */
    else if (event->key() == 0x200c)
    {
        insertPlainText (QChar (0x200C));
        event->accept();
        return;
    }
    else if (event->key() == Qt::Key_Space)
    {
        if (autoReplace && event->modifiers() == Qt::NoModifier)
        {
            QTextCursor cur = textCursor();
            if (!cur.hasSelection())
            {
                const int p = cur.positionInBlock();
                if (p > 1)
                {
                    cur.beginEditBlock();
                    cur.movePosition (QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor, 2);
                    const QString selTxt = cur.selectedText();
                    if (!selTxt.endsWith ("."))
                    {
                        if (selTxt == "--")
                        {
                            QTextCursor prevCur = cur;
                            prevCur.setPosition (cur.position());
                            prevCur.movePosition (QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor);
                            if (prevCur.selectedText() != "-")
                                cur.insertText ("—");
                        }
                        else if (selTxt == "->")
                            cur.insertText ("→");
                        else if (selTxt == "<-")
                            cur.insertText ("←");
                        else if (selTxt == ">=")
                            cur.insertText ("≥");
                        else if (selTxt == "<=")
                            cur.insertText ("≤");
                    }
                    else if (p > 2)
                    {
                        cur = textCursor();
                        cur.movePosition (QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor, 3);
                        const QString selTxt = cur.selectedText();
                        if (selTxt == "...")
                        {
                            QTextCursor prevCur = cur;
                            prevCur.setPosition (cur.position());
                            if (p > 3)
                            {
                                prevCur.movePosition (QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor);
                                if (prevCur.selectedText() != ".")
                                    cur.insertText ("…");
                            }
                            else cur.insertText ("…");
                        }
                    }
                    cur.endEditBlock();
                }
            }
        }
    }
    else if (event->key() == Qt::Key_Home)
    {
        if (!(event->modifiers() & Qt::ControlModifier))
        { // Qt's default behavior isn't acceptable
            QTextCursor cur = textCursor();
            int p = cur.positionInBlock();
            int indx = 0;
            QRegularExpressionMatch match;
            if (cur.block().text().indexOf (QRegularExpression ("^\\s+"), 0, &match) > -1)
                indx = match.capturedLength();
            if (p > 0)
            {
                if (p <= indx) p = 0;
                else p = indx;
            }
            else p = indx;
            cur.setPosition (p + cur.block().position(),
                             event->modifiers() & Qt::ShiftModifier ? QTextCursor::KeepAnchor
                                                                    : QTextCursor::MoveAnchor);
            setTextCursor (cur);
            ensureCursorVisible();
            event->accept();
            return;
        }
    }

    QTextEdit::keyPressEvent (event);
}
/*************************/
QString TextEdit::computeIndentation (const QTextCursor& cur) const
{
    QTextCursor cusror = cur;
    if (cusror.hasSelection())
    {// this is more intuitive to me
        if (cusror.anchor() <= cusror.position())
            cusror.setPosition (cusror.anchor());
        else
            cusror.setPosition (cusror.position());
    }
    QTextCursor tmp = cusror;
    tmp.movePosition (QTextCursor::StartOfBlock);
    QString str;
    if (tmp.atBlockEnd())
        return str;
    int pos = tmp.position();
    tmp.setPosition (++pos, QTextCursor::KeepAnchor);
    QString selected;
    while (!tmp.atBlockEnd()
           && tmp <= cusror
           && ((selected = tmp.selectedText()) == " "
               || (selected = tmp.selectedText()) == "\t"))
    {
        str.append (selected);
        tmp.setPosition (pos);
        tmp.setPosition (++pos, QTextCursor::KeepAnchor);
    }
    if (tmp.atBlockEnd()
        && tmp <= cusror
        && ((selected = tmp.selectedText()) == " "
            || (selected = tmp.selectedText()) == "\t"))
    {
        str.append (selected);
    }
    return str;
}
/*************************/
// Emit resized().
void TextEdit::resizeEvent (QResizeEvent *e)
{
    QTextEdit::resizeEvent (e);
    emit resized();
}
/*************************/
// Show links as tooltips.
bool TextEdit::event (QEvent *e)
{
    if (e->type() == QEvent::ToolTip)
    {
        QHelpEvent *helpEvent = static_cast<QHelpEvent *>(e);
        QString str = anchorAt (helpEvent->pos());
        if (!str.isEmpty())
            QToolTip::showText (helpEvent->globalPos(),
                                "<p style='white-space:pre'>" + str + "</p>");
        else
        {
            QToolTip::hideText();
            e->ignore();
        }
        return true;
    }

    return QTextEdit::event (e);
}
/*************************/
bool TextEdit::canInsertFromMimeData (const QMimeData *source) const
{
    if (source->hasImage() || source->hasUrls())
        return true;
    else
        return QTextEdit::canInsertFromMimeData (source);
}
/*************************/
void TextEdit::insertFromMimeData (const QMimeData *source)
{
    if (source->hasImage())
    {
        QImage image = qvariant_cast<QImage>(source->imageData());
        if (!image.isNull())
        {
            QByteArray rawarray;
            QBuffer buffer (&rawarray);
            buffer.open (QIODevice::WriteOnly);
            image.save (&buffer, "PNG");
            buffer.close();

            insertHtml (QString ("<img src=\"data:image;base64,%1\" />")
                        .arg (QString (rawarray.toBase64())));
        }
    }
    else if (source->hasUrls())
    {
        const auto urls = source->urls();
        for (const QUrl &url : urls)
        {
            QMimeDatabase mimeDatabase;
            QMimeType mimeType = mimeDatabase.mimeTypeForFile (QFileInfo (url.toLocalFile()));
            QByteArray ba = mimeType.name().toUtf8();
            if (QImageReader::supportedMimeTypes().contains (ba))
                emit imageDropped (url.path());
            else
            {
                if (url.fileName().endsWith (".fnx")
                    || mimeType.name() == "text/feathernotes-fnx")
                {
                    emit FNDocDropped (url.path());
                    return; // only open the first file
                }
                else
                    textCursor().insertText (url.toString());
            }
        }
    }
    else
        QTextEdit::insertFromMimeData (source);
}
/*************************/
void TextEdit::mouseMoveEvent (QMouseEvent *e)
{
    QTextEdit::mouseMoveEvent (e);

    QString str = anchorAt (e->pos());
    if (!str.isEmpty())
        viewport()->setCursor (Qt::PointingHandCursor);
    else
        viewport()->setCursor (Qt::IBeamCursor);
}
/*************************/
void TextEdit::mousePressEvent (QMouseEvent *e)
{
    /* On triple clicking, select the current block without
       selecting its newline and start and end whitespaces, if any. */
    if (tripleClickTimer_.isValid())
    {
        if (!tripleClickTimer_.hasExpired (qApp->doubleClickInterval())
            && e->buttons() == Qt::LeftButton)
        {
            tripleClickTimer_.invalidate();
            QTextCursor txtCur = textCursor();
            const QString blockText = txtCur.block().text();
            const int l = blockText.length();
            txtCur.movePosition (QTextCursor::StartOfBlock);
            int i = 0;
            while (i < l && blockText.at (i).isSpace())
                ++i;
            /* WARNING: QTextCursor::movePosition() can be a mess with RTL
                        but QTextCursor::setPosition() works fine. */
            if (i < l)
            {
                txtCur.setPosition (txtCur.position() + i);
                int j = l;
                while (j > i && blockText.at (j -  1).isSpace())
                    --j;
                txtCur.setPosition (txtCur.position() + j - i, QTextCursor::KeepAnchor);
            }
            else
                txtCur.setPosition (txtCur.position() + i, QTextCursor::KeepAnchor);
            setTextCursor (txtCur);
            return;
        }
        tripleClickTimer_.invalidate();
    }

    QTextEdit::mousePressEvent (e);
    pressPoint = e->pos();
}

/*************************/
void TextEdit::mouseReleaseEvent (QMouseEvent *e)
{
    QTextEdit::mouseReleaseEvent (e);

    /* give only a plain text to the selection clipboard */
    QTextCursor cursor = textCursor();
    if (cursor.hasSelection())
    {
        QClipboard *cl = QApplication::clipboard();
        if (cl->supportsSelection())
            cl->setText (cursor.selection().toPlainText(), QClipboard::Selection);
    }

    QString str = anchorAt (e->pos());
    if (!str.isEmpty()
        && cursorForPosition (e->pos()) == cursorForPosition (pressPoint))
    {
        QUrl url (str);
        if (url.isRelative()) // treat relative URLs as local paths
            url = QUrl::fromUserInput (str, "/", QUrl::AssumeLocalFile);
        /* QDesktopServices::openUrl() may resort to "xdg-open", which isn't
           the best choice. "gio" is always reliable, so we check it first. */
        if (!QProcess::startDetached ("gio", QStringList() << "open" << url.toString()))
            QDesktopServices::openUrl (url);
    }
    pressPoint = QPoint();
}
/*************************/
void TextEdit::mouseDoubleClickEvent (QMouseEvent *e)
{
    tripleClickTimer_.start();
    QTextEdit::mouseDoubleClickEvent (e);
}
/*************************/
void TextEdit::wheelEvent (QWheelEvent *e)
{
    if (e->modifiers() & Qt::ControlModifier)
    {
        float delta = e->angleDelta().y() / 120.f;
        zooming (delta);
        return;
    }

    /* smooth scrolling */
    if (e->spontaneous() && e->source() == Qt::MouseEventNotSynthesized)
    {
#if (QT_VERSION >= QT_VERSION_CHECK(5,12,0))
        bool horizontal (e->angleDelta().x() != 0);
#else
        bool horizontal (e->orientation() == Qt::Horizontal);
#endif
        QScrollBar* sbar = horizontal
                               ? horizontalScrollBar() : verticalScrollBar();
        if (sbar)
        {
            int delta = horizontal
                            ? e->angleDelta().x() : e->angleDelta().y();
            if (e->modifiers() & Qt::ShiftModifier) // scrolling with minimum speed
                delta /= QApplication::wheelScrollLines();
            if ((delta > 0 && sbar->value() == sbar->minimum())
                || (delta < 0 && sbar->value() == sbar->maximum()))
            {
                return; // the scrollbar can't move
            }

            if (!scrollTimer_)
            {
                scrollTimer_ = new QTimer();
                scrollTimer_->setTimerType (Qt::PreciseTimer);
                connect (scrollTimer_, &QTimer::timeout, this, &TextEdit::scrollSmoothly);
            }

            /* set the data for inertial scrolling */
            scrollData data;
            data.delta = delta;
            data.leftFrames = scrollAnimFrames;
            data.vertical = !horizontal;
            queuedScrollSteps_.append (data);
            scrollTimer_->start (1000 / SCROLL_FRAMES_PER_SEC);
            return;
        }
    }

    /* as in QTextEdit::wheelEvent() */
    QAbstractScrollArea::wheelEvent (e);
    updateMicroFocus();
}
/*************************/
void TextEdit::scrollSmoothly()
{
    int totalDeltaH = 0, totalDeltaV = 0;
    QList<scrollData>::iterator it = queuedScrollSteps_.begin();
    while (it != queuedScrollSteps_.end())
    {
        int delta = qRound (static_cast<qreal>(it->delta) / static_cast<qreal>(scrollAnimFrames));
        int remainingDelta = it->delta - (scrollAnimFrames - it->leftFrames) * delta;
        if (qAbs (delta) >= qAbs (remainingDelta))
        { // this is the last frame or, due to rounding, there can be no more frame
            if (it->vertical)
                totalDeltaV += remainingDelta;
            else
                totalDeltaH += remainingDelta;
            it = queuedScrollSteps_.erase (it);
        }
        else
        {
            if (it->vertical)
                totalDeltaV += delta;
            else
                totalDeltaH += delta;
            -- it->leftFrames;
            ++it;
        }
    }

    if (totalDeltaH != 0 && horizontalScrollBar())
    {
#if (QT_VERSION >= QT_VERSION_CHECK(5,12,0))
        QWheelEvent eventH (QPointF(),
                            QPointF(),
                            QPoint(),
#if (QT_VERSION == QT_VERSION_CHECK(5,14,0))
                            QPoint (totalDeltaH, 0),
#else
                            QPoint (0, totalDeltaH),
#endif
                            Qt::NoButton,
                            Qt::NoModifier,
                            Qt::NoScrollPhase,
                            false);
#else
        QWheelEvent eventH (QPointF(),
                            QPointF(),
                            totalDeltaH,
                            Qt::NoButton,
                            Qt::NoModifier,
                            Qt::Vertical);
#endif
        QCoreApplication::sendEvent (horizontalScrollBar(), &eventH);
    }
    if (totalDeltaV != 0 && verticalScrollBar())
    {
#if (QT_VERSION >= QT_VERSION_CHECK(5,12,0))
        QWheelEvent eventV (QPointF(),
                            QPointF(),
                            QPoint(),
                            QPoint (0, totalDeltaV),
                            Qt::NoButton,
                            Qt::NoModifier,
                            Qt::NoScrollPhase,
                            false);
#else
        QWheelEvent eventV (QPointF(),
                            QPointF(),
                            totalDeltaV,
                            Qt::NoButton,
                            Qt::NoModifier,
                            Qt::Vertical);
#endif
        QCoreApplication::sendEvent (verticalScrollBar(), &eventV);
    }

    /* update text selection if the left mouse button is pressed (-> QPlainTextEdit::timerEvent) */
    if (QApplication::mouseButtons() & Qt::LeftButton)
    {
        const QPoint globalPos = QCursor::pos();
        QPoint pos = viewport()->mapFromGlobal (globalPos);
        QMouseEvent ev (QEvent::MouseMove, pos, viewport()->mapTo (viewport()->topLevelWidget(), pos), globalPos,
                        Qt::LeftButton, Qt::LeftButton, Qt::NoModifier);
        mouseMoveEvent (&ev);
    }

    if (queuedScrollSteps_.empty())
        scrollTimer_->stop();
}
/*************************/
void TextEdit::zooming (float range)
{
    if (range == 0.f) return;
    QFont f = document()->defaultFont();
    const float newSize = static_cast<float>(f.pointSizeF()) + range;
    if (newSize <= 0) return;
    f.setPointSizeF (static_cast<qreal>(newSize));
    setFont (f);
#if (QT_VERSION >= QT_VERSION_CHECK(5,11,0))
    QFontMetricsF metrics (f);
    setTabStopDistance (4 * metrics.horizontalAdvance (' '));
#elif (QT_VERSION >= QT_VERSION_CHECK(5,10,0))
    QFontMetricsF metrics (f);
    setTabStopDistance (4 * metrics.width (' '));
#else
    QFontMetrics metrics (f);
    setTabStopWidth (4 * metrics.width (' '));
#endif

    /* if this is a zoom-out, the text will need
       to be formatted and/or highlighted again */
    if (range < 0) emit zoomedOut (this);
}
/*************************/
void TextEdit::undo()
{
    QTextEdit::undo();
    /* This is a workaround for a weird Qt bug: If some text is pasted at the document start,
       the pasting is undone and then, some text is pasted at the document start again, the
       text color of the document stylesheet will be ignored. */
    if (CSSTextColor.isValid())
    {
        QTextCursor cur = textCursor();
        if (cur.atStart() && cur.atEnd())
        {
            QTextCharFormat fmt;
            fmt.setForeground (CSSTextColor);
            cur.setBlockCharFormat (fmt);
        }
    }
}

}
