sw/CppunitTest_sw_core_doc.mk | 1 sw/qa/core/doc/DocumentRedlineManager.cxx | 72 ++++ sw/qa/core/doc/data/ins.docx |binary sw/source/core/doc/DocumentRedlineManager.cxx | 431 +++++++++++++------------- sw/source/core/inc/DocumentRedlineManager.hxx | 2 5 files changed, 301 insertions(+), 205 deletions(-)
New commits: commit eef0dfed817e40cd83e8ba8e290f45c224257f97 Author: Miklos Vajna <vmik...@collabora.com> AuthorDate: Thu Jun 12 09:02:52 2025 +0200 Commit: Miklos Vajna <vmik...@collabora.com> CommitDate: Thu Jun 12 19:38:58 2025 +0200 tdf#166319 sw interdependent redlines: add UI to create format inside insert The bugdoc has <ins>AABBCC</ins> in it, selecting BB and pressing e.g. bold simply applied the formatting without creating a redline, even if the change is from another user. This was working for the case when a delete redline is created on top of an insert one, but was broken for insert-then-format: simply code was missing to try to handle the non-IsOwnRedline() case. Fix the problem by extracting this "split existing redline as necessary, then turn the overlapping range into a hierarchical redline" logic from sw::DocumentRedlineManager::PreAppendDeleteRedline() into a new PreAppendForeignRedline(), and next to the old insert-then-delete case, also use it for insert-then-format and delete-then-format. The new PreAppendForeignRedline() has a single part where it deals with moves for insert-then-delete, disable that for insert-then-format, since that's not relevant. Undo still needs to be fixed. Finally keep the logic unchanged the own delete/insert + own format redline case for now, we want different behavior for insert-then-delete and insert-then-format in this case, so intentionally not sharing that. Change-Id: Ifd2903f39d92a1bee96de0a38976eb5111c49223 Reviewed-on: https://gerrit.libreoffice.org/c/core/+/186423 Reviewed-by: Miklos Vajna <vmik...@collabora.com> Tested-by: Jenkins diff --git a/sw/CppunitTest_sw_core_doc.mk b/sw/CppunitTest_sw_core_doc.mk index ebd8db9d0781..9e12126c1c7a 100644 --- a/sw/CppunitTest_sw_core_doc.mk +++ b/sw/CppunitTest_sw_core_doc.mk @@ -15,6 +15,7 @@ $(eval $(call gb_CppunitTest_use_common_precompiled_header,sw_core_doc)) $(eval $(call gb_CppunitTest_add_exception_objects,sw_core_doc, \ sw/qa/core/doc/doc \ + sw/qa/core/doc/DocumentRedlineManager \ )) $(eval $(call gb_CppunitTest_use_libraries,sw_core_doc, \ diff --git a/sw/qa/core/doc/DocumentRedlineManager.cxx b/sw/qa/core/doc/DocumentRedlineManager.cxx new file mode 100644 index 000000000000..699b1f61570e --- /dev/null +++ b/sw/qa/core/doc/DocumentRedlineManager.cxx @@ -0,0 +1,72 @@ +/* -*- 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 <editeng/wghtitem.hxx> + +#include <IDocumentRedlineAccess.hxx> +#include <docsh.hxx> +#include <redline.hxx> +#include <view.hxx> +#include <wrtsh.hxx> + +namespace +{ +/// Covers sw/source/core/doc/DocumentRedlineManager.cxx fixes. +class Test : public SwModelTestBase +{ +public: + Test() + : SwModelTestBase(u"/sw/qa/core/doc/data/"_ustr) + { + } +}; + +CPPUNIT_TEST_FIXTURE(Test, testRedlineIns) +{ + // Given a document with an insert redline: + createSwDoc("ins.docx"); + + // When selecting BBB (a subset of the insert) and marking that as bold: + SwDocShell* pDocShell = getSwDocShell(); + SwWrtShell* pWrtShell = pDocShell->GetWrtShell(); + pWrtShell->SttEndDoc(/*bStt=*/true); + pWrtShell->Right(SwCursorSkipMode::Chars, /*bSelect=*/false, 3, /*bBasicCall=*/false); + pWrtShell->Right(SwCursorSkipMode::Chars, /*bSelect=*/true, 3, /*bBasicCall=*/false); + SwView& rView = pWrtShell->GetView(); + { + SvxWeightItem aWeightItem(WEIGHT_BOLD, RES_CHRATR_WEIGHT); + SfxItemSetFixed<RES_CHRATR_BEGIN, RES_CHRATR_END> aSet(rView.GetPool()); + aSet.Put(aWeightItem); + pWrtShell->SetAttrSet(aSet); + } + + // Then make sure BBB is covered by an insert-then-format redline: + SwDoc* pDoc = getSwDocShell()->GetDoc(); + IDocumentRedlineAccess& rIDRA = pDoc->getIDocumentRedlineAccess(); + SwRedlineTable& rRedlines = rIDRA.GetRedlineTable(); + { + // Without the accompanying fix in place, this test would have failed with: + // - Expected: 1 + // - Actual : 3 + // i.e. no redline was created for the format change. + CPPUNIT_ASSERT_EQUAL(static_cast<size_t>(3), rRedlines.size()); + CPPUNIT_ASSERT_EQUAL(RedlineType::Insert, rRedlines[0]->GetType()); + const SwRedlineData& rRedlineData1 = rRedlines[1]->GetRedlineData(0); + CPPUNIT_ASSERT_EQUAL(RedlineType::Format, rRedlineData1.GetType()); + CPPUNIT_ASSERT(rRedlineData1.Next()); + const SwRedlineData& rInnerRedlineData = *rRedlineData1.Next(); + CPPUNIT_ASSERT_EQUAL(RedlineType::Insert, rInnerRedlineData.GetType()); + CPPUNIT_ASSERT_EQUAL(RedlineType::Insert, rRedlines[2]->GetType()); + } +} +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sw/qa/core/doc/data/ins.docx b/sw/qa/core/doc/data/ins.docx new file mode 100644 index 000000000000..9460aa306716 Binary files /dev/null and b/sw/qa/core/doc/data/ins.docx differ diff --git a/sw/source/core/doc/DocumentRedlineManager.cxx b/sw/source/core/doc/DocumentRedlineManager.cxx index 54127a714826..95d472dfb341 100644 --- a/sw/source/core/doc/DocumentRedlineManager.cxx +++ b/sw/source/core/doc/DocumentRedlineManager.cxx @@ -1487,6 +1487,171 @@ public: std::set<sal_uInt32>& deletedMoveIDs; }; +void DocumentRedlineManager::PreAppendForeignRedline(AppendRedlineContext& rCtx) +{ + // it may be necessary to split the existing redline in + // two. In this case, pRedl will be changed to cover + // only part of its former range, and pNew will cover + // the remainder. + SwRangeRedline* pNew = nullptr; + + switch( rCtx.eCmpPos ) + { + case SwComparePosition::Equal: + { + rCtx.pRedl->PushData( *rCtx.pNewRedl ); + delete rCtx.pNewRedl; + rCtx.pNewRedl = nullptr; + if( IsHideChanges( GetRedlineFlags() )) + { + rCtx.pRedl->Hide(0, maRedlineTable.GetPos(rCtx.pRedl)); + } + rCtx.bCompress = true; + + if (rCtx.pNewRedl && rCtx.pNewRedl->GetType() == RedlineType::Delete) + { + // set IsMoved checking nearby redlines + SwRedlineTable::size_type nRIdx = maRedlineTable.GetPos(rCtx.pRedl); + if (nRIdx < maRedlineTable.size()) // in case above 're-insert' failed + maRedlineTable.isMoved(nRIdx); + } + + } + break; + + case SwComparePosition::Inside: + { + if( *rCtx.pRStt == *rCtx.pStart ) + { + // #i97421# + // redline w/out extent loops + if (*rCtx.pStart != *rCtx.pEnd) + { + rCtx.pNewRedl->PushData( *rCtx.pRedl, false ); + rCtx.pRedl->SetStart( *rCtx.pEnd, rCtx.pRStt ); + // re-insert + maRedlineTable.Remove( rCtx.n ); + maRedlineTable.Insert( rCtx.pRedl, rCtx.n ); + rCtx.bDec = true; + } + } + else + { + rCtx.pNewRedl->PushData( *rCtx.pRedl, false ); + if( *rCtx.pREnd != *rCtx.pEnd ) + { + pNew = new SwRangeRedline( *rCtx.pRedl ); + pNew->SetStart( *rCtx.pEnd ); + } + rCtx.pRedl->SetEnd( *rCtx.pStart, rCtx.pREnd ); + if( !rCtx.pRedl->HasValidRange() ) + { + // re-insert + maRedlineTable.Remove( rCtx.n ); + maRedlineTable.Insert( rCtx.pRedl, rCtx.n ); + } + } + } + break; + + case SwComparePosition::Outside: + { + rCtx.pRedl->PushData( *rCtx.pNewRedl ); + if( *rCtx.pEnd == *rCtx.pREnd ) + { + rCtx.pNewRedl->SetEnd( *rCtx.pRStt, rCtx.pEnd ); + } + else if (*rCtx.pStart == *rCtx.pRStt) + { + rCtx.pNewRedl->SetStart(*rCtx.pREnd, rCtx.pStart); + } + else + { + pNew = new SwRangeRedline( *rCtx.pNewRedl ); + pNew->SetEnd( *rCtx.pRStt ); + rCtx.pNewRedl->SetStart( *rCtx.pREnd, rCtx.pStart ); + } + rCtx.bCompress = true; + } + break; + + case SwComparePosition::OverlapBefore: + { + if( *rCtx.pEnd == *rCtx.pREnd ) + { + rCtx.pRedl->PushData( *rCtx.pNewRedl ); + rCtx.pNewRedl->SetEnd( *rCtx.pRStt, rCtx.pEnd ); + if( IsHideChanges( GetRedlineFlags() )) + { + maRedlineTable.Insert(rCtx.pNewRedl); + rCtx.pRedl->Hide(0, maRedlineTable.GetPos(rCtx.pRedl)); + maRedlineTable.Remove( rCtx.pNewRedl ); + } + } + else + { + pNew = new SwRangeRedline( *rCtx.pRedl ); + pNew->PushData( *rCtx.pNewRedl ); + pNew->SetEnd( *rCtx.pEnd ); + rCtx.pNewRedl->SetEnd( *rCtx.pRStt, rCtx.pEnd ); + rCtx.pRedl->SetStart( *pNew->End(), rCtx.pRStt ) ; + // re-insert + maRedlineTable.Remove( rCtx.n ); + maRedlineTable.Insert( rCtx.pRedl ); + rCtx.bDec = true; + } + } + break; + + case SwComparePosition::OverlapBehind: + { + if( *rCtx.pStart == *rCtx.pRStt ) + { + rCtx.pRedl->PushData( *rCtx.pNewRedl ); + rCtx.pNewRedl->SetStart( *rCtx.pREnd, rCtx.pStart ); + if( IsHideChanges( GetRedlineFlags() )) + { + maRedlineTable.Insert( rCtx.pNewRedl ); + rCtx.pRedl->Hide(0, maRedlineTable.GetPos(rCtx.pRedl)); + maRedlineTable.Remove( rCtx.pNewRedl ); + } + } + else + { + pNew = new SwRangeRedline( *rCtx.pRedl ); + pNew->PushData( *rCtx.pNewRedl ); + pNew->SetStart( *rCtx.pStart ); + rCtx.pNewRedl->SetStart( *rCtx.pREnd, rCtx.pStart ); + rCtx.pRedl->SetEnd( *pNew->Start(), rCtx.pREnd ); + if( !rCtx.pRedl->HasValidRange() ) + { + // re-insert + maRedlineTable.Remove( rCtx.n ); + maRedlineTable.Insert( rCtx.pRedl ); + } + } + } + break; + default: + break; + } + + // insert the pNew part (if it exists) + if( pNew ) + { + maRedlineTable.Insert( pNew ); + + // pNew must be deleted if Insert() wasn't + // successful. But that can't happen, since pNew is + // part of the original pRedl redline. + // OSL_ENSURE( bRet, "Can't insert existing redline?" ); + + // restart (now with pRedl being split up) + rCtx.n = 0; + rCtx.bDec = true; + } +} + void DocumentRedlineManager::PreAppendInsertRedline(AppendRedlineContext& rCtx) { switch( rCtx.pRedl->GetType() ) @@ -1993,164 +2158,7 @@ void DocumentRedlineManager::PreAppendDeleteRedline(AppendRedlineContext& rCtx) } else { - // it may be necessary to split the existing redline in - // two. In this case, pRedl will be changed to cover - // only part of its former range, and pNew will cover - // the remainder. - SwRangeRedline* pNew = nullptr; - - switch( rCtx.eCmpPos ) - { - case SwComparePosition::Equal: - { - rCtx.pRedl->PushData( *rCtx.pNewRedl ); - delete rCtx.pNewRedl; - rCtx.pNewRedl = nullptr; - if( IsHideChanges( GetRedlineFlags() )) - { - rCtx.pRedl->Hide(0, maRedlineTable.GetPos(rCtx.pRedl)); - } - rCtx.bCompress = true; - - // set IsMoved checking nearby redlines - SwRedlineTable::size_type nRIdx = maRedlineTable.GetPos(rCtx.pRedl); - if (nRIdx < maRedlineTable.size()) // in case above 're-insert' failed - maRedlineTable.isMoved(nRIdx); - - } - break; - - case SwComparePosition::Inside: - { - if( *rCtx.pRStt == *rCtx.pStart ) - { - // #i97421# - // redline w/out extent loops - if (*rCtx.pStart != *rCtx.pEnd) - { - rCtx.pNewRedl->PushData( *rCtx.pRedl, false ); - rCtx.pRedl->SetStart( *rCtx.pEnd, rCtx.pRStt ); - // re-insert - maRedlineTable.Remove( rCtx.n ); - maRedlineTable.Insert( rCtx.pRedl, rCtx.n ); - rCtx.bDec = true; - } - } - else - { - rCtx.pNewRedl->PushData( *rCtx.pRedl, false ); - if( *rCtx.pREnd != *rCtx.pEnd ) - { - pNew = new SwRangeRedline( *rCtx.pRedl ); - pNew->SetStart( *rCtx.pEnd ); - } - rCtx.pRedl->SetEnd( *rCtx.pStart, rCtx.pREnd ); - if( !rCtx.pRedl->HasValidRange() ) - { - // re-insert - maRedlineTable.Remove( rCtx.n ); - maRedlineTable.Insert( rCtx.pRedl, rCtx.n ); - } - } - } - break; - - case SwComparePosition::Outside: - { - rCtx.pRedl->PushData( *rCtx.pNewRedl ); - if( *rCtx.pEnd == *rCtx.pREnd ) - { - rCtx.pNewRedl->SetEnd( *rCtx.pRStt, rCtx.pEnd ); - } - else if (*rCtx.pStart == *rCtx.pRStt) - { - rCtx.pNewRedl->SetStart(*rCtx.pREnd, rCtx.pStart); - } - else - { - pNew = new SwRangeRedline( *rCtx.pNewRedl ); - pNew->SetEnd( *rCtx.pRStt ); - rCtx.pNewRedl->SetStart( *rCtx.pREnd, rCtx.pStart ); - } - rCtx.bCompress = true; - } - break; - - case SwComparePosition::OverlapBefore: - { - if( *rCtx.pEnd == *rCtx.pREnd ) - { - rCtx.pRedl->PushData( *rCtx.pNewRedl ); - rCtx.pNewRedl->SetEnd( *rCtx.pRStt, rCtx.pEnd ); - if( IsHideChanges( GetRedlineFlags() )) - { - maRedlineTable.Insert(rCtx.pNewRedl); - rCtx.pRedl->Hide(0, maRedlineTable.GetPos(rCtx.pRedl)); - maRedlineTable.Remove( rCtx.pNewRedl ); - } - } - else - { - pNew = new SwRangeRedline( *rCtx.pRedl ); - pNew->PushData( *rCtx.pNewRedl ); - pNew->SetEnd( *rCtx.pEnd ); - rCtx.pNewRedl->SetEnd( *rCtx.pRStt, rCtx.pEnd ); - rCtx.pRedl->SetStart( *pNew->End(), rCtx.pRStt ) ; - // re-insert - maRedlineTable.Remove( rCtx.n ); - maRedlineTable.Insert( rCtx.pRedl ); - rCtx.bDec = true; - } - } - break; - - case SwComparePosition::OverlapBehind: - { - if( *rCtx.pStart == *rCtx.pRStt ) - { - rCtx.pRedl->PushData( *rCtx.pNewRedl ); - rCtx.pNewRedl->SetStart( *rCtx.pREnd, rCtx.pStart ); - if( IsHideChanges( GetRedlineFlags() )) - { - maRedlineTable.Insert( rCtx.pNewRedl ); - rCtx.pRedl->Hide(0, maRedlineTable.GetPos(rCtx.pRedl)); - maRedlineTable.Remove( rCtx.pNewRedl ); - } - } - else - { - pNew = new SwRangeRedline( *rCtx.pRedl ); - pNew->PushData( *rCtx.pNewRedl ); - pNew->SetStart( *rCtx.pStart ); - rCtx.pNewRedl->SetStart( *rCtx.pREnd, rCtx.pStart ); - rCtx.pRedl->SetEnd( *pNew->Start(), rCtx.pREnd ); - if( !rCtx.pRedl->HasValidRange() ) - { - // re-insert - maRedlineTable.Remove( rCtx.n ); - maRedlineTable.Insert( rCtx.pRedl ); - } - } - } - break; - default: - break; - } - - // insert the pNew part (if it exists) - if( pNew ) - { - maRedlineTable.Insert( pNew ); - - // pNew must be deleted if Insert() wasn't - // successful. But that can't happen, since pNew is - // part of the original pRedl redline. - // OSL_ENSURE( bRet, "Can't insert existing redline?" ); - - // restart (now with pRedl being split up) - rCtx.n = 0; - rCtx.bDec = true; - } + PreAppendForeignRedline(rCtx); } } break; @@ -2214,61 +2222,74 @@ void DocumentRedlineManager::PreAppendFormatRedline(AppendRedlineContext& rCtx) { case RedlineType::Insert: case RedlineType::Delete: - switch( rCtx.eCmpPos ) + { + RedlineFlags eOld = GetRedlineFlags(); + bool bCombineRedlines = !(eOld & RedlineFlags::DontCombineRedlines) + && rCtx.pRedl->IsOwnRedline(*rCtx.pNewRedl) + && !rCtx.pRedl->GetRedlineData(0).IsAnonymized(); + if (bCombineRedlines || rCtx.pRedl->IsMoved()) { - case SwComparePosition::OverlapBefore: - rCtx.pNewRedl->SetEnd( *rCtx.pRStt, rCtx.pEnd ); - break; - - case SwComparePosition::OverlapBehind: - rCtx.pNewRedl->SetStart( *rCtx.pREnd, rCtx.pStart ); - break; + switch( rCtx.eCmpPos ) + { + case SwComparePosition::OverlapBefore: + rCtx.pNewRedl->SetEnd( *rCtx.pRStt, rCtx.pEnd ); + break; - case SwComparePosition::Inside: - if (*rCtx.pRStt < *rCtx.pStart && *rCtx.pREnd == *rCtx.pEnd) - { - // pRedl start is before pNewRedl start, the ends match: then create the - // format on top of insert/delete & reduce the end of the original - // insert/delete to avoid an overlap. - rCtx.pNewRedl->PushData(*rCtx.pRedl, false); - rCtx.pRedl->SetEnd(*rCtx.pStart); - rCtx.n = 0; - rCtx.bDec = true; + case SwComparePosition::OverlapBehind: + rCtx.pNewRedl->SetStart( *rCtx.pREnd, rCtx.pStart ); break; - } - [[fallthrough]]; - case SwComparePosition::Equal: - delete rCtx.pNewRedl; - rCtx.pNewRedl = nullptr; - MaybeNotifyRedlineModification(*rCtx.pRedl, m_rDoc); - break; + case SwComparePosition::Inside: + if (*rCtx.pRStt < *rCtx.pStart && *rCtx.pREnd == *rCtx.pEnd) + { + // pRedl start is before pNewRedl start, the ends match: then create the + // format on top of insert/delete & reduce the end of the original + // insert/delete to avoid an overlap. + rCtx.pNewRedl->PushData(*rCtx.pRedl, false); + rCtx.pRedl->SetEnd(*rCtx.pStart); + rCtx.n = 0; + rCtx.bDec = true; + break; + } + [[fallthrough]]; + case SwComparePosition::Equal: + delete rCtx.pNewRedl; + rCtx.pNewRedl = nullptr; - case SwComparePosition::Outside: - // Overlaps the current one completely, - // split or shorten the new one - if (*rCtx.pEnd == *rCtx.pREnd) - { - rCtx.pNewRedl->SetEnd(*rCtx.pRStt, rCtx.pEnd); - } - else if (*rCtx.pStart == *rCtx.pRStt) - { - rCtx.pNewRedl->SetStart(*rCtx.pREnd, rCtx.pStart); - } - else - { - SwRangeRedline* pNew = new SwRangeRedline( *rCtx.pNewRedl ); - pNew->SetStart( *rCtx.pREnd ); - rCtx.pNewRedl->SetEnd( *rCtx.pRStt, rCtx.pEnd ); - AppendRedline( pNew, rCtx.bCallDelete ); - rCtx.n = 0; // re-initialize - rCtx.bDec = true; + MaybeNotifyRedlineModification(*rCtx.pRedl, m_rDoc); + break; + + case SwComparePosition::Outside: + // Overlaps the current one completely, + // split or shorten the new one + if (*rCtx.pEnd == *rCtx.pREnd) + { + rCtx.pNewRedl->SetEnd(*rCtx.pRStt, rCtx.pEnd); + } + else if (*rCtx.pStart == *rCtx.pRStt) + { + rCtx.pNewRedl->SetStart(*rCtx.pREnd, rCtx.pStart); + } + else + { + SwRangeRedline* pNew = new SwRangeRedline( *rCtx.pNewRedl ); + pNew->SetStart( *rCtx.pREnd ); + rCtx.pNewRedl->SetEnd( *rCtx.pRStt, rCtx.pEnd ); + AppendRedline( pNew, rCtx.bCallDelete ); + rCtx.n = 0; // re-initialize + rCtx.bDec = true; + } + break; + default: + break; } - break; - default: - break; + } + else + { + PreAppendForeignRedline(rCtx); } break; + } case RedlineType::Format: switch( rCtx.eCmpPos ) { diff --git a/sw/source/core/inc/DocumentRedlineManager.hxx b/sw/source/core/inc/DocumentRedlineManager.hxx index 817861b0fbbd..bc4187406dc0 100644 --- a/sw/source/core/inc/DocumentRedlineManager.hxx +++ b/sw/source/core/inc/DocumentRedlineManager.hxx @@ -168,6 +168,8 @@ private: void PreAppendInsertRedline(AppendRedlineContext& rCtx); void PreAppendDeleteRedline(AppendRedlineContext& rCtx); void PreAppendFormatRedline(AppendRedlineContext& rCtx); + /// Append a next redline partially on top of another existing redline. + void PreAppendForeignRedline(AppendRedlineContext& rCtx); DocumentRedlineManager(DocumentRedlineManager const&) = delete; DocumentRedlineManager& operator=(DocumentRedlineManager const&) = delete;