tdf#138141 sw: fix textbox z-order

Textboxes are implemented as loosely connected
shape-text frame pairs. Missing synchronization
of their z-orders resulted e.g invisible or
only partially visible textbox content using
Arrange options with textboxes (see in local menu
or on Drawing Object Properties toolbar).

Note: because it's not possible to send frames
to the background, Arrange->To Background hasn't
supported, so likely it's worth to remove that
option later from local menu of textboxes.

Change-Id: I1aa50903ba55dd5b9e72ef203c4e30218bee68fb
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/115227
Tested-by: László Németh <nemeth@numbertext.org>
Reviewed-by: László Németh <nemeth@numbertext.org>
diff --git a/svx/source/svdraw/svdpage.cxx b/svx/source/svdraw/svdpage.cxx
index adcbb8f..c1d412a 100644
--- a/svx/source/svdraw/svdpage.cxx
+++ b/svx/source/svdraw/svdpage.cxx
@@ -769,7 +769,10 @@ size_t SdrObjList::GetObjCount() const

SdrObject* SdrObjList::GetObj(size_t nNum) const
{
    return maList[nNum];
    if (nNum < maList.size())
        return maList[nNum];

    return nullptr;
}


diff --git a/sw/inc/textboxhelper.hxx b/sw/inc/textboxhelper.hxx
index 0db8044..1c9fdc6 100644
--- a/sw/inc/textboxhelper.hxx
+++ b/sw/inc/textboxhelper.hxx
@@ -107,6 +107,10 @@ public:
    /// Returns true if the given shape has a valid textframe.
    static bool isTextBoxShapeHasValidTextFrame(SwFrameFormat* pShape);

    // Returns true on success. Synchronize z-order of the text frame of the given textbox
    // by setting it one level higher than the z-order of the shape of the textbox.
    static bool DoTextBoxZOrderCorrection(SwFrameFormat* pShape);

    /**
     * If we have an associated TextFrame, then return that.
     *
diff --git a/sw/qa/extras/ooxmlexport/data/testTextBoxZOrder.docx b/sw/qa/extras/ooxmlexport/data/testTextBoxZOrder.docx
new file mode 100644
index 0000000..c6dd0b8
--- /dev/null
+++ b/sw/qa/extras/ooxmlexport/data/testTextBoxZOrder.docx
Binary files differ
diff --git a/sw/qa/extras/ooxmlexport/ooxmlexport16.cxx b/sw/qa/extras/ooxmlexport/ooxmlexport16.cxx
index 16cd111..165f104 100644
--- a/sw/qa/extras/ooxmlexport/ooxmlexport16.cxx
+++ b/sw/qa/extras/ooxmlexport/ooxmlexport16.cxx
@@ -26,6 +26,7 @@
#include <com/sun/star/text/XTextViewCursorSupplier.hpp>
#include <com/sun/star/text/XTextTable.hpp>
#include <com/sun/star/text/XTextTablesSupplier.hpp>
#include <com/sun/star/text/XTextFrame.hpp>
#include <com/sun/star/packages/zip/ZipFileAccess.hpp>
#include <comphelper/configuration.hxx>
#include <comphelper/propertysequence.hxx>
@@ -338,6 +339,36 @@ DECLARE_OOXMLEXPORT_TEST(testTdf133473_shadowSize, "tdf133473.docx")
    CPPUNIT_ASSERT_EQUAL(static_cast<sal_Int32>(200000), nSize1);
}

DECLARE_OOXMLEXPORT_TEST(testTextBoxZOrder, "testTextBoxZOrder.docx")
{
    // Is load successful?
    CPPUNIT_ASSERT(mxComponent);
    // Collect the z-order values of the textboxes
    std::vector<sal_uInt64> ShapeZorders;
    std::vector<sal_uInt64> FrameZorders;
    for (int i = 1; i < 4; i++)
    {
        uno::Reference<drawing::XShape> xShape(getShape(i));
        CPPUNIT_ASSERT(xShape);
        uno::Reference<beans::XPropertySet> xShapeProperties(xShape, uno::UNO_QUERY);
        CPPUNIT_ASSERT(xShapeProperties);
        uno::Reference<text::XTextFrame> xFrame = SwTextBoxHelper::getUnoTextFrame(xShape);
        CPPUNIT_ASSERT(xFrame.is());
        uno::Reference<beans::XPropertySet> const xFrameProperties(xFrame, uno::UNO_QUERY);
        CPPUNIT_ASSERT(xFrameProperties);
        ShapeZorders.push_back(xShapeProperties->getPropertyValue("ZOrder").get<sal_uInt64>());
        FrameZorders.push_back(xFrameProperties->getPropertyValue("ZOrder").get<sal_uInt64>());
    }
    // Check the z-order values.
    for (int i = 1; i < 3; i++)
    {
        CPPUNIT_ASSERT_GREATER(ShapeZorders[i - 1], ShapeZorders[i]);
        CPPUNIT_ASSERT_GREATER(FrameZorders[i - 1], FrameZorders[i]);
        CPPUNIT_ASSERT_GREATER(ShapeZorders[i - 1], FrameZorders[i - 1]);
    }
    // Without the fix it failed, because the z-order was wrong.
}

DECLARE_OOXMLEXPORT_TEST(testTdf141550, "tdf141550.docx")
{
    uno::Reference<drawing::XShape> xShape(getShape(1));
diff --git a/sw/source/core/doc/textboxhelper.cxx b/sw/source/core/doc/textboxhelper.cxx
index cba67e1..f901bea 100644
--- a/sw/source/core/doc/textboxhelper.cxx
+++ b/sw/source/core/doc/textboxhelper.cxx
@@ -38,6 +38,9 @@
#include <sal/log.hxx>
#include <tools/UnitConversion.hxx>
#include <svx/swframetypes.hxx>
#include <drawdoc.hxx>
#include <IDocumentUndoRedo.hxx>
#include <DocumentDrawModelManager.hxx>

#include <com/sun/star/document/XActionLockable.hpp>
#include <com/sun/star/lang/IndexOutOfBoundsException.hpp>
@@ -135,6 +138,7 @@ void SwTextBoxHelper::create(SwFrameFormat* pShape, bool bCopyText)
        pShape->SetFormatAttr(aSet);
    }

    DoTextBoxZOrderCorrection(pShape);
    // Also initialize the properties, which are not constant, but inherited from the shape's ones.
    uno::Reference<drawing::XShape> xShape(pShape->FindRealSdrObject()->getUnoShape(),
                                           uno::UNO_QUERY);
@@ -1016,6 +1020,8 @@ void SwTextBoxHelper::syncFlyFrameAttr(SwFrameFormat& rShape, SfxItemSet const& 

    if (aTextBoxSet.Count())
        pFormat->GetDoc()->SetFlyFrameAttr(*pFormat, aTextBoxSet);

    DoTextBoxZOrderCorrection(&rShape);
}

void SwTextBoxHelper::updateTextBoxMargin(SdrObject* pObj)
@@ -1065,6 +1071,7 @@ bool SwTextBoxHelper::setWrapThrough(SwFrameFormat* pShape)
    {
        if (auto pFormat = getOtherTextBoxFormat(pShape, RES_DRAWFRMFMT))
        {
            ::sw::UndoGuard const UndoGuard(pShape->GetDoc()->GetIDocumentUndoRedo());
            if (auto xFrame = SwXTextFrame::CreateXTextFrame(*pFormat->GetDoc(), pFormat))
                try
                {
@@ -1109,6 +1116,7 @@ bool SwTextBoxHelper::changeAnchor(SwFrameFormat* pShape)
            {
                try
                {
                    ::sw::UndoGuard const UndoGuard(pShape->GetDoc()->GetIDocumentUndoRedo());
                    uno::Reference<beans::XPropertySet> const xPropertySet(
                        SwXTextFrame::CreateXTextFrame(*pFormat->GetDoc(), pFormat),
                        uno::UNO_QUERY);
@@ -1175,7 +1183,7 @@ bool SwTextBoxHelper::changeAnchor(SwFrameFormat* pShape)
                }
            }

            return doTextBoxPositioning(pShape);
            return doTextBoxPositioning(pShape) && DoTextBoxZOrderCorrection(pShape);
        }
    }
    return false;
@@ -1187,6 +1195,7 @@ bool SwTextBoxHelper::doTextBoxPositioning(SwFrameFormat* pShape)
    {
        if (auto pFormat = getOtherTextBoxFormat(pShape, RES_DRAWFRMFMT))
        {
            ::sw::UndoGuard const UndoGuard(pShape->GetDoc()->GetIDocumentUndoRedo());
            if (pShape->GetAnchor().GetAnchorId() == RndStdIds::FLY_AS_CHAR)
            {
                tools::Rectangle aRect(getTextRectangle(pShape, false));
@@ -1249,19 +1258,68 @@ std::optional<bool> SwTextBoxHelper::isAnchorTypeDifferent(SwFrameFormat* pShape

bool SwTextBoxHelper::isTextBoxShapeHasValidTextFrame(SwFrameFormat* pShape)
{
    OUString sErrMsg;
    if (pShape && pShape->Which() == RES_DRAWFRMFMT)
        if (auto pFormat = getOtherTextBoxFormat(pShape, RES_DRAWFRMFMT))
            if (pFormat && pFormat->Which() == RES_FLYFRMFMT)
                return true;
            else
                sErrMsg = "Shape do not have valid textframe!";
                SAL_WARN("sw.core", "SwTextBoxHelper::isTextBoxShapeHasValidTextFrame: "
                                    "Shape does not have valid textframe!");
        else
            sErrMsg = "Shape do not have associated frame!";
            SAL_WARN("sw.core", "SwTextBoxHelper::isTextBoxShapeHasValidTextFrame: "
                                "Shape does not have associated frame!");
    else
        sErrMsg = "Not valid shape!";
        SAL_WARN("sw.core", "SwTextBoxHelper::isTextBoxShapeHasValidTextFrame: Not valid shape!");
    return false;
}

    SAL_WARN("sw.core", "SwTextBoxHelper::isTextBoxShapeHasValidTextFrame: " << sErrMsg);
bool SwTextBoxHelper::DoTextBoxZOrderCorrection(SwFrameFormat* pShape)
{
    if (isTextBoxShapeHasValidTextFrame(pShape))
    {
        if (SdrObject* pShpObj = pShape->FindRealSdrObject())
        {
            if (SdrObject* pFrmObj
                = getOtherTextBoxFormat(pShape, RES_DRAWFRMFMT)->FindRealSdrObject())
            {
                // Get the draw model from the doc
                SwDrawModel* pDrawModel
                    = pShape->GetDoc()->getIDocumentDrawModelAccess().GetDrawModel();
                if (pDrawModel)
                {
                    // Not really sure this will work all page, but it seems it will.
                    auto pPage = pDrawModel->GetPage(0);
                    // Recalc all Zorders
                    pPage->RecalcObjOrdNums();
                    // If the shape is behind the frame, is good, but if there are some objects
                    // between of them that is wrong so put the frame exactly one level higher
                    // than the shape.
                    if (pFrmObj->GetOrdNum() > pShpObj->GetOrdNum())
                        pPage->SetObjectOrdNum(pFrmObj->GetOrdNum(), pShpObj->GetOrdNum() + 1);
                    else
                        // Else, if the frame is behind the shape, bring to the front of it.
                        while (pFrmObj->GetOrdNum() <= pShpObj->GetOrdNum())
                        {
                            pPage->SetObjectOrdNum(pFrmObj->GetOrdNum(), pFrmObj->GetOrdNum() + 1);
                            // If there is any problem with the indexes, do not run over the infinity
                            if (pPage->GetObjCount() == pFrmObj->GetOrdNum())
                                break;
                        }
                    pPage->RecalcObjOrdNums();
                    return true; // Success
                }
                SAL_WARN("sw.core", "SwTextBoxHelper::DoTextBoxZOrderCorrection(): "
                                    "No Valid Draw model for SdrObject for the shape!");
            }
            SAL_WARN("sw.core", "SwTextBoxHelper::DoTextBoxZOrderCorrection(): "
                                "No Valid SdrObject for the frame!");
        }
        SAL_WARN("sw.core", "SwTextBoxHelper::DoTextBoxZOrderCorrection(): "
                            "No Valid SdrObject for the shape!");
    }
    SAL_WARN("sw.core", "SwTextBoxHelper::DoTextBoxZOrderCorrection(): "
                        "No Valid TextFrame!");

    return false;
}

diff --git a/sw/source/core/frmedt/feshview.cxx b/sw/source/core/frmedt/feshview.cxx
index 31c30e3..6f4dbf2 100644
--- a/sw/source/core/frmedt/feshview.cxx
+++ b/sw/source/core/frmedt/feshview.cxx
@@ -1056,6 +1056,55 @@ void SwFEShell::SelectionToTop( bool bTop )
    else
        Imp()->GetDrawView()->MovMarkedToTop();
    ::lcl_NotifyNeighbours( &rMrkList );

    // Does the selection contain a textbox?
    for (size_t i = 0; i < rMrkList.GetMarkCount(); i++)
        if (auto pObj = rMrkList.GetMark(i)->GetMarkedSdrObj())
            // Get the textbox-shape
            if (auto pFormat = FindFrameFormat(pObj))
            {
                // If it has not textframe skip...
                if (!SwTextBoxHelper::isTextBoxShapeHasValidTextFrame(pFormat))
                    continue;
                // If it has a textframe so it is a textbox, get its page
                if (auto pDrwModel
                    = pFormat->GetDoc()->getIDocumentDrawModelAccess().GetDrawModel())
                    // Not really understood why everything is on page 0...
                    // but it is easier to handle sdrobjects, thats true
                    if (auto pPage = pDrwModel->GetPage(0))
                    {
                        // nShift: it means how many layers the pObj have to be shifted up,
                        // in order not to interfere with other shapes and textboxes.
                        // Situations:
                        // - The next shape has textframe: This shape have to shifted with
                        //   two layers.
                        // - The next shape has not got textframe: This shape have to be
                        //   shifted only one layer up.
                        // - The next shape is null:
                        //      - This shape is already at heaven: Only the textframe have
                        //        to be adjusted.
                        sal_uInt32 nShift = 0;
                        // Get the one level higher object (note: can be nullptr!)
                        const auto pNextObj = pPage->SetObjectOrdNum(pObj->GetOrdNum() + 1, pObj->GetOrdNum() + 1);
                        // If there is a higher object (not null)...
                        if (pNextObj)
                        {
                            // One level shift is neccessary
                            nShift++;
                            // If this object is a textbox, two level increasing needed
                            // (one for the shape and one for the frame)
                            if (auto pNextFormat = FindFrameFormat(pNextObj))
                                if (SwTextBoxHelper::isTextBox(pNextFormat, RES_DRAWFRMFMT)
                                    || SwTextBoxHelper::isTextBox(pNextFormat, RES_FLYFRMFMT))
                                    nShift++;
                        }
                        // Set the new z-order.
                        pPage->SetObjectOrdNum(pObj->GetOrdNum(), pObj->GetOrdNum() + nShift);
                    }
                // The shape is on the right level, correct the layer of the frame
                SwTextBoxHelper::DoTextBoxZOrderCorrection(pFormat);
            }

    GetDoc()->getIDocumentState().SetModified();
    EndAllAction();
}
@@ -1076,6 +1125,36 @@ void SwFEShell::SelectionToBottom( bool bBottom )
    else
        Imp()->GetDrawView()->MovMarkedToBtm();
    ::lcl_NotifyNeighbours( &rMrkList );

    // If the selection has textbox
    for(size_t i = 0; i < rMrkList.GetMarkCount(); i++)
        if (auto pObj = rMrkList.GetMark(i)->GetMarkedSdrObj())
            // Get the shape of the textbox
            if (auto pFormat = FindFrameFormat(pObj))
            {
                // If the shape has not textframes skip.
                if (!SwTextBoxHelper::isTextBoxShapeHasValidTextFrame(pFormat))
                    continue;
                // If has, move the shape to correct level with...
                if (auto pDrwModel
                    = pFormat->GetDoc()->getIDocumentDrawModelAccess().GetDrawModel())
                    if (auto pPage = pDrwModel->GetPage(0))
                    {
                        const auto pNextObj = pPage->SetObjectOrdNum(pObj->GetOrdNum() - 1, pObj->GetOrdNum() - 1);
                        // If there is a lower object (not null)...
                        if (pNextObj)
                        {
                            // If the lower has no textframe, just do nothing, else move by one lower
                            if (auto pNextFormat = FindFrameFormat(pNextObj))
                                if (SwTextBoxHelper::isTextBox(pNextFormat, RES_DRAWFRMFMT)
                                    || SwTextBoxHelper::isTextBox(pNextFormat, RES_FLYFRMFMT))
                                    pPage->SetObjectOrdNum(pObj->GetOrdNum(), pObj->GetOrdNum() - 1);
                        }
                    }
                // And set correct layer for the selected textbox.
                SwTextBoxHelper::DoTextBoxZOrderCorrection(pFormat);
            }

    GetDoc()->getIDocumentState().SetModified();
    EndAllAction();
}
@@ -1139,6 +1218,9 @@ void SwFEShell::ChangeOpaque( SdrLayerID nLayerId )
                SvxOpaqueItem aOpa( pFormat->GetOpaque() );
                aOpa.SetValue(  nLayerId == rIDDMA.GetHellId() );
                pFormat->SetFormatAttr( aOpa );
                // If pObj has textframe, put its textframe to the right level
                if (auto pTextBx = FindFrameFormat(pObj))
                    SwTextBoxHelper::DoTextBoxZOrderCorrection(pTextBx);
            }
        }
    }