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