sw: floating table split

The problem is that the Table Split feature on a floating table results
in 2 tables inside 1 frame, which then cannot be exported to DOCX as a
floating table.

Split the fly frame in this case, and add an anchor node because every
floating table needs its own anchor node.

Change-Id: I7444a75a953fcea60ffc92c2fa7eaca1b044f54b
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/190009
Tested-by: Jenkins CollaboraOffice <jenkinscollaboraoffice@gmail.com>
Reviewed-by: Miklos Vajna <vmiklos@collabora.com>
(cherry picked from commit 6f55b9a74f8fdd8609e1aa72388a5f8b97dfcea0)
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/190075
Tested-by: Jenkins
This commit is contained in:
Michael Stahl
2025-09-03 11:22:11 +02:00
committed by Miklos Vajna
parent 0f393e0c1d
commit fc53ab8e76
7 changed files with 315 additions and 15 deletions
+6 -1
View File
@@ -26,6 +26,7 @@
#include <vector>
#include <memory>
#include <optional>
#include <tuple>
#include "bparr.hxx"
#include "ndtyp.hxx"
@@ -38,6 +39,7 @@ class SwAttrSet;
class SfxItemSet;
class SwContentNode;
class SwDoc;
class SwFrameFormat;
class SwGrfFormatColl;
class SwGrfNode;
class SwNode;
@@ -301,11 +303,14 @@ public:
Is the flag bCalcNewSize set to TRUE, the new SSize for both
tables is calculated from the Maximum of the boxes, provided
SSize is set "absolute" (LONG_MAX).
(Momentarily this is needed only for the RTF-parser.) */
*/
SwTableNode* SplitTable( SwNode& rPos, bool bAfter = true,
bool bCalcNewSize = false );
void SplitFloatingTableFrame(SwTableNode & rNewTableNode,
::std::tuple<SwStartNode &, SwFrameFormat &, SwTextNode &> floatingFrame);
/// Two Tables that are following one another are merged.
bool MergeTable( SwNode& rPos, bool bWithPrev = true );
void MergeFloatingTableFrame(SwNodeOffset nEndOfOldFlyIndex);
/// Insert a new SwSection.
SwSectionNode* InsertTextSection(SwNode& rNd,
Binary file not shown.
+148
View File
@@ -271,6 +271,154 @@ CPPUNIT_TEST_FIXTURE(SwUiWriterTest9, testTdf159377)
CPPUNIT_ASSERT_EQUAL(SwNodeOffset(28), pDoc->GetNodes().Count());
}
CPPUNIT_TEST_FIXTURE(SwUiWriterTest9, testSplitFloatingTable)
{
createSwDoc("floattable-split.docx");
SwWrtShell* pWrtShell = getSwDocShell()->GetWrtShell();
{
xmlDocUniquePtr pXmlDoc = parseLayoutDump();
// apparently follow fly is on the text frame of page 1 even though
// positioned on page 2...
assertXPath(pXmlDoc, "/root/page[1]/body/txt[2]/anchored/fly", 2);
assertXPath(pXmlDoc, "/root/page[1]/body/txt[2]/anchored/fly/txt", 0);
assertXPath(pXmlDoc,
"/root/page[1]/body/txt[2]/anchored/fly[infos/bounds/@top < "
"/root/page[2]/infos/bounds/@top]/tab",
1);
assertXPath(pXmlDoc,
"/root/page[1]/body/txt[2]/anchored/fly[infos/bounds/@top > "
"/root/page[2]/infos/bounds/@top]/tab",
1);
assertXPath(pXmlDoc,
"/root/page[1]/body/txt[2]/anchored/fly[infos/bounds/@top < "
"/root/page[2]/infos/bounds/@top]/tab/row",
5);
assertXPath(pXmlDoc,
"/root/page[1]/body/txt[2]/anchored/fly[infos/bounds/@top > "
"/root/page[2]/infos/bounds/@top]/tab/row",
4);
}
pWrtShell->GotoFly(UIName{ u"Frame1"_ustr });
pWrtShell->Down(/*bSelect=*/false, 3);
pWrtShell->SplitTable(SplitTable_HeadlineOption::BorderCopy);
{
xmlDocUniquePtr pXmlDoc = parseLayoutDump();
assertXPath(pXmlDoc, "/root/page[1]/body/txt[2]/anchored/fly", 1);
assertXPath(pXmlDoc, "/root/page[1]/body/txt[2]/anchored/fly/txt", 0);
assertXPath(pXmlDoc, "/root/page[1]/body/txt[2]/anchored/fly/tab", 1);
assertXPath(pXmlDoc, "/root/page[1]/body/txt[2]/anchored/fly/tab/row", 3);
assertXPath(pXmlDoc, "/root/page[1]/body/txt[3]/anchored/fly", 2);
assertXPath(pXmlDoc, "/root/page[1]/body/txt[3]/anchored/fly/txt", 0);
assertXPath(pXmlDoc,
"/root/page[1]/body/txt[3]/anchored/fly[infos/bounds/@top < "
"/root/page[2]/infos/bounds/@top]/tab",
1);
assertXPath(pXmlDoc,
"/root/page[1]/body/txt[3]/anchored/fly[infos/bounds/@top > "
"/root/page[2]/infos/bounds/@top]/tab",
1);
assertXPath(pXmlDoc,
"/root/page[1]/body/txt[3]/anchored/fly[infos/bounds/@top < "
"/root/page[2]/infos/bounds/@top]/tab/row",
2);
assertXPath(pXmlDoc,
"/root/page[1]/body/txt[3]/anchored/fly[infos/bounds/@top > "
"/root/page[2]/infos/bounds/@top]/tab/row",
4);
}
pWrtShell->Do(SwWrtShell::UNDO);
{
xmlDocUniquePtr pXmlDoc = parseLayoutDump();
// apparently follow fly is on the text frame of page 1 even though
// positioned on page 2...
assertXPath(pXmlDoc, "/root/page[1]/body/txt[2]/anchored/fly", 2);
assertXPath(pXmlDoc, "/root/page[1]/body/txt[2]/anchored/fly/txt", 0);
assertXPath(pXmlDoc,
"/root/page[1]/body/txt[2]/anchored/fly[infos/bounds/@top < "
"/root/page[2]/infos/bounds/@top]/tab",
1);
assertXPath(pXmlDoc,
"/root/page[1]/body/txt[2]/anchored/fly[infos/bounds/@top > "
"/root/page[2]/infos/bounds/@top]/tab",
1);
assertXPath(pXmlDoc,
"/root/page[1]/body/txt[2]/anchored/fly[infos/bounds/@top < "
"/root/page[2]/infos/bounds/@top]/tab/row",
5);
assertXPath(pXmlDoc,
"/root/page[1]/body/txt[2]/anchored/fly[infos/bounds/@top > "
"/root/page[2]/infos/bounds/@top]/tab/row",
4);
}
pWrtShell->Do(SwWrtShell::REDO);
{
xmlDocUniquePtr pXmlDoc = parseLayoutDump();
assertXPath(pXmlDoc, "/root/page[1]/body/txt[2]/anchored/fly", 1);
assertXPath(pXmlDoc, "/root/page[1]/body/txt[2]/anchored/fly/txt", 0);
assertXPath(pXmlDoc, "/root/page[1]/body/txt[2]/anchored/fly/tab", 1);
assertXPath(pXmlDoc, "/root/page[1]/body/txt[2]/anchored/fly/tab/row", 3);
assertXPath(pXmlDoc, "/root/page[1]/body/txt[3]/anchored/fly", 2);
assertXPath(pXmlDoc, "/root/page[1]/body/txt[3]/anchored/fly/txt", 0);
assertXPath(pXmlDoc,
"/root/page[1]/body/txt[3]/anchored/fly[infos/bounds/@top < "
"/root/page[2]/infos/bounds/@top]/tab",
1);
assertXPath(pXmlDoc,
"/root/page[1]/body/txt[3]/anchored/fly[infos/bounds/@top > "
"/root/page[2]/infos/bounds/@top]/tab",
1);
assertXPath(pXmlDoc,
"/root/page[1]/body/txt[3]/anchored/fly[infos/bounds/@top < "
"/root/page[2]/infos/bounds/@top]/tab/row",
2);
assertXPath(pXmlDoc,
"/root/page[1]/body/txt[3]/anchored/fly[infos/bounds/@top > "
"/root/page[2]/infos/bounds/@top]/tab/row",
4);
}
// now check that these round-trip as floating tables
saveAndReload(u"Office Open XML Text"_ustr);
{
xmlDocUniquePtr pXmlDoc = parseLayoutDump();
assertXPath(pXmlDoc, "/root/page[1]/body/txt[2]/anchored/fly", 1);
assertXPath(pXmlDoc, "/root/page[1]/body/txt[2]/anchored/fly/txt", 0);
assertXPath(pXmlDoc, "/root/page[1]/body/txt[2]/anchored/fly/tab", 1);
assertXPath(pXmlDoc, "/root/page[1]/body/txt[2]/anchored/fly/tab/row", 3);
assertXPath(pXmlDoc, "/root/page[1]/body/txt[3]/anchored/fly", 2);
assertXPath(pXmlDoc, "/root/page[1]/body/txt[3]/anchored/fly/txt", 0);
assertXPath(pXmlDoc,
"/root/page[1]/body/txt[3]/anchored/fly[infos/bounds/@top < "
"/root/page[2]/infos/bounds/@top]/tab",
1);
assertXPath(pXmlDoc,
"/root/page[1]/body/txt[3]/anchored/fly[infos/bounds/@top > "
"/root/page[2]/infos/bounds/@top]/tab",
1);
assertXPath(pXmlDoc,
"/root/page[1]/body/txt[3]/anchored/fly[infos/bounds/@top < "
"/root/page[2]/infos/bounds/@top]/tab/row",
2);
assertXPath(pXmlDoc,
"/root/page[1]/body/txt[3]/anchored/fly[infos/bounds/@top > "
"/root/page[2]/infos/bounds/@top]/tab/row",
4);
}
}
CPPUNIT_TEST_FIXTURE(SwUiWriterTest9, testPasteTableInMiddleOfParagraph)
{
createSwDoc();
+134 -7
View File
@@ -28,10 +28,12 @@
#include <svl/stritem.hxx>
#include <editeng/shaditem.hxx>
#include <fmtfsize.hxx>
#include <formatflysplit.hxx>
#include <fmtornt.hxx>
#include <fmtfordr.hxx>
#include <fmtpdsc.hxx>
#include <fmtanchr.hxx>
#include <fmtcntnt.hxx>
#include <fmtlsplt.hxx>
#include <frmatr.hxx>
#include <cellfrm.hxx>
@@ -80,6 +82,7 @@
#include <strings.hrc>
#include <docsh.hxx>
#include <unochart.hxx>
#include <unoframe.hxx>
#include <node.hxx>
#include <ndtxt.hxx>
#include <cstdlib>
@@ -3174,6 +3177,118 @@ void sw_BoxSetSplitBoxFormats( SwTableBox* pBox, SwCollectTableLineBoxes* pSplPa
}
}
namespace {
::std::optional<::std::tuple<SwStartNode &, SwFrameFormat &, SwTextNode &>>
FindFloatingTable(SwTableNode & rTableNode)
{
SwStartNode *const pStartNode{rTableNode.GetNodes()[rTableNode.GetIndex()-1]->GetStartNode()};
if (pStartNode
&& pStartNode->GetStartNodeType() == SwFlyStartNode
&& pStartNode->EndOfSectionIndex()-1 == rTableNode.EndOfSectionIndex())
{
SwFrameFormat *const pFormat{rTableNode.GetFlyFormat()};
if (pFormat && pFormat->GetFlySplit().GetValue())
{
SwFormatAnchor const& rAnchor{pFormat->GetAnchor()};
SwNode *const pNode{rAnchor.GetAnchorNode()};
if (pNode && pNode->IsTextNode() // cannot work for at-fly, at-page
&& rAnchor.GetAnchorId() != RndStdIds::FLY_AS_CHAR)
{
return {{*pStartNode, *pFormat, *pNode->GetTextNode()}};
}
}
}
return {};
}
} // namespace
void SwNodes::MergeFloatingTableFrame(SwNodeOffset const nEndOfOldFlyIndex)
{
SwEndNode *const pFrameEndNode{(*this)[nEndOfOldFlyIndex]->GetEndNode()};
assert(pFrameEndNode);
SwStartNode *const pFrameStartNode{pFrameEndNode->StartOfSectionNode()};
assert(pFrameStartNode && pFrameStartNode->GetStartNodeType() == SwFlyStartNode);
SwStartNode *const pNewFrameStartNode{(*this)[nEndOfOldFlyIndex+1]->GetStartNode()};
assert(pNewFrameStartNode && pNewFrameStartNode->GetStartNodeType() == SwFlyStartNode);
SwTableNode *const pNewTableNode{(*this)[nEndOfOldFlyIndex+2]->GetTableNode()};
assert(pNewTableNode);
assert(pNewTableNode->StartOfSectionNode() == pNewFrameStartNode);
SwTableNode *const pOldTableNode{(*this)[pFrameStartNode->GetIndex()+1]->GetTableNode()};
assert(pOldTableNode);
assert(pOldTableNode->StartOfSectionNode() == pFrameStartNode);
SwFrameFormat *const pNewFlyFormat{pNewTableNode->GetFlyFormat()};
pNewFlyFormat->DelFrames();
// reset anchor item
SwFrameFormat & rFlyFormat{*pOldTableNode->GetFlyFormat()};
SwFormatAnchor anchor{rFlyFormat.GetAnchor()};
SwNodeIndex const newAnchor(*(*this)[anchor.GetAnchorNode()->GetIndex()]);
SwTextNode *const pOldAnchorNode{(*this)[anchor.GetAnchorNode()->GetIndex()+1]->GetTextNode()};
assert(pOldAnchorNode);
SwPosition const pos{*pOldAnchorNode};
anchor.SetAnchor(&pos);
rFlyFormat.SetFormatAttr(anchor);
// delete inserted anchor node
{
SwPaM pamToCorr(newAnchor);
SwPaM pamSafe(newAnchor);
bool const success = pamSafe.Move(fnMoveForward, GoInContent);
assert(success); (void) success; // must move to old anchor node
::PaMCorrAbs(pamToCorr, *pamSafe.GetPoint());
}
Delete(newAnchor);
// delete new format (also deletes its SdrObject)
m_rMyDoc.DelFrameFormat(pNewFlyFormat);
// merge fly nodes/delete inserted nodes
pFrameStartNode->m_pEndOfSection = pNewFrameStartNode->EndOfSectionNode();
pFrameStartNode->EndOfSectionNode()->m_pStartOfSection = pFrameStartNode;
pNewTableNode->m_pStartOfSection = pFrameStartNode;
DelNodes(SwNodeIndex(*pFrameEndNode), SwNodeOffset(2));
}
void SwNodes::SplitFloatingTableFrame(SwTableNode & rNewTableNode,
::std::tuple<SwStartNode &, SwFrameFormat &, SwTextNode &> const floatingFrame)
{
auto const [rFrameStartNode, rFlyFormat, rAnchorNode]{floatingFrame};
SwEndNode *const pOldFrameEndNode{rFrameStartNode.EndOfSectionNode()->GetEndNode()};
assert(pOldFrameEndNode);
// insert new fly nodes between tables
new SwEndNode(rNewTableNode, rFrameStartNode);
SwStartNode *const pNewFrameStartNode{new SwStartNode{rNewTableNode, SwNodeType::Start, SwFlyStartNode}};
pOldFrameEndNode->m_pStartOfSection = pNewFrameStartNode;
pNewFrameStartNode->m_pEndOfSection = pOldFrameEndNode;
rNewTableNode.m_pStartOfSection = pNewFrameStartNode;
// relevant part of DocumentLayoutManager::CopyLayoutFormat()
auto const newName{m_rMyDoc.GetUniqueFrameName()};
SwFlyFrameFormat *const pNewFlyFormat{m_rMyDoc.MakeFlyFrameFormat(newName,
static_cast<SwFrameFormat *>(rFlyFormat.GetRegisteredIn()))};
SwXFrame::GetOrCreateSdrObject(*pNewFlyFormat);
pNewFlyFormat->CopyAttrs(rFlyFormat);
pNewFlyFormat->ResetFormatAttr(RES_CHAIN);
// deleting existing frames for anchor change should not be necessary, and
// crashes because it was already added to mpFlyDestroy by FndBox_::DelFrames()
// create new anchor node *before* existing one
SwTextNode *const pNewAnchorNode{MakeTextNode(rAnchorNode,
m_rMyDoc.getIDocumentStylePoolAccess().GetTextCollFromPool(RES_POOLCOLL_TEXT))};
SwFormatAnchor anchor{rFlyFormat.GetAnchor()};
SwPosition const pos{*pNewAnchorNode};
anchor.SetAnchor(&pos);
rFlyFormat.SetFormatAttr(anchor);
pNewFlyFormat->SetFormatAttr(SwFormatContent{pNewFrameStartNode});
pNewFlyFormat->MakeFrames();
}
/**
* Splits a Table in the top-level Line which contains the Index.
* All succeeding top-level Lines go into a new Table/Node.
@@ -3220,7 +3335,9 @@ void SwDoc::SplitTable( const SwPosition& rPos, SplitTable_HeadlineOption eHdlnM
aFndBox.SetTableLines( rTable );
aFndBox.DelFrames( rTable );
SwTableNode* pNew = GetNodes().SplitTable( rPos.GetNode(), false, bCalcNewSize );
auto const oFloatingFrame{FindFloatingTable(*pTNd)};
SwTableNode* pNew = GetNodes().SplitTable(rPos.GetNode(), false, bCalcNewSize);
if( pNew )
{
@@ -3228,8 +3345,12 @@ void SwDoc::SplitTable( const SwPosition& rPos, SplitTable_HeadlineOption eHdlnM
SwUndoSplitTable* pUndo = nullptr;
if (GetIDocumentUndoRedo().DoesUndo())
{
pUndo = new SwUndoSplitTable(
*pNew, std::move(pSaveRowSp), eHdlnMode, bCalcNewSize);
pUndo = new SwUndoSplitTable(*pNew, std::move(pSaveRowSp),
eHdlnMode, bCalcNewSize, oFloatingFrame
? ::std::get<0>(*oFloatingFrame).GetIndex() < ::std::get<2>(*oFloatingFrame).GetIndex()
? SwUndoSplitTable::FloatingMode::FloatingAnchoredAfter
: SwUndoSplitTable::FloatingMode::FloatingAnchoredBefore
: SwUndoSplitTable::FloatingMode::Not);
GetIDocumentUndoRedo().AppendUndo(std::unique_ptr<SwUndo>(pUndo));
if( aHistory.Count() )
pUndo->SaveFormula( aHistory );
@@ -3289,12 +3410,18 @@ void SwDoc::SplitTable( const SwPosition& rPos, SplitTable_HeadlineOption eHdlnM
break;
}
// And insert Frames
pNew->MakeOwnFrames();
if (oFloatingFrame)
{
GetNodes().SplitFloatingTableFrame(*pNew, *oFloatingFrame);
}
else
{
pNew->MakeOwnFrames();
// Insert a paragraph between the Table
GetNodes().MakeTextNode( *pNew,
// Insert a paragraph between the tables
GetNodes().MakeTextNode( *pNew,
getIDocumentStylePoolAccess().GetTextCollFromPool( RES_POOLCOLL_TEXT ) );
}
}
// Update Layout
+6 -1
View File
@@ -319,7 +319,12 @@ public:
class SwUndoSplitTable final : public SwUndo
{
public:
enum class FloatingMode { Not, FloatingAnchoredAfter, FloatingAnchoredBefore };
private:
SwNodeOffset m_nTableNode, m_nOffset;
FloatingMode m_eFloatingMode;
std::unique_ptr<SwSaveRowSpan> mpSaveRowSpan; // stores row span values at the splitting row
std::unique_ptr<SaveTable> m_pSavedTable;
std::unique_ptr<SwHistory> m_pHistory;
@@ -329,7 +334,7 @@ class SwUndoSplitTable final : public SwUndo
public:
SwUndoSplitTable( const SwTableNode& rTableNd, std::unique_ptr<SwSaveRowSpan> pRowSp,
SplitTable_HeadlineOption nMode, bool bCalcNewSize );
SplitTable_HeadlineOption nMode, bool bCalcNewSize, FloatingMode eMode);
virtual ~SwUndoSplitTable() override;
+20 -5
View File
@@ -2822,9 +2822,13 @@ void SwUndoCpyTable::RedoImpl(::sw::UndoRedoContext & rContext)
}
SwUndoSplitTable::SwUndoSplitTable( const SwTableNode& rTableNd,
std::unique_ptr<SwSaveRowSpan> pRowSp, SplitTable_HeadlineOption eMode, bool bNewSize )
: SwUndo( SwUndoId::SPLIT_TABLE, rTableNd.GetDoc() ),
m_nTableNode( rTableNd.GetIndex() ), m_nOffset( 0 ), mpSaveRowSpan( std::move(pRowSp) ),
std::unique_ptr<SwSaveRowSpan> pRowSp, SplitTable_HeadlineOption const eMode,
bool const bNewSize, FloatingMode const eFloatingMode)
: SwUndo( SwUndoId::SPLIT_TABLE, rTableNd.GetDoc() )
, m_nTableNode( rTableNd.GetIndex() )
, m_nOffset( 0 )
, m_eFloatingMode(eFloatingMode)
, mpSaveRowSpan( std::move(pRowSp) ),
m_nMode( eMode ), m_nFormulaEnd( 0 ), m_bCalcNewSize( bNewSize )
{
switch( m_nMode )
@@ -2853,10 +2857,20 @@ void SwUndoSplitTable::UndoImpl(::sw::UndoRedoContext & rContext)
SwPaM *const pPam(& rContext.GetCursorSupplier().CreateNewShellCursor());
SwPosition& rPtPos = *pPam->GetPoint();
rPtPos.Assign( m_nTableNode + m_nOffset );
assert(rPtPos.GetNode().GetContentNode()->Len() == 0); // empty para inserted
if (m_eFloatingMode != FloatingMode::Not)
{
// index members are *before* frame-split
auto const nEndOfOldFlyIndex{m_nTableNode + m_nOffset
+ (m_eFloatingMode == FloatingMode::FloatingAnchoredBefore ? 1 : 0)};
rDoc.GetNodes().MergeFloatingTableFrame(nEndOfOldFlyIndex);
}
else
{
rPtPos.Assign(m_nTableNode + m_nOffset);
assert(rPtPos.GetNode().IsTextNode()); // inserted text node
assert(rPtPos.GetNode().GetContentNode()->Len() == 0); // empty para inserted
// avoid asserts from ~SwContentIndexReg
SwNodeIndex const idx(rDoc.GetNodes(), m_nTableNode + m_nOffset);
{
@@ -2871,6 +2885,7 @@ void SwUndoSplitTable::UndoImpl(::sw::UndoRedoContext & rContext)
rPtPos.Assign( m_nTableNode + m_nOffset );
SwTableNode* pTableNd = rPtPos.GetNode().GetTableNode();
assert(pTableNd);
SwTable& rTable = pTableNd->GetTable();
rTable.SwitchFormulasToInternalRepresentation();
+1 -1
View File
@@ -392,7 +392,7 @@ typedef bool (SwWrtShell::*FNSimpleMove)();
enum class FieldDialogPressedButton { NONE, Previous, Next };
void Do(DoType eDoType, sal_uInt16 nCnt = 1, sal_uInt16 nOffset = 0);
SW_DLLPUBLIC void Do(DoType eDoType, sal_uInt16 nCnt = 1, sal_uInt16 nOffset = 0);
OUString GetDoString( DoType eDoType ) const;
OUString GetRepeatString() const;
void GetDoStrings( DoType eDoType, SfxStringListItem& rStrLstItem ) const;