class MarkdownWindow {
    Q_OBJECT
public:
    explicit MarkdownWindowManager(InkCanvas *canvas, QObject *parent = nullptr);
    ~MarkdownWindowManager() {
    clearAllWindows();
}
    MarkdownWindow* createMarkdownWindow(const QRect &rect);
    void removeMarkdownWindow(MarkdownWindow *window) {
    if (!window) return;
    currentWindows.removeAll(window);
    for (auto it = pageWindows.begin(); it != pageWindows.end(); ++it) {
        it.value().removeAll(window);
    }
    emit windowRemoved(window);
    window->deleteLater();
}
    void clearAllWindows() {
    transparencyTimer->stop();
    currentlyFocusedWindow = nullptr;
    windowsAreTransparent = false;
    for (MarkdownWindow *window : currentWindows) {
        window->deleteLater();
    }
    currentWindows.clear();
    for (auto it = pageWindows.begin(); it != pageWindows.end(); ++it) {
        for (MarkdownWindow *window : it.value()) {
            window->deleteLater();
        }
    }
    pageWindows.clear();
}
    void saveWindowsForPage(int pageNumber) {
    if (!canvas || currentWindows.isEmpty()) return;
    pageWindows[pageNumber] = currentWindows;
    saveWindowData(pageNumber, currentWindows);
}
    void loadWindowsForPage(int pageNumber) {
    if (!canvas) return;
    QList<MarkdownWindow*> newPageWindows;
    if (pageWindows.contains(pageNumber)) {
        newPageWindows = pageWindows[pageNumber];
    } else {
        newPageWindows = loadWindowData(pageNumber);
        if (!newPageWindows.isEmpty()) {
            pageWindows[pageNumber] = newPageWindows;
        }
    }
    currentWindows = newPageWindows;
    for (MarkdownWindow *window : currentWindows) {
        if (!window->isValidForCanvas()) {
            QRect canvasBounds = canvas->getCanvasRect();
            QRect windowRect = window->getCanvasRect();
            int newX = qMax(0, qMin(windowRect.x(), canvasBounds.width() - windowRect.width()));
            int newY = qMax(0, qMin(windowRect.y(), canvasBounds.height() - windowRect.height()));
            if (newX != windowRect.x() || newY != windowRect.y()) {
                QRect adjustedRect(newX, newY, windowRect.width(), windowRect.height());
                window->setCanvasRect(adjustedRect);
            }
        }
        window->ensureCanvasConnections();
        window->show();
        window->updateScreenPosition();
        window->setTransparent(false);
    }
    if (!currentWindows.isEmpty()) {
        resetTransparencyTimer();
    } else {
    }
}
    void deleteWindowsForPage(int pageNumber) {
    if (pageWindows.contains(pageNumber)) {
        QList<MarkdownWindow*> windows = pageWindows[pageNumber];
        for (MarkdownWindow *window : windows) {
            window->deleteLater();
        }
        pageWindows.remove(pageNumber);
    }
    QString filePath = getWindowDataFilePath(pageNumber);
    if (QFile::exists(filePath)) {
        QFile::remove(filePath);
    }
    if (pageWindows.value(pageNumber) == currentWindows) {
        currentWindows.clear();
    }
}
    void setSelectionMode(bool enabled) {
    selectionMode = enabled;
    if (canvas) {
        canvas->setCursor(enabled ? Qt::CrossCursor : Qt::ArrowCursor);
    }
}
    QList<MarkdownWindow*> getCurrentPageWindows() const {
    return currentWindows;
}
    void resetTransparencyTimer() {
    transparencyTimer->stop();
    windowsAreTransparent = false;
    transparencyTimer->start();
}
    void setWindowsTransparent(bool transparent) {
    if (windowsAreTransparent == transparent) return;
    windowsAreTransparent = transparent;
    for (MarkdownWindow *window : currentWindows) {
        if (transparent) {
            if (window != currentlyFocusedWindow) {
                window->setTransparent(true);
            } else {
                window->setTransparent(false);
            }
        } else {
            window->setTransparent(false);
        }
    }
}
    void hideAllWindows() {
    transparencyTimer->stop();
    currentlyFocusedWindow = nullptr;
    windowsAreTransparent = false;
    for (MarkdownWindow *window : currentWindows) {
        window->hide();
        window->setTransparent(false);
    }
}
    void windowCreated(MarkdownWindow *window);
    void windowRemoved(MarkdownWindow *window);
  private:
    void onWindowDeleteRequested(MarkdownWindow *window) {
    removeMarkdownWindow(window);
    if (canvas) {
        canvas->setEdited(true);
    }
    if (canvas) {
        MainWindow *mainWindow = qobject_cast<MainWindow*>(canvas->parent());
        if (mainWindow) {
            int currentPage = mainWindow->getCurrentPageForCanvas(canvas);
            saveWindowsForPage(currentPage);
        }
    }
}
    void onWindowFocusChanged(MarkdownWindow *window, bool focused) {
    if (focused) {
        currentlyFocusedWindow = window;
        window->setTransparent(false);
        resetTransparencyTimer();
    } else {
        if (currentlyFocusedWindow == window) {
            currentlyFocusedWindow = nullptr;
        }
        bool anyWindowFocused = false;
        for (MarkdownWindow *w : currentWindows) {
            if (w->isEditorFocused()) {
                anyWindowFocused = true;
                currentlyFocusedWindow = w;
                w->setTransparent(false);
                break;
            }
        }
        if (!anyWindowFocused) {
            resetTransparencyTimer();
        } else {
            resetTransparencyTimer();
        }
    }
}
    void onWindowContentChanged(MarkdownWindow *window) {
    currentlyFocusedWindow = window;
    window->setTransparent(false);
    resetTransparencyTimer();
}
    void onTransparencyTimerTimeout() {
    setWindowsTransparent(true);
}
  private:
    InkCanvas *canvas;
    bool selectionMode = false;
    QMap<int, QList<MarkdownWindow*>> pageWindows;
    QList<MarkdownWindow*> currentWindows;
    QTimer *transparencyTimer;
    MarkdownWindow *currentlyFocusedWindow = nullptr;
    bool windowsAreTransparent = false;
    QString getWindowDataFilePath(int pageNumber) const {
    QString saveFolder = getSaveFolder();
    QString notebookId = getNotebookId();
    if (saveFolder.isEmpty() || notebookId.isEmpty()) {
        return QString();
    }
    return saveFolder + QString("/.%1_markdown_%2.json").arg(notebookId).arg(pageNumber, 5, 10, QChar('0'));
}
    void saveWindowData(int pageNumber, const QList<MarkdownWindow*> &windows) {
    QString filePath = getWindowDataFilePath(pageNumber);
    if (filePath.isEmpty()) return;
    QJsonArray windowsArray;
    for (MarkdownWindow *window : windows) {
        QVariantMap windowData = window->serialize();
        windowsArray.append(QJsonObject::fromVariantMap(windowData));
    }
    QJsonDocument doc(windowsArray);
    QFile file(filePath);
    if (file.open(QIODevice::WriteOnly)) {
        file.write(doc.toJson());
        file.close();
        #ifdef Q_OS_WIN
        SetFileAttributesW(reinterpret_cast<const wchar_t *>(filePath.utf16()), FILE_ATTRIBUTE_HIDDEN);
        #endif
    }
}
    QList<MarkdownWindow*> loadWindowData(int pageNumber) {
    QList<MarkdownWindow*> windows;
    QString filePath = getWindowDataFilePath(pageNumber);
    if (filePath.isEmpty() || !QFile::exists(filePath)) {
        return windows;
    }
    QFile file(filePath);
    if (!file.open(QIODevice::ReadOnly)) {
        return windows;
    }
    QByteArray data = file.readAll();
    file.close();
    QJsonParseError error;
    QJsonDocument doc = QJsonDocument::fromJson(data, &error);
    if (error.error != QJsonParseError::NoError) {
        qWarning() << "Failed to parse markdown window data:" << error.errorString();
        return windows;
    }
    QJsonArray windowsArray = doc.array();
    for (const QJsonValue &value : windowsArray) {
        QJsonObject windowObj = value.toObject();
        QVariantMap windowData = windowObj.toVariantMap();
        MarkdownWindow *window = new MarkdownWindow(QRect(0, 0, 300, 200), canvas);
        window->deserialize(windowData);
        connectWindowSignals(window);
        windows.append(window);
        window->show();
    }
    return windows;
}
    QString getSaveFolder() const {
    return canvas ? canvas->getSaveFolder() : QString();
}
    QString getNotebookId() const {
    if (!canvas) return QString();
    QString saveFolder = canvas->getSaveFolder();
    if (saveFolder.isEmpty()) return QString();
    QString idFile = saveFolder + "/.notebook_id.txt";
    if (QFile::exists(idFile)) {
        QFile file(idFile);
        if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
            QTextStream in(&file);
            QString notebookId = in.readLine().trimmed();
            file.close();
            return notebookId;
        }
    }
    return "notebook";
}
    QRect convertScreenToCanvasRect(const QRect &screenRect) const {
    if (!canvas) {
        return screenRect;
    }
    QRect canvasRect = canvas->mapWidgetToCanvas(screenRect);
    return canvasRect;
}
    void connectWindowSignals(MarkdownWindow *window) {
    connect(window, &MarkdownWindow::deleteRequested, this, &MarkdownWindowManager::onWindowDeleteRequested);
    connect(window, &MarkdownWindow::contentChanged, this, [this, window]() {
        onWindowContentChanged(window);
        if (canvas) {
            canvas->setEdited(true);
            MainWindow *mainWindow = qobject_cast<MainWindow*>(canvas->parent());
            if (mainWindow) {
                int currentPage = mainWindow->getCurrentPageForCanvas(canvas);
                saveWindowsForPage(currentPage);
            }
        }
    });
    connect(window, &MarkdownWindow::windowMoved, this, [this, window](MarkdownWindow*) {
        currentlyFocusedWindow = window;
        window->setTransparent(false);
        resetTransparencyTimer();
        if (canvas) {
            canvas->setEdited(true);
        }
    });
    connect(window, &MarkdownWindow::windowResized, this, [this, window](MarkdownWindow*) {
        currentlyFocusedWindow = window;
        window->setTransparent(false);
        resetTransparencyTimer();
        if (canvas) {
            canvas->setEdited(true);
        }
    });
    connect(window, &MarkdownWindow::focusChanged, this, &MarkdownWindowManager::onWindowFocusChanged);
    connect(window, &MarkdownWindow::editorFocusChanged, this, &MarkdownWindowManager::onWindowFocusChanged);
    connect(window, &MarkdownWindow::windowInteracted, this, [this, window](MarkdownWindow*) {
        currentlyFocusedWindow = window;
        window->setTransparent(false);
        resetTransparencyTimer();
    });
}
  public:
    void updateAllWindowPositions() {
    for (MarkdownWindow *window : currentWindows) {
        if (window) {
            window->updateScreenPosition();
        }
    }
}
}

class LineNumArea {
    Q_OBJECT
    Q_PROPERTY(
        bool highlighting READ highlightingEnabled WRITE setHighlightingEnabled);
    friend class LineNumArea;
  public:
    enum AutoTextOption {
        None = 0x0000,

        // inserts closing characters for brackets and Markdown characters
        BracketClosing = 0x0001,

        // removes matching brackets and Markdown characters
        BracketRemoval = 0x0002
    };
    Q_DECLARE_FLAGS(AutoTextOptions, AutoTextOption);
    explicit QMarkdownTextEdit(QWidget *parent = nullptr,
                               bool initHighlighter = true) {
    installEventFilter(this);
    viewport()->installEventFilter(this);
    _autoTextOptions = AutoTextOption::BracketClosing;

    _lineNumArea = new LineNumArea(this);
    updateLineNumberAreaWidth(0);

    // Markdown highlighting is enabled by default
    _highlightingEnabled = initHighlighter;
    if (initHighlighter) {
        _highlighter = new MarkdownHighlighter(document());
    }

    QFont font = this->font();

    // set the tab stop to the width of 4 spaces in the editor
    constexpr int tabStop = 4;
    QFontMetrics metrics(font);

#if QT_VERSION < QT_VERSION_CHECK(5, 11, 0)
    setTabStopWidth(tabStop * metrics.width(' '));
#else
    setTabStopDistance(tabStop * metrics.horizontalAdvance(QLatin1Char(' ')));
#endif

    // add shortcuts for duplicating text
    //    new QShortcut( QKeySequence( "Ctrl+D" ), this, SLOT( duplicateText() )
    //    ); new QShortcut( QKeySequence( "Ctrl+Alt+Down" ), this, SLOT(
    //    duplicateText() ) );

    // add a layout to the widget
    auto *layout = new QVBoxLayout(this);
    layout->setContentsMargins(0, 0, 0, 0);
    layout->addStretch();
    this->setLayout(layout);

    // add the hidden search widget
    _searchWidget = new QPlainTextEditSearchWidget(this);
    this->layout()->addWidget(_searchWidget);

    connect(this, &QPlainTextEdit::textChanged, this,
            &QMarkdownTextEdit::adjustRightMargin);
    connect(this, &QPlainTextEdit::cursorPositionChanged, this,
            &QMarkdownTextEdit::centerTheCursor);
    connect(verticalScrollBar(), &QScrollBar::valueChanged, this,
            [this](int) { _lineNumArea->update(); });
    connect(this, &QPlainTextEdit::cursorPositionChanged, this, [this]() {
        _lineNumArea->update();

        auto oldArea = blockBoundingGeometry(_textCursor.block())
                           .translated(contentOffset());
        _textCursor = textCursor();
        auto newArea = blockBoundingGeometry(_textCursor.block())
                           .translated(contentOffset());
        auto areaToUpdate = oldArea | newArea;
        viewport()->update(areaToUpdate.toRect());
    });
    connect(document(), &QTextDocument::blockCountChanged, this,
            &QMarkdownTextEdit::updateLineNumberAreaWidth);
    connect(this, &QPlainTextEdit::updateRequest, this,
            &QMarkdownTextEdit::updateLineNumberArea);

    updateSettings();

    // workaround for disabled signals up initialization
    QTimer::singleShot(300, this, &QMarkdownTextEdit::adjustRightMargin);
}
    MarkdownHighlighter *highlighter();
    QPlainTextEditSearchWidget *searchWidget();
    void setIgnoredClickUrlSchemata(QStringList ignoredUrlSchemata) {
    _ignoredClickUrlSchemata = std::move(ignoredUrlSchemata);
}
    virtual void openUrl(const QString &urlString) {
    qDebug() << "QMarkdownTextEdit " << __func__
             << " - 'urlString': " << urlString;

    QDesktopServices::openUrl(QUrl(urlString));
}
    QString getMarkdownUrlAtPosition(const QString &text, int position) {
    QString url;

    // get a map of parsed Markdown urls with their link texts as key
    const QMap<QString, QString> urlMap = parseMarkdownUrlsFromText(text);
    QMap<QString, QString>::const_iterator i = urlMap.constBegin();
    for (; i != urlMap.constEnd(); ++i) {
        const QString &linkText = i.key();
        const QString &urlString = i.value();

        const int foundPositionStart = text.indexOf(linkText);

        if (foundPositionStart >= 0) {
            // calculate end position of found linkText
            const int foundPositionEnd = foundPositionStart + linkText.size();

            // check if position is in found string range
            if ((position >= foundPositionStart) &&
                (position <= foundPositionEnd)) {
                url = urlString;
                break;
            }
        }
    }

    return url;
}
    void initSearchFrame(QWidget *searchFrame, bool darkMode = false) {
    _searchFrame = searchFrame;

    // remove the search widget from our layout
    layout()->removeWidget(_searchWidget);

    QLayout *layout = _searchFrame->layout();

    // create a grid layout for the frame and add the search widget to it
    if (layout == nullptr) {
        layout = new QVBoxLayout(_searchFrame);
        layout->setSpacing(0);
        layout->setContentsMargins(0, 0, 0, 0);
    }

    _searchWidget->setDarkMode(darkMode);
    layout->addWidget(_searchWidget);
    _searchFrame->setLayout(layout);
}
    void setAutoTextOptions(AutoTextOptions options) {
    _autoTextOptions = options;
}
    static bool isValidUrl(const QString &urlString) {
    static QRegularExpression regex(R"(^\w+:\/\/.+)");
    const QRegularExpressionMatch match = regex.match(urlString);
    return match.hasMatch();
}
    void resetMouseCursor() const {
    QWidget *viewPort = viewport();
    viewPort->setCursor(Qt::IBeamCursor);
}
    void setReadOnly(bool ro) {
    QPlainTextEdit::setReadOnly(ro);

    // attempted to fix a problem with Chinese and Japanese input methods
    // @see https://github.com/pbek/QOwnNotes/issues/976
    setAttribute(Qt::WA_InputMethodEnabled, !isReadOnly());
}
    void doSearch(QString &searchText,
                  QPlainTextEditSearchWidget::SearchMode searchMode =
                      QPlainTextEditSearchWidget::SearchMode::PlainTextMode) {
    _searchWidget->setSearchText(searchText);
    _searchWidget->setSearchMode(searchMode);
    _searchWidget->doSearchCount();
    _searchWidget->activate(false);
}
    void hideSearchWidget(bool reset) {
    _searchWidget->deactivate();

    if (reset) {
        _searchWidget->reset();
    }
}
    void updateSettings() {
    // if true: centers the screen if cursor reaches bottom (but not top)
    searchWidget()->setDebounceDelay(_debounceDelay);
    setCenterOnScroll(_centerCursor);
}
    void setLineNumbersCurrentLineColor(QColor color) {
    _lineNumArea->setCurrentLineColor(std::move(color));
}
    void setLineNumbersOtherLineColor(QColor color) {
    _lineNumArea->setOtherLineColor(std::move(color));
}
    void setSearchWidgetDebounceDelay(uint debounceDelay) {
    _debounceDelay = debounceDelay;
    searchWidget()->setDebounceDelay(_debounceDelay);
}
    void setHighlightingEnabled(bool enabled) {
    if (_highlightingEnabled == enabled || _highlighter == nullptr) {
        return;
    }

    _highlightingEnabled = enabled;
    _highlighter->setDocument(enabled ? document() : Q_NULLPTR);

    if (enabled) {
        _highlighter->rehighlight();
    }
}
    [[nodiscard]] bool highlightingEnabled() const {
    return _highlightingEnabled && _highlighter != nullptr;
}
    void setHighlightCurrentLine(bool set) {
    _highlightCurrentLine = set;
}
    bool highlightCurrentLine() { return _highlightCurrentLine; }
    void setCurrentLineHighlightColor(const QColor &c) {
    _currentLineHighlightColor = color;
}
    QColor currentLineHighlightColor() {
    return _currentLineHighlightColor;
}
  public:
    void duplicateText() {
    if (isReadOnly()) {
        return;
    }

    QTextCursor cursor = this->textCursor();
    QString selectedText = cursor.selectedText();

    // duplicate line if no text was selected
    if (selectedText.isEmpty()) {
        const int position = cursor.position();

        // select the whole line
        cursor.movePosition(QTextCursor::StartOfBlock);
        cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);

        const int positionDiff = cursor.position() - position;
        selectedText = "\n" + cursor.selectedText();

        // insert text with new line at end of the selected line
        cursor.setPosition(cursor.selectionEnd());
        cursor.insertText(selectedText);

        // set the position to same position it was in the duplicated line
        cursor.setPosition(cursor.position() - positionDiff);
    } else {
        // duplicate selected text
        cursor.setPosition(cursor.selectionEnd());
        const int selectionStart = cursor.position();

        // insert selected text
        cursor.insertText(selectedText);
        const int selectionEnd = cursor.position();

        // select the inserted text
        cursor.setPosition(selectionStart);
        cursor.setPosition(selectionEnd, QTextCursor::KeepAnchor);
    }

    this->setTextCursor(cursor);
}
    void setText(const QString &text) { setPlainText(text); }
    void setPlainText(const QString &text) {
    // clear the dirty blocks vector to increase performance and prevent
    // a possible crash in QSyntaxHighlighter::rehighlightBlock
    if (_highlighter) _highlighter->clearDirtyBlocks();

    QPlainTextEdit::setPlainText(text);
    adjustRightMargin();
}
    void adjustRightMargin() {
    QMargins margins = layout()->contentsMargins();
    const int rightMargin =
        document()->size().height() > viewport()->size().height() ? 24 : 0;
    margins.setRight(rightMargin);
    layout()->setContentsMargins(margins);
}
    void hide() {
    _searchWidget->hide();
    QWidget::hide();
}
    bool openLinkAtCursorPosition() {
    QTextCursor cursor = this->textCursor();
    const int clickedPosition = cursor.position();

    // select the text in the clicked block and find out on
    // which position we clicked
    cursor.movePosition(QTextCursor::StartOfBlock);
    const int positionFromStart = clickedPosition - cursor.position();
    cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);

    const QString selectedText = cursor.selectedText();

    // find out which url in the selected text was clicked
    const QString urlString =
        getMarkdownUrlAtPosition(selectedText, positionFromStart);
    const QUrl url = QUrl(urlString);
    const bool isRelativeFileUrl =
        urlString.startsWith(QLatin1String("file://.."));
    const bool isLegacyAttachmentUrl =
        urlString.startsWith(QLatin1String("file://attachments"));

    qDebug() << __func__ << " - 'emit urlClicked( urlString )': " << urlString;

    Q_EMIT urlClicked(urlString);

    if ((url.isValid() && isValidUrl(urlString)) || isRelativeFileUrl ||
        isLegacyAttachmentUrl) {
        // ignore some schemata
        if (!(_ignoredClickUrlSchemata.contains(url.scheme()) ||
              isRelativeFileUrl || isLegacyAttachmentUrl)) {
            // open the url
            openUrl(urlString);
        }

        return true;
    }

    return false;
}
    bool handleBackspaceEntered() {
    if (!(_autoTextOptions & AutoTextOption::BracketRemoval) || isReadOnly()) {
        return false;
    }

    QTextCursor cursor = textCursor();

    // return if some text was selected
    if (!cursor.selectedText().isEmpty()) {
        return false;
    }

    int position = cursor.position();
    const int positionInBlock = cursor.positionInBlock();
    int block = cursor.block().blockNumber();

    if (_highlighter)
        if (_highlighter->isPosInACodeSpan(block, positionInBlock - 1))
            return false;

    // return if backspace was pressed at the beginning of a block
    if (positionInBlock == 0) {
        return false;
    }

    // get the current text from the block
    const QString text = cursor.block().text();

    char charToRemove{};

    // current char
    const char charInFront = text.at(positionInBlock - 1).toLatin1();

    if (charInFront == '*')
        return handleCharRemoval(MarkdownHighlighter::RangeType::Emphasis,
                                 block, positionInBlock - 1);
    else if (charInFront == '`')
        return handleCharRemoval(MarkdownHighlighter::RangeType::CodeSpan,
                                 block, positionInBlock - 1);

    // handle removal of ", ', and brackets

    // is it opener?
    int pos = _openingCharacters.indexOf(charInFront);
    // for " and '
    bool isOpener = false;
    bool isCloser = false;
    if (pos == 5 || pos == 6) {
        isOpener = isQuotOpener(positionInBlock - 1, text);
    } else {
        isOpener = pos != -1;
    }
    if (isOpener) {
        charToRemove = _closingCharacters.at(pos);
    } else {
        // is it closer?
        pos = _closingCharacters.indexOf(charInFront);
        if (pos == 5 || pos == 6)
            isCloser = isQuotCloser(positionInBlock - 1, text);
        else
            isCloser = pos != -1;
        if (isCloser)
            charToRemove = _openingCharacters.at(pos);
        else
            return false;
    }

    int charToRemoveIndex = -1;
    if (isOpener) {
        bool closer = true;
        charToRemoveIndex = text.indexOf(charToRemove, positionInBlock);
        if (charToRemoveIndex == -1) return false;
        if (pos == 5 || pos == 6)
            closer = isQuotCloser(charToRemoveIndex, text);
        if (!closer) return false;
        cursor.setPosition(position + (charToRemoveIndex - positionInBlock));
        cursor.deleteChar();
    } else if (isCloser) {
        charToRemoveIndex = text.lastIndexOf(charToRemove, positionInBlock - 2);
        if (charToRemoveIndex == -1) return false;
        bool opener = true;
        if (pos == 5 || pos == 6)
            opener = isQuotOpener(charToRemoveIndex, text);
        if (!opener) return false;
        const int pos = position - (positionInBlock - charToRemoveIndex);
        cursor.setPosition(pos);
        cursor.deleteChar();
        position -= 1;
    } else {
        charToRemoveIndex = text.lastIndexOf(charToRemove, positionInBlock - 2);
        if (charToRemoveIndex == -1) return false;
        const int pos = position - (positionInBlock - charToRemoveIndex);
        cursor.setPosition(pos);
        cursor.deleteChar();
        position -= 1;
    }

    // moving the cursor back to the old position so the previous character
    // can be removed
    cursor.setPosition(position);
    setTextCursor(cursor);
    return false;
}
    void centerTheCursor() {
    if (_mouseButtonDown || !_centerCursor) {
        return;
    }

    // Centers the cursor every time, but not on the top and bottom,
    // bottom is done by setCenterOnScroll() in updateSettings()
    centerCursor();

    /*
        QRect cursor = cursorRect();
        QRect vp = viewport()->rect();

        qDebug() << __func__ << " - 'cursor.top': " << cursor.top();
        qDebug() << __func__ << " - 'cursor.bottom': " << cursor.bottom();
        qDebug() << __func__ << " - 'vp': " << vp.bottom();

        int bottom = 0;
        int top = 0;

        qDebug() << __func__ << " - 'viewportMargins().top()': "
                 << viewportMargins().top();

        qDebug() << __func__ << " - 'viewportMargins().bottom()': "
                 << viewportMargins().bottom();

        int vpBottom = viewportMargins().top() + viewportMargins().bottom() +
    vp.bottom(); int vpCenter = vpBottom / 2; int cBottom = cursor.bottom() +
    viewportMargins().top();

        qDebug() << __func__ << " - 'vpBottom': " << vpBottom;
        qDebug() << __func__ << " - 'vpCenter': " << vpCenter;
        qDebug() << __func__ << " - 'cBottom': " << cBottom;

        if (cBottom >= vpCenter) {
            bottom = cBottom + viewportMargins().top() / 2 +
    viewportMargins().bottom() / 2 - (vp.bottom() / 2);
    //        bottom = cBottom - (vp.bottom() / 2);
    //        bottom *= 1.5;
        }

    //    setStyleSheet(QString("QPlainTextEdit {padding-bottom:
    %1px;}").arg(QString::number(bottom)));

    //    if (cursor.top() < (vp.bottom() / 2)) {
    //        top = (vp.bottom() / 2) - cursor.top() + viewportMargins().top() /
    2 + viewportMargins().bottom() / 2;
    ////        top *= -1;
    ////        bottom *= 1.5;
    //    }
        qDebug() << __func__ << " - 'top': " << top;
        qDebug() << __func__ << " - 'bottom': " << bottom;
        setViewportMargins(0,top,0, bottom);

    //    QScrollBar* scrollbar = verticalScrollBar();
    //
    //    qDebug() << __func__ << " - 'scrollbar->value();': " <<
    scrollbar->value();;
    //    qDebug() << __func__ << " - 'scrollbar->maximum();': "
    //             << scrollbar->maximum();;

    //    scrollbar->setValue(scrollbar->value() - offset.y());
    //
    //    setViewportMargins

    //    setViewportMargins(0, 0, 0, bottom);
    */
}
    void undo() {
    if (isReadOnly()) {
        return;
    }

    QTextCursor cursor = textCursor();
    // if no text selected, call undo
    if (!cursor.hasSelection()) {
        QPlainTextEdit::undo();
        return;
    }

    // if text is selected and bracket closing was used
    // we retain our selection
    if (_handleBracketClosingUsed) {
        // get the selection
        int selectionEnd = cursor.selectionEnd();
        int selectionStart = cursor.selectionStart();
        // call undo
        QPlainTextEdit::undo();
        // select again
        cursor.setPosition(selectionStart - 1);
        cursor.setPosition(selectionEnd - 1, QTextCursor::KeepAnchor);
        this->setTextCursor(cursor);
        _handleBracketClosingUsed = false;
    } else {
        // if text was selected but bracket closing wasn't used
        // do normal undo
        QPlainTextEdit::undo();
        return;
    }
}
    void moveTextUpDown(bool up) {
    if (isReadOnly()) {
        return;
    }

    QTextCursor cursor = textCursor();
    QTextCursor move = cursor;

    move.setVisualNavigation(false);

    move.beginEditBlock();    // open an edit block to keep undo operations sane
    bool hasSelection = cursor.hasSelection();

    if (hasSelection) {
        // if there's a selection inside the block, we select the whole block
        move.setPosition(cursor.selectionStart());
        move.movePosition(QTextCursor::StartOfBlock);
        move.setPosition(cursor.selectionEnd(), QTextCursor::KeepAnchor);
        move.movePosition(
            move.atBlockStart() ? QTextCursor::Left : QTextCursor::EndOfBlock,
            QTextCursor::KeepAnchor);
    } else {
        move.movePosition(QTextCursor::StartOfBlock);
        move.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
    }

    // get the text of the current block
    QString text = move.selectedText();

    move.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor);
    move.removeSelectedText();

    if (up) {    // up key
        move.movePosition(QTextCursor::PreviousBlock);
        move.insertBlock();
        move.movePosition(QTextCursor::Left);
    } else {    // down key
        move.movePosition(QTextCursor::EndOfBlock);
        if (move.atBlockStart()) {    // empty block
            move.movePosition(QTextCursor::NextBlock);
            move.insertBlock();
            move.movePosition(QTextCursor::Left);
        } else {
            move.insertBlock();
        }
    }

    int start = move.position();
    move.clearSelection();
    move.insertText(text);
    int end = move.position();

    // reselect
    if (hasSelection) {
        move.setPosition(end);
        move.setPosition(start, QTextCursor::KeepAnchor);
    } else {
        move.setPosition(start);
    }

    move.endEditBlock();

    setTextCursor(move);
}
    void setLineNumberEnabled(bool enabled) {
    _lineNumArea->setLineNumAreaEnabled(enabled);
    updateLineNumberAreaWidth(0);
}
  protected:
    QTextCursor _textCursor;
    MarkdownHighlighter *_highlighter = nullptr;
    bool _highlightingEnabled;
    QStringList _ignoredClickUrlSchemata;
    QPlainTextEditSearchWidget *_searchWidget;
    QWidget *_searchFrame;
    AutoTextOptions _autoTextOptions;
    bool _mouseButtonDown = false;
    bool _centerCursor = false;
    bool _highlightCurrentLine = false;
    QColor _currentLineHighlightColor = QColor();
    uint _debounceDelay = 0;
    bool eventFilter(QObject *obj, QEvent *event) override {
    // qDebug() << event->type();
    if (event->type() == QEvent::HoverMove) {
        auto *mouseEvent = static_cast<QMouseEvent *>(event);

        QWidget *viewPort = this->viewport();
        // toggle cursor when control key has been pressed or released
        viewPort->setCursor(
            mouseEvent->modifiers().testFlag(Qt::ControlModifier)
                ? Qt::PointingHandCursor
                : Qt::IBeamCursor);
    } else if (event->type() == QEvent::KeyPress) {
        auto *keyEvent = static_cast<QKeyEvent *>(event);

        // set cursor to pointing hand if control key was pressed
        if (keyEvent->modifiers().testFlag(Qt::ControlModifier)) {
            QWidget *viewPort = this->viewport();
            viewPort->setCursor(Qt::PointingHandCursor);
        }

        // disallow keys if text edit hasn't focus
        if (!this->hasFocus()) {
            return true;
        }

        if ((keyEvent->key() == Qt::Key_Escape) && _searchWidget->isVisible()) {
            _searchWidget->deactivate();
            return true;
        } else if (keyEvent->key() == Qt::Key_Insert &&
                   keyEvent->modifiers().testFlag(Qt::NoModifier)) {
            setOverwriteMode(!overwriteMode());

            // This solves a UI glitch if the visual cursor was not properly
            // updated when characters have different widths
            QTextCursor cursor = this->textCursor();
            cursor.movePosition(QTextCursor::Right);
            setTextCursor(cursor);
            cursor.movePosition(QTextCursor::Left);
            setTextCursor(cursor);

            return false;
        } else if ((keyEvent->key() == Qt::Key_Tab) ||
                   (keyEvent->key() == Qt::Key_Backtab)) {
            // handle entered tab and reverse tab keys
            return handleTabEntered(keyEvent->key() == Qt::Key_Backtab);
        } else if ((keyEvent->key() == Qt::Key_F) &&
                   keyEvent->modifiers().testFlag(Qt::ControlModifier)) {
            _searchWidget->activate();
            return true;
        } else if ((keyEvent->key() == Qt::Key_R) &&
                   keyEvent->modifiers().testFlag(Qt::ControlModifier)) {
            _searchWidget->activateReplace();
            return true;
            //        } else if (keyEvent->key() == Qt::Key_Delete) {
        } else if (keyEvent->key() == Qt::Key_Backspace) {
            return handleBackspaceEntered();
        } else if (keyEvent->key() == Qt::Key_Asterisk) {
            return handleBracketClosing(QLatin1Char('*'));
        } else if (keyEvent->key() == Qt::Key_QuoteDbl) {
            return quotationMarkCheck(QLatin1Char('"'));
            // apostrophe bracket closing is temporary disabled because
            // apostrophes are used in different contexts
            //        } else if (keyEvent->key() == Qt::Key_Apostrophe) {
            //            return handleBracketClosing("'");
            // underline bracket closing is temporary disabled because
            // underlines are used in different contexts
            //        } else if (keyEvent->key() == Qt::Key_Underscore) {
            //            return handleBracketClosing("_");
        } else if (keyEvent->key() == Qt::Key_QuoteLeft) {
            return quotationMarkCheck(QLatin1Char('`'));
        } else if (keyEvent->key() == Qt::Key_AsciiTilde) {
            return handleBracketClosing(QLatin1Char('~'));
#ifdef Q_OS_MAC
        } else if (keyEvent->modifiers().testFlag(Qt::AltModifier) &&
                   keyEvent->key() == Qt::Key_ParenLeft) {
            // bracket closing for US keyboard on macOS
            return handleBracketClosing(QLatin1Char('{'), QLatin1Char('}'));
#endif
        } else if (keyEvent->key() == Qt::Key_ParenLeft) {
            return handleBracketClosing(QLatin1Char('('), QLatin1Char(')'));
        } else if (keyEvent->key() == Qt::Key_BraceLeft) {
            return handleBracketClosing(QLatin1Char('{'), QLatin1Char('}'));
        } else if (keyEvent->key() == Qt::Key_BracketLeft) {
            return handleBracketClosing(QLatin1Char('['), QLatin1Char(']'));
        } else if (keyEvent->key() == Qt::Key_Less) {
            return handleBracketClosing(QLatin1Char('<'), QLatin1Char('>'));
#ifdef Q_OS_MAC
        } else if (keyEvent->modifiers().testFlag(Qt::AltModifier) &&
                   keyEvent->key() == Qt::Key_ParenRight) {
            // bracket closing for US keyboard on macOS
            return bracketClosingCheck(QLatin1Char('{'), QLatin1Char('}'));
#endif
        } else if (keyEvent->key() == Qt::Key_ParenRight) {
            return bracketClosingCheck(QLatin1Char('('), QLatin1Char(')'));
        } else if (keyEvent->key() == Qt::Key_BraceRight) {
            return bracketClosingCheck(QLatin1Char('{'), QLatin1Char('}'));
        } else if (keyEvent->key() == Qt::Key_BracketRight) {
            return bracketClosingCheck(QLatin1Char('['), QLatin1Char(']'));
        } else if (keyEvent->key() == Qt::Key_Greater) {
            return bracketClosingCheck(QLatin1Char('<'), QLatin1Char('>'));
        } else if ((keyEvent->key() == Qt::Key_Return ||
                    keyEvent->key() == Qt::Key_Enter) &&
                   !isReadOnly() &&
                   keyEvent->modifiers().testFlag(Qt::ShiftModifier)) {
            QTextCursor cursor = this->textCursor();
            cursor.insertText("  \n");
            return true;
        } else if ((keyEvent->key() == Qt::Key_Return ||
                    keyEvent->key() == Qt::Key_Enter) &&
                   !isReadOnly() &&
                   keyEvent->modifiers().testFlag(Qt::ControlModifier)) {
            QTextCursor cursor = this->textCursor();
            cursor.movePosition(QTextCursor::EndOfBlock);
            cursor.insertText(QStringLiteral("\n"));
            setTextCursor(cursor);
            return true;
        } else if (keyEvent == QKeySequence::Copy ||
                   keyEvent == QKeySequence::Cut) {
            QTextCursor cursor = this->textCursor();
            if (!cursor.hasSelection()) {
                QString text;
                if (cursor.block().length() <= 1)    // no content
                    text = "\n";
                else {
                    // cursor.select(QTextCursor::BlockUnderCursor); //
                    // negative, it will include the previous paragraph
                    // separator
                    cursor.movePosition(QTextCursor::StartOfBlock);
                    cursor.movePosition(QTextCursor::EndOfBlock,
                                        QTextCursor::KeepAnchor);
                    text = cursor.selectedText();
                    if (!cursor.atEnd()) {
                        text += "\n";
                        // this is the paragraph separator
                        cursor.movePosition(QTextCursor::NextCharacter,
                                            QTextCursor::KeepAnchor, 1);
                    }
                }
                if (keyEvent == QKeySequence::Cut) {
                    if (!cursor.atEnd() && text == "\n")
                        cursor.deletePreviousChar();
                    else
                        cursor.removeSelectedText();
                    cursor.movePosition(QTextCursor::StartOfBlock);
                    setTextCursor(cursor);
                }
                qApp->clipboard()->setText(text);
                return true;
            }
        } else if ((keyEvent->key() == Qt::Key_Down) &&
                   keyEvent->modifiers().testFlag(Qt::ControlModifier) &&
                   keyEvent->modifiers().testFlag(Qt::AltModifier)) {
            // duplicate text with `Ctrl + Alt + Down`
            duplicateText();
            return true;
#ifndef Q_OS_MAC
        } else if ((keyEvent->key() == Qt::Key_Down) &&
                   keyEvent->modifiers().testFlag(Qt::ControlModifier) &&
                   !keyEvent->modifiers().testFlag(Qt::ShiftModifier)) {
            // scroll the page down
            auto *scrollBar = verticalScrollBar();
            scrollBar->setSliderPosition(scrollBar->sliderPosition() + 1);
            return true;
        } else if ((keyEvent->key() == Qt::Key_Up) &&
                   keyEvent->modifiers().testFlag(Qt::ControlModifier) &&
                   !keyEvent->modifiers().testFlag(Qt::ShiftModifier)) {
            // scroll the page up
            auto *scrollBar = verticalScrollBar();
            scrollBar->setSliderPosition(scrollBar->sliderPosition() - 1);
            return true;
#endif
        } else if ((keyEvent->key() == Qt::Key_Down) &&
                   keyEvent->modifiers().testFlag(Qt::NoModifier)) {
            // if you are in the last line and press cursor down the cursor will
            // jump to the end of the line
            QTextCursor cursor = textCursor();
            if (cursor.position() >= document()->lastBlock().position()) {
                cursor.movePosition(QTextCursor::EndOfLine);

                // check if we are really in the last line, not only in
                // the last block
                if (cursor.atBlockEnd()) {
                    setTextCursor(cursor);
                }
            }
            return QPlainTextEdit::eventFilter(obj, event);
        } else if ((keyEvent->key() == Qt::Key_Up) &&
                   keyEvent->modifiers().testFlag(Qt::NoModifier)) {
            // if you are in the first line and press cursor up the cursor will
            // jump to the start of the line
            QTextCursor cursor = textCursor();
            QTextBlock block = document()->firstBlock();
            int endOfFirstLinePos = block.position() + block.length();

            if (cursor.position() <= endOfFirstLinePos) {
                cursor.movePosition(QTextCursor::StartOfLine);

                // check if we are really in the first line, not only in
                // the first block
                if (cursor.atBlockStart()) {
                    setTextCursor(cursor);
                }
            }
            return QPlainTextEdit::eventFilter(obj, event);
        } else if (keyEvent->key() == Qt::Key_Return ||
                   keyEvent->key() == Qt::Key_Enter) {
            return handleReturnEntered();
        } else if ((keyEvent->key() == Qt::Key_F3)) {
            _searchWidget->doSearch(
                !keyEvent->modifiers().testFlag(Qt::ShiftModifier));
            return true;
        } else if ((keyEvent->key() == Qt::Key_Z) &&
                   (keyEvent->modifiers().testFlag(Qt::ControlModifier)) &&
                   !(keyEvent->modifiers().testFlag(Qt::ShiftModifier))) {
            undo();
            return true;
        } else if ((keyEvent->key() == Qt::Key_Down) &&
                   (keyEvent->modifiers().testFlag(Qt::ControlModifier)) &&
                   (keyEvent->modifiers().testFlag(Qt::ShiftModifier))) {
            moveTextUpDown(false);
            return true;
        } else if ((keyEvent->key() == Qt::Key_Up) &&
                   (keyEvent->modifiers().testFlag(Qt::ControlModifier)) &&
                   (keyEvent->modifiers().testFlag(Qt::ShiftModifier))) {
            moveTextUpDown(true);
            return true;
#ifdef Q_OS_MAC
            // https://github.com/pbek/QOwnNotes/issues/1593
            // https://github.com/pbek/QOwnNotes/issues/2643
        } else if (keyEvent->key() == Qt::Key_Home) {
            QTextCursor cursor = textCursor();
            // Meta is Control on macOS
            cursor.movePosition(
                keyEvent->modifiers().testFlag(Qt::MetaModifier)
                    ? QTextCursor::Start
                    : QTextCursor::StartOfLine,
                keyEvent->modifiers().testFlag(Qt::ShiftModifier)
                    ? QTextCursor::KeepAnchor
                    : QTextCursor::MoveAnchor);
            this->setTextCursor(cursor);
            return true;
        } else if (keyEvent->key() == Qt::Key_End) {
            QTextCursor cursor = textCursor();
            // Meta is Control on macOS
            cursor.movePosition(
                keyEvent->modifiers().testFlag(Qt::MetaModifier)
                    ? QTextCursor::End
                    : QTextCursor::EndOfLine,
                keyEvent->modifiers().testFlag(Qt::ShiftModifier)
                    ? QTextCursor::KeepAnchor
                    : QTextCursor::MoveAnchor);
            this->setTextCursor(cursor);
            return true;
#endif
        }

        return QPlainTextEdit::eventFilter(obj, event);
    } else if (event->type() == QEvent::KeyRelease) {
        auto *keyEvent = static_cast<QKeyEvent *>(event);

        // reset cursor if control key was released
        if (keyEvent->key() == Qt::Key_Control) {
            resetMouseCursor();
        }

        return QPlainTextEdit::eventFilter(obj, event);
    } else if (event->type() == QEvent::MouseButtonRelease) {
        _mouseButtonDown = false;
        auto *mouseEvent = static_cast<QMouseEvent *>(event);

        // track `Ctrl + Click` in the text edit
        if ((obj == this->viewport()) &&
            (mouseEvent->button() == Qt::LeftButton) &&
            (QGuiApplication::keyboardModifiers() == Qt::ExtraButton24)) {
            // open the link (if any) at the current position
            // in the noteTextEdit
            openLinkAtCursorPosition();
            return true;
        }
    } else if (event->type() == QEvent::MouseButtonPress) {
        _mouseButtonDown = true;
    } else if (event->type() == QEvent::MouseButtonDblClick) {
        _mouseButtonDown = true;
    } else if (event->type() == QEvent::Wheel) {
        auto *wheel = dynamic_cast<QWheelEvent *>(event);

        // emit zoom signals
        if (wheel->modifiers() == Qt::ControlModifier) {
            if (wheel->angleDelta().y() > 0) {
                Q_EMIT zoomIn();
            } else {
                Q_EMIT zoomOut();
            }

            return true;
        }
    }

    return QPlainTextEdit::eventFilter(obj, event);
}
    QMargins viewportMargins() {
    return QPlainTextEdit::viewportMargins();
}
    bool increaseSelectedTextIndention(
        bool reverse,
        const QString &indentCharacters = QChar::fromLatin1('\t')) {
    QTextCursor cursor = this->textCursor();
    QString selectedText = cursor.selectedText();

    if (!selectedText.isEmpty()) {
        // Start the selection at start of the first block of the selection
        int end = cursor.selectionEnd();
        cursor.setPosition(cursor.selectionStart());
        cursor.movePosition(QTextCursor::StartOfBlock, QTextCursor::MoveAnchor);
        cursor.setPosition(end, QTextCursor::KeepAnchor);
        this->setTextCursor(cursor);
        selectedText = cursor.selectedText();

        // we need this strange newline character we are getting in the
        // selected text for newlines
        const QString newLine =
            QString::fromUtf8(QByteArray::fromHex(QByteArrayLiteral("e280a9")));
        QString newText;

        if (reverse) {
            // un-indent text

            const int indentSize = indentCharacters == QStringLiteral("\t")
                                       ? 4
                                       : indentCharacters.length();

            // remove leading \t or spaces in following lines
            newText = selectedText.replace(
                QRegularExpression(newLine + QStringLiteral("(\\t| {1,") +
                                   QString::number(indentSize) +
                                   QStringLiteral("})")),
                QStringLiteral("\n"));

            // remove leading \t or spaces in first line
            newText.remove(QRegularExpression(QStringLiteral("^(\\t| {1,") +
                                              QString::number(indentSize) +
                                              QStringLiteral("})")));
        } else {
            // replace trailing new line to prevent an indent of the line after
            // the selection
            newText = selectedText.replace(
                QRegularExpression(QRegularExpression::escape(newLine) +
                                   QStringLiteral("$")),
                QStringLiteral("\n"));

            // indent text
            newText.replace(newLine, QStringLiteral("\n") + indentCharacters)
                .prepend(indentCharacters);

            // remove trailing \t
            static QRegularExpression regex1(QStringLiteral("\\t$"));
            newText.remove(regex1);
        }

        // insert the new text
        cursor.insertText(newText);

        // update the selection to the new text
        cursor.setPosition(cursor.position() - newText.size(),
                           QTextCursor::KeepAnchor);
        this->setTextCursor(cursor);

        return true;
    } else if (reverse) {
        const int indentSize = indentCharacters.length();

        // do the check as often as we have characters to un-indent
        for (int i = 1; i <= indentSize; i++) {
            // if nothing was selected but we want to reverse the indention
            // check if there is a \t in front or after the cursor and remove it
            // if so
            const int position = cursor.position();

            if (!cursor.atStart()) {
                // get character in front of cursor
                cursor.setPosition(position - 1, QTextCursor::KeepAnchor);
            }

            // check for \t or space in front of cursor
            static QRegularExpression regex1(QStringLiteral("[\\t ]"));
            QRegularExpressionMatch match = regex1.match(cursor.selectedText());

            if (!match.hasMatch()) {
                // (select to) check for \t or space after the cursor
                cursor.setPosition(position);

                if (!cursor.atEnd()) {
                    cursor.setPosition(position + 1, QTextCursor::KeepAnchor);
                }
            }

            match = regex1.match(cursor.selectedText());

            if (match.hasMatch()) {
                cursor.removeSelectedText();
            }

            cursor = this->textCursor();
        }

        return true;
    }

    // else just insert indentCharacters
    cursor.insertText(indentCharacters);

    return true;
}
    bool handleTabEntered(bool reverse, const QString &indentCharacters =
                                            QChar::fromLatin1('\t')) {
    if (isReadOnly()) {
        return true;
    }

    QTextCursor cursor = this->textCursor();

    // only check for lists if we haven't a text selected
    if (cursor.selectedText().isEmpty()) {
        cursor.movePosition(QTextCursor::StartOfBlock, QTextCursor::KeepAnchor);
        const QString currentLineText = cursor.selectedText();

        // check if we want to indent or un-indent an ordered list
        // Valid listCharacters: '+ ', '-' , '* ', '+ [ ] ', '+ [x] ', '- [ ] ',
        // '- [x] ', '- [-] ', '* [ ] ', '* [x] '.
        static QRegularExpression regex1(
            R"(^(\s*)([+|\-|\*] \[(x|-| )\]|[+\-\*])(\s+)$)");
        QRegularExpressionMatchIterator i = regex1.globalMatch(currentLineText);

        if (i.hasNext()) {
            QRegularExpressionMatch match = i.next();
            QString whitespaces = match.captured(1);
            const QString listCharacter = match.captured(2);
            const QString whitespaceCharacter = match.captured(4);

            // add or remove one tabulator key
            if (reverse) {
                // remove one set of indentCharacters or a tabulator
                whitespaces.remove(QRegularExpression(
                    QStringLiteral("^(\\t|") +
                    QRegularExpression::escape(indentCharacters) +
                    QStringLiteral(")")));

            } else {
                whitespaces += indentCharacters;
            }

            cursor.insertText(whitespaces + listCharacter +
                              whitespaceCharacter);
            return true;
        }

        // check if we want to indent or un-indent an ordered list
        static QRegularExpression regex2(R"(^(\s*)(\d+)([\.|\)])(\s+)$)");
        i = regex2.globalMatch(currentLineText);

        if (i.hasNext()) {
            const QRegularExpressionMatch match = i.next();
            QString whitespaces = match.captured(1);
            const QString listCharacter = match.captured(2);
            const QString listMarker = match.captured(3);
            const QString whitespaceCharacter = match.captured(4);

            // add or remove one tabulator key
            if (reverse) {
                whitespaces.chop(1);
            } else {
                whitespaces += indentCharacters;
            }

            cursor.insertText(whitespaces + listCharacter + listMarker +
                              whitespaceCharacter);
            return true;
        }
    }

    // check if we want to indent the whole text
    return increaseSelectedTextIndention(reverse, indentCharacters);
}
    QMap<QString, QString> parseMarkdownUrlsFromText(const QString &text) {
    QMap<QString, QString> urlMap;
    QRegularExpressionMatchIterator iterator;

    // match urls like this: <http://mylink>
    //    re = QRegularExpression("(<(.+?:\\/\\/.+?)>)");
    static QRegularExpression regex1(QStringLiteral("(<(.+?)>)"));
    iterator = regex1.globalMatch(text);
    while (iterator.hasNext()) {
        QRegularExpressionMatch match = iterator.next();
        QString linkText = match.captured(1);
        QString url = match.captured(2);
        urlMap[linkText] = url;
    }

    // match urls like this: [this url](http://mylink)
    //    QRegularExpression re("(\\[.*?\\]\\((.+?:\\/\\/.+?)\\))");
    static QRegularExpression regex2(R"((\[.*?\]\((.+?)\)))");
    iterator = regex2.globalMatch(text);
    while (iterator.hasNext()) {
        QRegularExpressionMatch match = iterator.next();
        QString linkText = match.captured(1);
        QString url = match.captured(2);
        urlMap[linkText] = url;
    }

    // match urls like this: http://mylink
    static QRegularExpression regex3(R"(\b\w+?:\/\/[^\s]+[^\s>\)])");
    iterator = regex3.globalMatch(text);
    while (iterator.hasNext()) {
        QRegularExpressionMatch match = iterator.next();
        QString url = match.captured(0);
        urlMap[url] = url;
    }

    // match urls like this: www.github.com
    static QRegularExpression regex4(R"(\bwww\.[^\s]+\.[^\s]+\b)");
    iterator = regex4.globalMatch(text);
    while (iterator.hasNext()) {
        QRegularExpressionMatch match = iterator.next();
        QString url = match.captured(0);
        urlMap[url] = QStringLiteral("http://") + url;
    }

    // match reference urls like this: [this url][1] with this later:
    // [1]: http://domain
    static QRegularExpression regex5(R"((\[.*?\]\[(.+?)\]))");
    iterator = regex5.globalMatch(text);
    while (iterator.hasNext()) {
        QRegularExpressionMatch match = iterator.next();
        QString linkText = match.captured(1);
        QString referenceId = match.captured(2);

        // search for the referenced url in the whole text edit
        //        QRegularExpression refRegExp(
        //                "\\[" + QRegularExpression::escape(referenceId) +
        //                        "\\]: (.+?:\\/\\/.+)");
        QRegularExpression refRegExp(QStringLiteral("\\[") +
                                     QRegularExpression::escape(referenceId) +
                                     QStringLiteral("\\]: (.+)"));
        QRegularExpressionMatch urlMatch = refRegExp.match(toPlainText());

        if (urlMatch.hasMatch()) {
            QString url = urlMatch.captured(1);
            urlMap[linkText] = url;
        }
    }

    return urlMap;
}
    bool handleReturnEntered() {
    if (isReadOnly()) {
        return true;
    }

    // This will be the main cursor to add or remove text
    QTextCursor cursor = this->textCursor();

    // We need a 2nd cursor to get the text of the current block without moving
    // the main cursor that is used to remove the selected text
    QTextCursor cursor2 = this->textCursor();
    cursor2.select(QTextCursor::BlockUnderCursor);
    const QString currentLineText = cursor2.selectedText().trimmed();

    const int position = cursor.position();
    const bool cursorAtBlockStart = cursor.atBlockStart();
    cursor.movePosition(QTextCursor::StartOfBlock, QTextCursor::KeepAnchor);
    const QString currentLinePartialText = cursor.selectedText();

    // if return is pressed and there is just an unordered list symbol then we
    // want to remove the list symbol Valid listCharacters: '+ ', '-' , '* ', '+
    // [ ] ', '+ [x] ', '- [ ] ', '- [-] ', '- [x] ', '* [ ] ', '* [x] '.
    static QRegularExpression regex1(
        R"(^(\s*)([+|\-|\*] \[(x|-| |)\]|[+\-\*])(\s+)$)");
    QRegularExpressionMatchIterator iterator =
        regex1.globalMatch(currentLinePartialText);
    if (iterator.hasNext()) {
        cursor.removeSelectedText();
        return true;
    }

    // if return is pressed and there is just an ordered list symbol then we
    // want to remove the list symbol
    static QRegularExpression regex2(R"(^(\s*)(\d+[\.|\)])(\s+)$)");
    iterator = regex2.globalMatch(currentLinePartialText);
    if (iterator.hasNext()) {
        qDebug() << cursor.selectedText();
        cursor.removeSelectedText();
        return true;
    }

    // Check if we are in an unordered list.
    // We are in a list when we have '* ', '- ' or '+ ', possibly with preceding
    // whitespace. If e.g. user has entered '**text**' and pressed enter - we
    // don't want to do more list-stuff.
    QString currentLine = currentLinePartialText.trimmed();
    QChar char0;
    QChar char1;
    if (currentLine.length() >= 1) char0 = currentLine.at(0);
    if (currentLine.length() >= 2) char1 = currentLine.at(1);
    const bool inList =
        ((char0 == QLatin1Char('*') || char0 == QLatin1Char('-') ||
          char0 == QLatin1Char('+')) &&
         char1 == QLatin1Char(' '));

    if (inList) {
        // if the current line starts with a list character (possibly after
        // whitespaces) add the whitespaces at the next line too
        // Valid listCharacters: '+ ', '-' , '* ', '+ [ ] ', '+ [x] ', '- [ ] ',
        // '- [x] ', '- [-] ', '* [ ] ', '* [x] '.
        static QRegularExpression regex3(
            R"(^(\s*)([+|\-|\*] \[(x|-| |)\]|[+\-\*])(\s+))");
        iterator = regex3.globalMatch(currentLinePartialText);
        if (iterator.hasNext()) {
            const QRegularExpressionMatch match = iterator.next();
            const QString whitespaces = match.captured(1);
            QString listCharacter = match.captured(2);
            const QString whitespaceCharacter = match.captured(4);

            static QRegularExpression regex4(R"(^([+|\-|\*]) \[(x| |\-|)\])");
            // start new checkbox list item with an unchecked checkbox
            iterator = regex4.globalMatch(listCharacter);
            if (iterator.hasNext()) {
                const QRegularExpressionMatch match1 = iterator.next();
                const QString realListCharacter = match1.captured(1);
                listCharacter = realListCharacter + QStringLiteral(" [ ]");
            }

            cursor.setPosition(position);
            cursor.insertText("\n" + whitespaces + listCharacter +
                              whitespaceCharacter);

            // scroll to the cursor if we are at the bottom of the document
            ensureCursorVisible();
            return true;
        }
    }

    // check for ordered lists and increment the list number in the next line
    static QRegularExpression regex5(R"(^(\s*)(\d+)([\.|\)])(\s+))");
    iterator = regex5.globalMatch(currentLinePartialText);
    if (iterator.hasNext()) {
        const QRegularExpressionMatch match = iterator.next();
        const QString whitespaces = match.captured(1);
        const uint listNumber = match.captured(2).toUInt();
        const QString listMarker = match.captured(3);
        const QString whitespaceCharacter = match.captured(4);

        cursor.setPosition(position);
        cursor.insertText("\n" + whitespaces + QString::number(listNumber + 1) +
                          listMarker + whitespaceCharacter);

        // scroll to the cursor if we are at the bottom of the document
        ensureCursorVisible();
        return true;
    }

    // intent next line with same whitespaces as in current line
    static QRegularExpression regex6(R"(^(\s+))");
    iterator = regex6.globalMatch(currentLinePartialText);
    if (iterator.hasNext()) {
        const QRegularExpressionMatch match = iterator.next();
        const QString whitespaces = match.captured(1);

        cursor.setPosition(position);
        cursor.insertText("\n" + whitespaces);

        // scroll to the cursor if we are at the bottom of the document
        ensureCursorVisible();
        return true;
    }

    // Add new list item above current line if we are at the start the line of a
    // list item
    if (cursorAtBlockStart) {
        static QRegularExpression regex7(
            R"(^([+|\-|\*] \[(x|-| |)\]|[+\-\*])(\s+))");
        iterator = regex7.globalMatch(currentLineText);
        if (iterator.hasNext()) {
            const QRegularExpressionMatch match = iterator.next();
            QString listCharacter = match.captured(1);
            const QString whitespaceCharacter = match.captured(3);

            static QRegularExpression regex8(R"(^([+|\-|\*]) \[(x| |\-|)\])");
            // start new checkbox list item with an unchecked checkbox
            iterator = regex8.globalMatch(listCharacter);
            if (iterator.hasNext()) {
                const QRegularExpressionMatch match1 = iterator.next();
                const QString realListCharacter = match1.captured(1);
                listCharacter = realListCharacter + QStringLiteral(" [ ]");
            }

            cursor.setPosition(position);
            // Enter new list item above current line
            cursor.insertText(listCharacter + whitespaceCharacter + "\n");
            // Move the cursor at the end of the new list item
            cursor.movePosition(QTextCursor::Left);
            setTextCursor(cursor);

            // scroll to the cursor if we are at the bottom of the document
            ensureCursorVisible();
            return true;
        }
    }

    return false;
}
    bool handleBracketClosing(const QChar openingCharacter,
                              QChar closingCharacter = QChar()) {
    // check if bracket closing or read-only are enabled
    if (!(_autoTextOptions & AutoTextOption::BracketClosing) || isReadOnly()) {
        return false;
    }

    QTextCursor cursor = textCursor();

    if (closingCharacter.isNull()) {
        closingCharacter = openingCharacter;
    }

    const QString selectedText = cursor.selectedText();

    // When user currently has text selected, we prepend the openingCharacter
    // and append the closingCharacter. E.g. 'text' -> '(text)'. We keep the
    // current selectedText selected.
    if (!selectedText.isEmpty()) {
        // Insert. The selectedText is overwritten.
        const QString newText =
            openingCharacter + selectedText + closingCharacter;
        cursor.insertText(newText);

        // Re-select the selectedText.
        const int selectionEnd = cursor.position() - 1;
        const int selectionStart = selectionEnd - selectedText.length();

        cursor.setPosition(selectionStart);
        cursor.setPosition(selectionEnd, QTextCursor::KeepAnchor);
        this->setTextCursor(cursor);
        _handleBracketClosingUsed = true;
        return true;
    }

    // get the current text from the block (inserted character not included)
    // Remove whitespace at start of string (e.g. in multilevel-lists).
    static QRegularExpression regex1("^\\s+");
    const QString text = cursor.block().text().remove(regex1);

    const int pib = cursor.positionInBlock();
    bool isPreviousAsterisk =
        pib > 0 && pib < text.length() && text.at(pib - 1) == '*';
    bool isNextAsterisk = pib < text.length() && text.at(pib) == '*';
    bool isMaybeBold = isPreviousAsterisk && isNextAsterisk;
    if (pib < text.length() && !isMaybeBold && !text.at(pib).isSpace()) {
        return false;
    }

    // Default positions to move the cursor back.
    int cursorSubtract = 1;
    // Special handling for `*` opening character, as this could be:
    // - start of a list (or sublist);
    // - start of a bold text;
    if (openingCharacter == QLatin1Char('*')) {
        // don't auto complete in code block
        bool isInCode =
            MarkdownHighlighter::isCodeBlock(cursor.block().userState());
        // we only do auto completion if there is a space before the cursor pos
        bool hasSpaceOrAsteriskBefore = !text.isEmpty() && pib > 0 &&
                                        (text.at(pib - 1).isSpace() ||
                                         text.at(pib - 1) == QLatin1Char('*'));
        // This could be the start of a list, don't autocomplete.
        bool isEmpty = text.isEmpty();

        if (isInCode || !hasSpaceOrAsteriskBefore || isEmpty) {
            return false;
        }

        // bold
        if (isPreviousAsterisk && isNextAsterisk) {
            cursorSubtract = 1;
        }

        // User wants: '**'.
        // Not the start of a list, probably bold text. We autocomplete with
        // extra closingCharacter and cursorSubtract to 'catchup'.
        if (text == QLatin1String("*")) {
            cursor.insertText(QStringLiteral("*"));
            cursorSubtract = 2;
        }
    }

    // Auto-completion for ``` pair
    if (openingCharacter == QLatin1Char('`')) {
#if QT_VERSION < QT_VERSION_CHECK(5, 12, 0)
        if (QRegExp(QStringLiteral("[^`]*``")).exactMatch(text)) {
#else
        if (QRegularExpression(
                QRegularExpression::anchoredPattern(QStringLiteral("[^`]*``")))
                .match(text)
                .hasMatch()) {
#endif
            cursor.insertText(QStringLiteral("``"));
            cursorSubtract = 3;
        }
    }

    // don't auto complete in code block
    if (openingCharacter == QLatin1Char('<') &&
        MarkdownHighlighter::isCodeBlock(cursor.block().userState())) {
        return false;
    }

    cursor.beginEditBlock();
    cursor.insertText(openingCharacter);
    cursor.insertText(closingCharacter);
    cursor.setPosition(cursor.position() - cursorSubtract);
    cursor.endEditBlock();

    setTextCursor(cursor);
    return true;
}

/**
 * Checks if the closing character should be output or not
 *
 * @param openingCharacter
 * @param closingCharacter
 * @return
 */
bool QMarkdownTextEdit::bracketClosingCheck(const QChar openingCharacter,
                                            QChar closingCharacter) {
    // check if bracket closing or read-only are enabled
    if (!(_autoTextOptions & AutoTextOption::BracketClosing) || isReadOnly()) {
        return false;
    }

    if (closingCharacter.isNull()) {
        closingCharacter = openingCharacter;
    }

    QTextCursor cursor = textCursor();
    const int positionInBlock = cursor.positionInBlock();

    // get the current text from the block
    const QString text = cursor.block().text();
    const int textLength = text.length();

    // if we are at the end of the line we just want to enter the character
    if (positionInBlock >= textLength) {
        return false;
    }

    const QChar currentChar = text.at(positionInBlock);

    //    if (closingCharacter == openingCharacter) {

    //    }

    qDebug() << __func__ << " - 'currentChar': " << currentChar;

    // if the current character is not the closing character we just want to
    // enter the character
    if (currentChar != closingCharacter) {
        return false;
    }

    const QString leftText = text.left(positionInBlock);
    const int openingCharacterCount = leftText.count(openingCharacter);
    const int closingCharacterCount = leftText.count(closingCharacter);

    // if there were enough opening characters just enter the character
    if (openingCharacterCount < (closingCharacterCount + 1)) {
        return false;
    }

    // move the cursor to the right and don't enter the character
    cursor.movePosition(QTextCursor::Right);
    setTextCursor(cursor);
    return true;
}

/**
 * Checks if the closing character should be output or not or if a closing
 * character after an opening character if needed
 *
 * @param quotationCharacter
 * @return
 */
bool QMarkdownTextEdit::quotationMarkCheck(const QChar quotationCharacter) {
    // check if bracket closing or read-only are enabled
    if (!(_autoTextOptions & AutoTextOption::BracketClosing) || isReadOnly()) {
        return false;
    }

    QTextCursor cursor = textCursor();
    const int positionInBlock = cursor.positionInBlock();

    // get the current text from the block
    const QString text = cursor.block().text();
    const int textLength = text.length();

    // if last char is not space, we are at word end, no autocompletion
    const bool isBacktick = quotationCharacter == '`';
    if (!isBacktick && positionInBlock != 0 &&
        !text.at(positionInBlock - 1).isSpace()) {
        return false;
    }

    // if we are at the end of the line we just want to enter the character
    if (positionInBlock >= textLength) {
        return handleBracketClosing(quotationCharacter);
    }

    const QChar currentChar = text.at(positionInBlock);

    // if the current character is not the quotation character we just want to
    // enter the character
    if (currentChar != quotationCharacter) {
        return handleBracketClosing(quotationCharacter);
    }

    // move the cursor to the right and don't enter the character
    cursor.movePosition(QTextCursor::Right);
    setTextCursor(cursor);
    return true;
}

/***********************************
 * helper methods for char removal
 * Rules for (') and ("):
 * if [sp]" -> opener (sp = space)
 * if "[sp] -> closer
 ***********************************/
bool isQuotOpener(int position, const QString &text) {
    if (position == 0) return true;
    const int prevCharPos = position - 1;
    return text.at(prevCharPos).isSpace();
}
bool isQuotCloser(int position, const QString &text) {
    const int nextCharPos = position + 1;
    if (nextCharPos >= text.length()) return true;
    return text.at(nextCharPos).isSpace();
}

/**
 * Handles removing of matching brackets and other Markdown characters
 * Only works with backspace to remove text
 *
 * @return
 */
bool QMarkdownTextEdit::handleBackspaceEntered() {
    if (!(_autoTextOptions & AutoTextOption::BracketRemoval) || isReadOnly()) {
        return false;
    }

    QTextCursor cursor = textCursor();

    // return if some text was selected
    if (!cursor.selectedText().isEmpty()) {
        return false;
    }

    int position = cursor.position();
    const int positionInBlock = cursor.positionInBlock();
    int block = cursor.block().blockNumber();

    if (_highlighter)
        if (_highlighter->isPosInACodeSpan(block, positionInBlock - 1))
            return false;

    // return if backspace was pressed at the beginning of a block
    if (positionInBlock == 0) {
        return false;
    }

    // get the current text from the block
    const QString text = cursor.block().text();

    char charToRemove{};

    // current char
    const char charInFront = text.at(positionInBlock - 1).toLatin1();

    if (charInFront == '*')
        return handleCharRemoval(MarkdownHighlighter::RangeType::Emphasis,
                                 block, positionInBlock - 1);
    else if (charInFront == '`')
        return handleCharRemoval(MarkdownHighlighter::RangeType::CodeSpan,
                                 block, positionInBlock - 1);

    // handle removal of ", ', and brackets

    // is it opener?
    int pos = _openingCharacters.indexOf(charInFront);
    // for " and '
    bool isOpener = false;
    bool isCloser = false;
    if (pos == 5 || pos == 6) {
        isOpener = isQuotOpener(positionInBlock - 1, text);
    } else {
        isOpener = pos != -1;
    }
    if (isOpener) {
        charToRemove = _closingCharacters.at(pos);
    } else {
        // is it closer?
        pos = _closingCharacters.indexOf(charInFront);
        if (pos == 5 || pos == 6)
            isCloser = isQuotCloser(positionInBlock - 1, text);
        else
            isCloser = pos != -1;
        if (isCloser)
            charToRemove = _openingCharacters.at(pos);
        else
            return false;
    }

    int charToRemoveIndex = -1;
    if (isOpener) {
        bool closer = true;
        charToRemoveIndex = text.indexOf(charToRemove, positionInBlock);
        if (charToRemoveIndex == -1) return false;
        if (pos == 5 || pos == 6)
            closer = isQuotCloser(charToRemoveIndex, text);
        if (!closer) return false;
        cursor.setPosition(position + (charToRemoveIndex - positionInBlock));
        cursor.deleteChar();
    } else if (isCloser) {
        charToRemoveIndex = text.lastIndexOf(charToRemove, positionInBlock - 2);
        if (charToRemoveIndex == -1) return false;
        bool opener = true;
        if (pos == 5 || pos == 6)
            opener = isQuotOpener(charToRemoveIndex, text);
        if (!opener) return false;
        const int pos = position - (positionInBlock - charToRemoveIndex);
        cursor.setPosition(pos);
        cursor.deleteChar();
        position -= 1;
    } else {
        charToRemoveIndex = text.lastIndexOf(charToRemove, positionInBlock - 2);
        if (charToRemoveIndex == -1) return false;
        const int pos = position - (positionInBlock - charToRemoveIndex);
        cursor.setPosition(pos);
        cursor.deleteChar();
        position -= 1;
    }

    // moving the cursor back to the old position so the previous character
    // can be removed
    cursor.setPosition(position);
    setTextCursor(cursor);
    return false;
}

bool QMarkdownTextEdit::handleCharRemoval(MarkdownHighlighter::RangeType type,
                                          int block, int position) {
    if (!_highlighter) return false;

    auto range = _highlighter->findPositionInRanges(type, block, position);
    if (range == QPair<int, int>{-1, -1}) return false;

    int charToRemovePos = range.first;
    if (position == range.first) charToRemovePos = range.second;

    QTextCursor cursor = textCursor();
    auto gpos = cursor.position();

    if (charToRemovePos > position) {
        cursor.setPosition(gpos + (charToRemovePos - (position + 1)));
    } else {
        cursor.setPosition(gpos - (position - charToRemovePos + 1));
        gpos--;
    }

    cursor.deleteChar();
    cursor.setPosition(gpos);
    setTextCursor(cursor);
    return false;
}

void QMarkdownTextEdit::updateLineNumAreaGeometry() {
    const auto contentsRect = this->contentsRect();
    const QRect newGeometry = {contentsRect.left(), contentsRect.top(),
                               _lineNumArea->sizeHint().width(),
                               contentsRect.height()};
    auto oldGeometry = _lineNumArea->geometry();
    if (newGeometry != oldGeometry) {
        _lineNumArea->setGeometry(newGeometry);
    }
}

void QMarkdownTextEdit::resizeEvent(QResizeEvent *event) {
    QPlainTextEdit::resizeEvent(event);
    updateLineNumAreaGeometry();
}

/**
 * Increases (or decreases) the indention of the selected text
 * (if there is a text selected) in the noteTextEdit
 * @return
 */
bool QMarkdownTextEdit::increaseSelectedTextIndention(
    bool reverse, const QString &indentCharacters) {
    QTextCursor cursor = this->textCursor();
    QString selectedText = cursor.selectedText();

    if (!selectedText.isEmpty()) {
        // Start the selection at start of the first block of the selection
        int end = cursor.selectionEnd();
        cursor.setPosition(cursor.selectionStart());
        cursor.movePosition(QTextCursor::StartOfBlock, QTextCursor::MoveAnchor);
        cursor.setPosition(end, QTextCursor::KeepAnchor);
        this->setTextCursor(cursor);
        selectedText = cursor.selectedText();

        // we need this strange newline character we are getting in the
        // selected text for newlines
        const QString newLine =
            QString::fromUtf8(QByteArray::fromHex(QByteArrayLiteral("e280a9")));
        QString newText;

        if (reverse) {
            // un-indent text

            const int indentSize = indentCharacters == QStringLiteral("\t")
                                       ? 4
                                       : indentCharacters.length();

            // remove leading \t or spaces in following lines
            newText = selectedText.replace(
                QRegularExpression(newLine + QStringLiteral("(\\t| {1,") +
                                   QString::number(indentSize) +
                                   QStringLiteral("})")),
                QStringLiteral("\n"));

            // remove leading \t or spaces in first line
            newText.remove(QRegularExpression(QStringLiteral("^(\\t| {1,") +
                                              QString::number(indentSize) +
                                              QStringLiteral("})")));
        } else {
            // replace trailing new line to prevent an indent of the line after
            // the selection
            newText = selectedText.replace(
                QRegularExpression(QRegularExpression::escape(newLine) +
                                   QStringLiteral("$")),
                QStringLiteral("\n"));

            // indent text
            newText.replace(newLine, QStringLiteral("\n") + indentCharacters)
                .prepend(indentCharacters);

            // remove trailing \t
            static QRegularExpression regex1(QStringLiteral("\\t$"));
            newText.remove(regex1);
        }

        // insert the new text
        cursor.insertText(newText);

        // update the selection to the new text
        cursor.setPosition(cursor.position() - newText.size(),
                           QTextCursor::KeepAnchor);
        this->setTextCursor(cursor);

        return true;
    } else if (reverse) {
        const int indentSize = indentCharacters.length();

        // do the check as often as we have characters to un-indent
        for (int i = 1; i <= indentSize; i++) {
            // if nothing was selected but we want to reverse the indention
            // check if there is a \t in front or after the cursor and remove it
            // if so
            const int position = cursor.position();

            if (!cursor.atStart()) {
                // get character in front of cursor
                cursor.setPosition(position - 1, QTextCursor::KeepAnchor);
            }

            // check for \t or space in front of cursor
            static QRegularExpression regex1(QStringLiteral("[\\t ]"));
            QRegularExpressionMatch match = regex1.match(cursor.selectedText());

            if (!match.hasMatch()) {
                // (select to) check for \t or space after the cursor
                cursor.setPosition(position);

                if (!cursor.atEnd()) {
                    cursor.setPosition(position + 1, QTextCursor::KeepAnchor);
                }
            }

            match = regex1.match(cursor.selectedText());

            if (match.hasMatch()) {
                cursor.removeSelectedText();
            }

            cursor = this->textCursor();
        }

        return true;
    }

    // else just insert indentCharacters
    cursor.insertText(indentCharacters);

    return true;
}

/**
 * @brief Opens the link (if any) at the current cursor position
 */
bool QMarkdownTextEdit::openLinkAtCursorPosition() {
    QTextCursor cursor = this->textCursor();
    const int clickedPosition = cursor.position();

    // select the text in the clicked block and find out on
    // which position we clicked
    cursor.movePosition(QTextCursor::StartOfBlock);
    const int positionFromStart = clickedPosition - cursor.position();
    cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);

    const QString selectedText = cursor.selectedText();

    // find out which url in the selected text was clicked
    const QString urlString =
        getMarkdownUrlAtPosition(selectedText, positionFromStart);
    const QUrl url = QUrl(urlString);
    const bool isRelativeFileUrl =
        urlString.startsWith(QLatin1String("file://.."));
    const bool isLegacyAttachmentUrl =
        urlString.startsWith(QLatin1String("file://attachments"));

    qDebug() << __func__ << " - 'emit urlClicked( urlString )': " << urlString;

    Q_EMIT urlClicked(urlString);

    if ((url.isValid() && isValidUrl(urlString)) || isRelativeFileUrl ||
        isLegacyAttachmentUrl) {
        // ignore some schemata
        if (!(_ignoredClickUrlSchemata.contains(url.scheme()) ||
              isRelativeFileUrl || isLegacyAttachmentUrl)) {
            // open the url
            openUrl(urlString);
        }

        return true;
    }

    return false;
}

/**
 * Checks if urlString is a valid url
 *
 * @param urlString
 * @return
 */
bool QMarkdownTextEdit::isValidUrl(const QString &urlString) {
    static QRegularExpression regex(R"(^\w+:\/\/.+)");
    const QRegularExpressionMatch match = regex.match(urlString);
    return match.hasMatch();
}

/**
 * Handles clicked urls
 *
 * examples:
 * - <https://www.qownnotes.org> opens the webpage
 * - <file:///path/to/my/file/QOwnNotes.pdf> opens the file
 *   "/path/to/my/file/QOwnNotes.pdf" if the operating system supports that
 *  handler
 */
void QMarkdownTextEdit::openUrl(const QString &urlString) {
    qDebug() << "QMarkdownTextEdit " << __func__
             << " - 'urlString': " << urlString;

    QDesktopServices::openUrl(QUrl(urlString));
}

/**
 * @brief Returns the highlighter instance
 * @return
 */
MarkdownHighlighter *QMarkdownTextEdit::highlighter() { return _highlighter; }

/**
 * @brief Returns the searchWidget instance
 * @return
 */
QPlainTextEditSearchWidget *QMarkdownTextEdit::searchWidget() {
    return _searchWidget;
}

/**
 * @brief Sets url schemata that will be ignored when clicked on
 * @param urlSchemes
 */
void QMarkdownTextEdit::setIgnoredClickUrlSchemata(
    QStringList ignoredUrlSchemata) {
    _ignoredClickUrlSchemata = std::move(ignoredUrlSchemata);
}

/**
 * @brief Returns a map of parsed Markdown urls with their link texts as key
 *
 * @param text
 * @return parsed urls
 */
QMap<QString, QString> QMarkdownTextEdit::parseMarkdownUrlsFromText(
    const QString &text) {
    QMap<QString, QString> urlMap;
    QRegularExpressionMatchIterator iterator;

    // match urls like this: <http://mylink>
    //    re = QRegularExpression("(<(.+?:\\/\\/.+?)>)");
    static QRegularExpression regex1(QStringLiteral("(<(.+?)>)"));
    iterator = regex1.globalMatch(text);
    while (iterator.hasNext()) {
        QRegularExpressionMatch match = iterator.next();
        QString linkText = match.captured(1);
        QString url = match.captured(2);
        urlMap[linkText] = url;
    }

    // match urls like this: [this url](http://mylink)
    //    QRegularExpression re("(\\[.*?\\]\\((.+?:\\/\\/.+?)\\))");
    static QRegularExpression regex2(R"((\[.*?\]\((.+?)\)))");
    iterator = regex2.globalMatch(text);
    while (iterator.hasNext()) {
        QRegularExpressionMatch match = iterator.next();
        QString linkText = match.captured(1);
        QString url = match.captured(2);
        urlMap[linkText] = url;
    }

    // match urls like this: http://mylink
    static QRegularExpression regex3(R"(\b\w+?:\/\/[^\s]+[^\s>\)])");
    iterator = regex3.globalMatch(text);
    while (iterator.hasNext()) {
        QRegularExpressionMatch match = iterator.next();
        QString url = match.captured(0);
        urlMap[url] = url;
    }

    // match urls like this: www.github.com
    static QRegularExpression regex4(R"(\bwww\.[^\s]+\.[^\s]+\b)");
    iterator = regex4.globalMatch(text);
    while (iterator.hasNext()) {
        QRegularExpressionMatch match = iterator.next();
        QString url = match.captured(0);
        urlMap[url] = QStringLiteral("http://") + url;
    }

    // match reference urls like this: [this url][1] with this later:
    // [1]: http://domain
    static QRegularExpression regex5(R"((\[.*?\]\[(.+?)\]))");
    iterator = regex5.globalMatch(text);
    while (iterator.hasNext()) {
        QRegularExpressionMatch match = iterator.next();
        QString linkText = match.captured(1);
        QString referenceId = match.captured(2);

        // search for the referenced url in the whole text edit
        //        QRegularExpression refRegExp(
        //                "\\[" + QRegularExpression::escape(referenceId) +
        //                        "\\]: (.+?:\\/\\/.+)");
        QRegularExpression refRegExp(QStringLiteral("\\[") +
                                     QRegularExpression::escape(referenceId) +
                                     QStringLiteral("\\]: (.+)"));
        QRegularExpressionMatch urlMatch = refRegExp.match(toPlainText());

        if (urlMatch.hasMatch()) {
            QString url = urlMatch.captured(1);
            urlMap[linkText] = url;
        }
    }

    return urlMap;
}

/**
 * @brief Returns the Markdown url at position
 * @param text
 * @param position
 * @return url string
 */
QString QMarkdownTextEdit::getMarkdownUrlAtPosition(const QString &text,
                                                    int position) {
    QString url;

    // get a map of parsed Markdown urls with their link texts as key
    const QMap<QString, QString> urlMap = parseMarkdownUrlsFromText(text);
    QMap<QString, QString>::const_iterator i = urlMap.constBegin();
    for (; i != urlMap.constEnd(); ++i) {
        const QString &linkText = i.key();
        const QString &urlString = i.value();

        const int foundPositionStart = text.indexOf(linkText);

        if (foundPositionStart >= 0) {
            // calculate end position of found linkText
            const int foundPositionEnd = foundPositionStart + linkText.size();

            // check if position is in found string range
            if ((position >= foundPositionStart) &&
                (position <= foundPositionEnd)) {
                url = urlString;
                break;
            }
        }
    }

    return url;
}

/**
 * @brief Duplicates the text in the text edit
 */
void QMarkdownTextEdit::duplicateText() {
    if (isReadOnly()) {
        return;
    }

    QTextCursor cursor = this->textCursor();
    QString selectedText = cursor.selectedText();

    // duplicate line if no text was selected
    if (selectedText.isEmpty()) {
        const int position = cursor.position();

        // select the whole line
        cursor.movePosition(QTextCursor::StartOfBlock);
        cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);

        const int positionDiff = cursor.position() - position;
        selectedText = "\n" + cursor.selectedText();

        // insert text with new line at end of the selected line
        cursor.setPosition(cursor.selectionEnd());
        cursor.insertText(selectedText);

        // set the position to same position it was in the duplicated line
        cursor.setPosition(cursor.position() - positionDiff);
    } else {
        // duplicate selected text
        cursor.setPosition(cursor.selectionEnd());
        const int selectionStart = cursor.position();

        // insert selected text
        cursor.insertText(selectedText);
        const int selectionEnd = cursor.position();

        // select the inserted text
        cursor.setPosition(selectionStart);
        cursor.setPosition(selectionEnd, QTextCursor::KeepAnchor);
    }

    this->setTextCursor(cursor);
}

void QMarkdownTextEdit::setText(const QString &text) { setPlainText(text); }

void QMarkdownTextEdit::setPlainText(const QString &text) {
    // clear the dirty blocks vector to increase performance and prevent
    // a possible crash in QSyntaxHighlighter::rehighlightBlock
    if (_highlighter) _highlighter->clearDirtyBlocks();

    QPlainTextEdit::setPlainText(text);
    adjustRightMargin();
}

/**
 * Uses another widget as parent for the search widget
 */
void QMarkdownTextEdit::initSearchFrame(QWidget *searchFrame, bool darkMode) {
    _searchFrame = searchFrame;

    // remove the search widget from our layout
    layout()->removeWidget(_searchWidget);

    QLayout *layout = _searchFrame->layout();

    // create a grid layout for the frame and add the search widget to it
    if (layout == nullptr) {
        layout = new QVBoxLayout(_searchFrame);
        layout->setSpacing(0);
        layout->setContentsMargins(0, 0, 0, 0);
    }

    _searchWidget->setDarkMode(darkMode);
    layout->addWidget(_searchWidget);
    _searchFrame->setLayout(layout);
}

/**
 * Hides the text edit and the search widget
 */
void QMarkdownTextEdit::hide() {
    _searchWidget->hide();
    QWidget::hide();
}

/**
 * Handles an entered return key
 */
bool QMarkdownTextEdit::handleReturnEntered() {
    if (isReadOnly()) {
        return true;
    }

    // This will be the main cursor to add or remove text
    QTextCursor cursor = this->textCursor();

    // We need a 2nd cursor to get the text of the current block without moving
    // the main cursor that is used to remove the selected text
    QTextCursor cursor2 = this->textCursor();
    cursor2.select(QTextCursor::BlockUnderCursor);
    const QString currentLineText = cursor2.selectedText().trimmed();

    const int position = cursor.position();
    const bool cursorAtBlockStart = cursor.atBlockStart();
    cursor.movePosition(QTextCursor::StartOfBlock, QTextCursor::KeepAnchor);
    const QString currentLinePartialText = cursor.selectedText();

    // if return is pressed and there is just an unordered list symbol then we
    // want to remove the list symbol Valid listCharacters: '+ ', '-' , '* ', '+
    // [ ] ', '+ [x] ', '- [ ] ', '- [-] ', '- [x] ', '* [ ] ', '* [x] '.
    static QRegularExpression regex1(
        R"(^(\s*)([+|\-|\*] \[(x|-| |)\]|[+\-\*])(\s+)$)");
    QRegularExpressionMatchIterator iterator =
        regex1.globalMatch(currentLinePartialText);
    if (iterator.hasNext()) {
        cursor.removeSelectedText();
        return true;
    }

    // if return is pressed and there is just an ordered list symbol then we
    // want to remove the list symbol
    static QRegularExpression regex2(R"(^(\s*)(\d+[\.|\)])(\s+)$)");
    iterator = regex2.globalMatch(currentLinePartialText);
    if (iterator.hasNext()) {
        qDebug() << cursor.selectedText();
        cursor.removeSelectedText();
        return true;
    }

    // Check if we are in an unordered list.
    // We are in a list when we have '* ', '- ' or '+ ', possibly with preceding
    // whitespace. If e.g. user has entered '**text**' and pressed enter - we
    // don't want to do more list-stuff.
    QString currentLine = currentLinePartialText.trimmed();
    QChar char0;
    QChar char1;
    if (currentLine.length() >= 1) char0 = currentLine.at(0);
    if (currentLine.length() >= 2) char1 = currentLine.at(1);
    const bool inList =
        ((char0 == QLatin1Char('*') || char0 == QLatin1Char('-') ||
          char0 == QLatin1Char('+')) &&
         char1 == QLatin1Char(' '));

    if (inList) {
        // if the current line starts with a list character (possibly after
        // whitespaces) add the whitespaces at the next line too
        // Valid listCharacters: '+ ', '-' , '* ', '+ [ ] ', '+ [x] ', '- [ ] ',
        // '- [x] ', '- [-] ', '* [ ] ', '* [x] '.
        static QRegularExpression regex3(
            R"(^(\s*)([+|\-|\*] \[(x|-| |)\]|[+\-\*])(\s+))");
        iterator = regex3.globalMatch(currentLinePartialText);
        if (iterator.hasNext()) {
            const QRegularExpressionMatch match = iterator.next();
            const QString whitespaces = match.captured(1);
            QString listCharacter = match.captured(2);
            const QString whitespaceCharacter = match.captured(4);

            static QRegularExpression regex4(R"(^([+|\-|\*]) \[(x| |\-|)\])");
            // start new checkbox list item with an unchecked checkbox
            iterator = regex4.globalMatch(listCharacter);
            if (iterator.hasNext()) {
                const QRegularExpressionMatch match1 = iterator.next();
                const QString realListCharacter = match1.captured(1);
                listCharacter = realListCharacter + QStringLiteral(" [ ]");
            }

            cursor.setPosition(position);
            cursor.insertText("\n" + whitespaces + listCharacter +
                              whitespaceCharacter);

            // scroll to the cursor if we are at the bottom of the document
            ensureCursorVisible();
            return true;
        }
    }

    // check for ordered lists and increment the list number in the next line
    static QRegularExpression regex5(R"(^(\s*)(\d+)([\.|\)])(\s+))");
    iterator = regex5.globalMatch(currentLinePartialText);
    if (iterator.hasNext()) {
        const QRegularExpressionMatch match = iterator.next();
        const QString whitespaces = match.captured(1);
        const uint listNumber = match.captured(2).toUInt();
        const QString listMarker = match.captured(3);
        const QString whitespaceCharacter = match.captured(4);

        cursor.setPosition(position);
        cursor.insertText("\n" + whitespaces + QString::number(listNumber + 1) +
                          listMarker + whitespaceCharacter);

        // scroll to the cursor if we are at the bottom of the document
        ensureCursorVisible();
        return true;
    }

    // intent next line with same whitespaces as in current line
    static QRegularExpression regex6(R"(^(\s+))");
    iterator = regex6.globalMatch(currentLinePartialText);
    if (iterator.hasNext()) {
        const QRegularExpressionMatch match = iterator.next();
        const QString whitespaces = match.captured(1);

        cursor.setPosition(position);
        cursor.insertText("\n" + whitespaces);

        // scroll to the cursor if we are at the bottom of the document
        ensureCursorVisible();
        return true;
    }

    // Add new list item above current line if we are at the start the line of a
    // list item
    if (cursorAtBlockStart) {
        static QRegularExpression regex7(
            R"(^([+|\-|\*] \[(x|-| |)\]|[+\-\*])(\s+))");
        iterator = regex7.globalMatch(currentLineText);
        if (iterator.hasNext()) {
            const QRegularExpressionMatch match = iterator.next();
            QString listCharacter = match.captured(1);
            const QString whitespaceCharacter = match.captured(3);

            static QRegularExpression regex8(R"(^([+|\-|\*]) \[(x| |\-|)\])");
            // start new checkbox list item with an unchecked checkbox
            iterator = regex8.globalMatch(listCharacter);
            if (iterator.hasNext()) {
                const QRegularExpressionMatch match1 = iterator.next();
                const QString realListCharacter = match1.captured(1);
                listCharacter = realListCharacter + QStringLiteral(" [ ]");
            }

            cursor.setPosition(position);
            // Enter new list item above current line
            cursor.insertText(listCharacter + whitespaceCharacter + "\n");
            // Move the cursor at the end of the new list item
            cursor.movePosition(QTextCursor::Left);
            setTextCursor(cursor);

            // scroll to the cursor if we are at the bottom of the document
            ensureCursorVisible();
            return true;
        }
    }

    return false;
}

/**
 * Handles entered tab or reverse tab keys
 */
bool QMarkdownTextEdit::handleTabEntered(bool reverse,
                                         const QString &indentCharacters) {
    if (isReadOnly()) {
        return true;
    }

    QTextCursor cursor = this->textCursor();

    // only check for lists if we haven't a text selected
    if (cursor.selectedText().isEmpty()) {
        cursor.movePosition(QTextCursor::StartOfBlock, QTextCursor::KeepAnchor);
        const QString currentLineText = cursor.selectedText();

        // check if we want to indent or un-indent an ordered list
        // Valid listCharacters: '+ ', '-' , '* ', '+ [ ] ', '+ [x] ', '- [ ] ',
        // '- [x] ', '- [-] ', '* [ ] ', '* [x] '.
        static QRegularExpression regex1(
            R"(^(\s*)([+|\-|\*] \[(x|-| )\]|[+\-\*])(\s+)$)");
        QRegularExpressionMatchIterator i = regex1.globalMatch(currentLineText);

        if (i.hasNext()) {
            QRegularExpressionMatch match = i.next();
            QString whitespaces = match.captured(1);
            const QString listCharacter = match.captured(2);
            const QString whitespaceCharacter = match.captured(4);

            // add or remove one tabulator key
            if (reverse) {
                // remove one set of indentCharacters or a tabulator
                whitespaces.remove(QRegularExpression(
                    QStringLiteral("^(\\t|") +
                    QRegularExpression::escape(indentCharacters) +
                    QStringLiteral(")")));

            } else {
                whitespaces += indentCharacters;
            }

            cursor.insertText(whitespaces + listCharacter +
                              whitespaceCharacter);
            return true;
        }

        // check if we want to indent or un-indent an ordered list
        static QRegularExpression regex2(R"(^(\s*)(\d+)([\.|\)])(\s+)$)");
        i = regex2.globalMatch(currentLineText);

        if (i.hasNext()) {
            const QRegularExpressionMatch match = i.next();
            QString whitespaces = match.captured(1);
            const QString listCharacter = match.captured(2);
            const QString listMarker = match.captured(3);
            const QString whitespaceCharacter = match.captured(4);

            // add or remove one tabulator key
            if (reverse) {
                whitespaces.chop(1);
            } else {
                whitespaces += indentCharacters;
            }

            cursor.insertText(whitespaces + listCharacter + listMarker +
                              whitespaceCharacter);
            return true;
        }
    }

    // check if we want to indent the whole text
    return increaseSelectedTextIndention(reverse, indentCharacters);
}

/**
 * Sets the auto text options
 */
void QMarkdownTextEdit::setAutoTextOptions(AutoTextOptions options) {
    _autoTextOptions = options;
}

void QMarkdownTextEdit::updateLineNumberArea(const QRect rect, int dy) {
    if (dy)
        _lineNumArea->scroll(0, dy);
    else
        _lineNumArea->update(0, rect.y(), _lineNumArea->sizeHint().width(),
                             rect.height());

    updateLineNumAreaGeometry();

    if (rect.contains(viewport()->rect())) {
        updateLineNumberAreaWidth(0);
    }
}

void QMarkdownTextEdit::updateLineNumberAreaWidth(int) {
    QSignalBlocker blocker(this);
    const auto oldMargins = viewportMargins();
    const int width =
        _lineNumArea->isLineNumAreaEnabled()
            ? _lineNumArea->sizeHint().width() + _lineNumberLeftMarginOffset
            : oldMargins.left();
    const auto newMargins = QMargins{width, oldMargins.top(),
                                     oldMargins.right(), oldMargins.bottom()};

    if (newMargins != oldMargins) {
        setViewportMargins(newMargins);
    }

    // Grow lineNumArea font-size with the font size of the editor
    const int pointSize = this->font().pointSize();
    if (pointSize > 0) {
        QFont font = _lineNumArea->font();
        font.setPointSize(pointSize);
        _lineNumArea->setFont(font);
    }
}

/**
 * @param e
 * @details This does two things
 * 1. Overrides QPlainTextEdit::paintEvent to fix the RTL bug of QPlainTextEdit
 * 2. Paints a rectangle around code block fences [Code taken from
 * ghostwriter(which in turn is based on QPlaintextEdit::paintEvent() with
 * modifications and minor improvements for our use
 */
void QMarkdownTextEdit::paintEvent(QPaintEvent *e) {
    QTextBlock block = firstVisibleBlock();

    QPainter painter(viewport());
    const QRect viewportRect = viewport()->rect();
    // painter.fillRect(viewportRect, Qt::transparent);
    bool firstVisible = true;
    QPointF offset(contentOffset());
    QRectF blockAreaRect;    // Code or block quote rect.
    bool inBlockArea = false;

    bool clipTop = false;
    bool drawBlock = false;
    qreal dy = 0.0;
    bool done = false;

    const QColor &color = MarkdownHighlighter::codeBlockBackgroundColor();
    const int cornerRadius = 5;

    while (block.isValid() && !done) {
        const QRectF r = blockBoundingRect(block).translated(offset);
        const int state = block.userState();

        if (!inBlockArea && MarkdownHighlighter::isCodeBlock(state)) {
            // skip the backticks
            if (!block.text().startsWith(QLatin1String("```")) &&
                !block.text().startsWith(QLatin1String("~~~"))) {
                blockAreaRect = r;
                dy = 0.0;
                inBlockArea = true;
            }

            // If this is the first visible block within the viewport
            // and if the previous block is part of the text block area,
            // then the rectangle to draw for the block area will have
            // its top clipped by the viewport and will need to be
            // drawn specially.
            const int prevBlockState = block.previous().userState();
            if (firstVisible &&
                MarkdownHighlighter::isCodeBlock(prevBlockState)) {
                clipTop = true;
            }
        }
        // Else if the block ends a text block area...
        else if (inBlockArea && MarkdownHighlighter::isCodeBlockEnd(state)) {
            drawBlock = true;
            inBlockArea = false;
            blockAreaRect.setHeight(dy);
        }
        // If the block is at the end of the document and ends a text
        // block area...
        //
        if (inBlockArea && block == this->document()->lastBlock()) {
            drawBlock = true;
            inBlockArea = false;
            dy += r.height();
            blockAreaRect.setHeight(dy);
        }
        offset.ry() += r.height();
        dy += r.height();

        // If this is the last text block visible within the viewport...
        if (offset.y() > viewportRect.height()) {
            if (inBlockArea) {
                blockAreaRect.setHeight(dy);
                drawBlock = true;
            }

            // Finished drawing.
            done = true;
        }
        // If this is the last text block visible within the viewport...
        if (offset.y() > viewportRect.height()) {
            if (inBlockArea) {
                blockAreaRect.setHeight(dy);
                drawBlock = true;
            }
            // Finished drawing.
            done = true;
        }

        if (drawBlock) {
            painter.setCompositionMode(QPainter::CompositionMode_SourceOver);
            painter.setPen(Qt::NoPen);
            painter.setBrush(QBrush(color));

            // If the first visible block is "clipped" such that the previous
            // block is part of the text block area, then only draw a rectangle
            // with the bottom corners rounded, and with the top corners square
            // to reflect that the first visible block is part of a larger block
            // of text.
            //
            if (clipTop) {
                QPainterPath path;
                path.setFillRule(Qt::WindingFill);
                path.addRoundedRect(blockAreaRect, cornerRadius, cornerRadius);
                qreal adjustedHeight = blockAreaRect.height() / 2;
                path.addRect(blockAreaRect.adjusted(0, 0, 0, -adjustedHeight));
                painter.drawPath(path.simplified());
                clipTop = false;
            }
            // Else draw the entire rectangle with all corners rounded.
            else {
                painter.drawRoundedRect(blockAreaRect, cornerRadius,
                                        cornerRadius);
            }

            drawBlock = false;
        }

        // this fixes the RTL bug of QPlainTextEdit
        // https://bugreports.qt.io/browse/QTBUG-7516
        if (block.text().isRightToLeft()) {
            QTextLayout *layout = block.layout();
            // opt = document()->defaultTextOption();
            QTextOption opt = QTextOption(Qt::AlignRight);
            opt.setTextDirection(Qt::RightToLeft);
            layout->setTextOption(opt);
        }

        // Current line highlight
        QTextCursor cursor = textCursor();
        if (highlightCurrentLine() && cursor.block() == block) {
            QTextLine line =
                block.layout()->lineForTextPosition(cursor.positionInBlock());
            QRectF lineRect = line.rect();
            lineRect.moveTop(lineRect.top() + r.top());
            lineRect.setLeft(0.);
            lineRect.setRight(viewportRect.width());
            painter.fillRect(lineRect.toAlignedRect(),
                             currentLineHighlightColor());
        }

        block = block.next();
        firstVisible = false;
    }

    painter.end();
    QPlainTextEdit::paintEvent(e);
}

/**
 * Overrides QPlainTextEdit::setReadOnly to fix a problem with Chinese and
 * Japanese input methods
 *
 * @param ro
 */
void QMarkdownTextEdit::setReadOnly(bool ro) {
    QPlainTextEdit::setReadOnly(ro);

    // attempted to fix a problem with Chinese and Japanese input methods
    // @see https://github.com/pbek/QOwnNotes/issues/976
    setAttribute(Qt::WA_InputMethodEnabled, !isReadOnly());
}

void QMarkdownTextEdit::doSearch(
    QString &searchText, QPlainTextEditSearchWidget::SearchMode searchMode) {
    _searchWidget->setSearchText(searchText);
    _searchWidget->setSearchMode(searchMode);
    _searchWidget->doSearchCount();
    _searchWidget->activate(false);
}

void QMarkdownTextEdit::hideSearchWidget(bool reset) {
    _searchWidget->deactivate();

    if (reset) {
        _searchWidget->reset();
    }
}

void QMarkdownTextEdit::updateSettings() {
    // if true: centers the screen if cursor reaches bottom (but not top)
    searchWidget()->setDebounceDelay(_debounceDelay);
    setCenterOnScroll(_centerCursor);
}

void QMarkdownTextEdit::setLineNumberLeftMarginOffset(int offset) {
    _lineNumberLeftMarginOffset = offset;
}

QMargins QMarkdownTextEdit::viewportMargins() {
    return QPlainTextEdit::viewportMargins();
}
    bool bracketClosingCheck(const QChar openingCharacter,
                             QChar closingCharacter) {
    // check if bracket closing or read-only are enabled
    if (!(_autoTextOptions & AutoTextOption::BracketClosing) || isReadOnly()) {
        return false;
    }

    if (closingCharacter.isNull()) {
        closingCharacter = openingCharacter;
    }

    QTextCursor cursor = textCursor();
    const int positionInBlock = cursor.positionInBlock();

    // get the current text from the block
    const QString text = cursor.block().text();
    const int textLength = text.length();

    // if we are at the end of the line we just want to enter the character
    if (positionInBlock >= textLength) {
        return false;
    }

    const QChar currentChar = text.at(positionInBlock);

    //    if (closingCharacter == openingCharacter) {

    //    }

    qDebug() << __func__ << " - 'currentChar': " << currentChar;

    // if the current character is not the closing character we just want to
    // enter the character
    if (currentChar != closingCharacter) {
        return false;
    }

    const QString leftText = text.left(positionInBlock);
    const int openingCharacterCount = leftText.count(openingCharacter);
    const int closingCharacterCount = leftText.count(closingCharacter);

    // if there were enough opening characters just enter the character
    if (openingCharacterCount < (closingCharacterCount + 1)) {
        return false;
    }

    // move the cursor to the right and don't enter the character
    cursor.movePosition(QTextCursor::Right);
    setTextCursor(cursor);
    return true;
}
    bool quotationMarkCheck(const QChar quotationCharacter) {
    // check if bracket closing or read-only are enabled
    if (!(_autoTextOptions & AutoTextOption::BracketClosing) || isReadOnly()) {
        return false;
    }

    QTextCursor cursor = textCursor();
    const int positionInBlock = cursor.positionInBlock();

    // get the current text from the block
    const QString text = cursor.block().text();
    const int textLength = text.length();

    // if last char is not space, we are at word end, no autocompletion
    const bool isBacktick = quotationCharacter == '`';
    if (!isBacktick && positionInBlock != 0 &&
        !text.at(positionInBlock - 1).isSpace()) {
        return false;
    }

    // if we are at the end of the line we just want to enter the character
    if (positionInBlock >= textLength) {
        return handleBracketClosing(quotationCharacter);
    }

    const QChar currentChar = text.at(positionInBlock);

    // if the current character is not the quotation character we just want to
    // enter the character
    if (currentChar != quotationCharacter) {
        return handleBracketClosing(quotationCharacter);
    }

    // move the cursor to the right and don't enter the character
    cursor.movePosition(QTextCursor::Right);
    setTextCursor(cursor);
    return true;
}
    void focusOutEvent(QFocusEvent *event) override {
    resetMouseCursor();
    QPlainTextEdit::focusOutEvent(event);
}
    void paintEvent(QPaintEvent *e) override {
    QTextBlock block = firstVisibleBlock();

    QPainter painter(viewport());
    const QRect viewportRect = viewport()->rect();
    // painter.fillRect(viewportRect, Qt::transparent);
    bool firstVisible = true;
    QPointF offset(contentOffset());
    QRectF blockAreaRect;    // Code or block quote rect.
    bool inBlockArea = false;

    bool clipTop = false;
    bool drawBlock = false;
    qreal dy = 0.0;
    bool done = false;

    const QColor &color = MarkdownHighlighter::codeBlockBackgroundColor();
    const int cornerRadius = 5;

    while (block.isValid() && !done) {
        const QRectF r = blockBoundingRect(block).translated(offset);
        const int state = block.userState();

        if (!inBlockArea && MarkdownHighlighter::isCodeBlock(state)) {
            // skip the backticks
            if (!block.text().startsWith(QLatin1String("```")) &&
                !block.text().startsWith(QLatin1String("~~~"))) {
                blockAreaRect = r;
                dy = 0.0;
                inBlockArea = true;
            }

            // If this is the first visible block within the viewport
            // and if the previous block is part of the text block area,
            // then the rectangle to draw for the block area will have
            // its top clipped by the viewport and will need to be
            // drawn specially.
            const int prevBlockState = block.previous().userState();
            if (firstVisible &&
                MarkdownHighlighter::isCodeBlock(prevBlockState)) {
                clipTop = true;
            }
        }
        // Else if the block ends a text block area...
        else if (inBlockArea && MarkdownHighlighter::isCodeBlockEnd(state)) {
            drawBlock = true;
            inBlockArea = false;
            blockAreaRect.setHeight(dy);
        }
        // If the block is at the end of the document and ends a text
        // block area...
        //
        if (inBlockArea && block == this->document()->lastBlock()) {
            drawBlock = true;
            inBlockArea = false;
            dy += r.height();
            blockAreaRect.setHeight(dy);
        }
        offset.ry() += r.height();
        dy += r.height();

        // If this is the last text block visible within the viewport...
        if (offset.y() > viewportRect.height()) {
            if (inBlockArea) {
                blockAreaRect.setHeight(dy);
                drawBlock = true;
            }

            // Finished drawing.
            done = true;
        }
        // If this is the last text block visible within the viewport...
        if (offset.y() > viewportRect.height()) {
            if (inBlockArea) {
                blockAreaRect.setHeight(dy);
                drawBlock = true;
            }
            // Finished drawing.
            done = true;
        }

        if (drawBlock) {
            painter.setCompositionMode(QPainter::CompositionMode_SourceOver);
            painter.setPen(Qt::NoPen);
            painter.setBrush(QBrush(color));

            // If the first visible block is "clipped" such that the previous
            // block is part of the text block area, then only draw a rectangle
            // with the bottom corners rounded, and with the top corners square
            // to reflect that the first visible block is part of a larger block
            // of text.
            //
            if (clipTop) {
                QPainterPath path;
                path.setFillRule(Qt::WindingFill);
                path.addRoundedRect(blockAreaRect, cornerRadius, cornerRadius);
                qreal adjustedHeight = blockAreaRect.height() / 2;
                path.addRect(blockAreaRect.adjusted(0, 0, 0, -adjustedHeight));
                painter.drawPath(path.simplified());
                clipTop = false;
            }
            // Else draw the entire rectangle with all corners rounded.
            else {
                painter.drawRoundedRect(blockAreaRect, cornerRadius,
                                        cornerRadius);
            }

            drawBlock = false;
        }

        // this fixes the RTL bug of QPlainTextEdit
        // https://bugreports.qt.io/browse/QTBUG-7516
        if (block.text().isRightToLeft()) {
            QTextLayout *layout = block.layout();
            // opt = document()->defaultTextOption();
            QTextOption opt = QTextOption(Qt::AlignRight);
            opt.setTextDirection(Qt::RightToLeft);
            layout->setTextOption(opt);
        }

        // Current line highlight
        QTextCursor cursor = textCursor();
        if (highlightCurrentLine() && cursor.block() == block) {
            QTextLine line =
                block.layout()->lineForTextPosition(cursor.positionInBlock());
            QRectF lineRect = line.rect();
            lineRect.moveTop(lineRect.top() + r.top());
            lineRect.setLeft(0.);
            lineRect.setRight(viewportRect.width());
            painter.fillRect(lineRect.toAlignedRect(),
                             currentLineHighlightColor());
        }

        block = block.next();
        firstVisible = false;
    }

    painter.end();
    QPlainTextEdit::paintEvent(e);
}
    bool handleCharRemoval(MarkdownHighlighter::RangeType type, int block,
                           int position) {
    if (!_highlighter) return false;

    auto range = _highlighter->findPositionInRanges(type, block, position);
    if (range == QPair<int, int>{-1, -1}) return false;

    int charToRemovePos = range.first;
    if (position == range.first) charToRemovePos = range.second;

    QTextCursor cursor = textCursor();
    auto gpos = cursor.position();

    if (charToRemovePos > position) {
        cursor.setPosition(gpos + (charToRemovePos - (position + 1)));
    } else {
        cursor.setPosition(gpos - (position - charToRemovePos + 1));
        gpos--;
    }

    cursor.deleteChar();
    cursor.setPosition(gpos);
    setTextCursor(cursor);
    return false;
}
    void resizeEvent(QResizeEvent *event) override {
    QPlainTextEdit::resizeEvent(event);
    updateLineNumAreaGeometry();
}
    void setLineNumberLeftMarginOffset(int offset) {
    _lineNumberLeftMarginOffset = offset;
}
    int _lineNumberLeftMarginOffset = 0;
    void updateLineNumAreaGeometry() {
    const auto contentsRect = this->contentsRect();
    const QRect newGeometry = {contentsRect.left(), contentsRect.top(),
                               _lineNumArea->sizeHint().width(),
                               contentsRect.height()};
    auto oldGeometry = _lineNumArea->geometry();
    if (newGeometry != oldGeometry) {
        _lineNumArea->setGeometry(newGeometry);
    }
}
    void updateLineNumberArea(const QRect rect, int dy) {
    if (dy)
        _lineNumArea->scroll(0, dy);
    else
        _lineNumArea->update(0, rect.y(), _lineNumArea->sizeHint().width(),
                             rect.height());

    updateLineNumAreaGeometry();

    if (rect.contains(viewport()->rect())) {
        updateLineNumberAreaWidth(0);
    }
}
    Q_SLOT void updateLineNumberAreaWidth(int) {
    QSignalBlocker blocker(this);
    const auto oldMargins = viewportMargins();
    const int width =
        _lineNumArea->isLineNumAreaEnabled()
            ? _lineNumArea->sizeHint().width() + _lineNumberLeftMarginOffset
            : oldMargins.left();
    const auto newMargins = QMargins{width, oldMargins.top(),
                                     oldMargins.right(), oldMargins.bottom()};

    if (newMargins != oldMargins) {
        setViewportMargins(newMargins);
    }

    // Grow lineNumArea font-size with the font size of the editor
    const int pointSize = this->font().pointSize();
    if (pointSize > 0) {
        QFont font = _lineNumArea->font();
        font.setPointSize(pointSize);
        _lineNumArea->setFont(font);
    }
}
    bool _handleBracketClosingUsed;
    LineNumArea *_lineNumArea;
    void urlClicked(QString url);
    void zoomIn();
    void zoomOut();
};

class ControlPanelDialog {
    Q_OBJECT

public:
    explicit ControlPanelDialog(MainWindow *mainWindow, InkCanvas *targetCanvas, QWidget *parent = nullptr);
  private:
    void applyChanges() {
    if (!canvas) return;

    BackgroundStyle style = static_cast<BackgroundStyle>(
        styleCombo->currentData().toInt()
    );

    canvas->setBackgroundStyle(style);
    canvas->setBackgroundColor(selectedColor);
    canvas->setBackgroundDensity(densitySpin->value());
    canvas->update();
    canvas->saveBackgroundMetadata();

    // ✅ Save these settings as defaults for new tabs
    if (mainWindowRef) {
        mainWindowRef->saveDefaultBackgroundSettings(style, selectedColor, densitySpin->value());
    }

    // ✅ Apply button mappings back to MainWindow with internal keys
    if (mainWindowRef) {
        for (const QString &buttonKey : holdMappingCombos.keys()) {
            QString displayString = holdMappingCombos[buttonKey]->currentText();
            QString internalKey = ButtonMappingHelper::displayToInternalKey(displayString, true);  // true = isDialMode
            mainWindowRef->setHoldMapping(buttonKey, internalKey);
        }
        for (const QString &buttonKey : pressMappingCombos.keys()) {
            QString displayString = pressMappingCombos[buttonKey]->currentText();
            QString internalKey = ButtonMappingHelper::displayToInternalKey(displayString, false);  // false = isAction
            mainWindowRef->setPressMapping(buttonKey, internalKey);
        }

        // ✅ Save to persistent settings
        mainWindowRef->saveButtonMappings();

        // ✅ Apply theme settings
        mainWindowRef->setUseCustomAccentColor(useCustomAccentCheckbox->isChecked());
        if (selectedAccentColor.isValid()) {
            mainWindowRef->setCustomAccentColor(selectedAccentColor);
        }

        // ✅ Apply color palette setting
        mainWindowRef->setUseBrighterPalette(useBrighterPaletteCheckbox->isChecked());
    }
}
    void chooseColor() {
    QColor chosen = QColorDialog::getColor(selectedColor, this, tr("Select Background Color"));
    if (chosen.isValid()) {
        selectedColor = chosen;
        colorButton->setStyleSheet(QString("background-color: %1").arg(selectedColor.name()));
    }
}
    void addKeyboardMapping() {
    // Step 1: Capture key sequence
    KeyCaptureDialog captureDialog(this);
    if (captureDialog.exec() != QDialog::Accepted) {
        return;
    }

    QString keySequence = captureDialog.getCapturedKeySequence();
    if (keySequence.isEmpty()) {
        return;
    }

    // Check if key sequence already exists
    if (mainWindowRef && mainWindowRef->getKeyboardMappings().contains(keySequence)) {
        QMessageBox::warning(this, tr("Key Already Mapped"),
            tr("The key sequence '%1' is already mapped. Please choose a different key combination.").arg(keySequence));
        return;
    }

    // Step 2: Choose action
    QStringList actions = ButtonMappingHelper::getTranslatedActions();
    bool ok;
    QString selectedAction = QInputDialog::getItem(this, tr("Select Action"),
        tr("Choose the action to perform when '%1' is pressed:").arg(keySequence),
        actions, 0, false, &ok);

    if (!ok || selectedAction.isEmpty()) {
        return;
    }

    // Convert display name to internal key
    QString internalKey = ButtonMappingHelper::displayToInternalKey(selectedAction, false);

    // Add the mapping
    if (mainWindowRef) {
        mainWindowRef->addKeyboardMapping(keySequence, internalKey);

        // Update table
        int row = keyboardTable->rowCount();
        keyboardTable->insertRow(row);
        keyboardTable->setItem(row, 0, new QTableWidgetItem(keySequence));
        keyboardTable->setItem(row, 1, new QTableWidgetItem(selectedAction));
    }
}
    void removeKeyboardMapping() {
    int currentRow = keyboardTable->currentRow();
    if (currentRow < 0) {
        QMessageBox::information(this, tr("No Selection"), tr("Please select a mapping to remove."));
        return;
    }

    QTableWidgetItem *keyItem = keyboardTable->item(currentRow, 0);
    if (!keyItem) return;

    QString keySequence = keyItem->text();

    // Confirm removal
    int ret = QMessageBox::question(this, tr("Remove Mapping"),
        tr("Are you sure you want to remove the keyboard shortcut '%1'?").arg(keySequence),
        QMessageBox::Yes | QMessageBox::No, QMessageBox::No);

    if (ret == QMessageBox::Yes) {
        // Remove from MainWindow
        if (mainWindowRef) {
            mainWindowRef->removeKeyboardMapping(keySequence);
        }

        // Remove from table
        keyboardTable->removeRow(currentRow);
    }
}
    void openControllerMapping() {
    if (!mainWindowRef) {
        QMessageBox::warning(this, tr("Error"), tr("MainWindow reference not available."));
        return;
    }

    SDLControllerManager *controllerManager = mainWindowRef->getControllerManager();
    if (!controllerManager) {
        QMessageBox::warning(this, tr("Controller Not Available"),
            tr("Controller manager is not available. Please ensure a controller is connected and restart the application."));
        return;
    }

    if (!controllerManager->getJoystick()) {
        QMessageBox::warning(this, tr("No Controller Detected"),
            tr("No controller is currently connected. Please connect your controller and restart the application."));
        return;
    }

    ControllerMappingDialog dialog(controllerManager, this);
    dialog.exec();
}
    void reconnectController() {
    if (!mainWindowRef) {
        QMessageBox::warning(this, tr("Error"), tr("MainWindow reference not available."));
        return;
    }

    SDLControllerManager *controllerManager = mainWindowRef->getControllerManager();
    if (!controllerManager) {
        QMessageBox::warning(this, tr("Controller Not Available"),
            tr("Controller manager is not available."));
        return;
    }

    // Show reconnecting message
    controllerStatusLabel->setText(tr("🔄 Reconnecting..."));
    controllerStatusLabel->setStyleSheet("color: orange;");

    // Force the UI to update immediately
    QApplication::processEvents();

    // Attempt to reconnect using thread-safe method
    QMetaObject::invokeMethod(controllerManager, "reconnect", Qt::BlockingQueuedConnection);

    // Update status after reconnection attempt
    updateControllerStatus();

    // Show result message
    if (controllerManager->getJoystick()) {
        // Reconnect the controller signals in MainWindow
        mainWindowRef->reconnectControllerSignals();

        QMessageBox::information(this, tr("Reconnection Successful"),
            tr("Controller has been successfully reconnected!"));
    } else {
        QMessageBox::warning(this, tr("Reconnection Failed"),
            tr("Failed to reconnect controller. Please ensure your controller is powered on and in pairing mode, then try again."));
    }
}
  private:
    InkCanvas *canvas;
    QTabWidget *tabWidget;
    QWidget *backgroundTab;
    QComboBox *styleCombo;
    QPushButton *colorButton;
    QSpinBox *densitySpin;
    QPushButton *applyButton;
    QPushButton *okButton;
    QPushButton *cancelButton;
    QColor selectedColor;
    void createBackgroundTab() {
    backgroundTab = new QWidget(this);

    QLabel *styleLabel = new QLabel(tr("Background Style:"));
    styleCombo = new QComboBox();
    styleCombo->addItem(tr("None"), static_cast<int>(BackgroundStyle::None));
    styleCombo->addItem(tr("Grid"), static_cast<int>(BackgroundStyle::Grid));
    styleCombo->addItem(tr("Lines"), static_cast<int>(BackgroundStyle::Lines));

    QLabel *colorLabel = new QLabel(tr("Background Color:"));
    colorButton = new QPushButton();
    connect(colorButton, &QPushButton::clicked, this, &ControlPanelDialog::chooseColor);

    QLabel *densityLabel = new QLabel(tr("Density:"));
    densitySpin = new QSpinBox();
    densitySpin->setRange(10, 200);
    densitySpin->setSuffix(" px");
    densitySpin->setSingleStep(5);

    QGridLayout *layout = new QGridLayout(backgroundTab);
    layout->addWidget(styleLabel, 0, 0);
    layout->addWidget(styleCombo, 0, 1);
    layout->addWidget(colorLabel, 1, 0);
    layout->addWidget(colorButton, 1, 1);
    layout->addWidget(densityLabel, 2, 0);
    layout->addWidget(densitySpin, 2, 1);
    // layout->setColumnStretch(1, 1); // Stretch the second column
    layout->setRowStretch(3, 1); // Stretch the last row
}
    void loadFromCanvas() {
    styleCombo->setCurrentIndex(static_cast<int>(canvas->getBackgroundStyle()));
    densitySpin->setValue(canvas->getBackgroundDensity());
    selectedColor = canvas->getBackgroundColor();

    colorButton->setStyleSheet(QString("background-color: %1").arg(selectedColor.name()));

    if (mainWindowRef) {
        for (const QString &buttonKey : holdMappingCombos.keys()) {
            QString internalKey = mainWindowRef->getHoldMapping(buttonKey);
            QString displayString = ButtonMappingHelper::internalKeyToDisplay(internalKey, true);  // true = isDialMode
            int index = holdMappingCombos[buttonKey]->findText(displayString);
            if (index >= 0) holdMappingCombos[buttonKey]->setCurrentIndex(index);
        }

        for (const QString &buttonKey : pressMappingCombos.keys()) {
            QString internalKey = mainWindowRef->getPressMapping(buttonKey);
            QString displayString = ButtonMappingHelper::internalKeyToDisplay(internalKey, false);  // false = isAction
            int index = pressMappingCombos[buttonKey]->findText(displayString);
            if (index >= 0) pressMappingCombos[buttonKey]->setCurrentIndex(index);
        }

        // Load theme settings
        useCustomAccentCheckbox->setChecked(mainWindowRef->isUsingCustomAccentColor());

        // Get the stored custom accent color
        selectedAccentColor = mainWindowRef->getCustomAccentColor();

        accentColorButton->setStyleSheet(QString("background-color: %1").arg(selectedAccentColor.name()));
        accentColorButton->setEnabled(useCustomAccentCheckbox->isChecked());

        // Load color palette setting
        useBrighterPaletteCheckbox->setChecked(mainWindowRef->isUsingBrighterPalette());
    }
}
    MainWindow *mainWindowRef;
    InkCanvas *canvasRef;
    QWidget *performanceTab;
    QWidget *toolbarTab;
    void createToolbarTab() {
    toolbarTab = new QWidget(this);
    QVBoxLayout *toolbarLayout = new QVBoxLayout(toolbarTab);

    // ✅ Checkbox to show/hide benchmark controls
    QCheckBox *benchmarkVisibilityCheckbox = new QCheckBox(tr("Show Benchmark Controls"), toolbarTab);
    benchmarkVisibilityCheckbox->setChecked(mainWindowRef->areBenchmarkControlsVisible());
    toolbarLayout->addWidget(benchmarkVisibilityCheckbox);
    QLabel *benchmarkNote = new QLabel(tr("This will show/hide the benchmark controls on the toolbar. Press the clock button to start/stop the benchmark."));
    benchmarkNote->setWordWrap(true);
    benchmarkNote->setStyleSheet("color: gray; font-size: 10px;");
    toolbarLayout->addWidget(benchmarkNote);

    // ✅ Checkbox to show/hide zoom buttons
    QCheckBox *zoomButtonsVisibilityCheckbox = new QCheckBox(tr("Show Zoom Buttons"), toolbarTab);
    zoomButtonsVisibilityCheckbox->setChecked(mainWindowRef->areZoomButtonsVisible());
    toolbarLayout->addWidget(zoomButtonsVisibilityCheckbox);
    QLabel *zoomButtonsNote = new QLabel(tr("This will show/hide the 0.5x, 1x, and 2x zoom buttons on the toolbar"));
    zoomButtonsNote->setWordWrap(true);
    zoomButtonsNote->setStyleSheet("color: gray; font-size: 10px;");
    toolbarLayout->addWidget(zoomButtonsNote);

    QCheckBox *scrollOnTopCheckBox = new QCheckBox(tr("Scroll on Top after Page Switching"), toolbarTab);
    scrollOnTopCheckBox->setChecked(mainWindowRef->isScrollOnTopEnabled());
    toolbarLayout->addWidget(scrollOnTopCheckBox);
    QLabel *scrollNote = new QLabel(tr("Enabling this will make the page scroll to the top after switching to a new page."));
    scrollNote->setWordWrap(true);
    scrollNote->setStyleSheet("color: gray; font-size: 10px;");
    toolbarLayout->addWidget(scrollNote);

    toolbarLayout->addStretch();
    toolbarTab->setLayout(toolbarLayout);
    tabWidget->addTab(toolbarTab, tr("Features"));

    // Connect the checkbox
    connect(benchmarkVisibilityCheckbox, &QCheckBox::toggled, mainWindowRef, &MainWindow::setBenchmarkControlsVisible);
    connect(zoomButtonsVisibilityCheckbox, &QCheckBox::toggled, mainWindowRef, &MainWindow::setZoomButtonsVisible);
    connect(scrollOnTopCheckBox, &QCheckBox::toggled, mainWindowRef, &MainWindow::setScrollOnTopEnabled);
}
    void createPerformanceTab() {
    performanceTab = new QWidget();
    QVBoxLayout *layout = new QVBoxLayout(performanceTab);

    QCheckBox *previewToggle = new QCheckBox(tr("Enable Low-Resolution PDF Previews"));
    previewToggle->setChecked(mainWindowRef->isLowResPreviewEnabled());

    connect(previewToggle, &QCheckBox::toggled, mainWindowRef, &MainWindow::setLowResPreviewEnabled);

    QLabel *note = new QLabel(tr("Disabling this may improve dial smoothness on low-end devices."));
    note->setWordWrap(true);
    note->setStyleSheet("color: gray; font-size: 10px;");

    QLabel *dpiLabel = new QLabel(tr("PDF Rendering DPI:"));
    QComboBox *dpiSelector = new QComboBox();
    dpiSelector->addItems({"96", "192", "288", "384", "480"});
    dpiSelector->setCurrentText(QString::number(mainWindowRef->getPdfDPI()));

    connect(dpiSelector, &QComboBox::currentTextChanged, this, [=](const QString &value) {
        mainWindowRef->setPdfDPI(value.toInt());
    });

    QLabel *notePDF = new QLabel(tr("Adjust how the PDF is rendered. Higher DPI means better quality but slower performance. DO NOT CHANGE THIS OPTION WHEN MULTIPLE TABS ARE OPEN. THIS MAY LEAD TO UNDEFINED BEHAVIOR!"));
    notePDF->setWordWrap(true);
    notePDF->setStyleSheet("color: gray; font-size: 10px;");

    layout->addWidget(previewToggle);
    layout->addWidget(note);
    layout->addWidget(dpiLabel);
    layout->addWidget(dpiSelector);
    layout->addWidget(notePDF);

    layout->addStretch();

    // return performanceTab;
}
    QWidget *controllerMappingTab;
    QPushButton *reconnectButton;
    QLabel *controllerStatusLabel;
    QMap<QString, QComboBox*> holdMappingCombos;
    QMap<QString, QComboBox*> pressMappingCombos;
    void createButtonMappingTab() {
    QWidget *buttonTab = new QWidget(this);
    QVBoxLayout *layout = new QVBoxLayout(buttonTab);

    QStringList buttonKeys = ButtonMappingHelper::getInternalButtonKeys();
    QStringList buttonDisplayNames = ButtonMappingHelper::getTranslatedButtons();
    QStringList dialModes = ButtonMappingHelper::getTranslatedDialModes();
    QStringList actions = ButtonMappingHelper::getTranslatedActions();

    for (int i = 0; i < buttonKeys.size(); ++i) {
        const QString &buttonKey = buttonKeys[i];
        const QString &buttonDisplayName = buttonDisplayNames[i];

        QHBoxLayout *h = new QHBoxLayout();
        h->addWidget(new QLabel(buttonDisplayName));  // Use translated button name

        QComboBox *holdCombo = new QComboBox();
        holdCombo->addItems(dialModes);  // Add translated dial mode names
        holdMappingCombos[buttonKey] = holdCombo;
        h->addWidget(new QLabel(tr("Hold:")));
        h->addWidget(holdCombo);

        QComboBox *pressCombo = new QComboBox();
        pressCombo->addItems(actions);  // Add translated action names
        pressMappingCombos[buttonKey] = pressCombo;
        h->addWidget(new QLabel(tr("Press:")));
        h->addWidget(pressCombo);

        layout->addLayout(h);
    }

    layout->addStretch();
    buttonTab->setLayout(layout);
    tabWidget->addTab(buttonTab, tr("Button Mapping"));
}
    void createControllerMappingTab() {
    controllerMappingTab = new QWidget(this);
    QVBoxLayout *layout = new QVBoxLayout(controllerMappingTab);

    // Instructions
    QLabel *instructionLabel = new QLabel(tr("Configure physical controller button mappings for your Joy-Con or other controller:"), controllerMappingTab);
    instructionLabel->setWordWrap(true);
    layout->addWidget(instructionLabel);

    QLabel *noteLabel = new QLabel(tr("Note: This maps your physical controller buttons to the logical Joy-Con functions used by the application. "
                                     "After setting up the physical mapping, you can configure what actions each logical button performs in the 'Button Mapping' tab."), controllerMappingTab);
    noteLabel->setWordWrap(true);
    noteLabel->setStyleSheet("color: gray; font-size: 10px; margin-bottom: 10px;");
    layout->addWidget(noteLabel);

    // Button to open controller mapping dialog
    QPushButton *openMappingButton = new QPushButton(tr("Configure Controller Mapping"), controllerMappingTab);
    openMappingButton->setMinimumHeight(40);
    connect(openMappingButton, &QPushButton::clicked, this, &ControlPanelDialog::openControllerMapping);
    layout->addWidget(openMappingButton);

    // Button to reconnect controller
    reconnectButton = new QPushButton(tr("Reconnect Controller"), controllerMappingTab);
    reconnectButton->setMinimumHeight(40);
    reconnectButton->setStyleSheet("QPushButton { background-color: #4CAF50; color: white; font-weight: bold; }");
    connect(reconnectButton, &QPushButton::clicked, this, &ControlPanelDialog::reconnectController);
    layout->addWidget(reconnectButton);

    // Status information
    QLabel *statusLabel = new QLabel(tr("Current controller status:"), controllerMappingTab);
    statusLabel->setStyleSheet("font-weight: bold; margin-top: 20px;");
    layout->addWidget(statusLabel);

    // Dynamic status label
    controllerStatusLabel = new QLabel(controllerMappingTab);
    updateControllerStatus();
    layout->addWidget(controllerStatusLabel);

    layout->addStretch();

    tabWidget->addTab(controllerMappingTab, tr("Controller Mapping"));
}
    void createKeyboardMappingTab() {
    keyboardTab = new QWidget(this);
    QVBoxLayout *layout = new QVBoxLayout(keyboardTab);

    // Instructions
    QLabel *instructionLabel = new QLabel(tr("Configure custom keyboard shortcuts for application actions:"), keyboardTab);
    instructionLabel->setWordWrap(true);
    layout->addWidget(instructionLabel);

    // Table to show current mappings
    keyboardTable = new QTableWidget(0, 2, keyboardTab);
    keyboardTable->setHorizontalHeaderLabels({tr("Key Sequence"), tr("Action")});
    keyboardTable->horizontalHeader()->setStretchLastSection(true);
    keyboardTable->setSelectionBehavior(QAbstractItemView::SelectRows);
    keyboardTable->setEditTriggers(QAbstractItemView::NoEditTriggers);
    layout->addWidget(keyboardTable);

    // Buttons
    QHBoxLayout *buttonLayout = new QHBoxLayout();
    addKeyboardMappingButton = new QPushButton(tr("Add Mapping"), keyboardTab);
    removeKeyboardMappingButton = new QPushButton(tr("Remove Mapping"), keyboardTab);

    buttonLayout->addWidget(addKeyboardMappingButton);
    buttonLayout->addWidget(removeKeyboardMappingButton);
    buttonLayout->addStretch();

    layout->addLayout(buttonLayout);

    // Connections
    connect(addKeyboardMappingButton, &QPushButton::clicked, this, &ControlPanelDialog::addKeyboardMapping);
    connect(removeKeyboardMappingButton, &QPushButton::clicked, this, &ControlPanelDialog::removeKeyboardMapping);

    // Load current mappings
    if (mainWindowRef) {
        QMap<QString, QString> mappings = mainWindowRef->getKeyboardMappings();
        keyboardTable->setRowCount(mappings.size());
        int row = 0;
        for (auto it = mappings.begin(); it != mappings.end(); ++it) {
            keyboardTable->setItem(row, 0, new QTableWidgetItem(it.key()));
            QString displayAction = ButtonMappingHelper::internalKeyToDisplay(it.value(), false);
            keyboardTable->setItem(row, 1, new QTableWidgetItem(displayAction));
            row++;
        }
    }

    tabWidget->addTab(keyboardTab, tr("Keyboard Shortcuts"));
}
    QWidget *keyboardTab;
    QTableWidget *keyboardTable;
    QPushButton *addKeyboardMappingButton;
    QPushButton *removeKeyboardMappingButton;
    QWidget *themeTab;
    QCheckBox *useCustomAccentCheckbox;
    QPushButton *accentColorButton;
    QColor selectedAccentColor;
    QCheckBox *useBrighterPaletteCheckbox;
    void createThemeTab() {
    themeTab = new QWidget(this);
    QVBoxLayout *layout = new QVBoxLayout(themeTab);

    // Custom accent color
    useCustomAccentCheckbox = new QCheckBox(tr("Use Custom Accent Color"), themeTab);
    layout->addWidget(useCustomAccentCheckbox);

    QLabel *accentColorLabel = new QLabel(tr("Accent Color:"), themeTab);
    accentColorButton = new QPushButton(themeTab);
    accentColorButton->setFixedSize(100, 30);
    connect(accentColorButton, &QPushButton::clicked, this, &ControlPanelDialog::chooseAccentColor);

    QHBoxLayout *accentColorLayout = new QHBoxLayout();
    accentColorLayout->addWidget(accentColorLabel);
    accentColorLayout->addWidget(accentColorButton);
    accentColorLayout->addStretch();
    layout->addLayout(accentColorLayout);

    QLabel *accentColorNote = new QLabel(tr("When enabled, use a custom accent color instead of the system accent color for the toolbar, dial, and tab selection."));
    accentColorNote->setWordWrap(true);
    accentColorNote->setStyleSheet("color: gray; font-size: 10px;");
    layout->addWidget(accentColorNote);

    // Enable/disable accent color button based on checkbox
    connect(useCustomAccentCheckbox, &QCheckBox::toggled, accentColorButton, &QPushButton::setEnabled);
    connect(useCustomAccentCheckbox, &QCheckBox::toggled, accentColorLabel, &QLabel::setEnabled);

    // Color palette preference
    useBrighterPaletteCheckbox = new QCheckBox(tr("Use Brighter Color Palette"), themeTab);
    layout->addWidget(useBrighterPaletteCheckbox);

    QLabel *paletteNote = new QLabel(tr("When enabled, use brighter colors (good for dark PDF backgrounds). When disabled, use darker colors (good for light PDF backgrounds). This setting is independent of the UI theme."));
    paletteNote->setWordWrap(true);
    paletteNote->setStyleSheet("color: gray; font-size: 10px;");
    layout->addWidget(paletteNote);

    layout->addStretch();

    tabWidget->addTab(themeTab, tr("Theme"));
}
    void chooseAccentColor() {
    QColor chosen = QColorDialog::getColor(selectedAccentColor, this, tr("Select Accent Color"));
    if (chosen.isValid()) {
        selectedAccentColor = chosen;
        accentColorButton->setStyleSheet(QString("background-color: %1").arg(selectedAccentColor.name()));
    }
}
    void updateControllerStatus() {
    if (!mainWindowRef || !controllerStatusLabel) return;

    SDLControllerManager *controllerManager = mainWindowRef->getControllerManager();
    if (!controllerManager) {
        controllerStatusLabel->setText(tr("✗ Controller manager not available"));
        controllerStatusLabel->setStyleSheet("color: red;");
        return;
    }

    if (controllerManager->getJoystick()) {
        controllerStatusLabel->setText(tr("✓ Controller connected"));
        controllerStatusLabel->setStyleSheet("color: green; font-weight: bold;");
    } else {
        controllerStatusLabel->setText(tr("✗ No controller detected"));
        controllerStatusLabel->setStyleSheet("color: red; font-weight: bold;");
    }
}
};

class QPlainTextEditSearchWidget {
    Q_OBJECT

   public:
    enum SearchMode { PlainTextMode, WholeWordsMode, RegularExpressionMode };
    explicit QPlainTextEditSearchWidget(QPlainTextEdit *parent = nullptr) {
    ui->setupUi(this);
    _textEdit = parent;
    _darkMode = false;
    hide();
    ui->searchCountLabel->setStyleSheet(QStringLiteral("* {color: grey}"));
    // hiding will leave a open space in the horizontal layout
    ui->searchCountLabel->setEnabled(false);
    _currentSearchResult = 0;
    _searchResultCount = 0;

    connect(ui->closeButton, &QPushButton::clicked, this,
            &QPlainTextEditSearchWidget::deactivate);
    connect(ui->searchLineEdit, &QLineEdit::textChanged, this,
            &QPlainTextEditSearchWidget::searchLineEditTextChanged);
    connect(ui->searchDownButton, &QPushButton::clicked, this,
            &QPlainTextEditSearchWidget::doSearchDown);
    connect(ui->searchUpButton, &QPushButton::clicked, this,
            &QPlainTextEditSearchWidget::doSearchUp);
    connect(ui->replaceToggleButton, &QPushButton::toggled, this,
            &QPlainTextEditSearchWidget::setReplaceMode);
    connect(ui->replaceButton, &QPushButton::clicked, this,
            &QPlainTextEditSearchWidget::doReplace);
    connect(ui->replaceAllButton, &QPushButton::clicked, this,
            &QPlainTextEditSearchWidget::doReplaceAll);

    connect(&_debounceTimer, &QTimer::timeout, this,
            &QPlainTextEditSearchWidget::performSearch);

    installEventFilter(this);
    ui->searchLineEdit->installEventFilter(this);
    ui->replaceLineEdit->installEventFilter(this);

#ifdef Q_OS_MAC
    // set the spacing to 8 for OS X
    layout()->setSpacing(8);
    ui->buttonFrame->layout()->setSpacing(9);

    // set the margin to 0 for the top buttons for OS X
    QString buttonStyle = QStringLiteral("QPushButton {margin: 0}");
    ui->closeButton->setStyleSheet(buttonStyle);
    ui->searchDownButton->setStyleSheet(buttonStyle);
    ui->searchUpButton->setStyleSheet(buttonStyle);
    ui->replaceToggleButton->setStyleSheet(buttonStyle);
    ui->matchCaseSensitiveButton->setStyleSheet(buttonStyle);
#endif
}
    bool doSearch(bool searchDown = true, bool allowRestartAtTop = true,
                  bool updateUI = true) {
    if (_debounceTimer.isActive()) {
        stopDebounce();
    }

    const QString text = ui->searchLineEdit->text();

    if (text.isEmpty()) {
        if (updateUI) {
            ui->searchLineEdit->setStyleSheet(QLatin1String(""));
        }

        return false;
    }

    const int searchMode = ui->modeComboBox->currentIndex();
    const bool caseSensitive = ui->matchCaseSensitiveButton->isChecked();

    QFlags<QTextDocument::FindFlag> options =
        searchDown ? QTextDocument::FindFlag(0) : QTextDocument::FindBackward;
    if (searchMode == WholeWordsMode) {
        options |= QTextDocument::FindWholeWords;
    }

    if (caseSensitive) {
        options |= QTextDocument::FindCaseSensitively;
    }

    // block signal to reduce too many signals being fired and too many updates
    _textEdit->blockSignals(true);

    bool found =
        searchMode == RegularExpressionMode
            ?
#if (QT_VERSION >= QT_VERSION_CHECK(5, 13, 0))
            _textEdit->find(
                QRegularExpression(
                    text, caseSensitive
                              ? QRegularExpression::NoPatternOption
                              : QRegularExpression::CaseInsensitiveOption),
                options)
            :
#else
            _textEdit->find(QRegExp(text, caseSensitive ? Qt::CaseSensitive
                                                        : Qt::CaseInsensitive),
                            options)
            :
#endif
            _textEdit->find(text, options);

    _textEdit->blockSignals(false);

    if (found) {
        const int result =
            searchDown ? ++_currentSearchResult : --_currentSearchResult;
        _currentSearchResult = std::min(result, _searchResultCount);

        updateSearchCountLabelText();
    }

    // start at the top (or bottom) if not found
    if (!found && allowRestartAtTop) {
        _textEdit->moveCursor(searchDown ? QTextCursor::Start
                                         : QTextCursor::End);
        found =
            searchMode == RegularExpressionMode
                ?
#if (QT_VERSION >= QT_VERSION_CHECK(5, 13, 0))
                _textEdit->find(
                    QRegularExpression(
                        text, caseSensitive
                                  ? QRegularExpression::NoPatternOption
                                  : QRegularExpression::CaseInsensitiveOption),
                    options)
                :
#else
                _textEdit->find(
                    QRegExp(text, caseSensitive ? Qt::CaseSensitive
                                                : Qt::CaseInsensitive),
                    options)
                :
#endif
                _textEdit->find(text, options);

        if (found && updateUI) {
            _currentSearchResult = searchDown ? 1 : _searchResultCount;
            updateSearchCountLabelText();
        }
    }

    if (updateUI) {
        const QRect rect = _textEdit->cursorRect();
        QMargins margins = _textEdit->layout()->contentsMargins();
        const int searchWidgetHotArea = _textEdit->height() - this->height();
        const int marginBottom =
            (rect.y() > searchWidgetHotArea) ? (this->height() + 10) : 0;

        // move the search box a bit up if we would block the search result
        if (margins.bottom() != marginBottom) {
            margins.setBottom(marginBottom);
            _textEdit->layout()->setContentsMargins(margins);
        }

        // add a background color according if we found the text or not
        const QString bgColorCode = _darkMode
                                        ? (found ? QStringLiteral("#135a13")
                                                 : QStringLiteral("#8d2b36"))
                                    : found ? QStringLiteral("#D5FAE2")
                                            : QStringLiteral("#FAE9EB");
        const QString fgColorCode =
            _darkMode ? QStringLiteral("#cccccc") : QStringLiteral("#404040");

        ui->searchLineEdit->setStyleSheet(
            QStringLiteral("* { background: ") + bgColorCode +
            QStringLiteral("; color: ") + fgColorCode + QStringLiteral("; }"));

        // restore the search extra selections after the find command
        this->setSearchExtraSelections();
    }

    return found;
}
    void setDarkMode(bool enabled) {
    _darkMode = enabled;
}
    ~QPlainTextEditSearchWidget() { delete ui; }
    void setSearchText(const QString &searchText) {
    ui->searchLineEdit->setText(searchText);
}
    void setSearchMode(SearchMode searchMode) {
    ui->modeComboBox->setCurrentIndex(searchMode);
}
    void setDebounceDelay(uint debounceDelay) {
    _debounceTimer.setInterval(static_cast<int>(debounceDelay));
}
    void activate(bool focus) {
    setReplaceMode(ui->modeComboBox->currentIndex() !=
                   SearchMode::PlainTextMode);
    show();

    // preset the selected text as search text if there is any and there is no
    // other search text
    const QString selectedText = _textEdit->textCursor().selectedText();
    if (!selectedText.isEmpty() && ui->searchLineEdit->text().isEmpty()) {
        ui->searchLineEdit->setText(selectedText);
    }

    if (focus) {
        ui->searchLineEdit->setFocus();
    }

    ui->searchLineEdit->selectAll();
    updateSearchExtraSelections();
    doSearchDown();
}
    void clearSearchExtraSelections() {
    _searchExtraSelections.clear();
    setSearchExtraSelections();
}
    void updateSearchExtraSelections() {
    _searchExtraSelections.clear();
    const auto textCursor = _textEdit->textCursor();
    _textEdit->moveCursor(QTextCursor::Start);
    const QColor color = selectionColor;
    QTextCharFormat extraFmt;
    extraFmt.setBackground(color);
    int findCounter = 0;
    const int searchMode = ui->modeComboBox->currentIndex();

    while (doSearch(true, false, false)) {
        findCounter++;

        // prevent infinite loops from regular expression searches like "$", "^"
        // or "\b"
        if (searchMode == RegularExpressionMode && findCounter >= 10000) {
            break;
        }

        QTextEdit::ExtraSelection extra = QTextEdit::ExtraSelection();
        extra.format = extraFmt;

        extra.cursor = _textEdit->textCursor();
        _searchExtraSelections.append(extra);
    }

    _textEdit->setTextCursor(textCursor);
    this->setSearchExtraSelections();
}
  private:
    Ui::QPlainTextEditSearchWidget *ui;
    int _searchResultCount;
    int _currentSearchResult;
    QList<QTextEdit::ExtraSelection> _searchExtraSelections;
    QColor selectionColor;
    QTimer _debounceTimer;
    QString _searchTerm;
    void setSearchExtraSelections() const {
    this->_textEdit->setExtraSelections(this->_searchExtraSelections);
}
    void stopDebounce() {
    _debounceTimer.stop();
    ui->searchDownButton->setEnabled(true);
    ui->searchUpButton->setEnabled(true);
}
  protected:
    QPlainTextEdit *_textEdit;
    bool _darkMode;
    bool eventFilter(QObject *obj, QEvent *event) override {
    if (event->type() == QEvent::KeyPress) {
        auto *keyEvent = static_cast<QKeyEvent *>(event);

        if (keyEvent->key() == Qt::Key_Escape) {
            deactivate();
            return true;
        } else if ((!_debounceTimer.isActive() &&
                    keyEvent->modifiers().testFlag(Qt::ShiftModifier) &&
                    (keyEvent->key() == Qt::Key_Return)) ||
                   (keyEvent->key() == Qt::Key_Up)) {
            doSearchUp();
            return true;
        } else if (!_debounceTimer.isActive() &&
                   ((keyEvent->key() == Qt::Key_Return) ||
                    (keyEvent->key() == Qt::Key_Down))) {
            doSearchDown();
            return true;
        } else if (!_debounceTimer.isActive() &&
                   keyEvent->key() == Qt::Key_F3) {
            doSearch(!keyEvent->modifiers().testFlag(Qt::ShiftModifier));
            return true;
        }

        //        if ((obj == ui->replaceLineEdit) && (keyEvent->key() ==
        //        Qt::Key_Tab)
        //                && ui->replaceToggleButton->isChecked()) {
        //            ui->replaceLineEdit->setFocus();
        //        }

        return false;
    }

    return QWidget::eventFilter(obj, event);
}
  public:
    void activate() {
    setReplaceMode(ui->modeComboBox->currentIndex() !=
                   SearchMode::PlainTextMode);
    show();

    // preset the selected text as search text if there is any and there is no
    // other search text
    const QString selectedText = _textEdit->textCursor().selectedText();
    if (!selectedText.isEmpty() && ui->searchLineEdit->text().isEmpty()) {
        ui->searchLineEdit->setText(selectedText);
    }

    if (focus) {
        ui->searchLineEdit->setFocus();
    }

    ui->searchLineEdit->selectAll();
    updateSearchExtraSelections();
    doSearchDown();
}
    void deactivate() {
    stopDebounce();

    hide();

    // Clear the search extra selections when closing the search bar
    clearSearchExtraSelections();

    _textEdit->setFocus();
}
    void doSearchDown() { doSearch(true); }
    void doSearchUp() { doSearch(false); }
    void setReplaceMode(bool enabled) {
    ui->replaceToggleButton->setChecked(enabled);
    ui->replaceLabel->setVisible(enabled);
    ui->replaceLineEdit->setVisible(enabled);
    ui->modeLabel->setVisible(enabled);
    ui->buttonFrame->setVisible(enabled);
    ui->matchCaseSensitiveButton->setVisible(enabled);
}
    void activateReplace() {
    // replacing is prohibited if the text edit is readonly
    if (_textEdit->isReadOnly()) {
        return;
    }

    ui->searchLineEdit->setText(_textEdit->textCursor().selectedText());
    ui->searchLineEdit->selectAll();
    activate();
    setReplaceMode(true);
}
    bool doReplace(bool forAll = false) {
    if (_textEdit->isReadOnly()) {
        return false;
    }

    QTextCursor cursor = _textEdit->textCursor();

    if (!forAll && cursor.selectedText().isEmpty()) {
        return false;
    }

    const int searchMode = ui->modeComboBox->currentIndex();
    if (searchMode == RegularExpressionMode) {
        QString text = cursor.selectedText();
        text.replace(QRegularExpression(ui->searchLineEdit->text()),
                     ui->replaceLineEdit->text());
        cursor.insertText(text);
    } else {
        cursor.insertText(ui->replaceLineEdit->text());
    }

    if (!forAll) {
        const int position = cursor.position();

        if (!doSearch(true)) {
            // restore the last cursor position if text wasn't found any more
            cursor.setPosition(position);
            _textEdit->setTextCursor(cursor);
        }
    }

    return true;
}
    void doReplaceAll() {
    if (_textEdit->isReadOnly()) {
        return;
    }

    // start at the top
    _textEdit->moveCursor(QTextCursor::Start);

    // replace until everything to the bottom is replaced
    while (doSearch(true, false) && doReplace(true)) {
    }
}
    void reset() {
    ui->searchLineEdit->clear();
    setSearchMode(SearchMode::PlainTextMode);
    setReplaceMode(false);
    ui->searchCountLabel->setEnabled(false);
}
    void doSearchCount() {
    // Note that we are moving the anchor, so the search will start from the top
    // again! Alternative: Restore cursor position afterward, but then we will
    // not know
    //              at what _currentSearchResult we currently are
    _textEdit->moveCursor(QTextCursor::Start, QTextCursor::MoveAnchor);

    bool found;
    _searchResultCount = 0;
    _currentSearchResult = 0;
    const int searchMode = ui->modeComboBox->currentIndex();

    do {
        found = doSearch(true, false, false);
        if (found) {
            _searchResultCount++;
        }

        // prevent infinite loops from regular expression searches like "$", "^"
        // or "\b"
        if (searchMode == RegularExpressionMode &&
            _searchResultCount >= 10000) {
            break;
        }
    } while (found);

    updateSearchCountLabelText();
}
  protected:
    void searchLineEditTextChanged(const QString &arg1) {
    _searchTerm = arg1;

    if (_debounceTimer.interval() != 0 && !_searchTerm.isEmpty()) {
        _debounceTimer.start();
        ui->searchDownButton->setEnabled(false);
        ui->searchUpButton->setEnabled(false);
    } else {
        performSearch();
    }
}
    void performSearch() {
    doSearchCount();
    updateSearchExtraSelections();
    doSearchDown();
}
    void updateSearchCountLabelText() {
    ui->searchCountLabel->setEnabled(true);
    ui->searchCountLabel->setText(QStringLiteral("%1/%2").arg(
        _currentSearchResult == 0 ? QChar('-')
                                  : QString::number(_currentSearchResult),
        _searchResultCount == 0 ? QChar('-')
                                : QString::number(_searchResultCount)));
}
    void setSearchSelectionColor(const QColor &color) {
    selectionColor = color;
}
  private:
    void on_modeComboBox_currentIndexChanged(int index) {
    Q_UNUSED(index)
    doSearchCount();
    doSearchDown();
}
    void on_matchCaseSensitiveButton_toggled(bool checked) {
    Q_UNUSED(checked)
    doSearchCount();
    doSearchDown();
}
};

class SDLControllerManager {
    Q_OBJECT
public:
    explicit SDLControllerManager(QObject *parent = nullptr);
    ~SDLControllerManager() {
    if (joystick) SDL_JoystickClose(joystick);
    if (sdlInitialized) SDL_QuitSubSystem(SDL_INIT_JOYSTICK);
}
    void setPhysicalButtonMapping(const QString &logicalButton, int physicalSDLButton) {
    physicalButtonMappings[logicalButton] = physicalSDLButton;
    saveControllerMappings();
}
    int getPhysicalButtonMapping(const QString &logicalButton) const {
    return physicalButtonMappings.value(logicalButton, -1);
}
    QMap<QString, int> getAllPhysicalMappings() const {
    return physicalButtonMappings;
}
    void saveControllerMappings() {
    QSettings settings("SpeedyNote", "App");
    settings.beginGroup("ControllerPhysicalMappings");
    for (auto it = physicalButtonMappings.begin(); it != physicalButtonMappings.end(); ++it) {
        settings.setValue(it.key(), it.value());
    }
    settings.endGroup();
}
    void loadControllerMappings() {
    QSettings settings("SpeedyNote", "App");
    settings.beginGroup("ControllerPhysicalMappings");
    QStringList keys = settings.allKeys();

    if (keys.isEmpty()) {
        // No saved mappings, use defaults
        physicalButtonMappings = getDefaultMappings();
        saveControllerMappings(); // Save defaults for next time
    } else {
        // Load saved mappings
        for (const QString &key : keys) {
            physicalButtonMappings[key] = settings.value(key).toInt();
        }
    }
    settings.endGroup();
}
    QStringList getAvailablePhysicalButtons() const {
    QStringList buttons;
    if (joystick) {
        int buttonCount = SDL_JoystickNumButtons(joystick);
        for (int i = 0; i < buttonCount; ++i) {
            buttons << getPhysicalButtonName(i);
        }
    } else {
        // If no joystick connected, show a reasonable range
        for (int i = 0; i < 20; ++i) {
            buttons << getPhysicalButtonName(i);
        }
    }
    return buttons;
}
    QString getPhysicalButtonName(int sdlButton) const {
    // Return human-readable names for physical SDL joystick buttons
    // Since we're using raw joystick buttons, we'll use generic names
    return QString("Button %1").arg(sdlButton);
}
    int getJoystickButtonCount() const {
    if (joystick) {
        return SDL_JoystickNumButtons(joystick);
    }
    return 0;
}
    QMap<QString, int> getDefaultMappings() const {
    // Default mappings for Joy-Con L using raw button indices
    // These will need to be adjusted based on actual Joy-Con button indices
    QMap<QString, int> defaults;
    defaults["LEFTSHOULDER"] = 4;    // L button
    defaults["RIGHTSHOULDER"] = 6;   // ZL button
    defaults["PADDLE2"] = 14;        // SL button
    defaults["PADDLE4"] = 15;        // SR button
    defaults["Y"] = 0;               // Up arrow
    defaults["A"] = 1;               // Down arrow
    defaults["B"] = 2;               // Left arrow
    defaults["X"] = 3;               // Right arrow
    defaults["LEFTSTICK"] = 10;      // Stick press
    defaults["START"] = 8;           // Minus button
    defaults["GUIDE"] = 13;          // Screenshot button
    return defaults;
}
    void buttonHeld(QString buttonName);
    void buttonReleased(QString buttonName);
    void buttonSinglePress(QString buttonName);
    void leftStickAngleChanged(int angle);
    void leftStickReleased();
    void rawButtonPressed(int sdlButton, QString buttonName);
  public:
    void start() {
    if (!sdlInitialized) {
        if (SDL_InitSubSystem(SDL_INIT_JOYSTICK) != 0) {
            qWarning() << "Failed to initialize SDL joystick subsystem:" << SDL_GetError();
            return;
        }
        sdlInitialized = true;
    }

    SDL_JoystickEventState(SDL_ENABLE);  // ✅ Enable joystick events

    // Look for any available joystick
    int numJoysticks = SDL_NumJoysticks();
    // qDebug() << "Found" << numJoysticks << "joystick(s)";

    for (int i = 0; i < numJoysticks; ++i) {
        const char* joystickName = SDL_JoystickNameForIndex(i);
        // qDebug() << "Joystick" << i << ":" << (joystickName ? joystickName : "Unknown");

        joystick = SDL_JoystickOpen(i);
        if (joystick) {
            // qDebug() << "Joystick connected!";
            // qDebug() << "Number of buttons:" << SDL_JoystickNumButtons(joystick);
            // qDebug() << "Number of axes:" << SDL_JoystickNumAxes(joystick);
            // qDebug() << "Number of hats:" << SDL_JoystickNumHats(joystick);
            break;
        }
    }

    if (!joystick) {
        qWarning() << "No joystick could be opened";
    }

    pollTimer->start(16); // 60 FPS polling
}
    void stop() {
    pollTimer->stop();
}
    void reconnect() {
    // Stop current polling
    pollTimer->stop();

    // Close existing joystick if open
    if (joystick) {
        SDL_JoystickClose(joystick);
        joystick = nullptr;
    }

    // Clear any cached state
    buttonPressTime.clear();
    buttonHeldEmitted.clear();
    lastAngle = -1;
    leftStickActive = false;
    buttonDetectionMode = false;

    // Re-initialize SDL joystick subsystem
    SDL_QuitSubSystem(SDL_INIT_JOYSTICK);
    if (SDL_InitSubSystem(SDL_INIT_JOYSTICK) != 0) {
        qWarning() << "Failed to re-initialize SDL joystick subsystem:" << SDL_GetError();
        sdlInitialized = false;
        return;
    }
    sdlInitialized = true;

    SDL_JoystickEventState(SDL_ENABLE);

    // Look for any available joystick
    int numJoysticks = SDL_NumJoysticks();
    qDebug() << "Reconnect: Found" << numJoysticks << "joystick(s)";

    for (int i = 0; i < numJoysticks; ++i) {
        const char* joystickName = SDL_JoystickNameForIndex(i);
        qDebug() << "Reconnect: Trying joystick" << i << ":" << (joystickName ? joystickName : "Unknown");

        joystick = SDL_JoystickOpen(i);
        if (joystick) {
            qDebug() << "Reconnect: Joystick connected successfully!";
            qDebug() << "Number of buttons:" << SDL_JoystickNumButtons(joystick);
            qDebug() << "Number of axes:" << SDL_JoystickNumAxes(joystick);
            qDebug() << "Number of hats:" << SDL_JoystickNumHats(joystick);
            break;
        }
    }

    if (!joystick) {
        qWarning() << "Reconnect: No joystick could be opened";
    }

    // Restart polling
    pollTimer->start(16); // 60 FPS polling
}
    void startButtonDetection() {
    buttonDetectionMode = true;
}
    void stopButtonDetection() {
    buttonDetectionMode = false;
}
  private:
    QTimer *pollTimer;
    SDL_Joystick *joystick = nullptr;
    bool sdlInitialized = false;
    bool leftStickActive = false;
    bool buttonDetectionMode = false;
    int lastAngle = -1;
    QString getButtonName(Uint8 sdlButton) {
    // This method is now deprecated in favor of getLogicalButtonName
    return getLogicalButtonName(sdlButton);
}
    QString getLogicalButtonName(Uint8 sdlButton) {
    // Find which logical button this physical button is mapped to
    for (auto it = physicalButtonMappings.begin(); it != physicalButtonMappings.end(); ++it) {
        if (it.value() == sdlButton) {
            return it.key(); // Return the logical button name
        }
    }
    return QString(); // Return empty string if not mapped
}
    QMap<QString, int> physicalButtonMappings;
    QMap<QString, quint32> buttonPressTime;
    QMap<QString, bool> buttonHeldEmitted;
    const quint32 HOLD_THRESHOLD = 300;
    const quint32 POLL_INTERVAL = 16;
};

#ifndef LINENUMBERAREA_H
#define LINENUMBERAREA_H

#include <QDebug>
#include <QPainter>
#include <QScrollBar>
#include <QWidget>

#include "qmarkdowntextedit.h"

class LineNumArea final : public QWidget {
    Q_OBJECT

   public:
    explicit LineNumArea(QMarkdownTextEdit *parent)
        : QWidget(parent), textEdit(parent) {
        Q_ASSERT(parent);

        _currentLineColor = QColor(QStringLiteral("#eef067"));
        _otherLinesColor = QColor(QStringLiteral("#a6a6a6"));
        setHidden(true);

        // We always use fixed font to avoid "width" issues
        setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
    }

    void setCurrentLineColor(QColor color) { _currentLineColor = color; }

    void setOtherLineColor(QColor color) {
        _otherLinesColor = std::move(color);
    }

    int lineNumAreaWidth() const {
        if (!enabled) {
            return 0;
        }

        int digits = 2;
        int max = std::max(1, textEdit->blockCount());
        while (max >= 10) {
            max /= 10;
            ++digits;
        }

#if QT_VERSION >= 0x050B00
        int space =
            13 + textEdit->fontMetrics().horizontalAdvance(u'9') * digits;
#else
        int space =
            13 + textEdit->fontMetrics().width(QLatin1Char('9')) * digits;
#endif

        return space;
    }

    bool isLineNumAreaEnabled() const { return enabled; }

    void setLineNumAreaEnabled(bool e) {
        enabled = e;
        setHidden(!e);
    }

    QSize sizeHint() const override { return {lineNumAreaWidth(), 0}; }

   protected:
    void paintEvent(QPaintEvent *event) override {
        QPainter painter(this);

        painter.fillRect(event->rect(),
                         palette().color(QPalette::Active, QPalette::Window));

        auto block = textEdit->firstVisibleBlock();
        int blockNumber = block.blockNumber();
        qreal top = textEdit->blockBoundingGeometry(block)
                        .translated(textEdit->contentOffset())
                        .top();
        // Maybe the top is not 0?
        top += textEdit->viewportMargins().top();
        qreal bottom = top;

        const QPen currentLine = _currentLineColor;
        const QPen otherLines = _otherLinesColor;
        painter.setFont(font());

        while (block.isValid() && top <= event->rect().bottom()) {
            top = bottom;
            bottom = top + textEdit->blockBoundingRect(block).height();
            if (block.isVisible() && bottom >= event->rect().top()) {
                QString number = QString::number(blockNumber + 1);

                auto isCurrentLine =
                    textEdit->textCursor().blockNumber() == blockNumber;
                painter.setPen(isCurrentLine ? currentLine : otherLines);

                painter.drawText(-5, top, sizeHint().width(),
                                 textEdit->fontMetrics().height(),
                                 Qt::AlignRight, number);
            }

            block = block.next();
            ++blockNumber;
        }
    }

   private:
    bool enabled = false;
    QMarkdownTextEdit *textEdit;
    QColor _currentLineColor;
    QColor _otherLinesColor;
};

#endif    // LINENUMBERAREA_H

#ifdef _WIN32
#include <windows.h>
#endif

#include <QApplication>
#include <QTranslator>
#include <QLocale>
#include "MainWindow.h"

int main(int argc, char *argv[]) {
#ifdef _WIN32
    FreeConsole();  // Hide console safely on Windows

    /*
    AllocConsole();
    freopen("CONOUT$", "w", stdout);
    freopen("CONOUT$", "w", stderr);
    */

     // to show console for debugging

#endif
    SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI, "1");
    SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_SWITCH, "1");
    SDL_Init(SDL_INIT_GAMECONTROLLER | SDL_INIT_JOYSTICK);

    /*
    qDebug() << "SDL2 version:" << SDL_GetRevision();
    qDebug() << "Num Joysticks:" << SDL_NumJoysticks();

    for (int i = 0; i < SDL_NumJoysticks(); ++i) {
        if (SDL_IsGameController(i)) {
            qDebug() << "Controller" << i << "is" << SDL_GameControllerNameForIndex(i);
        } else {
            qDebug() << "Joystick" << i << "is not a recognized controller";
        }
    }
    */  // For sdl2 debugging

    // Enable Windows IME support for multi-language input
    QApplication app(argc, argv);

    // Ensure IME is properly enabled for Windows
    #ifdef _WIN32
    app.setAttribute(Qt::AA_EnableHighDpiScaling, true);
    app.setAttribute(Qt::AA_UseHighDpiPixmaps, true);
    #endif

    QTranslator translator;
    QString locale = QLocale::system().name(); // e.g., "zh_CN", "es_ES"
    QString langCode = locale.section('_', 0, 0); // e.g., "zh"

    // printf("Locale: %s\n", locale.toStdString().c_str());
    // printf("Language Code: %s\n", langCode.toStdString().c_str());

    // QString locale = "es-ES"; // e.g., "zh_CN", "es_ES"
    // QString langCode = "es"; // e.g., "zh"
    QString translationsPath = QCoreApplication::applicationDirPath();

    if (translator.load(translationsPath + "/app_" + langCode + ".qm")) {
        app.installTranslator(&translator);
    }

    QString inputFile;
    if (argc >= 2) {
        inputFile = QString::fromLocal8Bit(argv[1]);
        // qDebug() << "Input file received:" << inputFile;
    }

    MainWindow w;
    if (!inputFile.isEmpty()) {
        // Check file extension to determine how to handle it
        if (inputFile.toLower().endsWith(".pdf")) {
            // Handle PDF file association
            w.show(); // Show window first for dialog parent
            w.openPdfFile(inputFile);
        } else if (inputFile.toLower().endsWith(".snpkg")) {
            // Handle notebook package import
            w.importNotebookFromFile(inputFile);
            w.show();
        } else {
            // Unknown file type, just show the application
            w.show();
    }
    } else {
    w.show();
    }
    return app.exec();
}

class InternalDialMode {
    Q_DECLARE_TR_FUNCTIONS(ButtonMappingHelper)
  public:
    static QString dialModeToInternalKey(InternalDialMode mode) {
    switch (mode) {
        case InternalDialMode::None: return "none";
        case InternalDialMode::PageSwitching: return "page_switching";
        case InternalDialMode::ZoomControl: return "zoom_control";
        case InternalDialMode::ThicknessControl: return "thickness_control";

        case InternalDialMode::ToolSwitching: return "tool_switching";
        case InternalDialMode::PresetSelection: return "preset_selection";
        case InternalDialMode::PanAndPageScroll: return "pan_and_page_scroll";
    }
    return "none";
}
    static QString actionToInternalKey(InternalControllerAction action) {
    switch (action) {
        case InternalControllerAction::None: return "none";
        case InternalControllerAction::ToggleFullscreen: return "toggle_fullscreen";
        case InternalControllerAction::ToggleDial: return "toggle_dial";
        case InternalControllerAction::Zoom50: return "zoom_50";
        case InternalControllerAction::ZoomOut: return "zoom_out";
        case InternalControllerAction::Zoom200: return "zoom_200";
        case InternalControllerAction::AddPreset: return "add_preset";
        case InternalControllerAction::DeletePage: return "delete_page";
        case InternalControllerAction::FastForward: return "fast_forward";
        case InternalControllerAction::OpenControlPanel: return "open_control_panel";
        case InternalControllerAction::RedColor: return "red_color";
        case InternalControllerAction::BlueColor: return "blue_color";
        case InternalControllerAction::YellowColor: return "yellow_color";
        case InternalControllerAction::GreenColor: return "green_color";
        case InternalControllerAction::BlackColor: return "black_color";
        case InternalControllerAction::WhiteColor: return "white_color";
        case InternalControllerAction::CustomColor: return "custom_color";
        case InternalControllerAction::ToggleSidebar: return "toggle_sidebar";
        case InternalControllerAction::Save: return "save";
        case InternalControllerAction::StraightLineTool: return "straight_line_tool";
        case InternalControllerAction::RopeTool: return "rope_tool";
        case InternalControllerAction::SetPenTool: return "set_pen_tool";
        case InternalControllerAction::SetMarkerTool: return "set_marker_tool";
        case InternalControllerAction::SetEraserTool: return "set_eraser_tool";
        case InternalControllerAction::TogglePdfTextSelection: return "toggle_pdf_text_selection";
        case InternalControllerAction::ToggleOutline: return "toggle_outline";
        case InternalControllerAction::ToggleBookmarks: return "toggle_bookmarks";
        case InternalControllerAction::AddBookmark: return "add_bookmark";
        case InternalControllerAction::ToggleTouchGestures: return "toggle_touch_gestures";
    }
    return "none";
}
    static InternalDialMode internalKeyToDialMode(const QString &key) {
    if (key == "none") return InternalDialMode::None;
    if (key == "page_switching") return InternalDialMode::PageSwitching;
    if (key == "zoom_control") return InternalDialMode::ZoomControl;
    if (key == "thickness_control") return InternalDialMode::ThicknessControl;

    if (key == "tool_switching") return InternalDialMode::ToolSwitching;
    if (key == "preset_selection") return InternalDialMode::PresetSelection;
    if (key == "pan_and_page_scroll") return InternalDialMode::PanAndPageScroll;
    return InternalDialMode::None;
}
    static InternalControllerAction internalKeyToAction(const QString &key) {
    if (key == "none") return InternalControllerAction::None;
    if (key == "toggle_fullscreen") return InternalControllerAction::ToggleFullscreen;
    if (key == "toggle_dial") return InternalControllerAction::ToggleDial;
    if (key == "zoom_50") return InternalControllerAction::Zoom50;
    if (key == "zoom_out") return InternalControllerAction::ZoomOut;
    if (key == "zoom_200") return InternalControllerAction::Zoom200;
    if (key == "add_preset") return InternalControllerAction::AddPreset;
    if (key == "delete_page") return InternalControllerAction::DeletePage;
    if (key == "fast_forward") return InternalControllerAction::FastForward;
    if (key == "open_control_panel") return InternalControllerAction::OpenControlPanel;
    if (key == "red_color") return InternalControllerAction::RedColor;
    if (key == "blue_color") return InternalControllerAction::BlueColor;
    if (key == "yellow_color") return InternalControllerAction::YellowColor;
    if (key == "green_color") return InternalControllerAction::GreenColor;
    if (key == "black_color") return InternalControllerAction::BlackColor;
    if (key == "white_color") return InternalControllerAction::WhiteColor;
    if (key == "custom_color") return InternalControllerAction::CustomColor;
    if (key == "toggle_sidebar") return InternalControllerAction::ToggleSidebar;
    if (key == "save") return InternalControllerAction::Save;
    if (key == "straight_line_tool") return InternalControllerAction::StraightLineTool;
    if (key == "rope_tool") return InternalControllerAction::RopeTool;
    if (key == "set_pen_tool") return InternalControllerAction::SetPenTool;
    if (key == "set_marker_tool") return InternalControllerAction::SetMarkerTool;
    if (key == "set_eraser_tool") return InternalControllerAction::SetEraserTool;
    if (key == "toggle_pdf_text_selection") return InternalControllerAction::TogglePdfTextSelection;
    if (key == "toggle_outline") return InternalControllerAction::ToggleOutline;
    if (key == "toggle_bookmarks") return InternalControllerAction::ToggleBookmarks;
    if (key == "add_bookmark") return InternalControllerAction::AddBookmark;
    if (key == "toggle_touch_gestures") return InternalControllerAction::ToggleTouchGestures;
    return InternalControllerAction::None;
}
    static QStringList getTranslatedDialModes() {
    return {
        tr("None"),
        tr("Page Switching"),
        tr("Zoom Control"),
        tr("Thickness Control"),

        tr("Tool Switching"),
        tr("Preset Selection"),
        tr("Pan and Page Scroll")
    };
}
    static QStringList getTranslatedActions() {
    return {
        tr("None"),
        tr("Toggle Fullscreen"),
        tr("Toggle Dial"),
        tr("Zoom 50%"),
        tr("Zoom Out"),
        tr("Zoom 200%"),
        tr("Add Preset"),
        tr("Delete Page"),
        tr("Fast Forward"),
        tr("Open Control Panel"),
        tr("Red"),
        tr("Blue"),
        tr("Yellow"),
        tr("Green"),
        tr("Black"),
        tr("White"),
        tr("Custom Color"),
        tr("Toggle Sidebar"),
        tr("Save"),
        tr("Straight Line Tool"),
        tr("Rope Tool"),
        tr("Set Pen Tool"),
        tr("Set Marker Tool"),
        tr("Set Eraser Tool"),
        tr("Toggle PDF Text Selection"),
        tr("Toggle PDF Outline"),
        tr("Toggle Bookmarks"),
        tr("Add/Remove Bookmark"),
        tr("Toggle Touch Gestures")
    };
}
    static QStringList getTranslatedButtons() {
    return {
        tr("Left Shoulder"),
        tr("Right Shoulder"),
        tr("Paddle 2"),
        tr("Paddle 4"),
        tr("Y Button"),
        tr("A Button"),
        tr("B Button"),
        tr("X Button"),
        tr("Left Stick"),
        tr("Start Button"),
        tr("Guide Button")
    };
}
    static QStringList getInternalDialModeKeys() {
    return {
        "none",
        "page_switching",
        "zoom_control",
        "thickness_control",

        "tool_switching",
        "preset_selection",
        "pan_and_page_scroll"
    };
}
    static QStringList getInternalActionKeys() {
    return {
        "none",
        "toggle_fullscreen",
        "toggle_dial",
        "zoom_50",
        "zoom_out",
        "zoom_200",
        "add_preset",
        "delete_page",
        "fast_forward",
        "open_control_panel",
        "red_color",
        "blue_color",
        "yellow_color",
        "green_color",
        "black_color",
        "white_color",
        "custom_color",
        "toggle_sidebar",
        "save",
        "straight_line_tool",
        "rope_tool",
        "set_pen_tool",
        "set_marker_tool",
        "set_eraser_tool",
        "toggle_pdf_text_selection",
        "toggle_outline",
        "toggle_bookmarks",
        "add_bookmark",
        "toggle_touch_gestures"
    };
}
    static QStringList getInternalButtonKeys() {
    return {
        "LEFTSHOULDER",
        "RIGHTSHOULDER",
        "PADDLE2",
        "PADDLE4",
        "Y",
        "A",
        "B",
        "X",
        "LEFTSTICK",
        "START",
        "GUIDE"
    };
}
    static QString displayToInternalKey(const QString &displayString, bool isDialMode) {
    if (isDialMode) {
        QStringList translated = getTranslatedDialModes();
        QStringList internal = getInternalDialModeKeys();
        int index = translated.indexOf(displayString);
        return (index >= 0) ? internal[index] : "none";
    } else {
        QStringList translated = getTranslatedActions();
        QStringList internal = getInternalActionKeys();
        int index = translated.indexOf(displayString);
        return (index >= 0) ? internal[index] : "none";
    }
}
    static QString internalKeyToDisplay(const QString &internalKey, bool isDialMode) {
    if (isDialMode) {
        QStringList translated = getTranslatedDialModes();
        QStringList internal = getInternalDialModeKeys();
        int index = internal.indexOf(internalKey);
        return (index >= 0) ? translated[index] : tr("None");
    } else {
        QStringList translated = getTranslatedActions();
        QStringList internal = getInternalActionKeys();
        int index = internal.indexOf(internalKey);
        return (index >= 0) ? translated[index] : tr("None");
    }
}
};

class KeyCaptureDialog {
    Q_OBJECT

public:
    explicit KeyCaptureDialog(QWidget *parent = nullptr);
  protected:
    void keyPressEvent(QKeyEvent *event) override {
    // Build key sequence string
    QStringList modifiers;

    if (event->modifiers() & Qt::ControlModifier) modifiers << "Ctrl";
    if (event->modifiers() & Qt::ShiftModifier) modifiers << "Shift";
    if (event->modifiers() & Qt::AltModifier) modifiers << "Alt";
    if (event->modifiers() & Qt::MetaModifier) modifiers << "Meta";

    // Don't capture modifier keys alone
    if (event->key() == Qt::Key_Control || event->key() == Qt::Key_Shift ||
        event->key() == Qt::Key_Alt || event->key() == Qt::Key_Meta) {
        return;
    }

    // Don't capture Escape (let it close the dialog)
    if (event->key() == Qt::Key_Escape) {
        QDialog::keyPressEvent(event);
        return;
    }

    QString keyString = QKeySequence(event->key()).toString();

    if (!modifiers.isEmpty()) {
        capturedSequence = modifiers.join("+") + "+" + keyString;
    } else {
        capturedSequence = keyString;
    }

    updateDisplay();
    event->accept();
}
  private:
    void clearSequence() {
    capturedSequence.clear();
    updateDisplay();
    setFocus();  // Return focus to dialog for key capture
}
  private:
    QLabel *instructionLabel;
    QLabel *capturedLabel;
    QPushButton *clearButton;
    QPushButton *okButton;
    QPushButton *cancelButton;
    QString capturedSequence;
    void updateDisplay() {
    if (capturedSequence.isEmpty()) {
        capturedLabel->setText(tr("(No key captured yet)"));
        okButton->setEnabled(false);
    } else {
        capturedLabel->setText(capturedSequence);
        okButton->setEnabled(true);
    }
}
};

/*
 * MIT License
 *
 * Copyright (c) 2014-2025 Patrizio Bekerle -- <patrizio@bekerle.com>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

#include <QApplication>

#include "mainwindow.h"

int main(int argc, char *argv[]) {
    QApplication a(argc, argv);
    MainWindow w;
    w.show();

    return a.exec();
}

class MainWindow {
    Q_OBJECT

   public:
    explicit MainWindow(QWidget *parent = nullptr);
    ~MainWindow() { delete ui; }
  private:
    Ui::MainWindow *ui;
};

#ifndef TOOLTYPE_H
#define TOOLTYPE_H

// ✅ Now it can be used anywhere
enum class ToolType {
    Pen,
    Marker,
    Eraser
};

#endif // TOOLTYPE_H

class
MarkdownWindow
{
Q_OBJECT

public:
    explicit MarkdownWindowManager(InkCanvas *canvas, QObject *parent = nullptr);
~MarkdownWindowManager() {}
MarkdownWindow* createMarkdownWindow(const QRect &rect);
void removeMarkdownWindow(MarkdownWindow *window) {}
void clearAllWindows() {}
void saveWindowsForPage(int pageNumber) {}
void loadWindowsForPage(int pageNumber) {}
void deleteWindowsForPage(int pageNumber) {}
void setSelectionMode(bool enabled) {}
QList<MarkdownWindow*> getCurrentPageWindows() const {}
void resetTransparencyTimer() {}
void setWindowsTransparent(bool transparent) {}
void hideAllWindows() {}
void windowCreated(MarkdownWindow *window);
void windowRemoved(MarkdownWindow *window);
private:
void onWindowDeleteRequested(MarkdownWindow *window) {}
void onWindowFocusChanged(MarkdownWindow *window, bool focused) {}
void onWindowContentChanged(MarkdownWindow *window) {}
void onTransparencyTimerTimeout() {}
private:
InkCanvas *canvas;
bool selectionMode = false;
QMap<int, QList<MarkdownWindow*>> pageWindows;
QList<MarkdownWindow*> currentWindows;
QTimer *transparencyTimer;
MarkdownWindow *currentlyFocusedWindow = nullptr;
bool windowsAreTransparent = false;
QString getWindowDataFilePath(int pageNumber) const {}
void saveWindowData(int pageNumber, const QList<MarkdownWindow*> &windows) {}
QList<MarkdownWindow*> loadWindowData(int pageNumber) {}
QString getSaveFolder() const {}
QString getNotebookId() const {}
QRect convertScreenToCanvasRect(const QRect &screenRect) const {}
void connectWindowSignals(MarkdownWindow *window) {}
public:
void updateAllWindowPositions() {}
};

This code segment functions to: Manage the lifecycle and state persistence of interactive elements across multiple workspace pages, including visibility control, user interaction handling, and automatic transparency adjustments based on activity patterns.

```cpp
[TASK_BEGIN]
class MarkdownWindowManager
{
    Q_OBJECT

public:
    explicit MarkdownWindowManager(InkCanvas *canvas, QObject *parent = nullptr)
        : QObject(parent), canvas(canvas)
    {
        transparencyTimer = new QTimer(this);
        transparencyTimer->setSingleShot(true);
        connect(transparencyTimer, &QTimer::timeout, this, &MarkdownWindowManager::onTransparencyTimerTimeout);
    }

    MarkdownWindow* createMarkdownWindow(const QRect &rect)
    {
        MarkdownWindow* window = new MarkdownWindow(canvas->parentWidget());
        window->setGeometry(convertScreenToCanvasRect(rect));
        currentWindows.append(window);
        connectWindowSignals(window);
        emit windowCreated(window);
        return window;
    }

    void clearAllWindows()
    {
        qDeleteAll(currentWindows);
        currentWindows.clear();
    }

    void saveWindowsForPage(int pageNumber)
    {
        saveWindowData(pageNumber, currentWindows);
    }

    void loadWindowsForPage(int pageNumber)
    {
        clearAllWindows();
        currentWindows = loadWindowData(pageNumber);
        for(MarkdownWindow* window : currentWindows) {
            window->setParent(canvas->parentWidget());
            connectWindowSignals(window);
        }
    }

    void deleteWindowsForPage(int pageNumber)
    {
        if(pageWindows.contains(pageNumber)) {
            qDeleteAll(pageWindows[pageNumber]);
            pageWindows.remove(pageNumber);
        }
    }

    void setSelectionMode(bool enabled) { selectionMode = enabled; }
    QList<MarkdownWindow*> getCurrentPageWindows() const { return currentWindows; }

    void resetTransparencyTimer()
    {
        transparencyTimer->start(3000);
        setWindowsTransparent(false);
    }

    void setWindowsTransparent(bool transparent)
    {
        windowsAreTransparent = transparent;
        for(MarkdownWindow* window : currentWindows)
            window->setWindowOpacity(transparent ? 0.3 : 1.0);
    }

    void hideAllWindows()
    {
        for(MarkdownWindow* window : currentWindows)
            window->hide();
    }

signals:
    void windowCreated(MarkdownWindow *window);
    void windowRemoved(MarkdownWindow *window);

private:
    void onWindowDeleteRequested(MarkdownWindow *window)
    {
        currentWindows.removeOne(window);
        emit windowRemoved(window);
        window->deleteLater();
    }

    void onWindowFocusChanged(MarkdownWindow *window, bool focused)
    {
        if(focused) {
            currentlyFocusedWindow = window;
            resetTransparencyTimer();
        }
    }

    void onTransparencyTimerTimeout()
    {
        if(!selectionMode && currentlyFocusedWindow == nullptr)
            setWindowsTransparent(true);
    }

    QString getWindowDataFilePath(int pageNumber) const
    {
        return QDir(getSaveFolder()).filePath(QString("%1_%2.mw").arg(getNotebookId()).arg(pageNumber));
    }

    void connectWindowSignals(MarkdownWindow *window)
    {
        connect(window, &MarkdownWindow::deleteRequested, this, [=] { onWindowDeleteRequested(window); });
        connect(window, &MarkdownWindow::focusChanged, this, [=](bool focused) { onWindowFocusChanged(window, focused); });
        connect(window, &MarkdownWindow::contentChanged, this, [=] { onWindowContentChanged(window); });
    }

private:
    InkCanvas *canvas;
    bool selectionMode = false;
    QMap<int, QList<MarkdownWindow*>> pageWindows;
    QList<MarkdownWindow*> currentWindows;
    QTimer *transparencyTimer;
    MarkdownWindow *currentlyFocusedWindow = nullptr;
    bool windowsAreTransparent = false;
};
[TASK_END]
```