editeng/qa/unit/core-test.cxx                           |   85 ++++++++++++++++
 editeng/source/editeng/impedit2.cxx                     |   83 +++++++++------
 sw/inc/IDocumentContentOperations.hxx                   |    2 
 sw/qa/extras/txtimport/data/tdf157037-automatic-dir.txt |    9 +
 sw/qa/extras/txtimport/txtimport.cxx                    |   25 ++++
 sw/source/core/doc/DocumentContentOperationsManager.cxx |   56 ++++++++++
 sw/source/core/edit/editsh.cxx                          |   52 ---------
 sw/source/core/inc/DocumentContentOperationsManager.hxx |    1 
 sw/source/core/inc/frame.hxx                            |    2 
 sw/source/filter/ascii/parasc.cxx                       |    2 
 10 files changed, 233 insertions(+), 84 deletions(-)

New commits:
commit 974d50f66f16762f82857eaf817328460afb8b6b
Author:     Jonathan Clark <[email protected]>
AuthorDate: Thu Jan 22 07:54:24 2026 -0700
Commit:     Jonathan Clark <[email protected]>
CommitDate: Sat Jan 24 00:48:10 2026 +0100

    tdf#157037 Auto-detect paragraph directions in plain text
    
    Updates Writer and Edit Engine to automatically set paragraph directions
    when opening or pasting plain text.
    
    Change-Id: I9c81b0313d6ab717c064c2bc35043a86f2c8ea5b
    Reviewed-on: https://gerrit.libreoffice.org/c/core/+/197924
    Tested-by: Jenkins
    Reviewed-by: Jonathan Clark <[email protected]>

diff --git a/editeng/qa/unit/core-test.cxx b/editeng/qa/unit/core-test.cxx
index 5d9736592280..1f3e9c7adfce 100644
--- a/editeng/qa/unit/core-test.cxx
+++ b/editeng/qa/unit/core-test.cxx
@@ -135,6 +135,7 @@ public:
     void testTdf154248MultilineFieldWrapping();
     void testTdf151748StaleKashidaArray();
     void testTdf162803StaleKashidaArray();
+    void testTdf157037PasteTextAutoDirection();
 
     DECL_STATIC_LINK(Test, CalcFieldValueHdl, EditFieldInfo*, void);
 
@@ -169,6 +170,7 @@ public:
     CPPUNIT_TEST(testTdf154248MultilineFieldWrapping);
     CPPUNIT_TEST(testTdf151748StaleKashidaArray);
     CPPUNIT_TEST(testTdf162803StaleKashidaArray);
+    CPPUNIT_TEST(testTdf157037PasteTextAutoDirection);
     CPPUNIT_TEST_SUITE_END();
 
 private:
@@ -2373,6 +2375,89 @@ void Test::testTdf162803StaleKashidaArray()
     }
 }
 
+class TestTextTransferable : public 
cppu::WeakImplHelper<datatransfer::XTransferable>
+{
+    std::vector<OUString> m_aContent;
+    std::vector<OUString> m_aMimeType;
+
+public:
+    TestTextTransferable(std::vector<OUString> rContent, std::vector<OUString> 
rMimeType)
+        : m_aContent(std::move(rContent))
+        , m_aMimeType(std::move(rMimeType))
+    {
+        CPPUNIT_ASSERT_EQUAL(m_aContent.size(), m_aMimeType.size());
+    }
+
+    uno::Any SAL_CALL getTransferData(const datatransfer::DataFlavor& rFlavor) 
override
+    {
+        for (size_t nType = 0; nType < m_aMimeType.size(); ++nType)
+        {
+            if (rFlavor.MimeType == m_aMimeType[nType])
+            {
+                uno::Any aRet;
+                aRet <<= m_aContent.at(nType);
+                return aRet;
+            }
+        }
+        return {};
+    }
+
+    uno::Sequence<datatransfer::DataFlavor> SAL_CALL getTransferDataFlavors() 
override
+    {
+        std::vector<datatransfer::DataFlavor> aFlavourVac;
+        for (size_t nType = 0; nType < m_aMimeType.size(); ++nType)
+        {
+            datatransfer::DataFlavor aFlavor;
+            aFlavor.DataType = cppu::UnoType<OUString>::get();
+            aFlavor.MimeType = m_aMimeType[nType];
+            aFlavor.HumanPresentableName = aFlavor.MimeType;
+            aFlavourVac.push_back(aFlavor);
+        }
+        uno::Sequence<datatransfer::DataFlavor> aFlavors(aFlavourVac.data(), 
m_aMimeType.size());
+        return aFlavors;
+    }
+
+    sal_Bool SAL_CALL isDataFlavorSupported(const datatransfer::DataFlavor& 
rFlavor) override
+    {
+        for (size_t nType = 0; nType < m_aMimeType.size(); ++nType)
+        {
+            if (rFlavor.MimeType == m_aMimeType[nType]
+                && rFlavor.DataType == cppu::UnoType<OUString>::get())
+            {
+                return true;
+            }
+        }
+        return false;
+    }
+};
+
+void Test::testTdf157037PasteTextAutoDirection()
+{
+    // Given an empty editeng document:
+    EditEngine aEditEngine(mpItemPool.get());
+    EditDoc& rDoc = aEditEngine.GetEditDoc();
+
+    std::vector<OUString> aContent{ u"Example
مثال
Example
مثال
Example"_ustr };
+    std::vector<OUString> aMime{ u"text/plain;charset=utf-16"_ustr };
+    uno::Reference<datatransfer::XTransferable> xData(
+        new TestTextTransferable(std::move(aContent), std::move(aMime)));
+    aEditEngine.InsertText(xData, OUString(), rDoc.GetEndPaM(), /*paste 
special*/ true);
+
+    // Check that the paste worked
+    CPPUNIT_ASSERT_EQUAL(29, rDoc.GetTextLen());
+    CPPUNIT_ASSERT_EQUAL(u"Example"_ustr, rDoc.GetParaAsString(sal_Int32(0)));
+    CPPUNIT_ASSERT_EQUAL(u"مثال"_ustr, rDoc.GetParaAsString(sal_Int32(1)));
+    CPPUNIT_ASSERT_EQUAL(u"Example"_ustr, rDoc.GetParaAsString(sal_Int32(2)));
+    CPPUNIT_ASSERT_EQUAL(u"مثال"_ustr, rDoc.GetParaAsString(sal_Int32(3)));
+    CPPUNIT_ASSERT_EQUAL(u"Example"_ustr, rDoc.GetParaAsString(sal_Int32(4)));
+
+    CPPUNIT_ASSERT(!aEditEngine.IsRightToLeft(0));
+    CPPUNIT_ASSERT(aEditEngine.IsRightToLeft(1));
+    CPPUNIT_ASSERT(!aEditEngine.IsRightToLeft(2));
+    CPPUNIT_ASSERT(aEditEngine.IsRightToLeft(3));
+    CPPUNIT_ASSERT(!aEditEngine.IsRightToLeft(4));
+}
+
 CPPUNIT_TEST_SUITE_REGISTRATION(Test);
 }
 
diff --git a/editeng/source/editeng/impedit2.cxx 
b/editeng/source/editeng/impedit2.cxx
index 68f93b6c6354..b983334a00e2 100644
--- a/editeng/source/editeng/impedit2.cxx
+++ b/editeng/source/editeng/impedit2.cxx
@@ -2782,49 +2782,62 @@ EditPaM ImpEditEngine::InsertTextUserInput( const 
EditSelection& rCurSel,
 
 void ImpEditEngine::UpdateAutoParaDirection(const EditSelection& rCurSel)
 {
-    EditPaM aPaM(rCurSel.Min());
-
-    auto nPara = maEditDoc.GetPos(aPaM.GetNode());
-    if (nPara >= GetParaPortions().Count())
+    sal_Int32 nStartNode = maEditDoc.GetPos(rCurSel.Min().GetNode());
+    sal_Int32 nEndNode = maEditDoc.GetPos(rCurSel.Max().GetNode());
+    if (nStartNode > nEndNode)
     {
-        return;
+        std::swap(nStartNode, nEndNode);
     }
 
-    const SvxAutoFrameDirectionItem& rItem = GetParaAttrib(nPara, 
EE_PARA_AUTOWRITINGDIR);
-    if (!rItem.GetValue())
+    for (sal_Int32 nPara = nStartNode; nPara <= nEndNode; ++nPara)
     {
-        return;
-    }
+        if (nPara >= GetParaPortions().Count())
+        {
+            break;
+        }
 
-    bool bIsAlreadyRtl = IsRightToLeft(nPara);
+        const SvxAutoFrameDirectionItem& rItem = GetParaAttrib(nPara, 
EE_PARA_AUTOWRITINGDIR);
+        if (!rItem.GetValue())
+        {
+            continue;
+        }
 
-    bool bShouldBeRtl = bIsAlreadyRtl;
-    switch (i18nutil::GuessParagraphDirection(aPaM.GetNode()->GetString()))
-    {
-        case i18nutil::ParagraphDirection::Ambiguous:
-            bShouldBeRtl = bIsAlreadyRtl;
-            break;
+        const auto* pPara = GetParaPortions().SafeGetObject(nPara);
+        if (!pPara)
+        {
+            continue;
+        }
 
-        case i18nutil::ParagraphDirection::LeftToRight:
-            bShouldBeRtl = false;
-            break;
+        bool bIsAlreadyRtl = IsRightToLeft(nPara);
 
-        case i18nutil::ParagraphDirection::RightToLeft:
-            bShouldBeRtl = true;
-            break;
-    }
+        bool bShouldBeRtl = bIsAlreadyRtl;
+        switch 
(i18nutil::GuessParagraphDirection(pPara->GetNode()->GetString()))
+        {
+            case i18nutil::ParagraphDirection::Ambiguous:
+                bShouldBeRtl = bIsAlreadyRtl;
+                break;
 
-    if (bShouldBeRtl == bIsAlreadyRtl)
-    {
-        return;
-    }
+            case i18nutil::ParagraphDirection::LeftToRight:
+                bShouldBeRtl = false;
+                break;
+
+            case i18nutil::ParagraphDirection::RightToLeft:
+                bShouldBeRtl = true;
+                break;
+        }
+
+        if (bShouldBeRtl == bIsAlreadyRtl)
+        {
+            continue;
+        }
 
-    SvxFrameDirection eNeeded
-        = bShouldBeRtl ? SvxFrameDirection::Horizontal_RL_TB : 
SvxFrameDirection::Horizontal_LR_TB;
+        SvxFrameDirection eNeeded = bShouldBeRtl ? 
SvxFrameDirection::Horizontal_RL_TB
+                                                 : 
SvxFrameDirection::Horizontal_LR_TB;
 
-    SfxItemSet aSet{ GetParaAttribs(nPara) };
-    aSet.Put(SvxFrameDirectionItem{ eNeeded, EE_PARA_WRITINGDIR });
-    SetParaAttribs(nPara, aSet);
+        SfxItemSet aSet{ GetParaAttribs(nPara) };
+        aSet.Put(SvxFrameDirectionItem{ eNeeded, EE_PARA_WRITINGDIR });
+        SetParaAttribs(nPara, aSet);
+    }
 }
 
 EditPaM ImpEditEngine::ImpInsertText(const EditSelection& aCurSel, const 
OUString& rStr)
@@ -4338,7 +4351,11 @@ EditSelection ImpEditEngine::PasteText( uno::Reference< 
datatransfer::XTransfera
                 uno::Any aData = rxDataObj->getTransferData( aFlavor );
                 OUString aText;
                 aData >>= aText;
-                aNewSelection = ImpInsertText( EditSelection(rPaM), aText );
+                auto aNewPaM = ImpInsertText(EditSelection(rPaM), aText);
+                aNewSelection = aNewPaM;
+
+                // tdf#157037: Automatically adjust paragraph directions after 
pasting text
+                UpdateAutoParaDirection(EditSelection{ rPaM, aNewPaM });
             }
             catch( ... )
             {
diff --git a/sw/inc/IDocumentContentOperations.hxx 
b/sw/inc/IDocumentContentOperations.hxx
index aae3c9a2a776..abf9b06deeb5 100644
--- a/sw/inc/IDocumentContentOperations.hxx
+++ b/sw/inc/IDocumentContentOperations.hxx
@@ -245,6 +245,8 @@ public:
     virtual void RemoveLeadingWhiteSpace(const SwPosition & rPos ) = 0;
     virtual void RemoveLeadingWhiteSpace(SwPaM& rPaM) = 0;
 
+    virtual void AutoSetParagraphDirections(SwPaM& rPaM, const SwRootFrame* 
pLayout = nullptr) = 0;
+
 protected:
     virtual ~IDocumentContentOperations() {};
 };
diff --git a/sw/qa/extras/txtimport/data/tdf157037-automatic-dir.txt 
b/sw/qa/extras/txtimport/data/tdf157037-automatic-dir.txt
new file mode 100644
index 000000000000..9d8c4ca25c56
--- /dev/null
+++ b/sw/qa/extras/txtimport/data/tdf157037-automatic-dir.txt
@@ -0,0 +1,9 @@
+Example
+
+مثال
+
+Example
+
+مثال
+
+Example
diff --git a/sw/qa/extras/txtimport/txtimport.cxx 
b/sw/qa/extras/txtimport/txtimport.cxx
index 4f681693fd72..eea88dc9d3b3 100644
--- a/sw/qa/extras/txtimport/txtimport.cxx
+++ b/sw/qa/extras/txtimport/txtimport.cxx
@@ -14,7 +14,9 @@
 #include <iodetect.hxx>
 #include <unotxdoc.hxx>
 #include <docsh.hxx>
+#include <ndtxt.hxx>
 #include <wrtsh.hxx>
+#include <txtfrm.hxx>
 #include <rtl/ustrbuf.hxx>
 
 namespace
@@ -199,6 +201,29 @@ CPPUNIT_TEST_FIXTURE(TxtImportTest, testTdf70423)
     CPPUNIT_ASSERT_EQUAL(aResStr, aPara);
 }
 
+CPPUNIT_TEST_FIXTURE(TxtImportTest, testTdf157037AutomaticDirection)
+{
+    createSwDoc("tdf157037-automatic-dir.txt");
+
+    auto* pDoc = getSwDoc();
+    SwNodeIndex stNodes{ pDoc->GetNodes().GetEndOfContent(), -1 };
+
+    std::vector<SwTextFrame const*> aFrames;
+    for (size_t i = 0; i < 5; ++i)
+    {
+        aFrames.push_back(&dynamic_cast<SwTextFrame const&>(
+            *stNodes.GetNode().GetTextNode()->getLayoutFrame(nullptr)));
+        --stNodes;
+        --stNodes;
+    }
+
+    CPPUNIT_ASSERT(!aFrames.at(0)->IsRightToLeft());
+    CPPUNIT_ASSERT(aFrames.at(1)->IsRightToLeft());
+    CPPUNIT_ASSERT(!aFrames.at(2)->IsRightToLeft());
+    CPPUNIT_ASSERT(aFrames.at(3)->IsRightToLeft());
+    CPPUNIT_ASSERT(!aFrames.at(4)->IsRightToLeft());
+}
+
 } // end of anonymous namespace
 CPPUNIT_PLUGIN_IMPLEMENT();
 
diff --git a/sw/source/core/doc/DocumentContentOperationsManager.cxx 
b/sw/source/core/doc/DocumentContentOperationsManager.cxx
index 9dd24bf13d34..f1ac409c525b 100644
--- a/sw/source/core/doc/DocumentContentOperationsManager.cxx
+++ b/sw/source/core/doc/DocumentContentOperationsManager.cxx
@@ -82,6 +82,7 @@
 #include <unotools/configmgr.hxx>
 #include <unotools/transliterationwrapper.hxx>
 #include <i18nutil/transliteration.hxx>
+#include <i18nutil/guessparadirection.hxx>
 #include <sfx2/Metadatable.hxx>
 #include <sot/exchange.hxx>
 #include <svl/stritem.hxx>
@@ -89,7 +90,9 @@
 #include <svx/svdobj.hxx>
 #include <svx/svdouno.hxx>
 #include <tools/globname.hxx>
+#include <editeng/autodiritem.hxx>
 #include <editeng/formatbreakitem.hxx>
+#include <editeng/frmdiritem.hxx>
 #include <com/sun/star/i18n/Boundary.hpp>
 #include <com/sun/star/i18n/WordType.hpp>
 #include <com/sun/star/i18n/XBreakIterator.hpp>
@@ -3820,6 +3823,59 @@ void 
DocumentContentOperationsManager::RemoveLeadingWhiteSpace(SwPaM& rPaM )
     }
 }
 
+void DocumentContentOperationsManager::AutoSetParagraphDirections(SwPaM& rPaM,
+                                                                  const 
SwRootFrame* pLayout)
+{
+    for (SwPaM& rSel : rPaM.GetRingContainer())
+    {
+        auto* pNode = rSel.GetPointNode().GetTextNode();
+        if (!pNode)
+        {
+            continue;
+        }
+
+        if 
(!pNode->GetSwAttrSet().GetItem(RES_PARATR_AUTOFRAMEDIR)->GetValue())
+        {
+            continue;
+        }
+
+        std::optional<bool> bIsCurrentlyRtl;
+        if (pLayout)
+        {
+            Point aPt;
+            std::pair<Point, bool> const tmp(aPt, false);
+            const SwTextFrame* pFrame
+                = static_cast<SwTextFrame*>(pNode->getLayoutFrame(pLayout, 
rPaM.GetPoint(), &tmp));
+            if (pFrame)
+            {
+                bIsCurrentlyRtl = pFrame->IsRightToLeft();
+            }
+        }
+
+        switch (i18nutil::GuessParagraphDirection(pNode->GetText()))
+        {
+            case i18nutil::ParagraphDirection::Ambiguous:
+                break;
+
+            case i18nutil::ParagraphDirection::LeftToRight:
+                if (bIsCurrentlyRtl.value_or(true))
+                {
+                    InsertPoolItem(rSel, SvxFrameDirectionItem{ 
SvxFrameDirection::Horizontal_LR_TB,
+                                                                RES_FRAMEDIR 
});
+                }
+                break;
+
+            case i18nutil::ParagraphDirection::RightToLeft:
+                if (!bIsCurrentlyRtl.value_or(false))
+                {
+                    InsertPoolItem(rSel, SvxFrameDirectionItem{ 
SvxFrameDirection::Horizontal_RL_TB,
+                                                                RES_FRAMEDIR 
});
+                }
+                break;
+        }
+    }
+}
+
 // Copy method from SwDoc - "copy Flys in Flys"
 /// note: rRg/rInsPos *exclude* a partially selected start text node;
 ///       pCopiedPaM *includes* a partially selected start text node
diff --git a/sw/source/core/edit/editsh.cxx b/sw/source/core/edit/editsh.cxx
index a67fa15fb8d9..a81cedd695c6 100644
--- a/sw/source/core/edit/editsh.cxx
+++ b/sw/source/core/edit/editsh.cxx
@@ -64,56 +64,8 @@ using namespace com::sun::star;
 
 void SwEditShell::UpdateSelectionAutoParaDirection()
 {
-    for (SwPaM& rPaM : getShellCursor(true)->GetRingContainer())
-    {
-        auto* pNode = rPaM.GetPointNode().GetTextNode();
-        if (!pNode)
-        {
-            continue;
-        }
-
-        if 
(!pNode->GetSwAttrSet().GetItem(RES_PARATR_AUTOFRAMEDIR)->GetValue())
-        {
-            continue;
-        }
-
-        Point aPt;
-        std::pair<Point, bool> const tmp(aPt, false);
-        const SwTextFrame* pFrame
-            = static_cast<SwTextFrame*>(pNode->getLayoutFrame(GetLayout(), 
rPaM.GetPoint(), &tmp));
-        if (!pFrame)
-        {
-            continue;
-        }
-
-        bool bIsAlreadyRtl = pFrame->IsRightToLeft();
-
-        bool bShouldBeRtl = bIsAlreadyRtl;
-        switch (i18nutil::GuessParagraphDirection(pNode->GetText()))
-        {
-            case i18nutil::ParagraphDirection::Ambiguous:
-                bShouldBeRtl = bIsAlreadyRtl;
-                break;
-
-            case i18nutil::ParagraphDirection::LeftToRight:
-                bShouldBeRtl = false;
-                break;
-
-            case i18nutil::ParagraphDirection::RightToLeft:
-                bShouldBeRtl = true;
-                break;
-        }
-
-        if (bShouldBeRtl == bIsAlreadyRtl)
-        {
-            continue;
-        }
-
-        SvxFrameDirection eNeeded = bShouldBeRtl ? 
SvxFrameDirection::Horizontal_RL_TB
-                                                 : 
SvxFrameDirection::Horizontal_LR_TB;
-        rPaM.GetDoc().getIDocumentContentOperations().InsertPoolItem(
-            rPaM, SvxFrameDirectionItem{ eNeeded, RES_FRAMEDIR });
-    }
+    
GetDoc()->getIDocumentContentOperations().AutoSetParagraphDirections(*getShellCursor(true),
+                                                                         
GetLayout());
 }
 
 void SwEditShell::Insert( sal_Unicode c, bool bOnlyCurrCursor )
diff --git a/sw/source/core/inc/DocumentContentOperationsManager.hxx 
b/sw/source/core/inc/DocumentContentOperationsManager.hxx
index 35088eca08af..1570649738a4 100644
--- a/sw/source/core/inc/DocumentContentOperationsManager.hxx
+++ b/sw/source/core/inc/DocumentContentOperationsManager.hxx
@@ -94,6 +94,7 @@ public:
     void RemoveLeadingWhiteSpace(const SwPosition & rPos ) override;
     void RemoveLeadingWhiteSpace(SwPaM& rPaM) override;
 
+    void AutoSetParagraphDirections(SwPaM& rPaM, const SwRootFrame* pLayout = 
nullptr) override;
 
     //Non-Interface methods
 
diff --git a/sw/source/core/inc/frame.hxx b/sw/source/core/inc/frame.hxx
index cbb6f3f7e1d9..008ca7fbb5a0 100644
--- a/sw/source/core/inc/frame.hxx
+++ b/sw/source/core/inc/frame.hxx
@@ -414,7 +414,7 @@ class SAL_DLLPUBLIC_RTTI SwFrame : public 
SwFrameAreaDefinition, public SwClient
     void UpdateAttrFrame( const SfxPoolItem*, const SfxPoolItem*, 
SwFrameInvFlags & );
     static void UpdateAttrFrameForFormatChange( SwFrameInvFlags & );
     SwFrame* GetIndNext_();
-    void SetDirFlags( bool bVert );
+    SW_DLLPUBLIC void SetDirFlags( bool bVert );
 
     const SwLayoutFrame* ImplGetNextLayoutLeaf( bool bFwd ) const;
 
diff --git a/sw/source/filter/ascii/parasc.cxx 
b/sw/source/filter/ascii/parasc.cxx
index 45f1d30c1e65..d2f42c3eaaad 100644
--- a/sw/source/filter/ascii/parasc.cxx
+++ b/sw/source/filter/ascii/parasc.cxx
@@ -512,6 +512,8 @@ ErrCode SwASCIIParser::ReadChars()
 void SwASCIIParser::InsertText( const OUString& rStr )
 {
     m_rDoc.getIDocumentContentOperations().InsertString(*m_oPam, rStr);
+    m_rDoc.getIDocumentContentOperations().AutoSetParagraphDirections(*m_oPam,
+                                                                      
/*layout*/ nullptr);
 
     if (m_oItemSet && g_pBreakIt
         && m_nScript != (SvtScriptType::LATIN | SvtScriptType::ASIAN | 
SvtScriptType::COMPLEX))

Reply via email to