Better_Software_Header_MobileBetter_Software_Header_Web

Find what you need - explore our website and developer resources

Formatting Selected Text in QML

Quasi-Backporting Qt 6.7 QML cursorSelection to Older Versions

TextEdit {
    id: txtEdit

    anchors.fill: parent
    selectByMouse: true
    textFormat: TextEdit.RichText
}

Shortcut {
    sequence: StandardKey.Bold
    onActivated: {
        if (txtEdit.selectedText.length > 0)
        {
            const start = txtEdit.selectionStart
            const end = txtEdit.selectionEnd
            let sel = txtEdit.getFormattedText(start, end)
                             .split("<!--StartFragment-->")[1]
                             .split("<!--EndFragment-->")[0]
            txtEdit.remove(start, end)
            if (sel.includes("font-weight:600;"))
                sel = sel.replace("font-weight:600;", "")
            else
                sel = "<b>" + sel + "</b>"
            txtEdit.insert(txtEdit.cursorPosition, sel)
            txtEdit.select(start, end)
        }
    }
}
FontDialog {
    id: fontDlg
}

Shortcut {
    id: fontShortcut

    property string sel: ""
    property int start: 0
    property int end: 0

    sequence: StandardKey.Find
    onActivated: {
        if (txtEdit.selectedText.length > 0)
        {
            start = txtEdit.selectionStart
            end = txtEdit.selectionEnd
            sel = txtEdit.getFormattedText(start, end)
                         .split("<!--StartFragment-->")[1]
                         .split("<!--EndFragment-->")[0]
            fontDlg.open()
        }
    }
}

Connections {
    target: fontDlg

    function onAccepted() {
        txtEdit.remove(fontShortcut.start, fontShortcut.end)
        if (fontShortcut.sel.includes("font-family:")) {
            let fontToReplace = fontShortcut.sel.split("font-family:'")[1].split("';")[0]
            fontShortcut.sel = fontShortcut.sel.replace(fontToReplace, fontDlg.font.family)
        } else {
            fontShortcut.sel = "<span style=\"font-family: '"
                             + fontDlg.font.family + "'; font-size:"
                             + (fontDlg.font.pixelSize ? fontDlg.font.pixelSize
                                                       : fontDlg.font.pointSize)
                             + "\">" + fontShortcut.sel + "</span>"
        }
        txtEdit.insert(txtEdit.cursorPosition, fontShortcut.sel)
        txtEdit.select(fontShortcut.start, fontShortcut.end)
    }
}
class Q_QUICK_EXPORT QQuickTextSelection : public QObject
{
    Q_OBJECT

    Q_PROPERTY(QString text READ text WRITE setText NOTIFY textChanged FINAL)
    Q_PROPERTY(QFont font READ font WRITE setFont NOTIFY fontChanged FINAL)
    Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged FINAL)
    Q_PROPERTY(Qt::Alignment alignment READ alignment WRITE setAlignment NOTIFY alignmentChanged FINAL)

    QML_ANONYMOUS
    QML_ADDED_IN_VERSION(6, 7)

public:
    explicit QQuickTextSelection(QObject *parent = nullptr);

    QString text() const;
    void setText(const QString &text);

    QFont font() const;
    void setFont(const QFont &font);

    QColor color() const;
    void setColor(QColor color);

    Qt::Alignment alignment() const;
    void setAlignment(Qt::Alignment align);

Q_SIGNALS:
    void textChanged();
    void fontChanged();
    void colorChanged();
    void alignmentChanged();

private:
    QTextCursor cursor() const;
    void updateFromCharFormat(const QTextCharFormat &fmt);
    void updateFromBlockFormat();

private:
    QTextCursor m_cursor;
    QTextCharFormat m_charFormat;
    QTextBlockFormat m_blockFormat;
    QQuickTextDocument *m_doc = nullptr;
    QQuickTextControl *m_control = nullptr;
};
QTextCursor m_cursor;
QTextCharFormat m_charFormat;
QTextBlockFormat m_blockFormat;
QQuickTextDocument *m_doc = nullptr;
QQuickTextControl *m_control = nullptr;
QQuickTextSelection::QQuickTextSelection(QObject *parent)
    : QObject(parent)
{
    // When QQuickTextEdit creates its cursorSelection, it passes itself as the parent
    if (auto *textEdit = qmlobject_cast<QQuickTextEdit *>(parent)) {
        m_doc = textEdit->textDocument();
        m_control = QQuickTextEditPrivate::get(textEdit)->control;
        // ...
        // ...
QTextCursor QQuickTextSelection::cursor() const
{
    if (m_control)
        return m_control->textCursor();
    return m_cursor;
}
QFont QQuickTextSelection::font() const
{
    return cursor().charFormat().font();
}

// ...

QColor QQuickTextSelection::color() const
{
    return cursor().charFormat().foreground().color();
}

// ...

Qt::Alignment QQuickTextSelection::alignment() const
{
    return cursor().blockFormat().alignment();
}
QQuickTextSelection::QQuickTextSelection(QObject *parent)
    : QObject(parent)
{
    // When QQuickTextEdit creates its cursorSelection, it passes itself as the parent
    if (auto *textEdit = qmlobject_cast<QQuickTextEdit *>(parent)) {
        m_doc = textEdit->textDocument();
        m_control = QQuickTextEditPrivate::get(textEdit)->control;
        connect(m_control, &QQuickTextControl::currentCharFormatChanged,
                this, &QQuickTextSelection::updateFromCharFormat);
        connect(m_control, &QQuickTextControl::cursorPositionChanged,
                this, &QQuickTextSelection::updateFromBlockFormat);
    }
}

// ...
// ...
// ...

inline void QQuickTextSelection::updateFromCharFormat(const QTextCharFormat &fmt)
{
    if (fmt.font() != m_charFormat.font())
        emit fontChanged();
    if (fmt.foreground().color() != m_charFormat.foreground().color())
        emit colorChanged();

    m_charFormat = fmt;
}

inline void QQuickTextSelection::updateFromBlockFormat()
{
    QTextBlockFormat fmt = cursor().blockFormat();

    if (fmt.alignment() != m_blockFormat.alignment())
        emit alignmentChanged();

    m_blockFormat = fmt;
}
void QQuickTextSelection::setText(const QString &text)
{
    auto cur = cursor();
    if (cur.selectedText() == text)
        return;

    cur.insertText(text);
    emit textChanged();
}

// ...

void QQuickTextSelection::setFont(const QFont &font)
{
    auto cur = cursor();
    if (cur.selection().isEmpty())
        cur.select(QTextCursor::WordUnderCursor);

    if (font == cur.charFormat().font())
        return;

    QTextCharFormat fmt;
    fmt.setFont(font);
    cur.mergeCharFormat(fmt);
    emit fontChanged();
}

// ...

void QQuickTextSelection::setColor(QColor color)
{
    auto cur = cursor();
    if (cur.selection().isEmpty())
        cur.select(QTextCursor::WordUnderCursor);

    if (color == cur.charFormat().foreground().color())
        return;

    QTextCharFormat fmt;
    fmt.setForeground(color);
    cur.mergeCharFormat(fmt);
    emit colorChanged();
}

// ...

void QQuickTextSelection::setAlignment(Qt::Alignment align)
{
    if (align == alignment())
        return;

    QTextBlockFormat format;
    format.setAlignment(align);
    cursor().mergeBlockFormat(format);
    emit alignmentChanged();
}
Item {
    // ...
    // ...
    // ...

    Shortcut {
        // ctrl+B to toggle bold / not bold for selection
        sequence: StandardKey.Bold
        onActivated: {
            txtEdit.CursorSelection.font = Qt.font({
                bold: txtEdit.CursorSelection.font.bold !== true
            })
        }
    }

    TextEdit {
        id: txtEdit

        // ...

        CursorSelection.font {
            bold: false
            italic: false
            underline: false
        }
    }
}
// CursorSelection.h

class CursorSelection : public QObject
{
    Q_OBJECT
    QML_ATTACHED(CursorSelectionAttached)
    QML_ELEMENT

public:
    static CursorSelectionAttached *qmlAttachedProperties(QObject *object);
};

QML_DECLARE_TYPEINFO(CursorSelection, QML_HAS_ATTACHED_PROPERTIES)
// CursorSelection.cpp

CursorSelectionAttached *CursorSelection::qmlAttachedProperties(QObject *object)
{
    if (auto *textEdit = qobject_cast<QQuickTextEdit *>(object))
        return new CursorSelectionAttached(textEdit);
    return nullptr;
}
// we know that parent will be a QQuickTextEdit *
CursorSelectionAttached::CursorSelectionAttached(QQuickTextEdit *parent) noexcept
    : QObject(parent)
    , mEdit(parent)		// this is the TextEdit we are attached to 
{
    // make sure the QTextDocument exists
    const auto *const quickDoc = mEdit->textDocument(); // QQuickTextDocument *
    auto *doc = quickDoc->textDocument();               // QTextDocument *
    Q_ASSERT(doc != nullptr);

    // retrieve QTextCursor from the QTextDocument
    mCursor = QTextCursor(doc);

    // When deselecting, the cursor position and anchor are
    // set to the TextEdit's cursor position
    connect(mEdit, &QQuickTextEdit::selectedTextChanged,
            this, &CursorSelectionAttached::moveAnchorIfDeselected);

    connect(mEdit, &QQuickTextEdit::cursorPositionChanged,
            this, &CursorSelectionAttached::updatePosition);

    // if we set a format with no selection, we keep it in an optional
    // then when new text is added, it will have this formatting
    // for example, with no selection we press ctrl+B and then start
    // typing. we expect the text to be bold.
    connect(mEdit->textDocument()->textDocument(),
            &QTextDocument::contentsChange,
            this,
            &CursorSelectionAttached::applyFormatToNewTextIfNeeded);
}
void CursorSelectionAttached::moveAnchorIfDeselected()
{
    if (mEdit->selectedText().isEmpty())
        mCursor.setPosition(mEdit->cursorPosition(), QTextCursor::MoveAnchor);
}
void CursorSelectionAttached::updatePosition()
{
    // if there's no selection, just move the cursor & anchor
    if (mEdit->selectionEnd() == mEdit->selectionStart())
    {
        mCursor.setPosition(mEdit->cursorPosition(), QTextCursor::MoveAnchor);
    }

    // if both the start and end need to be updated:
    // move cursor and anchor to selection start, and
    // move cursor to selection end while keeping anchor at start
    //
    // we have to make sure the anchor is moved correctly so the
    // whole selection matches up -- otherwise cursor selection 
    // start or end might be in the middle of the actual
    // selection, wherever the anchor is
    else if (mEdit->selectionStart() != mCursor.selectionStart() &&
             mEdit->selectionEnd() != mCursor.selectionEnd())
    {
        mCursor.setPosition(mEdit->selectionStart(), QTextCursor::MoveAnchor);
        mCursor.setPosition(mEdit->selectionEnd(), QTextCursor::KeepAnchor);
    }

    // these two cases are for selection dragging, only start or
    // end will move, so anchor stays in place
    else if (mEdit->selectionStart() != mCursor.selectionStart())
    {
        mCursor.setPosition(mEdit->selectionStart(), QTextCursor::KeepAnchor);
    }
    else if (mEdit->selectionEnd() != mCursor.selectionEnd())
    {
        mCursor.setPosition(mEdit->selectionEnd(), QTextCursor::KeepAnchor);
    }
}
void CursorSelectionAttached::applyFormatToNewTextIfNeeded(int from, int charsRemoved, int charsAdded)
{
    if (charsAdded && mOptFormat)
    {
        mCursor.setPosition(mCursor.position() - 1, QTextCursor::KeepAnchor);
        mCursor.mergeCharFormat(mOptFormat.value());
        mOptFormat.reset();
    }
}
Q_PROPERTY(QString text READ text WRITE setText NOTIFY textChanged FINAL)
Q_PROPERTY(QFont font READ font WRITE setFont NOTIFY fontChanged FINAL)
[[nodiscard]] QString text() const;
[[nodiscard]] QFont font() const;
void setText(const QString &text);
void setFont(const QFont &font);
void textChanged();
void fontChanged();
QString CursorSelectionAttached::text() const
{
    return mCursor.selectedText();
}

QFont CursorSelectionAttached::font() const
{
    // simply get the font at the cursor position using charFormat
    auto ret = mCursor.charFormat().font();

    // if the cursor is at the start of a selection, we need to take the font
    // at the position right in front of it. otherwise, the font will refer to the 
    // character at the position right before the selection begins
    if (mCursor.hasSelection() && mCursor.position() == mCursor.selectionStart())
    {
        auto cur = mCursor;
        cur.setPosition(cur.position() + 1);
        ret = cur.charFormat().font();
    }
    return ret;
}
void CursorSelectionAttached::setText(const QString &text)
{
    if (mCursor.selectedText() == text)
        return;

    mCursor.insertText(text);
    emit textChanged();
}

void CursorSelectionAttached::setFont(const QFont &font)
{
    if (font == mCursor.charFormat().font())
        return;

    QTextCharFormat fmt = mCursor.charFormat();
    fmt.setFont(font, QTextCharFormat::FontPropertiesSpecifiedOnly);

    // when no selection, formatting must be set on the next insertion
    if (mCursor.selection().isEmpty())
        mOptFormat = fmt;
    else
        mCursor.mergeCharFormat(fmt);

    emit fontChanged();
}
~CursorSelectionAttached() override = default;
#pragma once

#include <QObject>
#include <QTextCursor>
#include <QtQml>
#include <optional>

class QQuickTextEdit;

class CursorSelectionAttached : public QObject
{
    Q_OBJECT
    Q_PROPERTY(QString text READ text WRITE setText NOTIFY textChanged FINAL)
    Q_PROPERTY(QFont font READ font WRITE setFont NOTIFY fontChanged FINAL)
    QML_ANONYMOUS

public:
    explicit CursorSelectionAttached(QQuickTextEdit *parent) noexcept;
    ~CursorSelectionAttached() override = default;
    [[nodiscard]] QString text() const;
    [[nodiscard]] QFont font() const;
    void setText(const QString &text);
    void setFont(const QFont &font);

signals:
    void textChanged();
    void fontChanged();

private slots:
    void moveAnchorIfDeselected();
    void updatePosition();
    void applyFormatToNewTextIfNeeded(int from, int charsRemoved, int charsAdded);

private:
    QTextCursor mCursor;
    QQuickTextEdit *mEdit;
    std::optional<QTextCharFormat> mOptFormat;
};

class CursorSelection : public QObject
{
    Q_OBJECT
    QML_ATTACHED(CursorSelectionAttached)
    QML_ELEMENT

public:
    static CursorSelectionAttached *qmlAttachedProperties(QObject *object);
};

QML_DECLARE_TYPEINFO(CursorSelection, QML_HAS_ATTACHED_PROPERTIES)

About KDAB