engine/desktop/source/lib/init.cxx | 28 +++++
engine/sd/qa/unit/SdrPdfImportTest.cxx | 30 +++++
engine/sd/source/ui/annotations/annotationmanager.cxx | 76 ++++++++------
engine/sd/source/ui/annotations/annotationmanagerimpl.hxx | 10 +
engine/svx/sdi/svx.sdi | 4
5 files changed, 116 insertions(+), 32 deletions(-)
New commits:
commit b7b6ebe8d90172e54cc4615e6120556e6292579d
Author: Mike Kaganski <
mike.k...@collabora.com>
AuthorDate: Tue Apr 28 17:52:56 2026 +0500
Commit: Miklos Vajna <
vmi...@collabora.com>
CommitDate: Wed Apr 29 08:29:13 2026 +0000
sd: implement placing comments in specified position
Extend the .uno:InsertAnnotation slot with optional PositionX/PositionY
SfxInt32Item args (mm100s). When both are set, place the new annotation
at that point instead of running the free-space scan starting at (0, 0).
This is the core piece of the PDF "click-to-place" comment flow in
Online: the user clicks anywhere on the page to mark the spot, the
client dispatches the slot with PositionX/Y. Without it, every new
comment landed at the top-left of the page.
The data arriving from Online is in twips; handle the conversion in
doc_postUnoCommand, similar to .uno:TransformDialog.
Signed-off-by: Mike Kaganski <
mike.k...@collabora.com>
Change-Id: I07cd212eef0f5f79d05dd7be5f7e8c1bcaa5f857
Reviewed-on:
https://gerrit.collaboraoffice.com/c/online/+/1776
Tested-by: Jenkins CPCI <
rel...@collaboraoffice.com>
Reviewed-by: Miklos Vajna <
vmi...@collabora.com>
diff --git a/engine/desktop/source/lib/init.cxx b/engine/desktop/source/lib/init.cxx
index b5bc8ba94192..9ced382fa171 100644
--- a/engine/desktop/source/lib/init.cxx
+++ b/engine/desktop/source/lib/init.cxx
@@ -5641,6 +5641,34 @@ static void doc_postUnoCommand(COKitDocument* pThis, const char* pCommand, const
return;
}
}
+ else if (gImpl && aCommand == ".uno:InsertAnnotation")
+ {
+ // Online sends PositionX/PositionY in twips. The slot's native unit
+ // matches the doc's map mode; convert twip -> mm/100 for mm/100 docs
+ // (Draw/Impress) and leave the values as-is otherwise.
+ bool bNeedConversion = false;
+ if (const SdrView* pView = pViewShell->GetDrawView())
+ {
+ if (OutputDevice* pOutputDevice = pView->GetFirstOutputDevice())
+ {
+ bNeedConversion = (pOutputDevice->GetMapMode().GetMapUnit() == MapUnit::Map100thMM);
+ }
+ }
+
+ if (bNeedConversion)
+ {
+ sal_Int32 value;
+ for (beans::PropertyValue& rPropValue: aPropertyValuesVector)
+ {
+ if (rPropValue.Name == "PositionX" || rPropValue.Name == "PositionY")
+ {
+ rPropValue.Value >>= value;
+ value = o3tl::convert(value, o3tl::Length::twip, o3tl::Length::mm100);
+ rPropValue.Value <<= value;
+ }
+ }
+ }
+ }
else if (gImpl && aCommand == ".uno:KitSidebarWriterPage")
{
if (!sfx2::sidebar::Sidebar::Setup(u"WriterPageDeck"))
diff --git a/engine/sd/qa/unit/SdrPdfImportTest.cxx b/engine/sd/qa/unit/SdrPdfImportTest.cxx
index b13a6aad441e..b4312b3b6caf 100644
--- a/engine/sd/qa/unit/SdrPdfImportTest.cxx
+++ b/engine/sd/qa/unit/SdrPdfImportTest.cxx
@@ -447,6 +447,36 @@ CPPUNIT_TEST_FIXTURE(SdrPdfImportTest, testImportThreadedComments)
CPPUNIT_ASSERT(bDaveResolved);
}
+CPPUNIT_TEST_FIXTURE(SdrPdfImportTest, testInsertAnnotationAtPosition)
+{
+ auto pPdfium = vcl::pdf::PDFiumLibrary::get();
+ if (!pPdfium)
+ return;
+ EnvVarGuard UsePDFiumGuard("LO_IMPORT_USE_PDFIUM", "1");
+
+ loadFromFile(u"pdf/threaded_comments.pdf");
+ auto pImpressDocument = dynamic_cast<SdXImpressDocument*>(mxComponent.get());
+ sd::ViewShell* pViewShell = pImpressDocument->GetDocShell()->GetViewShell();
+ SdPage* pPage = pViewShell->GetActualPage();
+ CPPUNIT_ASSERT(pPage);
+
+ const size_t nBefore = pPage->getAnnotations().size();
+
+ // PositionX/PositionY are in mm/100. 2540 mm/100 = 1 inch; 5080 mm/100 = 2 inches.
+ uno::Sequence<beans::PropertyValue> aArgs(comphelper::InitPropertySequence({
+ { "Text", uno::Any(u"Click-to-place"_ustr) },
+ { "PositionX", uno::Any(sal_Int32(2540)) },
+ { "PositionY", uno::Any(sal_Int32(5080)) },
+ }));
+ dispatchCommand(mxComponent, u".uno:InsertAnnotation"_ustr, aArgs);
+
+ CPPUNIT_ASSERT_EQUAL(nBefore + 1, pPage->getAnnotations().size());
+ const auto& xNew = pPage->getAnnotations().back();
+ CPPUNIT_ASSERT_EQUAL(u"Click-to-place"_ustr, xNew->getTextRange()->getString());
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(25.4, xNew->getPosition().X, 0.01);
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(50.8, xNew->getPosition().Y, 0.01);
+}
+
CPPUNIT_PLUGIN_IMPLEMENT();
/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/engine/sd/source/ui/annotations/annotationmanager.cxx b/engine/sd/source/ui/annotations/annotationmanager.cxx
index b428652289f5..27f8fd66629f 100644
--- a/engine/sd/source/ui/annotations/annotationmanager.cxx
+++ b/engine/sd/source/ui/annotations/annotationmanager.cxx
@@ -378,15 +378,21 @@ void AnnotationManagerImpl::ExecuteInsertAnnotation(SfxRequest const & rReq)
const SfxItemSet* pArgs = rReq.GetArgs();
OUString sText;
+ std::optional<Point> oPosition;
if (pArgs)
{
if (const SfxStringItem* pPoolItem = pArgs->GetItemIfSet(SID_ATTR_POSTIT_TEXT))
{
sText = pPoolItem->GetValue();
}
+
+ const SfxInt32Item* pX = pArgs->GetItemIfSet(SID_ATTR_POSTIT_POSITION_X);
+ const SfxInt32Item* pY = pArgs->GetItemIfSet(SID_ATTR_POSTIT_POSITION_Y);
+ if (pX && pY && pX->GetValue() >= 0 && pY->GetValue() >= 0)
+ oPosition.emplace(pX->GetValue(), pY->GetValue());
}
- InsertAnnotation(sText);
+ InsertAnnotation(sText, oPosition);
}
void AnnotationManagerImpl::ExecuteDeleteAnnotation(SfxRequest const & rReq)
@@ -503,7 +509,8 @@ void AnnotationManagerImpl::ExecuteEditAnnotation(SfxRequest const & rReq)
UpdateTags(true);
}
-void AnnotationManagerImpl::InsertAnnotation(const OUString& rText)
+void AnnotationManagerImpl::InsertAnnotation(const OUString& rText,
+ std::optional<Point> oPosition)
{
SdPage* pPage = GetCurrentPage();
if (!pPage)
@@ -512,47 +519,56 @@ void AnnotationManagerImpl::InsertAnnotation(const OUString& rText)
if (mpDoc->IsUndoEnabled())
mpDoc->BegUndo(SdResId(STR_ANNOTATION_UNDO_INSERT));
- // find free space for new annotation
int y = 0;
int x = 0;
- sdr::annotation::AnnotationVector aAnnotations(pPage->getAnnotations());
- if (!aAnnotations.empty())
+ if (oPosition)
{
- const int fPageWidth = pPage->GetSize().Width();
- const int fWidth = 1000;
- const int fHeight = 800;
-
- while (true)
+ // Caller-supplied position.
+ x = oPosition->X();
+ y = oPosition->Y();
+ }
+ else
+ {
+ // For everything else, scan the page for the first free spot starting at (0, 0).
+ sdr::annotation::AnnotationVector aAnnotations(pPage->getAnnotations());
+ if (!aAnnotations.empty())
{
- ::tools::Rectangle aNewRect(Point(x, y), Size(fWidth, fHeight));
- bool bFree = true;
+ const int fPageWidth = pPage->GetSize().Width();
+ const int fWidth = 1000;
+ const int fHeight = 800;
- for (const auto& rxAnnotation : aAnnotations)
+ while (true)
{
- geometry::RealPoint2D aRealPoint2D(rxAnnotation->getPosition());
- Point aPoint(::tools::Long(aRealPoint2D.X * 100.0), ::tools::Long(aRealPoint2D.Y * 100.0));
- Size aSize(fWidth, fHeight);
+ ::tools::Rectangle aNewRect(Point(x, y), Size(fWidth, fHeight));
+ bool bFree = true;
- if (aNewRect.Overlaps(::tools::Rectangle(aPoint, aSize)))
+ for (const auto& rxAnnotation : aAnnotations)
{
- bFree = false;
- break;
+ geometry::RealPoint2D aRealPoint2D(rxAnnotation->getPosition());
+ Point aPoint(::tools::Long(aRealPoint2D.X * 100.0), ::tools::Long(aRealPoint2D.Y * 100.0));
+ Size aSize(fWidth, fHeight);
+
+ if (aNewRect.Overlaps(::tools::Rectangle(aPoint, aSize)))
+ {
+ bFree = false;
+ break;
+ }
}
- }
- if (!bFree)
- {
- x += fWidth;
- if (x > fPageWidth)
+ if (!bFree)
{
- x = 0;
- y += fHeight;
+ x += fWidth;
+ if (x > fPageWidth)
+ {
+ x = 0;
+ y += fHeight;
+ }
+ }
+ else
+ {
+ break;
}
- }
- else
- {
- break;
}
}
}
diff --git a/engine/sd/source/ui/annotations/annotationmanagerimpl.hxx b/engine/sd/source/ui/annotations/annotationmanagerimpl.hxx
index 7eaac01d4d55..44acaf67f5d8 100644
--- a/engine/sd/source/ui/annotations/annotationmanagerimpl.hxx
+++ b/engine/sd/source/ui/annotations/annotationmanagerimpl.hxx
@@ -22,9 +22,12 @@
#include <com/sun/star/document/XEventListener.hpp>
#include <rtl/ustring.hxx>
+#include <tools/gen.hxx>
#include <comphelper/compbase.hxx>
+#include <optional>
+
namespace com::sun::star::drawing { class XDrawView; }
namespace com::sun::star::office { class XAnnotationAccess; }
namespace com::sun::star::office { class XAnnotation; }
@@ -77,7 +80,12 @@ public:
void SelectAnnotation(rtl::Reference<sdr::annotation::Annotation> const& xAnnotation, bool bEdit = false);
void GetSelectedAnnotation(rtl::Reference<sdr::annotation::Annotation>& xAnnotation);
- void InsertAnnotation(const OUString& rText);
+ /**
+ * Insert a new annotation on the current page. When oPosition holds a
+ * value, the annotation is placed at that point (mm/100, top-left origin);
+ * otherwise the next free spot is found by scanning the page from (0, 0).
+ */
+ void InsertAnnotation(const OUString& rText, std::optional<Point> oPosition = {});
void DeleteAnnotation(rtl::Reference<sdr::annotation::Annotation> const& xAnnotation);
void DeleteAnnotationsByAuthor( std::u16string_view sAuthor );
void DeleteAllAnnotations();
diff --git a/engine/svx/sdi/svx.sdi b/engine/svx/sdi/svx.sdi
index 7e0d86ae1be1..9223c7924b61 100644
--- a/engine/svx/sdi/svx.sdi
+++ b/engine/svx/sdi/svx.sdi
@@ -4723,7 +4723,9 @@ SfxVoidItem InsertAnnotation SID_INSERT_POSTIT
(SvxPostItAuthorItem Author SID_ATTR_POSTIT_AUTHOR,
SvxPostItDateItem Date SID_ATTR_POSTIT_DATE,
SvxPostItTextItem Text SID_ATTR_POSTIT_TEXT,
- SvxPostItTextItem Html SID_ATTR_POSTIT_HTML)
+ SvxPostItTextItem Html SID_ATTR_POSTIT_HTML,
+ SfxInt32Item PositionX SID_ATTR_POSTIT_POSITION_X,
+ SfxInt32Item PositionY SID_ATTR_POSTIT_POSITION_Y)
[
AutoUpdate = FALSE,
FastCall = FALSE,