diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-15 05:54:39 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-15 05:54:39 +0000 |
commit | 267c6f2ac71f92999e969232431ba04678e7437e (patch) | |
tree | 358c9467650e1d0a1d7227a21dac2e3d08b622b2 /vcl/unx/gtk4 | |
parent | Initial commit. (diff) | |
download | libreoffice-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')
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: */ |