tdf#123626 Allow adding hyperlinks to shapes

* Support hyperlinks on Shapes in Writer
* Add menu items
* Add context menu items
* ODF import/export + test
* OOXML import/export + test

Change-Id: I7269064c4cabd16fdb8259a48429c508184d3ccf
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/119164
Tested-by: Jenkins
Reviewed-by: Samuel Mehrbrodt <samuel.mehrbrodt@allotropia.de>
diff --git a/include/editeng/unoprnms.hxx b/include/editeng/unoprnms.hxx
index 011e504..f99ddac 100644
--- a/include/editeng/unoprnms.hxx
+++ b/include/editeng/unoprnms.hxx
@@ -163,6 +163,8 @@
#define UNO_NAME_MISC_OBJ_TITLE                 "Title"
#define UNO_NAME_MISC_OBJ_DESCRIPTION           "Description"

#define UNO_NAME_HYPERLINK                      "Hyperlink"

#define UNO_NAME_GRAPHOBJ_FILLBITMAP            "GraphicObjectFillBitmap"
#define UNO_NAME_GRAPHOBJ_REPLACEMENT_GRAPHIC   "ReplacementGraphic"
#define UNO_NAME_GRAPHOBJ_GRAFSTREAMURL         "GraphicStreamURL"
diff --git a/include/oox/vml/vmlshape.hxx b/include/oox/vml/vmlshape.hxx
index 0e50e5b..d90ecc2 100644
--- a/include/oox/vml/vmlshape.hxx
+++ b/include/oox/vml/vmlshape.hxx
@@ -112,6 +112,7 @@ struct ShapeTypeModel
    OptValue<OUString> moCropRight; ///< Specifies how much to crop the image from the right in as a fraction of picture size.
    OptValue<OUString> moCropTop; ///< Specifies how much to crop the image from the top down as a fraction of picture size.
    OUString maLayoutFlowAlt; ///< Specifies the alternate layout flow for text in textboxes.
    OUString maHyperlink; ///< The hyperlink assigned to the shape

    explicit            ShapeTypeModel();

diff --git a/include/oox/vml/vmlshapecontext.hxx b/include/oox/vml/vmlshapecontext.hxx
index ddca7b1..0d4b3ddd 100644
--- a/include/oox/vml/vmlshapecontext.hxx
+++ b/include/oox/vml/vmlshapecontext.hxx
@@ -107,6 +107,8 @@ public:
private:
    /** Processes the 'style' attribute. */
    void                setStyle( const OUString& rStyle );
    /** Processes the 'href' attribute. */
    void                setHyperlink( const OUString& rHyperlink );

    /** Resolve a relation identifier to a fragment path. */
    OptValue< OUString > decodeFragmentPath( const AttributeList& rAttribs, sal_Int32 nToken ) const;
diff --git a/include/svl/solar.hrc b/include/svl/solar.hrc
index 521e243..5caeb64 100644
--- a/include/svl/solar.hrc
+++ b/include/svl/solar.hrc
@@ -23,7 +23,7 @@
// defines ------------------------------------------------------------------

#define OWN_ATTR_VALUE_START                    3900
#define OWN_ATTR_VALUE_END                      4006
#define OWN_ATTR_VALUE_END                      4007

#define RID_LIB_START               10000
#define RID_LIB_END                 19999
diff --git a/include/svx/svdobj.hxx b/include/svx/svdobj.hxx
index 0960826..d7ccf83 100644
--- a/include/svx/svdobj.hxx
+++ b/include/svx/svdobj.hxx
@@ -868,6 +868,9 @@ public:
    // If fillstyle is drawing::FillStyle_BITMAP, returns the graphic.
    const Graphic* getFillGraphic() const;

    OUString getHyperlink() const { return msHyperlink; }
    void setHyperlink(const OUString& sHyperlink) { msHyperlink = sHyperlink; }

protected:
    mutable tools::Rectangle    m_aOutRect;     // surrounding rectangle for Paint (incl. LineWidth, ...)
    Point                       m_aAnchor;      // anchor position (Writer)
@@ -971,6 +974,8 @@ private:
    // HACK: Do not automatically insert newly created object into a page.
    // The user needs to do it manually later.
    bool                        mbDoNotInsertIntoPageAutomatically;
    // Hyperlink for the whole shape
    OUString msHyperlink;

    // only for internal use!
    SvxShape* getSvxShape();
diff --git a/include/svx/unoshprp.hxx b/include/svx/unoshprp.hxx
index 3f61ce7..750a245 100644
--- a/include/svx/unoshprp.hxx
+++ b/include/svx/unoshprp.hxx
@@ -195,7 +195,8 @@
#define OWN_ATTR_QRCODE                         (OWN_ATTR_VALUE_START+104)
#define OWN_ATTR_TEXTFITTOSIZESCALE             (OWN_ATTR_VALUE_START+105)
#define OWN_ATTR_TEXTCOLUMNS                    (OWN_ATTR_VALUE_START+106)
// ATTENTION: current maximum is OWN_ATTR_VALUE_START+106 svx; wnen adding values, update
#define OWN_ATTR_HYPERLINK                      (OWN_ATTR_VALUE_START+107)
// ATTENTION: current maximum is OWN_ATTR_VALUE_START+107 svx; wnen adding values, update
// OWN_ATTR_VALUE_END in include/svl/solar.hrc accordingly

// #FontWork#
@@ -358,7 +359,8 @@
    { u"TextFitToSizeScale", OWN_ATTR_TEXTFITTOSIZESCALE, ::cppu::UnoType<sal_Int16>::get(), 0, 0}, \
    /* #i68101# */ \
    { u"" UNO_NAME_MISC_OBJ_TITLE,        OWN_ATTR_MISC_OBJ_TITLE         , ::cppu::UnoType<OUString>::get(),    0,  0}, \
    { u"" UNO_NAME_MISC_OBJ_DESCRIPTION,  OWN_ATTR_MISC_OBJ_DESCRIPTION   , ::cppu::UnoType<OUString>::get(),    0,  0},
    { u"" UNO_NAME_MISC_OBJ_DESCRIPTION,  OWN_ATTR_MISC_OBJ_DESCRIPTION   , ::cppu::UnoType<OUString>::get(),    0,  0}, \
    { u"" UNO_NAME_HYPERLINK, OWN_ATTR_HYPERLINK, ::cppu::UnoType<OUString>::get(), 0,  0},

#define LINKTARGET_PROPERTIES \
    { u"" UNO_NAME_LINKDISPLAYNAME,   OWN_ATTR_LDNAME             , ::cppu::UnoType<OUString>::get(),    css::beans::PropertyAttribute::READONLY, 0}, \
diff --git a/oox/source/export/vmlexport.cxx b/oox/source/export/vmlexport.cxx
index 77d3e16..5b26718 100644
--- a/oox/source/export/vmlexport.cxx
+++ b/oox/source/export/vmlexport.cxx
@@ -1389,6 +1389,10 @@ sal_Int32 VMLExport::StartShape()
            break;
    }

    if (!m_pSdrObject->getHyperlink().isEmpty())
        m_pShapeAttrList->add(
            XML_href, OUStringToOString(m_pSdrObject->getHyperlink(), RTL_TEXTENCODING_UTF8));

    m_pShapeAttrList->addNS(XML_o, XML_allowincell, m_IsFollowingTextFlow ? "t" : "f");

    // add style
diff --git a/oox/source/token/properties.txt b/oox/source/token/properties.txt
index 68b5e2d..04b550e 100644
--- a/oox/source/token/properties.txt
+++ b/oox/source/token/properties.txt
@@ -254,6 +254,7 @@ HoriOrientPosition
HoriOrientRelation
HorizontalSplitMode
HorizontalSplitPositionTwips
Hyperlink
IgnoreBlankCells
IgnoreCase
IgnoreLeadingSpaces
diff --git a/oox/source/vml/vmlshape.cxx b/oox/source/vml/vmlshape.cxx
index 9848129..d102917 100644
--- a/oox/source/vml/vmlshape.cxx
+++ b/oox/source/vml/vmlshape.cxx
@@ -813,6 +813,10 @@ Reference< XShape > SimpleShape::implConvertAndInsert( const Reference< XShapes 
        if(!maTypeModel.maWrapStyle.isEmpty())
            PropertySet(xShape).setAnyProperty(PROP_TextWordWrap, makeAny(maTypeModel.maWrapStyle == "square"));

        // tdf#123626
        if (!maTypeModel.maHyperlink.isEmpty())
            PropertySet(xShape).setAnyProperty(PROP_Hyperlink, makeAny(maTypeModel.maHyperlink));

        PropertySet(xShape).setAnyProperty(PROP_TextAutoGrowHeight,
                                           makeAny(maTypeModel.mbAutoHeight));

diff --git a/oox/source/vml/vmlshapecontext.cxx b/oox/source/vml/vmlshapecontext.cxx
index 0d0c00b..d53a924 100644
--- a/oox/source/vml/vmlshapecontext.cxx
+++ b/oox/source/vml/vmlshapecontext.cxx
@@ -312,6 +312,7 @@ ShapeTypeContext::ShapeTypeContext(ContextHandler2Helper const & rParent,
    mrTypeModel.moCoordPos = lclDecodeInt32Pair( rAttribs, XML_coordorigin );
    mrTypeModel.moCoordSize = lclDecodeInt32Pair( rAttribs, XML_coordsize );
    setStyle( rAttribs.getString( XML_style, OUString() ) );
    setHyperlink( rAttribs.getString( XML_href, OUString() ) );
    if( lclDecodeBool( rAttribs, O_TOKEN( hr )).get( false ))
    {   // MSO's handling of o:hr width is nowhere near what the spec says:
        // - o:hrpct is not in % but in 0.1%
@@ -469,6 +470,11 @@ void ShapeTypeContext::setStyle( const OUString& rStyle )
    }
}

void ShapeTypeContext::setHyperlink( const OUString& rHyperlink )
{
    mrTypeModel.maHyperlink = rHyperlink;
}

ShapeContext::ShapeContext(ContextHandler2Helper const& rParent,
                           const std::shared_ptr<ShapeBase>& pShape, const AttributeList& rAttribs)
    : ShapeTypeContext(rParent, pShape, rAttribs)
diff --git a/sc/source/filter/xml/xmlexprt.cxx b/sc/source/filter/xml/xmlexprt.cxx
index 28233c0..085d54d 100644
--- a/sc/source/filter/xml/xmlexprt.cxx
+++ b/sc/source/filter/xml/xmlexprt.cxx
@@ -3426,40 +3426,8 @@ void ScXMLExport::ExportShape(const uno::Reference < drawing::XShape >& xShape, 
        }
    }
    if (!bIsChart)
    {
        OUString sHlink;
        try
        {
            uno::Reference< beans::XPropertySet > xProps( xShape, uno::UNO_QUERY );
            if ( xProps.is() )
                xProps->getPropertyValue( SC_UNONAME_HYPERLINK ) >>= sHlink;
        }
        catch ( const beans::UnknownPropertyException& )
        {
            // no hyperlink property
        }

        std::unique_ptr< SvXMLElementExport > pDrawA;
        // enclose shapes with <draw:a> element only if sHlink contains something
        if ( !sHlink.isEmpty() )
        {
            // need to get delete the attributes that are pre-loaded
            // for the shape export ( otherwise they will become
            // attributes of the draw:a element ) This *shouldn't*
            // affect performance adversely as there are only a
            // couple of attributes involved
            uno::Reference< xml::sax::XAttributeList > xSaveAttribs( new  SvXMLAttributeList( GetAttrList() ) );
            ClearAttrList();
            // Add Hlink
            AddAttribute( XML_NAMESPACE_XLINK, XML_TYPE, XML_SIMPLE );
            AddAttribute( XML_NAMESPACE_XLINK, XML_HREF, sHlink);
            pDrawA.reset( new SvXMLElementExport( *this, XML_NAMESPACE_DRAW, XML_A, false, false ) );
            // Attribute list has been cleared by previous operation
            // re-add pre-loaded attributes
            AddAttributeList( xSaveAttribs );
        }
        GetShapeExport()->exportShape(xShape, SEF_DEFAULT, pPoint);
    }

    IncrementProgressBar(false);
}

diff --git a/svx/source/unodraw/unoshape.cxx b/svx/source/unodraw/unoshape.cxx
index 2a84ba1..57fd337 100644
--- a/svx/source/unodraw/unoshape.cxx
+++ b/svx/source/unodraw/unoshape.cxx
@@ -2514,6 +2514,17 @@ bool SvxShape::setPropertyValueImpl( const OUString&, const SfxItemPropertyMapEn
        return true;
    }

    case OWN_ATTR_HYPERLINK:
    {
        OUString sHyperlink;
        if (rValue >>= sHyperlink)
        {
            GetSdrObject()->setHyperlink(sHyperlink);
            return true;
        }
        break;
    }

    default:
    {
        return false;
@@ -2935,6 +2946,12 @@ bool SvxShape::getPropertyValueImpl( const OUString&, const SfxItemPropertyMapEn
        break;
    }

    case OWN_ATTR_HYPERLINK:
    {
        rValue <<= GetSdrObject()->getHyperlink();
        break;
    }

    default:
        return false;
    }
diff --git a/sw/qa/extras/odfexport/data/shape-with-hyperlink.odt b/sw/qa/extras/odfexport/data/shape-with-hyperlink.odt
new file mode 100644
index 0000000..7b11278
--- /dev/null
+++ b/sw/qa/extras/odfexport/data/shape-with-hyperlink.odt
Binary files differ
diff --git a/sw/qa/extras/odfexport/odfexport2.cxx b/sw/qa/extras/odfexport/odfexport2.cxx
index 5269779..0812f2c 100644
--- a/sw/qa/extras/odfexport/odfexport2.cxx
+++ b/sw/qa/extras/odfexport/odfexport2.cxx
@@ -82,6 +82,16 @@ DECLARE_ODFEXPORT_TEST(testListFormatDocx, "listformat.docx")
        "text:list-level-style-number[@text:level='3']", "num-suffix", "<<");
}

DECLARE_ODFEXPORT_TEST(testShapeWithHyperlink, "shape-with-hyperlink.odt")
{
    if (xmlDocUniquePtr pXmlDoc = parseExport("content.xml"))
    {
        // Check how conversion from prefix/suffix to list format did work
        assertXPath(pXmlDoc, "/office:document-content/office:body/office:text/text:p/draw:a",
                    "href", "http://shape.com/");
    }
}

DECLARE_ODFEXPORT_TEST(testListFormatOdt, "listformat.odt")
{
    // Ensure in resulting ODT we also have not just prefix/suffix, but custom delimiters
diff --git a/sw/qa/extras/ooxmlexport/data/hyperlinkshape.docx b/sw/qa/extras/ooxmlexport/data/hyperlinkshape.docx
new file mode 100644
index 0000000..3bf4f8c
--- /dev/null
+++ b/sw/qa/extras/ooxmlexport/data/hyperlinkshape.docx
Binary files differ
diff --git a/sw/qa/extras/ooxmlexport/ooxmlexport16.cxx b/sw/qa/extras/ooxmlexport/ooxmlexport16.cxx
index 7a1486f..37ed272 100644
--- a/sw/qa/extras/ooxmlexport/ooxmlexport16.cxx
+++ b/sw/qa/extras/ooxmlexport/ooxmlexport16.cxx
@@ -440,6 +440,13 @@ DECLARE_OOXMLEXPORT_EXPORTONLY_TEST(testTdf142404_tabOverSpacingC15, "tdf142404_
    CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE("8 lines high", 242*8, nHeight, 121);
}

DECLARE_OOXMLEXPORT_TEST(testShapeHyperlink, "hyperlinkshape.docx")
{
    // Test import/export of hyperlink property on shapes
    auto xShape(getShape(1));
    CPPUNIT_ASSERT_EQUAL(OUString("https://libreoffice.org/"), getProperty<OUString>(xShape, "Hyperlink"));
}

DECLARE_OOXMLEXPORT_TEST(testTdf139580, "tdf139580.odt")
{
    // Without the fix in place, this test would have crashed at export time
diff --git a/sw/sdi/_drwbase.sdi b/sw/sdi/_drwbase.sdi
index eae02c1..e90d50c 100644
--- a/sw/sdi/_drwbase.sdi
+++ b/sw/sdi/_drwbase.sdi
@@ -117,5 +117,38 @@ interface BaseTextDrawBase
        StateMethod = GetState ;
        DisableFlags="SfxDisableFlags::SwOnProtectedCursor";
    ]
    SID_EDIT_HYPERLINK
    [
        ExecMethod = Execute ;
        StateMethod = GetState ;
        DisableFlags="SfxDisableFlags::SwOnProtectedCursor";
    ]
    SID_HYPERLINK_DIALOG
    [
        ExecMethod = Execute;
        StateMethod = GetState;
        DisableFlags="SfxDisableFlags::SwOnProtectedCursor";
    ]
    SID_HYPERLINK_SETLINK // status()
    [
        ExecMethod = Execute ;
        DisableFlags="SfxDisableFlags::SwOnProtectedCursor";
    ]
    SID_HYPERLINK_GETLINK // status()
    [
        StateMethod = GetState ;
    ]
    SID_REMOVE_HYPERLINK
    [
        ExecMethod = Execute ;
        StateMethod = GetState;
        DisableFlags="SfxDisableFlags::SwOnProtectedCursor";
    ]
    SID_COPY_HYPERLINK_LOCATION
    [
        ExecMethod = Execute ;
        StateMethod = GetState;
        DisableFlags="SfxDisableFlags::SwOnProtectedCursor";
    ]
}

diff --git a/sw/source/filter/ww8/docxsdrexport.cxx b/sw/source/filter/ww8/docxsdrexport.cxx
index 8055445..5a8fb68 100644
--- a/sw/source/filter/ww8/docxsdrexport.cxx
+++ b/sw/source/filter/ww8/docxsdrexport.cxx
@@ -19,6 +19,7 @@
#include <svx/svdogrp.hxx>
#include <svx/svdobjkind.hxx>
#include <oox/token/namespaces.hxx>
#include <oox/token/relationship.hxx>
#include <textboxhelper.hxx>
#include <fmtanchr.hxx>
#include <fmtsrnd.hxx>
@@ -1269,7 +1270,20 @@ void DocxSdrExport::writeDMLDrawing(const SdrObject* pSdrObject, const SwFrameFo
        && pFrameFormat->GetAnchor().GetAnchorId() != RndStdIds::FLY_AS_CHAR)

        pDocPrAttrList->add(XML_hidden, OString::number(1).getStr());
    pFS->singleElementNS(XML_wp, XML_docPr, pDocPrAttrList);

    pFS->startElementNS(XML_wp, XML_docPr, pDocPrAttrList);
    OUString sHyperlink = pSdrObject->getHyperlink();
    if (!sHyperlink.isEmpty())
    {
        OUString sRelId = m_pImpl->getExport().GetFilter().addRelation(
            pFS->getOutputStream(), oox::getRelationship(Relationship::HYPERLINK),
            oox::drawingml::URLTransformer().getTransformedString(sHyperlink),
            oox::drawingml::URLTransformer().isExternalURL(sHyperlink));
        pFS->singleElementNS(XML_a, XML_hlinkClick, FSNS(XML_r, XML_id), sRelId,
                             FSNS(XML_xmlns, XML_a),
                             m_pImpl->getExport().GetFilter().getNamespaceURL(OOX_NS(dml)));
    }
    pFS->endElementNS(XML_wp, XML_docPr);

    uno::Reference<lang::XServiceInfo> xServiceInfo(xShape, uno::UNO_QUERY_THROW);
    const char* pNamespace = "http://schemas.microsoft.com/office/word/2010/wordprocessingShape";
diff --git a/sw/source/uibase/shells/drwbassh.cxx b/sw/source/uibase/shells/drwbassh.cxx
index 6d8ba41..29b7ec3 100644
--- a/sw/source/uibase/shells/drwbassh.cxx
+++ b/sw/source/uibase/shells/drwbassh.cxx
@@ -27,6 +27,7 @@
#include <svl/whiter.hxx>
#include <svx/swframevalidation.hxx>
#include <svx/anchorid.hxx>
#include <svx/hlnkitem.hxx>
#include <osl/diagnose.h>
#include <drawdoc.hxx>
#include <uitool.hxx>
@@ -47,16 +48,23 @@
#include <sfx2/msg.hxx>
#include <swslots.hxx>
#include <svx/svxdlg.hxx>
#include <vcl/unohelp2.hxx>
#include <swabstdlg.hxx>
#include <swundo.hxx>
#include <com/sun/star/beans/XPropertySet.hpp>
#include <com/sun/star/drawing/XShape.hpp>
#include <com/sun/star/text/HoriOrientation.hpp>
#include <com/sun/star/text/VertOrientation.hpp>
#include <com/sun/star/text/RelOrientation.hpp>
#include <com/sun/star/uno/Reference.hxx>
#include <IDocumentDrawModelAccess.hxx>
#include <fmtfollowtextflow.hxx>
#include <textboxhelper.hxx>

using namespace ::com::sun::star;
using namespace css::beans;
using namespace css::drawing;
using namespace css::uno;

SFX_IMPL_SUPERCLASS_INTERFACE(SwDrawBaseShell, SwBaseShell)

@@ -597,6 +605,43 @@ void SwDrawBaseShell::Execute(SfxRequest const &rReq)
            break;
        }

        case SID_EDIT_HYPERLINK:
        case SID_HYPERLINK_DIALOG:
        {
            GetView().GetViewFrame()->SetChildWindow(SID_HYPERLINK_DIALOG, true);
            break;
        }

        case SID_HYPERLINK_SETLINK:
        {
            if(pItem)
            {
                const SvxHyperlinkItem& rHLinkItem = *static_cast<const SvxHyperlinkItem *>(pItem);
                const SdrMarkList& rMarkList = pSdrView->GetMarkedObjectList();
                SdrObject* pObj = rMarkList.GetMark(0)->GetMarkedSdrObj();
                pObj->setHyperlink(rHLinkItem.GetURL());
            }
            break;
        }

        case SID_REMOVE_HYPERLINK:
        {
            const SdrMarkList& rMarkList = pSdrView->GetMarkedObjectList();
            SdrObject* pObj = rMarkList.GetMark(0)->GetMarkedSdrObj();
            pObj->setHyperlink(OUString());
            break;
        }

        case SID_COPY_HYPERLINK_LOCATION:
        {
            const SdrMarkList& rMarkList = pSdrView->GetMarkedObjectList();
            SdrObject* pObj = rMarkList.GetMark(0)->GetMarkedSdrObj();
            uno::Reference<datatransfer::clipboard::XClipboard> xClipboard
                = GetView().GetEditWin().GetClipboard();
            vcl::unohelper::TextDataObject::CopyStringTo(pObj->getHyperlink(), xClipboard);
            break;
        }

        default:
            OSL_ENSURE(false, "wrong Dispatcher");
            return;
@@ -791,6 +836,35 @@ void SwDrawBaseShell::GetState(SfxItemSet& rSet)
                    }
                }
                break;

            case SID_EDIT_HYPERLINK:
            case SID_HYPERLINK_DIALOG:
            case SID_REMOVE_HYPERLINK:
            case SID_COPY_HYPERLINK_LOCATION:
            {
                if (pSdrView->GetMarkedObjectCount() != 1)
                    rSet.DisableItem(nWhich);
                else if (nWhich == SID_REMOVE_HYPERLINK || nWhich == SID_EDIT_HYPERLINK
                         || nWhich == SID_COPY_HYPERLINK_LOCATION)
                {
                    const SdrMarkList& rMarkList = pSdrView->GetMarkedObjectList();
                    SdrObject* pObj = rMarkList.GetMark(0)->GetMarkedSdrObj();
                    if (pObj->getHyperlink().isEmpty())
                        rSet.DisableItem(nWhich);
                }
            }
            break;

            case SID_HYPERLINK_GETLINK:
            {
                const SdrMarkList& rMarkList = pSdrView->GetMarkedObjectList();
                SdrObject* pObj = rMarkList.GetMark(0)->GetMarkedSdrObj();
                OUString sHyperLink = pObj->getHyperlink();
                SvxHyperlinkItem aHLinkItem;
                aHLinkItem.SetURL(sHyperLink);
                rSet.Put(aHLinkItem);
            }
            break;
        }
        nWhich = aIter.NextWhich();
    }
diff --git a/sw/uiconfig/sglobal/popupmenu/draw.xml b/sw/uiconfig/sglobal/popupmenu/draw.xml
index d119c1a..799c02c 100644
--- a/sw/uiconfig/sglobal/popupmenu/draw.xml
+++ b/sw/uiconfig/sglobal/popupmenu/draw.xml
@@ -71,6 +71,11 @@
  <menu:menuitem menu:id=".uno:EnterGroup"/>
  <menu:menuitem menu:id=".uno:LeaveGroup"/>
  <menu:menuseparator/>
  <menu:menuitem menu:id=".uno:OpenHyperlinkOnCursor"/>
  <menu:menuitem menu:id=".uno:EditHyperlink"/>
  <menu:menuitem menu:id=".uno:CopyHyperlinkLocation"/>
  <menu:menuitem menu:id=".uno:RemoveHyperlink"/>
  <menu:menuseparator/>
  <menu:menuitem menu:id=".uno:AddTextBox"/>
  <menu:menuitem menu:id=".uno:RemoveTextBox"/>
  <menu:menuitem menu:id=".uno:EditSignatureLine"/>
diff --git a/sw/uiconfig/swform/popupmenu/draw.xml b/sw/uiconfig/swform/popupmenu/draw.xml
index d119c1a..799c02c 100644
--- a/sw/uiconfig/swform/popupmenu/draw.xml
+++ b/sw/uiconfig/swform/popupmenu/draw.xml
@@ -71,6 +71,11 @@
  <menu:menuitem menu:id=".uno:EnterGroup"/>
  <menu:menuitem menu:id=".uno:LeaveGroup"/>
  <menu:menuseparator/>
  <menu:menuitem menu:id=".uno:OpenHyperlinkOnCursor"/>
  <menu:menuitem menu:id=".uno:EditHyperlink"/>
  <menu:menuitem menu:id=".uno:CopyHyperlinkLocation"/>
  <menu:menuitem menu:id=".uno:RemoveHyperlink"/>
  <menu:menuseparator/>
  <menu:menuitem menu:id=".uno:AddTextBox"/>
  <menu:menuitem menu:id=".uno:RemoveTextBox"/>
  <menu:menuitem menu:id=".uno:EditSignatureLine"/>
diff --git a/sw/uiconfig/swreport/popupmenu/draw.xml b/sw/uiconfig/swreport/popupmenu/draw.xml
index d119c1a..799c02c 100644
--- a/sw/uiconfig/swreport/popupmenu/draw.xml
+++ b/sw/uiconfig/swreport/popupmenu/draw.xml
@@ -71,6 +71,11 @@
  <menu:menuitem menu:id=".uno:EnterGroup"/>
  <menu:menuitem menu:id=".uno:LeaveGroup"/>
  <menu:menuseparator/>
  <menu:menuitem menu:id=".uno:OpenHyperlinkOnCursor"/>
  <menu:menuitem menu:id=".uno:EditHyperlink"/>
  <menu:menuitem menu:id=".uno:CopyHyperlinkLocation"/>
  <menu:menuitem menu:id=".uno:RemoveHyperlink"/>
  <menu:menuseparator/>
  <menu:menuitem menu:id=".uno:AddTextBox"/>
  <menu:menuitem menu:id=".uno:RemoveTextBox"/>
  <menu:menuitem menu:id=".uno:EditSignatureLine"/>
diff --git a/sw/uiconfig/swriter/popupmenu/draw.xml b/sw/uiconfig/swriter/popupmenu/draw.xml
index d119c1a..799c02c 100644
--- a/sw/uiconfig/swriter/popupmenu/draw.xml
+++ b/sw/uiconfig/swriter/popupmenu/draw.xml
@@ -71,6 +71,11 @@
  <menu:menuitem menu:id=".uno:EnterGroup"/>
  <menu:menuitem menu:id=".uno:LeaveGroup"/>
  <menu:menuseparator/>
  <menu:menuitem menu:id=".uno:OpenHyperlinkOnCursor"/>
  <menu:menuitem menu:id=".uno:EditHyperlink"/>
  <menu:menuitem menu:id=".uno:CopyHyperlinkLocation"/>
  <menu:menuitem menu:id=".uno:RemoveHyperlink"/>
  <menu:menuseparator/>
  <menu:menuitem menu:id=".uno:AddTextBox"/>
  <menu:menuitem menu:id=".uno:RemoveTextBox"/>
  <menu:menuitem menu:id=".uno:EditSignatureLine"/>
diff --git a/sw/uiconfig/swxform/popupmenu/draw.xml b/sw/uiconfig/swxform/popupmenu/draw.xml
index d119c1a..799c02c 100644
--- a/sw/uiconfig/swxform/popupmenu/draw.xml
+++ b/sw/uiconfig/swxform/popupmenu/draw.xml
@@ -71,6 +71,11 @@
  <menu:menuitem menu:id=".uno:EnterGroup"/>
  <menu:menuitem menu:id=".uno:LeaveGroup"/>
  <menu:menuseparator/>
  <menu:menuitem menu:id=".uno:OpenHyperlinkOnCursor"/>
  <menu:menuitem menu:id=".uno:EditHyperlink"/>
  <menu:menuitem menu:id=".uno:CopyHyperlinkLocation"/>
  <menu:menuitem menu:id=".uno:RemoveHyperlink"/>
  <menu:menuseparator/>
  <menu:menuitem menu:id=".uno:AddTextBox"/>
  <menu:menuitem menu:id=".uno:RemoveTextBox"/>
  <menu:menuitem menu:id=".uno:EditSignatureLine"/>
diff --git a/writerfilter/source/dmapper/GraphicImport.cxx b/writerfilter/source/dmapper/GraphicImport.cxx
index 55a4eb6..940780c 100644
--- a/writerfilter/source/dmapper/GraphicImport.cxx
+++ b/writerfilter/source/dmapper/GraphicImport.cxx
@@ -408,9 +408,6 @@ public:
            uno::Reference< container::XNamed > xNamed( xGraphicObjectProperties, uno::UNO_QUERY_THROW );
            xNamed->setName(rDomainMapper.GetGraphicNamingHelper().NameGraphic(sName));

            if ( sHyperlinkURL.getLength() > 0 )
                xGraphicObjectProperties->setPropertyValue(getPropertyName( PROP_HYPER_LINK_U_R_L ),
                    uno::makeAny ( sHyperlinkURL ));
            xGraphicObjectProperties->setPropertyValue(getPropertyName( PROP_DESCRIPTION ),
                uno::makeAny( sAlternativeText ));
            xGraphicObjectProperties->setPropertyValue(getPropertyName( PROP_TITLE ),
@@ -422,6 +419,17 @@ public:
        }
    }

    void applyHyperlink(uno::Reference<beans::XPropertySet> const & xShapeProps, bool bIsShape)
    {
        // Graphic objects have a different hyperlink prop than shapes
        auto aHyperlinkProp = bIsShape ? PROP_HYPERLINK : PROP_HYPER_LINK_U_R_L;
        if (!sHyperlinkURL.isEmpty())
        {
            xShapeProps->setPropertyValue(
                getPropertyName(aHyperlinkProp), uno::makeAny(sHyperlinkURL));
        }
    }

    /// Getter for m_aInteropGrabBag, but also merges in the values from other members if they are set.
    comphelper::SequenceAsHashMap const & getInteropGrabBag()
    {
@@ -1271,6 +1279,7 @@ void GraphicImport::lcl_attribute(Id nName, Value& rValue)
                        xShapeProps->setPropertyValue("Surround", uno::makeAny(static_cast<sal_Int32>(m_pImpl->nWrap)));
                        m_pImpl->applyZOrder(xShapeProps);
                        m_pImpl->applyName(xShapeProps);
                        m_pImpl->applyHyperlink(xShapeProps, bUseShape);
                        xShapeProps->setPropertyValue("AllowOverlap",
                                                      uno::makeAny(m_pImpl->bAllowOverlap));

@@ -1858,6 +1867,7 @@ uno::Reference<text::XTextContent> GraphicImport::createGraphicObject(uno::Refer
                        uno::makeAny( awt::Size( m_pImpl->getXSize(), m_pImpl->getYSize() )));
                m_pImpl->applyMargins(xGraphicObjectProperties);
                m_pImpl->applyName(xGraphicObjectProperties);
                m_pImpl->applyHyperlink(xGraphicObjectProperties, false);
            }

            // Handle horizontal flip.
diff --git a/writerfilter/source/dmapper/PropertyIds.cxx b/writerfilter/source/dmapper/PropertyIds.cxx
index 0100313..c3e7cba 100644
--- a/writerfilter/source/dmapper/PropertyIds.cxx
+++ b/writerfilter/source/dmapper/PropertyIds.cxx
@@ -127,6 +127,7 @@ OUString getPropertyName( PropertyIds eId )
        case PROP_SUB_TYPE              :    sName = "SubType"; break;
        case PROP_FILE_FORMAT           :    sName = "FileFormat"; break;
        case PROP_HYPER_LINK_U_R_L      :    sName = "HyperLinkURL"; break;
        case PROP_HYPERLINK             :    sName = "Hyperlink"; break;
        case PROP_NUMBER_FORMAT         :    sName = "NumberFormat"; break;
        case PROP_NAME                  :    sName = "Name"; break;
        case PROP_IS_INPUT              :    sName = "IsInput"; break;
diff --git a/writerfilter/source/dmapper/PropertyIds.hxx b/writerfilter/source/dmapper/PropertyIds.hxx
index a6afe0c..f1bef16 100644
--- a/writerfilter/source/dmapper/PropertyIds.hxx
+++ b/writerfilter/source/dmapper/PropertyIds.hxx
@@ -185,6 +185,7 @@ enum PropertyIds
        ,PROP_HORI_ORIENT_POSITION
        ,PROP_HORI_ORIENT_RELATION
        ,PROP_HYPER_LINK_U_R_L
        ,PROP_HYPERLINK
        ,PROP_INDENT_AT
        ,PROP_INPUT_STREAM
        ,PROP_INSERT
diff --git a/xmloff/source/draw/shapeexport.cxx b/xmloff/source/draw/shapeexport.cxx
index 105281b..2c757e2 100644
--- a/xmloff/source/draw/shapeexport.cxx
+++ b/xmloff/source/draw/shapeexport.cxx
@@ -167,6 +167,7 @@ constexpr OUStringLiteral gsVerb( u"Verb" );
constexpr OUStringLiteral gsSoundURL( u"SoundURL" );
constexpr OUStringLiteral gsSpeed( u"Speed" );
constexpr OUStringLiteral gsStarBasic( u"StarBasic" );
constexpr OUStringLiteral gsHyperlink( u"Hyperlink" );

XMLShapeExport::XMLShapeExport(SvXMLExport& rExp,
                                SvXMLExportPropertyMapper *pExtMapper )
@@ -578,17 +579,30 @@ void XMLShapeExport::exportShape(const uno::Reference< drawing::XShape >& xShape
    }
    sal_Int32 nZIndex = 0;
    uno::Reference< beans::XPropertySet > xSet( xShape, uno::UNO_QUERY );
    OUString sHyperlink;
    try
    {
        xSet->getPropertyValue(gsHyperlink) >>= sHyperlink;
    }
    catch (beans::UnknownPropertyException)
    {
    }

    std::unique_ptr< SvXMLElementExport >  pHyperlinkElement;

    // export hyperlinks with <a><shape/></a>. Currently only in draw since draw
    // does not support document events
    // Need to stash the attributes that are pre-loaded for the shape export
    // (otherwise they will become attributes of the draw:a element)
    uno::Reference<xml::sax::XAttributeList> xSaveAttribs(
        new SvXMLAttributeList(GetExport().GetAttrList()));
    GetExport().ClearAttrList();
    if( xSet.is() && (GetExport().GetModelType() == SvtModuleOptions::EFactory::DRAW) )
    {
        // export hyperlinks with <a><shape/></a>. Currently only in draw since draw
        // does not support document events
        try
        {
            presentation::ClickAction eAction = presentation::ClickAction_NONE;
            xSet->getPropertyValue("OnClick") >>= eAction;
            xSet->getPropertyValue(gsOnClick) >>= eAction;

            if( (eAction == presentation::ClickAction_DOCUMENT) ||
                (eAction == presentation::ClickAction_BOOKMARK) )
@@ -610,6 +624,14 @@ void XMLShapeExport::exportShape(const uno::Reference< drawing::XShape >& xShape
            TOOLS_WARN_EXCEPTION("xmloff", "XMLShapeExport::exportShape(): exception during hyperlink export");
        }
    }
    else if (xSet.is() && !sHyperlink.isEmpty())
    {
        mrExport.AddAttribute( XML_NAMESPACE_XLINK, XML_HREF, sHyperlink );
        mrExport.AddAttribute( XML_NAMESPACE_XLINK, XML_TYPE, XML_SIMPLE );
        pHyperlinkElement.reset( new SvXMLElementExport(mrExport, XML_NAMESPACE_DRAW, XML_A, true, true) );
    }
    // re-add stashed attributes
    GetExport().AddAttributeList(xSaveAttribs);

    if( xSet.is() )
        xSet->getPropertyValue(gsZIndex) >>= nZIndex;
diff --git a/xmloff/source/text/XMLTextFrameContext.cxx b/xmloff/source/text/XMLTextFrameContext.cxx
index 39bbf59..a4f8cb6 100644
--- a/xmloff/source/text/XMLTextFrameContext.cxx
+++ b/xmloff/source/text/XMLTextFrameContext.cxx
@@ -1648,6 +1648,10 @@ css::uno::Reference< css::xml::sax::XFastContextHandler > XMLTextFrameContext::c
        }
        xContext = &dynamic_cast<SvXMLImportContext&>(*m_xImplContext->createFastChildContext(nElement, xAttrList));
    }
    else if (nElement == XML_ELEMENT(DRAW, XML_A))
    {
        xContext = &dynamic_cast<SvXMLImportContext&>(*m_xImplContext->createFastChildContext(nElement, xAttrList));
    }
    else
    {
        // the child is a drawing shape
diff --git a/xmloff/source/text/XMLTextFrameHyperlinkContext.cxx b/xmloff/source/text/XMLTextFrameHyperlinkContext.cxx
index 240d977..f3bf9c6 100644
--- a/xmloff/source/text/XMLTextFrameHyperlinkContext.cxx
+++ b/xmloff/source/text/XMLTextFrameHyperlinkContext.cxx
@@ -20,6 +20,7 @@
#include <sal/log.hxx>
#include <sax/tools/converter.hxx>

#include <xmloff/shapeimport.hxx>
#include <xmloff/xmlimp.hxx>
#include <xmloff/namespacemap.hxx>
#include <xmloff/xmlnamespace.hxx>
@@ -27,6 +28,9 @@
#include "XMLTextFrameContext.hxx"
#include "XMLTextFrameHyperlinkContext.hxx"

#include <com/sun/star/drawing/XShapes.hpp>

using namespace ::com::sun::star::drawing;
using namespace ::com::sun::star::uno;
using namespace ::com::sun::star::text;
using namespace ::com::sun::star::xml::sax;
@@ -106,6 +110,14 @@ css::uno::Reference< css::xml::sax::XFastContextHandler > XMLTextFrameHyperlinkC
        pContext = pTextFrameContext;
        xFrameContext = pContext;
    }
    if (nElement == XML_ELEMENT(DRAW, XML_CUSTOM_SHAPE))
    {
        Reference<XShapes> xShapes;
        SvXMLShapeContext* pShapeContext
            = XMLShapeImportHelper::CreateGroupChildContext(GetImport(), nElement, xAttrList, xShapes);
        pShapeContext->setHyperlink(sHRef);
        pContext = pShapeContext;
    }

    if (!pContext)
        XMLOFF_WARN_UNKNOWN_ELEMENT("xmloff", nElement);
diff --git a/xmloff/source/text/XMLTextFrameHyperlinkContext.hxx b/xmloff/source/text/XMLTextFrameHyperlinkContext.hxx
index 3b178bb..a760816 100644
--- a/xmloff/source/text/XMLTextFrameHyperlinkContext.hxx
+++ b/xmloff/source/text/XMLTextFrameHyperlinkContext.hxx
@@ -29,6 +29,7 @@ namespace com::sun::star {
    namespace beans { class XPropertySet; }
}

/// Used for hyperlinks attached to objects (drawing objects, text boxes, Writer frames)
class XMLTextFrameHyperlinkContext : public SvXMLImportContext
{
    OUString              sHRef;