DEV Community

slav
slav

Posted on

That "5-minute task": pasting plain text into a QML TextArea

You know the drill. Someone drops a ticket: "paste from clipboard should strip formatting." Sounds trivial. It's not.

The setup

Cross-platform mobile app. A TextArea that works with rich text internally — because "that's how it's always been." But when the user pastes something from outside the app, it should land as plain text, no formatting.

First instinct: textFormat: TextEdit.PlainText. Done, right?

Except the TextArea needs to stay in rich-text mode for internal use. So that's off the table.

OK, next idea: hook into QClipboard::changed, intercept incoming data, strip rich text before it lands.

Two problems with that:

One — you're mutating the system clipboard. User copies formatted text in some editor, switches to your app, your app silently strips it, user goes back and pastes... plain text. Not great. QA might miss it, users won't.

Two — it doesn't even work. The slot connected to changed doesn't fire when you'd expect it to — not on app activation, not during paste from the context menu. Mobile input handling in Qt is its own circle of hell.

Finding the actual paste event

The paste in question is triggered from the long-press context menu. So: event filter on the TextArea, log everything that comes through.

Two things immediately stand out: a flood of QInputMethodEvent and QInputMethodQueryEvent. Ignore those. Keep filtering.

What actually shows up on paste: KeyPress + KeyRelease with Ctrl+V. That's it. Qt maps the context menu "Paste" to a standard key sequence internally. And at that exact moment, QClipboard already has the data you need.

So interception point: found. Now what?

Getting to the cursor

TextArea::insert won't cut it — if the user has a selection, insert creates a new QTextCursor at a given position rather than using the existing one. Two cursors, selection gets ignored. Not what we want.

We need the actual cursor instance. Which isn't exposed. Which means Qt6::QuickPrivate.

Add it to target_link_libraries, then include — order matters here because of how QQuickTextControl is structured:

#include <QtQuick/private/qquicktextcontrol_p.h>
#include <QtQuick/private/qquicktextcontrol_p_p.h>
#include <QtQuick/private/qquicktextedit_p_p.h>
Enter fullscreen mode Exit fullscreen mode

Every text editor in QML inherits from QQuickTextEdit. Cast your editor to it via qobject_cast, grab the private part, dig out QQuickTextControl, then go one layer deeper:

QQuickTextEdit* editor = qobject_cast<QQuickTextEdit*>(mEditor);
if (editor != nullptr) {
    QQuickTextEditPrivate* editorPrivate = QQuickTextEditPrivate::get(editor);
    // QQuickTextControl has no standard static get() for its private part,
    // so we cast through QObjectPrivate
    QObjectPrivate* objPrivate = QQuickTextControlPrivate::get(editorPrivate->control);
    if (objPrivate != nullptr) {
        QQuickTextControlPrivate* controlPrivate =
            static_cast<QQuickTextControlPrivate*>(objPrivate);
    }
}
Enter fullscreen mode Exit fullscreen mode

The actual fix

Once you're there, inserting plain text is one line:

controlPrivate->cursor.insertFragment(
    QTextDocumentFragment::fromPlainText(cliptext)
);
event->setAccepted(true);
return true;
Enter fullscreen mode Exit fullscreen mode

Mark the event handled so Qt doesn't double-process it.

Full event filter

bool ClipboardHelper::eventFilter(QObject* obj, QEvent* event)
{
    const auto type = event->type();
    if (QEvent::KeyPress == type || QEvent::KeyRelease == type) {
        QKeyEvent* key = static_cast<QKeyEvent*>(event);
        if (QKeySequence::Paste == key) {
            if (QEvent::KeyPress == type) {
                return true;
            }
            auto* clip = qGuiApp->clipboard();
            if (clip != nullptr && clip->mimeData() != nullptr) {
                QString cliptext = clip->mimeData()->text();
                if (!cliptext.isEmpty()) {
                    QQuickTextEdit* editor = qobject_cast<QQuickTextEdit*>(mEditor);
                    if (editor != nullptr) {
                        QQuickTextEditPrivate* editorPrivate =
                            QQuickTextEditPrivate::get(editor);
                        QObjectPrivate* objPrivate =
                            QQuickTextControlPrivate::get(editorPrivate->control);
                        if (objPrivate != nullptr) {
                            QQuickTextControlPrivate* controlPrivate =
                                static_cast<QQuickTextControlPrivate*>(objPrivate);
                            controlPrivate->cursor.insertFragment(
                                QTextDocumentFragment::fromPlainText(cliptext));
                            event->setAccepted(true);
                            return true;
                        }
                    }
                }
            }
        }
    }
    return QObject::eventFilter(obj, event);
}
Enter fullscreen mode Exit fullscreen mode

Yes, this digs into private Qt internals. No, there's no clean public API for this. Private APIs can break between Qt versions — this was written against Qt 6.9.

Sometimes the right solution is the working one.

Top comments (0)