summaryrefslogtreecommitdiffstats
path: root/vcl/unx/gtk4/convert3to4.cxx
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:06:44 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:06:44 +0000
commited5640d8b587fbcfed7dd7967f3de04b37a76f26 (patch)
tree7a5f7c6c9d02226d7471cb3cc8fbbf631b415303 /vcl/unx/gtk4/convert3to4.cxx
parentInitial commit. (diff)
downloadlibreoffice-upstream.tar.xz
libreoffice-upstream.zip
Adding upstream version 4:7.4.7.upstream/4%7.4.7upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'vcl/unx/gtk4/convert3to4.cxx')
-rw-r--r--vcl/unx/gtk4/convert3to4.cxx1576
1 files changed, 1576 insertions, 0 deletions
diff --git a/vcl/unx/gtk4/convert3to4.cxx b/vcl/unx/gtk4/convert3to4.cxx
new file mode 100644
index 000000000..e00009cd1
--- /dev/null
+++ b/vcl/unx/gtk4/convert3to4.cxx
@@ -0,0 +1,1576 @@
+/* -*- 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 <cairo/cairo-gobject.h>
+#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)
+{
+ OString sA(rA.second.toUtf8());
+ OString sB(rB.second.toUtf8());
+ //order within groups according to platform rules
+ return getButtonPriority(sA) < getButtonPriority(sB);
+}
+
+// <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 insertAsFirstChild(const css::uno::Reference<css::xml::dom::XNode>& xParentNode,
+ const css::uno::Reference<css::xml::dom::XNode>& xChildNode)
+{
+ auto xFirstChild = xParentNode->getFirstChild();
+ if (xFirstChild.is())
+ xParentNode->insertBefore(xChildNode, xFirstChild);
+ else
+ xParentNode->appendChild(xChildNode);
+}
+
+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")
+ {
+ insertAsFirstChild(xObjectCandidate, 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();
+}
+
+css::uno::Reference<css::xml::dom::XNode>
+GetChildObject(const css::uno::Reference<css::xml::dom::XNode>& xChild)
+{
+ for (css::uno::Reference<css::xml::dom::XNode> xObjectCandidate = xChild->getFirstChild();
+ xObjectCandidate.is(); xObjectCandidate = xObjectCandidate->getNextSibling())
+ {
+ if (xObjectCandidate->getNodeName() == "object")
+ return xObjectCandidate;
+ }
+ return nullptr;
+}
+
+// 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);
+ insertAsFirstChild(xNode, 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 MenuEntry
+{
+ bool m_bDrawAsRadio;
+ css::uno::Reference<css::xml::dom::XNode> m_xPropertyLabel;
+
+ MenuEntry(bool bDrawAsRadio, const css::uno::Reference<css::xml::dom::XNode>& rPropertyLabel)
+ : m_bDrawAsRadio(bDrawAsRadio)
+ , m_xPropertyLabel(rPropertyLabel)
+ {
+ }
+};
+
+MenuEntry ConvertMenu(css::uno::Reference<css::xml::dom::XNode>& xMenuSection,
+ const css::uno::Reference<css::xml::dom::XNode>& xNode)
+{
+ bool bDrawAsRadio = false;
+ css::uno::Reference<css::xml::dom::XNode> xPropertyLabel;
+
+ css::uno::Reference<css::xml::dom::XNode> xChild = xNode->getFirstChild();
+ while (xChild.is())
+ {
+ 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 == "label")
+ {
+ xPropertyLabel = xChild;
+ }
+ else if (sName == "draw-as-radio")
+ {
+ bDrawAsRadio = toBool(xChild->getFirstChild()->getNodeValue());
+ }
+ }
+
+ auto xNextChild = xChild->getNextSibling();
+
+ auto xCurrentMenuSection = xMenuSection;
+
+ 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());
+
+ if (sClass == "GtkMenuItem" || sClass == "GtkRadioMenuItem")
+ {
+ /* <item> */
+ css::uno::Reference<css::xml::dom::XElement> xItem = xDoc->createElement("item");
+ xMenuSection->appendChild(xItem);
+ }
+ else if (sClass == "GtkSeparatorMenuItem")
+ {
+ /* <section> */
+ css::uno::Reference<css::xml::dom::XElement> xSection
+ = xDoc->createElement("section");
+ xMenuSection->getParentNode()->appendChild(xSection);
+ xMenuSection = xSection;
+ xCurrentMenuSection = xMenuSection;
+ }
+ else if (sClass == "GtkMenu")
+ {
+ xMenuSection->removeChild(xMenuSection->getLastChild()); // remove preceding <item>
+
+ css::uno::Reference<css::xml::dom::XElement> xSubMenu
+ = xDoc->createElement("submenu");
+ css::uno::Reference<css::xml::dom::XAttr> xIdAttr = xDoc->createAttribute("id");
+
+ css::uno::Reference<css::xml::dom::XNode> xId = xMap->getNamedItem("id");
+ OUString sId(xId->getNodeValue());
+
+ xIdAttr->setValue(sId);
+ xSubMenu->setAttributeNode(xIdAttr);
+ xMenuSection->appendChild(xSubMenu);
+
+ css::uno::Reference<css::xml::dom::XElement> xSection
+ = xDoc->createElement("section");
+ xSubMenu->appendChild(xSection);
+
+ xMenuSection = xSubMenu;
+ }
+ }
+
+ bool bChildDrawAsRadio = false;
+ css::uno::Reference<css::xml::dom::XNode> xChildPropertyLabel;
+ if (xChild->hasChildNodes())
+ {
+ MenuEntry aEntry = ConvertMenu(xMenuSection, xChild);
+ bChildDrawAsRadio = aEntry.m_bDrawAsRadio;
+ xChildPropertyLabel = aEntry.m_xPropertyLabel;
+ }
+
+ if (xChild->getNodeName() == "object")
+ {
+ xMenuSection = xCurrentMenuSection;
+
+ 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());
+
+ if (sClass == "GtkMenuItem" || sClass == "GtkRadioMenuItem")
+ {
+ css::uno::Reference<css::xml::dom::XNode> xId = xMap->getNamedItem("id");
+ OUString sId = xId->getNodeValue();
+
+ /*
+ <attribute name='label' translatable='yes'>whatever</attribute>
+ <attribute name='action'>menu.action</attribute>
+ <attribute name='target'>id</attribute>
+ */
+ auto xItem = xMenuSection->getLastChild();
+
+ if (xChildPropertyLabel)
+ {
+ css::uno::Reference<css::xml::dom::XElement> xChildPropertyElem(
+ xChildPropertyLabel, css::uno::UNO_QUERY_THROW);
+
+ css::uno::Reference<css::xml::dom::XElement> xLabelAttr
+ = xDoc->createElement("attribute");
+
+ css::uno::Reference<css::xml::dom::XNamedNodeMap> xLabelMap
+ = xChildPropertyLabel->getAttributes();
+ while (xLabelMap->getLength())
+ {
+ css::uno::Reference<css::xml::dom::XAttr> xAttr(xLabelMap->item(0),
+ css::uno::UNO_QUERY_THROW);
+ xLabelAttr->setAttributeNode(
+ xChildPropertyElem->removeAttributeNode(xAttr));
+ }
+ xLabelAttr->appendChild(
+ xChildPropertyLabel->removeChild(xChildPropertyLabel->getFirstChild()));
+
+ xChildPropertyLabel->getParentNode()->removeChild(xChildPropertyLabel);
+ xItem->appendChild(xLabelAttr);
+ }
+
+ css::uno::Reference<css::xml::dom::XElement> xActionAttr
+ = xDoc->createElement("attribute");
+ css::uno::Reference<css::xml::dom::XAttr> xActionName
+ = xDoc->createAttribute("name");
+ xActionName->setValue("action");
+ xActionAttr->setAttributeNode(xActionName);
+ if (bChildDrawAsRadio)
+ xActionAttr->appendChild(xDoc->createTextNode("menu.radio." + sId));
+ else
+ xActionAttr->appendChild(xDoc->createTextNode("menu.normal." + sId));
+ xItem->appendChild(xActionAttr);
+
+ css::uno::Reference<css::xml::dom::XElement> xTargetAttr
+ = xDoc->createElement("attribute");
+ css::uno::Reference<css::xml::dom::XAttr> xTargetName
+ = xDoc->createAttribute("name");
+ xTargetName->setValue("target");
+ xTargetAttr->setAttributeNode(xTargetName);
+ xTargetAttr->appendChild(xDoc->createTextNode(sId));
+ xItem->appendChild(xTargetAttr);
+
+ css::uno::Reference<css::xml::dom::XElement> xHiddenWhenAttr
+ = xDoc->createElement("attribute");
+ css::uno::Reference<css::xml::dom::XAttr> xHiddenWhenName
+ = xDoc->createAttribute("name");
+ xHiddenWhenName->setValue("hidden-when");
+ xHiddenWhenAttr->setAttributeNode(xHiddenWhenName);
+ xHiddenWhenAttr->appendChild(xDoc->createTextNode("action-missing"));
+ xItem->appendChild(xHiddenWhenAttr);
+ }
+ }
+
+ xChild = xNextChild;
+ }
+
+ return MenuEntry(bDrawAsRadio, xPropertyLabel);
+}
+
+struct ConvertResult
+{
+ bool m_bChildCanFocus;
+ bool m_bHasVisible;
+ bool m_bHasIconSize;
+ bool m_bAlwaysShowImage;
+ bool m_bUseUnderline;
+ bool m_bVertOrientation;
+ bool m_bXAlign;
+ GtkPositionType m_eImagePos;
+ css::uno::Reference<css::xml::dom::XNode> m_xPropertyLabel;
+ css::uno::Reference<css::xml::dom::XNode> m_xPropertyIconName;
+
+ ConvertResult(bool bChildCanFocus, bool bHasVisible, bool bHasIconSize, bool bAlwaysShowImage,
+ bool bUseUnderline, bool bVertOrientation, bool bXAlign,
+ GtkPositionType eImagePos,
+ const css::uno::Reference<css::xml::dom::XNode>& rPropertyLabel,
+ const css::uno::Reference<css::xml::dom::XNode>& rPropertyIconName)
+ : m_bChildCanFocus(bChildCanFocus)
+ , m_bHasVisible(bHasVisible)
+ , m_bHasIconSize(bHasIconSize)
+ , m_bAlwaysShowImage(bAlwaysShowImage)
+ , m_bUseUnderline(bUseUnderline)
+ , m_bVertOrientation(bVertOrientation)
+ , m_bXAlign(bXAlign)
+ , m_eImagePos(eImagePos)
+ , m_xPropertyLabel(rPropertyLabel)
+ , m_xPropertyIconName(rPropertyIconName)
+ {
+ }
+};
+
+bool IsAllowedBuiltInIcon(std::u16string_view iconName)
+{
+ // limit the named icons to those known by VclBuilder
+ return VclBuilder::mapStockToSymbol(iconName) != SymbolType::DONTKNOW;
+}
+
+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, false, false, false, GTK_POS_LEFT, nullptr,
+ nullptr);
+ }
+
+ std::vector<css::uno::Reference<css::xml::dom::XNode>> xRemoveList;
+
+ OUString sBorderWidth;
+ bool bChildCanFocus = false;
+ bool bHasVisible = false;
+ bool bHasIconSize = false;
+ bool bAlwaysShowImage = false;
+ GtkPositionType eImagePos = GTK_POS_LEFT;
+ bool bUseUnderline = false;
+ bool bVertOrientation = false;
+ bool bXAlign = false;
+ css::uno::Reference<css::xml::dom::XNode> xPropertyLabel;
+ css::uno::Reference<css::xml::dom::XNode> xPropertyIconName;
+ css::uno::Reference<css::xml::dom::XNode> xCantFocus;
+
+ css::uno::Reference<css::xml::dom::XElement> xGeneratedImageChild;
+
+ 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"
+ || sParentClass == "GtkViewport")
+ {
+ // 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")
+ {
+ OUString sParentClass = GetParentObjectType(xChild);
+ if (sParentClass == "GtkToolButton" || sParentClass == "GtkMenuToolButton"
+ || sParentClass == "GtkToggleToolButton")
+ {
+ xName->setNodeValue("tooltip-text");
+ }
+ xPropertyLabel = xChild;
+ }
+
+ if (sName == "modal")
+ {
+ OUString sParentClass = GetParentObjectType(xChild);
+ if (sParentClass == "GtkPopover")
+ xName->setNodeValue("autohide");
+ }
+
+ if (sName == "visible")
+ bHasVisible = true;
+
+ if (sName == "icon-name")
+ xPropertyIconName = xChild;
+
+ if (sName == "show-arrow")
+ xRemoveList.push_back(xChild);
+
+ if (sName == "events")
+ xRemoveList.push_back(xChild);
+
+ if (sName == "constrain-to")
+ 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. Unless the toplevel is a message dialog
+ if (sName == "label" && GetParentObjectType(xChild) == "GtkButton"
+ && !ToplevelIsMessageDialog(xChild))
+ {
+ 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")
+ {
+ bHasIconSize = true;
+
+ OUString sSize = xChild->getFirstChild()->getNodeValue();
+ /*
+ old:
+ 3 -> GTK_ICON_SIZE_LARGE_TOOLBAR: Size appropriate for large toolbars (24px)
+ 5 -> GTK_ICON_SIZE_DND: Size appropriate for drag and drop (32px)
+ 6 -> GTK_ICON_SIZE_DIALOG: Size appropriate for dialogs (48px)
+
+ new:
+ 2 -> GTK_ICON_SIZE_LARGE
+ */
+ if (sSize == "3" || sSize == "5" || sSize == "6")
+ {
+ auto xDoc = xChild->getOwnerDocument();
+ auto xIconSize = CreateProperty(xDoc, "icon-size", "2");
+ xChild->getParentNode()->insertBefore(xIconSize, xChild);
+ }
+
+ xRemoveList.push_back(xChild);
+ }
+
+ if (GetParentObjectType(xChild) == "GtkToolbar")
+ xRemoveList.push_back(xChild);
+ }
+
+ if (sName == "truncate-multiline")
+ {
+ if (GetParentObjectType(xChild) == "GtkSpinButton")
+ xRemoveList.push_back(xChild);
+ }
+
+ if (sName == "has-frame")
+ {
+ if (GetParentObjectType(xChild) == "GtkSpinButton")
+ xRemoveList.push_back(xChild);
+ }
+
+ if (sName == "toolbar-style")
+ {
+ // is there an equivalent for this ?
+ 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"
+ || GetParentObjectType(xChild) == "GtkMenuButton"
+ || GetParentObjectType(xChild) == "GtkToggleButton")
+ {
+ // 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 == "image-position")
+ {
+ 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.
+ OUString sImagePos = xChild->getFirstChild()->getNodeValue();
+ if (sImagePos == "top")
+ eImagePos = GTK_POS_TOP;
+ else if (sImagePos == "bottom")
+ eImagePos = GTK_POS_BOTTOM;
+ else if (sImagePos == "right")
+ eImagePos = GTK_POS_RIGHT;
+ else
+ assert(sImagePos == "left");
+ xRemoveList.push_back(xChild);
+ }
+ }
+
+ if (sName == "use-underline")
+ bUseUnderline = toBool(xChild->getFirstChild()->getNodeValue());
+
+ if (sName == "orientation")
+ bVertOrientation = xChild->getFirstChild()->getNodeValue() == "vertical";
+
+ if (sName == "relief")
+ {
+ if (GetParentObjectType(xChild) == "GtkToggleButton"
+ || GetParentObjectType(xChild) == "GtkMenuButton"
+ || 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");
+ bXAlign = true;
+ xRemoveList.push_back(xChild);
+ }
+ }
+
+ if (sName == "use-popover")
+ {
+ if (GetParentObjectType(xChild) == "GtkMenuButton")
+ 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 == "popup")
+ {
+ if (GetParentObjectType(xChild) == "GtkMenuButton")
+ {
+ OUString sMenuName = xChild->getFirstChild()->getNodeValue();
+ auto xDoc = xChild->getOwnerDocument();
+ auto xPopover = CreateProperty(xDoc, "popover", sMenuName);
+ xChild->getParentNode()->insertBefore(xPopover, xChild);
+ xRemoveList.push_back(xChild);
+ }
+ }
+
+ if (sName == "image")
+ {
+ if (GetParentObjectType(xChild) == "GtkButton"
+ || GetParentObjectType(xChild) == "GtkMenuButton"
+ || GetParentObjectType(xChild) == "GtkToggleButton")
+ {
+ // find the image object, expected to be a child of "interface"
+ 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();
+
+ // relocate it to be a child of this GtkButton
+ xGeneratedImageChild = xDoc->createElement("child");
+ xGeneratedImageChild->appendChild(
+ xImageNode->getParentNode()->removeChild(xImageNode));
+ xObjectCandidate->appendChild(xGeneratedImageChild);
+ }
+
+ xRemoveList.push_back(xChild);
+ }
+ }
+
+ if (sName == "draw-indicator")
+ {
+ assert(toBool(xChild->getFirstChild()->getNodeValue()));
+ if (GetParentObjectType(xChild) == "GtkMenuButton" && gtk_get_minor_version() >= 4)
+ {
+ auto xDoc = xChild->getOwnerDocument();
+ auto xAlwaysShowArrow = CreateProperty(xDoc, "always-show-arrow", "True");
+ xChild->getParentNode()->insertBefore(xAlwaysShowArrow, xChild);
+ }
+ 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);
+ }
+
+ if (sName == "AtkObject::accessible-description")
+ xName->setNodeValue("description");
+
+ if (sName == "AtkObject::accessible-name")
+ xName->setNodeValue("label");
+ }
+ 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);
+ }
+ }
+
+ if (bContentArea)
+ {
+ css::uno::Reference<css::xml::dom::XNode> xObject = GetChildObject(xChild);
+ if (xObject)
+ {
+ auto xDoc = xChild->getOwnerDocument();
+
+ auto xVExpand = CreateProperty(xDoc, "vexpand", "True");
+ insertAsFirstChild(xObject, xVExpand);
+
+ if (!sBorderWidth.isEmpty())
+ {
+ AddBorderAsMargins(xObject, sBorderWidth);
+ sBorderWidth.clear();
+ }
+ }
+ }
+ }
+ 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);
+ }
+ else if (sName == "pack-type")
+ {
+ // turn parent tag of <child> into <child type="start">
+ auto xParent = xChild->getParentNode();
+
+ css::uno::Reference<css::xml::dom::XNamedNodeMap> xParentMap
+ = xParent->getAttributes();
+ css::uno::Reference<css::xml::dom::XNode> xParentType
+ = xParentMap->getNamedItem("type");
+ assert(!xParentType || xParentType->getNodeValue() == "titlebar");
+ if (!xParentType)
+ {
+ css::uno::Reference<css::xml::dom::XAttr> xTypeStart
+ = xDoc->createAttribute("type");
+ xTypeStart->setValue(xCurrent->getFirstChild()->getNodeValue());
+ 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();
+ css::uno::Reference<css::xml::dom::XNode> xObject = GetChildObject(xParent);
+ if (xObject)
+ xObject->appendChild(xNew);
+ }
+
+ xRemoveList.push_back(xChild);
+ }
+ else if (xChild->getNodeName() == "accelerator")
+ {
+ // TODO is anything like this supported anymore in .ui files
+ xRemoveList.push_back(xChild);
+ }
+ else if (xChild->getNodeName() == "relation")
+ {
+ auto xDoc = xChild->getOwnerDocument();
+
+ css::uno::Reference<css::xml::dom::XNamedNodeMap> xMap = xChild->getAttributes();
+ css::uno::Reference<css::xml::dom::XNode> xType = xMap->getNamedItem("type");
+ if (xType->getNodeValue() == "label-for")
+ {
+ // there will be a matching labelled-by which should be sufficient in the gtk4 world
+ xRemoveList.push_back(xChild);
+ }
+ if (xType->getNodeValue() == "controlled-by")
+ {
+ // there will be a matching controller-for converted to -> controls
+ // which should be sufficient in the gtk4 world
+ xRemoveList.push_back(xChild);
+ }
+ else
+ {
+ css::uno::Reference<css::xml::dom::XNode> xTarget = xMap->getNamedItem("target");
+
+ css::uno::Reference<css::xml::dom::XText> xValue
+ = xDoc->createTextNode(xTarget->getNodeValue());
+ xChild->appendChild(xValue);
+
+ css::uno::Reference<css::xml::dom::XAttr> xName = xDoc->createAttribute("name");
+ if (xType->getNodeValue() == "controller-for")
+ xName->setValue("controls");
+ else
+ xName->setValue(xType->getNodeValue());
+
+ css::uno::Reference<css::xml::dom::XElement> xElem(xChild,
+ css::uno::UNO_QUERY_THROW);
+ xElem->setAttributeNode(xName);
+ xElem->removeAttribute("type");
+ xElem->removeAttribute("target");
+ }
+ }
+
+ auto xNextChild = xChild->getNextSibling();
+
+ 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());
+
+ if (sClass == "GtkMenu")
+ {
+ css::uno::Reference<css::xml::dom::XNode> xId = xMap->getNamedItem("id");
+ OUString sId(xId->getNodeValue() + "-menu-model");
+
+ // <menu id='menubar'>
+ css::uno::Reference<css::xml::dom::XElement> xMenu = xDoc->createElement("menu");
+ css::uno::Reference<css::xml::dom::XAttr> xIdAttr = xDoc->createAttribute("id");
+ xIdAttr->setValue(sId);
+ xMenu->setAttributeNode(xIdAttr);
+ xChild->getParentNode()->insertBefore(xMenu, xChild);
+
+ css::uno::Reference<css::xml::dom::XElement> xSection
+ = xDoc->createElement("section");
+ xMenu->appendChild(xSection);
+
+ css::uno::Reference<css::xml::dom::XNode> xMenuSection(xSection);
+ ConvertMenu(xMenuSection, xChild);
+
+ // now remove GtkMenu contents
+ while (true)
+ {
+ auto xFirstChild = xChild->getFirstChild();
+ if (!xFirstChild.is())
+ break;
+ xChild->removeChild(xFirstChild);
+ }
+
+ // change to GtkPopoverMenu
+ xClass->setNodeValue("GtkPopoverMenu");
+
+ // <property name="menu-model">
+ xChild->appendChild(CreateProperty(xDoc, "menu-model", sId));
+ xChild->appendChild(CreateProperty(xDoc, "visible", "False"));
+ }
+ }
+
+ bool bChildHasIconSize = false;
+ bool bChildHasVisible = false;
+ bool bChildAlwaysShowImage = false;
+ GtkPositionType eChildImagePos = GTK_POS_LEFT;
+ bool bChildUseUnderline = false;
+ bool bChildVertOrientation = false;
+ bool bChildXAlign = false;
+ css::uno::Reference<css::xml::dom::XNode> xChildPropertyLabel;
+ css::uno::Reference<css::xml::dom::XNode> xChildPropertyIconName;
+ if (xChild->hasChildNodes() && xChild != xGeneratedImageChild)
+ {
+ 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;
+ bChildHasIconSize = aChildRes.m_bHasIconSize;
+ bChildAlwaysShowImage = aChildRes.m_bAlwaysShowImage;
+ eChildImagePos = aChildRes.m_eImagePos;
+ bChildUseUnderline = aChildRes.m_bUseUnderline;
+ bChildVertOrientation = aChildRes.m_bVertOrientation;
+ bChildXAlign = aChildRes.m_bXAlign;
+ xChildPropertyLabel = aChildRes.m_xPropertyLabel;
+ xChildPropertyIconName = aChildRes.m_xPropertyIconName;
+ }
+ }
+
+ 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> xInternalChild
+ = xInternalChildCandidateMap->getNamedItem("internal-child");
+
+ // turn default gtk3 invisibility for widget objects into explicit invisible, but ignore internal-children
+ if (!bChildHasVisible && !xInternalChild)
+ {
+ 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");
+ insertAsFirstChild(xChild, xVisible);
+ }
+ }
+
+ if (sClass == "GtkButtonBox")
+ {
+ if (xInternalChild && xInternalChild->getNodeValue() == "action_area"
+ && !ToplevelIsMessageDialog(xChild))
+ {
+ xClass->setNodeValue("GtkHeaderBar");
+ auto xSpacingNode = CreateProperty(xDoc, "show-title-buttons", "False");
+ insertAsFirstChild(xChild, 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;
+
+ css::uno::Reference<css::xml::dom::XNode> xObject
+ = GetChildObject(xTitleChild);
+ if (xObject)
+ {
+ css::uno::Reference<css::xml::dom::XNamedNodeMap> xObjectMap
+ = xObject->getAttributes();
+ css::uno::Reference<css::xml::dom::XNode> xObjectId
+ = xObjectMap->getNamedItem("id");
+ sNodeId = xObjectId->getNodeValue();
+ }
+
+ 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 == "GtkToolbar")
+ {
+ xClass->setNodeValue("GtkBox");
+ css::uno::Reference<css::xml::dom::XElement> xStyle = xDoc->createElement("style");
+ css::uno::Reference<css::xml::dom::XElement> xToolbarClass
+ = xDoc->createElement("class");
+ css::uno::Reference<css::xml::dom::XAttr> xPropName = xDoc->createAttribute("name");
+ xPropName->setValue("toolbar");
+ xToolbarClass->setAttributeNode(xPropName);
+ xStyle->appendChild(xToolbarClass);
+ xChild->appendChild(xStyle);
+ }
+ else if (sClass == "GtkToolButton")
+ {
+ xClass->setNodeValue("GtkButton");
+ }
+ else if (sClass == "GtkToolItem")
+ {
+ xClass->setNodeValue("GtkBox");
+ }
+ else if (sClass == "GtkMenuToolButton")
+ {
+ xClass->setNodeValue("GtkMenuButton");
+ if (gtk_get_minor_version() >= 4)
+ {
+ auto xAlwaysShowArrow = CreateProperty(xDoc, "always-show-arrow", "True");
+ insertAsFirstChild(xChild, xAlwaysShowArrow);
+ }
+ }
+ else if (sClass == "GtkRadioToolButton")
+ {
+ xClass->setNodeValue("GtkCheckButton");
+ }
+ else if (sClass == "GtkToggleToolButton")
+ {
+ xClass->setNodeValue("GtkToggleButton");
+ }
+ else if (sClass == "GtkSeparatorToolItem")
+ {
+ xClass->setNodeValue("GtkSeparator");
+ }
+ else if (sClass == "GtkBox")
+ {
+ // reverse the order of the pack-type=end widgets
+ std::vector<css::uno::Reference<css::xml::dom::XNode>> aPackEnds;
+ std::vector<css::uno::Reference<css::xml::dom::XNode>> aPackStarts;
+ css::uno::Reference<css::xml::dom::XNode> xBoxChild = xChild->getFirstChild();
+ while (xBoxChild.is())
+ {
+ auto xNextBoxChild = xBoxChild->getNextSibling();
+
+ if (xBoxChild->getNodeName() == "child")
+ {
+ css::uno::Reference<css::xml::dom::XNamedNodeMap> xBoxChildMap
+ = xBoxChild->getAttributes();
+ css::uno::Reference<css::xml::dom::XNode> xType
+ = xBoxChildMap->getNamedItem("type");
+ if (xType && xType->getNodeValue() == "end")
+ aPackEnds.push_back(xChild->removeChild(xBoxChild));
+ else
+ aPackStarts.push_back(xBoxChild);
+ }
+
+ xBoxChild = xNextBoxChild;
+ }
+
+ if (!aPackEnds.empty())
+ {
+ std::reverse(aPackEnds.begin(), aPackEnds.end());
+
+ if (!bChildVertOrientation)
+ {
+ bool bHasStartObject = false;
+ bool bLastStartExpands = false;
+ if (!aPackStarts.empty())
+ {
+ css::uno::Reference<css::xml::dom::XNode> xLastStartObject;
+ for (auto it = aPackStarts.rbegin(); it != aPackStarts.rend(); ++it)
+ {
+ xLastStartObject = GetChildObject(*it);
+ if (xLastStartObject.is())
+ {
+ bHasStartObject = true;
+ for (css::uno::Reference<css::xml::dom::XNode> xExpandCandidate
+ = xLastStartObject->getFirstChild();
+ xExpandCandidate.is();
+ xExpandCandidate = xExpandCandidate->getNextSibling())
+ {
+ if (xExpandCandidate->getNodeName() == "property")
+ {
+ css::uno::Reference<css::xml::dom::XNamedNodeMap>
+ xExpandMap = xExpandCandidate->getAttributes();
+ css::uno::Reference<css::xml::dom::XNode> xName
+ = xExpandMap->getNamedItem("name");
+ OUString sPropName(xName->getNodeValue());
+ if (sPropName == "hexpand")
+ {
+ bLastStartExpands
+ = toBool(xExpandCandidate->getFirstChild()
+ ->getNodeValue());
+ break;
+ }
+ }
+ }
+ break;
+ }
+ }
+ }
+
+ if (bHasStartObject && !bLastStartExpands)
+ {
+ auto xAlign = CreateProperty(xDoc, "halign", "end");
+ insertAsFirstChild(GetChildObject(aPackEnds[0]), xAlign);
+ auto xExpand = CreateProperty(xDoc, "hexpand", "True");
+ insertAsFirstChild(GetChildObject(aPackEnds[0]), xExpand);
+ }
+ }
+
+ for (auto& xPackEnd : aPackEnds)
+ xChild->appendChild(xPackEnd);
+ }
+ }
+ else if (sClass == "GtkRadioButton")
+ {
+ xClass->setNodeValue("GtkCheckButton");
+ }
+ else if (sClass == "GtkImage")
+ {
+ /* a) keep symbolic icon-names as GtkImage, e.g. writer, format, columns, next/prev
+ buttons
+ b) assume that an explicit icon-size represents a request for a scaled icon
+ so keep those as GtkImage. e.g. hyperlink dialog notebook tab images and
+ calc paste special button images
+ c) turn everything else into a GtkPicture, e.g. help, about. If a GtkPicture
+ ends up used to display just a simple icon that's generally not a problem.
+ */
+ bool bKeepAsImage = false;
+ if (bChildHasIconSize)
+ bKeepAsImage = true;
+ else if (xChildPropertyIconName.is())
+ {
+ OUString sIconName(xChildPropertyIconName->getFirstChild()->getNodeValue());
+ bool bHasSymbolicIconName = IsAllowedBuiltInIcon(sIconName);
+ if (bHasSymbolicIconName)
+ {
+ if (sIconName != "missing-image")
+ bKeepAsImage = true;
+ else
+ {
+ // If the symbolic icon-name is missing-image then decide to make
+ // it a GtkPicture if it has a parent widget and keep it as GtkImage
+ // if it has just the root "interface" as parent.
+ // for e.g. view, user interface
+ css::uno::Reference<css::xml::dom::XNode> xParent
+ = xChild->getParentNode();
+ bKeepAsImage = xParent->getNodeName() == "interface";
+ if (!bKeepAsImage)
+ xChild->removeChild(xChildPropertyIconName);
+ }
+ }
+ else
+ {
+ // private:graphicrepository/ would be turned by gio (?) into private:///graphicrepository/
+ // so use private:///graphicrepository/ here. At the moment we just want this to be transported
+ // as-is to postprocess_widget. Though it might be nice to register a protocol handler with gio
+ // to avoid us doing the load in that second pass.
+ auto xUri = CreateProperty(xDoc, "file",
+ "private:///graphicrepository/" + sIconName);
+ xChild->insertBefore(xUri, xChildPropertyIconName);
+ // calc, insert, header and footer, custom header menubutton icon
+ auto xCanShrink = CreateProperty(xDoc, "can-shrink", "False");
+ xChild->insertBefore(xCanShrink, xChildPropertyIconName);
+ xChild->removeChild(xChildPropertyIconName);
+ }
+ }
+ if (!bKeepAsImage)
+ xClass->setNodeValue("GtkPicture");
+ }
+ else if (sClass == "GtkPopover" && !bHasVisible)
+ {
+ auto xVisible = CreateProperty(xDoc, "visible", "False");
+ insertAsFirstChild(xChild, xVisible);
+ }
+ else if (sClass == "AtkObject")
+ {
+ css::uno::Reference<css::xml::dom::XNode> xParentObject = xNode->getParentNode();
+ css::uno::Reference<css::xml::dom::XElement> xAccessibility
+ = xDoc->createElement("accessibility");
+ xParentObject->insertBefore(xAccessibility, xNode);
+
+ // move the converted old AtkObject:: properties into a new accessibility parent
+ for (css::uno::Reference<css::xml::dom::XNode> xCurrent = xChild->getFirstChild();
+ xCurrent.is(); xCurrent = xChild->getFirstChild())
+ {
+ xAccessibility->appendChild(xChild->removeChild(xCurrent));
+ }
+ }
+
+ // only create the child box for GtkButton/GtkToggleButton
+ if (bChildAlwaysShowImage)
+ {
+ auto xImageCandidateNode = xChild->getLastChild();
+ if (xImageCandidateNode && xImageCandidateNode->getNodeName() != "child")
+ xImageCandidateNode.clear();
+ if (xImageCandidateNode)
+ xChild->removeChild(xImageCandidateNode);
+
+ // for GtkMenuButton if this is a gearmenu with just an icon
+ // then "icon-name" is used for the indicator and there is
+ // expected to be no text. If there is a GtkPicture then treat
+ // this like a GtkButton and presumably it's a ToggleMenuButton
+ // and the relocation of contents happens in the builder
+ if (sClass == "GtkMenuButton")
+ {
+ bChildAlwaysShowImage = false;
+ if (xImageCandidateNode)
+ {
+ bChildAlwaysShowImage = true;
+ auto xImageObject = GetChildObject(xImageCandidateNode);
+ auto xProp = xImageObject->getFirstChild();
+ while (xProp.is())
+ {
+ if (xProp->getNodeName() == "property")
+ {
+ css::uno::Reference<css::xml::dom::XNamedNodeMap> xPropMap
+ = xProp->getAttributes();
+ css::uno::Reference<css::xml::dom::XNode> xPropName
+ = xPropMap->getNamedItem("name");
+ OUString sPropName(xPropName->getNodeValue().replace('_', '-'));
+ if (sPropName == "icon-name")
+ {
+ OUString sIconName(xProp->getFirstChild()->getNodeValue());
+ auto xIconName = CreateProperty(xDoc, "icon-name", sIconName);
+ insertAsFirstChild(xChild, xIconName);
+ bChildAlwaysShowImage = false;
+ break;
+ }
+ }
+
+ xProp = xProp->getNextSibling();
+ }
+ }
+ }
+
+ if (bChildAlwaysShowImage)
+ {
+ 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);
+
+ if (eChildImagePos == GTK_POS_TOP || eChildImagePos == GTK_POS_BOTTOM)
+ {
+ auto xOrientation = CreateProperty(xDoc, "orientation", "vertical");
+ xNewObjectNode->appendChild(xOrientation);
+ }
+
+ xNewObjectNode->appendChild(CreateProperty(xDoc, "spacing", "6"));
+ if (!bChildXAlign)
+ xNewObjectNode->appendChild(CreateProperty(xDoc, "halign", "center"));
+
+ 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));
+ }
+ else
+ {
+ auto xNotVisible = CreateProperty(xDoc, "visible", "False");
+ xNewChildObjectNode->appendChild(xNotVisible);
+ }
+ if (bChildUseUnderline)
+ {
+ auto xUseUnderline = CreateProperty(xDoc, "use-underline", "True");
+ xNewChildObjectNode->appendChild(xUseUnderline);
+ }
+ xNewLabelChildNode->appendChild(xNewChildObjectNode);
+
+ if (eChildImagePos == GTK_POS_LEFT || eChildImagePos == GTK_POS_TOP)
+ {
+ if (xImageCandidateNode)
+ xNewObjectNode->appendChild(xImageCandidateNode);
+ xNewObjectNode->appendChild(xNewLabelChildNode);
+ }
+ else
+ {
+ xNewObjectNode->appendChild(xNewLabelChildNode);
+ if (xImageCandidateNode)
+ xNewObjectNode->appendChild(xImageCandidateNode);
+ }
+ }
+ }
+ }
+
+ xChild = xNextChild;
+ }
+
+ if (!sBorderWidth.isEmpty())
+ AddBorderAsMargins(xNode, sBorderWidth);
+
+ for (auto& xRemove : xRemoveList)
+ xNode->removeChild(xRemove);
+
+ // https://gitlab.gnome.org/GNOME/gtk/-/issues/4041 double encode ampersands if use-underline is used
+ if (gtk_check_version(4, 3, 2) != nullptr)
+ {
+ if (xPropertyLabel)
+ {
+ auto xLabelText = xPropertyLabel->getFirstChild();
+ if (xLabelText.is())
+ {
+ OString sText = xLabelText->getNodeValue().toUtf8();
+ gchar* pText = g_markup_escape_text(sText.getStr(), sText.getLength());
+ xLabelText->setNodeValue(OUString(pText, strlen(pText), RTL_TEXTENCODING_UTF8));
+ g_free(pText);
+ }
+ }
+ }
+
+ return ConvertResult(bChildCanFocus, bHasVisible, bHasIconSize, bAlwaysShowImage, bUseUnderline,
+ bVertOrientation, bXAlign, eImagePos, xPropertyLabel, xPropertyIconName);
+}
+
+std::once_flag cairo_surface_type_flag;
+
+void ensure_cairo_surface_type()
+{
+ // cairo_gobject_surface_get_type needs to be called at least once for
+ // g_type_from_name to be able to resolve "CairoSurface". In gtk3 there was fallback
+ // mechanism to attempt to resolve such "missing" types which is not in place for
+ // gtk4 so ensure it can be found explicitly
+ std::call_once(cairo_surface_type_flag, []() { cairo_gobject_surface_get_type(); });
+}
+}
+
+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_THROW);
+ css::uno::Reference<css::io::XStream> xTempStream(xTempFile, css::uno::UNO_QUERY_THROW);
+ xTempFile->setPropertyValue("RemoveFile", css::uno::Any(false));
+
+ // serialize it back to xml
+ css::uno::Reference<css::xml::sax::XSAXSerializable> xSerializer(xDocument,
+ css::uno::UNO_QUERY_THROW);
+ 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>());
+
+ ensure_cairo_surface_type();
+
+ // 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()));
+ bool 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: */