split .ui conversion code into its own file

Change-Id: Ie27990a497e39ab2fd82a711fa4ec49b472616f6
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/116677
Tested-by: Jenkins
Reviewed-by: Caolán McNamara <caolanm@redhat.com>
diff --git a/vcl/Library_vclplug_gtk4.mk b/vcl/Library_vclplug_gtk4.mk
index 3f1c3eb..e3cc415 100644
--- a/vcl/Library_vclplug_gtk4.mk
+++ b/vcl/Library_vclplug_gtk4.mk
@@ -84,6 +84,7 @@ $(eval $(call gb_Library_add_exception_objects,vclplug_gtk4,\
    vcl/unx/gtk4/fpicker/SalGtkFilePicker \
    vcl/unx/gtk4/fpicker/SalGtkFolderPicker \
    vcl/unx/gtk4/fpicker/SalGtkPicker \
    vcl/unx/gtk4/convert3to4 \
    vcl/unx/gtk4/gtkdata \
    vcl/unx/gtk4/gtkinst \
    vcl/unx/gtk4/gtksys \
diff --git a/vcl/inc/unx/gtk/gtkdata.hxx b/vcl/inc/unx/gtk/gtkdata.hxx
index cc37cd1..ef257c7 100644
--- a/vcl/inc/unx/gtk/gtkdata.hxx
+++ b/vcl/inc/unx/gtk/gtkdata.hxx
@@ -195,6 +195,8 @@ inline GdkGLContext* surface_create_gl_context(GdkSurface* pSurface)
typedef GtkClipboard GdkClipboard;
#endif

int getButtonPriority(const OString &rType);

class GtkSalTimer final : public SalTimer
{
    struct SalGtkTimeoutSource *m_pTimeout;
diff --git a/vcl/unx/gtk3/gtkdata.cxx b/vcl/unx/gtk3/gtkdata.cxx
index 5bd818e..1487798 100644
--- a/vcl/unx/gtk3/gtkdata.cxx
+++ b/vcl/unx/gtk3/gtkdata.cxx
@@ -843,4 +843,61 @@ void GtkSalDisplay::deregisterFrame( SalFrame* pFrame )
    SalGenericDisplay::deregisterFrame( pFrame );
}

namespace {

struct ButtonOrder
{
    const char * m_aType;
    int m_nPriority;
};

}

int getButtonPriority(const OString &rType)
{
    static const size_t N_TYPES = 8;
    static const ButtonOrder aDiscardCancelSave[N_TYPES] =
    {
        { "/discard", 0 },
        { "/cancel", 1 },
        { "/close", 1 },
        { "/no", 2 },
        { "/open", 3 },
        { "/save", 3 },
        { "/yes", 3 },
        { "/ok", 3 }
    };

    static const ButtonOrder aSaveDiscardCancel[N_TYPES] =
    {
        { "/open", 0 },
        { "/save", 0 },
        { "/yes", 0 },
        { "/ok", 0 },
        { "/discard", 1 },
        { "/no", 1 },
        { "/cancel", 2 },
        { "/close", 2 }
    };

    const ButtonOrder* pOrder = &aDiscardCancelSave[0];

    const OUString &rEnv = Application::GetDesktopEnvironment();

    if (rEnv.equalsIgnoreAsciiCase("windows") ||
        rEnv.equalsIgnoreAsciiCase("tde") ||
        rEnv.startsWithIgnoreAsciiCase("kde"))
    {
        pOrder = &aSaveDiscardCancel[0];
    }

    for (size_t i = 0; i < N_TYPES; ++i, ++pOrder)
    {
        if (rType.endsWith(pOrder->m_aType))
            return pOrder->m_nPriority;
    }

    return -1;
}

/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/vcl/unx/gtk3/gtkinst.cxx b/vcl/unx/gtk3/gtkinst.cxx
index 3a42aac..90cc860 100644
--- a/vcl/unx/gtk3/gtkinst.cxx
+++ b/vcl/unx/gtk3/gtkinst.cxx
@@ -27,9 +27,9 @@
#include <headless/svpvd.hxx>
#include <headless/svpbmp.hxx>
#include <vcl/builder.hxx>
#include <vcl/toolkit/floatwin.hxx>
#include <vcl/inputtypes.hxx>
#include <vcl/transfer.hxx>
#include <vcl/toolkit/floatwin.hxx>
#include <unx/genpspgraphics.h>
#include <rtl/strbuf.hxx>
#include <sal/log.hxx>
@@ -44,8 +44,6 @@
#if !GTK_CHECK_VERSION(4, 0, 0)
#include "a11y/atkwrapper.hxx"
#endif
#include <com/sun/star/beans/XPropertySet.hpp>
#include <com/sun/star/io/TempFile.hpp>
#include <com/sun/star/datatransfer/XTransferable.hpp>
#include <com/sun/star/datatransfer/clipboard/XClipboard.hpp>
#include <com/sun/star/datatransfer/clipboard/XClipboardEx.hpp>
@@ -59,14 +57,11 @@
#include <com/sun/star/lang/XServiceInfo.hpp>
#include <com/sun/star/lang/XSingleServiceFactory.hpp>
#include <com/sun/star/lang/XInitialization.hpp>
#include <com/sun/star/xml/dom/DocumentBuilder.hpp>
#include <com/sun/star/xml/sax/Writer.hpp>
#include <com/sun/star/xml/sax/XSAXSerializable.hpp>
#include <comphelper/lok.hxx>
#include <comphelper/processfactory.hxx>
#include <comphelper/sequence.hxx>
#include <cppuhelper/compbase.hxx>
#include <comphelper/string.hxx>
#include <cppuhelper/compbase.hxx>
#include <cppuhelper/implbase.hxx>
#include <cppuhelper/supportsservice.hxx>
#include <officecfg/Office/Common.hxx>
@@ -148,16 +143,16 @@ extern "C"
            XInitThreads();
#endif

#if !GTK_CHECK_VERSION(4, 0, 0)
        // init gdk thread protection
        bool const sup = g_thread_supported();
            // extracted from the 'if' to avoid Clang -Wunreachable-code
        if ( !sup )
            g_thread_init( nullptr );

#if !GTK_CHECK_VERSION(4, 0, 0)
        gdk_threads_set_lock_functions (GdkThreadsEnter, GdkThreadsLeave);
#endif
        SAL_INFO("vcl.gtk", "Hooked gdk threads locks");
#endif

        auto pYieldMutex = std::make_unique<GtkYieldMutex>();

@@ -5173,70 +5168,6 @@ std::unique_ptr<weld::Container> GtkInstanceWidget::weld_parent() const

namespace {

struct ButtonOrder
{
    const char * m_aType;
    int m_nPriority;
};

int getButtonPriority(const OString &rType)
{
    static const size_t N_TYPES = 8;
    static const ButtonOrder aDiscardCancelSave[N_TYPES] =
    {
        { "/discard", 0 },
        { "/cancel", 1 },
        { "/close", 1 },
        { "/no", 2 },
        { "/open", 3 },
        { "/save", 3 },
        { "/yes", 3 },
        { "/ok", 3 }
    };

    static const ButtonOrder aSaveDiscardCancel[N_TYPES] =
    {
        { "/open", 0 },
        { "/save", 0 },
        { "/yes", 0 },
        { "/ok", 0 },
        { "/discard", 1 },
        { "/no", 1 },
        { "/cancel", 2 },
        { "/close", 2 }
    };

    const ButtonOrder* pOrder = &aDiscardCancelSave[0];

    const OUString &rEnv = Application::GetDesktopEnvironment();

    if (rEnv.equalsIgnoreAsciiCase("windows") ||
        rEnv.equalsIgnoreAsciiCase("tde") ||
        rEnv.startsWithIgnoreAsciiCase("kde"))
    {
        pOrder = &aSaveDiscardCancel[0];
    }

    for (size_t i = 0; i < N_TYPES; ++i, ++pOrder)
    {
        if (rType.endsWith(pOrder->m_aType))
            return pOrder->m_nPriority;
    }

    return -1;
}

#if GTK_CHECK_VERSION(4, 0, 0)
typedef std::pair<css::uno::Reference<css::xml::dom::XNode>, OUString> named_node;

bool sortButtonNodes(const named_node& rA, const named_node& rB)
{
    //order within groups according to platform rules
    return getButtonPriority("/" + rA.second.toUtf8()) <
           getButtonPriority("/" + rB.second.toUtf8());
}
#endif

bool sortButtons(const GtkWidget* pA, const GtkWidget* pB)
{
    //order within groups according to platform rules
@@ -21007,825 +20938,14 @@ bool IsAllowedBuiltInIcon(std::u16string_view iconName)

namespace {

#if GTK_CHECK_VERSION(4, 0, 0)

// <property name="spacing">6</property>
Reference<css::xml::dom::XNode> CreateProperty(const css::uno::Reference<css::xml::dom::XDocument>& xDoc,
                                               const OUString& rPropName, const OUString& rValue)
{
    css::uno::Reference<css::xml::dom::XElement> xProperty = xDoc->createElement("property");
    css::uno::Reference<css::xml::dom::XAttr> xPropName = xDoc->createAttribute("name");
    xPropName->setValue(rPropName);
    xProperty->setAttributeNode(xPropName);
    css::uno::Reference<css::xml::dom::XText> xValue = xDoc->createTextNode(rValue);
    xProperty->appendChild(xValue);
    return xProperty;
}

bool ToplevelIsMessageDialog(const Reference<css::xml::dom::XNode>& xNode)
{
    for (css::uno::Reference<css::xml::dom::XNode> xObjectCandidate = xNode->getParentNode();
         xObjectCandidate.is();
         xObjectCandidate = xObjectCandidate->getParentNode())
    {
        if (xObjectCandidate->getNodeName() == "object")
        {
            css::uno::Reference<css::xml::dom::XNamedNodeMap> xObjectMap = xObjectCandidate->getAttributes();
            css::uno::Reference<css::xml::dom::XNode> xClass = xObjectMap->getNamedItem("class");
            if (xClass->getNodeValue() == "GtkMessageDialog")
                return true;
        }
    }
    return false;
}

void SetPropertyOnTopLevel(const Reference<css::xml::dom::XNode>& xNode, const Reference<css::xml::dom::XNode>& xProperty)
{
    for (css::uno::Reference<css::xml::dom::XNode> xObjectCandidate = xNode->getParentNode();
         xObjectCandidate.is();
         xObjectCandidate = xObjectCandidate->getParentNode())
    {
        if (xObjectCandidate->getNodeName() == "object")
        {
            css::uno::Reference<css::xml::dom::XNamedNodeMap> xObjectMap = xObjectCandidate->getAttributes();
            css::uno::Reference<css::xml::dom::XNode> xClass = xObjectMap->getNamedItem("class");
            if (xClass->getNodeValue() == "GtkDialog")
            {
                auto xFirstChild = xObjectCandidate->getFirstChild();
                if (xFirstChild.is())
                    xObjectCandidate->insertBefore(xProperty, xFirstChild);
                else
                    xObjectCandidate->appendChild(xProperty);

                break;
            }
        }
    }
}

OUString GetParentObjectType(const Reference<css::xml::dom::XNode>& xNode)
{
    auto xParent = xNode->getParentNode();
    assert(xParent->getNodeName() == "object");
    css::uno::Reference<css::xml::dom::XNamedNodeMap> xParentMap = xParent->getAttributes();
    css::uno::Reference<css::xml::dom::XNode> xClass = xParentMap->getNamedItem("class");
    return xClass->getNodeValue();
}

// currently runs the risk of duplicate margin-* properties if there was already such as well
// as the border
void AddBorderAsMargins(const Reference<css::xml::dom::XNode>& xNode, const OUString& rBorderWidth)
{
    auto xDoc = xNode->getOwnerDocument();

    auto xMarginEnd = CreateProperty(xDoc, "margin-end", rBorderWidth);

    auto xFirstChild = xNode->getFirstChild();
    if (xFirstChild.is())
        xNode->insertBefore(xMarginEnd, xFirstChild);
    else
        xNode->appendChild(xMarginEnd);

    xNode->insertBefore(CreateProperty(xDoc, "margin-top", rBorderWidth), xMarginEnd);
    xNode->insertBefore(CreateProperty(xDoc, "margin-bottom", rBorderWidth), xMarginEnd);
    xNode->insertBefore(CreateProperty(xDoc, "margin-start", rBorderWidth), xMarginEnd);
}

struct ConvertResult
{
    bool m_bChildCanFocus;
    bool m_bHasVisible;
    bool m_bHasIconName;
    bool m_bAlwaysShowImage;
    css::uno::Reference<css::xml::dom::XNode> m_xPropertyLabel;

    ConvertResult(bool bChildCanFocus,
                  bool bHasVisible,
                  bool bHasIconName,
                  bool bAlwaysShowImage,
                  const css::uno::Reference<css::xml::dom::XNode>& rPropertyLabel)
        : m_bChildCanFocus(bChildCanFocus)
        , m_bHasVisible(bHasVisible)
        , m_bHasIconName(bHasIconName)
        , m_bAlwaysShowImage(bAlwaysShowImage)
        , m_xPropertyLabel(rPropertyLabel)
    {
    }
};

ConvertResult Convert3To4(const Reference<css::xml::dom::XNode>& xNode)
{
    css::uno::Reference<css::xml::dom::XNodeList> xNodeList = xNode->getChildNodes();
    if (!xNodeList.is())
        return ConvertResult(false, false, false, false, nullptr);

    std::vector<css::uno::Reference<css::xml::dom::XNode>> xRemoveList;

    OUString sBorderWidth;
    bool bChildCanFocus = false;
    bool bHasVisible = false;
    bool bHasIconName = false;
    bool bAlwaysShowImage = false;
    css::uno::Reference<css::xml::dom::XNode> xPropertyLabel;
    css::uno::Reference<css::xml::dom::XNode> xCantFocus;

    css::uno::Reference<css::xml::dom::XNode> xChild = xNode->getFirstChild();
    while (xChild.is())
    {
        if (xChild->getNodeName() == "requires")
        {
            css::uno::Reference<css::xml::dom::XNamedNodeMap> xMap = xChild->getAttributes();
            css::uno::Reference<css::xml::dom::XNode> xLib = xMap->getNamedItem("lib");
            assert(xLib->getNodeValue() == "gtk+");
            xLib->setNodeValue("gtk");
            css::uno::Reference<css::xml::dom::XNode> xVersion = xMap->getNamedItem("version");
            assert(xVersion->getNodeValue() == "3.20");
            xVersion->setNodeValue("4.0");
        }
        else if (xChild->getNodeName() == "property")
        {
            css::uno::Reference<css::xml::dom::XNamedNodeMap> xMap = xChild->getAttributes();
            css::uno::Reference<css::xml::dom::XNode> xName = xMap->getNamedItem("name");
            OUString sName(xName->getNodeValue().replace('_', '-'));

            if (sName == "border-width")
                sBorderWidth = xChild->getFirstChild()->getNodeValue();

            if (sName == "has-default")
            {
                css::uno::Reference<css::xml::dom::XNamedNodeMap> xParentMap = xChild->getParentNode()->getAttributes();
                css::uno::Reference<css::xml::dom::XNode> xId = xParentMap->getNamedItem("id");
                auto xDoc = xChild->getOwnerDocument();
                auto xDefaultWidget = CreateProperty(xDoc, "default-widget", xId->getNodeValue());
                SetPropertyOnTopLevel(xChild, xDefaultWidget);
                xRemoveList.push_back(xChild);
            }

            if (sName == "has-focus" || sName == "is-focus")
            {
                css::uno::Reference<css::xml::dom::XNamedNodeMap> xParentMap = xChild->getParentNode()->getAttributes();
                css::uno::Reference<css::xml::dom::XNode> xId = xParentMap->getNamedItem("id");
                auto xDoc = xChild->getOwnerDocument();
                auto xDefaultWidget = CreateProperty(xDoc, "focus-widget", xId->getNodeValue());
                SetPropertyOnTopLevel(xChild, xDefaultWidget);
                xRemoveList.push_back(xChild);
            }

            if (sName == "can-focus")
            {
                bChildCanFocus = toBool(xChild->getFirstChild()->getNodeValue());
                if (!bChildCanFocus)
                {
                    OUString sParentClass = GetParentObjectType(xChild);
                    if (sParentClass == "GtkBox" || sParentClass == "GtkGrid")
                    {
                        // e.g. for the case of notebooks without children yet, just remove the can't focus property
                        // from Boxes and Grids
                        xRemoveList.push_back(xChild);
                    }
                    else if (sParentClass == "GtkComboBoxText")
                    {
                        // this was always a bit finicky in gtk3, fix it up to default to can-focus
                        xRemoveList.push_back(xChild);
                    }
                    else
                    {
                        // otherwise mark the property as needing removal if there turns out to be a child
                        // with can-focus of true, in which case remove this parent conflicting property
                        xCantFocus = xChild;
                    }
                }
            }

            if (sName == "label")
                xPropertyLabel = xChild;

            if (sName == "visible")
                bHasVisible = true;

            if (sName == "icon-name")
                bHasIconName = true;

            if (sName == "events")
                xRemoveList.push_back(xChild);

            if (sName == "activates-default")
            {
                if (GetParentObjectType(xChild) == "GtkSpinButton")
                    xRemoveList.push_back(xChild);
            }

            if (sName == "width-chars")
            {
                if (GetParentObjectType(xChild) == "GtkEntry")
                {
                    // I don't quite get what the difference should be wrt width-chars and max-width-chars
                    // but glade doesn't write max-width-chars and in gtk4 where we have width-chars, e.g
                    // print dialog, then max-width-chars gives the effect we wanted with width-chars
                    auto xDoc = xChild->getOwnerDocument();
                    auto mMaxWidthChars = CreateProperty(xDoc, "max-width-chars", xChild->getFirstChild()->getNodeValue());
                    xChild->getParentNode()->insertBefore(mMaxWidthChars, xChild);
                }
            }

            // remove 'Help' button label and replace with a help icon instead
            if (sName == "label" && GetParentObjectType(xChild) == "GtkButton")
            {
                css::uno::Reference<css::xml::dom::XNamedNodeMap> xParentMap = xChild->getParentNode()->getAttributes();
                css::uno::Reference<css::xml::dom::XNode> xId = xParentMap->getNamedItem("id");
                if (xId && xId->getNodeValue() == "help")
                {
                    auto xDoc = xChild->getOwnerDocument();
                    auto xIconName = CreateProperty(xDoc, "icon-name", "help-browser-symbolic");
                    xChild->getParentNode()->insertBefore(xIconName, xChild);
                    xRemoveList.push_back(xChild);
                }
            }

            if (sName == "icon-size")
            {
                if (GetParentObjectType(xChild) == "GtkImage")
                {
                    if (xChild->getFirstChild()->getNodeValue() == "6")
                    {
                        auto xDoc = xChild->getOwnerDocument();
                        // convert old GTK_ICON_SIZE_DIALOG to new GTK_ICON_SIZE_LARGE
                        auto xIconSize = CreateProperty(xDoc, "icon-size", "2");
                        xChild->getParentNode()->insertBefore(xIconSize, xChild);
                        xRemoveList.push_back(xChild);
                    }
                    else
                        SAL_WARN( "vcl.gtk", "what should we do with an icon-size of: " << xChild->getFirstChild()->getNodeValue());
                }
            }

            if (sName == "truncate-multiline")
            {
                if (GetParentObjectType(xChild) == "GtkSpinButton")
                    xRemoveList.push_back(xChild);
            }

            if (sName == "homogeneous")
            {
                // e.g. the buttonbox in xml filter dialog
                if (GetParentObjectType(xChild) == "GtkButtonBox")
                    xRemoveList.push_back(xChild);
            }

            if (sName == "shadow-type")
            {
                if (GetParentObjectType(xChild) == "GtkFrame")
                    xRemoveList.push_back(xChild);
                else if (GetParentObjectType(xChild) == "GtkScrolledWindow")
                {
                    bool bHasFrame = xChild->getFirstChild()->getNodeValue() != "none";
                    auto xDoc = xChild->getOwnerDocument();
                    auto xHasFrame = CreateProperty(xDoc, "has-frame",
                                                    bHasFrame ? OUString("True") : OUString("False"));
                    xChild->getParentNode()->insertBefore(xHasFrame, xChild);
                    xRemoveList.push_back(xChild);
                }
            }

            if (sName == "always-show-image")
            {
                if (GetParentObjectType(xChild) == "GtkButton")
                {
                    // we will turn always-show-image into a GtkBox child for
                    // GtkButton and a GtkLabel child for the GtkBox and move
                    // the label property into it.
                    bAlwaysShowImage = toBool(xChild->getFirstChild()->getNodeValue());
                    xRemoveList.push_back(xChild);
                }
            }

            if (sName == "relief")
            {
                if (GetParentObjectType(xChild) == "GtkLinkButton" ||
                    GetParentObjectType(xChild) == "GtkButton")
                {
                    assert(xChild->getFirstChild()->getNodeValue() == "none");
                    auto xDoc = xChild->getOwnerDocument();
                    auto xHasFrame = CreateProperty(xDoc, "has-frame", "False");
                    xChild->getParentNode()->insertBefore(xHasFrame, xChild);
                    xRemoveList.push_back(xChild);
                }
            }

            if (sName == "xalign")
            {
                if (GetParentObjectType(xChild) == "GtkLinkButton" ||
                    GetParentObjectType(xChild) == "GtkMenuButton" ||
                    GetParentObjectType(xChild) == "GtkButton")
                {
                    // TODO expand into a GtkLabel child with alignment on that instead
                    assert(xChild->getFirstChild()->getNodeValue() == "0");
                    xRemoveList.push_back(xChild);
                }
            }

            if (sName == "hscrollbar-policy")
            {
                if (GetParentObjectType(xChild) == "GtkScrolledWindow")
                {
                    if (xChild->getFirstChild()->getNodeValue() == "never")
                    {
                        auto xDoc = xChild->getOwnerDocument();
                        auto xHasFrame = CreateProperty(xDoc, "propagate-natural-width", "True");
                        xChild->getParentNode()->insertBefore(xHasFrame, xChild);
                    }
                }
            }

            if (sName == "vscrollbar-policy")
            {
                if (GetParentObjectType(xChild) == "GtkScrolledWindow")
                {
                    if (xChild->getFirstChild()->getNodeValue() == "never")
                    {
                        auto xDoc = xChild->getOwnerDocument();
                        auto xHasFrame = CreateProperty(xDoc, "propagate-natural-height", "True");
                        xChild->getParentNode()->insertBefore(xHasFrame, xChild);
                    }
                }
            }

            if (sName == "image")
            {
                if (GetParentObjectType(xChild) == "GtkButton")
                {
                    // find the image object, expected to be a child of "interface" and relocate
                    // it to be a child of this GtkButton
                    auto xObjectCandidate = xChild->getParentNode();
                    if (xObjectCandidate->getNodeName() == "object")
                    {
                        OUString sImageId = xChild->getFirstChild()->getNodeValue();

                        css::uno::Reference<css::xml::dom::XNode> xRootCandidate = xChild->getParentNode();
                        while (xRootCandidate)
                        {
                             if (xRootCandidate->getNodeName() == "interface")
                                 break;
                             xRootCandidate = xRootCandidate->getParentNode();
                        }

                        css::uno::Reference<css::xml::dom::XNode> xImageNode;

                        for (auto xImageCandidate = xRootCandidate->getFirstChild(); xImageCandidate.is(); xImageCandidate= xImageCandidate->getNextSibling())
                        {
                            css::uno::Reference<css::xml::dom::XNamedNodeMap> xImageCandidateMap = xImageCandidate->getAttributes();
                            if (!xImageCandidateMap.is())
                                continue;
                            css::uno::Reference<css::xml::dom::XNode> xId = xImageCandidateMap->getNamedItem("id");
                            if (xId && xId->getNodeValue() == sImageId)
                            {
                                xImageNode = xImageCandidate;
                                break;
                            }
                        }

                        auto xDoc = xChild->getOwnerDocument();
                        css::uno::Reference<css::xml::dom::XElement> xImageChild = xDoc->createElement("child");
                        xImageChild->appendChild(xImageNode->getParentNode()->removeChild(xImageNode));
                        xObjectCandidate->appendChild(xImageChild);
                    }

                    xRemoveList.push_back(xChild);
                }
            }

            if (sName == "draw-indicator")
            {
                assert(toBool(xChild->getFirstChild()->getNodeValue()));
                xRemoveList.push_back(xChild);
            }

            if (sName == "type-hint" || sName == "skip-taskbar-hint" ||
                sName == "can-default" || sName == "border-width" ||
                sName == "layout-style" || sName == "no-show-all" ||
                sName == "ignore-hidden" || sName == "window-position")
            {
                xRemoveList.push_back(xChild);
            }
        }
        else if (xChild->getNodeName() == "child")
        {
            bool bContentArea = false;

            css::uno::Reference<css::xml::dom::XNamedNodeMap> xMap = xChild->getAttributes();
            css::uno::Reference<css::xml::dom::XNode> xName = xMap->getNamedItem("internal-child");
            if (xName)
            {
                OUString sName(xName->getNodeValue());
                if (sName == "vbox")
                {
                    xName->setNodeValue("content_area");
                    bContentArea = true;
                }
                else if (sName == "accessible")
                    xRemoveList.push_back(xChild); // Yikes!, what's the replacement for this going to be
            }

            if (bContentArea)
            {
                for (css::uno::Reference<css::xml::dom::XNode> xObjectCandidate = xChild->getFirstChild();
                     xObjectCandidate.is();
                     xObjectCandidate = xObjectCandidate->getNextSibling())
                {
                    if (xObjectCandidate->getNodeName() == "object")
                    {
                        auto xDoc = xChild->getOwnerDocument();

                        auto xVExpand = CreateProperty(xDoc, "vexpand", "True");
                        auto xFirstChild = xObjectCandidate->getFirstChild();
                        if (xFirstChild.is())
                            xObjectCandidate->insertBefore(xVExpand, xFirstChild);
                        else
                            xObjectCandidate->appendChild(xVExpand);

                        if (!sBorderWidth.isEmpty())
                        {
                            AddBorderAsMargins(xObjectCandidate, sBorderWidth);
                            sBorderWidth.clear();
                        }

                        break;
                    }
                }
            }
        }
        else if (xChild->getNodeName() == "packing")
        {
            // remove "packing" and if its grid packing insert a replacement "layout" into
            // the associated "object"
            auto xDoc = xChild->getOwnerDocument();
            css::uno::Reference<css::xml::dom::XElement> xNew = xDoc->createElement("layout");

            bool bGridPacking = false;

            // iterate over all children and append them to the new element
            for (css::uno::Reference<css::xml::dom::XNode> xCurrent = xChild->getFirstChild();
                 xCurrent.is();
                 xCurrent = xChild->getFirstChild())
            {
                css::uno::Reference<css::xml::dom::XNamedNodeMap> xMap = xCurrent->getAttributes();
                if (xMap.is())
                {
                    css::uno::Reference<css::xml::dom::XNode> xName = xMap->getNamedItem("name");
                    OUString sName(xName->getNodeValue().replace('_', '-'));
                    if (sName == "left-attach")
                    {
                        xName->setNodeValue("column");
                        bGridPacking = true;
                    }
                    else if (sName == "top-attach")
                    {
                        xName->setNodeValue("row");
                        bGridPacking = true;
                    }
                    else if (sName == "width")
                    {
                        xName->setNodeValue("column-span");
                        bGridPacking = true;
                    }
                    else if (sName == "height")
                    {
                        xName->setNodeValue("row-span");
                        bGridPacking = true;
                    }
                    else if (sName == "secondary")
                    {
                        // turn parent tag of <child> into <child type="start">
                        auto xParent = xChild->getParentNode();
                        css::uno::Reference<css::xml::dom::XAttr> xTypeStart = xDoc->createAttribute("type");
                        xTypeStart->setValue("start");
                        css::uno::Reference<css::xml::dom::XElement> xElem(xParent, css::uno::UNO_QUERY_THROW);
                        xElem->setAttributeNode(xTypeStart);
                    }
                }
                xNew->appendChild(xChild->removeChild(xCurrent));
            }

            if (bGridPacking)
            {
                // go back to parent and find the object child and insert this "layout" as a
                // new child of the object
                auto xParent = xChild->getParentNode();
                for (css::uno::Reference<css::xml::dom::XNode> xObjectCandidate = xParent->getFirstChild();
                     xObjectCandidate.is();
                     xObjectCandidate = xObjectCandidate->getNextSibling())
                {
                    if (xObjectCandidate->getNodeName() == "object")
                    {
                        xObjectCandidate->appendChild(xNew);
                        break;
                    }
                }
            }

            xRemoveList.push_back(xChild);
        }
        else if (xChild->getNodeName() == "accessibility")
        {
            // TODO <relation type="labelled-by" target="pagenumcb"/> -> <relation name="labelled-by">pagenumcb</relation>
            xRemoveList.push_back(xChild);
        }
        else if (xChild->getNodeName() == "accelerator")
        {
            // TODO is anything like this supported anymore in .ui files
            xRemoveList.push_back(xChild);
        }

        auto xNextChild = xChild->getNextSibling();

        bool bChildHasIconName = false;
        bool bChildHasVisible = false;
        bool bChildAlwaysShowImage = false;
        css::uno::Reference<css::xml::dom::XNode> xChildPropertyLabel;
        if (xChild->hasChildNodes())
        {
            auto aChildRes = Convert3To4(xChild);
            bChildCanFocus |= aChildRes.m_bChildCanFocus;
            if (bChildCanFocus && xCantFocus.is())
            {
                xNode->removeChild(xCantFocus);
                xCantFocus.clear();
            }
            if (xChild->getNodeName() == "object")
            {
                bChildHasVisible = aChildRes.m_bHasVisible;
                bChildHasIconName = aChildRes.m_bHasIconName;
                bChildAlwaysShowImage = aChildRes.m_bAlwaysShowImage;
                xChildPropertyLabel = aChildRes.m_xPropertyLabel;
            }
        }

        if (xChild->getNodeName() == "object")
        {
            auto xDoc = xChild->getOwnerDocument();

            css::uno::Reference<css::xml::dom::XNamedNodeMap> xMap = xChild->getAttributes();
            css::uno::Reference<css::xml::dom::XNode> xClass = xMap->getNamedItem("class");
            OUString sClass(xClass->getNodeValue());

            auto xInternalChildCandidate = xChild->getParentNode();
            css::uno::Reference<css::xml::dom::XNamedNodeMap> xInternalChildCandidateMap = xInternalChildCandidate->getAttributes();
            css::uno::Reference<css::xml::dom::XNode> xId = xInternalChildCandidateMap->getNamedItem("internal-child");

            // turn default gtk3 invisibility for widget objects into explicit invisible, but ignore internal-children
            if (!bChildHasVisible && !xId)
            {
                if (sClass == "GtkBox" || sClass == "GtkButton" ||
                    sClass == "GtkCalendar" || sClass == "GtkCheckButton" ||
                    sClass == "GtkRadioButton" || sClass == "GtkComboBox" ||
                    sClass == "GtkComboBoxText" || sClass == "GtkDrawingArea" ||
                    sClass == "GtkEntry" || sClass == "GtkExpander" ||
                    sClass == "GtkFrame" || sClass == "GtkGrid" ||
                    sClass == "GtkImage" || sClass == "GtkLabel" ||
                    sClass == "GtkMenuButton" || sClass == "GtkNotebook" ||
                    sClass == "GtkOverlay" || sClass == "GtkPaned" ||
                    sClass == "GtkProgressBar" || sClass == "GtkScrolledWindow" ||
                    sClass == "GtkSeparator" || sClass == "GtkSpinButton" ||
                    sClass == "GtkSpinner" || sClass == "GtkTextView" ||
                    sClass == "GtkTreeView" || sClass == "GtkViewport" ||
                    sClass == "GtkLinkButton" || sClass == "GtkToggleButton" ||
                    sClass == "GtkButtonBox")

                {
                    auto xVisible = CreateProperty(xDoc, "visible", "False");
                    auto xFirstChild = xChild->getFirstChild();
                    if (xFirstChild.is())
                        xChild->insertBefore(xVisible, xFirstChild);
                    else
                        xChild->appendChild(xVisible);
                }
            }

            if (sClass == "GtkButtonBox")
            {
                if (xId && xId->getNodeValue() == "action_area" && !ToplevelIsMessageDialog(xChild))
                {
                    xClass->setNodeValue("GtkHeaderBar");
                    auto xSpacingNode = CreateProperty(xDoc, "show-title-buttons", "False");
                    auto xFirstChild = xChild->getFirstChild();
                    if (xFirstChild.is())
                        xChild->insertBefore(xSpacingNode, xFirstChild);
                    else
                        xChild->appendChild(xSpacingNode);

                    // move the replacement GtkHeaderBar up to before the content_area
                    auto xContentAreaCandidate = xChild->getParentNode();
                    while (xContentAreaCandidate)
                    {
                        css::uno::Reference<css::xml::dom::XNamedNodeMap> xChildMap = xContentAreaCandidate->getAttributes();
                        css::uno::Reference<css::xml::dom::XNode> xName = xChildMap->getNamedItem("internal-child");
                        if (xName && xName->getNodeValue() == "content_area")
                        {
                            auto xActionArea = xChild->getParentNode();

                            xActionArea->getParentNode()->removeChild(xActionArea);

                            css::uno::Reference<css::xml::dom::XAttr> xTypeTitleBar = xDoc->createAttribute("type");
                            xTypeTitleBar->setValue("titlebar");
                            css::uno::Reference<css::xml::dom::XElement> xElem(xActionArea, css::uno::UNO_QUERY_THROW);
                            xElem->setAttributeNode(xTypeTitleBar);
                            xElem->removeAttribute("internal-child");

                            xContentAreaCandidate->getParentNode()->insertBefore(xActionArea, xContentAreaCandidate);

                            std::vector<named_node> aChildren;

                            css::uno::Reference<css::xml::dom::XNode> xTitleChild = xChild->getFirstChild();
                            while (xTitleChild.is())
                            {
                                auto xNextTitleChild = xTitleChild->getNextSibling();
                                if (xTitleChild->getNodeName() == "child")
                                {
                                    OUString sNodeId;

                                    for (css::uno::Reference<css::xml::dom::XNode> xObjectCandidate = xTitleChild->getFirstChild();
                                         xObjectCandidate.is();
                                         xObjectCandidate = xObjectCandidate->getNextSibling())
                                    {
                                        if (xObjectCandidate->getNodeName() == "object")
                                        {
                                            css::uno::Reference<css::xml::dom::XNamedNodeMap> xObjectMap = xObjectCandidate->getAttributes();
                                            css::uno::Reference<css::xml::dom::XNode> xObjectId = xObjectMap->getNamedItem("id");
                                            sNodeId = xObjectId->getNodeValue();
                                            break;
                                        }
                                    }

                                    aChildren.push_back(std::make_pair(xTitleChild, sNodeId));
                                }
                                else if (xTitleChild->getNodeName() == "property")
                                {
                                    // remove any <property name="homogeneous"> tag
                                    css::uno::Reference<css::xml::dom::XNamedNodeMap> xTitleChildMap = xTitleChild->getAttributes();
                                    css::uno::Reference<css::xml::dom::XNode> xPropName = xTitleChildMap->getNamedItem("name");
                                    OUString sPropName(xPropName->getNodeValue().replace('_', '-'));
                                    if (sPropName == "homogeneous")
                                        xChild->removeChild(xTitleChild);
                                }

                                xTitleChild = xNextTitleChild;
                            }

                            //sort child order within parent so that we match the platform button order
                            std::stable_sort(aChildren.begin(), aChildren.end(), sortButtonNodes);

                            int nNonHelpButtonCount = 0;

                            for (const auto& rTitleChild : aChildren)
                            {
                                xChild->removeChild(rTitleChild.first);
                                if (rTitleChild.second != "help")
                                    ++nNonHelpButtonCount;
                            }

                            std::reverse(aChildren.begin(), aChildren.end());

                            for (const auto& rTitleChild : aChildren)
                            {
                                xChild->appendChild(rTitleChild.first);

                                css::uno::Reference<css::xml::dom::XElement> xChildElem(rTitleChild.first, css::uno::UNO_QUERY_THROW);
                                if (!xChildElem->hasAttribute("type"))
                                {
                                    // turn parent tag of <child> into <child type="end"> except for cancel/close which we'll
                                    // put at start unless there is nothing at end
                                    css::uno::Reference<css::xml::dom::XAttr> xTypeEnd = xDoc->createAttribute("type");
                                    if (nNonHelpButtonCount >= 2 && (rTitleChild.second == "cancel" || rTitleChild.second == "close"))
                                        xTypeEnd->setValue("start");
                                    else
                                        xTypeEnd->setValue("end");
                                    xChildElem->setAttributeNode(xTypeEnd);
                                }
                            }

                            auto xUseHeaderBar = CreateProperty(xDoc, "use-header-bar", "1");
                            SetPropertyOnTopLevel(xContentAreaCandidate, xUseHeaderBar);

                            break;
                        }
                        xContentAreaCandidate = xContentAreaCandidate->getParentNode();
                    }
                }
                else // GtkMessageDialog
                    xClass->setNodeValue("GtkBox");
            }
            else if (sClass == "GtkRadioButton")
            {
                xClass->setNodeValue("GtkCheckButton");
            }
            else if (sClass == "GtkImage" && !bChildHasIconName)
            {
                xClass->setNodeValue("GtkPicture");
            }
            else if (sClass == "GtkPopover" && !bHasVisible)
            {
                auto xVisible = CreateProperty(xDoc, "visible", "False");
                auto xFirstChild = xChild->getFirstChild();
                if (xFirstChild.is())
                    xChild->insertBefore(xVisible, xFirstChild);
                else
                    xChild->appendChild(xVisible);
            }

            if (bChildAlwaysShowImage)
            {
                auto xImageCandidateNode = xChild->getLastChild();
                if (xImageCandidateNode && xImageCandidateNode->getNodeName() != "child")
                    xImageCandidateNode.clear();
                if (xImageCandidateNode)
                    xChild->removeChild(xImageCandidateNode);

                css::uno::Reference<css::xml::dom::XElement> xNewChildNode = xDoc->createElement("child");
                css::uno::Reference<css::xml::dom::XElement> xNewObjectNode = xDoc->createElement("object");
                css::uno::Reference<css::xml::dom::XAttr> xBoxClassName = xDoc->createAttribute("class");
                xBoxClassName->setValue("GtkBox");
                xNewObjectNode->setAttributeNode(xBoxClassName);
                xNewChildNode->appendChild(xNewObjectNode);

                xChild->appendChild(xNewChildNode);

                css::uno::Reference<css::xml::dom::XElement> xNewLabelChildNode = xDoc->createElement("child");
                css::uno::Reference<css::xml::dom::XElement> xNewChildObjectNode = xDoc->createElement("object");
                css::uno::Reference<css::xml::dom::XAttr> xLabelClassName = xDoc->createAttribute("class");
                xLabelClassName->setValue("GtkLabel");
                xNewChildObjectNode->setAttributeNode(xLabelClassName);
                if (xChildPropertyLabel)
                    xNewChildObjectNode->appendChild(xChildPropertyLabel->getParentNode()->removeChild(xChildPropertyLabel));
                xNewLabelChildNode->appendChild(xNewChildObjectNode);

                if (xImageCandidateNode)
                    xNewObjectNode->appendChild(xImageCandidateNode);
                xNewObjectNode->appendChild(xNewLabelChildNode);
            }
        }

        xChild = xNextChild;
    }

    if (!sBorderWidth.isEmpty())
        AddBorderAsMargins(xNode, sBorderWidth);

    for (auto& xRemove : xRemoveList)
        xNode->removeChild(xRemove);

    return ConvertResult(bChildCanFocus, bHasVisible, bHasIconName, bAlwaysShowImage, xPropertyLabel);
}
#endif

void load_ui_file(GtkBuilder* pBuilder, const OUString& rUri)
{
    GError *err = nullptr;

#if GTK_CHECK_VERSION(4, 0, 0)
    // load the xml
    css::uno::Reference<css::uno::XComponentContext> xContext = ::comphelper::getProcessComponentContext();
    css::uno::Reference<css::xml::dom::XDocumentBuilder> xBuilder = xml::dom::DocumentBuilder::create(xContext);
    css::uno::Reference<css::xml::dom::XDocument> xDocument = xBuilder->parseURI(rUri);

    // convert it from gtk3 to gtk4
    Convert3To4(xDocument);

    css::uno::Reference<css::beans::XPropertySet> xTempFile(io::TempFile::create(xContext), css::uno::UNO_QUERY);
    css::uno::Reference<css::io::XStream> xTempStream(xTempFile, css::uno::UNO_QUERY_THROW);
    xTempFile->setPropertyValue("RemoveFile", css::uno::makeAny(false));

    // serialize it back to xml
    css::uno::Reference<css::xml::sax::XSAXSerializable> xSerializer(xDocument, css::uno::UNO_QUERY);
    css::uno::Reference<css::xml::sax::XWriter> xWriter = css::xml::sax::Writer::create(xContext);
    css::uno::Reference<css::io::XOutputStream> xTempOut = xTempStream->getOutputStream();
    xWriter->setOutputStream(xTempOut);
    xSerializer->serialize(css::uno::Reference<css::xml::sax::XDocumentHandler>(xWriter, css::uno::UNO_QUERY_THROW),
            css::uno::Sequence<css::beans::StringPair>());

    // feed it to GtkBuilder
    css::uno::Reference<css::io::XSeekable> xTempSeek(xTempStream, css::uno::UNO_QUERY_THROW);
    xTempSeek->seek(0);
    auto xInput = xTempStream->getInputStream();
    css::uno::Sequence<sal_Int8> bytes;
    sal_Int32 nToRead = xInput->available();
    while (true)
    {
        sal_Int32 nRead = xInput->readBytes(bytes, std::max<sal_Int32>(nToRead, 4096));
        if (!nRead)
            break;
        // fprintf(stderr, "text is %s\n", reinterpret_cast<const gchar*>(bytes.getArray()));
        auto rc = gtk_builder_add_from_string(pBuilder, reinterpret_cast<const gchar*>(bytes.getArray()), nRead, &err);
        if (!rc)
        {
            SAL_WARN( "vcl.gtk", "GtkInstanceBuilder: error when calling gtk_builder_add_from_string: " << err->message);
            g_error_free(err);
        }
        assert(rc && "could not load UI file");
        // in the real world the first loop has read the entire file because its all 'available' without blocking
    }
    builder_add_from_gtk3_file(pBuilder, rUri);
#else
    OUString aPath;
    osl::FileBase::getSystemPathFromFileURL(rUri, aPath);
    GError *err = nullptr;
    auto rc = gtk_builder_add_from_file(pBuilder, OUStringToOString(aPath, RTL_TEXTENCODING_UTF8).getStr(), &err);

    if (!rc)
diff --git a/vcl/unx/gtk4/convert3to4.cxx b/vcl/unx/gtk4/convert3to4.cxx
new file mode 100644
index 0000000..06ca97f
--- /dev/null
+++ b/vcl/unx/gtk4/convert3to4.cxx
@@ -0,0 +1,889 @@
/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-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/.
 */

#include <com/sun/star/beans/XPropertySet.hpp>
#include <com/sun/star/io/TempFile.hpp>
#include <com/sun/star/xml/dom/DocumentBuilder.hpp>
#include <com/sun/star/xml/sax/Writer.hpp>
#include <com/sun/star/xml/sax/XSAXSerializable.hpp>
#include <comphelper/processfactory.hxx>
#include <unx/gtk/gtkdata.hxx>
#include <vcl/builder.hxx>
#include "convert3to4.hxx"

namespace
{
typedef std::pair<css::uno::Reference<css::xml::dom::XNode>, OUString> named_node;

bool sortButtonNodes(const named_node& rA, const named_node& rB)
{
    //order within groups according to platform rules
    return getButtonPriority("/" + rA.second.toUtf8())
           < getButtonPriority("/" + rB.second.toUtf8());
}

// <property name="spacing">6</property>
css::uno::Reference<css::xml::dom::XNode>
CreateProperty(const css::uno::Reference<css::xml::dom::XDocument>& xDoc, const OUString& rPropName,
               const OUString& rValue)
{
    css::uno::Reference<css::xml::dom::XElement> xProperty = xDoc->createElement("property");
    css::uno::Reference<css::xml::dom::XAttr> xPropName = xDoc->createAttribute("name");
    xPropName->setValue(rPropName);
    xProperty->setAttributeNode(xPropName);
    css::uno::Reference<css::xml::dom::XText> xValue = xDoc->createTextNode(rValue);
    xProperty->appendChild(xValue);
    return xProperty;
}

bool ToplevelIsMessageDialog(const css::uno::Reference<css::xml::dom::XNode>& xNode)
{
    for (css::uno::Reference<css::xml::dom::XNode> xObjectCandidate = xNode->getParentNode();
         xObjectCandidate.is(); xObjectCandidate = xObjectCandidate->getParentNode())
    {
        if (xObjectCandidate->getNodeName() == "object")
        {
            css::uno::Reference<css::xml::dom::XNamedNodeMap> xObjectMap
                = xObjectCandidate->getAttributes();
            css::uno::Reference<css::xml::dom::XNode> xClass = xObjectMap->getNamedItem("class");
            if (xClass->getNodeValue() == "GtkMessageDialog")
                return true;
        }
    }
    return false;
}

void SetPropertyOnTopLevel(const css::uno::Reference<css::xml::dom::XNode>& xNode,
                           const css::uno::Reference<css::xml::dom::XNode>& xProperty)
{
    for (css::uno::Reference<css::xml::dom::XNode> xObjectCandidate = xNode->getParentNode();
         xObjectCandidate.is(); xObjectCandidate = xObjectCandidate->getParentNode())
    {
        if (xObjectCandidate->getNodeName() == "object")
        {
            css::uno::Reference<css::xml::dom::XNamedNodeMap> xObjectMap
                = xObjectCandidate->getAttributes();
            css::uno::Reference<css::xml::dom::XNode> xClass = xObjectMap->getNamedItem("class");
            if (xClass->getNodeValue() == "GtkDialog")
            {
                auto xFirstChild = xObjectCandidate->getFirstChild();
                if (xFirstChild.is())
                    xObjectCandidate->insertBefore(xProperty, xFirstChild);
                else
                    xObjectCandidate->appendChild(xProperty);

                break;
            }
        }
    }
}

OUString GetParentObjectType(const css::uno::Reference<css::xml::dom::XNode>& xNode)
{
    auto xParent = xNode->getParentNode();
    assert(xParent->getNodeName() == "object");
    css::uno::Reference<css::xml::dom::XNamedNodeMap> xParentMap = xParent->getAttributes();
    css::uno::Reference<css::xml::dom::XNode> xClass = xParentMap->getNamedItem("class");
    return xClass->getNodeValue();
}

// currently runs the risk of duplicate margin-* properties if there was already such as well
// as the border
void AddBorderAsMargins(const css::uno::Reference<css::xml::dom::XNode>& xNode,
                        const OUString& rBorderWidth)
{
    auto xDoc = xNode->getOwnerDocument();

    auto xMarginEnd = CreateProperty(xDoc, "margin-end", rBorderWidth);

    auto xFirstChild = xNode->getFirstChild();
    if (xFirstChild.is())
        xNode->insertBefore(xMarginEnd, xFirstChild);
    else
        xNode->appendChild(xMarginEnd);

    xNode->insertBefore(CreateProperty(xDoc, "margin-top", rBorderWidth), xMarginEnd);
    xNode->insertBefore(CreateProperty(xDoc, "margin-bottom", rBorderWidth), xMarginEnd);
    xNode->insertBefore(CreateProperty(xDoc, "margin-start", rBorderWidth), xMarginEnd);
}

struct ConvertResult
{
    bool m_bChildCanFocus;
    bool m_bHasVisible;
    bool m_bHasIconName;
    bool m_bAlwaysShowImage;
    css::uno::Reference<css::xml::dom::XNode> m_xPropertyLabel;

    ConvertResult(bool bChildCanFocus, bool bHasVisible, bool bHasIconName, bool bAlwaysShowImage,
                  const css::uno::Reference<css::xml::dom::XNode>& rPropertyLabel)
        : m_bChildCanFocus(bChildCanFocus)
        , m_bHasVisible(bHasVisible)
        , m_bHasIconName(bHasIconName)
        , m_bAlwaysShowImage(bAlwaysShowImage)
        , m_xPropertyLabel(rPropertyLabel)
    {
    }
};

ConvertResult Convert3To4(const css::uno::Reference<css::xml::dom::XNode>& xNode)
{
    css::uno::Reference<css::xml::dom::XNodeList> xNodeList = xNode->getChildNodes();
    if (!xNodeList.is())
        return ConvertResult(false, false, false, false, nullptr);

    std::vector<css::uno::Reference<css::xml::dom::XNode>> xRemoveList;

    OUString sBorderWidth;
    bool bChildCanFocus = false;
    bool bHasVisible = false;
    bool bHasIconName = false;
    bool bAlwaysShowImage = false;
    css::uno::Reference<css::xml::dom::XNode> xPropertyLabel;
    css::uno::Reference<css::xml::dom::XNode> xCantFocus;

    css::uno::Reference<css::xml::dom::XNode> xChild = xNode->getFirstChild();
    while (xChild.is())
    {
        if (xChild->getNodeName() == "requires")
        {
            css::uno::Reference<css::xml::dom::XNamedNodeMap> xMap = xChild->getAttributes();
            css::uno::Reference<css::xml::dom::XNode> xLib = xMap->getNamedItem("lib");
            assert(xLib->getNodeValue() == "gtk+");
            xLib->setNodeValue("gtk");
            css::uno::Reference<css::xml::dom::XNode> xVersion = xMap->getNamedItem("version");
            assert(xVersion->getNodeValue() == "3.20");
            xVersion->setNodeValue("4.0");
        }
        else if (xChild->getNodeName() == "property")
        {
            css::uno::Reference<css::xml::dom::XNamedNodeMap> xMap = xChild->getAttributes();
            css::uno::Reference<css::xml::dom::XNode> xName = xMap->getNamedItem("name");
            OUString sName(xName->getNodeValue().replace('_', '-'));

            if (sName == "border-width")
                sBorderWidth = xChild->getFirstChild()->getNodeValue();

            if (sName == "has-default")
            {
                css::uno::Reference<css::xml::dom::XNamedNodeMap> xParentMap
                    = xChild->getParentNode()->getAttributes();
                css::uno::Reference<css::xml::dom::XNode> xId = xParentMap->getNamedItem("id");
                auto xDoc = xChild->getOwnerDocument();
                auto xDefaultWidget = CreateProperty(xDoc, "default-widget", xId->getNodeValue());
                SetPropertyOnTopLevel(xChild, xDefaultWidget);
                xRemoveList.push_back(xChild);
            }

            if (sName == "has-focus" || sName == "is-focus")
            {
                css::uno::Reference<css::xml::dom::XNamedNodeMap> xParentMap
                    = xChild->getParentNode()->getAttributes();
                css::uno::Reference<css::xml::dom::XNode> xId = xParentMap->getNamedItem("id");
                auto xDoc = xChild->getOwnerDocument();
                auto xDefaultWidget = CreateProperty(xDoc, "focus-widget", xId->getNodeValue());
                SetPropertyOnTopLevel(xChild, xDefaultWidget);
                xRemoveList.push_back(xChild);
            }

            if (sName == "can-focus")
            {
                bChildCanFocus = toBool(xChild->getFirstChild()->getNodeValue());
                if (!bChildCanFocus)
                {
                    OUString sParentClass = GetParentObjectType(xChild);
                    if (sParentClass == "GtkBox" || sParentClass == "GtkGrid")
                    {
                        // e.g. for the case of notebooks without children yet, just remove the can't focus property
                        // from Boxes and Grids
                        xRemoveList.push_back(xChild);
                    }
                    else if (sParentClass == "GtkComboBoxText")
                    {
                        // this was always a bit finicky in gtk3, fix it up to default to can-focus
                        xRemoveList.push_back(xChild);
                    }
                    else
                    {
                        // otherwise mark the property as needing removal if there turns out to be a child
                        // with can-focus of true, in which case remove this parent conflicting property
                        xCantFocus = xChild;
                    }
                }
            }

            if (sName == "label")
                xPropertyLabel = xChild;

            if (sName == "visible")
                bHasVisible = true;

            if (sName == "icon-name")
                bHasIconName = true;

            if (sName == "events")
                xRemoveList.push_back(xChild);

            if (sName == "activates-default")
            {
                if (GetParentObjectType(xChild) == "GtkSpinButton")
                    xRemoveList.push_back(xChild);
            }

            if (sName == "width-chars")
            {
                if (GetParentObjectType(xChild) == "GtkEntry")
                {
                    // I don't quite get what the difference should be wrt width-chars and max-width-chars
                    // but glade doesn't write max-width-chars and in gtk4 where we have width-chars, e.g
                    // print dialog, then max-width-chars gives the effect we wanted with width-chars
                    auto xDoc = xChild->getOwnerDocument();
                    auto mMaxWidthChars = CreateProperty(xDoc, "max-width-chars",
                                                         xChild->getFirstChild()->getNodeValue());
                    xChild->getParentNode()->insertBefore(mMaxWidthChars, xChild);
                }
            }

            // remove 'Help' button label and replace with a help icon instead
            if (sName == "label" && GetParentObjectType(xChild) == "GtkButton")
            {
                css::uno::Reference<css::xml::dom::XNamedNodeMap> xParentMap
                    = xChild->getParentNode()->getAttributes();
                css::uno::Reference<css::xml::dom::XNode> xId = xParentMap->getNamedItem("id");
                if (xId && xId->getNodeValue() == "help")
                {
                    auto xDoc = xChild->getOwnerDocument();
                    auto xIconName = CreateProperty(xDoc, "icon-name", "help-browser-symbolic");
                    xChild->getParentNode()->insertBefore(xIconName, xChild);
                    xRemoveList.push_back(xChild);
                }
            }

            if (sName == "icon-size")
            {
                if (GetParentObjectType(xChild) == "GtkImage")
                {
                    if (xChild->getFirstChild()->getNodeValue() == "6")
                    {
                        auto xDoc = xChild->getOwnerDocument();
                        // convert old GTK_ICON_SIZE_DIALOG to new GTK_ICON_SIZE_LARGE
                        auto xIconSize = CreateProperty(xDoc, "icon-size", "2");
                        xChild->getParentNode()->insertBefore(xIconSize, xChild);
                        xRemoveList.push_back(xChild);
                    }
                    else
                        SAL_WARN("vcl.gtk", "what should we do with an icon-size of: "
                                                << xChild->getFirstChild()->getNodeValue());
                }
            }

            if (sName == "truncate-multiline")
            {
                if (GetParentObjectType(xChild) == "GtkSpinButton")
                    xRemoveList.push_back(xChild);
            }

            if (sName == "homogeneous")
            {
                // e.g. the buttonbox in xml filter dialog
                if (GetParentObjectType(xChild) == "GtkButtonBox")
                    xRemoveList.push_back(xChild);
            }

            if (sName == "shadow-type")
            {
                if (GetParentObjectType(xChild) == "GtkFrame")
                    xRemoveList.push_back(xChild);
                else if (GetParentObjectType(xChild) == "GtkScrolledWindow")
                {
                    bool bHasFrame = xChild->getFirstChild()->getNodeValue() != "none";
                    auto xDoc = xChild->getOwnerDocument();
                    auto xHasFrame = CreateProperty(
                        xDoc, "has-frame", bHasFrame ? OUString("True") : OUString("False"));
                    xChild->getParentNode()->insertBefore(xHasFrame, xChild);
                    xRemoveList.push_back(xChild);
                }
            }

            if (sName == "always-show-image")
            {
                if (GetParentObjectType(xChild) == "GtkButton")
                {
                    // we will turn always-show-image into a GtkBox child for
                    // GtkButton and a GtkLabel child for the GtkBox and move
                    // the label property into it.
                    bAlwaysShowImage = toBool(xChild->getFirstChild()->getNodeValue());
                    xRemoveList.push_back(xChild);
                }
            }

            if (sName == "relief")
            {
                if (GetParentObjectType(xChild) == "GtkLinkButton"
                    || GetParentObjectType(xChild) == "GtkButton")
                {
                    assert(xChild->getFirstChild()->getNodeValue() == "none");
                    auto xDoc = xChild->getOwnerDocument();
                    auto xHasFrame = CreateProperty(xDoc, "has-frame", "False");
                    xChild->getParentNode()->insertBefore(xHasFrame, xChild);
                    xRemoveList.push_back(xChild);
                }
            }

            if (sName == "xalign")
            {
                if (GetParentObjectType(xChild) == "GtkLinkButton"
                    || GetParentObjectType(xChild) == "GtkMenuButton"
                    || GetParentObjectType(xChild) == "GtkButton")
                {
                    // TODO expand into a GtkLabel child with alignment on that instead
                    assert(xChild->getFirstChild()->getNodeValue() == "0");
                    xRemoveList.push_back(xChild);
                }
            }

            if (sName == "hscrollbar-policy")
            {
                if (GetParentObjectType(xChild) == "GtkScrolledWindow")
                {
                    if (xChild->getFirstChild()->getNodeValue() == "never")
                    {
                        auto xDoc = xChild->getOwnerDocument();
                        auto xHasFrame = CreateProperty(xDoc, "propagate-natural-width", "True");
                        xChild->getParentNode()->insertBefore(xHasFrame, xChild);
                    }
                }
            }

            if (sName == "vscrollbar-policy")
            {
                if (GetParentObjectType(xChild) == "GtkScrolledWindow")
                {
                    if (xChild->getFirstChild()->getNodeValue() == "never")
                    {
                        auto xDoc = xChild->getOwnerDocument();
                        auto xHasFrame = CreateProperty(xDoc, "propagate-natural-height", "True");
                        xChild->getParentNode()->insertBefore(xHasFrame, xChild);
                    }
                }
            }

            if (sName == "image")
            {
                if (GetParentObjectType(xChild) == "GtkButton")
                {
                    // find the image object, expected to be a child of "interface" and relocate
                    // it to be a child of this GtkButton
                    auto xObjectCandidate = xChild->getParentNode();
                    if (xObjectCandidate->getNodeName() == "object")
                    {
                        OUString sImageId = xChild->getFirstChild()->getNodeValue();

                        css::uno::Reference<css::xml::dom::XNode> xRootCandidate
                            = xChild->getParentNode();
                        while (xRootCandidate)
                        {
                            if (xRootCandidate->getNodeName() == "interface")
                                break;
                            xRootCandidate = xRootCandidate->getParentNode();
                        }

                        css::uno::Reference<css::xml::dom::XNode> xImageNode;

                        for (auto xImageCandidate = xRootCandidate->getFirstChild();
                             xImageCandidate.is();
                             xImageCandidate = xImageCandidate->getNextSibling())
                        {
                            css::uno::Reference<css::xml::dom::XNamedNodeMap> xImageCandidateMap
                                = xImageCandidate->getAttributes();
                            if (!xImageCandidateMap.is())
                                continue;
                            css::uno::Reference<css::xml::dom::XNode> xId
                                = xImageCandidateMap->getNamedItem("id");
                            if (xId && xId->getNodeValue() == sImageId)
                            {
                                xImageNode = xImageCandidate;
                                break;
                            }
                        }

                        auto xDoc = xChild->getOwnerDocument();
                        css::uno::Reference<css::xml::dom::XElement> xImageChild
                            = xDoc->createElement("child");
                        xImageChild->appendChild(
                            xImageNode->getParentNode()->removeChild(xImageNode));
                        xObjectCandidate->appendChild(xImageChild);
                    }

                    xRemoveList.push_back(xChild);
                }
            }

            if (sName == "draw-indicator")
            {
                assert(toBool(xChild->getFirstChild()->getNodeValue()));
                xRemoveList.push_back(xChild);
            }

            if (sName == "type-hint" || sName == "skip-taskbar-hint" || sName == "can-default"
                || sName == "border-width" || sName == "layout-style" || sName == "no-show-all"
                || sName == "ignore-hidden" || sName == "window-position")
            {
                xRemoveList.push_back(xChild);
            }
        }
        else if (xChild->getNodeName() == "child")
        {
            bool bContentArea = false;

            css::uno::Reference<css::xml::dom::XNamedNodeMap> xMap = xChild->getAttributes();
            css::uno::Reference<css::xml::dom::XNode> xName = xMap->getNamedItem("internal-child");
            if (xName)
            {
                OUString sName(xName->getNodeValue());
                if (sName == "vbox")
                {
                    xName->setNodeValue("content_area");
                    bContentArea = true;
                }
                else if (sName == "accessible")
                    xRemoveList.push_back(
                        xChild); // Yikes!, what's the replacement for this going to be
            }

            if (bContentArea)
            {
                for (css::uno::Reference<css::xml::dom::XNode> xObjectCandidate
                     = xChild->getFirstChild();
                     xObjectCandidate.is(); xObjectCandidate = xObjectCandidate->getNextSibling())
                {
                    if (xObjectCandidate->getNodeName() == "object")
                    {
                        auto xDoc = xChild->getOwnerDocument();

                        auto xVExpand = CreateProperty(xDoc, "vexpand", "True");
                        auto xFirstChild = xObjectCandidate->getFirstChild();
                        if (xFirstChild.is())
                            xObjectCandidate->insertBefore(xVExpand, xFirstChild);
                        else
                            xObjectCandidate->appendChild(xVExpand);

                        if (!sBorderWidth.isEmpty())
                        {
                            AddBorderAsMargins(xObjectCandidate, sBorderWidth);
                            sBorderWidth.clear();
                        }

                        break;
                    }
                }
            }
        }
        else if (xChild->getNodeName() == "packing")
        {
            // remove "packing" and if its grid packing insert a replacement "layout" into
            // the associated "object"
            auto xDoc = xChild->getOwnerDocument();
            css::uno::Reference<css::xml::dom::XElement> xNew = xDoc->createElement("layout");

            bool bGridPacking = false;

            // iterate over all children and append them to the new element
            for (css::uno::Reference<css::xml::dom::XNode> xCurrent = xChild->getFirstChild();
                 xCurrent.is(); xCurrent = xChild->getFirstChild())
            {
                css::uno::Reference<css::xml::dom::XNamedNodeMap> xMap = xCurrent->getAttributes();
                if (xMap.is())
                {
                    css::uno::Reference<css::xml::dom::XNode> xName = xMap->getNamedItem("name");
                    OUString sName(xName->getNodeValue().replace('_', '-'));
                    if (sName == "left-attach")
                    {
                        xName->setNodeValue("column");
                        bGridPacking = true;
                    }
                    else if (sName == "top-attach")
                    {
                        xName->setNodeValue("row");
                        bGridPacking = true;
                    }
                    else if (sName == "width")
                    {
                        xName->setNodeValue("column-span");
                        bGridPacking = true;
                    }
                    else if (sName == "height")
                    {
                        xName->setNodeValue("row-span");
                        bGridPacking = true;
                    }
                    else if (sName == "secondary")
                    {
                        // turn parent tag of <child> into <child type="start">
                        auto xParent = xChild->getParentNode();
                        css::uno::Reference<css::xml::dom::XAttr> xTypeStart
                            = xDoc->createAttribute("type");
                        xTypeStart->setValue("start");
                        css::uno::Reference<css::xml::dom::XElement> xElem(
                            xParent, css::uno::UNO_QUERY_THROW);
                        xElem->setAttributeNode(xTypeStart);
                    }
                }
                xNew->appendChild(xChild->removeChild(xCurrent));
            }

            if (bGridPacking)
            {
                // go back to parent and find the object child and insert this "layout" as a
                // new child of the object
                auto xParent = xChild->getParentNode();
                for (css::uno::Reference<css::xml::dom::XNode> xObjectCandidate
                     = xParent->getFirstChild();
                     xObjectCandidate.is(); xObjectCandidate = xObjectCandidate->getNextSibling())
                {
                    if (xObjectCandidate->getNodeName() == "object")
                    {
                        xObjectCandidate->appendChild(xNew);
                        break;
                    }
                }
            }

            xRemoveList.push_back(xChild);
        }
        else if (xChild->getNodeName() == "accessibility")
        {
            // TODO <relation type="labelled-by" target="pagenumcb"/> -> <relation name="labelled-by">pagenumcb</relation>
            xRemoveList.push_back(xChild);
        }
        else if (xChild->getNodeName() == "accelerator")
        {
            // TODO is anything like this supported anymore in .ui files
            xRemoveList.push_back(xChild);
        }

        auto xNextChild = xChild->getNextSibling();

        bool bChildHasIconName = false;
        bool bChildHasVisible = false;
        bool bChildAlwaysShowImage = false;
        css::uno::Reference<css::xml::dom::XNode> xChildPropertyLabel;
        if (xChild->hasChildNodes())
        {
            auto aChildRes = Convert3To4(xChild);
            bChildCanFocus |= aChildRes.m_bChildCanFocus;
            if (bChildCanFocus && xCantFocus.is())
            {
                xNode->removeChild(xCantFocus);
                xCantFocus.clear();
            }
            if (xChild->getNodeName() == "object")
            {
                bChildHasVisible = aChildRes.m_bHasVisible;
                bChildHasIconName = aChildRes.m_bHasIconName;
                bChildAlwaysShowImage = aChildRes.m_bAlwaysShowImage;
                xChildPropertyLabel = aChildRes.m_xPropertyLabel;
            }
        }

        if (xChild->getNodeName() == "object")
        {
            auto xDoc = xChild->getOwnerDocument();

            css::uno::Reference<css::xml::dom::XNamedNodeMap> xMap = xChild->getAttributes();
            css::uno::Reference<css::xml::dom::XNode> xClass = xMap->getNamedItem("class");
            OUString sClass(xClass->getNodeValue());

            auto xInternalChildCandidate = xChild->getParentNode();
            css::uno::Reference<css::xml::dom::XNamedNodeMap> xInternalChildCandidateMap
                = xInternalChildCandidate->getAttributes();
            css::uno::Reference<css::xml::dom::XNode> xId
                = xInternalChildCandidateMap->getNamedItem("internal-child");

            // turn default gtk3 invisibility for widget objects into explicit invisible, but ignore internal-children
            if (!bChildHasVisible && !xId)
            {
                if (sClass == "GtkBox" || sClass == "GtkButton" || sClass == "GtkCalendar"
                    || sClass == "GtkCheckButton" || sClass == "GtkRadioButton"
                    || sClass == "GtkComboBox" || sClass == "GtkComboBoxText"
                    || sClass == "GtkDrawingArea" || sClass == "GtkEntry" || sClass == "GtkExpander"
                    || sClass == "GtkFrame" || sClass == "GtkGrid" || sClass == "GtkImage"
                    || sClass == "GtkLabel" || sClass == "GtkMenuButton" || sClass == "GtkNotebook"
                    || sClass == "GtkOverlay" || sClass == "GtkPaned" || sClass == "GtkProgressBar"
                    || sClass == "GtkScrolledWindow" || sClass == "GtkSeparator"
                    || sClass == "GtkSpinButton" || sClass == "GtkSpinner"
                    || sClass == "GtkTextView" || sClass == "GtkTreeView" || sClass == "GtkViewport"
                    || sClass == "GtkLinkButton" || sClass == "GtkToggleButton"
                    || sClass == "GtkButtonBox")

                {
                    auto xVisible = CreateProperty(xDoc, "visible", "False");
                    auto xFirstChild = xChild->getFirstChild();
                    if (xFirstChild.is())
                        xChild->insertBefore(xVisible, xFirstChild);
                    else
                        xChild->appendChild(xVisible);
                }
            }

            if (sClass == "GtkButtonBox")
            {
                if (xId && xId->getNodeValue() == "action_area" && !ToplevelIsMessageDialog(xChild))
                {
                    xClass->setNodeValue("GtkHeaderBar");
                    auto xSpacingNode = CreateProperty(xDoc, "show-title-buttons", "False");
                    auto xFirstChild = xChild->getFirstChild();
                    if (xFirstChild.is())
                        xChild->insertBefore(xSpacingNode, xFirstChild);
                    else
                        xChild->appendChild(xSpacingNode);

                    // move the replacement GtkHeaderBar up to before the content_area
                    auto xContentAreaCandidate = xChild->getParentNode();
                    while (xContentAreaCandidate)
                    {
                        css::uno::Reference<css::xml::dom::XNamedNodeMap> xChildMap
                            = xContentAreaCandidate->getAttributes();
                        css::uno::Reference<css::xml::dom::XNode> xName
                            = xChildMap->getNamedItem("internal-child");
                        if (xName && xName->getNodeValue() == "content_area")
                        {
                            auto xActionArea = xChild->getParentNode();

                            xActionArea->getParentNode()->removeChild(xActionArea);

                            css::uno::Reference<css::xml::dom::XAttr> xTypeTitleBar
                                = xDoc->createAttribute("type");
                            xTypeTitleBar->setValue("titlebar");
                            css::uno::Reference<css::xml::dom::XElement> xElem(
                                xActionArea, css::uno::UNO_QUERY_THROW);
                            xElem->setAttributeNode(xTypeTitleBar);
                            xElem->removeAttribute("internal-child");

                            xContentAreaCandidate->getParentNode()->insertBefore(
                                xActionArea, xContentAreaCandidate);

                            std::vector<named_node> aChildren;

                            css::uno::Reference<css::xml::dom::XNode> xTitleChild
                                = xChild->getFirstChild();
                            while (xTitleChild.is())
                            {
                                auto xNextTitleChild = xTitleChild->getNextSibling();
                                if (xTitleChild->getNodeName() == "child")
                                {
                                    OUString sNodeId;

                                    for (css::uno::Reference<css::xml::dom::XNode> xObjectCandidate
                                         = xTitleChild->getFirstChild();
                                         xObjectCandidate.is();
                                         xObjectCandidate = xObjectCandidate->getNextSibling())
                                    {
                                        if (xObjectCandidate->getNodeName() == "object")
                                        {
                                            css::uno::Reference<css::xml::dom::XNamedNodeMap>
                                                xObjectMap = xObjectCandidate->getAttributes();
                                            css::uno::Reference<css::xml::dom::XNode> xObjectId
                                                = xObjectMap->getNamedItem("id");
                                            sNodeId = xObjectId->getNodeValue();
                                            break;
                                        }
                                    }

                                    aChildren.push_back(std::make_pair(xTitleChild, sNodeId));
                                }
                                else if (xTitleChild->getNodeName() == "property")
                                {
                                    // remove any <property name="homogeneous"> tag
                                    css::uno::Reference<css::xml::dom::XNamedNodeMap> xTitleChildMap
                                        = xTitleChild->getAttributes();
                                    css::uno::Reference<css::xml::dom::XNode> xPropName
                                        = xTitleChildMap->getNamedItem("name");
                                    OUString sPropName(xPropName->getNodeValue().replace('_', '-'));
                                    if (sPropName == "homogeneous")
                                        xChild->removeChild(xTitleChild);
                                }

                                xTitleChild = xNextTitleChild;
                            }

                            //sort child order within parent so that we match the platform button order
                            std::stable_sort(aChildren.begin(), aChildren.end(), sortButtonNodes);

                            int nNonHelpButtonCount = 0;

                            for (const auto& rTitleChild : aChildren)
                            {
                                xChild->removeChild(rTitleChild.first);
                                if (rTitleChild.second != "help")
                                    ++nNonHelpButtonCount;
                            }

                            std::reverse(aChildren.begin(), aChildren.end());

                            for (const auto& rTitleChild : aChildren)
                            {
                                xChild->appendChild(rTitleChild.first);

                                css::uno::Reference<css::xml::dom::XElement> xChildElem(
                                    rTitleChild.first, css::uno::UNO_QUERY_THROW);
                                if (!xChildElem->hasAttribute("type"))
                                {
                                    // turn parent tag of <child> into <child type="end"> except for cancel/close which we'll
                                    // put at start unless there is nothing at end
                                    css::uno::Reference<css::xml::dom::XAttr> xTypeEnd
                                        = xDoc->createAttribute("type");
                                    if (nNonHelpButtonCount >= 2
                                        && (rTitleChild.second == "cancel"
                                            || rTitleChild.second == "close"))
                                        xTypeEnd->setValue("start");
                                    else
                                        xTypeEnd->setValue("end");
                                    xChildElem->setAttributeNode(xTypeEnd);
                                }
                            }

                            auto xUseHeaderBar = CreateProperty(xDoc, "use-header-bar", "1");
                            SetPropertyOnTopLevel(xContentAreaCandidate, xUseHeaderBar);

                            break;
                        }
                        xContentAreaCandidate = xContentAreaCandidate->getParentNode();
                    }
                }
                else // GtkMessageDialog
                    xClass->setNodeValue("GtkBox");
            }
            else if (sClass == "GtkRadioButton")
            {
                xClass->setNodeValue("GtkCheckButton");
            }
            else if (sClass == "GtkImage" && !bChildHasIconName)
            {
                xClass->setNodeValue("GtkPicture");
            }
            else if (sClass == "GtkPopover" && !bHasVisible)
            {
                auto xVisible = CreateProperty(xDoc, "visible", "False");
                auto xFirstChild = xChild->getFirstChild();
                if (xFirstChild.is())
                    xChild->insertBefore(xVisible, xFirstChild);
                else
                    xChild->appendChild(xVisible);
            }

            if (bChildAlwaysShowImage)
            {
                auto xImageCandidateNode = xChild->getLastChild();
                if (xImageCandidateNode && xImageCandidateNode->getNodeName() != "child")
                    xImageCandidateNode.clear();
                if (xImageCandidateNode)
                    xChild->removeChild(xImageCandidateNode);

                css::uno::Reference<css::xml::dom::XElement> xNewChildNode
                    = xDoc->createElement("child");
                css::uno::Reference<css::xml::dom::XElement> xNewObjectNode
                    = xDoc->createElement("object");
                css::uno::Reference<css::xml::dom::XAttr> xBoxClassName
                    = xDoc->createAttribute("class");
                xBoxClassName->setValue("GtkBox");
                xNewObjectNode->setAttributeNode(xBoxClassName);
                xNewChildNode->appendChild(xNewObjectNode);

                xChild->appendChild(xNewChildNode);

                css::uno::Reference<css::xml::dom::XElement> xNewLabelChildNode
                    = xDoc->createElement("child");
                css::uno::Reference<css::xml::dom::XElement> xNewChildObjectNode
                    = xDoc->createElement("object");
                css::uno::Reference<css::xml::dom::XAttr> xLabelClassName
                    = xDoc->createAttribute("class");
                xLabelClassName->setValue("GtkLabel");
                xNewChildObjectNode->setAttributeNode(xLabelClassName);
                if (xChildPropertyLabel)
                    xNewChildObjectNode->appendChild(
                        xChildPropertyLabel->getParentNode()->removeChild(xChildPropertyLabel));
                xNewLabelChildNode->appendChild(xNewChildObjectNode);

                if (xImageCandidateNode)
                    xNewObjectNode->appendChild(xImageCandidateNode);
                xNewObjectNode->appendChild(xNewLabelChildNode);
            }
        }

        xChild = xNextChild;
    }

    if (!sBorderWidth.isEmpty())
        AddBorderAsMargins(xNode, sBorderWidth);

    for (auto& xRemove : xRemoveList)
        xNode->removeChild(xRemove);

    return ConvertResult(bChildCanFocus, bHasVisible, bHasIconName, bAlwaysShowImage,
                         xPropertyLabel);
}
}

void builder_add_from_gtk3_file(GtkBuilder* pBuilder, const OUString& rUri)
{
    GError* err = nullptr;

    // load the xml
    css::uno::Reference<css::uno::XComponentContext> xContext
        = ::comphelper::getProcessComponentContext();
    css::uno::Reference<css::xml::dom::XDocumentBuilder> xBuilder
        = css::xml::dom::DocumentBuilder::create(xContext);
    css::uno::Reference<css::xml::dom::XDocument> xDocument = xBuilder->parseURI(rUri);

    // convert it from gtk3 to gtk4
    Convert3To4(xDocument);

    css::uno::Reference<css::beans::XPropertySet> xTempFile(css::io::TempFile::create(xContext),
                                                            css::uno::UNO_QUERY);
    css::uno::Reference<css::io::XStream> xTempStream(xTempFile, css::uno::UNO_QUERY_THROW);
    xTempFile->setPropertyValue("RemoveFile", css::uno::makeAny(false));

    // serialize it back to xml
    css::uno::Reference<css::xml::sax::XSAXSerializable> xSerializer(xDocument,
                                                                     css::uno::UNO_QUERY);
    css::uno::Reference<css::xml::sax::XWriter> xWriter = css::xml::sax::Writer::create(xContext);
    css::uno::Reference<css::io::XOutputStream> xTempOut = xTempStream->getOutputStream();
    xWriter->setOutputStream(xTempOut);
    xSerializer->serialize(
        css::uno::Reference<css::xml::sax::XDocumentHandler>(xWriter, css::uno::UNO_QUERY_THROW),
        css::uno::Sequence<css::beans::StringPair>());

    // feed it to GtkBuilder
    css::uno::Reference<css::io::XSeekable> xTempSeek(xTempStream, css::uno::UNO_QUERY_THROW);
    xTempSeek->seek(0);
    auto xInput = xTempStream->getInputStream();
    css::uno::Sequence<sal_Int8> bytes;
    sal_Int32 nToRead = xInput->available();
    while (true)
    {
        sal_Int32 nRead = xInput->readBytes(bytes, std::max<sal_Int32>(nToRead, 4096));
        if (!nRead)
            break;
        // fprintf(stderr, "text is %s\n", reinterpret_cast<const gchar*>(bytes.getArray()));
        auto rc = gtk_builder_add_from_string(
            pBuilder, reinterpret_cast<const gchar*>(bytes.getArray()), nRead, &err);
        if (!rc)
        {
            SAL_WARN("vcl.gtk",
                     "GtkInstanceBuilder: error when calling gtk_builder_add_from_string: "
                         << err->message);
            g_error_free(err);
        }
        assert(rc && "could not load UI file");
        // in the real world the first loop has read the entire file because its all 'available' without blocking
    }
}

/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/vcl/unx/gtk4/gtkinst.cxx b/vcl/unx/gtk4/gtkinst.cxx
index a1066a6..e166777 100644
--- a/vcl/unx/gtk4/gtkinst.cxx
+++ b/vcl/unx/gtk4/gtkinst.cxx
@@ -10,6 +10,8 @@
// make gtk4 plug advertise correctly as gtk4
#define GTK_TOOLKIT_NAME "gtk4"

#include "convert3to4.hxx"

#include "../gtk3/gtkinst.cxx"

/* vim:set shiftwidth=4 softtabstop=4 expandtab: */