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))
