{"id":478409,"date":"2026-05-03T19:16:02","date_gmt":"2026-05-03T19:16:02","guid":{"rendered":"https:\/\/savepearlharbor.com\/?p=478409"},"modified":"-0001-11-30T00:00:00","modified_gmt":"-0001-11-29T21:00:00","slug":"","status":"publish","type":"post","link":"https:\/\/savepearlharbor.com\/?p=478409","title":{"rendered":"How to Paste Plain Text into a Rich-Text QML TextArea on Mobile (Qt 6)"},"content":{"rendered":"<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<p><em>A &#171;five-minute task&#187; that turned out to involve half of Qt, mobile keyboards, and a fair amount of pain.<\/em><\/p>\n<h4>The Problem<\/h4>\n<p>Here&#8217;s the setup: a cross-platform mobile app, a multiline\u00a0<code>TextArea<\/code>, and a requirement to paste rich-text from the clipboard as plain text.<\/p>\n<p>Sounds simple, right? Just set\u00a0<code>textFormat: TextEdit.PlainText<\/code>\u00a0and call it a day.<\/p>\n<p>Except \u2014 our\u00a0<code>TextArea<\/code>\u00a0has to support rich-text internally, because\u00a0<em>&#171;that&#8217;s how it historically worked.&#187;<\/em>\u00a0So we only want to strip formatting when pasting from the outside. The property approach is out.<\/p>\n<h4>Attempt #1: Intercept the Clipboard<\/h4>\n<p>The obvious next step: grab\u00a0<code>QClipboard<\/code>, connect to the\u00a0<code>changed<\/code>\u00a0signal, and strip rich-text there before it ever reaches the input field. Fast, bold, and wrong.<\/p>\n<p><strong>Why wrong?<\/strong><\/p>\n<p><strong>First<\/strong>, it breaks clipboard consistency across the OS. The user copies rich-text in some editor, switches to your app, your app silently mutates the clipboard, the user goes back and pastes \u2014 plain text. Surprise. QA might not catch it. Users will.<\/p>\n<p><strong>Second<\/strong>, it doesn&#8217;t actually work. The slot connected to\u00a0<code>changed<\/code>\u00a0never fires at the right moment \u2014 not when the app activates, not when the paste happens. This is tied to how Qt handles keyboards and input fields on mobile platforms\u00a0<em>(in short: it&#8217;s complicated and not great)<\/em>.<\/p>\n<h4>Finding the Right Event<\/h4>\n<p>Since everything here is a\u00a0<code>QObject<\/code>, let&#8217;s use what\u00a0<code>QObject<\/code>\u00a0does well: event filtering. Slap\u00a0<code>QObject::eventFilter<\/code>\u00a0on the\u00a0<code>TextArea<\/code>, log every event type to stdout, and see what shows up.<\/p>\n<p>You&#8217;ll immediately notice a flood of\u00a0<code>QInputMethodEvent<\/code>\u00a0and\u00a0<code>QInputMethodQueryEvent<\/code>. Nothing useful in them. Filter those out.<\/p>\n<p>What&#8217;s left is interesting: when you tap\u00a0<strong>Paste<\/strong>\u00a0in the context menu (the one that appears after a long press on a text field), the\u00a0<code>TextArea<\/code>\u00a0receives a\u00a0<code>KeyPress<\/code>\u00a0and\u00a0<code>KeyRelease<\/code>\u00a0pair \u2014 specifically\u00a0<code>Ctrl+V<\/code>. And at that exact moment,\u00a0<code>QClipboard<\/code>already holds the data being pasted.<\/p>\n<p>So we have the data. Now how do we tell Qt to use plain-text instead of rich-text?<\/p>\n<h4>The Actual Solution (Spoiler: It&#8217;s Not Pretty)<\/h4>\n<p>You might think: just call\u00a0<code>insert<\/code>\u00a0on the\u00a0<code>TextArea<\/code>. That won&#8217;t work when the user has a selection \u2014\u00a0<code>insert<\/code>\u00a0creates a\u00a0<em>new<\/em>\u00a0<code>QTextCursor<\/code>\u00a0at the given position, while we need to use the\u00a0<em>existing<\/em>\u00a0one that owns the current selection.<\/p>\n<p>The existing cursor isn&#8217;t publicly accessible. But if you&#8217;re determined enough&#8230;<\/p>\n<p>Add\u00a0<code>Qt6::QuickPrivate<\/code>\u00a0as a dependency and link against it in\u00a0<code>target_link_libraries<\/code>. Then include these headers \u2014\u00a0<strong>order matters<\/strong>\u00a0due to how\u00a0<code>QQuickTextControl<\/code>\u00a0is structured:<\/p>\n<pre><code class=\"cpp\">#include &lt;QtQuick\/private\/qquicktextcontrol_p.h&gt;#include &lt;QtQuick\/private\/qquicktextcontrol_p_p.h&gt;#include &lt;QtQuick\/private\/qquicktextedit_p_p.h&gt;<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:87px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>All QML text editors inherit from\u00a0<code>QQuickTextEdit<\/code>. Cast the editor pointer from QML using\u00a0<code>qobject_cast<\/code>, grab the private part, dig through to\u00a0<code>QQuickTextControl<\/code>, and from\u00a0<em>its<\/em>\u00a0private part you can finally reach the cursor:<\/p>\n<pre><code class=\"cpp\">QQuickTextEdit* editor = qobject_cast&lt;QQuickTextEdit*&gt;(mEditor);if (editor != nullptr) {    QQuickTextEditPrivate* editorPrivate = QQuickTextEditPrivate::get(editor);    \/\/ QQuickTextControl has no standard static 'get' for its private part,    \/\/ so here's a small trick with QObjectPrivate    QObjectPrivate* objPrivate = QQuickTextControlPrivate::get(editorPrivate-&gt;control);    if (objPrivate != nullptr) {        QQuickTextControlPrivate* controlPrivate =            static_cast&lt;QQuickTextControlPrivate*&gt;(objPrivate);    }}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>Once you have\u00a0<code>controlPrivate<\/code>, insert plain-text through the cursor and mark the event as handled:<\/p>\n<pre><code class=\"cpp\">controlPrivate-&gt;cursor.insertFragment(    QTextDocumentFragment::fromPlainText(someText));event-&gt;setAccepted(true);return true;<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<h4>Full Event Filter<\/h4>\n<pre><code class=\"cpp\">bool ClipboardHelper::eventFilter(QObject* obj, QEvent* event){    const auto type = event-&gt;type();    if (QEvent::KeyPress == type || QEvent::KeyRelease == type) {        QKeyEvent* key = static_cast&lt;QKeyEvent*&gt;(event);        if (QKeySequence::Paste == key) {            if (QEvent::KeyPress == type) {                return true;            }            auto* clip = qGuiApp-&gt;clipboard();            if (clip != nullptr &amp;&amp; clip-&gt;mimeData() != nullptr) {                QString cliptext = clip-&gt;mimeData()-&gt;text();                if (!cliptext.isEmpty()) {                    QQuickTextEdit* editor = qobject_cast&lt;QQuickTextEdit*&gt;(mEditor);                    if (editor != nullptr) {                        QQuickTextEditPrivate* editorPrivate =                            QQuickTextEditPrivate::get(editor);                        QObjectPrivate* objPrivate =                            QQuickTextControlPrivate::get(editorPrivate-&gt;control);                        if (objPrivate != nullptr) {                            QQuickTextControlPrivate* controlPrivate =                                static_cast&lt;QQuickTextControlPrivate*&gt;(objPrivate);                            controlPrivate-&gt;cursor.insertFragment(                                QTextDocumentFragment::fromPlainText(cliptext));                            event-&gt;setAccepted(true);                            return true;                        }                    }                }            }        }    }    return QObject::eventFilter(obj, event);}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<h4>Important Note<\/h4>\n<p>Using Qt&#8217;s private API comes with risks. Nobody guarantees stability between Qt versions. The solution above is valid for\u00a0<strong>Qt 6.9<\/strong>.<\/p>\n<p><em>More Qt\/C++\/QML cases \u2014\u00a0<\/em><a href=\"https:\/\/t.me\/c0de1e55\" rel=\"noopener noreferrer nofollow\"><em>t.me\/c0de1e55<\/em><\/a><br \/><em>GitHub \u2014\u00a0<\/em><a href=\"https:\/\/github.com\/0xc0de1e55-cm\" rel=\"noopener noreferrer nofollow\"><em>github.com\/0xc0de1e55-cm<\/em><\/a><\/p>\n<\/div>\n<p>\u0441\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b \u0441\u0442\u0430\u0442\u044c\u0438 <a href=\"https:\/\/habr.com\/ru\/articles\/1030916\/\">https:\/\/habr.com\/ru\/articles\/1030916\/<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>A &#171;five-minute task&#187; that turned out to involve half of Qt, mobile keyboards, and a fair amount of pain.The ProblemHere&#8217;s the setup: a cross-platform mobile app, a multiline\u00a0TextArea, and a requirement to paste rich-text from the clipboard as plain text.Sounds simple, right? Just set\u00a0textFormat: TextEdit.PlainText\u00a0and call it a day.Except \u2014 our\u00a0TextArea\u00a0has to support rich-text internally, because\u00a0&#171;that&#8217;s how it historically worked.&#187;\u00a0So we only want to strip formatting when pasting from the outside. The property approach is out.Attempt #1: Intercept the ClipboardThe obvious next step: grab\u00a0QClipboard, connect to the\u00a0changed\u00a0signal, and strip rich-text there before it ever reaches the input field. Fast, bold, and wrong.Why wrong?First, it breaks clipboard consistency across the OS. The user copies rich-text in some editor, switches to your app, your app silently mutates the clipboard, the user goes back and pastes \u2014 plain text. Surprise. QA might not catch it. Users will.Second, it doesn&#8217;t actually work. The slot connected to\u00a0changed\u00a0never fires at the right moment \u2014 not when the app activates, not when the paste happens. This is tied to how Qt handles keyboards and input fields on mobile platforms\u00a0(in short: it&#8217;s complicated and not great).Finding the Right EventSince everything here is a\u00a0QObject, let&#8217;s use what\u00a0QObject\u00a0does well: event filtering. Slap\u00a0QObject::eventFilter\u00a0on the\u00a0TextArea, log every event type to stdout, and see what shows up.You&#8217;ll immediately notice a flood of\u00a0QInputMethodEvent\u00a0and\u00a0QInputMethodQueryEvent. Nothing useful in them. Filter those out.What&#8217;s left is interesting: when you tap\u00a0Paste\u00a0in the context menu (the one that appears after a long press on a text field), the\u00a0TextArea\u00a0receives a\u00a0KeyPress\u00a0and\u00a0KeyRelease\u00a0pair \u2014 specifically\u00a0Ctrl+V. And at that exact moment,\u00a0QClipboardalready holds the data being pasted.So we have the data. Now how do we tell Qt to use plain-text instead of rich-text?The Actual Solution (Spoiler: It&#8217;s Not Pretty)You might think: just call\u00a0insert\u00a0on the\u00a0TextArea. That won&#8217;t work when the user has a selection \u2014\u00a0insert\u00a0creates a\u00a0new\u00a0QTextCursor\u00a0at the given position, while we need to use the\u00a0existing\u00a0one that owns the current selection.The existing cursor isn&#8217;t publicly accessible. But if you&#8217;re determined enough&#8230;Add\u00a0Qt6::QuickPrivate\u00a0as a dependency and link against it in\u00a0target_link_libraries. Then include these headers \u2014\u00a0order matters\u00a0due to how\u00a0QQuickTextControl\u00a0is structured:#include &lt;QtQuick\/private\/qquicktextcontrol_p.h&gt;#include &lt;QtQuick\/private\/qquicktextcontrol_p_p.h&gt;#include &lt;QtQuick\/private\/qquicktextedit_p_p.h&gt;All QML text editors inherit from\u00a0QQuickTextEdit. Cast the editor pointer from QML using\u00a0qobject_cast, grab the private part, dig through to\u00a0QQuickTextControl, and from\u00a0its\u00a0private part you can finally reach the cursor:QQuickTextEdit* editor = qobject_cast&lt;QQuickTextEdit*&gt;(mEditor);if (editor != nullptr) {    QQuickTextEditPrivate* editorPrivate = QQuickTextEditPrivate::get(editor);    \/\/ QQuickTextControl has no standard static &#8216;get&#8217; for its private part,    \/\/ so here&#8217;s a small trick with QObjectPrivate    QObjectPrivate* objPrivate = QQuickTextControlPrivate::get(editorPrivate-&gt;control);    if (objPrivate != nullptr) {        QQuickTextControlPrivate* controlPrivate =            static_cast&lt;QQuickTextControlPrivate*&gt;(objPrivate);    }}Once you have\u00a0controlPrivate, insert plain-text through the cursor and mark the event as handled:controlPrivate-&gt;cursor.insertFragment(    QTextDocumentFragment::fromPlainText(someText));event-&gt;setAccepted(true);return true;Full Event Filterbool ClipboardHelper::eventFilter(QObject* obj, QEvent* event){    const auto type = event-&gt;type();    if (QEvent::KeyPress == type || QEvent::KeyRelease == type) {        QKeyEvent* key = static_cast&lt;QKeyEvent*&gt;(event);        if (QKeySequence::Paste == key) {            if (QEvent::KeyPress == type) {                return true;            }            auto* clip = qGuiApp-&gt;clipboard();            if (clip != nullptr &amp;&amp; clip-&gt;mimeData() != nullptr) {                QString cliptext = clip-&gt;mimeData()-&gt;text();                if (!cliptext.isEmpty()) {                    QQuickTextEdit* editor = qobject_cast&lt;QQuickTextEdit*&gt;(mEditor);                    if (editor != nullptr) {                        QQuickTextEditPrivate* editorPrivate =                            QQuickTextEditPrivate::get(editor);                        QObjectPrivate* objPrivate =                            QQuickTextControlPrivate::get(editorPrivate-&gt;control);                        if (objPrivate != nullptr) {                            QQuickTextControlPrivate* controlPrivate =                                static_cast&lt;QQuickTextControlPrivate*&gt;(objPrivate);                            controlPrivate-&gt;cursor.insertFragment(                                QTextDocumentFragment::fromPlainText(cliptext));                            event-&gt;setAccepted(true);                            return true;                        }                    }                }            }        }    }    return QObject::eventFilter(obj, event);}Important NoteUsing Qt&#8217;s private API comes with risks. Nobody guarantees stability between Qt versions. The solution above is valid for\u00a0Qt 6.9.More Qt\/C++\/QML cases \u2014\u00a0t.me\/c0de1e55GitHub \u2014\u00a0github.com\/0xc0de1e55-cm\u0441\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b \u0441\u0442\u0430\u0442\u044c\u0438 https:\/\/habr.com\/ru\/articles\/1030916\/<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[],"tags":[],"class_list":["post-478409","post","type-post","status-publish","format-standard","hentry"],"_links":{"self":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/478409","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=478409"}],"version-history":[{"count":0,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/478409\/revisions"}],"wp:attachment":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=478409"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=478409"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=478409"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}