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:
committed by
Xisco Fauli
parent
e036e16a44
commit
5ab6a58968
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user