summaryrefslogtreecommitdiffstats
path: root/vcl/unx/gtk4
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-15 05:54:39 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-15 05:54:39 +0000
commit267c6f2ac71f92999e969232431ba04678e7437e (patch)
tree358c9467650e1d0a1d7227a21dac2e3d08b622b2 /vcl/unx/gtk4
parentInitial commit. (diff)
downloadlibreoffice-267c6f2ac71f92999e969232431ba04678e7437e.tar.xz
libreoffice-267c6f2ac71f92999e969232431ba04678e7437e.zip
Adding upstream version 4:24.2.0.upstream/4%24.2.0
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'vcl/unx/gtk4')
-rw-r--r--vcl/unx/gtk4/a11y.cxx851
-rw-r--r--vcl/unx/gtk4/a11y.hxx24
-rw-r--r--vcl/unx/gtk4/convert3to4.cxx1603
-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
33 files changed, 3465 insertions, 0 deletions
diff --git a/vcl/unx/gtk4/a11y.cxx b/vcl/unx/gtk4/a11y.cxx
new file mode 100644
index 0000000000..c8103471b0
--- /dev/null
+++ b/vcl/unx/gtk4/a11y.cxx
@@ -0,0 +1,851 @@
+/* -*- 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 <com/sun/star/accessibility/AccessibleRole.hpp>
+#include <com/sun/star/accessibility/AccessibleStateType.hpp>
+#include <com/sun/star/accessibility/XAccessibleComponent.hpp>
+#include <com/sun/star/accessibility/XAccessibleExtendedAttributes.hpp>
+#include <com/sun/star/accessibility/XAccessibleText.hpp>
+#include <com/sun/star/accessibility/XAccessibleValue.hpp>
+#include <unx/gtk/gtkframe.hxx>
+#include <gtk/gtk.h>
+#include <o3tl/string_view.hxx>
+
+#if GTK_CHECK_VERSION(4, 9, 0)
+
+#include "a11y.hxx"
+
+#define OOO_TYPE_FIXED (ooo_fixed_get_type())
+#define OOO_FIXED(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), OOO_TYPE_FIXED, OOoFixed))
+// #define OOO_IS_FIXED(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), OOO_TYPE_FIXED))
+
+struct OOoFixed
+{
+ GtkFixed parent_instance;
+ GtkATContext* at_context;
+};
+
+struct OOoFixedClass
+{
+ GtkFixedClass parent_class;
+};
+
+static GtkAccessibleRole
+map_accessible_role(const css::uno::Reference<css::accessibility::XAccessible>& rAccessible)
+{
+ GtkAccessibleRole eRole(GTK_ACCESSIBLE_ROLE_WIDGET);
+
+ if (rAccessible.is())
+ {
+ css::uno::Reference<css::accessibility::XAccessibleContext> xContext(
+ rAccessible->getAccessibleContext());
+ // https://w3c.github.io/core-aam/#mapping_role
+ // https://hg.mozilla.org/mozilla-central/file/tip/accessible/base/RoleMap.h
+ // https://gitlab.gnome.org/GNOME/gtk/-/blob/main/gtk/a11y/gtkatspiutils.c
+ switch (xContext->getAccessibleRole())
+ {
+ case css::accessibility::AccessibleRole::ALERT:
+ case css::accessibility::AccessibleRole::NOTIFICATION:
+ eRole = GTK_ACCESSIBLE_ROLE_ALERT;
+ break;
+ case css::accessibility::AccessibleRole::BLOCK_QUOTE:
+#if GTK_CHECK_VERSION(4, 13, 4)
+ eRole = GTK_ACCESSIBLE_ROLE_BLOCK_QUOTE;
+#else
+ eRole = GTK_ACCESSIBLE_ROLE_GROUP;
+#endif
+ break;
+ case css::accessibility::AccessibleRole::CAPTION:
+ eRole = GTK_ACCESSIBLE_ROLE_CAPTION;
+ break;
+ case css::accessibility::AccessibleRole::COLUMN_HEADER:
+ eRole = GTK_ACCESSIBLE_ROLE_COLUMN_HEADER;
+ break;
+ case css::accessibility::AccessibleRole::COMBO_BOX:
+ eRole = GTK_ACCESSIBLE_ROLE_COMBO_BOX;
+ break;
+ case css::accessibility::AccessibleRole::DIALOG:
+ case css::accessibility::AccessibleRole::FILE_CHOOSER:
+ eRole = GTK_ACCESSIBLE_ROLE_DIALOG;
+ break;
+ case css::accessibility::AccessibleRole::FOOTNOTE:
+ case css::accessibility::AccessibleRole::NOTE:
+ eRole = GTK_ACCESSIBLE_ROLE_NOTE;
+ break;
+ case css::accessibility::AccessibleRole::FORM:
+ eRole = GTK_ACCESSIBLE_ROLE_FORM;
+ break;
+ case css::accessibility::AccessibleRole::HEADING:
+ eRole = GTK_ACCESSIBLE_ROLE_HEADING;
+ break;
+ case css::accessibility::AccessibleRole::HYPER_LINK:
+ eRole = GTK_ACCESSIBLE_ROLE_LINK;
+ break;
+ case css::accessibility::AccessibleRole::PANEL:
+ eRole = GTK_ACCESSIBLE_ROLE_GROUP;
+ break;
+ case css::accessibility::AccessibleRole::ROOT_PANE:
+ eRole = GTK_ACCESSIBLE_ROLE_GROUP;
+ break;
+ case css::accessibility::AccessibleRole::MENU_BAR:
+ eRole = GTK_ACCESSIBLE_ROLE_MENU_BAR;
+ break;
+ case css::accessibility::AccessibleRole::STATUS_BAR:
+ eRole = GTK_ACCESSIBLE_ROLE_STATUS;
+ break;
+ case css::accessibility::AccessibleRole::MENU:
+ case css::accessibility::AccessibleRole::POPUP_MENU:
+ eRole = GTK_ACCESSIBLE_ROLE_MENU;
+ break;
+ case css::accessibility::AccessibleRole::SPLIT_PANE:
+ eRole = GTK_ACCESSIBLE_ROLE_GROUP;
+ break;
+ case css::accessibility::AccessibleRole::TOOL_BAR:
+ eRole = GTK_ACCESSIBLE_ROLE_TOOLBAR;
+ break;
+ case css::accessibility::AccessibleRole::LABEL:
+ case css::accessibility::AccessibleRole::STATIC:
+ eRole = GTK_ACCESSIBLE_ROLE_LABEL;
+ break;
+ case css::accessibility::AccessibleRole::LIST:
+ eRole = GTK_ACCESSIBLE_ROLE_LIST;
+ break;
+ case css::accessibility::AccessibleRole::LIST_ITEM:
+ eRole = GTK_ACCESSIBLE_ROLE_LIST_ITEM;
+ break;
+ case css::accessibility::AccessibleRole::MENU_ITEM:
+ eRole = GTK_ACCESSIBLE_ROLE_MENU_ITEM;
+ break;
+ case css::accessibility::AccessibleRole::SEPARATOR:
+ eRole = GTK_ACCESSIBLE_ROLE_SEPARATOR;
+ break;
+ case css::accessibility::AccessibleRole::CHECK_BOX:
+ eRole = GTK_ACCESSIBLE_ROLE_CHECKBOX;
+ break;
+ case css::accessibility::AccessibleRole::CHECK_MENU_ITEM:
+ eRole = GTK_ACCESSIBLE_ROLE_MENU_ITEM_CHECKBOX;
+ break;
+ case css::accessibility::AccessibleRole::RADIO_MENU_ITEM:
+ eRole = GTK_ACCESSIBLE_ROLE_MENU_ITEM_RADIO;
+ break;
+ case css::accessibility::AccessibleRole::DOCUMENT:
+ case css::accessibility::AccessibleRole::DOCUMENT_PRESENTATION:
+ case css::accessibility::AccessibleRole::DOCUMENT_SPREADSHEET:
+ case css::accessibility::AccessibleRole::DOCUMENT_TEXT:
+ eRole = GTK_ACCESSIBLE_ROLE_DOCUMENT;
+ break;
+ case css::accessibility::AccessibleRole::ROW_HEADER:
+ eRole = GTK_ACCESSIBLE_ROLE_ROW_HEADER;
+ break;
+ case css::accessibility::AccessibleRole::RULER:
+ eRole = GTK_ACCESSIBLE_ROLE_WIDGET;
+ break;
+ case css::accessibility::AccessibleRole::PARAGRAPH:
+#if GTK_CHECK_VERSION(4, 13, 1)
+ eRole = GTK_ACCESSIBLE_ROLE_PARAGRAPH;
+#else
+ eRole = GTK_ACCESSIBLE_ROLE_GROUP;
+#endif
+ break;
+ case css::accessibility::AccessibleRole::FILLER:
+ eRole = GTK_ACCESSIBLE_ROLE_GENERIC;
+ break;
+ case css::accessibility::AccessibleRole::PUSH_BUTTON:
+ case css::accessibility::AccessibleRole::BUTTON_DROPDOWN:
+ case css::accessibility::AccessibleRole::BUTTON_MENU:
+ eRole = GTK_ACCESSIBLE_ROLE_BUTTON;
+ break;
+ case css::accessibility::AccessibleRole::TOGGLE_BUTTON:
+ eRole = GTK_ACCESSIBLE_ROLE_TOGGLE_BUTTON;
+ break;
+ case css::accessibility::AccessibleRole::TABLE:
+ eRole = GTK_ACCESSIBLE_ROLE_TABLE;
+ break;
+ case css::accessibility::AccessibleRole::TABLE_CELL:
+ eRole = GTK_ACCESSIBLE_ROLE_CELL;
+ break;
+ case css::accessibility::AccessibleRole::PAGE_TAB:
+ eRole = GTK_ACCESSIBLE_ROLE_TAB;
+ break;
+ case css::accessibility::AccessibleRole::PAGE_TAB_LIST:
+ eRole = GTK_ACCESSIBLE_ROLE_TAB_LIST;
+ break;
+ case css::accessibility::AccessibleRole::PROGRESS_BAR:
+ eRole = GTK_ACCESSIBLE_ROLE_PROGRESS_BAR;
+ break;
+ case css::accessibility::AccessibleRole::RADIO_BUTTON:
+ eRole = GTK_ACCESSIBLE_ROLE_RADIO;
+ break;
+ case css::accessibility::AccessibleRole::SCROLL_BAR:
+ eRole = GTK_ACCESSIBLE_ROLE_SCROLLBAR;
+ break;
+ case css::accessibility::AccessibleRole::SLIDER:
+ eRole = GTK_ACCESSIBLE_ROLE_SLIDER;
+ break;
+ case css::accessibility::AccessibleRole::SPIN_BOX:
+ eRole = GTK_ACCESSIBLE_ROLE_SPIN_BUTTON;
+ break;
+ case css::accessibility::AccessibleRole::TEXT:
+ case css::accessibility::AccessibleRole::PASSWORD_TEXT:
+ eRole = GTK_ACCESSIBLE_ROLE_TEXT_BOX;
+ break;
+ case css::accessibility::AccessibleRole::TOOL_TIP:
+ eRole = GTK_ACCESSIBLE_ROLE_TOOLTIP;
+ break;
+ case css::accessibility::AccessibleRole::TREE:
+ eRole = GTK_ACCESSIBLE_ROLE_TREE;
+ break;
+ case css::accessibility::AccessibleRole::TREE_ITEM:
+ eRole = GTK_ACCESSIBLE_ROLE_TREE_ITEM;
+ break;
+ case css::accessibility::AccessibleRole::TREE_TABLE:
+ eRole = GTK_ACCESSIBLE_ROLE_TREE_GRID;
+ break;
+ case css::accessibility::AccessibleRole::GRAPHIC:
+ case css::accessibility::AccessibleRole::ICON:
+ case css::accessibility::AccessibleRole::SHAPE:
+ eRole = GTK_ACCESSIBLE_ROLE_IMG;
+ break;
+ default:
+ SAL_WARN("vcl.gtk",
+ "unmapped GtkAccessibleRole: " << xContext->getAccessibleRole());
+ break;
+ }
+ }
+
+ return eRole;
+}
+
+static css::uno::Reference<css::accessibility::XAccessible> get_uno_accessible(GtkWidget* pWidget)
+{
+ GtkWidget* pTopLevel = widget_get_toplevel(pWidget);
+ if (!pTopLevel)
+ return nullptr;
+
+ GtkSalFrame* pFrame = GtkSalFrame::getFromWindow(pTopLevel);
+ if (!pFrame)
+ return nullptr;
+
+ vcl::Window* pFrameWindow = pFrame->GetWindow();
+ if (!pFrameWindow)
+ return nullptr;
+
+ vcl::Window* pWindow = pFrameWindow;
+
+ // skip accessible objects already exposed by the frame objects
+ if (WindowType::BORDERWINDOW == pWindow->GetType())
+ pWindow = pFrameWindow->GetAccessibleChildWindow(0);
+
+ if (!pWindow)
+ return nullptr;
+
+ return pWindow->GetAccessible();
+}
+
+/**
+ * Based on the states set in xContext, set the corresponding Gtk states/properties
+ * in pGtkAccessible.
+ */
+static void applyStates(GtkAccessible* pGtkAccessible,
+ css::uno::Reference<css::accessibility::XAccessibleContext>& xContext)
+{
+ assert(pGtkAccessible);
+
+ if (!xContext.is())
+ return;
+
+ // Gtk differentiates between GtkAccessibleState and GtkAccessibleProperty
+ // (both handled here) and GtkAccessiblePlatformState (handled in
+ // 'lo_accessible_get_platform_state')
+ const sal_Int64 nStates = xContext->getAccessibleStateSet();
+ gtk_accessible_update_property(
+ pGtkAccessible, GTK_ACCESSIBLE_PROPERTY_MODAL,
+ bool(nStates & com::sun::star::accessibility::AccessibleStateType::MODAL),
+ GTK_ACCESSIBLE_PROPERTY_MULTI_LINE,
+ bool(nStates & com::sun::star::accessibility::AccessibleStateType::MULTI_LINE),
+ GTK_ACCESSIBLE_PROPERTY_MULTI_SELECTABLE,
+ bool(nStates & com::sun::star::accessibility::AccessibleStateType::MULTI_SELECTABLE),
+ GTK_ACCESSIBLE_PROPERTY_READ_ONLY,
+ bool(!(nStates & com::sun::star::accessibility::AccessibleStateType::EDITABLE)), -1);
+ if (nStates & com::sun::star::accessibility::AccessibleStateType::HORIZONTAL)
+ {
+ gtk_accessible_update_property(pGtkAccessible, GTK_ACCESSIBLE_PROPERTY_ORIENTATION,
+ GTK_ORIENTATION_HORIZONTAL, -1);
+ }
+ else if (nStates & com::sun::star::accessibility::AccessibleStateType::VERTICAL)
+ {
+ gtk_accessible_update_property(pGtkAccessible, GTK_ACCESSIBLE_PROPERTY_ORIENTATION,
+ GTK_ORIENTATION_VERTICAL, -1);
+ }
+
+ gtk_accessible_update_state(
+ pGtkAccessible, GTK_ACCESSIBLE_STATE_BUSY,
+ bool(nStates & com::sun::star::accessibility::AccessibleStateType::BUSY),
+ GTK_ACCESSIBLE_STATE_DISABLED,
+ bool(!(nStates & com::sun::star::accessibility::AccessibleStateType::ENABLED)),
+ GTK_ACCESSIBLE_STATE_EXPANDED,
+ bool(nStates & com::sun::star::accessibility::AccessibleStateType::EXPANDED),
+ GTK_ACCESSIBLE_STATE_SELECTED,
+ bool(nStates & com::sun::star::accessibility::AccessibleStateType::SELECTED), -1);
+
+ // when explicitly setting any value for GTK_ACCESSIBLE_STATE_CHECKED,
+ // Gtk will also report ATSPI_STATE_CHECKABLE on the AT-SPI layer
+ if (nStates & com::sun::star::accessibility::AccessibleStateType::CHECKABLE)
+ {
+ GtkAccessibleTristate eState = GTK_ACCESSIBLE_TRISTATE_FALSE;
+ if (nStates & com::sun::star::accessibility::AccessibleStateType::INDETERMINATE)
+ eState = GTK_ACCESSIBLE_TRISTATE_MIXED;
+ else if (nStates & com::sun::star::accessibility::AccessibleStateType::CHECKED)
+ eState = GTK_ACCESSIBLE_TRISTATE_TRUE;
+ gtk_accessible_update_state(pGtkAccessible, GTK_ACCESSIBLE_STATE_CHECKED, eState, -1);
+ }
+
+ const sal_Int16 nRole = xContext->getAccessibleRole();
+ if (nRole == com::sun::star::accessibility::AccessibleRole::TOGGLE_BUTTON)
+ {
+ GtkAccessibleTristate eState = GTK_ACCESSIBLE_TRISTATE_FALSE;
+ if (nStates & com::sun::star::accessibility::AccessibleStateType::INDETERMINATE)
+ eState = GTK_ACCESSIBLE_TRISTATE_MIXED;
+ else if (nStates & com::sun::star::accessibility::AccessibleStateType::PRESSED)
+ eState = GTK_ACCESSIBLE_TRISTATE_TRUE;
+ gtk_accessible_update_state(pGtkAccessible, GTK_ACCESSIBLE_STATE_PRESSED, eState, -1);
+ }
+}
+
+static void applyObjectAttribute(GtkAccessible* pGtkAccessible, std::u16string_view rName,
+ const OUString& rValue)
+{
+ assert(pGtkAccessible);
+
+ if (rName == u"colindextext")
+ {
+ gtk_accessible_update_relation(pGtkAccessible, GTK_ACCESSIBLE_RELATION_COL_INDEX_TEXT,
+ rValue.toUtf8().getStr(), -1);
+ }
+ else if (rName == u"level")
+ {
+ const int nLevel = o3tl::toInt32(rValue);
+ gtk_accessible_update_property(pGtkAccessible, GTK_ACCESSIBLE_PROPERTY_LEVEL, nLevel, -1);
+ }
+ else if (rName == u"rowindextext")
+ {
+ gtk_accessible_update_relation(pGtkAccessible, GTK_ACCESSIBLE_RELATION_ROW_INDEX_TEXT,
+ rValue.toUtf8().getStr(), -1);
+ }
+}
+
+/**
+ * Based on the object attributes set for xContext, set the corresponding Gtk equivalents
+ * in pGtkAccessible, where applicable.
+ */
+static void
+applyObjectAttributes(GtkAccessible* pGtkAccessible,
+ css::uno::Reference<css::accessibility::XAccessibleContext>& xContext)
+{
+ assert(pGtkAccessible);
+
+ css::uno::Reference<css::accessibility::XAccessibleExtendedAttributes> xAttributes(
+ xContext, css::uno::UNO_QUERY);
+ if (!xAttributes.is())
+ return;
+
+ OUString sAttrs;
+ xAttributes->getExtendedAttributes() >>= sAttrs;
+
+ sal_Int32 nIndex = 0;
+ do
+ {
+ const OUString sAttribute = sAttrs.getToken(0, ';', nIndex);
+ sal_Int32 nColonPos = 0;
+ const OUString sName = sAttribute.getToken(0, ':', nColonPos);
+ const OUString sValue = sAttribute.getToken(0, ':', nColonPos);
+ assert(nColonPos == -1
+ && "Too many colons in attribute that should have \"name:value\" syntax");
+
+ applyObjectAttribute(pGtkAccessible, sName, sValue);
+ } while (nIndex >= 0);
+}
+
+#define LO_TYPE_ACCESSIBLE (lo_accessible_get_type())
+#define LO_ACCESSIBLE(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), LO_TYPE_ACCESSIBLE, LoAccessible))
+// #define LO_IS_ACCESSIBLE(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), LO_TYPE_ACCESSIBLE))
+
+struct LoAccessible
+{
+ GObject parent_instance;
+ GdkDisplay* display;
+ GtkAccessible* parent;
+ GtkATContext* at_context;
+ css::uno::Reference<css::accessibility::XAccessible> uno_accessible;
+};
+
+struct LoAccessibleClass
+{
+ GObjectClass parent_class;
+};
+
+#if GTK_CHECK_VERSION(4, 10, 0)
+static void lo_accessible_range_init(GtkAccessibleRangeInterface* iface);
+static gboolean lo_accessible_range_set_current_value(GtkAccessibleRange* self, double fNewValue);
+#endif
+
+extern "C" {
+typedef GType (*GetGIfaceType)();
+}
+const struct
+{
+ const char* name;
+ GInterfaceInitFunc const aInit;
+ GetGIfaceType const aGetGIfaceType;
+ const css::uno::Type& (*aGetUnoType)();
+} TYPE_TABLE[] = {
+#if GTK_CHECK_VERSION(4, 10, 0)
+ { "Value", reinterpret_cast<GInterfaceInitFunc>(lo_accessible_range_init),
+ gtk_accessible_range_get_type, cppu::UnoType<css::accessibility::XAccessibleValue>::get }
+#endif
+};
+
+static bool isOfType(css::uno::XInterface* xInterface, const css::uno::Type& rType)
+{
+ if (!xInterface)
+ return false;
+
+ try
+ {
+ css::uno::Any aRet = xInterface->queryInterface(rType);
+ const bool bIs = (typelib_TypeClass_INTERFACE == aRet.pType->eTypeClass)
+ && (aRet.pReserved != nullptr);
+ return bIs;
+ }
+ catch (const css::uno::Exception&)
+ {
+ return false;
+ }
+}
+
+static GType ensureTypeFor(css::uno::XInterface* xAccessible)
+{
+ OStringBuffer aTypeNameBuf("OOoGtkAccessibleObj");
+ std::vector<bool> bTypes(std::size(TYPE_TABLE), false);
+ for (size_t i = 0; i < std::size(TYPE_TABLE); i++)
+ {
+ if (isOfType(xAccessible, TYPE_TABLE[i].aGetUnoType()))
+ {
+ aTypeNameBuf.append(TYPE_TABLE[i].name);
+ bTypes[i] = true;
+ }
+ }
+
+ const OString aTypeName = aTypeNameBuf.makeStringAndClear();
+ GType nType = g_type_from_name(aTypeName.getStr());
+ if (nType != G_TYPE_INVALID)
+ return nType;
+
+ GTypeInfo aTypeInfo = { sizeof(LoAccessibleClass), nullptr, nullptr, nullptr, nullptr, nullptr,
+ sizeof(LoAccessible), 0, nullptr, nullptr };
+ nType
+ = g_type_register_static(LO_TYPE_ACCESSIBLE, aTypeName.getStr(), &aTypeInfo, GTypeFlags(0));
+
+ for (size_t i = 0; i < std::size(TYPE_TABLE); i++)
+ {
+ if (bTypes[i])
+ {
+ GInterfaceInfo aIfaceInfo = { nullptr, nullptr, nullptr };
+ aIfaceInfo.interface_init = TYPE_TABLE[i].aInit;
+ g_type_add_interface_static(nType, TYPE_TABLE[i].aGetGIfaceType(), &aIfaceInfo);
+ }
+ }
+ return nType;
+}
+
+enum
+{
+ CHILD_PROP_0,
+ LAST_CHILD_PROP,
+
+ PROP_ACCESSIBLE_ROLE
+};
+
+static void lo_accessible_get_property(GObject* object, guint property_id, GValue* value,
+ GParamSpec* pspec)
+{
+ LoAccessible* accessible = LO_ACCESSIBLE(object);
+
+ switch (property_id)
+ {
+ case PROP_ACCESSIBLE_ROLE:
+ {
+ GtkAccessibleRole eRole(map_accessible_role(accessible->uno_accessible));
+ g_value_set_enum(value, eRole);
+ break;
+ }
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
+ break;
+ }
+}
+
+static void lo_accessible_set_property(GObject* object, guint property_id, const GValue* /*value*/,
+ GParamSpec* pspec)
+{
+ // LoAccessible* accessible = LO_ACCESSIBLE(object);
+
+ switch (property_id)
+ {
+ case PROP_ACCESSIBLE_ROLE:
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
+ break;
+ }
+}
+
+static GtkAccessible* lo_accessible_get_accessible_parent(GtkAccessible* accessible)
+{
+ LoAccessible* lo_accessible = LO_ACCESSIBLE(accessible);
+ if (!lo_accessible->parent)
+ return nullptr;
+ return GTK_ACCESSIBLE(g_object_ref(lo_accessible->parent));
+}
+
+static GtkATContext* lo_accessible_get_at_context(GtkAccessible* self)
+{
+ LoAccessible* pAccessible = LO_ACCESSIBLE(self);
+
+ GtkAccessibleRole eRole = map_accessible_role(pAccessible->uno_accessible);
+
+ if (!pAccessible->at_context
+ || gtk_at_context_get_accessible_role(pAccessible->at_context) != eRole)
+ {
+ pAccessible->at_context = gtk_at_context_create(eRole, self, pAccessible->display);
+ if (!pAccessible->at_context)
+ return nullptr;
+ }
+
+ return g_object_ref(pAccessible->at_context);
+}
+
+static GtkAccessible* lo_accessible_get_first_accessible_child(GtkAccessible* self);
+
+static GtkAccessible* lo_accessible_get_next_accessible_sibling(GtkAccessible* self);
+
+static gboolean lo_accessible_get_platform_state(GtkAccessible* self,
+ GtkAccessiblePlatformState state);
+
+static gboolean lo_accessible_get_bounds(GtkAccessible* accessible, int* x, int* y, int* width,
+ int* height);
+
+static void lo_accessible_accessible_init(GtkAccessibleInterface* iface)
+{
+ iface->get_accessible_parent = lo_accessible_get_accessible_parent;
+ iface->get_at_context = lo_accessible_get_at_context;
+ iface->get_bounds = lo_accessible_get_bounds;
+ iface->get_first_accessible_child = lo_accessible_get_first_accessible_child;
+ iface->get_next_accessible_sibling = lo_accessible_get_next_accessible_sibling;
+ iface->get_platform_state = lo_accessible_get_platform_state;
+}
+
+#if GTK_CHECK_VERSION(4, 10, 0)
+static void lo_accessible_range_init(GtkAccessibleRangeInterface* iface)
+{
+ iface->set_current_value = lo_accessible_range_set_current_value;
+}
+#endif
+
+G_DEFINE_TYPE_WITH_CODE(LoAccessible, lo_accessible, G_TYPE_OBJECT,
+ G_IMPLEMENT_INTERFACE(GTK_TYPE_ACCESSIBLE, lo_accessible_accessible_init))
+
+static void lo_accessible_class_init(LoAccessibleClass* klass)
+{
+ GObjectClass* object_class = G_OBJECT_CLASS(klass);
+
+ // object_class->finalize = lo_accessible_finalize;
+ // object_class->dispose = lo_accessible_dispose;
+ object_class->get_property = lo_accessible_get_property;
+ object_class->set_property = lo_accessible_set_property;
+ // object_class->constructed = lo_accessible_constructed;
+
+ // g_object_class_install_properties(object_class, LAST_CHILD_PROP, lo_accessible_props);
+ g_object_class_override_property(object_class, PROP_ACCESSIBLE_ROLE, "accessible-role");
+}
+
+static LoAccessible*
+lo_accessible_new(GdkDisplay* pDisplay, GtkAccessible* pParent,
+ const css::uno::Reference<css::accessibility::XAccessible>& rAccessible)
+{
+ assert(rAccessible.is());
+
+ GType nType = ensureTypeFor(rAccessible.get());
+ LoAccessible* ret = LO_ACCESSIBLE(g_object_new(nType, nullptr));
+ ret->display = pDisplay;
+ ret->parent = pParent;
+ ret->uno_accessible = rAccessible;
+
+ css::uno::Reference<css::accessibility::XAccessibleContext> xContext(
+ ret->uno_accessible->getAccessibleContext());
+ assert(xContext.is() && "No accessible context");
+
+ GtkAccessible* pGtkAccessible = GTK_ACCESSIBLE(ret);
+
+ // set accessible name and description
+ gtk_accessible_update_property(pGtkAccessible, GTK_ACCESSIBLE_PROPERTY_LABEL,
+ xContext->getAccessibleName().toUtf8().getStr(),
+ GTK_ACCESSIBLE_PROPERTY_DESCRIPTION,
+ xContext->getAccessibleDescription().toUtf8().getStr(), -1);
+
+ applyStates(pGtkAccessible, xContext);
+
+ applyObjectAttributes(GTK_ACCESSIBLE(ret), xContext);
+
+ // set values from XAccessibleValue interface if that's implemented
+ css::uno::Reference<css::accessibility::XAccessibleValue> xAccessibleValue(xContext,
+ css::uno::UNO_QUERY);
+ if (xAccessibleValue.is())
+ {
+ double fCurrentValue = 0, fMinValue = 0, fMaxValue = 0;
+ xAccessibleValue->getCurrentValue() >>= fCurrentValue;
+ xAccessibleValue->getMinimumValue() >>= fMinValue;
+ xAccessibleValue->getMaximumValue() >>= fMaxValue;
+ gtk_accessible_update_property(GTK_ACCESSIBLE(ret), GTK_ACCESSIBLE_PROPERTY_VALUE_NOW,
+ fCurrentValue, GTK_ACCESSIBLE_PROPERTY_VALUE_MIN, fMinValue,
+ GTK_ACCESSIBLE_PROPERTY_VALUE_MAX, fMaxValue, -1);
+ }
+
+ return ret;
+}
+
+static gboolean lo_accessible_get_bounds(GtkAccessible* self, int* x, int* y, int* width,
+ int* height)
+{
+ LoAccessible* pAccessible = LO_ACCESSIBLE(self);
+
+ if (!pAccessible->uno_accessible)
+ return false;
+
+ css::uno::Reference<css::accessibility::XAccessibleContext> xContext(
+ pAccessible->uno_accessible->getAccessibleContext());
+ css::uno::Reference<css::accessibility::XAccessibleComponent> xAccessibleComponent(
+ xContext, css::uno::UNO_QUERY);
+ if (!xAccessibleComponent)
+ return false;
+
+ css::awt::Rectangle aBounds = xAccessibleComponent->getBounds();
+ *x = aBounds.X;
+ *y = aBounds.Y;
+ *width = aBounds.Width;
+ *height = aBounds.Height;
+ return true;
+}
+
+static GtkAccessible* lo_accessible_get_first_accessible_child(GtkAccessible* self)
+{
+ LoAccessible* pAccessible = LO_ACCESSIBLE(self);
+
+ if (!pAccessible->uno_accessible)
+ return nullptr;
+
+ css::uno::Reference<css::accessibility::XAccessibleContext> xContext(
+ pAccessible->uno_accessible->getAccessibleContext());
+ if (!xContext->getAccessibleChildCount())
+ return nullptr;
+ css::uno::Reference<css::accessibility::XAccessible> xFirstChild(
+ xContext->getAccessibleChild(0));
+ if (!xFirstChild)
+ return nullptr;
+
+ LoAccessible* child_accessible = lo_accessible_new(pAccessible->display, self, xFirstChild);
+ return GTK_ACCESSIBLE(g_object_ref(child_accessible));
+}
+
+static GtkAccessible* lo_accessible_get_next_accessible_sibling(GtkAccessible* self)
+{
+ LoAccessible* pAccessible = LO_ACCESSIBLE(self);
+
+ if (!pAccessible->uno_accessible)
+ return nullptr;
+
+ css::uno::Reference<css::accessibility::XAccessibleContext> xContext(
+ pAccessible->uno_accessible->getAccessibleContext());
+ sal_Int64 nThisChildIndex = xContext->getAccessibleIndexInParent();
+ assert(nThisChildIndex != -1);
+ sal_Int64 nNextChildIndex = nThisChildIndex + 1;
+
+ css::uno::Reference<css::accessibility::XAccessible> xParent = xContext->getAccessibleParent();
+ css::uno::Reference<css::accessibility::XAccessibleContext> xParentContext(
+ xParent->getAccessibleContext());
+ if (nNextChildIndex >= xParentContext->getAccessibleChildCount())
+ return nullptr;
+ css::uno::Reference<css::accessibility::XAccessible> xNextChild(
+ xParentContext->getAccessibleChild(nNextChildIndex));
+ if (!xNextChild)
+ return nullptr;
+
+ LoAccessible* child_accessible
+ = lo_accessible_new(pAccessible->display, pAccessible->parent, xNextChild);
+ return GTK_ACCESSIBLE(g_object_ref(child_accessible));
+}
+
+static gboolean lo_accessible_get_platform_state(GtkAccessible* self,
+ GtkAccessiblePlatformState state)
+{
+ LoAccessible* pAccessible = LO_ACCESSIBLE(self);
+
+ if (!pAccessible->uno_accessible)
+ return false;
+
+ css::uno::Reference<css::accessibility::XAccessibleContext> xContext(
+ pAccessible->uno_accessible->getAccessibleContext());
+ sal_Int64 nStateSet = xContext->getAccessibleStateSet();
+
+ switch (state)
+ {
+ case GTK_ACCESSIBLE_PLATFORM_STATE_FOCUSABLE:
+ return (nStateSet & css::accessibility::AccessibleStateType::FOCUSABLE) != 0;
+ case GTK_ACCESSIBLE_PLATFORM_STATE_FOCUSED:
+ return (nStateSet & css::accessibility::AccessibleStateType::FOCUSED) != 0;
+ case GTK_ACCESSIBLE_PLATFORM_STATE_ACTIVE:
+ return (nStateSet & css::accessibility::AccessibleStateType::ACTIVE) != 0;
+ }
+
+ return false;
+}
+
+#if GTK_CHECK_VERSION(4, 10, 0)
+static gboolean lo_accessible_range_set_current_value(GtkAccessibleRange* self, double fNewValue)
+{
+ // return 'true' in any case, since otherwise no proper AT-SPI DBus reply gets sent
+ // and the app crashes, s.
+ // https://gitlab.gnome.org/GNOME/gtk/-/issues/6150
+ // https://gitlab.gnome.org/GNOME/gtk/-/commit/0dbd2bd09eff8c9233e45338a05daf2a835529ab
+
+ LoAccessible* pAccessible = LO_ACCESSIBLE(self);
+ if (!pAccessible->uno_accessible)
+ return true;
+
+ css::uno::Reference<css::accessibility::XAccessibleContext> xContext(
+ pAccessible->uno_accessible->getAccessibleContext());
+
+ css::uno::Reference<css::accessibility::XAccessibleValue> xValue(xContext, css::uno::UNO_QUERY);
+ if (!xValue.is())
+ return true;
+
+ // Different types of numerical values for XAccessibleValue are possible.
+ // If current value has an integer type, also use that for the new value, to make
+ // sure underlying implementations expecting that can handle the value properly.
+ const css::uno::Any aCurrentValue = xValue->getCurrentValue();
+ if (aCurrentValue.getValueTypeClass() == css::uno::TypeClass::TypeClass_LONG)
+ {
+ const sal_Int32 nValue = std::round<sal_Int32>(fNewValue);
+ xValue->setCurrentValue(css::uno::Any(nValue));
+ return true;
+ }
+ else if (aCurrentValue.getValueTypeClass() == css::uno::TypeClass::TypeClass_HYPER)
+ {
+ const sal_Int64 nValue = std::round<sal_Int64>(fNewValue);
+ xValue->setCurrentValue(css::uno::Any(nValue));
+ return true;
+ }
+
+ css::uno::Any aValue;
+ aValue <<= fNewValue;
+ xValue->setCurrentValue(aValue);
+ return true;
+}
+#endif
+
+static void lo_accessible_init(LoAccessible* /*iface*/) {}
+
+static GtkATContext* get_at_context(GtkAccessible* self)
+{
+ OOoFixed* pFixed = OOO_FIXED(self);
+
+ css::uno::Reference<css::accessibility::XAccessible> xAccessible(
+ get_uno_accessible(GTK_WIDGET(pFixed)));
+ GtkAccessibleRole eRole = map_accessible_role(xAccessible);
+
+ if (!pFixed->at_context || gtk_at_context_get_accessible_role(pFixed->at_context) != eRole)
+ {
+ // if (pFixed->at_context)
+ // g_clear_object(&pFixed->at_context);
+
+ pFixed->at_context
+ = gtk_at_context_create(eRole, self, gtk_widget_get_display(GTK_WIDGET(pFixed)));
+ if (!pFixed->at_context)
+ return nullptr;
+ }
+
+ return g_object_ref(pFixed->at_context);
+}
+
+#if 0
+gboolean get_platform_state(GtkAccessible* self, GtkAccessiblePlatformState state)
+{
+ return false;
+}
+#endif
+
+static gboolean get_bounds(GtkAccessible* accessible, int* x, int* y, int* width, int* height)
+{
+ OOoFixed* pFixed = OOO_FIXED(accessible);
+ css::uno::Reference<css::accessibility::XAccessible> xAccessible(
+ get_uno_accessible(GTK_WIDGET(pFixed)));
+ css::uno::Reference<css::accessibility::XAccessibleContext> xContext(
+ xAccessible->getAccessibleContext());
+ css::uno::Reference<css::accessibility::XAccessibleComponent> xAccessibleComponent(
+ xContext, css::uno::UNO_QUERY);
+
+ css::awt::Rectangle aBounds = xAccessibleComponent->getBounds();
+ *x = aBounds.X;
+ *y = aBounds.Y;
+ *width = aBounds.Width;
+ *height = aBounds.Height;
+ return true;
+}
+
+static GtkAccessible* get_first_accessible_child(GtkAccessible* accessible)
+{
+ OOoFixed* pFixed = OOO_FIXED(accessible);
+ css::uno::Reference<css::accessibility::XAccessible> xAccessible(
+ get_uno_accessible(GTK_WIDGET(pFixed)));
+ if (!xAccessible)
+ return nullptr;
+ css::uno::Reference<css::accessibility::XAccessibleContext> xContext(
+ xAccessible->getAccessibleContext());
+ if (!xContext->getAccessibleChildCount())
+ return nullptr;
+ css::uno::Reference<css::accessibility::XAccessible> xFirstChild(
+ xContext->getAccessibleChild(0));
+ LoAccessible* child_accessible
+ = lo_accessible_new(gtk_widget_get_display(GTK_WIDGET(pFixed)), accessible, xFirstChild);
+ return GTK_ACCESSIBLE(g_object_ref(child_accessible));
+}
+
+static void ooo_fixed_accessible_init(GtkAccessibleInterface* iface)
+{
+ GtkAccessibleInterface* parent_iface
+ = static_cast<GtkAccessibleInterface*>(g_type_interface_peek_parent(iface));
+ iface->get_at_context = get_at_context;
+ iface->get_bounds = get_bounds;
+ iface->get_first_accessible_child = get_first_accessible_child;
+ iface->get_platform_state = parent_iface->get_platform_state;
+ // iface->get_platform_state = get_platform_state;
+}
+
+G_DEFINE_TYPE_WITH_CODE(OOoFixed, ooo_fixed, GTK_TYPE_FIXED,
+ G_IMPLEMENT_INTERFACE(GTK_TYPE_ACCESSIBLE, ooo_fixed_accessible_init))
+
+static void ooo_fixed_class_init(OOoFixedClass* /*klass*/) {}
+
+static void ooo_fixed_init(OOoFixed* /*area*/) {}
+
+GtkWidget* ooo_fixed_new() { return GTK_WIDGET(g_object_new(OOO_TYPE_FIXED, nullptr)); }
+
+#endif
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s cinkeys+=0=break: */
diff --git a/vcl/unx/gtk4/a11y.hxx b/vcl/unx/gtk4/a11y.hxx
new file mode 100644
index 0000000000..90711515bc
--- /dev/null
+++ b/vcl/unx/gtk4/a11y.hxx
@@ -0,0 +1,24 @@
+/* -*- 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/.
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+
+//TODO: Silence various loplugin:external and loplugin:unreffun in (WIP?) a11y.cxx for now:
+struct LoAccessible;
+struct LoAccessibleClass;
+struct OOoFixed;
+struct OOoFixedClass;
+static inline gpointer lo_accessible_get_instance_private(LoAccessible*);
+GType lo_accessible_get_type();
+static inline gpointer ooo_fixed_get_instance_private(OOoFixed*);
+GtkWidget* ooo_fixed_new();
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s cinkeys+=0=break: */
diff --git a/vcl/unx/gtk4/convert3to4.cxx b/vcl/unx/gtk4/convert3to4.cxx
new file mode 100644
index 0000000000..16c7403bb2
--- /dev/null
+++ b/vcl/unx/gtk4/convert3to4.cxx
@@ -0,0 +1,1603 @@
+/* -*- 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)
+{
+ OUString sA(rA.second);
+ OUString sB(rB.second);
+ //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 xSavedMenuSection = 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;
+ xSavedMenuSection = 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 = xSection;
+ xSavedMenuSection = xMenuSection;
+ }
+ }
+
+ 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 = xSavedMenuSection;
+
+ 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) == "GtkToggleButton"
+ || 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");
+
+ if (sName == "AtkObject::accessible-role")
+ xName->setNodeValue("accessible-role");
+ }
+ 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, "has-arrow", "False"));
+ 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 == "GtkScrollbar"
+ || 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
+ css::uno::Reference<css::xml::dom::XNode> xRole;
+ for (css::uno::Reference<css::xml::dom::XNode> xCurrent = xChild->getFirstChild();
+ xCurrent.is(); xCurrent = xChild->getFirstChild())
+ {
+ if (css::uno::Reference<css::xml::dom::XNamedNodeMap> xCurrentMap
+ = xCurrent->getAttributes())
+ {
+ css::uno::Reference<css::xml::dom::XNode> xName
+ = xCurrentMap->getNamedItem("name");
+ OUString sName(xName->getNodeValue());
+ if (sName == "accessible-role")
+ {
+ xRole = xCurrent;
+ }
+ }
+
+ xAccessibility->appendChild(xChild->removeChild(xCurrent));
+ }
+
+ if (xRole)
+ {
+ auto xRoleText = xRole->getFirstChild();
+ if (xRoleText.is())
+ {
+ OUString sText = xRoleText->getNodeValue();
+ // don't really know what the right solution here should be, but "static" isn't
+ // a role that exists in gtk4, nothing seems to match exactly, but maybe "alert"
+ // is a useful place to land for now
+ if (sText == "static")
+ xRoleText->setNodeValue("alert");
+ }
+ // move out of accessibility section to property section
+ xParentObject->insertBefore(xRole->getParentNode()->removeChild(xRole),
+ xAccessibility);
+ }
+ }
+
+ // 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);
+ }
+ }
+ }
+ }
+ else if (xChild->getNodeName() == "items")
+ {
+ // gtk4-4.6.7 won't set an active item if the active item
+ // property precedes the list of combobox items so if we
+ // have "items" move them to the start
+ insertAsFirstChild(xNode, xNode->removeChild(xChild));
+ }
+
+ xChild = xNextChild;
+ }
+
+ if (!sBorderWidth.isEmpty())
+ AddBorderAsMargins(xNode, sBorderWidth);
+
+ for (auto& xRemove : xRemoveList)
+ xNode->removeChild(xRemove);
+
+ 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 0000000000..d75d9fd910
--- /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 0000000000..aebe2c89ed
--- /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 0000000000..a820b49956
--- /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 0000000000..44ea7bd44b
--- /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 0000000000..41dfdf0216
--- /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 0000000000..129a699336
--- /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 0000000000..159ef47040
--- /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 0000000000..0432004a05
--- /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 0000000000..b1e16bd653
--- /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 0000000000..d4251cc94a
--- /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 0000000000..f147af2834
--- /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 0000000000..cae9bc03a4
--- /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 0000000000..3178fdea02
--- /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 0000000000..97f63101dd
--- /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 0000000000..41d85217ce
--- /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 0000000000..e446ba8a55
--- /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 0000000000..658c01233c
--- /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 0000000000..8eb5dcf1a9
--- /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 0000000000..8950781246
--- /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 0000000000..dfdcf0a7c4
--- /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 0000000000..b2683b2bd7
--- /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 0000000000..46b9a2d95f
--- /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 0000000000..a717a30613
--- /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 0000000000..ed036e98c9
--- /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 0000000000..2ae0782b98
--- /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 0000000000..d908c04ceb
--- /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 0000000000..2e8aa98b26
--- /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 0000000000..3e8c79f8ac
--- /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 0000000000..554b80c0d4
--- /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 0000000000..df4d643d9f
--- /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: */