diff --git a/include/oox/export/shapes.hxx b/include/oox/export/shapes.hxx
index 05b0c730e624..f0e359727b60 100644
--- a/include/oox/export/shapes.hxx
+++ b/include/oox/export/shapes.hxx
@@ -178,7 +178,9 @@ public:
* @return *this
*/
ShapeExport& WriteShape( const css::uno::Reference< css::drawing::XShape >& xShape );
- ShapeExport& WriteTextBox( const css::uno::Reference< css::uno::XInterface >& xIface, sal_Int32 nXmlNamespace, bool bWritePropertiesAsLstStyles = false );
+ /** @param bText false writes the body properties and an empty paragraph, for a shape whose
+ text belongs to the application rather than to the file - a placeholder's prompt. */
+ ShapeExport& WriteTextBox( const css::uno::Reference< css::uno::XInterface >& xIface, sal_Int32 nXmlNamespace, bool bWritePropertiesAsLstStyles = false, bool bText = true );
virtual ShapeExport&
WriteTextShape( const css::uno::Reference< css::drawing::XShape >& xShape );
ShapeExport&
diff --git a/oox/source/drawingml/shape.cxx b/oox/source/drawingml/shape.cxx
index b8c63a1f0e9d..8d57c87fdc18 100644
--- a/oox/source/drawingml/shape.cxx
+++ b/oox/source/drawingml/shape.cxx
@@ -2225,8 +2225,8 @@ Reference< XShape > const & Shape::createAndInsert(
// Make sure to not set text to placeholders. Doing it here would eventually call
// SvxTextEditSourceImpl::UpdateData, SdrObject::SetEmptyPresObj(false), and that
- // would make the object behave like a standard outline object.
- // TODO/FIXME: support custom prompt text in placeholders.
+ // would make the object behave like a standard outline object. An authored prompt
+ // reaches the object through the CustomPromptText property instead.
if (rServiceName == "com.sun.star.presentation.GraphicObjectShape")
mpTextBody.reset();
diff --git a/oox/source/export/shapes.cxx b/oox/source/export/shapes.cxx
index 272eed581faf..a39cf8d627cd 100644
--- a/oox/source/export/shapes.cxx
+++ b/oox/source/export/shapes.cxx
@@ -2333,7 +2333,7 @@ static bool lcl_isTextBox(const Reference& xIface)
return aTextBox.get();
}
-ShapeExport& ShapeExport::WriteTextBox( const Reference< XInterface >& xIface, sal_Int32 nXmlNamespace, bool bWritePropertiesAsLstStyles )
+ShapeExport& ShapeExport::WriteTextBox( const Reference< XInterface >& xIface, sal_Int32 nXmlNamespace, bool bWritePropertiesAsLstStyles, bool bText )
{
// In case this shape has an associated textbox, then export that, and we're done.
if (GetDocumentType() == DOCUMENT_DOCX && !mbUserShapes && GetTextExport())
@@ -2354,8 +2354,11 @@ ShapeExport& ShapeExport::WriteTextBox( const Reference< XInterface >& xIface, s
pFS->startElementNS(nXmlNamespace,
(GetDocumentType() != DOCUMENT_DOCX || mbUserShapes ? XML_txBody : XML_txbx));
- WriteText(xIface, /*bBodyPr=*/(GetDocumentType() != DOCUMENT_DOCX || mbUserShapes), /*bText=*/true,
+ WriteText(xIface, /*bBodyPr=*/(GetDocumentType() != DOCUMENT_DOCX || mbUserShapes), bText,
/*nXmlNamespace=*/0, /*bWritePropertiesAsLstStyles=*/bWritePropertiesAsLstStyles);
+ if (!bText)
+ // A text body holds at least one paragraph, and an empty one says the shape has no text
+ pFS->singleElementNS(XML_a, XML_p);
pFS->endElementNS( nXmlNamespace, (GetDocumentType() != DOCUMENT_DOCX || mbUserShapes ? XML_txBody : XML_txbx) );
if (GetDocumentType() == DOCUMENT_DOCX && !mbUserShapes)
WriteText( xIface, /*bBodyPr=*/true, /*bText=*/false, /*nXmlNamespace=*/nXmlNamespace );
diff --git a/sd/qa/unit/export-tests-ooxml4.cxx b/sd/qa/unit/export-tests-ooxml4.cxx
index bd6a8d1fbdc9..401a568397a5 100644
--- a/sd/qa/unit/export-tests-ooxml4.cxx
+++ b/sd/qa/unit/export-tests-ooxml4.cxx
@@ -24,6 +24,7 @@
#include
#include
#include
+#include
#include
#include
@@ -932,8 +933,8 @@ CPPUNIT_TEST_FIXTURE(SdOOXMLExportTest4, testTdf140912_PicturePlaceholder)
>>= isEmptyPresentationObject;
CPPUNIT_ASSERT(isEmptyPresentationObject);
- // If we supported custom prompt text, here we would also test "String" property,
- // which would be equal to "Insert Image". See first tests: testCustomPromptTexts
+ // The slide-side shape does not carry the layout's prompt; only the layout placeholder
+ // does. See testPicturePlaceholderCustomPromptText for the prompt itself.
auto xShape = xShapeProps.queryThrow();
aSizeBefore = xShape->getSize();
@@ -1003,6 +1004,54 @@ CPPUNIT_TEST_FIXTURE(SdOOXMLExportTest4, testPicPlaceholderMovedToLayout)
assertXPath(pSlide, "/p:sld/p:cSld/p:spTree/p:sp/p:nvSpPr/p:nvPr/p:ph[@type='pic']", 1);
}
+CPPUNIT_TEST_FIXTURE(SdOOXMLExportTest4, testSlideShowsMasterCustomPrompt)
+{
+ // Given a layout whose picture placeholder authored a prompt:
+ createSdImpressDoc("pptx/picture-placeholder-custom-prompt.pptx");
+
+ // The slide's own placeholder shows it too. PlaceholderText is what the user reads on the
+ // slide; without the accompanying fix the slide ignored the layout and showed Impress's own
+ // resource string instead, so the layout and the slide disagreed about the same placeholder.
+ // The placeholder is looked up by what it is, not by where it sits: which shape comes first on
+ // the slide is not what this test is about.
+ SdPage* pPage = getSdDocShell()->GetDoc()->GetSdPage(0, PageKind::Standard);
+ SdrObject* pPlaceholder = pPage->GetPresObj(PresObjKind::Graphic);
+ CPPUNIT_ASSERT_MESSAGE("no picture placeholder on the slide", pPlaceholder);
+ auto xShapeProps = GetXShapeForSdrObject(pPlaceholder).queryThrow();
+
+ OUString aPlaceholderText;
+ CPPUNIT_ASSERT(xShapeProps->getPropertyValue(u"PlaceholderText"_ustr) >>= aPlaceholderText);
+ CPPUNIT_ASSERT_EQUAL(u"Custom prompt to insert an image"_ustr, aPlaceholderText);
+}
+
+CPPUNIT_TEST_FIXTURE(SdOOXMLExportTest4, testPicturePlaceholderCustomPromptText)
+{
+ // Given one layout whose picture placeholder authored a prompt, and another whose
+ // picture placeholder authored none:
+ createSdImpressDoc("pptx/picture-placeholder-custom-prompt.pptx");
+ save(TestFilter::PPTX);
+
+ static constexpr OString aPicPlaceholder(
+ "/p:sldLayout/p:cSld/p:spTree/p:sp[p:nvSpPr/p:nvPr/p:ph/@type='pic']"_ostr);
+
+ // The authored prompt survives. Without the accompanying fix it was replaced by Impress's
+ // own localized resource, since a slide layout arrives as a master page and the shown text
+ // was only ever set off a master.
+ xmlDocUniquePtr pAuthored = parseExportedLayoutNamed(u"Picture placeholder, custom prompt");
+ assertXPath(pAuthored, aPicPlaceholder + "/p:nvSpPr/p:nvPr/p:ph", "hasCustomPrompt", u"1");
+ assertXPathContent(pAuthored, aPicPlaceholder + "/p:txBody/a:p/a:r/a:t",
+ u"Custom prompt to insert an image");
+
+ // The one with no authored prompt writes no text, rather than that same resource string
+ // written out as if the document had contained it. Its body properties stay: a reader needs
+ // the insets and the anchor whether or not there is anything in the box yet.
+ xmlDocUniquePtr pDefault = parseExportedLayoutNamed(u"Picture with Caption");
+ assertXPath(pDefault, aPicPlaceholder, 1);
+ assertXPath(pDefault, aPicPlaceholder + "/p:txBody/a:bodyPr", 1);
+ assertXPath(pDefault, aPicPlaceholder + "/p:txBody/a:p", 1);
+ assertXPath(pDefault, aPicPlaceholder + "/p:txBody/a:p/a:r", 0);
+}
+
CPPUNIT_TEST_FIXTURE(SdOOXMLExportTest4, testEnhancedPathViewBox)
{
createSdImpressDoc("odp/tdf147978_enhancedPath_viewBox.odp");
diff --git a/sd/source/core/sdpage.cxx b/sd/source/core/sdpage.cxx
index a92d7e9259da..dda0a5587463 100644
--- a/sd/source/core/sdpage.cxx
+++ b/sd/source/core/sdpage.cxx
@@ -2727,6 +2727,18 @@ Orientation SdPage::GetOrientation() const
OUString SdPage::GetPresObjText(PresObjKind eObjKind) const
{
+ // A layout can author its own prompt, and it belongs to the slides using that layout, so ask
+ // the master for one before falling back to our own resource string.
+ if (!mbMaster && TRG_HasMasterPage())
+ {
+ SdPage& rMasterPage = static_cast(TRG_GetMasterPage());
+ if (SdrObject* pMasterObj = rMasterPage.GetPresObj(eObjKind))
+ {
+ if (!pMasterObj->GetCustomPromptText().isEmpty())
+ return pMasterObj->GetCustomPromptText();
+ }
+ }
+
OUString aString;
#if defined(IOS) || defined(ANDROID)
@@ -3018,7 +3030,8 @@ bool SdPage::RestoreDefaultText( SdrObject* pObj, const OUString& rStr )
if (ePresObjKind == PresObjKind::Title ||
ePresObjKind == PresObjKind::Outline ||
ePresObjKind == PresObjKind::Notes ||
- ePresObjKind == PresObjKind::Text)
+ ePresObjKind == PresObjKind::Text ||
+ ePresObjKind == PresObjKind::Graphic)
{
sd::ModifyGuard aGuard(static_cast(&getSdrModelFromSdrPage()));
diff --git a/sd/source/filter/eppt/pptx-epptooxml.cxx b/sd/source/filter/eppt/pptx-epptooxml.cxx
index 5c705599ab0a..b7ddf293cc57 100644
--- a/sd/source/filter/eppt/pptx-epptooxml.cxx
+++ b/sd/source/filter/eppt/pptx-epptooxml.cxx
@@ -2743,9 +2743,22 @@ ShapeExport& PowerPointShapeExport::WritePlaceholderShape(const Reference< XShap
= (mePageType == PageType::MASTER)
&& (ePlaceholder == Title || ePlaceholder == Subtitle || ePlaceholder == Outliner);
- // A slide-side empty picture placeholder inherits the prompt from the layout.
+ // An untouched placeholder shows Impress's own localized prompt resource unless the document
+ // authored one, and that must not reach the file - a reader supplies its own prompt.
+ bool bIsEmptyPresObj(false);
+ if (xProps
+ && xProps->getPropertySetInfo()->hasPropertyByName(u"IsEmptyPresentationObject"_ustr))
+ xProps->getPropertyValue(u"IsEmptyPresentationObject"_ustr) >>= bIsEmptyPresObj;
+ // Field placeholders carry their field content, and a master's text box carries list styles.
+ const bool bTextIsDefaultPrompt = bIsEmptyPresObj && !bUseCustomPrompt && !bUsePlaceholderIndex
+ && !bWritePropertiesAsLstStyles;
+
+ // A slide-side empty picture placeholder inherits the prompt from the layout. The body
+ // properties belong in the file even where the text does not - they carry the insets, the
+ // anchor, the writing direction and the autofit.
if (ePlaceholder != Picture || mbMaster)
- WriteTextBox(xShape, XML_p, bUsePlaceholderIndex || bWritePropertiesAsLstStyles);
+ WriteTextBox(xShape, XML_p, bUsePlaceholderIndex || bWritePropertiesAsLstStyles,
+ /*bText=*/!bTextIsDefaultPrompt);
mpFS->endElementNS(XML_p, XML_sp);
diff --git a/sd/source/ui/unoidl/unoobj.cxx b/sd/source/ui/unoidl/unoobj.cxx
index 7c865bcc4897..86c058c39bf2 100644
--- a/sd/source/ui/unoidl/unoobj.cxx
+++ b/sd/source/ui/unoidl/unoobj.cxx
@@ -983,15 +983,11 @@ void SdXShape::SetCustomPromptText(const OUString& aVal)
if (pObj == nullptr)
return;
- if (!pObj->getSdrPageFromSdrObject()->IsMasterPage())
- {
- if (pObj->getSdrPageFromSdrObject()->RestoreDefaultText(pObj, aVal))
- pObj->SetCustomPromptText(aVal);
- }
- else
- {
+ // A slide layout is imported as a master page, and a layout is where an authored prompt lives,
+ // so the shown text has to follow the property there too - not just on a slide.
+ const bool bTextSet = pObj->getSdrPageFromSdrObject()->RestoreDefaultText(pObj, aVal);
+ if (bTextSet || pObj->getSdrPageFromSdrObject()->IsMasterPage())
pObj->SetCustomPromptText(aVal);
- }
}
bool SdXShape::IsMasterDepend() const noexcept