sw/CppunitTest_sw_core_layout_flycnt.mk | 82 +++++++++++++++++++++++++++++++ sw/Module_sw.mk | 1 sw/inc/formatflysplit.hxx | 4 + sw/qa/core/layout/data/floattable.docx |binary sw/qa/core/layout/flycnt.cxx | 83 ++++++++++++++++++++++++++++++++ sw/source/core/attr/formatflysplit.cxx | 18 ++++++ sw/source/core/inc/flyfrms.hxx | 7 +- sw/source/core/inc/pagefrm.hxx | 3 - sw/source/core/inc/tabfrm.hxx | 4 + sw/source/core/layout/flycnt.cxx | 45 +++++++++++++++-- sw/source/core/text/xmldump.cxx | 30 +++++++++++ 11 files changed, 266 insertions(+), 11 deletions(-)
New commits: commit 995198bfff4ae8abaf2129fe99d9f8ef899a4f25 Author: Miklos Vajna <vmik...@collabora.com> AuthorDate: Mon Feb 13 12:55:32 2023 +0100 Commit: Miklos Vajna <vmik...@collabora.com> CommitDate: Mon Feb 13 15:04:12 2023 +0000 sw floattable: handle table-in-fly in SwFrame::GetNextFlyLeaf() Trying to lay out a split fly that contains a table resulted in a stack overflow. The reason for this was that SwObjectFormatter::FormatObjsAtFrame_() has a loop that will format all objects of the current fly frame, but we managed to anchor the follow fly into itself. Fix the problem by improving SwFrame::GetNextFlyLeaf(), somewhat based on how SwFrame::GetNextSctLeaf() has special cases for tables. This way once we split the fly frame, we'll try to move the follow anchor frame to the next page's body frame, and not to a child of the follow fly itself. Also add a first floattable testcase, we can already assert that the table is split correctly. Do this in a separate suite for now, since the pool's default SwFormatFlySplit is only created once, so SwFormatFlySplit::SetForce(false) doesn't have the wanted effect. The anchor's text is still on both pages, should be on page 2 only. Change-Id: Ie2ce75dbace5d9716008351aedb6a8989036badb Reviewed-on: https://gerrit.libreoffice.org/c/core/+/146854 Reviewed-by: Miklos Vajna <vmik...@collabora.com> Tested-by: Jenkins diff --git a/sw/CppunitTest_sw_core_layout_flycnt.mk b/sw/CppunitTest_sw_core_layout_flycnt.mk new file mode 100644 index 000000000000..c9668afbc9b8 --- /dev/null +++ b/sw/CppunitTest_sw_core_layout_flycnt.mk @@ -0,0 +1,82 @@ +# -*- Mode: makefile-gmake; tab-width: 4; indent-tabs-mode: t -*- +#************************************************************************* +# +# This file is part of the LibreOffice project. +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +#************************************************************************* + +$(eval $(call gb_CppunitTest_CppunitTest,sw_core_layout_flycnt)) + +$(eval $(call gb_CppunitTest_use_common_precompiled_header,sw_core_layout_flycnt)) + +# TODO merge this with sw_core_layout once SwFormatFlySplit::SetForce() is gone. +$(eval $(call gb_CppunitTest_add_exception_objects,sw_core_layout_flycnt, \ + sw/qa/core/layout/flycnt \ +)) + +$(eval $(call gb_CppunitTest_use_libraries,sw_core_layout_flycnt, \ + editeng \ + comphelper \ + cppu \ + cppuhelper \ + sal \ + sfx \ + subsequenttest \ + sw \ + swqahelper \ + test \ + unotest \ + utl \ + vcl \ + svt \ + tl \ + svl \ + svxcore \ +)) + +$(eval $(call gb_CppunitTest_use_externals,sw_core_layout_flycnt,\ + boost_headers \ + libxml2 \ +)) + +$(eval $(call gb_CppunitTest_set_include,sw_core_layout_flycnt,\ + -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_api,sw_core_layout_flycnt,\ + udkapi \ + offapi \ + oovbaapi \ +)) + +$(eval $(call gb_CppunitTest_use_ure,sw_core_layout_flycnt)) +$(eval $(call gb_CppunitTest_use_vcl,sw_core_layout_flycnt)) + +$(eval $(call gb_CppunitTest_use_rdb,sw_core_layout_flycnt,services)) + +$(eval $(call gb_CppunitTest_use_custom_headers,sw_core_layout_flycnt,\ + officecfg/registry \ +)) + +$(eval $(call gb_CppunitTest_use_configuration,sw_core_layout_flycnt)) + +$(eval $(call gb_CppunitTest_use_uiconfigs,sw_core_layout_flycnt, \ + modules/swriter \ + svt \ + svx \ +)) + +# assert if font/glyph fallback occurs +$(eval $(call gb_CppunitTest_set_non_application_font_use,sw_core_layout_flycnt,abort)) + +$(eval $(call gb_CppunitTest_use_more_fonts,sw_core_layout_flycnt)) + +# vim: set noet sw=4 ts=4: diff --git a/sw/Module_sw.mk b/sw/Module_sw.mk index 2b99d0bff22a..45325bf3ea9a 100644 --- a/sw/Module_sw.mk +++ b/sw/Module_sw.mk @@ -138,6 +138,7 @@ $(eval $(call gb_Module_add_slowcheck_targets,sw,\ CppunitTest_sw_uibase_wrtsh \ CppunitTest_sw_core_accessibilitycheck \ CppunitTest_sw_core_layout \ + CppunitTest_sw_core_layout_flycnt \ CppunitTest_sw_core_fields \ CppunitTest_sw_core_tox \ CppunitTest_sw_core_frmedt \ diff --git a/sw/inc/formatflysplit.hxx b/sw/inc/formatflysplit.hxx index 5f7dda675ed2..1db9587752b6 100644 --- a/sw/inc/formatflysplit.hxx +++ b/sw/inc/formatflysplit.hxx @@ -34,6 +34,10 @@ public: SwFormatFlySplit* Clone(SfxItemPool* pPool = nullptr) const override; void dumpAsXml(xmlTextWriterPtr pWriter) const override; + + // Force-enable for test purposes. + static void SetForce(bool bForce); + static bool GetForce(); }; inline const SwFormatFlySplit& SwAttrSet::GetFlySplit(bool bInP) const diff --git a/sw/qa/core/layout/data/floattable.docx b/sw/qa/core/layout/data/floattable.docx new file mode 100644 index 000000000000..ee1b029882dd Binary files /dev/null and b/sw/qa/core/layout/data/floattable.docx differ diff --git a/sw/qa/core/layout/flycnt.cxx b/sw/qa/core/layout/flycnt.cxx new file mode 100644 index 000000000000..a006ecb54f00 --- /dev/null +++ b/sw/qa/core/layout/flycnt.cxx @@ -0,0 +1,83 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include <swmodeltestbase.hxx> + +#include <comphelper/scopeguard.hxx> + +#include <IDocumentLayoutAccess.hxx> +#include <anchoredobject.hxx> +#include <flyfrms.hxx> +#include <formatflysplit.hxx> +#include <pagefrm.hxx> +#include <rootfrm.hxx> +#include <sortedobjs.hxx> +#include <tabfrm.hxx> + +namespace +{ +/// Covers sw/source/core/layout/flycnt.cxx fixes, i.e. mostly SwFlyAtContentFrame. +class Test : public SwModelTestBase +{ +public: + Test() + : SwModelTestBase("/sw/qa/core/layout/data/") + { + } +}; + +CPPUNIT_TEST_FIXTURE(Test, testSplitFlyWithTable) +{ + // Given a document with a multi-page floating table: + SwFormatFlySplit::SetForce(true); + comphelper::ScopeGuard g([] { SwFormatFlySplit::SetForce(false); }); + createSwDoc("floattable.docx"); + + // When laying out that document: + calcLayout(); + + // Then make sure that the first row goes to page 1 and the second row goes to page 2, while the + // table is floating: + SwDoc* pDoc = getSwDoc(); + // Without the accompanying fix in place, this test would have failed with a stack overflow + // because the follow frame of the anchor was moved into the follow frame of the fly, so the fly + // was anchored in itself. + SwRootFrame* pLayout = pDoc->getIDocumentLayoutAccess().GetCurrentLayout(); + // Page 1 has a master fly, which contains a master table: + auto pPage1 = dynamic_cast<SwPageFrame*>(pLayout->Lower()); + CPPUNIT_ASSERT(pPage1); + const SwSortedObjs& rPage1Objs = *pPage1->GetSortedObjs(); + CPPUNIT_ASSERT_EQUAL(static_cast<size_t>(1), rPage1Objs.size()); + auto pPage1Fly = dynamic_cast<SwFlyAtContentFrame*>(rPage1Objs[0]); + CPPUNIT_ASSERT(pPage1Fly); + CPPUNIT_ASSERT(!pPage1Fly->GetPrecede()); + CPPUNIT_ASSERT(pPage1Fly->GetFollow()); + auto pPage1Table = dynamic_cast<SwTabFrame*>(pPage1Fly->GetLower()); + CPPUNIT_ASSERT(pPage1Table); + CPPUNIT_ASSERT(!pPage1Table->GetPrecede()); + CPPUNIT_ASSERT(pPage1Table->GetFollow()); + // Page 2 has a follow fly, which contains a follow table: + auto pPage2 = dynamic_cast<SwPageFrame*>(pPage1->GetNext()); + CPPUNIT_ASSERT(pPage2); + const SwSortedObjs& rPage2Objs = *pPage2->GetSortedObjs(); + CPPUNIT_ASSERT_EQUAL(static_cast<size_t>(1), rPage2Objs.size()); + auto pPage2Fly = dynamic_cast<SwFlyAtContentFrame*>(rPage2Objs[0]); + CPPUNIT_ASSERT(pPage2Fly); + CPPUNIT_ASSERT(pPage2Fly->GetPrecede()); + CPPUNIT_ASSERT(!pPage2Fly->GetFollow()); + auto pPage2Table = dynamic_cast<SwTabFrame*>(pPage2Fly->GetLower()); + CPPUNIT_ASSERT(pPage2Table); + CPPUNIT_ASSERT(pPage2Table->GetPrecede()); + CPPUNIT_ASSERT(!pPage2Table->GetFollow()); +} +} + +CPPUNIT_PLUGIN_IMPLEMENT(); + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sw/source/core/attr/formatflysplit.cxx b/sw/source/core/attr/formatflysplit.cxx index 904fd9a8bb5c..7bc8a75594d7 100644 --- a/sw/source/core/attr/formatflysplit.cxx +++ b/sw/source/core/attr/formatflysplit.cxx @@ -21,6 +21,8 @@ #include <libxml/xmlwriter.h> +static std::optional<bool> g_oForce; + SwFormatFlySplit::SwFormatFlySplit(bool bSplit) : SfxBoolItem(RES_FLY_SPLIT, bSplit) { @@ -34,9 +36,9 @@ SwFormatFlySplit::SwFormatFlySplit(bool bSplit) // // - Both the master fly and the follow flys need an anchor. At the same time, we want all text // of the anchor frame to be wrapped around the last follow fly frame, for Word compatibility. - // These are solved by splitting the anchor frame as many times as needed, always at text + // These are solved by splitting the anchor frame as many times as needed, always at // TextFrameIndex 0. - if (getenv("SW_FORCE_FLY_SPLIT")) + if (SwFormatFlySplit::GetForce()) { SetValue(true); } @@ -57,4 +59,16 @@ void SwFormatFlySplit::dumpAsXml(xmlTextWriterPtr pWriter) const (void)xmlTextWriterEndElement(pWriter); } +void SwFormatFlySplit::SetForce(bool bForce) { g_oForce = bForce; } + +bool SwFormatFlySplit::GetForce() +{ + if (g_oForce.has_value()) + { + return *g_oForce; + } + + return getenv("SW_FORCE_FLY_SPLIT") != nullptr; +} + /* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sw/source/core/inc/flyfrms.hxx b/sw/source/core/inc/flyfrms.hxx index 608e04ff8f11..8d3bd95589bc 100644 --- a/sw/source/core/inc/flyfrms.hxx +++ b/sw/source/core/inc/flyfrms.hxx @@ -21,6 +21,8 @@ #include <sal/config.h> +#include <swdllapi.h> + #include "flyfrm.hxx" #include "flowfrm.hxx" @@ -30,7 +32,7 @@ double getLocalFrameRotation_from_SwNoTextFrame(const SwNoTextFrame& rNoTextFram // Base class for those Flys that can "move freely" or better that are not // bound in Content. -class SwFlyFreeFrame : public SwFlyFrame +class SW_DLLPUBLIC SwFlyFreeFrame : public SwFlyFrame { private: // #i34753# - flag for at-page anchored Writer fly frames @@ -157,7 +159,7 @@ public: }; // Flys that are bound to Content but not in Content -class SwFlyAtContentFrame final: public SwFlyFreeFrame, public SwFlowFrame +class SW_DLLPUBLIC SwFlyAtContentFrame final: public SwFlyFreeFrame, public SwFlowFrame { virtual void MakeAll(vcl::RenderContext* pRenderContext) override; @@ -198,6 +200,7 @@ public: SwFlyAtContentFrame* GetFollow(); const SwFlyAtContentFrame* GetPrecede() const; SwFlyAtContentFrame* GetPrecede(); + void dumpAsXmlAttributes(xmlTextWriterPtr pWriter) const override; }; // Flys that are bound to a character in Content diff --git a/sw/source/core/inc/pagefrm.hxx b/sw/source/core/inc/pagefrm.hxx index 73d81a6895b5..88004bb7ccb3 100644 --- a/sw/source/core/inc/pagefrm.hxx +++ b/sw/source/core/inc/pagefrm.hxx @@ -20,6 +20,7 @@ #define INCLUDED_SW_SOURCE_CORE_INC_PAGEFRM_HXX #include <viewsh.hxx> +#include <swdllapi.h> #include "ftnboss.hxx" #include "hffrm.hxx" @@ -54,7 +55,7 @@ namespace o3tl { /// A page of the document layout. Upper frame is expected to be an SwRootFrame /// instance. At least an SwBodyFrame lower is expected. -class SAL_DLLPUBLIC_RTTI SwPageFrame final: public SwFootnoteBossFrame +class SW_DLLPUBLIC SwPageFrame final: public SwFootnoteBossFrame { friend class SwFrame; diff --git a/sw/source/core/inc/tabfrm.hxx b/sw/source/core/inc/tabfrm.hxx index 8bf2b863548e..ca4af59b223c 100644 --- a/sw/source/core/inc/tabfrm.hxx +++ b/sw/source/core/inc/tabfrm.hxx @@ -19,6 +19,8 @@ #ifndef INCLUDED_SW_SOURCE_CORE_INC_TABFRM_HXX #define INCLUDED_SW_SOURCE_CORE_INC_TABFRM_HXX +#include <swdllapi.h> + #include "layfrm.hxx" #include "flowfrm.hxx" @@ -43,7 +45,7 @@ namespace o3tl { } /// SwTabFrame is one table in the document layout, containing rows (which contain cells). -class SAL_DLLPUBLIC_RTTI SwTabFrame final: public SwLayoutFrame, public SwFlowFrame +class SW_DLLPUBLIC SwTabFrame final: public SwLayoutFrame, public SwFlowFrame { friend void CalcContent( SwLayoutFrame *pLay, bool bNoColl ); diff --git a/sw/source/core/layout/flycnt.cxx b/sw/source/core/layout/flycnt.cxx index 2d96e0fcea64..b18cb6a21cb5 100644 --- a/sw/source/core/layout/flycnt.cxx +++ b/sw/source/core/layout/flycnt.cxx @@ -1552,14 +1552,49 @@ SwLayoutFrame *SwFrame::GetNextFlyLeaf( MakePageType eMakePage ) auto pFly = dynamic_cast<SwFlyAtContentFrame*>(FindFlyFrame()); assert(pFly && "GetNextFlyLeaf: missing fly frame"); - SwLayoutFrame *pLayLeaf = GetNextLayoutLeaf(); - if (!pLayLeaf) + SwLayoutFrame *pLayLeaf = nullptr; + // Look up the first candidate. + if (IsTabFrame()) { - if (eMakePage == MAKEPAGE_INSERT) + // If we're in a table, try to find the next frame of the table's last content. + SwFrame* pContent = static_cast<SwTabFrame*>(this)->FindLastContentOrTable(); + pLayLeaf = pContent ? pContent->GetUpper() : nullptr; + } + else + { + pLayLeaf = GetNextLayoutLeaf(); + } + + SwLayoutFrame* pOldLayLeaf = nullptr; + while (true) + { + if (pLayLeaf) { - InsertPage(FindPageFrame(), false); - pLayLeaf = GetNextLayoutLeaf(); + // If we have a candidate, make sure that it's a child of our follow. + if (pFly->IsFlySplitAllowed()) + { + if (pFly->GetFollow() != pLayLeaf->FindFlyFrame()) + { + // It's not in our follow, reject. + pOldLayLeaf = pLayLeaf; + pLayLeaf = pLayLeaf->GetNextLayoutLeaf(); + continue; + } + } + } + else + { + // No candidate: insert a page and try again. + if (eMakePage == MAKEPAGE_INSERT) + { + InsertPage(FindPageFrame(), false); + // If we already had a cancidate, continue trying with that instead of starting from + // scratch. + pLayLeaf = pOldLayLeaf ? pOldLayLeaf : GetNextLayoutLeaf(); + continue; + } } + break; } if( pLayLeaf ) diff --git a/sw/source/core/text/xmldump.cxx b/sw/source/core/text/xmldump.cxx index ba7a4d14ac4c..3fc64f1be7a2 100644 --- a/sw/source/core/text/xmldump.cxx +++ b/sw/source/core/text/xmldump.cxx @@ -26,6 +26,7 @@ #include <libxml/xmlwriter.h> #include <SwPortionHandler.hxx> #include <view.hxx> +#include <flyfrms.hxx> #include <svx/svdobj.hxx> #include "porlay.hxx" @@ -394,6 +395,11 @@ void SwFrame::dumpAsXml( xmlTextWriterPtr writer ) const else { dumpChildrenAsXml( writer ); + if (IsFlyFrame()) + { + auto pFlyFrame = static_cast<const SwFlyFrame*>(this); + pFlyFrame->SwAnchoredObject::dumpAsXml(writer); + } } (void)xmlTextWriterEndElement( writer ); } @@ -494,6 +500,12 @@ void SwAnchoredObject::dumpAsXml( xmlTextWriterPtr writer ) const (void)xmlTextWriterStartElement( writer, BAD_CAST( getElementName() ) ); (void)xmlTextWriterWriteFormatAttribute( writer, BAD_CAST( "ptr" ), "%p", this ); + (void)xmlTextWriterWriteAttribute(writer, BAD_CAST("anchor-frame"), BAD_CAST(OString::number(mpAnchorFrame->GetFrameId()).getStr())); + SwTextFrame* pAnchorCharFrame = const_cast<SwAnchoredObject*>(this)->FindAnchorCharFrame(); + if (pAnchorCharFrame) + { + (void)xmlTextWriterWriteAttribute(writer, BAD_CAST("anchor-char-frame"), BAD_CAST(OString::number(pAnchorCharFrame->GetFrameId()).getStr())); + } (void)xmlTextWriterStartElement( writer, BAD_CAST( "bounds" ) ); // don't call GetObjBoundRect(), it modifies the layout @@ -537,6 +549,24 @@ void SwTextFrame::dumpAsXmlAttributes( xmlTextWriterPtr writer ) const (void)xmlTextWriterWriteAttribute(writer, BAD_CAST("offset"), BAD_CAST(OString::number(static_cast<sal_Int32>(mnOffset)).getStr())); } +void SwFlyAtContentFrame::dumpAsXmlAttributes(xmlTextWriterPtr pWriter) const +{ + SwFlyFreeFrame::dumpAsXmlAttributes(pWriter); + + if (m_pFollow != nullptr) + { + (void)xmlTextWriterWriteAttribute( + pWriter, BAD_CAST("follow"), + BAD_CAST(OString::number(m_pFollow->GetFrame().GetFrameId()).getStr())); + } + if (m_pPrecede != nullptr) + { + (void)xmlTextWriterWriteAttribute( + pWriter, BAD_CAST("precede"), + BAD_CAST(OString::number(m_pPrecede->GetFrame().GetFrameId()).getStr())); + } +} + void SwSectionFrame::dumpAsXmlAttributes( xmlTextWriterPtr writer ) const { SwFrame::dumpAsXmlAttributes( writer );