sw/CppunitTest_sw_writerfilter_dmapper.mk | 11 + sw/qa/core/doc/doc.cxx | 37 ++++++ sw/qa/core/text/data/floattable-heading-split.docx |binary sw/qa/core/text/widorp.cxx | 22 +++ sw/qa/extras/ooxmlexport/data/floattable-anchorpos.docx |binary sw/qa/extras/ooxmlexport/ooxmlexport25.cxx | 24 ++++ sw/qa/extras/uiwriter/uiwriter3.cxx | 8 + sw/qa/writerfilter/dmapper/DomainMapperTableHandler.cxx | 31 ++++- sw/qa/writerfilter/dmapper/data/floattable-redline.docx |binary sw/qa/writerfilter/ooxml/data/floattable-anchorpos.docx |binary sw/qa/writerfilter/ooxml/ooxml.cxx | 37 ++++++ sw/source/core/doc/DocumentRedlineManager.cxx | 11 + sw/source/core/text/widorp.cxx | 8 + sw/source/core/undo/unredln.cxx | 4 sw/source/filter/ww8/docxattributeoutput.cxx | 41 +++++-- sw/source/filter/ww8/docxexport.cxx | 70 ++++++++++++ sw/source/filter/ww8/docxexport.hxx | 8 + sw/source/filter/ww8/wrtw8nds.cxx | 6 - sw/source/filter/ww8/wrtww8.hxx | 2 sw/source/writerfilter/dmapper/DomainMapperTableHandler.cxx | 5 sw/source/writerfilter/ooxml/OOXMLFastContextHandler.cxx | 19 +++ 21 files changed, 327 insertions(+), 17 deletions(-)
New commits: commit c37b34b50835b95c434967642fc8cb0472b543e8 Author: Miklos Vajna <vmik...@collabora.com> AuthorDate: Mon Jul 14 09:28:52 2025 +0200 Commit: Andras Timar <andras.ti...@collabora.com> CommitDate: Mon Jul 14 19:15:17 2025 +0200 tdf#167222 sw floattable: allow split of fly and its keep-together anchor text Regression from commit 3eb6d764b3023500f2299d36bf1860bc8e67db9f (DOCX import: floating table with negative top margin has to be a fly frame, 2022-01-21), open the bugdoc, notice that the floating table should be at the end of page 1 but is at the start of page 2. This happens because in general keep-together means no split should happen for all anchored objects and lines of a text frame, but Word allows floating tables to be split from the next regular paragraph in the document. Fix the problem by building on top of the work done in commit 2d0a4ef1d83b8de6cb133970c2c35ae706fb058e (sw floattable: fix negative vertical offset handling on page boundary, 2023-06-20), which makes this split to "floating table" and "anchor text" pieces possible, but that didn't help here, since the anchor text node happened to be a heading, with both a split property set to false and a keep-with-next property set to true. Now we ignore both properties in case the paragraph is a floating table anchor, which means that in SwTextFrame::FormatAdjust(), rFrameBreak.IsBreakNow() can return true, resulting in a call to SplitFrame(), as needed. The behavior for other types of anchored objects is unchanged, those don't go to a separate page in Word, either. Change-Id: I48835f355fa5aeb09b1a92bd8cee7e06cd148240 Reviewed-on: https://gerrit.libreoffice.org/c/core/+/187845 Tested-by: Jenkins CollaboraOffice <jenkinscollaboraoff...@gmail.com> Reviewed-by: Caolán McNamara <caolan.mcnam...@collabora.com> diff --git a/sw/qa/core/text/data/floattable-heading-split.docx b/sw/qa/core/text/data/floattable-heading-split.docx new file mode 100644 index 000000000000..adfd173c172e Binary files /dev/null and b/sw/qa/core/text/data/floattable-heading-split.docx differ diff --git a/sw/qa/core/text/widorp.cxx b/sw/qa/core/text/widorp.cxx index 54d26aa2db07..7dd48bc9b879 100644 --- a/sw/qa/core/text/widorp.cxx +++ b/sw/qa/core/text/widorp.cxx @@ -60,6 +60,28 @@ CPPUNIT_TEST_FIXTURE(Test, testHideWhitespaceWidorp) // went to page 1, so it was not split anymore. CPPUNIT_ASSERT(pPara2->HasFollow()); } + +CPPUNIT_TEST_FIXTURE(Test, testFloattableHeadingSplit) +{ + // Given a document which ends with a floating table and a heading paragraph: + // When loading that document & laying it out: + createSwDoc("floattable-heading-split.docx"); + + // Then make sure that the floating table is on page 1 and the last heading is on page 2: + SwDocShell* pDocShell = getSwDocShell(); + SwDoc* pDoc = pDocShell->GetDoc(); + SwRootFrame* pLayout = pDoc->getIDocumentLayoutAccess().GetCurrentLayout(); + auto pPage1 = pLayout->Lower()->DynCastPageFrame(); + // Without the accompanying fix in place, this test would have failed, the floating table went + // to page 2, not to page 1. + CPPUNIT_ASSERT(pPage1->GetSortedObjs()); + // Make sure that page 2 has no floating table and has the heading on the correct page. + auto pPage2 = pPage1->GetNext()->DynCastPageFrame(); + CPPUNIT_ASSERT(!pPage2->GetSortedObjs()); + SwLayoutFrame* pBody2 = pPage2->FindBodyCont(); + SwTextFrame* pPage2Para1 = pBody2->ContainsContent()->DynCastTextFrame(); + CPPUNIT_ASSERT_EQUAL(u"page 2"_ustr, pPage2Para1->GetText()); +} } /* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sw/source/core/text/widorp.cxx b/sw/source/core/text/widorp.cxx index 4c0dd129da04..b6c5dd5b70aa 100644 --- a/sw/source/core/text/widorp.cxx +++ b/sw/source/core/text/widorp.cxx @@ -73,6 +73,14 @@ SwTextFrameBreak::SwTextFrameBreak( SwTextFrame *pNewFrame, const SwTwips nRst ) } m_bKeep = m_bKeep || !m_pFrame->GetTextNodeForParaProps()->GetSwAttrSet().GetSplit().GetValue() || m_pFrame->GetTextNodeForParaProps()->GetSwAttrSet().GetKeep().GetValue(); + + if (m_bKeep && m_pFrame->HasSplitFlyDrawObjs()) + { + // Ignore keep-together and keep-with-next if this is an anchor for a floating table. It's + // OK to split the text frame into two, to separate the floating table and the anchor text. + m_bKeep = false; + } + m_bBreak = false; if( !m_nRstHeight && !m_pFrame->IsFollow() && m_pFrame->IsInFootnote() && m_pFrame->HasPara() ) commit c8a27b430dc9c0af37ced47a99390d2e39008227 Author: Miklos Vajna <vmik...@collabora.com> AuthorDate: Thu Jul 10 14:52:18 2025 +0200 Commit: Andras Timar <andras.ti...@collabora.com> CommitDate: Mon Jul 14 19:15:09 2025 +0200 tdf#167379 sw floattable: ignore dummy anchor nodes in DOCX export Open the bugdoc in Writer, looks OK, save to DOCX, open in Word: an unexpected paragraph appears between the two floating tables. What happens is that the DOCX import inserts dummy anchor paragraphs between floating tables, so we can maintain the invariant that each text node has at most one floating table anchored to it (which simplifies layout code), but then these dummy text nodes are not filtered out on the export side. Fix the problem by scanning the nodes array for such dummy nodes once at the start of the exporter, omitting such dummy nodes from the export result and write the affected floating tables when the next text node is written in the output. An alternative I considered is to leave MSWordExportBase::OutputContentNode() unchanged and just make some of the m_pSerializer calls conditional, so the dummy anchor node is missing from the export result. The problem is that it needed ~12 conditions and it would be easy to change the code in the future in a way that some part of the dummy node markup would be still emitted. So instead skip the entire text node and write the table with the next node. Change-Id: Ic15342d431a5c1e3086dc2029df6455ad675e13e Reviewed-on: https://gerrit.libreoffice.org/c/core/+/187638 Tested-by: Jenkins CollaboraOffice <jenkinscollaboraoff...@gmail.com> Reviewed-by: Caolán McNamara <caolan.mcnam...@collabora.com> diff --git a/sw/qa/extras/ooxmlexport/data/floattable-anchorpos.docx b/sw/qa/extras/ooxmlexport/data/floattable-anchorpos.docx new file mode 100644 index 000000000000..d93e5772aa3f Binary files /dev/null and b/sw/qa/extras/ooxmlexport/data/floattable-anchorpos.docx differ diff --git a/sw/qa/extras/ooxmlexport/ooxmlexport25.cxx b/sw/qa/extras/ooxmlexport/ooxmlexport25.cxx index 88769ef361ac..0ab86ddaf8b3 100644 --- a/sw/qa/extras/ooxmlexport/ooxmlexport25.cxx +++ b/sw/qa/extras/ooxmlexport/ooxmlexport25.cxx @@ -125,6 +125,30 @@ CPPUNIT_TEST_FIXTURE(Test, testTdf166620) } } +CPPUNIT_TEST_FIXTURE(Test, testFloatingTableAnchorPosExport) +{ + // Given a document with two floating tables after each other: + // When saving that document to DOCX: + loadAndSave("floattable-anchorpos.docx"); + + // Then make sure that the dummy anchor of the first floating table is not written to the export + // result: + xmlDocUniquePtr pXmlDoc = parseExport(u"word/document.xml"_ustr); + // Check the order of the floating tables: C is from the previous node, A is normal floating + // table. + CPPUNIT_ASSERT_EQUAL(u"C"_ustr, + getXPathContent(pXmlDoc, "//w:body/w:tbl[1]/w:tr/w:tc/w:p/w:r/w:t")); + CPPUNIT_ASSERT_EQUAL(u"A"_ustr, + getXPathContent(pXmlDoc, "//w:body/w:tbl[2]/w:tr/w:tc/w:p/w:r/w:t")); + // Without the accompanying fix in place, this test would have failed with: + // - Expected: 1 + // - Actual : 2 + // i.e. the dummy anchor node was written to DOCX, leading to a Writer vs Word layout + // difference. + CPPUNIT_ASSERT_EQUAL(1, countXPathNodes(pXmlDoc, "//w:body/w:p")); + CPPUNIT_ASSERT_EQUAL(u"D"_ustr, getXPathContent(pXmlDoc, "//w:body/w:p/w:r/w:t")); +} + } // end of anonymous namespace CPPUNIT_PLUGIN_IMPLEMENT(); diff --git a/sw/source/filter/ww8/docxattributeoutput.cxx b/sw/source/filter/ww8/docxattributeoutput.cxx index 72ae0ba83790..77023858b809 100644 --- a/sw/source/filter/ww8/docxattributeoutput.cxx +++ b/sw/source/filter/ww8/docxattributeoutput.cxx @@ -460,6 +460,7 @@ static void checkAndWriteFloatingTables(DocxAttributeOutput& rDocxAttributeOutpu { const auto& rExport = rDocxAttributeOutput.GetExport(); // iterate though all SpzFrameFormats and check whether they are anchored to the current text node + std::vector<ww8::Frame> aFrames; for( sal_uInt16 nCnt = rExport.m_rDoc.GetSpzFrameFormats()->size(); nCnt; ) { const SwFrameFormat* pFrameFormat = (*rExport.m_rDoc.GetSpzFrameFormats())[ --nCnt ]; @@ -469,7 +470,22 @@ static void checkAndWriteFloatingTables(DocxAttributeOutput& rDocxAttributeOutpu if (!pAnchorNode || ! rExport.m_pCurPam->GetPointNode().GetTextNode()) continue; - if (*pAnchorNode != *rExport.m_pCurPam->GetPointNode().GetTextNode()) + bool bAnchorMatchesNode = *pAnchorNode == *rExport.m_pCurPam->GetPointNode().GetTextNode(); + bool bAnchorIsPreviousNode = false; + if (!bAnchorMatchesNode) + { + // The anchor doesn't match, but see if the previous node is a dummy anchor, we should + // emit floating tables to that anchor here, too. + SwNodeIndex aNodeIndex(rExport.m_pCurPam->GetPointNode()); + --aNodeIndex; + if (*pAnchorNode == aNodeIndex.GetNode() && rExport.IsDummyFloattableAnchor(aNodeIndex.GetNode())) + { + bAnchorMatchesNode = true; + bAnchorIsPreviousNode = true; + } + } + + if (!bAnchorMatchesNode) continue; const SwNodeIndex* pStartNode = pFrameFormat->GetContent().GetContentIdx(); @@ -500,19 +516,26 @@ static void checkAndWriteFloatingTables(DocxAttributeOutput& rDocxAttributeOutpu const SfxGrabBagItem* pTableGrabBag = pTableFormat->GetAttrSet().GetItem<SfxGrabBagItem>(RES_FRMATR_GRABBAG); const std::map<OUString, css::uno::Any> & rTableGrabBag = pTableGrabBag->GetGrabBag(); // no grabbag? - if (rTableGrabBag.find(u"TablePosition"_ustr) == rTableGrabBag.end()) + if (rTableGrabBag.find(u"TablePosition"_ustr) == rTableGrabBag.end() && !pFrameFormat->GetFlySplit().GetValue()) { - if (pFrameFormat->GetFlySplit().GetValue()) - { - ww8::Frame aFrame(*pFrameFormat, *rAnchor.GetContentAnchor()); - rDocxAttributeOutput.WriteFloatingTable(&aFrame); - } continue; } - // write table to docx + // write table to docx: first tables from previous node, then from this node. ww8::Frame aFrame(*pFrameFormat, *rAnchor.GetContentAnchor()); - rDocxAttributeOutput.WriteFloatingTable(&aFrame); + if (bAnchorIsPreviousNode) + { + aFrames.insert(aFrames.begin(), aFrame); + } + else + { + aFrames.push_back(aFrame); + } + } + + for (const auto& rFrame : aFrames) + { + rDocxAttributeOutput.WriteFloatingTable(&rFrame); } } diff --git a/sw/source/filter/ww8/docxexport.cxx b/sw/source/filter/ww8/docxexport.cxx index 02ce025e6abe..5b72f3e5fa2b 100644 --- a/sw/source/filter/ww8/docxexport.cxx +++ b/sw/source/filter/ww8/docxexport.cxx @@ -101,6 +101,8 @@ #include <unotools/ucbstreamhelper.hxx> #include <comphelper/diagnose_ex.hxx> #include <unotxdoc.hxx> +#include <formatflysplit.hxx> +#include <fmtanchr.hxx> using namespace sax_fastparser; using namespace ::comphelper; @@ -524,6 +526,67 @@ void DocxExport::OutputDML(uno::Reference<drawing::XShape> const & xShape) aExport.WriteShape(xShape); } +void DocxExport::CollectFloatingTables() +{ + if (!m_rDoc.GetSpzFrameFormats()) + { + return; + } + + sw::FrameFormats<sw::SpzFrameFormat*>& rSpzFormats = *m_rDoc.GetSpzFrameFormats(); + for (sw::SpzFrameFormat* pFormat : rSpzFormats) + { + const SwFormatFlySplit& rFlySplit = pFormat->GetFlySplit(); + if (!rFlySplit.GetValue()) + { + continue; + } + + const SwFormatAnchor& rAnchor = pFormat->GetAnchor(); + const SwPosition* pContentAnchor = rAnchor.GetContentAnchor(); + if (!pContentAnchor) + { + continue; + } + + SwNode& rNode = pContentAnchor->GetNode(); + SwTextNode* pTextNode = rNode.GetTextNode(); + if (!pTextNode) + { + continue; + } + + SwNodeIndex aNodeIndex(*pTextNode); + ++aNodeIndex; + if (!aNodeIndex.GetNode().GetTextNode()) + { + // Only text nodes know to look for floating tables from previous text nodes. + continue; + } + + if (!pTextNode->HasSwAttrSet()) + { + continue; + } + + const SwAttrSet& rAttrSet = pTextNode->GetSwAttrSet(); + const SvxLineSpacingItem& rLineSpacing = rAttrSet.GetLineSpacing(); + if (rLineSpacing.GetLineSpaceRule() != SvxLineSpaceRule::Fix) + { + continue; + } + + if (rLineSpacing.GetLineHeight() != 0) + { + continue; + } + + // This is text node which is effectively invisible in Writer and has a floating table + // anchored to it; omit such nodes from the DOCX output. + m_aDummyFloatingTableAnchors.insert(&pContentAnchor->GetNode()); + } +} + ErrCode DocxExport::ExportDocument_Impl() { // Set the 'Reviewing' flags in the settings structure @@ -540,6 +603,8 @@ ErrCode DocxExport::ExportDocument_Impl() // Make sure images are counted from one, even when exporting multiple documents. rGraphicExportCache.push(); + CollectFloatingTables(); + WriteMainText(); WriteFootnotesEndnotes(); @@ -1931,6 +1996,11 @@ bool DocxExport::isMirroredMargin() return bMirroredMargins; } +bool DocxExport::IsDummyFloattableAnchor(SwNode& rNode) const +{ + return GetDummyFloatingTableAnchors().contains(&rNode); +} + void DocxExport::WriteDocumentBackgroundFill() { const std::unique_ptr<SvxBrushItem> pBrush = getBackground(); diff --git a/sw/source/filter/ww8/docxexport.hxx b/sw/source/filter/ww8/docxexport.hxx index 43cdccce8a5c..ad339ad3bde1 100644 --- a/sw/source/filter/ww8/docxexport.hxx +++ b/sw/source/filter/ww8/docxexport.hxx @@ -129,6 +129,8 @@ class DocxExport : public MSWordExportBase /// Storage for sdt data which need to be written to other XMLs std::vector<SdtData> m_SdtData; + std::set<SwNode*> m_aDummyFloatingTableAnchors; + public: DocxExportFilter& GetFilter() { return m_rFilter; }; @@ -254,6 +256,8 @@ private: /// Write comments.xml void WritePostitFields(); + void CollectFloatingTables(); + /// Write the numbering table. virtual void WriteNumbering() override; @@ -326,6 +330,10 @@ public: /// return true if Page Layout is set as Mirrored bool isMirroredMargin(); + const std::set<SwNode*>& GetDummyFloatingTableAnchors() const { return m_aDummyFloatingTableAnchors; } + + bool IsDummyFloattableAnchor(SwNode& rNode) const override; + private: DocxExport( const DocxExport& ) = delete; diff --git a/sw/source/filter/ww8/wrtw8nds.cxx b/sw/source/filter/ww8/wrtw8nds.cxx index fe499c5f3dc8..3e0fd1bd50f8 100644 --- a/sw/source/filter/ww8/wrtw8nds.cxx +++ b/sw/source/filter/ww8/wrtw8nds.cxx @@ -3755,7 +3755,11 @@ void MSWordExportBase::OutputContentNode( SwContentNode& rNode ) switch ( rNode.GetNodeType() ) { case SwNodeType::Text: - OutputTextNode( *rNode.GetTextNode() ); + // Skip dummy anchors: the next node will emit their floating tables. + if (!IsDummyFloattableAnchor(*rNode.GetTextNode())) + { + OutputTextNode(*rNode.GetTextNode()); + } break; case SwNodeType::Grf: OutputGrfNode( *rNode.GetGrfNode() ); diff --git a/sw/source/filter/ww8/wrtww8.hxx b/sw/source/filter/ww8/wrtww8.hxx index bd116fa83f12..fa34838426f2 100644 --- a/sw/source/filter/ww8/wrtww8.hxx +++ b/sw/source/filter/ww8/wrtww8.hxx @@ -930,6 +930,8 @@ protected: std::vector<const Graphic*> m_vecBulletPic; ///< Vector to record all the graphics of bullets + virtual bool IsDummyFloattableAnchor(SwNode& /*rNode*/) const { return false; } + public: MSWordExportBase(SwDoc& rDocument, std::shared_ptr<SwUnoCursor> & pCurrentPam, SwPaM* pOriginalPam); virtual ~MSWordExportBase(); commit d57cd07263418f6dca54ec2ebceec8e91a571f62 Author: Miklos Vajna <vmik...@collabora.com> AuthorDate: Wed Jul 9 13:37:02 2025 +0200 Commit: Andras Timar <andras.ti...@collabora.com> CommitDate: Mon Jul 14 19:14:22 2025 +0200 tdf#167379 sw floattable: make dummy paragraph from DOCX import less visible Open the bugdoc, the text C and D appears to have the same vertical position in Word, but D is shifted down in Writer. What happens is that since commit 01ad8ec4bb5425446e95dbada81de435646824b4 (sw floattable: fix lost tables around a floating table from DOCX, 2023-06-05), an empty paragraph is inserted between two floating tables immediately following each other, to have a suitable anchor point for all floating tables. But it wasn't considered that the anchor point takes vertical space, which has an impact on layout. Fix the problem by keeping the empty paragraph in the model & layout (the split fly frame needs those), but setting the paragraph properties (spacing, line spacing) in a way that the created text frame has 0 height, so it has no negative impact on the position of the text after the text frame. This is similar to what the DOC import does in WW8TabDesc::CreateSwTable(), though there we set the font size of the paragraph to 1pt, which improves the layout but still has a small impact. An alternative I considered is to set the "hidden" character property on the text node's item set, but that would create no text frame at all, so we would again have no anchor point for the floating table, so that would not work. Change-Id: Iceb2f51ad42028434822cb9c0701a7e6d6545845 Reviewed-on: https://gerrit.libreoffice.org/c/core/+/187566 Tested-by: Jenkins CollaboraOffice <jenkinscollaboraoff...@gmail.com> Reviewed-by: Caolán McNamara <caolan.mcnam...@collabora.com> diff --git a/sw/qa/extras/uiwriter/uiwriter3.cxx b/sw/qa/extras/uiwriter/uiwriter3.cxx index de126574ca78..466a2664e065 100644 --- a/sw/qa/extras/uiwriter/uiwriter3.cxx +++ b/sw/qa/extras/uiwriter/uiwriter3.cxx @@ -844,7 +844,13 @@ CPPUNIT_TEST_FIXTURE(SwUiWriterTest3, testTdf134227) CPPUNIT_ASSERT_EQUAL(4, getShapes()); - dispatchCommand(mxComponent, u".uno:SelectAll"_ustr, {}); + // Go to the end of the document, outside the dummy paragraph that just serves as an anchor for + // the floating table. + uno::Reference<frame::XModel> xModel(mxComponent, uno::UNO_QUERY); + uno::Reference<text::XTextViewCursorSupplier> xTextViewCursorSupplier( + xModel->getCurrentController(), uno::UNO_QUERY); + uno::Reference<text::XTextViewCursor> xViewCursor = xTextViewCursorSupplier->getViewCursor(); + xViewCursor->gotoEnd(/*bExpand=*/false); dispatchCommand(mxComponent, u".uno:SelectAll"_ustr, {}); // Without the fix in place, it would have crashed here diff --git a/sw/qa/writerfilter/ooxml/data/floattable-anchorpos.docx b/sw/qa/writerfilter/ooxml/data/floattable-anchorpos.docx new file mode 100644 index 000000000000..d93e5772aa3f Binary files /dev/null and b/sw/qa/writerfilter/ooxml/data/floattable-anchorpos.docx differ diff --git a/sw/qa/writerfilter/ooxml/ooxml.cxx b/sw/qa/writerfilter/ooxml/ooxml.cxx index c5f9af177a13..9a9d2745ba7c 100644 --- a/sw/qa/writerfilter/ooxml/ooxml.cxx +++ b/sw/qa/writerfilter/ooxml/ooxml.cxx @@ -17,6 +17,14 @@ #include <fmtcntnt.hxx> #include <ndtxt.hxx> +#include <docsh.hxx> +#include <wrtsh.hxx> +#include <rootfrm.hxx> +#include <pagefrm.hxx> +#include <txtfrm.hxx> +#include <sortedobjs.hxx> +#include <anchoredobject.hxx> +#include <flyfrm.hxx> using namespace ::com::sun::star; @@ -109,6 +117,35 @@ CPPUNIT_TEST_FIXTURE(Test, testNestedRuns) // Without the accompanying fix in place, this test would have failed, the shape was empty. CPPUNIT_ASSERT_EQUAL(u"Test text box"_ustr, pTextNode->GetText()); } + +CPPUNIT_TEST_FIXTURE(Test, testFloatingTableAnchorPos) +{ + // Given a document with a floating table (text: C) and a visually first body paragraph (text: + // D): + // When loading that document: + createSwDoc("floattable-anchorpos.docx"); + + // Then make sure that D is not shifted down vertically, compared to C: + SwWrtShell* pWrtShell = getSwDocShell()->GetWrtShell(); + SwRootFrame* pLayout = pWrtShell->GetLayout(); + SwPageFrame* pPage = pLayout->GetLower()->DynCastPageFrame(); + SwTextFrame* pBodyPara1 = pPage->FindFirstBodyContent()->DynCastTextFrame(); + CPPUNIT_ASSERT(pBodyPara1->HasSplitFlyDrawObjs()); + SwSortedObjs& rFlys1 = *pBodyPara1->GetDrawObjs(); + SwFlyFrame* pFly1 = rFlys1[0]->DynCastFlyFrame(); + SwTextFrame* pFly1Text = pFly1->ContainsContent()->DynCastTextFrame(); + CPPUNIT_ASSERT_EQUAL(u"C"_ustr, pFly1Text->GetText()); + SwTwips nFlyTop = pFly1Text->getFrameArea().Top(); + SwTextFrame* pBodyPara2 = pBodyPara1->GetNext()->DynCastTextFrame(); + CPPUNIT_ASSERT_EQUAL(u"D"_ustr, pBodyPara2->GetText()); + SwTwips nBodyTop = pBodyPara2->getFrameArea().Top(); + SwTwips nDiff = std::abs(nBodyTop - nFlyTop); + // Without the accompanying fix in place, this test would have failed with: + // - Expected less or equal than: 1 + // - Actual : 243 + // i.e. the vertical position of D was too big. + CPPUNIT_ASSERT_LESSEQUAL(static_cast<SwTwips>(1), nDiff); +} } CPPUNIT_PLUGIN_IMPLEMENT(); diff --git a/sw/source/writerfilter/ooxml/OOXMLFastContextHandler.cxx b/sw/source/writerfilter/ooxml/OOXMLFastContextHandler.cxx index 046d8c6aacb0..d8754cc6d2a6 100644 --- a/sw/source/writerfilter/ooxml/OOXMLFastContextHandler.cxx +++ b/sw/source/writerfilter/ooxml/OOXMLFastContextHandler.cxx @@ -1618,6 +1618,25 @@ void OOXMLFastContextHandlerTextTable::lcl_startFastElement // subset of '<resource name="CT_P" resource="Stream">' in model.xml: startParagraphGroup(); sendTableDepth(); + + OOXMLPropertySet::Pointer_t pSprms(new OOXMLPropertySet); + { + // Configure the dummy paragraph to have a zero height, so it has no impact on the + // layout. + OOXMLPropertySet::Pointer_t pAttributes(new OOXMLPropertySet); + OOXMLValue pSBVal = OOXMLValue::createInteger(0); + pAttributes->add(NS_ooxml::LN_CT_Spacing_before, pSBVal, OOXMLProperty::ATTRIBUTE); + OOXMLValue pSAVal = OOXMLValue::createInteger(0); + pAttributes->add(NS_ooxml::LN_CT_Spacing_after, pSAVal, OOXMLProperty::ATTRIBUTE); + OOXMLValue pSLRVal = OOXMLValue::createInteger(NS_ooxml::LN_Value_doc_ST_LineSpacingRule_exact); + pAttributes->add(NS_ooxml::LN_CT_Spacing_lineRule, pSLRVal, OOXMLProperty::ATTRIBUTE); + OOXMLValue pSLVal = OOXMLValue::createInteger(0); + pAttributes->add(NS_ooxml::LN_CT_Spacing_line, pSLVal, OOXMLProperty::ATTRIBUTE); + OOXMLValue pSprm = OOXMLValue::createPropertySet(pAttributes); + pSprms->add(NS_ooxml::LN_CT_PPrBase_spacing, pSprm, OOXMLProperty::SPRM); + } + mpStream->props(pSprms.get()); + endOfParagraph(); } commit 6d829c23b569b891b10d6bfaf06c97909b4e00dd Author: Miklos Vajna <vmik...@collabora.com> AuthorDate: Tue Jul 8 09:50:08 2025 +0200 Commit: Andras Timar <andras.ti...@collabora.com> CommitDate: Mon Jul 14 19:14:22 2025 +0200 tdf#167229 sw floattable: fix missing cell background with redlines Open the DOCX file, the 3rd table should have a gray background in its B2 cell, but its background is white. It seems this happens because there are 2 floating tables before the table and the first floating table has a redline. Commit 288db6eb47fbbd2b3ca34ffea0686d8ed8ed9be9 (tdf#132271 DOCX: import change tracking in floating tables, 2020-09-07) added special handling of redlines inside floating tables, and I moved that code around for SwFormatFlySplit in c50bf5a5daaae3d40f89ea0784a75a8a571c208d (sw floattable: remove no longer needed DOCX import heuristics, 2023-04-12). The trouble is that once these pending redlines are processed, nobody clears rFramedRedlines, so it gets processed also for a next floating table, which pollutes the state of the importer, which leads to missing cell properties for later tables. Fix the problem by clearing the redline list in DomainMapperTableHandler::endTable(), which restores the invariant that once the redline list is processed, it's cleared -- that was there in the old SectionPropertyMap::CloseSectionGroup() code before the SwFormatFlySplit work. An alternative I've considered is to just catch exceptions in BeforeConvertToTextFrame() when getting the TextTable and Cell properties, but that would still result in this unwanted behavior that a floattable-with-redlines has an influance on the next floattable. Change-Id: I9998a46d72a72172dffcf85bf96b3478d63c9269 Reviewed-on: https://gerrit.libreoffice.org/c/core/+/187530 Tested-by: Caolán McNamara <caolan.mcnam...@collabora.com> Tested-by: Jenkins CollaboraOffice <jenkinscollaboraoff...@gmail.com> Reviewed-by: Caolán McNamara <caolan.mcnam...@collabora.com> diff --git a/sw/CppunitTest_sw_writerfilter_dmapper.mk b/sw/CppunitTest_sw_writerfilter_dmapper.mk index 366cca5dd239..de6f844faeec 100644 --- a/sw/CppunitTest_sw_writerfilter_dmapper.mk +++ b/sw/CppunitTest_sw_writerfilter_dmapper.mk @@ -45,6 +45,17 @@ $(eval $(call gb_CppunitTest_use_libraries,sw_writerfilter_dmapper, \ utl \ tl \ vcl \ + svl \ + sw \ + swqahelper \ +)) + +$(eval $(call gb_CppunitTest_set_include,sw_writerfilter_dmapper,\ + -I$(SRCDIR)/sw/inc \ + -I$(SRCDIR)/sw/source/core/inc \ + -I$(SRCDIR)/sw/source/uibase/inc \ + -I$(SRCDIR)/sw/qa/inc \ + $$(INCLUDE) \ )) $(eval $(call gb_CppunitTest_use_sdk_api,sw_writerfilter_dmapper)) diff --git a/sw/qa/writerfilter/dmapper/DomainMapperTableHandler.cxx b/sw/qa/writerfilter/dmapper/DomainMapperTableHandler.cxx index 03c4727737b7..e36ae88a7728 100644 --- a/sw/qa/writerfilter/dmapper/DomainMapperTableHandler.cxx +++ b/sw/qa/writerfilter/dmapper/DomainMapperTableHandler.cxx @@ -7,7 +7,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -#include <test/unoapixml_test.hxx> +#include <swmodeltestbase.hxx> #include <com/sun/star/beans/XPropertySet.hpp> #include <com/sun/star/table/BorderLine2.hpp> @@ -21,16 +21,19 @@ #include <com/sun/star/text/XPageCursor.hpp> #include <com/sun/star/qa/XDumper.hpp> +#include <frmatr.hxx> +#include <swtable.hxx> + using namespace ::com::sun::star; namespace { /// Tests for sw/source/writerfilter/dmapper/DomainMapperTableHandler.cxx. -class Test : public UnoApiXmlTest +class Test : public SwModelTestBase { public: Test() - : UnoApiXmlTest(u"/sw/qa/writerfilter/dmapper/data/"_ustr) + : SwModelTestBase(u"/sw/qa/writerfilter/dmapper/data/"_ustr) { } }; @@ -277,6 +280,28 @@ CPPUNIT_TEST_FIXTURE(Test, testDOCXFloatingTableNestedLayout) CPPUNIT_ASSERT_GREATER(nTableTop, nFlyTop); CPPUNIT_ASSERT_LESS(nTableBottom, nFlyBottom); } + +CPPUNIT_TEST_FIXTURE(Test, testDOCXFloatingTableRedline) +{ + // Given a document with 3 floating tables, the last table has a last cell with a custom cell + // background: + // When importing that document from DOCX: + createSwDoc("floattable-redline.docx"); + + // Then make sure that the cell background is not lost: + SwDoc* pDoc = getSwDoc(); + sw::TableFrameFormats& rTableFormats = *pDoc->GetTableFrameFormats(); + SwTableFormat* pTableFormat = rTableFormats[2]; + SwTable* pTable = SwTable::FindTable(pTableFormat); + const SwTableBox* pCell = pTable->GetTableBox(u"B2"_ustr); + const SwAttrSet& rCellSet = pCell->GetFrameFormat()->GetAttrSet(); + const SvxBrushItem& rCellBackground = rCellSet.GetBackground(); + // Without the accompanying fix in place, this test would have failed with: + // - Expected: rgba[d0d8e8ff] + // - Actual : rgba[ffffff00] + // i.e. the cell background was white, should be gray. + CPPUNIT_ASSERT_EQUAL(Color(0xD0D8E8), rCellBackground.GetColor()); +} } CPPUNIT_PLUGIN_IMPLEMENT(); diff --git a/sw/qa/writerfilter/dmapper/data/floattable-redline.docx b/sw/qa/writerfilter/dmapper/data/floattable-redline.docx new file mode 100644 index 000000000000..9b45768ca9d2 Binary files /dev/null and b/sw/qa/writerfilter/dmapper/data/floattable-redline.docx differ diff --git a/sw/source/writerfilter/dmapper/DomainMapperTableHandler.cxx b/sw/source/writerfilter/dmapper/DomainMapperTableHandler.cxx index a98cd7f56403..74c25f9ad9ea 100644 --- a/sw/source/writerfilter/dmapper/DomainMapperTableHandler.cxx +++ b/sw/source/writerfilter/dmapper/DomainMapperTableHandler.cxx @@ -1668,7 +1668,7 @@ void DomainMapperTableHandler::endTable(unsigned int nestedTableLevel) bool bRecordChanges = m_rDMapper_Impl.GetSettingsTable()->GetRecordChanges(); if (xTextAppendAndConvert.is() && !(bInFootnote && bRecordChanges)) { - const std::deque<StoredRedline>& rFramedRedlines = m_rDMapper_Impl.m_aStoredRedlines[StoredRedlines::FRAME]; + std::deque<StoredRedline>& rFramedRedlines = m_rDMapper_Impl.m_aStoredRedlines[StoredRedlines::FRAME]; std::vector<sal_Int32> redPos, redLen; std::vector<OUString> redCell; std::vector<OUString> redTable; @@ -1722,6 +1722,9 @@ void DomainMapperTableHandler::endTable(unsigned int nestedTableLevel) } AfterConvertToTextFrame(m_rDMapper_Impl, rFramedRedlines, redPos, redLen, redCell, redTable); + + // These redlines are not relevant for the next floating table, clear them. + rFramedRedlines.clear(); } if (xFrameAnchor.is() && eBreakType != style::BreakType_NONE) commit f5761c003dfc99ec4df123a935285f9bf65de474 Author: Miklos Vajna <vmik...@collabora.com> AuthorDate: Mon Jul 7 10:52:26 2025 +0200 Commit: Andras Timar <andras.ti...@collabora.com> CommitDate: Mon Jul 14 19:14:22 2025 +0200 tdf#166319 sw interdependent redlines: fix redo of accept of ins-then-fmt's fmt The bugdoc has <ins>AA<format>BB</format>CC</ins>, go to middle of BB, accept, undo, redo: there is no format redline in the document, but there should be one. What happens is that the "accept" action creates an SwUndoAcceptRedline with the default depth = 0, but then it carefully calls lcl_DeleteInnerRedline() in sw::DocumentRedlineManager::AcceptRedlineRange() instead of a plain accept. The undo action already knows how to only accept the underlying redline, so fix the problem in sw::DocumentRedlineManager::AcceptRedlineRange() by correctly recording the UI action with depth = 1, which gives us both working undo & redo. The exec of the undo action's redo calls sw::DocumentRedlineManager::AcceptRedline(), which now calls the same lcl_DeleteInnerRedline() as the original UI action, so this looks consistent now. Change-Id: Ibb21c0745f3dd0aacec7cf7448c7c85245dbee69 Reviewed-on: https://gerrit.libreoffice.org/c/core/+/187467 Tested-by: Jenkins CollaboraOffice <jenkinscollaboraoff...@gmail.com> Reviewed-by: Caolán McNamara <caolan.mcnam...@collabora.com> diff --git a/sw/qa/core/doc/doc.cxx b/sw/qa/core/doc/doc.cxx index ba2520d4054e..96869044ce82 100644 --- a/sw/qa/core/doc/doc.cxx +++ b/sw/qa/core/doc/doc.cxx @@ -908,6 +908,43 @@ CPPUNIT_TEST_FIXTURE(SwCoreDocTest, testInsThenFormat) // Without the accompanying fix in place, this test would have failed, the text was AAABBBCCC, // just the format of BBB was dropped. CPPUNIT_ASSERT(pTextNode->GetText().isEmpty()); + + // And given a state where you're just after the undo for the accept of insert-then-format's + // format: + pWrtShell->Undo(); + // Undo() creates a new cursor. + pCursor = pWrtShell->GetCursor(); + pCursor->DeleteMark(); + pWrtShell->SttEndDoc(/*bStt=*/true); + // Move inside "BBB". + pWrtShell->Right(SwCursorSkipMode::Chars, /*bSelect=*/false, 4, /*bBasicCall=*/false); + nRedline = 0; + rRedlines.FindAtPosition(*pCursor->Start(), nRedline); + // A redline is found. + CPPUNIT_ASSERT_LESS(rRedlines.size(), nRedline); + pWrtShell->AcceptRedline(nRedline); + pWrtShell->Undo(); + + // When redoing that accept: + pWrtShell->Redo(); + + // Then make sure we have a single format redline as a result: + pCursor = pWrtShell->GetCursor(); + pCursor->DeleteMark(); + pWrtShell->SttEndDoc(/*bStt=*/true); + // Move inside "BBB". + pWrtShell->Right(SwCursorSkipMode::Chars, /*bSelect=*/false, 4, /*bBasicCall=*/false); + nRedline = 0; + pRedline = rRedlines.FindAtPosition(*pCursor->Start(), nRedline); + // Without the accompanying fix in place, this test would have failed with: + // - Expected: 1 + // - Actual : 0 + // i.e. everything was accepted, even the format redline was gone from the document, instead of + // just accepting the underlying insert. + CPPUNIT_ASSERT_EQUAL(static_cast<size_t>(1), rRedlines.size()); + CPPUNIT_ASSERT(pRedline); + CPPUNIT_ASSERT_EQUAL(RedlineType::Format, pRedline->GetType()); + CPPUNIT_ASSERT(!pRedline->GetRedlineData().Next()); } CPPUNIT_TEST_FIXTURE(SwCoreDocTest, testDelThenFormat) diff --git a/sw/source/core/doc/DocumentRedlineManager.cxx b/sw/source/core/doc/DocumentRedlineManager.cxx index a02f5b947a57..8d8358d0fe80 100644 --- a/sw/source/core/doc/DocumentRedlineManager.cxx +++ b/sw/source/core/doc/DocumentRedlineManager.cxx @@ -3358,15 +3358,22 @@ bool DocumentRedlineManager::AcceptRedlineRange(SwRedlineTable::size_type nPosOr } else if (pTmp->GetRedlineData(0).CanCombineForAcceptReject(aOrigData)) { + bool bHierarchicalFormat = pTmp->GetType() == RedlineType::Format && pTmp->GetStackCount() > 1; if (m_rDoc.GetIDocumentUndoRedo().DoesUndo()) { + sal_Int8 nDepth = 0; + if (bHierarchicalFormat && pTmp->GetType(1) == RedlineType::Insert) + { + // Only work with the underlying insert, so the undo action matches the UI + // action below. + nDepth = 1; + } m_rDoc.GetIDocumentUndoRedo().AppendUndo( - std::make_unique<SwUndoAcceptRedline>(*pTmp)); + std::make_unique<SwUndoAcceptRedline>(*pTmp, nDepth)); } nPamEndNI = pTmp->Start()->GetNodeIndex(); nPamEndCI = pTmp->Start()->GetContentIndex(); - bool bHierarchicalFormat = pTmp->GetType() == RedlineType::Format && pTmp->GetStackCount() > 1; if (bHierarchicalFormat && pTmp->GetType(1) == RedlineType::Insert) { // This combination of 2 redline types prefers accepting the inner one first. diff --git a/sw/source/core/undo/unredln.cxx b/sw/source/core/undo/unredln.cxx index 7786b1a5cf42..77c15f282b7d 100644 --- a/sw/source/core/undo/unredln.cxx +++ b/sw/source/core/undo/unredln.cxx @@ -125,6 +125,10 @@ void SwUndoRedline::dumpAsXml(xmlTextWriterPtr pWriter) const (void)xmlTextWriterWriteAttribute(pWriter, BAD_CAST("value"), BAD_CAST(OString::boolean(mbHierarchical).getStr())); (void)xmlTextWriterEndElement(pWriter); + (void)xmlTextWriterStartElement(pWriter, BAD_CAST("depth")); + (void)xmlTextWriterWriteAttribute(pWriter, BAD_CAST("value"), + BAD_CAST(OString::number(mnDepth).getStr())); + (void)xmlTextWriterEndElement(pWriter); (void)xmlTextWriterEndElement(pWriter); }