summaryrefslogtreecommitdiffstats
path: root/vcl/unx/gtk4
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--vcl/unx/gtk4/convert3to4.cxx1576
-rw-r--r--vcl/unx/gtk4/convert3to4.hxx16
-rw-r--r--vcl/unx/gtk4/customcellrenderer.cxx12
-rw-r--r--vcl/unx/gtk4/fpicker/SalGtkFilePicker.cxx12
-rw-r--r--vcl/unx/gtk4/fpicker/SalGtkFilePicker.hxx12
-rw-r--r--vcl/unx/gtk4/fpicker/SalGtkFolderPicker.cxx12
-rw-r--r--vcl/unx/gtk4/fpicker/SalGtkFolderPicker.hxx12
-rw-r--r--vcl/unx/gtk4/fpicker/SalGtkPicker.cxx12
-rw-r--r--vcl/unx/gtk4/fpicker/SalGtkPicker.hxx12
-rw-r--r--vcl/unx/gtk4/fpicker/eventnotification.hxx12
-rw-r--r--vcl/unx/gtk4/fpicker/resourceprovider.cxx12
-rw-r--r--vcl/unx/gtk4/gloactiongroup.cxx12
-rw-r--r--vcl/unx/gtk4/glomenu.cxx12
-rw-r--r--vcl/unx/gtk4/gtkcairo.cxx12
-rw-r--r--vcl/unx/gtk4/gtkcairo.hxx12
-rw-r--r--vcl/unx/gtk4/gtkdata.cxx12
-rw-r--r--vcl/unx/gtk4/gtkframe.cxx14
-rw-r--r--vcl/unx/gtk4/gtkinst.cxx21
-rw-r--r--vcl/unx/gtk4/gtkobject.cxx12
-rw-r--r--vcl/unx/gtk4/gtksalmenu.cxx12
-rw-r--r--vcl/unx/gtk4/gtksys.cxx12
-rw-r--r--vcl/unx/gtk4/hudawareness.cxx12
-rw-r--r--vcl/unx/gtk4/notifyinglayout.cxx88
-rw-r--r--vcl/unx/gtk4/notifyinglayout.hxx36
-rw-r--r--vcl/unx/gtk4/salnativewidgets-gtk.cxx12
-rw-r--r--vcl/unx/gtk4/surfacecellrenderer.cxx241
-rw-r--r--vcl/unx/gtk4/surfacecellrenderer.hxx42
-rw-r--r--vcl/unx/gtk4/surfacepaintable.cxx100
-rw-r--r--vcl/unx/gtk4/surfacepaintable.hxx32
-rw-r--r--vcl/unx/gtk4/transferableprovider.cxx118
-rw-r--r--vcl/unx/gtk4/transferableprovider.hxx51
31 files changed, 2563 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: */
diff --git a/vcl/unx/gtk4/convert3to4.hxx b/vcl/unx/gtk4/convert3to4.hxx
new file mode 100644
index 000000000..d75d9fd91
--- /dev/null
+++ b/vcl/unx/gtk4/convert3to4.hxx
@@ -0,0 +1,16 @@
+/* -*- 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 <rtl/ustring.hxx>
+#include <gtk/gtk.h>
+
+// convert gtk3 .ui to gtk4 and load it
+void builder_add_from_gtk3_file(GtkBuilder* pBuilder, const OUString& rUri);
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/vcl/unx/gtk4/customcellrenderer.cxx b/vcl/unx/gtk4/customcellrenderer.cxx
new file mode 100644
index 000000000..aebe2c89e
--- /dev/null
+++ b/vcl/unx/gtk4/customcellrenderer.cxx
@@ -0,0 +1,12 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */
+/*
+ * 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 "../gtk3/customcellrenderer.cxx"
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s cinkeys+=0=break: */
diff --git a/vcl/unx/gtk4/fpicker/SalGtkFilePicker.cxx b/vcl/unx/gtk4/fpicker/SalGtkFilePicker.cxx
new file mode 100644
index 000000000..a820b4995
--- /dev/null
+++ b/vcl/unx/gtk4/fpicker/SalGtkFilePicker.cxx
@@ -0,0 +1,12 @@
+/* -*- 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 "../../gtk3/fpicker/SalGtkFilePicker.cxx"
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/vcl/unx/gtk4/fpicker/SalGtkFilePicker.hxx b/vcl/unx/gtk4/fpicker/SalGtkFilePicker.hxx
new file mode 100644
index 000000000..44ea7bd44
--- /dev/null
+++ b/vcl/unx/gtk4/fpicker/SalGtkFilePicker.hxx
@@ -0,0 +1,12 @@
+/* -*- 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 "../../gtk3/fpicker/SalGtkFilePicker.hxx"
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/vcl/unx/gtk4/fpicker/SalGtkFolderPicker.cxx b/vcl/unx/gtk4/fpicker/SalGtkFolderPicker.cxx
new file mode 100644
index 000000000..41dfdf021
--- /dev/null
+++ b/vcl/unx/gtk4/fpicker/SalGtkFolderPicker.cxx
@@ -0,0 +1,12 @@
+/* -*- 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 "../../gtk3/fpicker/SalGtkFolderPicker.cxx"
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/vcl/unx/gtk4/fpicker/SalGtkFolderPicker.hxx b/vcl/unx/gtk4/fpicker/SalGtkFolderPicker.hxx
new file mode 100644
index 000000000..129a69933
--- /dev/null
+++ b/vcl/unx/gtk4/fpicker/SalGtkFolderPicker.hxx
@@ -0,0 +1,12 @@
+/* -*- 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 "../../gtk3/fpicker/SalGtkFolderPicker.hxx"
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/vcl/unx/gtk4/fpicker/SalGtkPicker.cxx b/vcl/unx/gtk4/fpicker/SalGtkPicker.cxx
new file mode 100644
index 000000000..159ef4704
--- /dev/null
+++ b/vcl/unx/gtk4/fpicker/SalGtkPicker.cxx
@@ -0,0 +1,12 @@
+/* -*- 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 "../../gtk3/fpicker/SalGtkPicker.cxx"
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/vcl/unx/gtk4/fpicker/SalGtkPicker.hxx b/vcl/unx/gtk4/fpicker/SalGtkPicker.hxx
new file mode 100644
index 000000000..0432004a0
--- /dev/null
+++ b/vcl/unx/gtk4/fpicker/SalGtkPicker.hxx
@@ -0,0 +1,12 @@
+/* -*- 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 "../../gtk3/fpicker/SalGtkPicker.hxx"
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/vcl/unx/gtk4/fpicker/eventnotification.hxx b/vcl/unx/gtk4/fpicker/eventnotification.hxx
new file mode 100644
index 000000000..b1e16bd65
--- /dev/null
+++ b/vcl/unx/gtk4/fpicker/eventnotification.hxx
@@ -0,0 +1,12 @@
+/* -*- 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 "../../gtk3/fpicker/eventnotification.hxx"
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/vcl/unx/gtk4/fpicker/resourceprovider.cxx b/vcl/unx/gtk4/fpicker/resourceprovider.cxx
new file mode 100644
index 000000000..d4251cc94
--- /dev/null
+++ b/vcl/unx/gtk4/fpicker/resourceprovider.cxx
@@ -0,0 +1,12 @@
+/* -*- 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 "../../gtk3/fpicker/resourceprovider.cxx"
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/vcl/unx/gtk4/gloactiongroup.cxx b/vcl/unx/gtk4/gloactiongroup.cxx
new file mode 100644
index 000000000..f147af283
--- /dev/null
+++ b/vcl/unx/gtk4/gloactiongroup.cxx
@@ -0,0 +1,12 @@
+/* -*- 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 "../gtk3/gloactiongroup.cxx"
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/vcl/unx/gtk4/glomenu.cxx b/vcl/unx/gtk4/glomenu.cxx
new file mode 100644
index 000000000..cae9bc03a
--- /dev/null
+++ b/vcl/unx/gtk4/glomenu.cxx
@@ -0,0 +1,12 @@
+/* -*- 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 "../gtk3/glomenu.cxx"
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/vcl/unx/gtk4/gtkcairo.cxx b/vcl/unx/gtk4/gtkcairo.cxx
new file mode 100644
index 000000000..3178fdea0
--- /dev/null
+++ b/vcl/unx/gtk4/gtkcairo.cxx
@@ -0,0 +1,12 @@
+/* -*- 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 "../gtk3/gtkcairo.cxx"
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/vcl/unx/gtk4/gtkcairo.hxx b/vcl/unx/gtk4/gtkcairo.hxx
new file mode 100644
index 000000000..97f63101d
--- /dev/null
+++ b/vcl/unx/gtk4/gtkcairo.hxx
@@ -0,0 +1,12 @@
+/* -*- 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 "../../gtk3/gtkdata.cxx"
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/vcl/unx/gtk4/gtkdata.cxx b/vcl/unx/gtk4/gtkdata.cxx
new file mode 100644
index 000000000..41d85217c
--- /dev/null
+++ b/vcl/unx/gtk4/gtkdata.cxx
@@ -0,0 +1,12 @@
+/* -*- 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 "../gtk3/gtkdata.cxx"
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/vcl/unx/gtk4/gtkframe.cxx b/vcl/unx/gtk4/gtkframe.cxx
new file mode 100644
index 000000000..e446ba8a5
--- /dev/null
+++ b/vcl/unx/gtk4/gtkframe.cxx
@@ -0,0 +1,14 @@
+/* -*- 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 "transferableprovider.hxx"
+
+#include "../gtk3/gtkframe.cxx"
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/vcl/unx/gtk4/gtkinst.cxx b/vcl/unx/gtk4/gtkinst.cxx
new file mode 100644
index 000000000..658c01233
--- /dev/null
+++ b/vcl/unx/gtk4/gtkinst.cxx
@@ -0,0 +1,21 @@
+/* -*- 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/.
+ */
+
+// make gtk4 plug advertise correctly as gtk4
+#define GTK_TOOLKIT_NAME "gtk4"
+
+#include "convert3to4.hxx"
+#include "notifyinglayout.hxx"
+#include "surfacecellrenderer.hxx"
+#include "surfacepaintable.hxx"
+#include "transferableprovider.hxx"
+
+#include "../gtk3/gtkinst.cxx"
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/vcl/unx/gtk4/gtkobject.cxx b/vcl/unx/gtk4/gtkobject.cxx
new file mode 100644
index 000000000..8eb5dcf1a
--- /dev/null
+++ b/vcl/unx/gtk4/gtkobject.cxx
@@ -0,0 +1,12 @@
+/* -*- 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 "../gtk3/gtkobject.cxx"
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/vcl/unx/gtk4/gtksalmenu.cxx b/vcl/unx/gtk4/gtksalmenu.cxx
new file mode 100644
index 000000000..895078124
--- /dev/null
+++ b/vcl/unx/gtk4/gtksalmenu.cxx
@@ -0,0 +1,12 @@
+/* -*- 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 "../gtk3/gtksalmenu.cxx"
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/vcl/unx/gtk4/gtksys.cxx b/vcl/unx/gtk4/gtksys.cxx
new file mode 100644
index 000000000..dfdcf0a7c
--- /dev/null
+++ b/vcl/unx/gtk4/gtksys.cxx
@@ -0,0 +1,12 @@
+/* -*- 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 "../gtk3/gtksys.cxx"
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/vcl/unx/gtk4/hudawareness.cxx b/vcl/unx/gtk4/hudawareness.cxx
new file mode 100644
index 000000000..b2683b2bd
--- /dev/null
+++ b/vcl/unx/gtk4/hudawareness.cxx
@@ -0,0 +1,12 @@
+/* -*- 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 "../gtk3/hudawareness.cxx"
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/vcl/unx/gtk4/notifyinglayout.cxx b/vcl/unx/gtk4/notifyinglayout.cxx
new file mode 100644
index 000000000..46b9a2d95
--- /dev/null
+++ b/vcl/unx/gtk4/notifyinglayout.cxx
@@ -0,0 +1,88 @@
+/* -*- 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 "notifyinglayout.hxx"
+
+struct _NotifyingLayout
+{
+ GtkLayoutManager parent_instance;
+
+ GtkWidget* m_pWidget;
+ GtkLayoutManager* m_pOrigManager;
+ Link<void*, void> m_aLink;
+};
+
+G_DEFINE_TYPE(NotifyingLayout, notifying_layout, GTK_TYPE_LAYOUT_MANAGER)
+
+static void notifying_layout_measure(GtkLayoutManager* pLayoutManager, GtkWidget* widget,
+ GtkOrientation orientation, int for_size, int* minimum,
+ int* natural, int* minimum_baseline, int* natural_baseline)
+{
+ NotifyingLayout* self = NOTIFYING_LAYOUT(pLayoutManager);
+ GtkLayoutManagerClass* pKlass
+ = GTK_LAYOUT_MANAGER_CLASS(G_OBJECT_GET_CLASS(self->m_pOrigManager));
+ pKlass->measure(self->m_pOrigManager, widget, orientation, for_size, minimum, natural,
+ minimum_baseline, natural_baseline);
+}
+
+static void notifying_layout_allocate(GtkLayoutManager* pLayoutManager, GtkWidget* widget,
+ int width, int height, int baseline)
+{
+ NotifyingLayout* self = NOTIFYING_LAYOUT(pLayoutManager);
+ GtkLayoutManagerClass* pKlass
+ = GTK_LAYOUT_MANAGER_CLASS(G_OBJECT_GET_CLASS(self->m_pOrigManager));
+ pKlass->allocate(self->m_pOrigManager, widget, width, height, baseline);
+ self->m_aLink.Call(nullptr);
+}
+
+static GtkSizeRequestMode notifying_layout_get_request_mode(GtkLayoutManager* pLayoutManager,
+ GtkWidget* widget)
+{
+ NotifyingLayout* self = NOTIFYING_LAYOUT(pLayoutManager);
+ GtkLayoutManagerClass* pKlass
+ = GTK_LAYOUT_MANAGER_CLASS(G_OBJECT_GET_CLASS(self->m_pOrigManager));
+ return pKlass->get_request_mode(self->m_pOrigManager, widget);
+}
+
+static void notifying_layout_class_init(NotifyingLayoutClass* klass)
+{
+ GtkLayoutManagerClass* layout_class = GTK_LAYOUT_MANAGER_CLASS(klass);
+
+ layout_class->get_request_mode = notifying_layout_get_request_mode;
+ layout_class->measure = notifying_layout_measure;
+ layout_class->allocate = notifying_layout_allocate;
+}
+
+static void notifying_layout_init(NotifyingLayout* self)
+{
+ self->m_pWidget = nullptr;
+ self->m_pOrigManager = nullptr;
+
+ // prevent loplugin:unreffun firing on macro generated function
+ (void)notifying_layout_get_instance_private(self);
+}
+
+void notifying_layout_start_watch(NotifyingLayout* self, GtkWidget* pWidget,
+ const Link<void*, void>& rLink)
+{
+ self->m_pWidget = pWidget;
+ self->m_aLink = rLink;
+
+ self->m_pOrigManager = gtk_widget_get_layout_manager(self->m_pWidget);
+ g_object_ref(self->m_pOrigManager);
+
+ gtk_widget_set_layout_manager(pWidget, GTK_LAYOUT_MANAGER(self));
+}
+
+void notifying_layout_stop_watch(NotifyingLayout* self)
+{
+ gtk_widget_set_layout_manager(self->m_pWidget, self->m_pOrigManager);
+}
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/vcl/unx/gtk4/notifyinglayout.hxx b/vcl/unx/gtk4/notifyinglayout.hxx
new file mode 100644
index 000000000..a717a3061
--- /dev/null
+++ b/vcl/unx/gtk4/notifyinglayout.hxx
@@ -0,0 +1,36 @@
+/* -*- 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/.
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+#include <tools/link.hxx>
+
+G_BEGIN_DECLS
+
+G_DECLARE_FINAL_TYPE(NotifyingLayout, notifying_layout, NOTIFYING, LAYOUT, GtkLayoutManager)
+
+/*
+ Replace the existing GtkLayoutManager of pWidget with pLayout instead which will
+ forward all requests to the original GtkLayoutManager but additionally call
+ rLink when a size is allocated to the pWidget.
+
+ This provides a workaround for the removal of the size-allocate signal in gtk4
+*/
+void notifying_layout_start_watch(NotifyingLayout* pLayout, GtkWidget* pWidget,
+ const Link<void*, void>& rLink);
+
+/*
+ Undo a previous notifying_layout_start_watch.
+*/
+void notifying_layout_stop_watch(NotifyingLayout* pLayout);
+
+G_END_DECLS
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/vcl/unx/gtk4/salnativewidgets-gtk.cxx b/vcl/unx/gtk4/salnativewidgets-gtk.cxx
new file mode 100644
index 000000000..ed036e98c
--- /dev/null
+++ b/vcl/unx/gtk4/salnativewidgets-gtk.cxx
@@ -0,0 +1,12 @@
+/* -*- 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 "../gtk3/salnativewidgets-gtk.cxx"
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/vcl/unx/gtk4/surfacecellrenderer.cxx b/vcl/unx/gtk4/surfacecellrenderer.cxx
new file mode 100644
index 000000000..2ae0782b9
--- /dev/null
+++ b/vcl/unx/gtk4/surfacecellrenderer.cxx
@@ -0,0 +1,241 @@
+/* -*- 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 <vcl/svapp.hxx>
+#include "surfacecellrenderer.hxx"
+#include <cairo/cairo-gobject.h>
+
+namespace
+{
+struct _SurfaceCellRendererClass : public GtkCellRendererClass
+{
+};
+
+enum
+{
+ PROP_SURFACE = 10000,
+};
+}
+
+G_DEFINE_TYPE(SurfaceCellRenderer, surface_cell_renderer, GTK_TYPE_CELL_RENDERER)
+
+static void surface_cell_renderer_init(SurfaceCellRenderer* self)
+{
+ self->surface = nullptr;
+ // prevent loplugin:unreffun firing on macro generated function
+ (void)surface_cell_renderer_get_instance_private(self);
+}
+
+static void surface_cell_renderer_get_property(GObject* object, guint param_id, GValue* value,
+ GParamSpec* pspec)
+{
+ SurfaceCellRenderer* cellsurface = SURFACE_CELL_RENDERER(object);
+
+ switch (param_id)
+ {
+ case PROP_SURFACE:
+ g_value_set_boxed(value, cellsurface->surface);
+ break;
+ default:
+ G_OBJECT_CLASS(surface_cell_renderer_parent_class)
+ ->get_property(object, param_id, value, pspec);
+ break;
+ }
+}
+
+static void surface_cell_renderer_set_property(GObject* object, guint param_id, const GValue* value,
+ GParamSpec* pspec)
+{
+ SurfaceCellRenderer* cellsurface = SURFACE_CELL_RENDERER(object);
+
+ switch (param_id)
+ {
+ case PROP_SURFACE:
+ if (cellsurface->surface)
+ cairo_surface_destroy(cellsurface->surface);
+ cellsurface->surface = static_cast<cairo_surface_t*>(g_value_get_boxed(value));
+ if (cellsurface->surface)
+ cairo_surface_reference(cellsurface->surface);
+ break;
+ default:
+ G_OBJECT_CLASS(surface_cell_renderer_parent_class)
+ ->set_property(object, param_id, value, pspec);
+ break;
+ }
+}
+
+static bool surface_cell_renderer_get_preferred_size(GtkCellRenderer* cell,
+ GtkOrientation orientation, gint* minimum_size,
+ gint* natural_size);
+
+static void surface_cell_renderer_snapshot(GtkCellRenderer* cell, GtkSnapshot* snapshot,
+ GtkWidget* widget, const GdkRectangle* background_area,
+ const GdkRectangle* cell_area,
+ GtkCellRendererState flags);
+
+static void surface_cell_renderer_render(GtkCellRenderer* cell, cairo_t* cr, GtkWidget* widget,
+ const GdkRectangle* background_area,
+ const GdkRectangle* cell_area, GtkCellRendererState flags);
+
+static void surface_cell_renderer_finalize(GObject* object)
+{
+ SurfaceCellRenderer* cellsurface = SURFACE_CELL_RENDERER(object);
+
+ if (cellsurface->surface)
+ cairo_surface_destroy(cellsurface->surface);
+
+ G_OBJECT_CLASS(surface_cell_renderer_parent_class)->finalize(object);
+}
+
+static void surface_cell_renderer_get_preferred_width(GtkCellRenderer* cell, GtkWidget* widget,
+ gint* minimum_size, gint* natural_size)
+{
+ if (!surface_cell_renderer_get_preferred_size(cell, GTK_ORIENTATION_HORIZONTAL, minimum_size,
+ natural_size))
+ {
+ // fallback to parent if we're empty
+ GTK_CELL_RENDERER_CLASS(surface_cell_renderer_parent_class)
+ ->get_preferred_width(cell, widget, minimum_size, natural_size);
+ }
+}
+
+static void surface_cell_renderer_get_preferred_height(GtkCellRenderer* cell, GtkWidget* widget,
+ gint* minimum_size, gint* natural_size)
+{
+ if (!surface_cell_renderer_get_preferred_size(cell, GTK_ORIENTATION_VERTICAL, minimum_size,
+ natural_size))
+ {
+ // fallback to parent if we're empty
+ GTK_CELL_RENDERER_CLASS(surface_cell_renderer_parent_class)
+ ->get_preferred_height(cell, widget, minimum_size, natural_size);
+ }
+}
+
+static void surface_cell_renderer_get_preferred_height_for_width(GtkCellRenderer* cell,
+ GtkWidget* widget, gint /*width*/,
+ gint* minimum_height,
+ gint* natural_height)
+{
+ gtk_cell_renderer_get_preferred_height(cell, widget, minimum_height, natural_height);
+}
+
+static void surface_cell_renderer_get_preferred_width_for_height(GtkCellRenderer* cell,
+ GtkWidget* widget, gint /*height*/,
+ gint* minimum_width,
+ gint* natural_width)
+{
+ gtk_cell_renderer_get_preferred_width(cell, widget, minimum_width, natural_width);
+}
+
+void surface_cell_renderer_class_init(SurfaceCellRendererClass* klass)
+{
+ GtkCellRendererClass* cell_class = GTK_CELL_RENDERER_CLASS(klass);
+ GObjectClass* object_class = G_OBJECT_CLASS(klass);
+
+ /* Hook up functions to set and get our custom cell renderer properties */
+ object_class->get_property = surface_cell_renderer_get_property;
+ object_class->set_property = surface_cell_renderer_set_property;
+
+ surface_cell_renderer_parent_class = g_type_class_peek_parent(klass);
+ object_class->finalize = surface_cell_renderer_finalize;
+
+ cell_class->get_preferred_width = surface_cell_renderer_get_preferred_width;
+ cell_class->get_preferred_height = surface_cell_renderer_get_preferred_height;
+ cell_class->get_preferred_width_for_height
+ = surface_cell_renderer_get_preferred_width_for_height;
+ cell_class->get_preferred_height_for_width
+ = surface_cell_renderer_get_preferred_height_for_width;
+
+ cell_class->snapshot = surface_cell_renderer_snapshot;
+
+ g_object_class_install_property(
+ object_class, PROP_SURFACE,
+ g_param_spec_boxed("surface", "Surface", "The cairo surface to render",
+ CAIRO_GOBJECT_TYPE_SURFACE, G_PARAM_READWRITE));
+}
+
+GtkCellRenderer* surface_cell_renderer_new()
+{
+ return GTK_CELL_RENDERER(g_object_new(SURFACE_TYPE_CELL_RENDERER, nullptr));
+}
+
+static void get_surface_size(cairo_surface_t* pSurface, int& rWidth, int& rHeight)
+{
+ double x1, x2, y1, y2;
+ cairo_t* cr = cairo_create(pSurface);
+ cairo_clip_extents(cr, &x1, &y1, &x2, &y2);
+ cairo_destroy(cr);
+
+ rWidth = x2 - x1;
+ rHeight = y2 - y1;
+}
+
+bool surface_cell_renderer_get_preferred_size(GtkCellRenderer* cell, GtkOrientation orientation,
+ gint* minimum_size, gint* natural_size)
+{
+ SurfaceCellRenderer* cellsurface = SURFACE_CELL_RENDERER(cell);
+
+ int nWidth = 0;
+ int nHeight = 0;
+
+ if (cellsurface->surface)
+ get_surface_size(cellsurface->surface, nWidth, nHeight);
+
+ if (orientation == GTK_ORIENTATION_HORIZONTAL)
+ {
+ if (minimum_size)
+ *minimum_size = nWidth;
+
+ if (natural_size)
+ *natural_size = nWidth;
+ }
+ else
+ {
+ if (minimum_size)
+ *minimum_size = nHeight;
+
+ if (natural_size)
+ *natural_size = nHeight;
+ }
+
+ return true;
+}
+
+void surface_cell_renderer_render(GtkCellRenderer* cell, cairo_t* cr, GtkWidget* /*widget*/,
+ const GdkRectangle* /*background_area*/,
+ const GdkRectangle* cell_area, GtkCellRendererState /*flags*/)
+{
+ SurfaceCellRenderer* cellsurface = SURFACE_CELL_RENDERER(cell);
+ if (!cellsurface->surface)
+ return;
+
+ int nWidth, nHeight;
+ get_surface_size(cellsurface->surface, nWidth, nHeight);
+ int nXOffset = (cell_area->width - nWidth) / 2;
+ int nYOffset = (cell_area->height - nHeight) / 2;
+
+ cairo_set_source_surface(cr, cellsurface->surface, cell_area->x + nXOffset,
+ cell_area->y + nYOffset);
+ cairo_paint(cr);
+}
+
+static void surface_cell_renderer_snapshot(GtkCellRenderer* cell, GtkSnapshot* snapshot,
+ GtkWidget* widget, const GdkRectangle* background_area,
+ const GdkRectangle* cell_area,
+ GtkCellRendererState flags)
+{
+ graphene_rect_t rect = GRAPHENE_RECT_INIT(
+ static_cast<float>(cell_area->x), static_cast<float>(cell_area->y),
+ static_cast<float>(cell_area->width), static_cast<float>(cell_area->height));
+ cairo_t* cr = gtk_snapshot_append_cairo(GTK_SNAPSHOT(snapshot), &rect);
+ surface_cell_renderer_render(cell, cr, widget, background_area, cell_area, flags);
+ cairo_destroy(cr);
+}
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/vcl/unx/gtk4/surfacecellrenderer.hxx b/vcl/unx/gtk4/surfacecellrenderer.hxx
new file mode 100644
index 000000000..d908c04ce
--- /dev/null
+++ b/vcl/unx/gtk4/surfacecellrenderer.hxx
@@ -0,0 +1,42 @@
+/* -*- 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/.
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+#include <cairo.h>
+
+G_BEGIN_DECLS
+
+struct _SurfaceCellRenderer
+{
+ GtkCellRenderer parent;
+ cairo_surface_t* surface;
+};
+
+/*
+ Provide a mechanism to support rendering a cairo surface in a GtkComboBox
+*/
+
+G_DECLARE_FINAL_TYPE(SurfaceCellRenderer, surface_cell_renderer, SURFACE, CELL_RENDERER,
+ GtkCellRenderer)
+
+#define SURFACE_TYPE_CELL_RENDERER (surface_cell_renderer_get_type())
+
+#define SURFACE_CELL_RENDERER(obj) \
+ (G_TYPE_CHECK_INSTANCE_CAST((obj), SURFACE_TYPE_CELL_RENDERER, SurfaceCellRenderer))
+
+#define SURFACE_IS_CELL_RENDERER(obj) \
+ (G_TYPE_CHECK_INSTANCE_TYPE((obj), SURFACE_TYPE_CELL_RENDERER))
+
+GtkCellRenderer* surface_cell_renderer_new();
+
+G_END_DECLS
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/vcl/unx/gtk4/surfacepaintable.cxx b/vcl/unx/gtk4/surfacepaintable.cxx
new file mode 100644
index 000000000..2e8aa98b2
--- /dev/null
+++ b/vcl/unx/gtk4/surfacepaintable.cxx
@@ -0,0 +1,100 @@
+/* -*- 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 "surfacepaintable.hxx"
+
+struct _SurfacePaintable
+{
+ GObject parent_instance;
+ int width;
+ int height;
+ cairo_surface_t* surface;
+};
+
+namespace
+{
+struct _SurfacePaintableClass : public GObjectClass
+{
+};
+}
+
+static void surface_paintable_snapshot(GdkPaintable* paintable, GdkSnapshot* snapshot, double width,
+ double height)
+{
+ graphene_rect_t rect
+ = GRAPHENE_RECT_INIT(0.0f, 0.0f, static_cast<float>(width), static_cast<float>(height));
+ SurfacePaintable* self = SURFACE_PAINTABLE(paintable);
+ cairo_t* cr = gtk_snapshot_append_cairo(GTK_SNAPSHOT(snapshot), &rect);
+ cairo_set_source_surface(cr, self->surface, 0, 0);
+ cairo_paint(cr);
+ cairo_destroy(cr);
+}
+
+static GdkPaintableFlags surface_paintable_get_flags(GdkPaintable* /*paintable*/)
+{
+ return static_cast<GdkPaintableFlags>(GDK_PAINTABLE_STATIC_SIZE
+ | GDK_PAINTABLE_STATIC_CONTENTS);
+}
+
+static int surface_paintable_get_intrinsic_width(GdkPaintable* paintable)
+{
+ SurfacePaintable* self = SURFACE_PAINTABLE(paintable);
+ return self->width;
+}
+
+static int surface_paintable_get_intrinsic_height(GdkPaintable* paintable)
+{
+ SurfacePaintable* self = SURFACE_PAINTABLE(paintable);
+ return self->height;
+}
+
+static void surface_paintable_init_interface(GdkPaintableInterface* iface)
+{
+ iface->snapshot = surface_paintable_snapshot;
+ iface->get_flags = surface_paintable_get_flags;
+ iface->get_intrinsic_width = surface_paintable_get_intrinsic_width;
+ iface->get_intrinsic_height = surface_paintable_get_intrinsic_height;
+}
+
+G_DEFINE_TYPE_WITH_CODE(SurfacePaintable, surface_paintable, G_TYPE_OBJECT,
+ G_IMPLEMENT_INTERFACE(GDK_TYPE_PAINTABLE,
+ surface_paintable_init_interface));
+
+static void surface_paintable_init(SurfacePaintable* self)
+{
+ self->width = 0;
+ self->height = 0;
+ self->surface = nullptr;
+
+ // prevent loplugin:unreffun firing on macro generated function
+ (void)surface_paintable_get_instance_private(self);
+}
+
+static void surface_paintable_dispose(GObject* object)
+{
+ SurfacePaintable* self = SURFACE_PAINTABLE(object);
+ cairo_surface_destroy(self->surface);
+ G_OBJECT_CLASS(surface_paintable_parent_class)->dispose(object);
+}
+
+static void surface_paintable_class_init(SurfacePaintableClass* klass)
+{
+ GObjectClass* object_class = G_OBJECT_CLASS(klass);
+ object_class->dispose = surface_paintable_dispose;
+}
+
+void surface_paintable_set_source(SurfacePaintable* pPaintable, cairo_surface_t* pSource,
+ int nWidth, int nHeight)
+{
+ pPaintable->surface = pSource;
+ pPaintable->width = nWidth;
+ pPaintable->height = nHeight;
+}
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/vcl/unx/gtk4/surfacepaintable.hxx b/vcl/unx/gtk4/surfacepaintable.hxx
new file mode 100644
index 000000000..3e8c79f8a
--- /dev/null
+++ b/vcl/unx/gtk4/surfacepaintable.hxx
@@ -0,0 +1,32 @@
+/* -*- 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/.
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+#include <cairo.h>
+
+G_BEGIN_DECLS
+
+/*
+ Provide a mechanism to allow continuing to use cairo surface where a GdkPaintable
+ is required
+*/
+
+G_DECLARE_FINAL_TYPE(SurfacePaintable, surface_paintable, SURFACE, PAINTABLE, GObject)
+
+/*
+ Set the surface to paint, takes ownership of pSource
+*/
+void surface_paintable_set_source(SurfacePaintable* pPaintable, cairo_surface_t* pSource,
+ int nWidth, int nHeight);
+
+G_END_DECLS
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/vcl/unx/gtk4/transferableprovider.cxx b/vcl/unx/gtk4/transferableprovider.cxx
new file mode 100644
index 000000000..554b80c0d
--- /dev/null
+++ b/vcl/unx/gtk4/transferableprovider.cxx
@@ -0,0 +1,118 @@
+/* -*- 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/datatransfer/XTransferable.hpp>
+#include <unx/gtk/gtkinst.hxx>
+#include "transferableprovider.hxx"
+
+#define TRANSFERABLE_CONTENT(obj) \
+ (G_TYPE_CHECK_INSTANCE_CAST((obj), transerable_content_get_type(), TransferableContent))
+
+struct _TransferableContent
+{
+ GdkContentProvider parent;
+ VclToGtkHelper* m_pConversionHelper;
+ css::datatransfer::XTransferable* m_pContents;
+ Link<void*, void> m_aDetachClipboardLink;
+};
+
+namespace
+{
+struct _TransferableContentClass : public GdkContentProviderClass
+{
+};
+}
+
+G_DEFINE_TYPE(TransferableContent, transerable_content, GDK_TYPE_CONTENT_PROVIDER)
+
+static void transerable_content_write_mime_type_async(GdkContentProvider* provider,
+ const char* mime_type, GOutputStream* stream,
+ int io_priority, GCancellable* cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ TransferableContent* self = TRANSFERABLE_CONTENT(provider);
+ if (!self->m_pContents)
+ return;
+ // tdf#129809 take a reference in case m_aContents is replaced during this
+ // call
+ css::uno::Reference<css::datatransfer::XTransferable> xCurrentContents(self->m_pContents);
+ self->m_pConversionHelper->setSelectionData(xCurrentContents, provider, mime_type, stream,
+ io_priority, cancellable, callback, user_data);
+}
+
+static gboolean transerable_content_write_mime_type_finish(GdkContentProvider*,
+ GAsyncResult* result, GError** error)
+{
+ return g_task_propagate_boolean(G_TASK(result), error);
+}
+
+static GdkContentFormats* transerable_content_ref_formats(GdkContentProvider* provider)
+{
+ TransferableContent* self = TRANSFERABLE_CONTENT(provider);
+ css::uno::Reference<css::datatransfer::XTransferable> xCurrentContents(self->m_pContents);
+ if (!xCurrentContents)
+ return nullptr;
+
+ auto aFormats = xCurrentContents->getTransferDataFlavors();
+ std::vector<OString> aGtkTargets(self->m_pConversionHelper->FormatsToGtk(aFormats));
+
+ GdkContentFormatsBuilder* pBuilder = gdk_content_formats_builder_new();
+ for (const auto& rFormat : aGtkTargets)
+ gdk_content_formats_builder_add_mime_type(pBuilder, rFormat.getStr());
+ return gdk_content_formats_builder_free_to_formats(pBuilder);
+}
+
+static void transerable_content_detach_clipboard(GdkContentProvider* provider, GdkClipboard*)
+{
+ TransferableContent* self = TRANSFERABLE_CONTENT(provider);
+ self->m_aDetachClipboardLink.Call(nullptr);
+}
+
+static void transerable_content_class_init(TransferableContentClass* klass)
+{
+ GdkContentProviderClass* provider_class = GDK_CONTENT_PROVIDER_CLASS(klass);
+
+ provider_class->ref_formats = transerable_content_ref_formats;
+ provider_class->detach_clipboard = transerable_content_detach_clipboard;
+ provider_class->write_mime_type_async = transerable_content_write_mime_type_async;
+ provider_class->write_mime_type_finish = transerable_content_write_mime_type_finish;
+}
+
+static void transerable_content_init(TransferableContent* self)
+{
+ self->m_pConversionHelper = nullptr;
+ self->m_pContents = nullptr;
+ // prevent loplugin:unreffun firing on macro generated function
+ (void)transerable_content_get_instance_private(self);
+}
+
+void transerable_content_set_transferable(TransferableContent* pContent,
+ css::datatransfer::XTransferable* pTransferable)
+{
+ pContent->m_pContents = pTransferable;
+}
+
+void transerable_content_set_detach_clipboard_link(TransferableContent* pContent,
+ const Link<void*, void>& rDetachClipboardLink)
+{
+ pContent->m_aDetachClipboardLink = rDetachClipboardLink;
+}
+
+GdkContentProvider* transerable_content_new(VclToGtkHelper* pConversionHelper,
+ css::datatransfer::XTransferable* pTransferable)
+{
+ TransferableContent* content
+ = TRANSFERABLE_CONTENT(g_object_new(transerable_content_get_type(), nullptr));
+ content->m_pConversionHelper = pConversionHelper;
+ content->m_pContents = pTransferable;
+ return GDK_CONTENT_PROVIDER(content);
+}
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/vcl/unx/gtk4/transferableprovider.hxx b/vcl/unx/gtk4/transferableprovider.hxx
new file mode 100644
index 000000000..df4d643d9
--- /dev/null
+++ b/vcl/unx/gtk4/transferableprovider.hxx
@@ -0,0 +1,51 @@
+/* -*- 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/.
+ */
+
+#pragma once
+
+#include <tools/link.hxx>
+
+#include <gtk/gtk.h>
+
+struct VclToGtkHelper;
+
+namespace com::sun::star::datatransfer
+{
+class XTransferable;
+}
+
+G_BEGIN_DECLS
+
+/*
+ Provide a mechanism to provide data from a LibreOffice XTransferable via a
+ GdkContentProvider for gtk clipboard or dnd
+*/
+
+G_DECLARE_FINAL_TYPE(TransferableContent, transerable_content, TRANSFERABLE, CONTENT,
+ GdkContentProvider)
+
+GdkContentProvider* transerable_content_new(VclToGtkHelper* pConversionHelper,
+ css::datatransfer::XTransferable* pTransferable);
+
+/*
+ Change to a new XTransferable
+ */
+void transerable_content_set_transferable(TransferableContent* pContent,
+ css::datatransfer::XTransferable* pTransferable);
+
+/*
+ If the GdkContentProvider is used by a clipboard call rDetachClipboardLink on losing
+ ownership of the clipboard
+*/
+void transerable_content_set_detach_clipboard_link(TransferableContent* pContent,
+ const Link<void*, void>& rDetachClipboardLink);
+
+G_END_DECLS
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */