sc: add threaded comment data model for Calc annotations

Add ScThreadedCommentData, ScThreadedCommentEntry, and ScPersonData
structs to support the Excel "threaded comments" feature ([MS-XLSX]
sections 2.3.7, 2.6.205). ScPostIt gains an optional
mpThreadedCommentData member with IsResolved()/SetResolved() accessors
for the "done" flag, and ScDocument gains a person list (AddPerson,
GetPersonById). Clone() preserves threaded data in both code paths.

This is the document model foundation for comment resolution in Calc;
import/export/LOK/Online follow separately.

Signed-off-by: Mike Kaganski <mike.kaganski@collabora.com>
Change-Id: Ic782f9126c38088292b068066ba0de3889563531
Reviewed-on: https://gerrit.collaboraoffice.com/c/core/+/800
Reviewed-by: Miklos Vajna <vmiklos@collabora.com>
Tested-by: Jenkins CPCI <releng@collaboraoffice.com>
(cherry picked from commit 3f8cd9c6ab6738a434788cb9280373413a0b7205)
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/205293
Reviewed-by: Xisco Fauli <xiscofauli@libreoffice.org>
Tested-by: Xisco Fauli <xiscofauli@libreoffice.org>
This commit is contained in:
Mike Kaganski
2026-05-19 09:27:39 +02:00
committed by Xisco Fauli
parent e036e16a44
commit 5ab6a58968
5 changed files with 193 additions and 1 deletions
+8
View File
@@ -205,6 +205,7 @@ class ScEditDataArray;
class EditTextObject;
struct ScRefCellValue;
class ScPostIt;
struct ScPersonData;
struct ScSubTotalParam;
struct ScQueryParam;
class ScHint;
@@ -476,6 +477,7 @@ private:
std::unique_ptr<ScConditionMode> pConditionalFormatDialogMode;
std::unique_ptr<ScAutoNameCache> pAutoNameCache; // for automatic name lookup during CompileXML
std::vector<ScPersonData> maPersonList; // persons for threaded comments
std::unique_ptr<SfxItemSet> pPreviewFont; // convert to std::unique_ptr or whatever
ScStyleSheet* pPreviewCellStyle;
@@ -1406,6 +1408,12 @@ public:
bool ContainsNotesInRange( const ScRangeList& rRange ) const;
// Person list (for threaded comments, [MS-XLSX] section 2.1.19)
SC_DLLPUBLIC void AddPerson(const ScPersonData& rPerson);
SC_DLLPUBLIC const ScPersonData* GetPersonById(const OUString& rId) const;
const std::vector<ScPersonData>& GetPersonList() const { return maPersonList; }
std::vector<ScPersonData>& GetPersonList() { return maPersonList; }
SC_DLLPUBLIC void SetDrawPageSize(SCTAB nTab);
bool IsMerged( const ScAddress& rPos ) const;
+41
View File
@@ -27,6 +27,7 @@
#include <memory>
#include <string_view>
#include <vector>
class EditTextObject;
class OutlinerParaObject;
@@ -37,6 +38,35 @@ class ScDocument;
namespace tools { class Rectangle; }
struct ScCaptionInitData;
/** A single entry in a threaded comment (root or reply).
Maps to CT_ThreadedComment in [MS-XLSX] section 2.6.205. */
struct ScThreadedCommentEntry
{
OUString maGuid; /// Unique ID (ST_Guid).
OUString maPersonId; /// Reference to person in ScDocument person list.
OUString maDateTime; /// ISO 8601 / xsd:dateTime (UTC).
OUString maText; /// Text of this entry.
};
/** Threaded-comment metadata attached to a ScPostIt.
When present the note participates in the Excel "threaded comments" feature. */
struct ScThreadedCommentData
{
ScThreadedCommentEntry maRoot; /// Root entry (guid, personId, dateTime).
bool mbDone = false; /// Resolved / done flag.
std::vector<ScThreadedCommentEntry> maReplies; /// Reply chain.
};
/** Person metadata for threaded comments.
Maps to CT_Person in [MS-XLSX] section 2.6.203. */
struct ScPersonData
{
OUString maId; /// Unique person GUID.
OUString maDisplayName; /// Display name.
OUString maUserId; /// Provider-specific user ID.
OUString maProviderId; /// Identity provider ID.
};
/** Internal data for a cell annotation. */
struct ScNoteData
{
@@ -157,6 +187,16 @@ public:
static OString NoteRangeToJsonString(const ScDocument& rDoc, const ScAddress& rPos);
/** Returns true if this note has been resolved (threaded comment "done" flag). */
bool IsResolved() const { return mpThreadedCommentData && mpThreadedCommentData->mbDone; }
/** Sets the resolved state. Creates threaded comment data if not yet present. */
void SetResolved(bool bResolved);
/** Returns the threaded comment data, or nullptr if this is a classic note. */
const ScThreadedCommentData* GetThreadedCommentData() const { return mpThreadedCommentData.get(); }
/** Sets (replaces) the threaded comment data. Pass nullptr to remove. */
SC_DLLPUBLIC void SetThreadedCommentData(std::unique_ptr<ScThreadedCommentData> pData);
private:
ScPostIt( const ScPostIt& ) = delete;
ScPostIt& operator=( const ScPostIt& ) = delete;
@@ -172,6 +212,7 @@ private:
ScDocument& mrDoc; /// Parent document containing the note.
mutable ScNoteData maNoteData; /// Note data with pointer to caption object.
sal_uInt32 mnPostItId;
std::unique_ptr<ScThreadedCommentData> mpThreadedCommentData; /// Threaded comment metadata (nullable).
};
class GenerateNoteCaption
+106
View File
@@ -42,6 +42,7 @@
#include <columnspanset.hxx>
#include <editable.hxx>
#include <postit.hxx>
#include <tabprotection.hxx>
#include <undomanager.hxx>
@@ -5169,6 +5170,111 @@ CPPUNIT_TEST_FIXTURE(Test, testNoteDefaultStyle)
m_pDoc->DeleteTab(0);
}
CPPUNIT_TEST_FIXTURE(Test, testNoteThreadedCommentData)
{
m_pDoc->InsertTab(0, u"PostIts"_ustr);
m_pDoc->InitDrawLayer(m_xDocShell.get());
ScAddress aPos(0, 0, 0);
ScPostIt* pNote = m_pDoc->GetOrCreateNote(aPos);
pNote->SetText(aPos, u"Hello"_ustr);
pNote->SetAuthor(u"Author"_ustr);
// Initially not resolved (no threaded data).
CPPUNIT_ASSERT(!pNote->IsResolved());
CPPUNIT_ASSERT(!pNote->GetThreadedCommentData());
// SetResolved creates threaded data on demand.
pNote->SetResolved(true);
CPPUNIT_ASSERT(pNote->IsResolved());
CPPUNIT_ASSERT(pNote->GetThreadedCommentData());
CPPUNIT_ASSERT(pNote->GetThreadedCommentData()->mbDone);
// SetResolved(false) clears the flag but keeps the data.
pNote->SetResolved(false);
CPPUNIT_ASSERT(!pNote->IsResolved());
CPPUNIT_ASSERT(pNote->GetThreadedCommentData());
CPPUNIT_ASSERT(!pNote->GetThreadedCommentData()->mbDone);
// Set full threaded comment data.
auto pData = std::make_unique<ScThreadedCommentData>();
pData->maRoot.maGuid = u"{11111111-1111-1111-1111-111111111111}"_ustr;
pData->maRoot.maPersonId = u"{AAAA}"_ustr;
pData->maRoot.maDateTime = u"2026-04-07T12:00:00Z"_ustr;
pData->mbDone = true;
ScThreadedCommentEntry aReply;
aReply.maGuid = u"{22222222-2222-2222-2222-222222222222}"_ustr;
aReply.maPersonId = u"{BBBB}"_ustr;
aReply.maText = u"Reply text"_ustr;
pData->maReplies.push_back(aReply);
pNote->SetThreadedCommentData(std::move(pData));
CPPUNIT_ASSERT(pNote->IsResolved());
CPPUNIT_ASSERT_EQUAL(size_t(1), pNote->GetThreadedCommentData()->maReplies.size());
CPPUNIT_ASSERT_EQUAL(u"Reply text"_ustr, pNote->GetThreadedCommentData()->maReplies[0].maText);
// Clone preserves threaded data.
std::unique_ptr<ScPostIt> pClone = pNote->Clone(aPos, *m_pDoc, ScAddress(1, 0, 0), true);
CPPUNIT_ASSERT(pClone->IsResolved());
CPPUNIT_ASSERT(pClone->GetThreadedCommentData());
CPPUNIT_ASSERT_EQUAL(u"{11111111-1111-1111-1111-111111111111}"_ustr,
pClone->GetThreadedCommentData()->maRoot.maGuid);
CPPUNIT_ASSERT_EQUAL(size_t(1), pClone->GetThreadedCommentData()->maReplies.size());
// Clone without caption also preserves threaded data.
std::unique_ptr<ScPostIt> pClone2 = pNote->Clone(aPos, *m_pDoc, ScAddress(2, 0, 0), false);
CPPUNIT_ASSERT(pClone2->IsResolved());
CPPUNIT_ASSERT_EQUAL(u"{11111111-1111-1111-1111-111111111111}"_ustr,
pClone2->GetThreadedCommentData()->maRoot.maGuid);
// Remove threaded data.
pNote->SetThreadedCommentData(nullptr);
CPPUNIT_ASSERT(!pNote->IsResolved());
CPPUNIT_ASSERT(!pNote->GetThreadedCommentData());
m_pDoc->DeleteTab(0);
}
CPPUNIT_TEST_FIXTURE(Test, testNotePersonList)
{
m_pDoc->InsertTab(0, u"PostIts"_ustr);
// Initially empty.
CPPUNIT_ASSERT(m_pDoc->GetPersonList().empty());
ScPersonData aPerson1;
aPerson1.maId = u"{AAAA}"_ustr;
aPerson1.maDisplayName = u"Alice"_ustr;
m_pDoc->AddPerson(aPerson1);
CPPUNIT_ASSERT_EQUAL(size_t(1), m_pDoc->GetPersonList().size());
// Duplicate by ID is ignored.
ScPersonData aPerson1Dup;
aPerson1Dup.maId = u"{AAAA}"_ustr;
aPerson1Dup.maDisplayName = u"Alice duplicate"_ustr;
m_pDoc->AddPerson(aPerson1Dup);
CPPUNIT_ASSERT_EQUAL(size_t(1), m_pDoc->GetPersonList().size());
CPPUNIT_ASSERT_EQUAL(u"Alice"_ustr, m_pDoc->GetPersonList()[0].maDisplayName);
// Different ID is added.
ScPersonData aPerson2;
aPerson2.maId = u"{BBBB}"_ustr;
aPerson2.maDisplayName = u"Bob"_ustr;
m_pDoc->AddPerson(aPerson2);
CPPUNIT_ASSERT_EQUAL(size_t(2), m_pDoc->GetPersonList().size());
// Lookup by ID.
const ScPersonData* pFound = m_pDoc->GetPersonById(u"{BBBB}"_ustr);
CPPUNIT_ASSERT(pFound);
CPPUNIT_ASSERT_EQUAL(u"Bob"_ustr, pFound->maDisplayName);
// Unknown ID returns nullptr.
CPPUNIT_ASSERT(!m_pDoc->GetPersonById(u"{CCCC}"_ustr));
m_pDoc->DeleteTab(0);
}
CPPUNIT_TEST_FIXTURE(Test, testNoteBasic)
{
m_pDoc->InsertTab(0, u"PostIts"_ustr);
+14
View File
@@ -6970,6 +6970,20 @@ bool ScDocument::ContainsNotesInRange( const ScRangeList& rRangeList ) const
return false;
}
void ScDocument::AddPerson(const ScPersonData& rPerson)
{
if (std::none_of(maPersonList.begin(), maPersonList.end(),
[&rPerson](const auto& r) { return r.maId == rPerson.maId; }))
maPersonList.push_back(rPerson);
}
const ScPersonData* ScDocument::GetPersonById(const OUString& rId) const
{
auto it = std::find_if(maPersonList.begin(), maPersonList.end(),
[&rId](const auto& r) { return r.maId == rId; });
return it != maPersonList.end() ? &*it : nullptr;
}
void ScDocument::SetAutoNameCache( std::unique_ptr<ScAutoNameCache> pCache )
{
pAutoNameCache = std::move(pCache);
+24 -1
View File
@@ -451,6 +451,8 @@ ScPostIt::ScPostIt( ScDocument& rDoc, const ScAddress& rPos, const ScPostIt& rNo
maNoteData( rNote.maNoteData )
{
mnPostItId = nPostItId == 0 ? mnLastPostItId++ : nPostItId;
if (rNote.mpThreadedCommentData)
mpThreadedCommentData = std::make_unique<ScThreadedCommentData>(*rNote.mpThreadedCommentData);
maNoteData.mxCaption.clear();
CreateCaption( rPos, rNote.maNoteData.mxCaption.get() );
}
@@ -477,7 +479,16 @@ std::unique_ptr<ScPostIt> ScPostIt::Clone( const ScAddress& rOwnPos, ScDocument&
bCloneCaption = false;
CreateCaptionFromInitData( rOwnPos );
sal_uInt32 nPostItId = comphelper::LibreOfficeKit::isActive() ? 0 : mnPostItId;
return bCloneCaption ? std::make_unique<ScPostIt>( rDestDoc, rDestPos, *this, nPostItId ) : std::make_unique<ScPostIt>( rDestDoc, rDestPos, maNoteData, false, mnPostItId );
std::unique_ptr<ScPostIt> pClone;
if (bCloneCaption)
pClone = std::make_unique<ScPostIt>(rDestDoc, rDestPos, *this, nPostItId);
else
{
pClone = std::make_unique<ScPostIt>(rDestDoc, rDestPos, maNoteData, false, mnPostItId);
if (mpThreadedCommentData)
pClone->mpThreadedCommentData = std::make_unique<ScThreadedCommentData>(*mpThreadedCommentData);
}
return pClone;
}
void ScPostIt::SetDate( const OUString& rDate )
@@ -490,6 +501,18 @@ void ScPostIt::SetAuthor( const OUString& rAuthor )
maNoteData.maAuthor = rAuthor;
}
void ScPostIt::SetResolved(bool bResolved)
{
if (!mpThreadedCommentData)
mpThreadedCommentData = std::make_unique<ScThreadedCommentData>();
mpThreadedCommentData->mbDone = bResolved;
}
void ScPostIt::SetThreadedCommentData(std::unique_ptr<ScThreadedCommentData> pData)
{
mpThreadedCommentData = std::move(pData);
}
void ScPostIt::AutoStamp(bool bCreate)
{
if (bCreate)