tdf#143574 sw: textboxes in group shapes - part 3 take 2

In this part, missing parameters have been fixed, and
queryInterface method can handle textbox groups now.

A new function, synchronizeGroupTextBoxProperty has been
introduced to do the sync for all group members. Nested
group textbox shape handling also has been introduced.

Note: Copy still has issues.

Change-Id: I3d2090fe6a4066edfd2cb417b30bca9dd9acb011
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/126052
Tested-by: László Németh <nemeth@numbertext.org>
Reviewed-by: László Németh <nemeth@numbertext.org>
diff --git a/sw/inc/textboxhelper.hxx b/sw/inc/textboxhelper.hxx
index 2e5b27c..924b3e6 100644
--- a/sw/inc/textboxhelper.hxx
+++ b/sw/inc/textboxhelper.hxx
@@ -65,7 +65,8 @@ public:
    /// to the given pObject shape.
    static void destroy(const SwFrameFormat* pShape, const SdrObject* pObject);
    /// Get interface of a shape's TextBox, if there is any.
    static css::uno::Any queryInterface(const SwFrameFormat* pShape, const css::uno::Type& rType);
    static css::uno::Any queryInterface(const SwFrameFormat* pShape, const css::uno::Type& rType,
                                        SdrObject* pObj);

    /// Sync property of TextBox with the one of the shape.
    static void syncProperty(SwFrameFormat* pShape, sal_uInt16 nWID, sal_uInt8 nMemberID,
@@ -107,6 +108,9 @@ public:
    /// this function, and returns true in that case too.
    static std::optional<bool> isAnchorTypeDifferent(const SwFrameFormat* pShape);

    /// Sets the correct size of textframe depending on the given SdrObject.
    static bool syncTextBoxSize(SwFrameFormat* pShape, SdrObject* pObj);

    /// Returns true if the given shape has a valid textframe.
    static bool isTextBoxShapeHasValidTextFrame(const SwFrameFormat* pShape);

@@ -177,6 +181,14 @@ public:
    /// Undo the effect of saveLinks() + individual resetLink() calls.
    static void restoreLinks(std::set<ZSortFly>& rOld, std::vector<SwFrameFormat*>& rNew,
                             SavedLink& rSavedLinks);

    /// Calls the method given by pFunc with every textboxes of the group given by pFormat.
    static void synchronizeGroupTextBoxProperty(bool pFunc(SwFrameFormat*, SdrObject*),
                                                SwFrameFormat* pFormat, SdrObject* pObj);
    /// Collect all textboxes of the group given by the pGoupObj Parameter. Returns with a
    /// vector filled with the textboxes.
    static std::vector<SwFrameFormat*> CollectTextBoxes(SdrObject* pGroupObject,
                                                        SwFrameFormat* pFormat);
};

/// Textboxes are basically textframe + shape pairs. This means one shape has one frame.
@@ -242,6 +254,8 @@ public:
    SwFrameFormat* GetOwnerShape() { return m_pOwnerShapeFormat; };
    // This will give the current number of textboxes.
    size_t GetTextBoxCount() const { return m_pTextBoxes.size(); };
    // Returns with a const collection of textboxes owned by this node.
    std::map<SdrObject*, SwFrameFormat*> GetAllTextBoxes() const;
};

#endif // INCLUDED_SW_INC_TEXTBOXHELPER_HXX
diff --git a/sw/qa/uitest/data/ComplexGroupShapeTest.odt b/sw/qa/uitest/data/ComplexGroupShapeTest.odt
new file mode 100644
index 0000000..8fe0932
--- /dev/null
+++ b/sw/qa/uitest/data/ComplexGroupShapeTest.odt
Binary files differ
diff --git a/sw/qa/uitest/writer_tests2/ComplexGroupShapeTest.py b/sw/qa/uitest/writer_tests2/ComplexGroupShapeTest.py
new file mode 100644
index 0000000..7e219d8
--- /dev/null
+++ b/sw/qa/uitest/writer_tests2/ComplexGroupShapeTest.py
@@ -0,0 +1,127 @@
# -*- tab-width: 4; indent-tabs-mode: nil; py-indent-offset: 4 -*-
#
# This file is part of the LibreOffice project.
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
#

from uitest.framework import UITestCase
from uitest.uihelper.common import get_state_as_dict
from uitest.uihelper.common import select_pos
from uitest.uihelper.common import get_url_for_data_file
from libreoffice.uno.propertyvalue import mkPropertyValues
import time

class ComplexGroupShapeTest(UITestCase):
    def test_ComplexGroupShape(self):
        with self.ui_test.load_file(get_url_for_data_file("ComplexGroupShapeTest.odt")):
            xWriterDoc = self.xUITest.getTopFocusWindow()
            xWriterEdit = xWriterDoc.getChild("writer_edit")
            document = self.ui_test.get_component()

            # check the shape type
            self.assertEqual("com.sun.star.drawing.GroupShape", document.DrawPage.getByIndex(1).ShapeType)

            # select the shape
            self.xUITest.executeCommand(".uno:JumpToNextFrame")
            self.ui_test.wait_until_child_is_available('metricfield')

            # go inside the group
            self.xUITest.executeCommand(".uno:EnterGroup")

            # select a shape in the group
            xWriterEdit.executeAction("TYPE", mkPropertyValues({"KEYCODE": "TAB"}))

            # add a textbox to this subshape
            self.xUITest.executeCommand(".uno:AddTextBox")

            # select the next shape in the group
            xWriterEdit.executeAction("TYPE", mkPropertyValues({"KEYCODE": "TAB"}))

            # add a textbox to this subshape
            self.xUITest.executeCommand(".uno:AddTextBox")

            # leave the groupshape
            self.xUITest.executeCommand(".uno:LeaveGroup")

            # select the other shape
            self.xUITest.executeCommand(".uno:JumpToNextFrame")
            self.ui_test.wait_until_child_is_available('metricfield')

            # get the current selection
            ShapeCollection = document.getCurrentSelection()

            # extend the selection with the grouped shape
            ShapeCollection.add(document.DrawPage.getByIndex(0))
            ShapeCollection.add(document.DrawPage.getByIndex(1))

            # select these shapes
            document.getCurrentController().select(ShapeCollection)

            # do ungroup
            self.xUITest.executeCommand(".uno:FormatGroup")

            # deselect
            xWriterEdit.executeAction("TYPE", mkPropertyValues({"KEYCODE":"ESC"}))
            time.sleep(0.1)

            # select the group
            self.xUITest.executeCommand(".uno:JumpToNextFrame")
            self.ui_test.wait_until_child_is_available('metricfield')

            # move it down
            for i in range(1, 30):
                xWriterEdit.executeAction("TYPE", mkPropertyValues({"KEYCODE":"DOWN"}))
                time.sleep(0.1)

            # select again
            self.xUITest.executeCommand(".uno:JumpToNextFrame")
            self.ui_test.wait_until_child_is_available('metricfield')

            # do ungroup
            self.xUITest.executeCommand(".uno:FormatUngroup")

            # deselect everything
            xWriterEdit.executeAction("TYPE", mkPropertyValues({"KEYCODE":"ESC"}))
            time.sleep(0.1)

            # select the first ex-group member shape
            self.xUITest.executeCommand(".uno:JumpToNextFrame")
            self.ui_test.wait_until_child_is_available('metricfield')
            xWriterEdit.executeAction("TYPE", mkPropertyValues({"KEYCODE": "TAB"}))

            # check if it is a textbox
            self.assertEqual(True,document.getCurrentSelection().getByIndex(0).TextBox)

            # go to the other one
            xWriterEdit.executeAction("TYPE", mkPropertyValues({"KEYCODE": "TAB"}))
            xWriterEdit.executeAction("TYPE", mkPropertyValues({"KEYCODE": "TAB"}))

            # this is still a group, so it cannot be a textbox
            self.assertEqual(False,document.getCurrentSelection().getByIndex(0).TextBox)

            # do ungroup
            self.xUITest.executeCommand(".uno:FormatUngroup")

            # deselect
            xWriterEdit.executeAction("TYPE", mkPropertyValues({"KEYCODE":"ESC"}))
            time.sleep(0.1)

            # select one shape of the last group
            self.xUITest.executeCommand(".uno:JumpToNextFrame")
            self.ui_test.wait_until_child_is_available('metricfield')
            xWriterEdit.executeAction("TYPE", mkPropertyValues({"KEYCODE": "TAB"}))
            xWriterEdit.executeAction("TYPE", mkPropertyValues({"KEYCODE": "TAB"}))
            xWriterEdit.executeAction("TYPE", mkPropertyValues({"KEYCODE": "TAB"}))

            # check if it is a textbox
            self.assertEqual(True,document.getCurrentSelection().getByIndex(0).TextBox)

            # Without the fix in place, the following problems occurred during this test:
            # - After the grouping old textbox frames detached from their shape before
            # - Moving caused messed layout
            # - After ungroup, the shapes in the embed group lost their textbox

# vim: set shiftwidth=4 softtabstop=4 expandtab:
diff --git a/sw/source/core/doc/docdraw.cxx b/sw/source/core/doc/docdraw.cxx
index 0aff4b89..6445ab7 100644
--- a/sw/source/core/doc/docdraw.cxx
+++ b/sw/source/core/doc/docdraw.cxx
@@ -209,7 +209,7 @@ SwDrawContact* SwDoc::GroupSelection( SdrView& rDrawView )
            bGroupMembersNotPositioned = pAnchoredDrawObj->NotYetPositioned();
        }

        std::vector<std::pair<SwFrameFormat*, SdrObject*>> vSavedTextBoxes;
        std::map<const SdrObject*, SwFrameFormat*> vSavedTextBoxes;
        // Destroy ContactObjects and formats.
        for( size_t i = 0; i < rMrkList.GetMarkCount(); ++i )
        {
@@ -224,8 +224,10 @@ SwDrawContact* SwDoc::GroupSelection( SdrView& rDrawView )
                    "<SwDoc::GroupSelection(..)> - group members have different positioning status!" );
#endif
            // Before the format will be killed, save its textbox for later use.
            if (auto pTextBox = SwTextBoxHelper::getOtherTextBoxFormat(pContact->GetFormat(), RES_DRAWFRMFMT, pObj))
                vSavedTextBoxes.push_back(std::pair<SwFrameFormat*, SdrObject*>(pTextBox, pObj));
            if (auto pShapeFormat = pContact->GetFormat())
                if (auto pTextBoxNode = pShapeFormat->GetOtherTextBoxFormat())
                    for (const auto& rTextBoxElement : pTextBoxNode->GetAllTextBoxes())
                        vSavedTextBoxes.emplace(rTextBoxElement);

            pFormat = static_cast<SwDrawFrameFormat*>(pContact->GetFormat());
            // Deletes itself!
@@ -254,10 +256,11 @@ SwDrawContact* SwDoc::GroupSelection( SdrView& rDrawView )

        // Add the saved textboxes to the new format.
        auto pTextBoxNode = new SwTextBoxNode(pFormat);
        for (auto& pTextBoxEntry : vSavedTextBoxes)
        for (const auto& pTextBoxEntry : vSavedTextBoxes)
        {
            pTextBoxNode->AddTextBox(pTextBoxEntry.second, pTextBoxEntry.first);
            pTextBoxEntry.first->SetOtherTextBoxFormat(pTextBoxNode);
            pTextBoxNode->AddTextBox(const_cast<SdrObject*>(pTextBoxEntry.first),
                                     pTextBoxEntry.second);
            pTextBoxEntry.second->SetOtherTextBoxFormat(pTextBoxNode);
        }
        pFormat->SetOtherTextBoxFormat(pTextBoxNode);
        vSavedTextBoxes.clear();
@@ -299,6 +302,27 @@ SwDrawContact* SwDoc::GroupSelection( SdrView& rDrawView )
    return pNewContact;
}

static void lcl_CollectTextBoxesForSubGroupObj(SwFrameFormat* pTargetFormat, SwTextBoxNode* pTextBoxNode,
                                               SdrObject* pSourceObjs)
{
    if (auto pChildrenObjs = pSourceObjs->getChildrenOfSdrObject())
        for (size_t i = 0; i < pChildrenObjs->GetObjCount(); ++i)
            lcl_CollectTextBoxesForSubGroupObj(pTargetFormat, pTextBoxNode, pChildrenObjs->GetObj(i));
    else
    {
        if (auto pTextBox = pTextBoxNode->GetTextBox(pSourceObjs))
        {
            if (!pTargetFormat->GetOtherTextBoxFormat())
            {
                pTargetFormat->SetOtherTextBoxFormat(new SwTextBoxNode(pTargetFormat));
            }
            pTargetFormat->GetOtherTextBoxFormat()->AddTextBox(pSourceObjs, pTextBox);
            pTextBox->SetOtherTextBoxFormat(pTargetFormat->GetOtherTextBoxFormat());
        }
    }
}


void SwDoc::UnGroupSelection( SdrView& rDrawView )
{
    bool const bUndo = GetIDocumentUndoRedo().DoesUndo();
@@ -349,14 +373,22 @@ void SwDoc::UnGroupSelection( SdrView& rDrawView )
                        pFormat->SetFormatAttr( aAnch );

                        if (pTextBoxNode)
                            if (auto pTextBoxFormat = pTextBoxNode->GetTextBox(pSubObj))
                        {
                            if (!pObj->getChildrenOfSdrObject())
                            {
                                auto pNewTextBoxNode = new SwTextBoxNode(pFormat);
                                pNewTextBoxNode->AddTextBox(pSubObj, pTextBoxFormat);
                                pFormat->SetOtherTextBoxFormat(pNewTextBoxNode);
                                pTextBoxFormat->SetOtherTextBoxFormat(pNewTextBoxNode);
                                if (auto pTextBoxFormat = pTextBoxNode->GetTextBox(pSubObj))
                                {
                                    auto pNewTextBoxNode = new SwTextBoxNode(pFormat);
                                    pNewTextBoxNode->AddTextBox(pSubObj, pTextBoxFormat);
                                    pFormat->SetOtherTextBoxFormat(pNewTextBoxNode);
                                    pTextBoxFormat->SetOtherTextBoxFormat(pNewTextBoxNode);
                                }
                            }

                            else
                            {
                                lcl_CollectTextBoxesForSubGroupObj(pFormat, pTextBoxNode, pSubObj);
                            }
                        }
                        // #i36010# - set layout direction of the position
                        pFormat->SetPositionLayoutDir(
                            text::PositionLayoutDir::PositionInLayoutDirOfAnchor );
diff --git a/sw/source/core/doc/textboxhelper.cxx b/sw/source/core/doc/textboxhelper.cxx
index 418c699..707cc5d 100644
--- a/sw/source/core/doc/textboxhelper.cxx
+++ b/sw/source/core/doc/textboxhelper.cxx
@@ -367,7 +367,8 @@ SwFrameFormat* SwTextBoxHelper::getOtherTextBoxFormat(uno::Reference<drawing::XS
        return nullptr;

    SwFrameFormat* pFormat = pShape->GetFrameFormat();
    return getOtherTextBoxFormat(pFormat, RES_DRAWFRMFMT);
    return getOtherTextBoxFormat(pFormat, RES_DRAWFRMFMT,
                                 SdrObject::getSdrObjectFromXShape(xShape));
}

uno::Reference<text::XTextFrame>
@@ -388,9 +389,11 @@ SwTextBoxHelper::getUnoTextFrame(uno::Reference<drawing::XShape> const& xShape)
    return {};
}

template <typename T> static void lcl_queryInterface(const SwFrameFormat* pShape, uno::Any& rAny)
template <typename T>
static void lcl_queryInterface(const SwFrameFormat* pShape, uno::Any& rAny, SdrObject* pObj)
{
    if (SwFrameFormat* pFormat = SwTextBoxHelper::getOtherTextBoxFormat(pShape, RES_DRAWFRMFMT))
    if (SwFrameFormat* pFormat
        = SwTextBoxHelper::getOtherTextBoxFormat(pShape, RES_DRAWFRMFMT, pObj))
    {
        uno::Reference<T> const xInterface(
            SwXTextFrame::CreateXTextFrame(*pFormat->GetDoc(), pFormat), uno::UNO_QUERY);
@@ -398,21 +401,22 @@ template <typename T> static void lcl_queryInterface(const SwFrameFormat* pShape
    }
}

uno::Any SwTextBoxHelper::queryInterface(const SwFrameFormat* pShape, const uno::Type& rType)
uno::Any SwTextBoxHelper::queryInterface(const SwFrameFormat* pShape, const uno::Type& rType,
                                         SdrObject* pObj)
{
    uno::Any aRet;

    if (rType == cppu::UnoType<css::text::XTextAppend>::get())
    {
        lcl_queryInterface<text::XTextAppend>(pShape, aRet);
        lcl_queryInterface<text::XTextAppend>(pShape, aRet, pObj);
    }
    else if (rType == cppu::UnoType<css::text::XText>::get())
    {
        lcl_queryInterface<text::XText>(pShape, aRet);
        lcl_queryInterface<text::XText>(pShape, aRet, pObj);
    }
    else if (rType == cppu::UnoType<css::text::XTextRange>::get())
    {
        lcl_queryInterface<text::XTextRange>(pShape, aRet);
        lcl_queryInterface<text::XTextRange>(pShape, aRet, pObj);
    }

    return aRet;
@@ -1308,6 +1312,25 @@ std::optional<bool> SwTextBoxHelper::isAnchorTypeDifferent(const SwFrameFormat* 
    return bRet;
}

bool SwTextBoxHelper::syncTextBoxSize(SwFrameFormat* pShape, SdrObject* pObj)
{
    if (!pShape || !pObj)
        return false;

    if (auto pTextBox = getOtherTextBoxFormat(pShape, RES_DRAWFRMFMT, pObj))
    {
        const auto& rSize = getTextRectangle(pObj, false).GetSize();
        if (!rSize.IsEmpty())
        {
            SwFormatFrameSize aSize(pTextBox->GetFrameSize());
            aSize.SetSize(rSize);
            return pTextBox->SetFormatAttr(aSize);
        }
    }

    return false;
}

bool SwTextBoxHelper::isTextBoxShapeHasValidTextFrame(const SwFrameFormat* pShape)
{
    if (pShape && pShape->Which() == RES_DRAWFRMFMT)
@@ -1382,6 +1405,41 @@ bool SwTextBoxHelper::DoTextBoxZOrderCorrection(SwFrameFormat* pShape, const Sdr
    return false;
}

void SwTextBoxHelper::synchronizeGroupTextBoxProperty(bool pFunc(SwFrameFormat*, SdrObject*),
                                                      SwFrameFormat* pShape, SdrObject* pObj)
{
    if (auto pChildren = pObj->getChildrenOfSdrObject())
    {
        for (size_t i = 0; i < pChildren->GetObjCount(); ++i)
            synchronizeGroupTextBoxProperty(pFunc, pShape, pChildren->GetObj(i));
    }
    else
    {
        (*pFunc)(pShape, pObj);
    }
}

std::vector<SwFrameFormat*> SwTextBoxHelper::CollectTextBoxes(SdrObject* pGroupObject,
                                                              SwFrameFormat* pFormat)
{
    std::vector<SwFrameFormat*> vRet;
    if (auto pChildren = pGroupObject->getChildrenOfSdrObject())
    {
        for (size_t i = 0; i < pChildren->GetObjCount(); ++i)
        {
            auto pChildTextBoxes = CollectTextBoxes(pChildren->GetObj(i), pFormat);
            for (auto& rChildTextBox : pChildTextBoxes)
                vRet.push_back(rChildTextBox);
        }
    }
    else
    {
        if (isTextBox(pFormat, RES_DRAWFRMFMT, pGroupObject))
            vRet.push_back(getOtherTextBoxFormat(pFormat, RES_DRAWFRMFMT, pGroupObject));
    }
    return vRet;
}

SwTextBoxNode::SwTextBoxNode(SwFrameFormat* pOwnerShape)
{
    assert(pOwnerShape);
@@ -1505,4 +1563,14 @@ void SwTextBoxNode::SetTextBoxInactive(const SdrObject* pDrawObject)

bool SwTextBoxNode::IsGroupTextBox() const { return m_pTextBoxes.size() > 1; }

std::map<SdrObject*, SwFrameFormat*> SwTextBoxNode::GetAllTextBoxes() const
{
    std::map<SdrObject*, SwFrameFormat*> aRet;
    for (auto& rElem : m_pTextBoxes)
    {
        aRet.emplace(rElem.m_pDrawObject, rElem.m_pTextBoxFormat);
    }
    return aRet;
}

/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/sw/source/core/draw/dcontact.cxx b/sw/source/core/draw/dcontact.cxx
index 984cacf..48c29c5 100644
--- a/sw/source/core/draw/dcontact.cxx
+++ b/sw/source/core/draw/dcontact.cxx
@@ -1250,10 +1250,8 @@ void SwDrawContact::Changed_( const SdrObject& rObj,
                    // use geometry of drawing object
                    aObjRect = pGroupObj->GetSnapRect();

                    for (size_t i = 0; i < pGroupObj->getChildrenOfSdrObject()->GetObjCount(); ++i )
                    {
                        SwTextBoxHelper::doTextBoxPositioning(GetFormat(), pGroupObj->getChildrenOfSdrObject()->GetObj(i));
                    }
                    SwTextBoxHelper::synchronizeGroupTextBoxProperty(&SwTextBoxHelper::changeAnchor, GetFormat(), &const_cast<SdrObject&>(rObj));
                    SwTextBoxHelper::synchronizeGroupTextBoxProperty(&SwTextBoxHelper::syncTextBoxSize, GetFormat(), &const_cast<SdrObject&>(rObj));

                }
                SwTwips nXPosDiff(0);
diff --git a/sw/source/core/draw/dview.cxx b/sw/source/core/draw/dview.cxx
index 510addf..12b955d 100644
--- a/sw/source/core/draw/dview.cxx
+++ b/sw/source/core/draw/dview.cxx
@@ -967,12 +967,11 @@ void SwDrawView::DeleteMarked()
        SdrObject *pObject = rMarkList.GetMark(i)->GetMarkedSdrObj();
        SwContact* pContact = GetUserCall(pObject);
        SwFrameFormat* pFormat = pContact->GetFormat();
        if (auto pChildren = pObject->getChildrenOfSdrObject())
        if (pObject->getChildrenOfSdrObject())
        {
            for (size_t it = 0; it < pChildren->GetObjCount(); ++it)
                if (SwFrameFormat* pTextBox = SwTextBoxHelper::getOtherTextBoxFormat(
                        pFormat, RES_DRAWFRMFMT, pChildren->GetObj(it)))
                    aTextBoxesToDelete.push_back(pTextBox);
            auto pChildTextBoxes = SwTextBoxHelper::CollectTextBoxes(pObject, pFormat);
            for (auto& rChildTextBox : pChildTextBoxes)
                aTextBoxesToDelete.push_back(rChildTextBox);
        }
        else
            if (SwFrameFormat* pTextBox = SwTextBoxHelper::getOtherTextBoxFormat(pFormat, RES_DRAWFRMFMT))
diff --git a/sw/source/core/unocore/unodraw.cxx b/sw/source/core/unocore/unodraw.cxx
index 4d04857..af5b74b 100644
--- a/sw/source/core/unocore/unodraw.cxx
+++ b/sw/source/core/unocore/unodraw.cxx
@@ -963,10 +963,19 @@ SwXShape::~SwXShape()

uno::Any SwXShape::queryInterface( const uno::Type& aType )
{
    uno::Any aRet = SwTextBoxHelper::queryInterface(GetFrameFormat(), aType);
    if (aRet.hasValue())
        return aRet;
    uno::Any aRet;
    SdrObject* pObj = nullptr;

    if ((aType == cppu::UnoType<text::XText>::get())
        || (aType == cppu::UnoType<text::XTextRange>::get())
        || (aType == cppu::UnoType<text::XTextAppend>::get()))
    {
        pObj = SdrObject::getSdrObjectFromXShape(mxShape);

        aRet = SwTextBoxHelper::queryInterface(GetFrameFormat(), aType, pObj);
        if (aRet.hasValue())
            return aRet;
    }
    aRet = SwXShapeBaseClass::queryInterface(aType);
    // #i53320# - follow-up of #i31698#
    // interface drawing::XShape is overloaded. Thus, provide