diff options
Diffstat (limited to '')
89 files changed, 43333 insertions, 0 deletions
diff --git a/vcl/unx/gtk3/a11y/TODO b/vcl/unx/gtk3/a11y/TODO new file mode 100644 index 000000000..1048bd96e --- /dev/null +++ b/vcl/unx/gtk3/a11y/TODO @@ -0,0 +1,49 @@ +cws 'atkbridge' +#Issue number: i#47890# +Submitted by: mmeeks + +Hacked up prototype of atk bridge + + +Serious problems + + Threading/locking: + + incoming CORBA calls & the GDK lock + + how are these being processed & on what thread ? + + are we holding the GDK_THREADS lock ? + + can we even do that ? + + is it really necessary to be thread safe ? + + how does this work in combination with the (unsafe) GAIL code ? + + what should incoming CORBA calls be doing ? + + esp. since we can't tell if they're coming from + in-proc or not either [ though this is unlikely ] + + +Test: + + in-line text editing, does the TEXT_CHANGED signal get it right, + + why not copy/paste/delete etc. ? + + check vs. writer & other bits ... + + AtkSelection + + AtkHyper* + +* At-poke + + implement non-gui mode - for to-console event logging + + logging + + more detail from remaining events + + add a Tree navigation thing instead (?) + + poke a sub-child (?) + + embed a tree widget inside the tree view ? + + AtkHyperText testing (?) + + +Known bugs: + + AtkText + + selection interface - multiple selections ? + + word boundary issues + + copy AccessibleTextImpl.java's getAfterIndex eg. + + the 'getFoo' methods need to use UNO_QUERY_THROW & + throw an exception to avoid null pointer dereferences. + + AtkAttributeSet (etc.) + + AtkEditableText + + finish/test AtkTable + + HyperLink 'link_activated', HyperText 'link_selected' (?) + + tooltips create new toplevels with broken roles. diff --git a/vcl/unx/gtk3/a11y/atkfactory.hxx b/vcl/unx/gtk3/a11y/atkfactory.hxx new file mode 100644 index 000000000..ac72b5897 --- /dev/null +++ b/vcl/unx/gtk3/a11y/atkfactory.hxx @@ -0,0 +1,33 @@ +/* -*- 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#ifndef INCLUDED_VCL_UNX_GTK_A11Y_ATKFACTORY_HXX +#define INCLUDED_VCL_UNX_GTK_A11Y_ATKFACTORY_HXX + +#include <atk/atk.h> + +extern "C" { + +GType wrapper_factory_get_type(); + +} // extern "C" + +#endif + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/a11y/atklistener.hxx b/vcl/unx/gtk3/a11y/atklistener.hxx new file mode 100644 index 000000000..58798d443 --- /dev/null +++ b/vcl/unx/gtk3/a11y/atklistener.hxx @@ -0,0 +1,71 @@ +/* -*- 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#ifndef INCLUDED_VCL_UNX_GTK_A11Y_ATKLISTENER_HXX +#define INCLUDED_VCL_UNX_GTK_A11Y_ATKLISTENER_HXX + +#include <com/sun/star/accessibility/XAccessibleEventListener.hpp> +#include <cppuhelper/implbase.hxx> + +#include <vector> + +#include "atkwrapper.hxx" + +class AtkListener : public ::cppu::WeakImplHelper< css::accessibility::XAccessibleEventListener > +{ +public: + explicit AtkListener(AtkObjectWrapper * pWrapper); + + // XEventListener + virtual void SAL_CALL disposing( const css::lang::EventObject& Source ) override; + + // XAccessibleEventListener + virtual void SAL_CALL notifyEvent( const css::accessibility::AccessibleEventObject& aEvent ) override; + +private: + + AtkObjectWrapper *mpWrapper; + std::vector< css::uno::Reference< css::accessibility::XAccessible > > + m_aChildList; + + virtual ~AtkListener() override; + + // Updates the child list held to provide the old IndexInParent on children_changed::remove + void updateChildList( + css::uno::Reference<css::accessibility::XAccessibleContext> const & + pContext); + + // Process CHILD_EVENT notifications with a new child added + void handleChildAdded( + const css::uno::Reference< css::accessibility::XAccessibleContext >& rxParent, + const css::uno::Reference< css::accessibility::XAccessible>& rxChild); + + // Process CHILD_EVENT notifications with a child removed + void handleChildRemoved( + const css::uno::Reference< css::accessibility::XAccessibleContext >& rxParent, + const css::uno::Reference< css::accessibility::XAccessible>& rxChild); + + // Process INVALIDATE_ALL_CHILDREN notification + void handleInvalidateChildren( + const css::uno::Reference< css::accessibility::XAccessibleContext >& rxParent); +}; + +#endif // INCLUDED_VCL_UNX_GTK_A11Y_ATKLISTENER_HXX + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/a11y/atkregistry.hxx b/vcl/unx/gtk3/a11y/atkregistry.hxx new file mode 100644 index 000000000..b69229084 --- /dev/null +++ b/vcl/unx/gtk3/a11y/atkregistry.hxx @@ -0,0 +1,35 @@ +/* -*- 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#ifndef INCLUDED_VCL_UNX_GTK_A11Y_ATKREGISTRY_HXX +#define INCLUDED_VCL_UNX_GTK_A11Y_ATKREGISTRY_HXX + +#include <com/sun/star/accessibility/XAccessible.hpp> +#include <atk/atk.h> + +AtkObject * ooo_wrapper_registry_get(const css::uno::Reference< css::accessibility::XAccessible >& rxAccessible); + +void ooo_wrapper_registry_add(const css::uno::Reference< css::accessibility::XAccessible >& rxAccessible, AtkObject *obj); + +void ooo_wrapper_registry_remove( + css::uno::Reference<css::accessibility::XAccessible> const & pAccessible); + +#endif // __ATK_REGISTRY_HXX_ + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/a11y/atktextattributes.hxx b/vcl/unx/gtk3/a11y/atktextattributes.hxx new file mode 100644 index 000000000..f86c82f4f --- /dev/null +++ b/vcl/unx/gtk3/a11y/atktextattributes.hxx @@ -0,0 +1,52 @@ +/* -*- 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#ifndef INCLUDED_VCL_UNX_GTK_A11Y_ATKTEXTATTRIBUTES_HXX +#define INCLUDED_VCL_UNX_GTK_A11Y_ATKTEXTATTRIBUTES_HXX + +#include <com/sun/star/uno/Sequence.hxx> +#include <com/sun/star/beans/PropertyValue.hpp> +#include <com/sun/star/accessibility/XAccessibleExtendedAttributes.hpp> + +#include <atk/atk.h> + +AtkAttributeSet* +attribute_set_new_from_property_values( + const css::uno::Sequence< css::beans::PropertyValue >& rAttributeList, + bool run_attributes_only, + AtkText *text); + +AtkAttributeSet* +attribute_set_new_from_extended_attributes( + const css::uno::Reference< css::accessibility::XAccessibleExtendedAttributes >& rExtendedAttributes ); + +bool +attribute_set_map_to_property_values( + AtkAttributeSet* attribute_set, + css::uno::Sequence< css::beans::PropertyValue >& rValueList ); + +AtkAttributeSet* attribute_set_prepend_misspelled( AtkAttributeSet* attribute_set ); +// #i92232# +AtkAttributeSet* attribute_set_prepend_tracked_change_insertion( AtkAttributeSet* attribute_set ); +AtkAttributeSet* attribute_set_prepend_tracked_change_deletion( AtkAttributeSet* attribute_set ); +AtkAttributeSet* attribute_set_prepend_tracked_change_formatchange( AtkAttributeSet* attribute_set ); + +#endif + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/a11y/atkutil.hxx b/vcl/unx/gtk3/a11y/atkutil.hxx new file mode 100644 index 000000000..3df45c1ca --- /dev/null +++ b/vcl/unx/gtk3/a11y/atkutil.hxx @@ -0,0 +1,29 @@ +/* -*- 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#ifndef INCLUDED_VCL_UNX_GTK_A11Y_ATKUTIL_HXX +#define INCLUDED_VCL_UNX_GTK_A11Y_ATKUTIL_HXX + +#include <atk/atk.h> + +void ooo_atk_util_ensure_event_listener(); + +#endif + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/a11y/atkwrapper.hxx b/vcl/unx/gtk3/a11y/atkwrapper.hxx new file mode 100644 index 000000000..d9c651a2e --- /dev/null +++ b/vcl/unx/gtk3/a11y/atkwrapper.hxx @@ -0,0 +1,125 @@ +/* -*- 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#ifndef INCLUDED_VCL_UNX_GTK_A11Y_ATKWRAPPER_HXX +#define INCLUDED_VCL_UNX_GTK_A11Y_ATKWRAPPER_HXX + +#include <atk/atk.h> +#include <gtk/gtk.h> +#include <gtk/gtk-a11y.h> +#include <com/sun/star/accessibility/XAccessible.hpp> + +extern "C" { + +namespace com::sun::star::accessibility { + class XAccessibleAction; + class XAccessibleComponent; + class XAccessibleEditableText; + class XAccessibleHypertext; + class XAccessibleImage; + class XAccessibleMultiLineText; + class XAccessibleSelection; + class XAccessibleTable; + class XAccessibleText; + class XAccessibleTextMarkup; + class XAccessibleTextAttributes; + class XAccessibleValue; +} + +struct AtkObjectWrapper +{ + AtkObject aParent; + AtkObject* mpOrig; //if we're a GtkDrawingArea acting as a custom LibreOffice widget, this is the toolkit default impl + + css::uno::Reference<css::accessibility::XAccessible> mpAccessible; + css::uno::Reference<css::accessibility::XAccessibleContext> mpContext; + css::uno::Reference<css::accessibility::XAccessibleAction> mpAction; + css::uno::Reference<css::accessibility::XAccessibleComponent> mpComponent; + css::uno::Reference<css::accessibility::XAccessibleEditableText> + mpEditableText; + css::uno::Reference<css::accessibility::XAccessibleHypertext> mpHypertext; + css::uno::Reference<css::accessibility::XAccessibleImage> mpImage; + css::uno::Reference<css::accessibility::XAccessibleMultiLineText> + mpMultiLineText; + css::uno::Reference<css::accessibility::XAccessibleSelection> mpSelection; + css::uno::Reference<css::accessibility::XAccessibleTable> mpTable; + css::uno::Reference<css::accessibility::XAccessibleText> mpText; + css::uno::Reference<css::accessibility::XAccessibleTextMarkup> mpTextMarkup; + css::uno::Reference<css::accessibility::XAccessibleTextAttributes> + mpTextAttributes; + css::uno::Reference<css::accessibility::XAccessibleValue> mpValue; + + AtkObject *child_about_to_be_removed; + gint index_of_child_about_to_be_removed; +// OString * m_pKeyBindings +}; + +struct AtkObjectWrapperClass +{ + GtkWidgetAccessibleClass aParentClass; +}; + +GType atk_object_wrapper_get_type() G_GNUC_CONST; +AtkObject * atk_object_wrapper_ref( + const css::uno::Reference< css::accessibility::XAccessible >& rxAccessible, + bool create = true ); + +AtkObject * atk_object_wrapper_new( + const css::uno::Reference< css::accessibility::XAccessible >& rxAccessible, + AtkObject* parent = nullptr, AtkObject* orig = nullptr ); + +void atk_object_wrapper_add_child(AtkObjectWrapper* wrapper, AtkObject *child, gint index); +void atk_object_wrapper_remove_child(AtkObjectWrapper* wrapper, AtkObject *child, gint index); +void atk_object_wrapper_set_role(AtkObjectWrapper* wrapper, sal_Int16 role); + +void atk_object_wrapper_dispose(AtkObjectWrapper* wrapper); + +AtkStateType mapAtkState( sal_Int16 nState ); + +AtkRelation* atk_object_wrapper_relation_new(const css::accessibility::AccessibleRelation& rRelation); + +void actionIfaceInit(AtkActionIface *iface); +void componentIfaceInit(AtkComponentIface *iface); +void editableTextIfaceInit(AtkEditableTextIface *iface); +void hypertextIfaceInit(AtkHypertextIface *iface); +void imageIfaceInit(AtkImageIface *iface); +void selectionIfaceInit(AtkSelectionIface *iface); +void tableIfaceInit(AtkTableIface *iface); +void textIfaceInit(AtkTextIface *iface); +void valueIfaceInit(AtkValueIface *iface); + +} // extern "C" + +#define ATK_TYPE_OBJECT_WRAPPER atk_object_wrapper_get_type() +#define ATK_OBJECT_WRAPPER(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST ((obj), ATK_TYPE_OBJECT_WRAPPER, AtkObjectWrapper)) +#define ATK_IS_OBJECT_WRAPPER(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), ATK_TYPE_OBJECT_WRAPPER)) + +static inline gchar * +OUStringToGChar(const OUString& rString ) +{ + OString aUtf8 = OUStringToOString( rString, RTL_TEXTENCODING_UTF8 ); + return g_strdup( aUtf8.getStr() ); +} + +#define OUStringToConstGChar( string ) OUStringToOString( string, RTL_TEXTENCODING_UTF8 ).getStr() + +#endif // INCLUDED_VCL_UNX_GTK_A11Y_ATKWRAPPER_HXX + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/a11y/gtk3atkaction.cxx b/vcl/unx/gtk3/a11y/gtk3atkaction.cxx new file mode 100644 index 000000000..fa9d09660 --- /dev/null +++ b/vcl/unx/gtk3/a11y/gtk3atkaction.cxx @@ -0,0 +1,275 @@ +/* -*- 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include "atkwrapper.hxx" + +#include <com/sun/star/accessibility/XAccessibleAction.hpp> +#include <com/sun/star/accessibility/XAccessibleKeyBinding.hpp> + +#include <com/sun/star/awt/Key.hpp> +#include <com/sun/star/awt/KeyModifier.hpp> + +#include <rtl/strbuf.hxx> +#include <algorithm> +#include <map> + +using namespace ::com::sun::star; + +// FIXME +static const gchar * +getAsConst( const OString& rString ) +{ + static const int nMax = 10; + static OString aUgly[nMax]; + static int nIdx = 0; + nIdx = (nIdx + 1) % nMax; + aUgly[nIdx] = rString; + return aUgly[ nIdx ].getStr(); +} + +/// @throws uno::RuntimeException +static css::uno::Reference<css::accessibility::XAccessibleAction> + getAction( AtkAction *action ) +{ + AtkObjectWrapper *pWrap = ATK_OBJECT_WRAPPER( action ); + + if( pWrap ) + { + if( !pWrap->mpAction.is() ) + { + pWrap->mpAction.set(pWrap->mpContext, css::uno::UNO_QUERY); + } + + return pWrap->mpAction; + } + + return css::uno::Reference<css::accessibility::XAccessibleAction>(); +} + +extern "C" { + +static gboolean +action_wrapper_do_action (AtkAction *action, + gint i) +{ + try { + css::uno::Reference<css::accessibility::XAccessibleAction> pAction + = getAction( action ); + if( pAction.is() ) + return pAction->doAccessibleAction( i ); + } + catch(const uno::Exception&) { + g_warning( "Exception in doAccessibleAction()" ); + } + + return FALSE; +} + +static gint +action_wrapper_get_n_actions (AtkAction *action) +{ + try { + css::uno::Reference<css::accessibility::XAccessibleAction> pAction + = getAction( action ); + if( pAction.is() ) + return pAction->getAccessibleActionCount(); + } + catch(const uno::Exception&) { + g_warning( "Exception in getAccessibleActionCount()" ); + } + + return 0; +} + +static const gchar * +action_wrapper_get_description (AtkAction *, gint) +{ + // GAIL implement this only for cells + g_warning( "Not implemented: get_description()" ); + return ""; +} + +static const gchar * +action_wrapper_get_localized_name (AtkAction *, gint) +{ + // GAIL doesn't implement this as well + g_warning( "Not implemented: get_localized_name()" ); + return ""; +} + +#define ACTION_NAME_PAIR( OOoName, AtkName ) \ + std::pair< const OUString, const gchar * > ( OUString( OOoName ), AtkName ) + +static const gchar * +action_wrapper_get_name (AtkAction *action, + gint i) +{ + static std::map< OUString, const gchar * > aNameMap; + + if( aNameMap.empty() ) + { + aNameMap.insert( ACTION_NAME_PAIR( "click", "click" ) ); + aNameMap.insert( ACTION_NAME_PAIR( "select", "click" ) ); + aNameMap.insert( ACTION_NAME_PAIR( "togglePopup", "push" ) ); + } + + try { + css::uno::Reference<css::accessibility::XAccessibleAction> pAction + = getAction( action ); + if( pAction.is() ) + { + std::map< OUString, const gchar * >::iterator iter; + + OUString aDesc( pAction->getAccessibleActionDescription( i ) ); + + iter = aNameMap.find( aDesc ); + if( iter != aNameMap.end() ) + return iter->second; + + std::pair< const OUString, const gchar * > aNewVal( aDesc, + g_strdup( OUStringToConstGChar(aDesc) ) ); + + if( aNameMap.insert( aNewVal ).second ) + return aNewVal.second; + } + } + catch(const uno::Exception&) { + g_warning( "Exception in getAccessibleActionDescription()" ); + } + + return ""; +} + +/* +* GNOME Expects a string in the format: +* +* <mnemonic>;<full-path>;<accelerator> +* +* The keybindings in <full-path> should be separated by ":" +*/ + +static void +appendKeyStrokes(OStringBuffer& rBuffer, const uno::Sequence< awt::KeyStroke >& rKeyStrokes) +{ + for( const auto& rKeyStroke : rKeyStrokes ) + { + if( rKeyStroke.Modifiers & awt::KeyModifier::SHIFT ) + rBuffer.append("<Shift>"); + if( rKeyStroke.Modifiers & awt::KeyModifier::MOD1 ) + rBuffer.append("<Control>"); + if( rKeyStroke.Modifiers & awt::KeyModifier::MOD2 ) + rBuffer.append("<Alt>"); + + if( ( rKeyStroke.KeyCode >= awt::Key::A ) && ( rKeyStroke.KeyCode <= awt::Key::Z ) ) + rBuffer.append( static_cast<char>( 'a' + ( rKeyStroke.KeyCode - awt::Key::A ) ) ); + else + { + char c = '\0'; + + switch( rKeyStroke.KeyCode ) + { + case awt::Key::TAB: c = '\t'; break; + case awt::Key::SPACE: c = ' '; break; + case awt::Key::ADD: c = '+'; break; + case awt::Key::SUBTRACT: c = '-'; break; + case awt::Key::MULTIPLY: c = '*'; break; + case awt::Key::DIVIDE: c = '/'; break; + case awt::Key::POINT: c = '.'; break; + case awt::Key::COMMA: c = ','; break; + case awt::Key::LESS: c = '<'; break; + case awt::Key::GREATER: c = '>'; break; + case awt::Key::EQUAL: c = '='; break; + case 0: + break; + default: + g_warning( "Unmapped KeyCode: %d", rKeyStroke.KeyCode ); + break; + } + + if( c != '\0' ) + rBuffer.append( c ); + else + { + // The KeyCode approach did not work, probably a non ascii character + // let's hope that there is a character given in KeyChar. + rBuffer.append( OUStringToGChar( OUString( rKeyStroke.KeyChar ) ) ); + } + } + } +} + +static const gchar * +action_wrapper_get_keybinding (AtkAction *action, + gint i) +{ + try { + css::uno::Reference<css::accessibility::XAccessibleAction> pAction + = getAction( action ); + if( pAction.is() ) + { + uno::Reference< accessibility::XAccessibleKeyBinding > xBinding( pAction->getAccessibleActionKeyBinding( i )); + + if( xBinding.is() ) + { + OStringBuffer aRet; + + sal_Int32 nmax = std::min( xBinding->getAccessibleKeyBindingCount(), sal_Int32(3) ); + for( sal_Int32 n = 0; n < nmax; n++ ) + { + appendKeyStrokes( aRet, xBinding->getAccessibleKeyBinding( n ) ); + + if( n < 2 ) + aRet.append( ';' ); + } + + // !! FIXME !! remember keystroke in wrapper object ? + return getAsConst( aRet.makeStringAndClear() ); + } + } + } + catch(const uno::Exception&) { + g_warning( "Exception in get_keybinding()" ); + } + + return ""; +} + +static gboolean +action_wrapper_set_description (AtkAction *, gint, const gchar *) +{ + return FALSE; +} + +} // extern "C" + +void +actionIfaceInit (AtkActionIface *iface) +{ + g_return_if_fail (iface != nullptr); + + iface->do_action = action_wrapper_do_action; + iface->get_n_actions = action_wrapper_get_n_actions; + iface->get_description = action_wrapper_get_description; + iface->get_keybinding = action_wrapper_get_keybinding; + iface->get_name = action_wrapper_get_name; + iface->get_localized_name = action_wrapper_get_localized_name; + iface->set_description = action_wrapper_set_description; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/a11y/gtk3atkbridge.cxx b/vcl/unx/gtk3/a11y/gtk3atkbridge.cxx new file mode 100644 index 000000000..ddcc20f20 --- /dev/null +++ b/vcl/unx/gtk3/a11y/gtk3atkbridge.cxx @@ -0,0 +1,34 @@ +/* -*- 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <unx/gtk/atkbridge.hxx> + +#include "atkutil.hxx" + +bool InitAtkBridge() +{ + ooo_atk_util_ensure_event_listener(); + return true; +} + +void DeInitAtkBridge() +{ +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/a11y/gtk3atkcomponent.cxx b/vcl/unx/gtk3/a11y/gtk3atkcomponent.cxx new file mode 100644 index 000000000..21301d4da --- /dev/null +++ b/vcl/unx/gtk3/a11y/gtk3atkcomponent.cxx @@ -0,0 +1,387 @@ +/* -*- 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include "atkwrapper.hxx" +#include <com/sun/star/accessibility/XAccessibleComponent.hpp> +#include <gtk/gtk.h> + +using namespace ::com::sun::star; + +static AtkObjectWrapper* getObjectWrapper(AtkComponent *pComponent) +{ + AtkObjectWrapper *pWrap = nullptr; + if (ATK_IS_OBJECT_WRAPPER(pComponent)) + pWrap = ATK_OBJECT_WRAPPER(pComponent); + else if (GTK_IS_DRAWING_AREA(pComponent)) //when using a GtkDrawingArea as a custom widget in welded gtk3 + { + GtkWidget* pDrawingArea = GTK_WIDGET(pComponent); + AtkObject* pAtkObject = gtk_widget_get_accessible(pDrawingArea); + pWrap = ATK_IS_OBJECT_WRAPPER(pAtkObject) ? ATK_OBJECT_WRAPPER(pAtkObject) : nullptr; + } + return pWrap; +} + +/// @throws uno::RuntimeException +static css::uno::Reference<css::accessibility::XAccessibleComponent> + getComponent(AtkObjectWrapper *pWrap) +{ + if (pWrap) + { + if (!pWrap->mpComponent.is()) + pWrap->mpComponent.set(pWrap->mpContext, css::uno::UNO_QUERY); + return pWrap->mpComponent; + } + + return css::uno::Reference<css::accessibility::XAccessibleComponent>(); +} + +/*****************************************************************************/ + +static awt::Point +translatePoint( css::uno::Reference<accessibility::XAccessibleComponent> const & pComponent, + gint x, gint y, AtkCoordType t) +{ + awt::Point aOrigin( 0, 0 ); + if( t == ATK_XY_SCREEN ) + aOrigin = pComponent->getLocationOnScreen(); + return awt::Point( x - aOrigin.X, y - aOrigin.Y ); +} + +/*****************************************************************************/ + +extern "C" { + +static gboolean +component_wrapper_grab_focus (AtkComponent *component) +{ + AtkObjectWrapper* obj = getObjectWrapper(component); + //if we're a native GtkDrawingArea with custom a11y, use the default toolkit a11y + if (obj && obj->mpOrig) + return atk_component_grab_focus(ATK_COMPONENT(obj->mpOrig)); + + try + { + css::uno::Reference<css::accessibility::XAccessibleComponent> pComponent + = getComponent(obj); + if( pComponent.is() ) + { + pComponent->grabFocus(); + return true; + } + } + catch( const uno::Exception & ) + { + g_warning( "Exception in grabFocus()" ); + } + + return FALSE; +} + +/*****************************************************************************/ + +static gboolean +component_wrapper_contains (AtkComponent *component, + gint x, + gint y, + AtkCoordType coord_type) +{ + AtkObjectWrapper* obj = getObjectWrapper(component); + //if we're a native GtkDrawingArea with custom a11y, use the default toolkit a11y + if (obj && obj->mpOrig) + return atk_component_contains(ATK_COMPONENT(obj->mpOrig), x, y, coord_type); + + try + { + css::uno::Reference<css::accessibility::XAccessibleComponent> pComponent + = getComponent(obj); + if( pComponent.is() ) + return pComponent->containsPoint( translatePoint( pComponent, x, y, coord_type ) ); + } + catch( const uno::Exception & ) + { + g_warning( "Exception in containsPoint()" ); + } + + return FALSE; +} + +/*****************************************************************************/ + +static AtkObject * +component_wrapper_ref_accessible_at_point (AtkComponent *component, + gint x, + gint y, + AtkCoordType coord_type) +{ + AtkObjectWrapper* obj = getObjectWrapper(component); + //if we're a native GtkDrawingArea with custom a11y, use the default toolkit a11y + if (obj && obj->mpOrig) + return atk_component_ref_accessible_at_point(ATK_COMPONENT(obj->mpOrig), x, y, coord_type); + + try + { + css::uno::Reference<css::accessibility::XAccessibleComponent> pComponent + = getComponent(obj); + + if( pComponent.is() ) + { + uno::Reference< accessibility::XAccessible > xAccessible = pComponent->getAccessibleAtPoint( + translatePoint( pComponent, x, y, coord_type ) ); + return atk_object_wrapper_ref( xAccessible ); + } + } + catch( const uno::Exception & ) + { + g_warning( "Exception in getAccessibleAtPoint()" ); + } + + return nullptr; +} + +/*****************************************************************************/ + +static void +component_wrapper_get_position (AtkComponent *component, + gint *x, + gint *y, + AtkCoordType coord_type) +{ + AtkObjectWrapper* obj = getObjectWrapper(component); + //if we're a native GtkDrawingArea with custom a11y, use the default toolkit a11y + if (obj && obj->mpOrig) + { + atk_component_get_extents(ATK_COMPONENT(obj->mpOrig), x, y, nullptr, nullptr, coord_type); + return; + } + + *x = *y = -1; + + try + { + css::uno::Reference<css::accessibility::XAccessibleComponent> pComponent + = getComponent(obj); + if( pComponent.is() ) + { + awt::Point aPos; + + if( coord_type == ATK_XY_SCREEN ) + aPos = pComponent->getLocationOnScreen(); + else + aPos = pComponent->getLocation(); + + *x = aPos.X; + *y = aPos.Y; + } + } + catch( const uno::Exception & ) + { + g_warning( "Exception in getLocation[OnScreen]()" ); + } +} + +/*****************************************************************************/ + +static void +component_wrapper_get_size (AtkComponent *component, + gint *width, + gint *height) +{ + AtkObjectWrapper* obj = getObjectWrapper(component); + //if we're a native GtkDrawingArea with custom a11y, use the default toolkit a11y + if (obj && obj->mpOrig) + { + atk_component_get_extents(ATK_COMPONENT(obj->mpOrig), nullptr, nullptr, width, height, ATK_XY_WINDOW); + return; + } + + *width = *height = -1; + + try + { + css::uno::Reference<css::accessibility::XAccessibleComponent> pComponent + = getComponent(obj); + if( pComponent.is() ) + { + awt::Size aSize = pComponent->getSize(); + *width = aSize.Width; + *height = aSize.Height; + } + } + catch( const uno::Exception & ) + { + g_warning( "Exception in getSize()" ); + } +} + +/*****************************************************************************/ + +static void +component_wrapper_get_extents (AtkComponent *component, + gint *x, + gint *y, + gint *width, + gint *height, + AtkCoordType coord_type) +{ + component_wrapper_get_position( component, x, y, coord_type ); + component_wrapper_get_size( component, width, height ); +} + +/*****************************************************************************/ + +static gboolean +component_wrapper_set_extents (AtkComponent *, gint, gint, gint, gint, AtkCoordType) +{ + g_warning( "AtkComponent::set_extents unimplementable" ); + return FALSE; +} + +/*****************************************************************************/ + +static gboolean +component_wrapper_set_position (AtkComponent *, gint, gint, AtkCoordType) +{ + g_warning( "AtkComponent::set_position unimplementable" ); + return FALSE; +} + +/*****************************************************************************/ + +static gboolean +component_wrapper_set_size (AtkComponent *, gint, gint) +{ + g_warning( "AtkComponent::set_size unimplementable" ); + return FALSE; +} + +/*****************************************************************************/ + +static AtkLayer +component_wrapper_get_layer (AtkComponent *component) +{ + AtkRole role = atk_object_get_role( ATK_OBJECT( component ) ); + AtkLayer layer = ATK_LAYER_WIDGET; + + switch (role) + { + case ATK_ROLE_POPUP_MENU: + case ATK_ROLE_MENU_ITEM: + case ATK_ROLE_CHECK_MENU_ITEM: + case ATK_ROLE_SEPARATOR: + case ATK_ROLE_LIST_ITEM: + layer = ATK_LAYER_POPUP; + break; + case ATK_ROLE_MENU: + { + AtkObject * parent = atk_object_get_parent( ATK_OBJECT( component ) ); + if( atk_object_get_role( parent ) != ATK_ROLE_MENU_BAR ) + layer = ATK_LAYER_POPUP; + } + break; + + case ATK_ROLE_LIST: + { + AtkObject * parent = atk_object_get_parent( ATK_OBJECT( component ) ); + if( atk_object_get_role( parent ) == ATK_ROLE_COMBO_BOX ) + layer = ATK_LAYER_POPUP; + } + break; + + default: + ; + } + + return layer; +} + +/*****************************************************************************/ + +static gint +component_wrapper_get_mdi_zorder (AtkComponent *) +{ + // only needed for ATK_LAYER_MDI (not used) or ATK_LAYER_WINDOW (inherited from GAIL) + return G_MININT; +} + +/*****************************************************************************/ + +// This code is mostly stolen from libgail .. + +static guint +component_wrapper_add_focus_handler (AtkComponent *component, + AtkFocusHandler handler) +{ + GSignalMatchType match_type; + gulong ret; + guint signal_id; + + match_type = GSignalMatchType(G_SIGNAL_MATCH_ID | G_SIGNAL_MATCH_FUNC); + signal_id = g_signal_lookup( "focus-event", ATK_TYPE_OBJECT ); + + ret = g_signal_handler_find( component, match_type, signal_id, 0, nullptr, + static_cast<gpointer>(&handler), nullptr); + if (!ret) + { + return g_signal_connect_closure_by_id (component, + signal_id, 0, + g_cclosure_new ( + G_CALLBACK (handler), nullptr, + nullptr), + FALSE); + } + else + { + return 0; + } +} + +/*****************************************************************************/ + +static void +component_wrapper_remove_focus_handler (AtkComponent *component, + guint handler_id) +{ + g_signal_handler_disconnect (component, handler_id); +} + +/*****************************************************************************/ + +} // extern "C" + +void +componentIfaceInit (AtkComponentIface *iface) +{ + g_return_if_fail (iface != nullptr); + + iface->add_focus_handler = component_wrapper_add_focus_handler; + iface->contains = component_wrapper_contains; + iface->get_extents = component_wrapper_get_extents; + iface->get_layer = component_wrapper_get_layer; + iface->get_mdi_zorder = component_wrapper_get_mdi_zorder; + iface->get_position = component_wrapper_get_position; + iface->get_size = component_wrapper_get_size; + iface->grab_focus = component_wrapper_grab_focus; + iface->ref_accessible_at_point = component_wrapper_ref_accessible_at_point; + iface->remove_focus_handler = component_wrapper_remove_focus_handler; + iface->set_extents = component_wrapper_set_extents; + iface->set_position = component_wrapper_set_position; + iface->set_size = component_wrapper_set_size; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/a11y/gtk3atkeditabletext.cxx b/vcl/unx/gtk3/a11y/gtk3atkeditabletext.cxx new file mode 100644 index 000000000..f240c3232 --- /dev/null +++ b/vcl/unx/gtk3/a11y/gtk3atkeditabletext.cxx @@ -0,0 +1,193 @@ +/* -*- 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include "atkwrapper.hxx" +#include "atktextattributes.hxx" + +#include <com/sun/star/accessibility/XAccessibleEditableText.hpp> + +#include <string.h> + +using namespace ::com::sun::star; + +/// @throws uno::RuntimeException +static css::uno::Reference<css::accessibility::XAccessibleEditableText> + getEditableText( AtkEditableText *pEditableText ) +{ + AtkObjectWrapper *pWrap = ATK_OBJECT_WRAPPER( pEditableText ); + if( pWrap ) + { + if( !pWrap->mpEditableText.is() ) + { + pWrap->mpEditableText.set(pWrap->mpContext, css::uno::UNO_QUERY); + } + + return pWrap->mpEditableText; + } + + return css::uno::Reference<css::accessibility::XAccessibleEditableText>(); +} + +/*****************************************************************************/ + +extern "C" { + +static gboolean +editable_text_wrapper_set_run_attributes( AtkEditableText *text, + AtkAttributeSet *attribute_set, + gint nStartOffset, + gint nEndOffset) +{ + try { + css::uno::Reference<css::accessibility::XAccessibleEditableText> + pEditableText = getEditableText( text ); + if( pEditableText.is() ) + { + uno::Sequence< beans::PropertyValue > aAttributeList; + + if( attribute_set_map_to_property_values( attribute_set, aAttributeList ) ) + return pEditableText->setAttributes(nStartOffset, nEndOffset, aAttributeList); + } + } + catch(const uno::Exception&) { + g_warning( "Exception in setAttributes()" ); + } + + return FALSE; +} + +static void +editable_text_wrapper_set_text_contents( AtkEditableText *text, + const gchar *string ) +{ + try { + css::uno::Reference<css::accessibility::XAccessibleEditableText> + pEditableText = getEditableText( text ); + if( pEditableText.is() ) + { + OUString aString ( string, strlen(string), RTL_TEXTENCODING_UTF8 ); + pEditableText->setText( aString ); + } + } + catch(const uno::Exception&) { + g_warning( "Exception in setText()" ); + } +} + +static void +editable_text_wrapper_insert_text( AtkEditableText *text, + const gchar *string, + gint length, + gint *pos ) +{ + try { + css::uno::Reference<css::accessibility::XAccessibleEditableText> + pEditableText = getEditableText( text ); + if( pEditableText.is() ) + { + OUString aString ( string, length, RTL_TEXTENCODING_UTF8 ); + if( pEditableText->insertText( aString, *pos ) ) + *pos += length; + } + } + catch(const uno::Exception&) { + g_warning( "Exception in insertText()" ); + } +} + +static void +editable_text_wrapper_cut_text( AtkEditableText *text, + gint start, + gint end ) +{ + try { + css::uno::Reference<css::accessibility::XAccessibleEditableText> + pEditableText = getEditableText( text ); + if( pEditableText.is() ) + pEditableText->cutText( start, end ); + } + catch(const uno::Exception&) { + g_warning( "Exception in cutText()" ); + } +} + +static void +editable_text_wrapper_delete_text( AtkEditableText *text, + gint start, + gint end ) +{ + try { + css::uno::Reference<css::accessibility::XAccessibleEditableText> + pEditableText = getEditableText( text ); + if( pEditableText.is() ) + pEditableText->deleteText( start, end ); + } + catch(const uno::Exception&) { + g_warning( "Exception in deleteText()" ); + } +} + +static void +editable_text_wrapper_paste_text( AtkEditableText *text, + gint pos ) +{ + try { + css::uno::Reference<css::accessibility::XAccessibleEditableText> + pEditableText = getEditableText( text ); + if( pEditableText.is() ) + pEditableText->pasteText( pos ); + } + catch(const uno::Exception&) { + g_warning( "Exception in pasteText()" ); + } +} + +static void +editable_text_wrapper_copy_text( AtkEditableText *text, + gint start, + gint end ) +{ + try { + css::uno::Reference<css::accessibility::XAccessibleEditableText> + pEditableText = getEditableText( text ); + if( pEditableText.is() ) + pEditableText->copyText( start, end ); + } + catch(const uno::Exception&) { + g_warning( "Exception in copyText()" ); + } +} + +} // extern "C" + +void +editableTextIfaceInit (AtkEditableTextIface *iface) +{ + g_return_if_fail (iface != nullptr); + + iface->set_text_contents = editable_text_wrapper_set_text_contents; + iface->insert_text = editable_text_wrapper_insert_text; + iface->copy_text = editable_text_wrapper_copy_text; + iface->cut_text = editable_text_wrapper_cut_text; + iface->delete_text = editable_text_wrapper_delete_text; + iface->paste_text = editable_text_wrapper_paste_text; + iface->set_run_attributes = editable_text_wrapper_set_run_attributes; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/a11y/gtk3atkfactory.cxx b/vcl/unx/gtk3/a11y/gtk3atkfactory.cxx new file mode 100644 index 000000000..f92f9a667 --- /dev/null +++ b/vcl/unx/gtk3/a11y/gtk3atkfactory.cxx @@ -0,0 +1,186 @@ +/* -*- 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <unx/gtk/gtkframe.hxx> +#include <vcl/window.hxx> +#include "atkwrapper.hxx" +#include "atkfactory.hxx" +#include "atkregistry.hxx" + +using namespace ::com::sun::star; + +extern "C" { + +/* + * Instances of this dummy object class are returned whenever we have to + * create an AtkObject, but can't touch the OOo object anymore since it + * is already disposed. + */ + +static AtkStateSet * +noop_wrapper_ref_state_set( AtkObject * ) +{ + AtkStateSet *state_set = atk_state_set_new(); + atk_state_set_add_state( state_set, ATK_STATE_DEFUNCT ); + return state_set; +} + +static void +atk_noop_object_wrapper_class_init(AtkNoOpObjectClass *klass) +{ + AtkObjectClass *atk_class = ATK_OBJECT_CLASS( klass ); + atk_class->ref_state_set = noop_wrapper_ref_state_set; +} + +static GType +atk_noop_object_wrapper_get_type() +{ + static GType type = 0; + + if (!type) + { + static const GTypeInfo typeInfo = + { + sizeof (AtkNoOpObjectClass), + nullptr, + nullptr, + reinterpret_cast<GClassInitFunc>(atk_noop_object_wrapper_class_init), + nullptr, + nullptr, + sizeof (AtkObjectWrapper), + 0, + nullptr, + nullptr + } ; + + type = g_type_register_static (ATK_TYPE_OBJECT, "OOoAtkNoOpObj", &typeInfo, GTypeFlags(0)) ; + } + return type; +} + +static AtkObject* +atk_noop_object_wrapper_new() +{ + AtkObject *accessible; + + accessible = static_cast<AtkObject *>(g_object_new (atk_noop_object_wrapper_get_type(), nullptr)); + g_return_val_if_fail (accessible != nullptr, nullptr); + + accessible->role = ATK_ROLE_INVALID; + accessible->layer = ATK_LAYER_INVALID; + + return accessible; +} + +/* + * The wrapper factory + */ + +static GType +wrapper_factory_get_accessible_type() +{ + return atk_object_wrapper_get_type(); +} + +static AtkObject* +wrapper_factory_create_accessible( GObject *obj ) +{ + GtkWidget* pEventBox = gtk_widget_get_parent(GTK_WIDGET(obj)); + + // gail_container_real_remove_gtk tries to re-instantiate an accessible + // for a widget that is about to vanish .. + if (!pEventBox) + return atk_noop_object_wrapper_new(); + + GtkWidget* pTopLevelGrid = gtk_widget_get_parent(pEventBox); + if (!pTopLevelGrid) + return atk_noop_object_wrapper_new(); + + GtkWidget* pTopLevel = gtk_widget_get_parent(pTopLevelGrid); + if (!pTopLevel) + return atk_noop_object_wrapper_new(); + + GtkSalFrame* pFrame = GtkSalFrame::getFromWindow(pTopLevel); + g_return_val_if_fail( pFrame != nullptr, nullptr ); + + vcl::Window* pFrameWindow = pFrame->GetWindow(); + if( pFrameWindow ) + { + vcl::Window* pWindow = pFrameWindow; + + // skip accessible objects already exposed by the frame objects + if( WindowType::BORDERWINDOW == pWindow->GetType() ) + pWindow = pFrameWindow->GetAccessibleChildWindow(0); + + if( pWindow ) + { + uno::Reference< accessibility::XAccessible > xAccessible = pWindow->GetAccessible(); + if( xAccessible.is() ) + { + AtkObject *accessible = ooo_wrapper_registry_get( xAccessible ); + + if( accessible ) + g_object_ref( G_OBJECT(accessible) ); + else + accessible = atk_object_wrapper_new( xAccessible, gtk_widget_get_accessible(pTopLevel) ); + + return accessible; + } + } + } + + return nullptr; +} + +AtkObject* ooo_fixed_get_accessible(GtkWidget *obj) +{ + return wrapper_factory_create_accessible(G_OBJECT(obj)); +} + +static void +wrapper_factory_class_init( AtkObjectFactoryClass *klass ) +{ + klass->create_accessible = wrapper_factory_create_accessible; + klass->get_accessible_type = wrapper_factory_get_accessible_type; +} + +GType +wrapper_factory_get_type() +{ + static GType t = 0; + + if (!t) { + static const GTypeInfo tinfo = + { + sizeof (AtkObjectFactoryClass), + nullptr, nullptr, reinterpret_cast<GClassInitFunc>(wrapper_factory_class_init), + nullptr, nullptr, sizeof (AtkObjectFactory), 0, nullptr, nullptr + }; + + t = g_type_register_static ( + ATK_TYPE_OBJECT_FACTORY, "OOoAtkObjectWrapperFactory", + &tinfo, GTypeFlags(0)); + } + + return t; +} + +} // extern C + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/a11y/gtk3atkhypertext.cxx b/vcl/unx/gtk3/a11y/gtk3atkhypertext.cxx new file mode 100644 index 000000000..83f6817fc --- /dev/null +++ b/vcl/unx/gtk3/a11y/gtk3atkhypertext.cxx @@ -0,0 +1,277 @@ +/* -*- 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include "atkwrapper.hxx" + +#include <com/sun/star/accessibility/XAccessibleHypertext.hpp> + +using namespace ::com::sun::star; + +// ---------------------- AtkHyperlink ---------------------- + +namespace { + +struct HyperLink { + AtkHyperlink const atk_hyper_link; + + uno::Reference< accessibility::XAccessibleHyperlink > xLink; +}; + +} + +static uno::Reference< accessibility::XAccessibleHyperlink > const & + getHyperlink( AtkHyperlink *pHyperlink ) +{ + HyperLink *pLink = reinterpret_cast<HyperLink *>(pHyperlink); + return pLink->xLink; +} + +static GObjectClass *hyper_parent_class = nullptr; + +extern "C" { + +static void +hyper_link_finalize (GObject *obj) +{ + HyperLink *hl = reinterpret_cast<HyperLink *>(obj); + hl->xLink.clear(); + hyper_parent_class->finalize (obj); +} + +static gchar * +hyper_link_get_uri( AtkHyperlink *pLink, + gint i ) +{ + try { + uno::Any aAny = getHyperlink( pLink )->getAccessibleActionObject( i ); + OUString aUri = aAny.get< OUString > (); + return OUStringToGChar(aUri); + } + catch(const uno::Exception&) { + g_warning( "Exception in hyper_link_get_uri" ); + } + return nullptr; +} + +static AtkObject * +hyper_link_get_object( AtkHyperlink *, + gint ) +{ + g_warning( "FIXME: hyper_link_get_object unimplemented" ); + return nullptr; +} +static gint +hyper_link_get_end_index( AtkHyperlink *pLink ) +{ + try { + return getHyperlink( pLink )->getEndIndex(); + } + catch(const uno::Exception&) { + } + return -1; +} +static gint +hyper_link_get_start_index( AtkHyperlink *pLink ) +{ + try { + return getHyperlink( pLink )->getStartIndex(); + } + catch(const uno::Exception&) { + } + return -1; +} +static gboolean +hyper_link_is_valid( AtkHyperlink *pLink ) +{ + try { + return getHyperlink( pLink )->isValid(); + } + catch(const uno::Exception&) { + } + return FALSE; +} +static gint +hyper_link_get_n_anchors( AtkHyperlink *pLink ) +{ + try { + return getHyperlink( pLink )->getAccessibleActionCount(); + } + catch(const uno::Exception&) { + } + return 0; +} + +static guint +hyper_link_link_state( AtkHyperlink * ) +{ + g_warning( "FIXME: hyper_link_link_state unimplemented" ); + return 0; +} +static gboolean +hyper_link_is_selected_link( AtkHyperlink * ) +{ + g_warning( "FIXME: hyper_link_is_selected_link unimplemented" ); + return FALSE; +} + +static void +hyper_link_class_init (AtkHyperlinkClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + gobject_class->finalize = hyper_link_finalize; + + hyper_parent_class = static_cast<GObjectClass *>(g_type_class_peek_parent (klass)); + + klass->get_uri = hyper_link_get_uri; + klass->get_object = hyper_link_get_object; + klass->get_end_index = hyper_link_get_end_index; + klass->get_start_index = hyper_link_get_start_index; + klass->is_valid = hyper_link_is_valid; + klass->get_n_anchors = hyper_link_get_n_anchors; + klass->link_state = hyper_link_link_state; + klass->is_selected_link = hyper_link_is_selected_link; +} + +static GType +hyper_link_get_type() +{ + static GType type = 0; + + if (!type) { + static const GTypeInfo tinfo = { + sizeof (AtkHyperlinkClass), + nullptr, /* base init */ + nullptr, /* base finalize */ + reinterpret_cast<GClassInitFunc>(hyper_link_class_init), + nullptr, /* class finalize */ + nullptr, /* class data */ + sizeof (HyperLink), /* instance size */ + 0, /* nb preallocs */ + nullptr, /* instance init */ + nullptr /* value table */ + }; + + static const GInterfaceInfo atk_action_info = { + reinterpret_cast<GInterfaceInitFunc>(actionIfaceInit), + nullptr, + nullptr + }; + + type = g_type_register_static (ATK_TYPE_HYPERLINK, + "OOoAtkObjHyperLink", &tinfo, + GTypeFlags(0)); + g_type_add_interface_static (type, ATK_TYPE_ACTION, + &atk_action_info); + } + + return type; +} + +// ---------------------- AtkHyperText ---------------------- + +/// @throws uno::RuntimeException +static css::uno::Reference<css::accessibility::XAccessibleHypertext> + getHypertext( AtkHypertext *pHypertext ) +{ + AtkObjectWrapper *pWrap = ATK_OBJECT_WRAPPER( pHypertext ); + if( pWrap ) + { + if( !pWrap->mpHypertext.is() ) + { + pWrap->mpHypertext.set(pWrap->mpContext, css::uno::UNO_QUERY); + } + + return pWrap->mpHypertext; + } + + return css::uno::Reference<css::accessibility::XAccessibleHypertext>(); +} + +static AtkHyperlink * +hypertext_get_link( AtkHypertext *hypertext, + gint link_index) +{ + try { + css::uno::Reference<css::accessibility::XAccessibleHypertext> pHypertext + = getHypertext( hypertext ); + if( pHypertext.is() ) + { + HyperLink *pLink = static_cast<HyperLink *>(g_object_new( hyper_link_get_type(), nullptr )); + pLink->xLink = pHypertext->getHyperLink( link_index ); + if( !pLink->xLink.is() ) { + g_object_unref( G_OBJECT( pLink ) ); + pLink = nullptr; + } + return ATK_HYPERLINK( pLink ); + } + } + catch(const uno::Exception&) { + g_warning( "Exception in getHyperLink()" ); + } + + return nullptr; +} + +static gint +hypertext_get_n_links( AtkHypertext *hypertext ) +{ + try { + css::uno::Reference<css::accessibility::XAccessibleHypertext> pHypertext + = getHypertext( hypertext ); + if( pHypertext.is() ) + return pHypertext->getHyperLinkCount(); + } + catch(const uno::Exception&) { + g_warning( "Exception in getHyperLinkCount()" ); + } + + return 0; +} + +static gint +hypertext_get_link_index( AtkHypertext *hypertext, + gint index) +{ + try { + css::uno::Reference<css::accessibility::XAccessibleHypertext> pHypertext + = getHypertext( hypertext ); + if( pHypertext.is() ) + return pHypertext->getHyperLinkIndex( index ); + } + catch(const uno::Exception&) { + g_warning( "Exception in getHyperLinkIndex()" ); + } + + return 0; +} + +} // extern "C" + +void +hypertextIfaceInit (AtkHypertextIface *iface) +{ + g_return_if_fail (iface != nullptr); + + iface->get_link = hypertext_get_link; + iface->get_n_links = hypertext_get_n_links; + iface->get_link_index = hypertext_get_link_index; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/a11y/gtk3atkimage.cxx b/vcl/unx/gtk3/a11y/gtk3atkimage.cxx new file mode 100644 index 000000000..acd43f467 --- /dev/null +++ b/vcl/unx/gtk3/a11y/gtk3atkimage.cxx @@ -0,0 +1,131 @@ +/* -*- 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include "atkwrapper.hxx" + +#include <com/sun/star/accessibility/XAccessibleImage.hpp> + +using namespace ::com::sun::star; + +// FIXME +static const gchar * +getAsConst( const OUString& rString ) +{ + static const int nMax = 10; + static OString aUgly[nMax]; + static int nIdx = 0; + nIdx = (nIdx + 1) % nMax; + aUgly[nIdx] = OUStringToOString( rString, RTL_TEXTENCODING_UTF8 ); + return aUgly[ nIdx ].getStr(); +} + +/// @throws uno::RuntimeException +static css::uno::Reference<css::accessibility::XAccessibleImage> + getImage( AtkImage *pImage ) +{ + AtkObjectWrapper *pWrap = ATK_OBJECT_WRAPPER( pImage ); + if( pWrap ) + { + if( !pWrap->mpImage.is() ) + { + pWrap->mpImage.set(pWrap->mpContext, css::uno::UNO_QUERY); + } + + return pWrap->mpImage; + } + + return css::uno::Reference<css::accessibility::XAccessibleImage>(); +} + +extern "C" { + +static const gchar * +image_get_image_description( AtkImage *image ) +{ + try { + css::uno::Reference<css::accessibility::XAccessibleImage> pImage + = getImage( image ); + if( pImage.is() ) + return getAsConst( pImage->getAccessibleImageDescription() ); + } + catch(const uno::Exception&) { + g_warning( "Exception in getAccessibleImageDescription()" ); + } + + return nullptr; +} + +static void +image_get_image_position( AtkImage *image, + gint *x, + gint *y, + AtkCoordType coord_type ) +{ + *x = *y = -1; + if( ATK_IS_COMPONENT( image ) ) + { + SAL_WNODEPRECATED_DECLARATIONS_PUSH + atk_component_get_position( ATK_COMPONENT( image ), x, y, coord_type ); + SAL_WNODEPRECATED_DECLARATIONS_POP + } + else + g_warning( "FIXME: no image position information" ); +} + +static void +image_get_image_size( AtkImage *image, + gint *width, + gint *height ) +{ + *width = *height = -1; + try { + css::uno::Reference<css::accessibility::XAccessibleImage> pImage + = getImage( image ); + if( pImage.is() ) + { + *width = pImage->getAccessibleImageWidth(); + *height = pImage->getAccessibleImageHeight(); + } + } + catch(const uno::Exception&) { + g_warning( "Exception in getAccessibleImageHeight() or Width" ); + } +} + +static gboolean +image_set_image_description( AtkImage *, const gchar * ) +{ + g_warning ("FIXME: no set image description"); + return FALSE; +} + +} // extern "C" + +void +imageIfaceInit (AtkImageIface *iface) +{ + g_return_if_fail (iface != nullptr); + + iface->set_image_description = image_set_image_description; + iface->get_image_description = image_get_image_description; + iface->get_image_position = image_get_image_position; + iface->get_image_size = image_get_image_size; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/a11y/gtk3atklistener.cxx b/vcl/unx/gtk3/a11y/gtk3atklistener.cxx new file mode 100644 index 000000000..8606b7bde --- /dev/null +++ b/vcl/unx/gtk3/a11y/gtk3atklistener.cxx @@ -0,0 +1,748 @@ +/* -*- 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#ifdef AIX +#define _LINUX_SOURCE_COMPAT +#include <sys/timer.h> +#undef _LINUX_SOURCE_COMPAT +#endif + +#include <com/sun/star/accessibility/TextSegment.hpp> +#include <com/sun/star/accessibility/AccessibleEventId.hpp> +#include <com/sun/star/accessibility/AccessibleStateType.hpp> +#include <com/sun/star/accessibility/AccessibleTableModelChange.hpp> +#include <com/sun/star/accessibility/AccessibleTableModelChangeType.hpp> +#include <com/sun/star/accessibility/XAccessibleEventBroadcaster.hpp> +#include <com/sun/star/accessibility/XAccessibleContext3.hpp> +#include <com/sun/star/lang/IndexOutOfBoundsException.hpp> + +#include "atklistener.hxx" +#include "atkwrapper.hxx" +#include <comphelper/sequence.hxx> +#include <vcl/svapp.hxx> + +#include <sal/log.hxx> + +#define DEBUG_ATK_LISTENER 0 + +#if DEBUG_ATK_LISTENER +#include <iostream> +#include <sstream> +#endif + +using namespace com::sun::star; + +AtkListener::AtkListener( AtkObjectWrapper* pWrapper ) : mpWrapper( pWrapper ) +{ + if( mpWrapper ) + { + g_object_ref( mpWrapper ); + updateChildList( mpWrapper->mpContext ); + } +} + +AtkListener::~AtkListener() +{ + if( mpWrapper ) + g_object_unref( mpWrapper ); +} + +/*****************************************************************************/ + +static AtkStateType mapState( const uno::Any &rAny ) +{ + sal_Int16 nState = accessibility::AccessibleStateType::INVALID; + rAny >>= nState; + return mapAtkState( nState ); +} + +/*****************************************************************************/ + +extern "C" { + // rhbz#1001768 - down to horrific problems releasing the solar mutex + // while destroying a Window - which occurs inside these notifications. + static gboolean + idle_defunc_state_change( AtkObject *atk_obj ) + { + SolarMutexGuard aGuard; + + // This is an equivalent to a state change to DEFUNC(T). + atk_object_notify_state_change( atk_obj, ATK_STATE_DEFUNCT, true ); + if( atk_get_focus_object() == atk_obj ) + { + SAL_WNODEPRECATED_DECLARATIONS_PUSH + atk_focus_tracker_notify( nullptr ); + SAL_WNODEPRECATED_DECLARATIONS_POP + } + g_object_unref( G_OBJECT( atk_obj ) ); + return false; + } +} + +// XEventListener implementation +void AtkListener::disposing( const lang::EventObject& ) +{ + if( mpWrapper ) + { + AtkObject *atk_obj = ATK_OBJECT( mpWrapper ); + + // Release all interface references to avoid shutdown problems with + // global mutex + atk_object_wrapper_dispose( mpWrapper ); + + g_idle_add( reinterpret_cast<GSourceFunc>(idle_defunc_state_change), + g_object_ref( G_OBJECT( atk_obj ) ) ); + + // Release the wrapper object so that it can vanish .. + g_object_unref( mpWrapper ); + mpWrapper = nullptr; + } +} + +/*****************************************************************************/ + +static AtkObject *getObjFromAny( const uno::Any &rAny ) +{ + uno::Reference< accessibility::XAccessible > xAccessible; + rAny >>= xAccessible; + return xAccessible.is() ? atk_object_wrapper_ref( xAccessible ) : nullptr; +} + +/*****************************************************************************/ + +// Updates the child list held to provide the old IndexInParent on children_changed::remove +void AtkListener::updateChildList( + css::uno::Reference<css::accessibility::XAccessibleContext> const & + pContext) +{ + m_aChildList.clear(); + + uno::Reference< accessibility::XAccessibleStateSet > xStateSet = pContext->getAccessibleStateSet(); + if( xStateSet.is() + && !xStateSet->contains(accessibility::AccessibleStateType::DEFUNC) + && !xStateSet->contains(accessibility::AccessibleStateType::MANAGES_DESCENDANTS) ) + { + css::uno::Reference<css::accessibility::XAccessibleContext3> xContext3(pContext, css::uno::UNO_QUERY); + if (xContext3.is()) + { + m_aChildList = comphelper::sequenceToContainer<std::vector<css::uno::Reference< css::accessibility::XAccessible >>>(xContext3->getAccessibleChildren()); + } + else + { + sal_Int32 nChildren = pContext->getAccessibleChildCount(); + m_aChildList.resize(nChildren); + for(sal_Int32 n = 0; n < nChildren; n++) + { + try + { + m_aChildList[n] = pContext->getAccessibleChild(n); + } + catch (lang::IndexOutOfBoundsException const&) + { + sal_Int32 nChildren2 = pContext->getAccessibleChildCount(); + assert(nChildren2 <= n && "consistency?"); + m_aChildList.resize(std::min(nChildren2, n)); + break; + } + } + } + } +} + +/*****************************************************************************/ + +void AtkListener::handleChildAdded( + const uno::Reference< accessibility::XAccessibleContext >& rxParent, + const uno::Reference< accessibility::XAccessible>& rxAccessible) +{ + AtkObject * pChild = rxAccessible.is() ? atk_object_wrapper_ref( rxAccessible ) : nullptr; + + if( pChild ) + { + updateChildList(rxParent); + + atk_object_wrapper_add_child( mpWrapper, pChild, + atk_object_get_index_in_parent( pChild )); + + g_object_unref( pChild ); + } +} + +/*****************************************************************************/ + +void AtkListener::handleChildRemoved( + const uno::Reference< accessibility::XAccessibleContext >& rxParent, + const uno::Reference< accessibility::XAccessible>& rxChild) +{ + sal_Int32 nIndex = -1; + + // Locate the child in the children list + size_t n, nmax = m_aChildList.size(); + for( n = 0; n < nmax; ++n ) + { + if( rxChild == m_aChildList[n] ) + { + nIndex = n; + break; + } + } + + // FIXME: two problems here: + // a) we get child-removed events for objects that are no real children + // in the accessibility hierarchy or have been removed before due to + // some child removing batch. + // b) spi_atk_bridge_signal_listener ignores the given parameters + // for children_changed events and always asks the parent for the + // 0. child, which breaks somehow on vanishing list boxes. + // Ignoring "remove" events for objects not in the m_aChildList + // for now. + if( nIndex >= 0 ) + { + uno::Reference<accessibility::XAccessibleEventBroadcaster> xBroadcaster( + rxChild->getAccessibleContext(), uno::UNO_QUERY); + + if (xBroadcaster.is()) + { + uno::Reference<accessibility::XAccessibleEventListener> xListener(this); + xBroadcaster->removeAccessibleEventListener(xListener); + } + + updateChildList(rxParent); + + AtkObject * pChild = atk_object_wrapper_ref( rxChild, false ); + if( pChild ) + { + atk_object_wrapper_remove_child( mpWrapper, pChild, nIndex ); + g_object_unref( pChild ); + } + } +} + +/*****************************************************************************/ + +void AtkListener::handleInvalidateChildren( + const uno::Reference< accessibility::XAccessibleContext >& rxParent) +{ + // Send notifications for all previous children + size_t n = m_aChildList.size(); + while( n-- > 0 ) + { + if( m_aChildList[n].is() ) + { + AtkObject * pChild = atk_object_wrapper_ref( m_aChildList[n], false ); + if( pChild ) + { + atk_object_wrapper_remove_child( mpWrapper, pChild, n ); + g_object_unref( pChild ); + } + } + } + + updateChildList(rxParent); + + // Send notifications for all new children + size_t nmax = m_aChildList.size(); + for( n = 0; n < nmax; ++n ) + { + if( m_aChildList[n].is() ) + { + AtkObject * pChild = atk_object_wrapper_ref( m_aChildList[n] ); + + if( pChild ) + { + atk_object_wrapper_add_child( mpWrapper, pChild, n ); + g_object_unref( pChild ); + } + } + } +} + +/*****************************************************************************/ + +static uno::Reference< accessibility::XAccessibleContext > +getAccessibleContextFromSource( const uno::Reference< uno::XInterface >& rxSource ) +{ + uno::Reference< accessibility::XAccessibleContext > xContext(rxSource, uno::UNO_QUERY); + if( ! xContext.is() ) + { + g_warning( "ERROR: Event source does not implement XAccessibleContext" ); + + // Second try - query for XAccessible, which should give us access to + // XAccessibleContext. + uno::Reference< accessibility::XAccessible > xAccessible(rxSource, uno::UNO_QUERY); + if( xAccessible.is() ) + xContext = xAccessible->getAccessibleContext(); + } + + return xContext; +} + +#if DEBUG_ATK_LISTENER + +namespace { + +void printNotifyEvent( const accessibility::AccessibleEventObject& rEvent ) +{ + static std::vector<const char*> aLabels = { + 0, + "NAME_CHANGED", // 01 + "DESCRIPTION_CHANGED", // 02 + "ACTION_CHANGED", // 03 + "STATE_CHANGED", // 04 + "ACTIVE_DESCENDANT_CHANGED", // 05 + "BOUNDRECT_CHANGED", // 06 + "CHILD", // 07 + "INVALIDATE_ALL_CHILDREN", // 08 + "SELECTION_CHANGED", // 09 + "VISIBLE_DATA_CHANGED", // 10 + "VALUE_CHANGED", // 11 + "CONTENT_FLOWS_FROM_RELATION_CHANGED", // 12 + "CONTENT_FLOWS_TO_RELATION_CHANGED", // 13 + "CONTROLLED_BY_RELATION_CHANGED", // 14 + "CONTROLLER_FOR_RELATION_CHANGED", // 15 + "LABEL_FOR_RELATION_CHANGED", // 16 + "LABELED_BY_RELATION_CHANGED", // 17 + "MEMBER_OF_RELATION_CHANGED", // 18 + "SUB_WINDOW_OF_RELATION_CHANGED", // 19 + "CARET_CHANGED", // 20 + "TEXT_SELECTION_CHANGED", // 21 + "TEXT_CHANGED", // 22 + "TEXT_ATTRIBUTE_CHANGED", // 23 + "HYPERTEXT_CHANGED", // 24 + "TABLE_CAPTION_CHANGED", // 25 + "TABLE_COLUMN_DESCRIPTION_CHANGED", // 26 + "TABLE_COLUMN_HEADER_CHANGED", // 27 + "TABLE_MODEL_CHANGED", // 28 + "TABLE_ROW_DESCRIPTION_CHANGED", // 29 + "TABLE_ROW_HEADER_CHANGED", // 30 + "TABLE_SUMMARY_CHANGED", // 31 + "LISTBOX_ENTRY_EXPANDED", // 32 + "LISTBOX_ENTRY_COLLAPSED", // 33 + "ACTIVE_DESCENDANT_CHANGED_NOFOCUS", // 34 + "SELECTION_CHANGED_ADD", // 35 + "SELECTION_CHANGED_REMOVE", // 36 + "SELECTION_CHANGED_WITHIN", // 37 + "PAGE_CHANGED", // 38 + "SECTION_CHANGED", // 39 + "COLUMN_CHANGED", // 40 + "ROLE_CHANGED", // 41 + }; + + static std::vector<const char*> aStates = { + "INVALID", // 00 + "ACTIVE", // 01 + "ARMED", // 02 + "BUSY", // 03 + "CHECKED", // 04 + "DEFUNC", // 05 + "EDITABLE", // 06 + "ENABLED", // 07 + "EXPANDABLE", // 08 + "EXPANDED", // 09 + "FOCUSABLE", // 10 + "FOCUSED", // 11 + "HORIZONTAL", // 12 + "ICONIFIED", // 13 + "INDETERMINATE", // 14 + "MANAGES_DESCENDANTS", // 15 + "MODAL", // 16 + "MULTI_LINE", // 17 + "MULTI_SELECTABLE", // 18 + "OPAQUE", // 19 + "PRESSED", // 20 + "RESIZABLE", // 21 + "SELECTABLE", // 22 + "SELECTED", // 23 + "SENSITIVE", // 24 + "SHOWING", // 25 + "SINGLE_LINE", // 26 + "STALE", // 27 + "TRANSIENT", // 28 + "VERTICAL", // 29 + "VISIBLE", // 30 + "MOVEABLE", // 31 + "DEFAULT", // 32 + "OFFSCREEN", // 33 + "COLLAPSE", // 34 + }; + + auto getOrUnknown = [](const std::vector<const char*>& rCont, size_t nIndex) -> std::string + { + return (nIndex < rCont.size()) ? rCont[nIndex] : "<unknown>"; + }; + + std::ostringstream os; + os << "--" << std::endl; + os << "* event = " << getOrUnknown(aLabels, rEvent.EventId) << std::endl; + + switch (rEvent.EventId) + { + case accessibility::AccessibleEventId::STATE_CHANGED: + { + sal_Int16 nState; + if (rEvent.OldValue >>= nState) + os << " * old state = " << getOrUnknown(aStates, nState); + if (rEvent.NewValue >>= nState) + os << " * new state = " << getOrUnknown(aStates, nState); + + os << std::endl; + break; + } + default: + ; + } + + std::cout << os.str(); +} + +} + +#endif + +void AtkListener::notifyEvent( const accessibility::AccessibleEventObject& aEvent ) +{ + if( !mpWrapper ) + return; + + AtkObject *atk_obj = ATK_OBJECT( mpWrapper ); + + switch( aEvent.EventId ) + { + // AtkObject signals: + // Hierarchy signals + case accessibility::AccessibleEventId::CHILD: + { + uno::Reference< accessibility::XAccessibleContext > xParent; + uno::Reference< accessibility::XAccessible > xChild; + + xParent = getAccessibleContextFromSource(aEvent.Source); + g_return_if_fail( xParent.is() ); + + if( aEvent.OldValue >>= xChild ) + handleChildRemoved(xParent, xChild); + + if( aEvent.NewValue >>= xChild ) + handleChildAdded(xParent, xChild); + break; + } + + case accessibility::AccessibleEventId::INVALIDATE_ALL_CHILDREN: + { + uno::Reference< accessibility::XAccessibleContext > xParent = getAccessibleContextFromSource(aEvent.Source); + g_return_if_fail( xParent.is() ); + + handleInvalidateChildren(xParent); + break; + } + + case accessibility::AccessibleEventId::NAME_CHANGED: + { + OUString aName; + if( aEvent.NewValue >>= aName ) + { + atk_object_set_name(atk_obj, + OUStringToOString(aName, RTL_TEXTENCODING_UTF8).getStr()); + } + break; + } + + case accessibility::AccessibleEventId::DESCRIPTION_CHANGED: + { + OUString aDescription; + if( aEvent.NewValue >>= aDescription ) + { + atk_object_set_description(atk_obj, + OUStringToOString(aDescription, RTL_TEXTENCODING_UTF8).getStr()); + } + break; + } + + case accessibility::AccessibleEventId::STATE_CHANGED: + { + AtkStateType eOldState = mapState( aEvent.OldValue ); + AtkStateType eNewState = mapState( aEvent.NewValue ); + + bool bState = eNewState != ATK_STATE_INVALID; + AtkStateType eRealState = bState ? eNewState : eOldState; + + atk_object_notify_state_change( atk_obj, eRealState, bState ); + break; + } + + case accessibility::AccessibleEventId::BOUNDRECT_CHANGED: + +#ifdef HAS_ATKRECTANGLE + if( ATK_IS_COMPONENT( atk_obj ) ) + { + AtkRectangle rect; + + atk_component_get_extents( ATK_COMPONENT( atk_obj ), + &rect.x, + &rect.y, + &rect.width, + &rect.height, + ATK_XY_SCREEN ); + + g_signal_emit_by_name( atk_obj, "bounds_changed", &rect ); + } + else + g_warning( "bounds_changed event for object not implementing AtkComponent\n"); +#endif + + break; + + case accessibility::AccessibleEventId::VISIBLE_DATA_CHANGED: + g_signal_emit_by_name( atk_obj, "visible-data-changed" ); + break; + + case accessibility::AccessibleEventId::ACTIVE_DESCENDANT_CHANGED: + { + AtkObject *pChild = getObjFromAny( aEvent.NewValue ); + if( pChild ) + { + g_signal_emit_by_name( atk_obj, "active-descendant-changed", pChild ); + g_object_unref( pChild ); + } + break; + } + + //ACTIVE_DESCENDANT_CHANGED_NOFOCUS (sic) appears to have been added + //as a workaround or an aid for the ia2 winaccessibility implementation + //so ignore it silently without warning here + case accessibility::AccessibleEventId::ACTIVE_DESCENDANT_CHANGED_NOFOCUS: + break; + + // #i92103# + case accessibility::AccessibleEventId::LISTBOX_ENTRY_EXPANDED: + { + AtkObject *pChild = getObjFromAny( aEvent.NewValue ); + if( pChild ) + { + atk_object_notify_state_change( pChild, ATK_STATE_EXPANDED, true ); + g_object_unref( pChild ); + } + break; + } + + case accessibility::AccessibleEventId::LISTBOX_ENTRY_COLLAPSED: + { + AtkObject *pChild = getObjFromAny( aEvent.NewValue ); + if( pChild ) + { + atk_object_notify_state_change( pChild, ATK_STATE_EXPANDED, false ); + g_object_unref( pChild ); + } + break; + } + + // AtkAction signals ... + case accessibility::AccessibleEventId::ACTION_CHANGED: + g_signal_emit_by_name( G_OBJECT( atk_obj ), "property_change::accessible-actions"); + break; + + // AtkText + case accessibility::AccessibleEventId::CARET_CHANGED: + { + sal_Int32 nPos=0; + aEvent.NewValue >>= nPos; + g_signal_emit_by_name( atk_obj, "text_caret_moved", nPos ); + break; + } + case accessibility::AccessibleEventId::TEXT_CHANGED: + { + // TESTME: and remove this comment: + // cf. comphelper/source/misc/accessibletexthelper.cxx (implInitTextChangedEvent) + accessibility::TextSegment aDeletedText; + accessibility::TextSegment aInsertedText; + + // TODO: when GNOME starts to send "update" kind of events, change + // we need to re-think this implementation as well + if( aEvent.OldValue >>= aDeletedText ) + { + /* Remember the text segment here to be able to return removed text in get_text(). + * This is clearly a hack to be used until appropriate API exists in atk to pass + * the string value directly or we find a compelling reason to start caching the + * UTF-8 converted strings in the atk wrapper object. + */ + + g_object_set_data( G_OBJECT(atk_obj), "ooo::text_changed::delete", &aDeletedText); + + g_signal_emit_by_name( atk_obj, "text_changed::delete", + static_cast<gint>(aDeletedText.SegmentStart), + static_cast<gint>( aDeletedText.SegmentEnd - aDeletedText.SegmentStart ) ); + + g_object_steal_data( G_OBJECT(atk_obj), "ooo::text_changed::delete" ); + } + + if( aEvent.NewValue >>= aInsertedText ) + g_signal_emit_by_name( atk_obj, "text_changed::insert", + static_cast<gint>(aInsertedText.SegmentStart), + static_cast<gint>( aInsertedText.SegmentEnd - aInsertedText.SegmentStart ) ); + break; + } + + case accessibility::AccessibleEventId::TEXT_SELECTION_CHANGED: + { + g_signal_emit_by_name( atk_obj, "text-selection-changed" ); + break; + } + + case accessibility::AccessibleEventId::TEXT_ATTRIBUTE_CHANGED: + g_signal_emit_by_name( atk_obj, "text-attributes-changed" ); + break; + + // AtkValue + case accessibility::AccessibleEventId::VALUE_CHANGED: + g_object_notify( G_OBJECT( atk_obj ), "accessible-value" ); + break; + + case accessibility::AccessibleEventId::CONTENT_FLOWS_FROM_RELATION_CHANGED: + case accessibility::AccessibleEventId::CONTENT_FLOWS_TO_RELATION_CHANGED: + case accessibility::AccessibleEventId::CONTROLLED_BY_RELATION_CHANGED: + case accessibility::AccessibleEventId::CONTROLLER_FOR_RELATION_CHANGED: + case accessibility::AccessibleEventId::LABEL_FOR_RELATION_CHANGED: + case accessibility::AccessibleEventId::LABELED_BY_RELATION_CHANGED: + case accessibility::AccessibleEventId::MEMBER_OF_RELATION_CHANGED: + case accessibility::AccessibleEventId::SUB_WINDOW_OF_RELATION_CHANGED: + // FIXME: ask Bill how Atk copes with this little lot ... + break; + + // AtkTable + case accessibility::AccessibleEventId::TABLE_MODEL_CHANGED: + { + accessibility::AccessibleTableModelChange aChange; + aEvent.NewValue >>= aChange; + + sal_Int32 nRowsChanged = aChange.LastRow - aChange.FirstRow + 1; + sal_Int32 nColumnsChanged = aChange.LastColumn - aChange.FirstColumn + 1; + + static const struct { + const char *row; + const char *col; + } aSignalNames[] = + { + { nullptr, nullptr }, // dummy + { "row_inserted", "column_inserted" }, // INSERT = 1 + { "row_deleted", "column_deleted" } // DELETE = 2 + }; + switch( aChange.Type ) + { + case accessibility::AccessibleTableModelChangeType::INSERT: + case accessibility::AccessibleTableModelChangeType::DELETE: + if( nRowsChanged > 0 ) + g_signal_emit_by_name( G_OBJECT( atk_obj ), + aSignalNames[aChange.Type].row, + aChange.FirstRow, nRowsChanged ); + if( nColumnsChanged > 0 ) + g_signal_emit_by_name( G_OBJECT( atk_obj ), + aSignalNames[aChange.Type].col, + aChange.FirstColumn, nColumnsChanged ); + break; + + case accessibility::AccessibleTableModelChangeType::UPDATE: + // This is not really a model change, is it ? + break; + default: + g_warning( "TESTME: unusual table model change %d\n", aChange.Type ); + break; + } + g_signal_emit_by_name( G_OBJECT( atk_obj ), "model-changed" ); + break; + } + + case accessibility::AccessibleEventId::TABLE_COLUMN_HEADER_CHANGED: + { + accessibility::AccessibleTableModelChange aChange; + aEvent.NewValue >>= aChange; + + AtkPropertyValues values; + memset(&values, 0, sizeof(AtkPropertyValues)); + g_value_init (&values.new_value, G_TYPE_INT); + values.property_name = "accessible-table-column-header"; + + for (sal_Int32 nChangedColumn = aChange.FirstColumn; nChangedColumn <= aChange.LastColumn; ++nChangedColumn) + { + g_value_set_int (&values.new_value, nChangedColumn); + g_signal_emit_by_name(G_OBJECT(atk_obj), "property_change::accessible-table-column-header", &values, nullptr); + } + break; + } + + case accessibility::AccessibleEventId::TABLE_CAPTION_CHANGED: + g_signal_emit_by_name( G_OBJECT( atk_obj ), "property_change::accessible-table-caption"); + break; + + case accessibility::AccessibleEventId::TABLE_COLUMN_DESCRIPTION_CHANGED: + g_signal_emit_by_name( G_OBJECT( atk_obj ), "property_change::accessible-table-column-description"); + break; + + case accessibility::AccessibleEventId::TABLE_ROW_DESCRIPTION_CHANGED: + g_signal_emit_by_name( G_OBJECT( atk_obj ), "property_change::accessible-table-row-description"); + break; + + case accessibility::AccessibleEventId::TABLE_ROW_HEADER_CHANGED: + g_signal_emit_by_name( G_OBJECT( atk_obj ), "property_change::accessible-table-row-header"); + break; + + case accessibility::AccessibleEventId::TABLE_SUMMARY_CHANGED: + g_signal_emit_by_name( G_OBJECT( atk_obj ), "property_change::accessible-table-summary"); + break; + + case accessibility::AccessibleEventId::SELECTION_CHANGED: + case accessibility::AccessibleEventId::SELECTION_CHANGED_ADD: + case accessibility::AccessibleEventId::SELECTION_CHANGED_REMOVE: + case accessibility::AccessibleEventId::SELECTION_CHANGED_WITHIN: + if (ATK_IS_SELECTION(atk_obj)) + g_signal_emit_by_name(G_OBJECT(atk_obj), "selection_changed"); + else + { + // e.g. tdf#122353, when such dialogs become native the problem will go away anyway + SAL_INFO("vcl.gtk", "selection change from obj which doesn't support XAccessibleSelection"); + } + break; + + case accessibility::AccessibleEventId::HYPERTEXT_CHANGED: + g_signal_emit_by_name( G_OBJECT( atk_obj ), "property_change::accessible-hypertext-offset"); + break; + + case accessibility::AccessibleEventId::ROLE_CHANGED: + { + uno::Reference< accessibility::XAccessibleContext > xContext = getAccessibleContextFromSource( aEvent.Source ); + atk_object_wrapper_set_role( mpWrapper, xContext->getAccessibleRole() ); + break; + } + + case accessibility::AccessibleEventId::PAGE_CHANGED: + { + /* // If we implemented AtkDocument then I imagine this is what this + // handler should look like + sal_Int32 nPos=0; + aEvent.NewValue >>= nPos; + g_signal_emit_by_name( G_OBJECT( atk_obj ), "page_changed", nPos ); + */ + break; + } + + default: + SAL_WARN("vcl.gtk", "Unknown event notification: " << aEvent.EventId); + break; + } +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/a11y/gtk3atkregistry.cxx b/vcl/unx/gtk3/a11y/gtk3atkregistry.cxx new file mode 100644 index 000000000..ff96378c4 --- /dev/null +++ b/vcl/unx/gtk3/a11y/gtk3atkregistry.cxx @@ -0,0 +1,66 @@ +/* -*- 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include "atkregistry.hxx" + +using namespace ::com::sun::star::accessibility; +using namespace ::com::sun::star::uno; + +static GHashTable *uno_to_gobject = nullptr; + +/*****************************************************************************/ + +AtkObject * +ooo_wrapper_registry_get(const Reference< XAccessible >& rxAccessible) +{ + if( uno_to_gobject ) + { + gpointer cached = + g_hash_table_lookup(uno_to_gobject, static_cast<gpointer>(rxAccessible.get())); + + if( cached ) + return ATK_OBJECT( cached ); + } + + return nullptr; +} + +/*****************************************************************************/ + +void +ooo_wrapper_registry_add(const Reference< XAccessible >& rxAccessible, AtkObject *obj) +{ + if( !uno_to_gobject ) + uno_to_gobject = g_hash_table_new (nullptr, nullptr); + + g_hash_table_insert( uno_to_gobject, static_cast<gpointer>(rxAccessible.get()), obj ); +} + +/*****************************************************************************/ + +void +ooo_wrapper_registry_remove( + css::uno::Reference<css::accessibility::XAccessible> const & pAccessible) +{ + if( uno_to_gobject ) + g_hash_table_remove( + uno_to_gobject, static_cast<gpointer>(pAccessible.get()) ); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/a11y/gtk3atkselection.cxx b/vcl/unx/gtk3/a11y/gtk3atkselection.cxx new file mode 100644 index 000000000..91759e8d0 --- /dev/null +++ b/vcl/unx/gtk3/a11y/gtk3atkselection.cxx @@ -0,0 +1,190 @@ +/* -*- 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include "atkwrapper.hxx" + +#include <com/sun/star/accessibility/XAccessibleSelection.hpp> + +using namespace ::com::sun::star; + +/// @throws uno::RuntimeException +static css::uno::Reference<css::accessibility::XAccessibleSelection> + getSelection( AtkSelection *pSelection ) +{ + AtkObjectWrapper *pWrap = ATK_OBJECT_WRAPPER( pSelection ); + if( pWrap ) + { + if( !pWrap->mpSelection.is() ) + { + pWrap->mpSelection.set(pWrap->mpContext, css::uno::UNO_QUERY); + } + + return pWrap->mpSelection; + } + + return css::uno::Reference<css::accessibility::XAccessibleSelection>(); +} + +extern "C" { + +static gboolean +selection_add_selection( AtkSelection *selection, + gint i ) +{ + try { + css::uno::Reference<css::accessibility::XAccessibleSelection> pSelection + = getSelection( selection ); + if( pSelection.is() ) + { + pSelection->selectAccessibleChild( i ); + return true; + } + } + catch(const uno::Exception&) { + g_warning( "Exception in selectAccessibleChild()" ); + } + + return FALSE; +} + +static gboolean +selection_clear_selection( AtkSelection *selection ) +{ + try { + css::uno::Reference<css::accessibility::XAccessibleSelection> pSelection + = getSelection( selection ); + if( pSelection.is() ) + { + pSelection->clearAccessibleSelection(); + return true; + } + } + catch(const uno::Exception&) { + g_warning( "Exception in selectAccessibleChild()" ); + } + + return FALSE; +} + +static AtkObject* +selection_ref_selection( AtkSelection *selection, + gint i ) +{ + try { + css::uno::Reference<css::accessibility::XAccessibleSelection> pSelection + = getSelection( selection ); + if( pSelection.is() ) + return atk_object_wrapper_ref( pSelection->getSelectedAccessibleChild( i ) ); + } + catch(const uno::Exception&) { + g_warning( "Exception in getSelectedAccessibleChild()" ); + } + + return nullptr; +} + +static gint +selection_get_selection_count( AtkSelection *selection) +{ + try { + css::uno::Reference<css::accessibility::XAccessibleSelection> pSelection + = getSelection( selection ); + if( pSelection.is() ) + return pSelection->getSelectedAccessibleChildCount(); + } + catch(const uno::Exception&) { + g_warning( "Exception in getSelectedAccessibleChildCount()" ); + } + + return -1; +} + +static gboolean +selection_is_child_selected( AtkSelection *selection, + gint i) +{ + try { + css::uno::Reference<css::accessibility::XAccessibleSelection> pSelection + = getSelection( selection ); + if( pSelection.is() ) + return pSelection->isAccessibleChildSelected( i ); + } + catch(const uno::Exception&) { + g_warning( "Exception in getSelectedAccessibleChildCount()" ); + } + + return FALSE; +} + +static gboolean +selection_remove_selection( AtkSelection *selection, + gint i ) +{ + try { + css::uno::Reference<css::accessibility::XAccessibleSelection> pSelection + = getSelection( selection ); + if( pSelection.is() ) + { + pSelection->deselectAccessibleChild( i ); + return true; + } + } + catch(const uno::Exception&) { + g_warning( "Exception in getSelectedAccessibleChildCount()" ); + } + + return FALSE; +} + +static gboolean +selection_select_all_selection( AtkSelection *selection) +{ + try { + css::uno::Reference<css::accessibility::XAccessibleSelection> pSelection + = getSelection( selection ); + if( pSelection.is() ) + { + pSelection->selectAllAccessibleChildren(); + return true; + } + } + catch(const uno::Exception&) { + g_warning( "Exception in getSelectedAccessibleChildCount()" ); + } + + return FALSE; +} + +} // extern "C" + +void +selectionIfaceInit( AtkSelectionIface *iface) +{ + g_return_if_fail (iface != nullptr); + + iface->add_selection = selection_add_selection; + iface->clear_selection = selection_clear_selection; + iface->ref_selection = selection_ref_selection; + iface->get_selection_count = selection_get_selection_count; + iface->is_child_selected = selection_is_child_selected; + iface->remove_selection = selection_remove_selection; + iface->select_all_selection = selection_select_all_selection; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/a11y/gtk3atktable.cxx b/vcl/unx/gtk3/a11y/gtk3atktable.cxx new file mode 100644 index 000000000..221e55d2b --- /dev/null +++ b/vcl/unx/gtk3/a11y/gtk3atktable.cxx @@ -0,0 +1,580 @@ +/* -*- 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include "atkwrapper.hxx" + +#include <com/sun/star/accessibility/XAccessibleTable.hpp> +#include <comphelper/sequence.hxx> + +using namespace ::com::sun::star; + +static AtkObject * +atk_object_wrapper_conditional_ref( const uno::Reference< accessibility::XAccessible >& rxAccessible ) +{ + if( rxAccessible.is() ) + return atk_object_wrapper_ref( rxAccessible ); + + return nullptr; +} + +/*****************************************************************************/ + +// FIXME +static const gchar * +getAsConst( const OUString& rString ) +{ + static const int nMax = 10; + static OString aUgly[nMax]; + static int nIdx = 0; + nIdx = (nIdx + 1) % nMax; + aUgly[nIdx] = OUStringToOString( rString, RTL_TEXTENCODING_UTF8 ); + return aUgly[ nIdx ].getStr(); +} + +/*****************************************************************************/ + +/// @throws uno::RuntimeException +static css::uno::Reference<css::accessibility::XAccessibleTable> + getTable( AtkTable *pTable ) +{ + AtkObjectWrapper *pWrap = ATK_OBJECT_WRAPPER( pTable ); + if( pWrap ) + { + if( !pWrap->mpTable.is() ) + { + pWrap->mpTable.set(pWrap->mpContext, css::uno::UNO_QUERY); + } + + return pWrap->mpTable; + } + + return css::uno::Reference<css::accessibility::XAccessibleTable>(); +} + +/*****************************************************************************/ + +extern "C" { + +static AtkObject* +table_wrapper_ref_at (AtkTable *table, + gint row, + gint column) +{ + try { + css::uno::Reference<css::accessibility::XAccessibleTable> pTable = getTable( table ); + if( pTable.is() ) + return atk_object_wrapper_conditional_ref( pTable->getAccessibleCellAt( row, column ) ); + } + + catch(const uno::Exception&) { + g_warning( "Exception in getAccessibleCellAt()" ); + } + + return nullptr; +} + +/*****************************************************************************/ + +static gint +table_wrapper_get_index_at (AtkTable *table, + gint row, + gint column) +{ + try { + css::uno::Reference<css::accessibility::XAccessibleTable> pTable + = getTable( table ); + if( pTable.is() ) + return pTable->getAccessibleIndex( row, column ); + } + catch(const uno::Exception&) { + g_warning( "Exception in getAccessibleIndex()" ); + } + + return -1; +} + +/*****************************************************************************/ + +static gint +table_wrapper_get_column_at_index (AtkTable *table, + gint nIndex) +{ + try { + css::uno::Reference<css::accessibility::XAccessibleTable> pTable + = getTable( table ); + if( pTable.is() ) + return pTable->getAccessibleColumn( nIndex ); + } + catch(const uno::Exception&) { + g_warning( "Exception in getAccessibleColumn()" ); + } + + return -1; +} + +/*****************************************************************************/ + +static gint +table_wrapper_get_row_at_index( AtkTable *table, + gint nIndex ) +{ + try { + css::uno::Reference<css::accessibility::XAccessibleTable> pTable + = getTable( table ); + if( pTable.is() ) + return pTable->getAccessibleRow( nIndex ); + } + catch(const uno::Exception&) { + g_warning( "Exception in getAccessibleRow()" ); + } + + return -1; +} + +/*****************************************************************************/ + +static gint +table_wrapper_get_n_columns( AtkTable *table ) +{ + try { + css::uno::Reference<css::accessibility::XAccessibleTable> pTable + = getTable( table ); + if( pTable.is() ) + return pTable->getAccessibleColumnCount(); + } + catch(const uno::Exception&) { + g_warning( "Exception in getAccessibleColumnCount()" ); + } + + return -1; +} + +/*****************************************************************************/ + +static gint +table_wrapper_get_n_rows( AtkTable *table ) +{ + try { + css::uno::Reference<css::accessibility::XAccessibleTable> pTable + = getTable( table ); + if( pTable.is() ) + return pTable->getAccessibleRowCount(); + } + catch(const uno::Exception&) { + g_warning( "Exception in getAccessibleRowCount()" ); + } + + return -1; +} + +/*****************************************************************************/ + +static gint +table_wrapper_get_column_extent_at( AtkTable *table, + gint row, + gint column ) +{ + try { + css::uno::Reference<css::accessibility::XAccessibleTable> pTable + = getTable( table ); + if( pTable.is() ) + return pTable->getAccessibleColumnExtentAt( row, column ); + } + catch(const uno::Exception&) { + g_warning( "Exception in getAccessibleColumnExtentAt()" ); + } + + return -1; +} + +/*****************************************************************************/ + +static gint +table_wrapper_get_row_extent_at( AtkTable *table, + gint row, + gint column ) +{ + try { + css::uno::Reference<css::accessibility::XAccessibleTable> pTable + = getTable( table ); + if( pTable.is() ) + return pTable->getAccessibleRowExtentAt( row, column ); + } + catch(const uno::Exception&) { + g_warning( "Exception in getAccessibleRowExtentAt()" ); + } + + return -1; +} + +/*****************************************************************************/ + +static AtkObject * +table_wrapper_get_caption( AtkTable *table ) +{ + try { + css::uno::Reference<css::accessibility::XAccessibleTable> pTable + = getTable( table ); + if( pTable.is() ) + return atk_object_wrapper_conditional_ref( pTable->getAccessibleCaption() ); + } + + catch(const uno::Exception&) { + g_warning( "Exception in getAccessibleCaption()" ); + } + + return nullptr; +} + +/*****************************************************************************/ + +static const gchar * +table_wrapper_get_row_description( AtkTable *table, + gint row ) +{ + try { + css::uno::Reference<css::accessibility::XAccessibleTable> pTable + = getTable( table ); + if( pTable.is() ) + return getAsConst( pTable->getAccessibleRowDescription( row ) ); + } + catch(const uno::Exception&) { + g_warning( "Exception in getAccessibleRowDescription()" ); + } + + return nullptr; +} + +/*****************************************************************************/ + +static const gchar * +table_wrapper_get_column_description( AtkTable *table, + gint column ) +{ + try { + css::uno::Reference<css::accessibility::XAccessibleTable> pTable + = getTable( table ); + if( pTable.is() ) + return getAsConst( pTable->getAccessibleColumnDescription( column ) ); + } + catch(const uno::Exception&) { + g_warning( "Exception in getAccessibleColumnDescription()" ); + } + + return nullptr; +} + +/*****************************************************************************/ + +static AtkObject * +table_wrapper_get_row_header( AtkTable *table, + gint row ) +{ + try { + css::uno::Reference<css::accessibility::XAccessibleTable> pTable + = getTable( table ); + if( pTable.is() ) + { + uno::Reference< accessibility::XAccessibleTable > xRowHeaders( pTable->getAccessibleRowHeaders() ); + if( xRowHeaders.is() ) + return atk_object_wrapper_conditional_ref( xRowHeaders->getAccessibleCellAt( row, 0 ) ); + } + } + catch(const uno::Exception&) { + g_warning( "Exception in getAccessibleRowHeaders()" ); + } + + return nullptr; +} + +/*****************************************************************************/ + +static AtkObject * +table_wrapper_get_column_header( AtkTable *table, + gint column ) +{ + try { + css::uno::Reference<css::accessibility::XAccessibleTable> pTable + = getTable( table ); + if( pTable.is() ) + { + uno::Reference< accessibility::XAccessibleTable > xColumnHeaders( pTable->getAccessibleColumnHeaders() ); + if( xColumnHeaders.is() ) + return atk_object_wrapper_conditional_ref( xColumnHeaders->getAccessibleCellAt( 0, column ) ); + } + } + catch(const uno::Exception&) { + g_warning( "Exception in getAccessibleColumnHeaders()" ); + } + + return nullptr; +} + +/*****************************************************************************/ + +static AtkObject * +table_wrapper_get_summary( AtkTable *table ) +{ + try { + css::uno::Reference<css::accessibility::XAccessibleTable> pTable + = getTable( table ); + if( pTable.is() ) + { + return atk_object_wrapper_conditional_ref( pTable->getAccessibleSummary() ); + } + } + catch(const uno::Exception&) { + g_warning( "Exception in getAccessibleSummary()" ); + } + + return nullptr; +} + +/*****************************************************************************/ + +static gint +convertToGIntArray( const uno::Sequence< ::sal_Int32 >& aSequence, gint **pSelected ) +{ + if( aSequence.hasElements() ) + { + *pSelected = g_new( gint, aSequence.getLength() ); + + *pSelected = comphelper::sequenceToArray(*pSelected, aSequence); + } + + return aSequence.getLength(); +} + +/*****************************************************************************/ + +static gint +table_wrapper_get_selected_columns( AtkTable *table, + gint **pSelected ) +{ + *pSelected = nullptr; + try { + css::uno::Reference<css::accessibility::XAccessibleTable> pTable + = getTable( table ); + if( pTable.is() ) + return convertToGIntArray( pTable->getSelectedAccessibleColumns(), pSelected ); + } + catch(const uno::Exception&) { + g_warning( "Exception in getSelectedAccessibleColumns()" ); + } + + return 0; +} + +/*****************************************************************************/ + +static gint +table_wrapper_get_selected_rows( AtkTable *table, + gint **pSelected ) +{ + *pSelected = nullptr; + try { + css::uno::Reference<css::accessibility::XAccessibleTable> pTable + = getTable( table ); + if( pTable.is() ) + return convertToGIntArray( pTable->getSelectedAccessibleRows(), pSelected ); + } + catch(const uno::Exception&) { + g_warning( "Exception in getSelectedAccessibleRows()" ); + } + + return 0; +} + +/*****************************************************************************/ + +static gboolean +table_wrapper_is_column_selected( AtkTable *table, + gint column ) +{ + try { + css::uno::Reference<css::accessibility::XAccessibleTable> pTable + = getTable( table ); + if( pTable.is() ) + return pTable->isAccessibleColumnSelected( column ); + } + catch(const uno::Exception&) { + g_warning( "Exception in isAccessibleColumnSelected()" ); + } + + return 0; +} + +/*****************************************************************************/ + +static gboolean +table_wrapper_is_row_selected( AtkTable *table, + gint row ) +{ + try { + css::uno::Reference<css::accessibility::XAccessibleTable> pTable + = getTable( table ); + if( pTable.is() ) + return pTable->isAccessibleRowSelected( row ); + } + catch(const uno::Exception&) { + g_warning( "Exception in isAccessibleRowSelected()" ); + } + + return FALSE; +} + +/*****************************************************************************/ + +static gboolean +table_wrapper_is_selected( AtkTable *table, + gint row, + gint column ) +{ + try { + css::uno::Reference<css::accessibility::XAccessibleTable> pTable + = getTable( table ); + if( pTable.is() ) + return pTable->isAccessibleSelected( row, column ); + } + catch(const uno::Exception&) { + g_warning( "Exception in isAccessibleSelected()" ); + } + + return FALSE; +} + +/*****************************************************************************/ + +static gboolean +table_wrapper_add_row_selection( AtkTable *, gint ) +{ + g_warning( "FIXME: no simple analogue for add_row_selection" ); + return 0; +} + +/*****************************************************************************/ + +static gboolean +table_wrapper_remove_row_selection( AtkTable *, gint ) +{ + g_warning( "FIXME: no simple analogue for remove_row_selection" ); + return 0; +} + +/*****************************************************************************/ + +static gboolean +table_wrapper_add_column_selection( AtkTable *, gint ) +{ + g_warning( "FIXME: no simple analogue for add_column_selection" ); + return 0; +} + +/*****************************************************************************/ + +static gboolean +table_wrapper_remove_column_selection( AtkTable *, gint ) +{ + g_warning( "FIXME: no simple analogue for remove_column_selection" ); + return 0; +} + +/*****************************************************************************/ + +static void +table_wrapper_set_caption( AtkTable *, AtkObject * ) +{ // meaningless helper +} + +/*****************************************************************************/ + +static void +table_wrapper_set_column_description( AtkTable *, gint, const gchar * ) +{ // meaningless helper +} + +/*****************************************************************************/ + +static void +table_wrapper_set_column_header( AtkTable *, gint, AtkObject * ) +{ // meaningless helper +} + +/*****************************************************************************/ + +static void +table_wrapper_set_row_description( AtkTable *, gint, const gchar * ) +{ // meaningless helper +} + +/*****************************************************************************/ + +static void +table_wrapper_set_row_header( AtkTable *, gint, AtkObject * ) +{ // meaningless helper +} + +/*****************************************************************************/ + +static void +table_wrapper_set_summary( AtkTable *, AtkObject * ) +{ // meaningless helper +} + +/*****************************************************************************/ + +} // extern "C" + +void +tableIfaceInit (AtkTableIface *iface) +{ + g_return_if_fail (iface != nullptr); + + iface->ref_at = table_wrapper_ref_at; + iface->get_n_rows = table_wrapper_get_n_rows; + iface->get_n_columns = table_wrapper_get_n_columns; + iface->get_index_at = table_wrapper_get_index_at; + iface->get_column_at_index = table_wrapper_get_column_at_index; + iface->get_row_at_index = table_wrapper_get_row_at_index; + iface->is_row_selected = table_wrapper_is_row_selected; + iface->is_selected = table_wrapper_is_selected; + iface->get_selected_rows = table_wrapper_get_selected_rows; + iface->add_row_selection = table_wrapper_add_row_selection; + iface->remove_row_selection = table_wrapper_remove_row_selection; + iface->add_column_selection = table_wrapper_add_column_selection; + iface->remove_column_selection = table_wrapper_remove_column_selection; + iface->get_selected_columns = table_wrapper_get_selected_columns; + iface->is_column_selected = table_wrapper_is_column_selected; + iface->get_column_extent_at = table_wrapper_get_column_extent_at; + iface->get_row_extent_at = table_wrapper_get_row_extent_at; + iface->get_row_header = table_wrapper_get_row_header; + iface->set_row_header = table_wrapper_set_row_header; + iface->get_column_header = table_wrapper_get_column_header; + iface->set_column_header = table_wrapper_set_column_header; + iface->get_caption = table_wrapper_get_caption; + iface->set_caption = table_wrapper_set_caption; + iface->get_summary = table_wrapper_get_summary; + iface->set_summary = table_wrapper_set_summary; + iface->get_row_description = table_wrapper_get_row_description; + iface->set_row_description = table_wrapper_set_row_description; + iface->get_column_description = table_wrapper_get_column_description; + iface->set_column_description = table_wrapper_set_column_description; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/a11y/gtk3atktext.cxx b/vcl/unx/gtk3/a11y/gtk3atktext.cxx new file mode 100644 index 000000000..713b03325 --- /dev/null +++ b/vcl/unx/gtk3/a11y/gtk3atktext.cxx @@ -0,0 +1,897 @@ +/* -*- 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include "atkwrapper.hxx" +#include "atktextattributes.hxx" +#include <algorithm> + +#include <osl/diagnose.h> + +#include <com/sun/star/accessibility/AccessibleScrollType.hpp> +#include <com/sun/star/accessibility/AccessibleTextType.hpp> +#include <com/sun/star/accessibility/TextSegment.hpp> +#include <com/sun/star/accessibility/XAccessibleMultiLineText.hpp> +#include <com/sun/star/accessibility/XAccessibleText.hpp> +#include <com/sun/star/accessibility/XAccessibleTextAttributes.hpp> +#include <com/sun/star/accessibility/XAccessibleTextMarkup.hpp> +#include <com/sun/star/lang/NoSupportException.hpp> +#include <com/sun/star/text/TextMarkupType.hpp> + +using namespace ::com::sun::star; + +static sal_Int16 +text_type_from_boundary(AtkTextBoundary boundary_type) +{ + switch(boundary_type) + { + case ATK_TEXT_BOUNDARY_CHAR: + return accessibility::AccessibleTextType::CHARACTER; + case ATK_TEXT_BOUNDARY_WORD_START: + case ATK_TEXT_BOUNDARY_WORD_END: + return accessibility::AccessibleTextType::WORD; + case ATK_TEXT_BOUNDARY_SENTENCE_START: + case ATK_TEXT_BOUNDARY_SENTENCE_END: + return accessibility::AccessibleTextType::SENTENCE; + case ATK_TEXT_BOUNDARY_LINE_START: + case ATK_TEXT_BOUNDARY_LINE_END: + return accessibility::AccessibleTextType::LINE; + default: + return -1; + } +} + +/*****************************************************************************/ + +#if ATK_CHECK_VERSION(2,32,0) +static accessibility::AccessibleScrollType +scroll_type_from_scroll_type(AtkScrollType type) +{ + switch(type) + { + case ATK_SCROLL_TOP_LEFT: + return accessibility::AccessibleScrollType_SCROLL_TOP_LEFT; + case ATK_SCROLL_BOTTOM_RIGHT: + return accessibility::AccessibleScrollType_SCROLL_BOTTOM_RIGHT; + case ATK_SCROLL_TOP_EDGE: + return accessibility::AccessibleScrollType_SCROLL_TOP_EDGE; + case ATK_SCROLL_BOTTOM_EDGE: + return accessibility::AccessibleScrollType_SCROLL_BOTTOM_EDGE; + case ATK_SCROLL_LEFT_EDGE: + return accessibility::AccessibleScrollType_SCROLL_LEFT_EDGE; + case ATK_SCROLL_RIGHT_EDGE: + return accessibility::AccessibleScrollType_SCROLL_RIGHT_EDGE; + case ATK_SCROLL_ANYWHERE: + return accessibility::AccessibleScrollType_SCROLL_ANYWHERE; + default: + throw lang::NoSupportException(); + } +} +#endif + +/*****************************************************************************/ + +static gchar * +adjust_boundaries( css::uno::Reference<css::accessibility::XAccessibleText> const & pText, + accessibility::TextSegment const & rTextSegment, + AtkTextBoundary boundary_type, + gint * start_offset, gint * end_offset ) +{ + accessibility::TextSegment aTextSegment; + OUString aString; + gint start = 0, end = 0; + + if( !rTextSegment.SegmentText.isEmpty() ) + { + switch(boundary_type) + { + case ATK_TEXT_BOUNDARY_CHAR: + case ATK_TEXT_BOUNDARY_LINE_START: + case ATK_TEXT_BOUNDARY_LINE_END: + case ATK_TEXT_BOUNDARY_SENTENCE_START: + start = rTextSegment.SegmentStart; + end = rTextSegment.SegmentEnd; + aString = rTextSegment.SegmentText; + break; + + // the OOo break iterator behaves as SENTENCE_START + case ATK_TEXT_BOUNDARY_SENTENCE_END: + start = rTextSegment.SegmentStart; + end = rTextSegment.SegmentEnd; + + if( start > 0 ) + --start; + if( end > 0 && end < pText->getCharacterCount() - 1 ) + --end; + + aString = pText->getTextRange(start, end); + break; + + case ATK_TEXT_BOUNDARY_WORD_START: + start = rTextSegment.SegmentStart; + + // Determine the start index of the next segment + aTextSegment = pText->getTextBehindIndex(rTextSegment.SegmentEnd, + text_type_from_boundary(boundary_type)); + if( !aTextSegment.SegmentText.isEmpty() ) + end = aTextSegment.SegmentStart; + else + end = pText->getCharacterCount(); + + aString = pText->getTextRange(start, end); + break; + + case ATK_TEXT_BOUNDARY_WORD_END: + end = rTextSegment.SegmentEnd; + + // Determine the end index of the previous segment + aTextSegment = pText->getTextBeforeIndex(rTextSegment.SegmentStart, + text_type_from_boundary(boundary_type)); + if( !aTextSegment.SegmentText.isEmpty() ) + start = aTextSegment.SegmentEnd; + else + start = 0; + + aString = pText->getTextRange(start, end); + break; + + default: + return nullptr; + } + } + + *start_offset = start; + *end_offset = end; + + return OUStringToGChar(aString); +} + +/*****************************************************************************/ + +/// @throws uno::RuntimeException +static css::uno::Reference<css::accessibility::XAccessibleText> + getText( AtkText *pText ) +{ + AtkObjectWrapper *pWrap = ATK_OBJECT_WRAPPER( pText ); + if( pWrap ) + { + if( !pWrap->mpText.is() ) + { + pWrap->mpText.set(pWrap->mpContext, css::uno::UNO_QUERY); + } + + return pWrap->mpText; + } + + return css::uno::Reference<css::accessibility::XAccessibleText>(); +} + +/*****************************************************************************/ + +/// @throws uno::RuntimeException +static css::uno::Reference<css::accessibility::XAccessibleTextMarkup> + getTextMarkup( AtkText *pText ) +{ + AtkObjectWrapper *pWrap = ATK_OBJECT_WRAPPER( pText ); + if( pWrap ) + { + if( !pWrap->mpTextMarkup.is() ) + { + pWrap->mpTextMarkup.set(pWrap->mpContext, css::uno::UNO_QUERY); + } + + return pWrap->mpTextMarkup; + } + + return css::uno::Reference<css::accessibility::XAccessibleTextMarkup>(); +} + +/*****************************************************************************/ + +/// @throws uno::RuntimeException +static css::uno::Reference<css::accessibility::XAccessibleTextAttributes> + getTextAttributes( AtkText *pText ) +{ + AtkObjectWrapper *pWrap = ATK_OBJECT_WRAPPER( pText ); + if( pWrap ) + { + if( !pWrap->mpTextAttributes.is() ) + { + pWrap->mpTextAttributes.set(pWrap->mpContext, css::uno::UNO_QUERY); + } + + return pWrap->mpTextAttributes; + } + + return css::uno::Reference<css::accessibility::XAccessibleTextAttributes>(); +} + +/*****************************************************************************/ + +/// @throws uno::RuntimeException +static css::uno::Reference<css::accessibility::XAccessibleMultiLineText> + getMultiLineText( AtkText *pText ) +{ + AtkObjectWrapper *pWrap = ATK_OBJECT_WRAPPER( pText ); + if( pWrap ) + { + if( !pWrap->mpMultiLineText.is() ) + { + pWrap->mpMultiLineText.set(pWrap->mpContext, css::uno::UNO_QUERY); + } + + return pWrap->mpMultiLineText; + } + + return css::uno::Reference<css::accessibility::XAccessibleMultiLineText>(); +} + +/*****************************************************************************/ + +extern "C" { + +static gchar * +text_wrapper_get_text (AtkText *text, + gint start_offset, + gint end_offset) +{ + gchar * ret = nullptr; + + g_return_val_if_fail( (end_offset == -1) || (end_offset >= start_offset), nullptr ); + + /* at-spi expects the delete event to be send before the deletion happened + * so we save the deleted string object in the UNO event notification and + * fool libatk-bridge.so here .. + */ + void * pData = g_object_get_data( G_OBJECT(text), "ooo::text_changed::delete" ); + if( pData != nullptr ) + { + accessibility::TextSegment * pTextSegment = + static_cast <accessibility::TextSegment *> (pData); + + if( pTextSegment->SegmentStart == start_offset && + pTextSegment->SegmentEnd == end_offset ) + { + OString aUtf8 = OUStringToOString( pTextSegment->SegmentText, RTL_TEXTENCODING_UTF8 ); + return g_strdup( aUtf8.getStr() ); + } + } + + try { + css::uno::Reference<css::accessibility::XAccessibleText> pText + = getText( text ); + if( pText.is() ) + { + OUString aText; + sal_Int32 n = pText->getCharacterCount(); + + if( start_offset < n ) + { + if( -1 == end_offset ) + aText = pText->getTextRange(start_offset, n - start_offset); + else + aText = pText->getTextRange(start_offset, end_offset); + } + + ret = g_strdup( OUStringToOString(aText, RTL_TEXTENCODING_UTF8 ).getStr() ); + } + } + catch(const uno::Exception&) { + g_warning( "Exception in getText()" ); + } + + return ret; +} + +static gchar * +text_wrapper_get_text_after_offset (AtkText *text, + gint offset, + AtkTextBoundary boundary_type, + gint *start_offset, + gint *end_offset) +{ + try { + css::uno::Reference<css::accessibility::XAccessibleText> pText + = getText( text ); + if( pText.is() ) + { + accessibility::TextSegment aTextSegment = pText->getTextBehindIndex(offset, text_type_from_boundary(boundary_type)); + return adjust_boundaries(pText, aTextSegment, boundary_type, start_offset, end_offset); + } + } + catch(const uno::Exception&) { + g_warning( "Exception in get_text_after_offset()" ); + } + + return nullptr; +} + +static gchar * +text_wrapper_get_text_at_offset (AtkText *text, + gint offset, + AtkTextBoundary boundary_type, + gint *start_offset, + gint *end_offset) +{ + try { + css::uno::Reference<css::accessibility::XAccessibleText> pText + = getText( text ); + if( pText.is() ) + { + /* If the user presses the 'End' key, the caret will be placed behind the last character, + * which is the same index as the first character of the next line. In atk the magic offset + * '-2' is used to cover this special case. + */ + if ( + -2 == offset && + (ATK_TEXT_BOUNDARY_LINE_START == boundary_type || + ATK_TEXT_BOUNDARY_LINE_END == boundary_type) + ) + { + css::uno::Reference< + css::accessibility::XAccessibleMultiLineText> pMultiLineText + = getMultiLineText( text ); + if( pMultiLineText.is() ) + { + accessibility::TextSegment aTextSegment = pMultiLineText->getTextAtLineWithCaret(); + return adjust_boundaries(pText, aTextSegment, boundary_type, start_offset, end_offset); + } + } + + accessibility::TextSegment aTextSegment = pText->getTextAtIndex(offset, text_type_from_boundary(boundary_type)); + return adjust_boundaries(pText, aTextSegment, boundary_type, start_offset, end_offset); + } + } + catch(const uno::Exception&) { + g_warning( "Exception in get_text_at_offset()" ); + } + + return nullptr; +} + +static gunichar +text_wrapper_get_character_at_offset (AtkText *text, + gint offset) +{ + gint start, end; + gunichar uc = 0; + + gchar * char_as_string = + text_wrapper_get_text_at_offset(text, offset, ATK_TEXT_BOUNDARY_CHAR, + &start, &end); + if( char_as_string ) + { + uc = g_utf8_get_char( char_as_string ); + g_free( char_as_string ); + } + + return uc; +} + +static gchar * +text_wrapper_get_text_before_offset (AtkText *text, + gint offset, + AtkTextBoundary boundary_type, + gint *start_offset, + gint *end_offset) +{ + try { + css::uno::Reference<css::accessibility::XAccessibleText> pText + = getText( text ); + if( pText.is() ) + { + accessibility::TextSegment aTextSegment = pText->getTextBeforeIndex(offset, text_type_from_boundary(boundary_type)); + return adjust_boundaries(pText, aTextSegment, boundary_type, start_offset, end_offset); + } + } + catch(const uno::Exception&) { + g_warning( "Exception in text_before_offset()" ); + } + + return nullptr; +} + +static gint +text_wrapper_get_caret_offset (AtkText *text) +{ + gint offset = -1; + + try { + css::uno::Reference<css::accessibility::XAccessibleText> pText + = getText( text ); + if( pText.is() ) + offset = pText->getCaretPosition(); + } + catch(const uno::Exception&) { + g_warning( "Exception in getCaretPosition()" ); + } + + return offset; +} + +static gboolean +text_wrapper_set_caret_offset (AtkText *text, + gint offset) +{ + try { + css::uno::Reference<css::accessibility::XAccessibleText> pText + = getText( text ); + if( pText.is() ) + return pText->setCaretPosition( offset ); + } + catch(const uno::Exception&) { + g_warning( "Exception in setCaretPosition()" ); + } + + return FALSE; +} + +// #i92232# +static AtkAttributeSet* +handle_text_markup_as_run_attribute( css::uno::Reference<css::accessibility::XAccessibleTextMarkup> const & pTextMarkup, + const gint nTextMarkupType, + const gint offset, + AtkAttributeSet* pSet, + gint *start_offset, + gint *end_offset ) +{ + const gint nTextMarkupCount( pTextMarkup->getTextMarkupCount( nTextMarkupType ) ); + if ( nTextMarkupCount > 0 ) + { + for ( gint nTextMarkupIndex = 0; + nTextMarkupIndex < nTextMarkupCount; + ++nTextMarkupIndex ) + { + accessibility::TextSegment aTextSegment = + pTextMarkup->getTextMarkup( nTextMarkupIndex, nTextMarkupType ); + const gint nStartOffsetTextMarkup = aTextSegment.SegmentStart; + const gint nEndOffsetTextMarkup = aTextSegment.SegmentEnd; + if ( nStartOffsetTextMarkup <= offset ) + { + if ( offset < nEndOffsetTextMarkup ) + { + // text markup at <offset> + *start_offset = ::std::max( *start_offset, + nStartOffsetTextMarkup ); + *end_offset = ::std::min( *end_offset, + nEndOffsetTextMarkup ); + switch ( nTextMarkupType ) + { + case css::text::TextMarkupType::SPELLCHECK: + { + pSet = attribute_set_prepend_misspelled( pSet ); + } + break; + case css::text::TextMarkupType::TRACK_CHANGE_INSERTION: + { + pSet = attribute_set_prepend_tracked_change_insertion( pSet ); + } + break; + case css::text::TextMarkupType::TRACK_CHANGE_DELETION: + { + pSet = attribute_set_prepend_tracked_change_deletion( pSet ); + } + break; + case css::text::TextMarkupType::TRACK_CHANGE_FORMATCHANGE: + { + pSet = attribute_set_prepend_tracked_change_formatchange( pSet ); + } + break; + default: + { + OSL_ASSERT( false ); + } + } + break; // no further iteration needed. + } + else + { + *start_offset = ::std::max( *start_offset, + nEndOffsetTextMarkup ); + // continue iteration. + } + } + else + { + *end_offset = ::std::min( *end_offset, + nStartOffsetTextMarkup ); + break; // no further iteration. + } + } // eof iteration over text markups + } + + return pSet; +} + +static AtkAttributeSet * +text_wrapper_get_run_attributes( AtkText *text, + gint offset, + gint *start_offset, + gint *end_offset) +{ + AtkAttributeSet *pSet = nullptr; + + try { + bool bOffsetsAreValid = false; + + css::uno::Reference<css::accessibility::XAccessibleText> pText + = getText( text ); + if( pText.is()) + { + uno::Sequence< beans::PropertyValue > aAttributeList; + + css::uno::Reference<css::accessibility::XAccessibleTextAttributes> + pTextAttributes = getTextAttributes( text ); + if(pTextAttributes.is()) // Text attributes are available for paragraphs only + { + aAttributeList = pTextAttributes->getRunAttributes( offset, uno::Sequence< OUString > () ); + } + else // For other text objects use character attributes + { + aAttributeList = pText->getCharacterAttributes( offset, uno::Sequence< OUString > () ); + } + + pSet = attribute_set_new_from_property_values( aAttributeList, true, text ); + // #i100938# + // - always provide start_offset and end_offset + { + accessibility::TextSegment aTextSegment = + pText->getTextAtIndex(offset, accessibility::AccessibleTextType::ATTRIBUTE_RUN); + + *start_offset = aTextSegment.SegmentStart; + // #i100938# + // Do _not_ increment the end_offset provide by <accessibility::TextSegment> instance + *end_offset = aTextSegment.SegmentEnd; + bOffsetsAreValid = true; + } + } + + // Special handling for misspelled text + // #i92232# + // - add special handling for tracked changes and refactor the + // corresponding code for handling misspelled text. + css::uno::Reference<css::accessibility::XAccessibleTextMarkup> + pTextMarkup = getTextMarkup( text ); + if( pTextMarkup.is() ) + { + // Get attribute run here if it hasn't been done before + if (!bOffsetsAreValid && pText.is()) + { + accessibility::TextSegment aAttributeTextSegment = + pText->getTextAtIndex(offset, accessibility::AccessibleTextType::ATTRIBUTE_RUN); + *start_offset = aAttributeTextSegment.SegmentStart; + *end_offset = aAttributeTextSegment.SegmentEnd; + } + // handle misspelled text + pSet = handle_text_markup_as_run_attribute( + pTextMarkup, + css::text::TextMarkupType::SPELLCHECK, + offset, pSet, start_offset, end_offset ); + // handle tracked changes + pSet = handle_text_markup_as_run_attribute( + pTextMarkup, + css::text::TextMarkupType::TRACK_CHANGE_INSERTION, + offset, pSet, start_offset, end_offset ); + pSet = handle_text_markup_as_run_attribute( + pTextMarkup, + css::text::TextMarkupType::TRACK_CHANGE_DELETION, + offset, pSet, start_offset, end_offset ); + pSet = handle_text_markup_as_run_attribute( + pTextMarkup, + css::text::TextMarkupType::TRACK_CHANGE_FORMATCHANGE, + offset, pSet, start_offset, end_offset ); + } + } + catch(const uno::Exception&){ + + g_warning( "Exception in get_run_attributes()" ); + + if( pSet ) + { + atk_attribute_set_free( pSet ); + pSet = nullptr; + } + } + + return pSet; +} + +/*****************************************************************************/ + +static AtkAttributeSet * +text_wrapper_get_default_attributes( AtkText *text ) +{ + AtkAttributeSet *pSet = nullptr; + + try { + css::uno::Reference<css::accessibility::XAccessibleTextAttributes> + pTextAttributes = getTextAttributes( text ); + if( pTextAttributes.is() ) + { + uno::Sequence< beans::PropertyValue > aAttributeList = + pTextAttributes->getDefaultAttributes( uno::Sequence< OUString > () ); + + pSet = attribute_set_new_from_property_values( aAttributeList, false, text ); + } + } + catch(const uno::Exception&) { + + g_warning( "Exception in get_default_attributes()" ); + + if( pSet ) + { + atk_attribute_set_free( pSet ); + pSet = nullptr; + } + } + + return pSet; +} + +/*****************************************************************************/ + +static void +text_wrapper_get_character_extents( AtkText *text, + gint offset, + gint *x, + gint *y, + gint *width, + gint *height, + AtkCoordType coords ) +{ + *x = *y = *width = *height = -1; + + try { + css::uno::Reference<css::accessibility::XAccessibleText> pText + = getText( text ); + if( pText.is() ) + { + awt::Rectangle aRect = pText->getCharacterBounds( offset ); + + gint origin_x = 0; + gint origin_y = 0; + + if( coords == ATK_XY_SCREEN ) + { + g_return_if_fail( ATK_IS_COMPONENT( text ) ); + SAL_WNODEPRECATED_DECLARATIONS_PUSH + atk_component_get_position( ATK_COMPONENT( text ), &origin_x, &origin_y, coords); + SAL_WNODEPRECATED_DECLARATIONS_POP + } + + *x = aRect.X + origin_x; + *y = aRect.Y + origin_y; + *width = aRect.Width; + *height = aRect.Height; + } + } + catch(const uno::Exception&) { + g_warning( "Exception in getCharacterBounds" ); + } +} + +static gint +text_wrapper_get_character_count (AtkText *text) +{ + gint rv = 0; + + try { + css::uno::Reference<css::accessibility::XAccessibleText> pText + = getText( text ); + if( pText.is() ) + rv = pText->getCharacterCount(); + } + catch(const uno::Exception&) { + g_warning( "Exception in getCharacterCount" ); + } + + return rv; +} + +static gint +text_wrapper_get_offset_at_point (AtkText *text, + gint x, + gint y, + AtkCoordType coords) +{ + try { + css::uno::Reference<css::accessibility::XAccessibleText> pText + = getText( text ); + if( pText.is() ) + { + gint origin_x = 0; + gint origin_y = 0; + + if( coords == ATK_XY_SCREEN ) + { + g_return_val_if_fail( ATK_IS_COMPONENT( text ), -1 ); + SAL_WNODEPRECATED_DECLARATIONS_PUSH + atk_component_get_position( ATK_COMPONENT( text ), &origin_x, &origin_y, coords); + SAL_WNODEPRECATED_DECLARATIONS_POP + } + + return pText->getIndexAtPoint( awt::Point(x - origin_x, y - origin_y) ); + } + } + catch(const uno::Exception&) { + g_warning( "Exception in getIndexAtPoint" ); + } + + return -1; +} + +// FIXME: the whole series of selections API is problematic ... + +static gint +text_wrapper_get_n_selections (AtkText *text) +{ + gint rv = 0; + + try { + css::uno::Reference<css::accessibility::XAccessibleText> pText + = getText( text ); + if( pText.is() ) + rv = ( pText->getSelectionEnd() > pText->getSelectionStart() ) ? 1 : 0; + } + catch(const uno::Exception&) { + g_warning( "Exception in getSelectionEnd() or getSelectionStart()" ); + } + + return rv; +} + +static gchar * +text_wrapper_get_selection (AtkText *text, + gint selection_num, + gint *start_offset, + gint *end_offset) +{ + g_return_val_if_fail( selection_num == 0, FALSE ); + + try { + css::uno::Reference<css::accessibility::XAccessibleText> pText + = getText( text ); + if( pText.is() ) + { + *start_offset = pText->getSelectionStart(); + *end_offset = pText->getSelectionEnd(); + + return OUStringToGChar( pText->getSelectedText() ); + } + } + catch(const uno::Exception&) { + g_warning( "Exception in getSelectionEnd(), getSelectionStart() or getSelectedText()" ); + } + + return nullptr; +} + +static gboolean +text_wrapper_add_selection (AtkText *text, + gint start_offset, + gint end_offset) +{ + // FIXME: can we try to be more compatible by expanding an + // existing adjacent selection ? + + try { + css::uno::Reference<css::accessibility::XAccessibleText> pText + = getText( text ); + if( pText.is() ) + return pText->setSelection( start_offset, end_offset ); // ? + } + catch(const uno::Exception&) { + g_warning( "Exception in setSelection()" ); + } + + return FALSE; +} + +static gboolean +text_wrapper_remove_selection (AtkText *text, + gint selection_num) +{ + g_return_val_if_fail( selection_num == 0, FALSE ); + + try { + css::uno::Reference<css::accessibility::XAccessibleText> pText + = getText( text ); + if( pText.is() ) + return pText->setSelection( 0, 0 ); // ? + } + catch(const uno::Exception&) { + g_warning( "Exception in setSelection()" ); + } + + return FALSE; +} + +static gboolean +text_wrapper_set_selection (AtkText *text, + gint selection_num, + gint start_offset, + gint end_offset) +{ + g_return_val_if_fail( selection_num == 0, FALSE ); + + try { + css::uno::Reference<css::accessibility::XAccessibleText> pText + = getText( text ); + if( pText.is() ) + return pText->setSelection( start_offset, end_offset ); + } + catch(const uno::Exception&) { + g_warning( "Exception in setSelection()" ); + } + + return FALSE; +} + +#if ATK_CHECK_VERSION(2,32,0) +static gboolean +text_wrapper_scroll_substring_to(AtkText *text, + gint start_offset, + gint end_offset, + AtkScrollType scroll_type) +{ + try { + css::uno::Reference<css::accessibility::XAccessibleText> pText + = getText( text ); + + if( pText.is() ) + return pText->scrollSubstringTo( start_offset, end_offset, + scroll_type_from_scroll_type( scroll_type ) ); + } + catch(const uno::Exception&) { + g_warning( "Exception in scrollSubstringTo()" ); + } + + return FALSE; +} +#endif + +} // extern "C" + +void +textIfaceInit (AtkTextIface *iface) +{ + g_return_if_fail (iface != nullptr); + + iface->get_text = text_wrapper_get_text; + iface->get_character_at_offset = text_wrapper_get_character_at_offset; + iface->get_text_before_offset = text_wrapper_get_text_before_offset; + iface->get_text_at_offset = text_wrapper_get_text_at_offset; + iface->get_text_after_offset = text_wrapper_get_text_after_offset; + iface->get_caret_offset = text_wrapper_get_caret_offset; + iface->set_caret_offset = text_wrapper_set_caret_offset; + iface->get_character_count = text_wrapper_get_character_count; + iface->get_n_selections = text_wrapper_get_n_selections; + iface->get_selection = text_wrapper_get_selection; + iface->add_selection = text_wrapper_add_selection; + iface->remove_selection = text_wrapper_remove_selection; + iface->set_selection = text_wrapper_set_selection; + iface->get_run_attributes = text_wrapper_get_run_attributes; + iface->get_default_attributes = text_wrapper_get_default_attributes; + iface->get_character_extents = text_wrapper_get_character_extents; + iface->get_offset_at_point = text_wrapper_get_offset_at_point; +#if ATK_CHECK_VERSION(2,32,0) + iface->scroll_substring_to = text_wrapper_scroll_substring_to; +#endif +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/a11y/gtk3atktextattributes.cxx b/vcl/unx/gtk3/a11y/gtk3atktextattributes.cxx new file mode 100644 index 000000000..81115fb6a --- /dev/null +++ b/vcl/unx/gtk3/a11y/gtk3atktextattributes.cxx @@ -0,0 +1,1390 @@ +/* -*- 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include "atktextattributes.hxx" + +#include <com/sun/star/awt/FontSlant.hpp> +#include <com/sun/star/awt/FontStrikeout.hpp> +#include <com/sun/star/awt/FontUnderline.hpp> + +#include <com/sun/star/style/CaseMap.hpp> +#include <com/sun/star/style/LineSpacing.hpp> +#include <com/sun/star/style/LineSpacingMode.hpp> +#include <com/sun/star/style/ParagraphAdjust.hpp> +#include <com/sun/star/style/TabAlign.hpp> +#include <com/sun/star/style/TabStop.hpp> + +#include <com/sun/star/text/WritingMode2.hpp> + +#include "atkwrapper.hxx" + +#include <com/sun/star/accessibility/XAccessibleComponent.hpp> + +#include <i18nlangtag/languagetag.hxx> + +#include <stdio.h> +#include <string.h> + +using namespace ::com::sun::star; + +typedef gchar* (* AtkTextAttrFunc) ( const uno::Any& rAny ); +typedef bool (* TextPropertyValueFunc) ( uno::Any& rAny, const gchar * value ); + +#define STRNCMP_PARAM( s ) s,sizeof( s )-1 + +/*****************************************************************************/ + +static AtkTextAttribute atk_text_attribute_paragraph_style = ATK_TEXT_ATTR_INVALID; +static AtkTextAttribute atk_text_attribute_font_effect = ATK_TEXT_ATTR_INVALID; +static AtkTextAttribute atk_text_attribute_decoration = ATK_TEXT_ATTR_INVALID; +static AtkTextAttribute atk_text_attribute_line_height = ATK_TEXT_ATTR_INVALID; +static AtkTextAttribute atk_text_attribute_rotation = ATK_TEXT_ATTR_INVALID; +static AtkTextAttribute atk_text_attribute_shadow = ATK_TEXT_ATTR_INVALID; +static AtkTextAttribute atk_text_attribute_tab_interval = ATK_TEXT_ATTR_INVALID; +static AtkTextAttribute atk_text_attribute_tab_stops = ATK_TEXT_ATTR_INVALID; +static AtkTextAttribute atk_text_attribute_writing_mode = ATK_TEXT_ATTR_INVALID; +static AtkTextAttribute atk_text_attribute_vertical_align = ATK_TEXT_ATTR_INVALID; +static AtkTextAttribute atk_text_attribute_misspelled = ATK_TEXT_ATTR_INVALID; +// #i92232# +static AtkTextAttribute atk_text_attribute_tracked_change = ATK_TEXT_ATTR_INVALID; +// #i92233# +static AtkTextAttribute atk_text_attribute_mm_to_pixel_ratio = ATK_TEXT_ATTR_INVALID; + +/*****************************************************************************/ + +/** + * !! IMPORTANT NOTE !! : when adding items to this list, KEEP THE LIST SORTED + * and re-arrange the enum values accordingly. + */ + +namespace { + +enum ExportedAttribute +{ + TEXT_ATTRIBUTE_BACKGROUND_COLOR = 0, + TEXT_ATTRIBUTE_CASEMAP, + TEXT_ATTRIBUTE_FOREGROUND_COLOR, + TEXT_ATTRIBUTE_CONTOURED, + TEXT_ATTRIBUTE_CHAR_ESCAPEMENT, + TEXT_ATTRIBUTE_BLINKING, + TEXT_ATTRIBUTE_FONT_NAME, + TEXT_ATTRIBUTE_HEIGHT, + TEXT_ATTRIBUTE_HIDDEN, + TEXT_ATTRIBUTE_KERNING, + TEXT_ATTRIBUTE_LOCALE, + TEXT_ATTRIBUTE_POSTURE, + TEXT_ATTRIBUTE_RELIEF, + TEXT_ATTRIBUTE_ROTATION, + TEXT_ATTRIBUTE_SCALE, + TEXT_ATTRIBUTE_SHADOWED, + TEXT_ATTRIBUTE_STRIKETHROUGH, + TEXT_ATTRIBUTE_UNDERLINE, + TEXT_ATTRIBUTE_WEIGHT, + // #i92233# + TEXT_ATTRIBUTE_MM_TO_PIXEL_RATIO, + TEXT_ATTRIBUTE_JUSTIFICATION, + TEXT_ATTRIBUTE_BOTTOM_MARGIN, + TEXT_ATTRIBUTE_FIRST_LINE_INDENT, + TEXT_ATTRIBUTE_LEFT_MARGIN, + TEXT_ATTRIBUTE_LINE_SPACING, + TEXT_ATTRIBUTE_RIGHT_MARGIN, + TEXT_ATTRIBUTE_STYLE_NAME, + TEXT_ATTRIBUTE_TAB_STOPS, + TEXT_ATTRIBUTE_TOP_MARGIN, + TEXT_ATTRIBUTE_WRITING_MODE, + TEXT_ATTRIBUTE_LAST +}; + +} + +static const char * ExportedTextAttributes[TEXT_ATTRIBUTE_LAST] = +{ + "CharBackColor", // TEXT_ATTRIBUTE_BACKGROUND_COLOR + "CharCaseMap", // TEXT_ATTRIBUTE_CASEMAP + "CharColor", // TEXT_ATTRIBUTE_FOREGROUND_COLOR + "CharContoured", // TEXT_ATTRIBUTE_CONTOURED + "CharEscapement", // TEXT_ATTRIBUTE_CHAR_ESCAPEMENT + "CharFlash", // TEXT_ATTRIBUTE_BLINKING + "CharFontName", // TEXT_ATTRIBUTE_FONT_NAME + "CharHeight", // TEXT_ATTRIBUTE_HEIGHT + "CharHidden", // TEXT_ATTRIBUTE_HIDDEN + "CharKerning", // TEXT_ATTRIBUTE_KERNING + "CharLocale", // TEXT_ATTRIBUTE_LOCALE + "CharPosture", // TEXT_ATTRIBUTE_POSTURE + "CharRelief", // TEXT_ATTRIBUTE_RELIEF + "CharRotation", // TEXT_ATTRIBUTE_ROTATION + "CharScaleWidth", // TEXT_ATTRIBUTE_SCALE + "CharShadowed", // TEXT_ATTRIBUTE_SHADOWED + "CharStrikeout", // TEXT_ATTRIBUTE_STRIKETHROUGH + "CharUnderline", // TEXT_ATTRIBUTE_UNDERLINE + "CharWeight", // TEXT_ATTRIBUTE_WEIGHT + // #i92233# + "MMToPixelRatio", // TEXT_ATTRIBUTE_MM_TO_PIXEL_RATIO + "ParaAdjust", // TEXT_ATTRIBUTE_JUSTIFICATION + "ParaBottomMargin", // TEXT_ATTRIBUTE_BOTTOM_MARGIN + "ParaFirstLineIndent", // TEXT_ATTRIBUTE_FIRST_LINE_INDENT + "ParaLeftMargin", // TEXT_ATTRIBUTE_LEFT_MARGIN + "ParaLineSpacing", // TEXT_ATTRIBUTE_LINE_SPACING + "ParaRightMargin", // TEXT_ATTRIBUTE_RIGHT_MARGIN + "ParaStyleName", // TEXT_ATTRIBUTE_STYLE_NAME + "ParaTabStops", // TEXT_ATTRIBUTE_TAB_STOPS + "ParaTopMargin", // TEXT_ATTRIBUTE_TOP_MARGIN + "WritingMode" // TEXT_ATTRIBUTE_WRITING_MODE +}; + +/*****************************************************************************/ + +static gchar* +get_value( const uno::Sequence< beans::PropertyValue >& rAttributeList, + sal_Int32 nIndex, AtkTextAttrFunc func ) +{ + if( nIndex != -1 ) + return func(rAttributeList[nIndex].Value); + + return nullptr; +} + +#define get_bool_value( list, index ) get_value( list, index, Bool2String ) +#define get_height_value( list, index ) get_value( list, index, Float2String ) +#define get_justification_value( list, index ) get_value( list, index, Adjust2Justification ) +#define get_cmm_value( list, index ) get_value( list, index, CMM2UnitString ) +#define get_scale_width( list, index ) get_value( list, index, Scale2String ) +#define get_strikethrough_value( list, index ) get_value( list, index, Strikeout2String ) +#define get_string_value( list, index ) get_value( list, index, GetString ) +#define get_style_value( list, index ) get_value( list, index, FontSlant2Style ) +#define get_underline_value( list, index ) get_value( list, index, Underline2String ) +#define get_variant_value( list, index ) get_value( list, index, CaseMap2String ) +#define get_weight_value( list, index ) get_value( list, index, Weight2String ) +#define get_language_string( list, index ) get_value( list, index, Locale2String ) + +static double toPoint(sal_Int16 n) +{ + // 100th mm -> pt + return static_cast<double>(n * 72) / 2540; +} + +/*****************************************************************************/ + +static bool +InvalidValue( uno::Any&, const gchar * ) +{ + return false; +} + +/*****************************************************************************/ + +static gchar* +Float2String(const uno::Any& rAny) +{ + return g_strdup_printf( "%g", rAny.get<float>() ); +} + +static bool +String2Float( uno::Any& rAny, const gchar * value ) +{ + float fval; + + if( 1 != sscanf( value, "%g", &fval ) ) + return false; + + rAny <<= fval; + return true; +} + +/*****************************************************************************/ + +/// @throws uno::RuntimeException +static css::uno::Reference<css::accessibility::XAccessibleComponent> + getComponent( AtkText *pText ) +{ + AtkObjectWrapper *pWrap = ATK_OBJECT_WRAPPER( pText ); + if( pWrap ) + { + if( !pWrap->mpComponent.is() ) + { + pWrap->mpComponent.set(pWrap->mpContext, css::uno::UNO_QUERY); + } + + return pWrap->mpComponent; + } + + return css::uno::Reference<css::accessibility::XAccessibleComponent>(); +} + +static gchar* +get_color_value(const uno::Sequence< beans::PropertyValue >& rAttributeList, + const sal_Int32 * pIndexArray, + ExportedAttribute attr, + AtkText * text) +{ + sal_Int32 nColor = -1; // AUTOMATIC + sal_Int32 nIndex = pIndexArray[attr]; + + if( nIndex != -1 ) + nColor = rAttributeList[nIndex].Value.get<sal_Int32>(); + + /* + * Check for color value for 100% alpha white, which means + * "automatic". Grab the RGB value from XAccessibleComponent + * in this case. + */ + + if( (nColor == -1) && text ) + { + try + { + css::uno::Reference<css::accessibility::XAccessibleComponent> + pComponent = getComponent( text ); + if( pComponent.is() ) + { + switch( attr ) + { + case TEXT_ATTRIBUTE_BACKGROUND_COLOR: + nColor = pComponent->getBackground(); + break; + case TEXT_ATTRIBUTE_FOREGROUND_COLOR: + nColor = pComponent->getForeground(); + break; + default: + break; + } + } + } + + catch(const uno::Exception&) { + g_warning( "Exception in get[Fore|Back]groundColor()" ); + } + } + + if( nColor != -1 ) + { + sal_uInt8 blue = nColor & 0xFF; + sal_uInt8 green = (nColor >> 8) & 0xFF; + sal_uInt8 red = (nColor >> 16) & 0xFF; + + return g_strdup_printf( "%u,%u,%u", red, green, blue ); + } + + return nullptr; +} + +static bool +String2Color( uno::Any& rAny, const gchar * value ) +{ + int red, green, blue; + + if( 3 != sscanf( value, "%d,%d,%d", &red, &green, &blue ) ) + return false; + + sal_Int32 nColor = static_cast<sal_Int32>(blue) | ( static_cast<sal_Int32>(green) << 8 ) | ( static_cast<sal_Int32>(red) << 16 ); + rAny <<= nColor; + return true; +} + +/*****************************************************************************/ + +static gchar* +FontSlant2Style(const uno::Any& rAny) +{ + const gchar * value = nullptr; + + awt::FontSlant aFontSlant; + if(!(rAny >>= aFontSlant)) + return nullptr; + + switch( aFontSlant ) + { + case awt::FontSlant_NONE: + value = "normal"; + break; + + case awt::FontSlant_OBLIQUE: + value = "oblique"; + break; + + case awt::FontSlant_ITALIC: + value = "italic"; + break; + + case awt::FontSlant_REVERSE_OBLIQUE: + value = "reverse oblique"; + break; + + case awt::FontSlant_REVERSE_ITALIC: + value = "reverse italic"; + break; + + default: + break; + } + + if( value ) + return g_strdup( value ); + + return nullptr; +} + +static bool +Style2FontSlant( uno::Any& rAny, const gchar * value ) +{ + awt::FontSlant aFontSlant; + + if( strncmp( value, STRNCMP_PARAM( "normal" ) ) == 0 ) + aFontSlant = awt::FontSlant_NONE; + else if( strncmp( value, STRNCMP_PARAM( "oblique" ) ) == 0 ) + aFontSlant = awt::FontSlant_OBLIQUE; + else if( strncmp( value, STRNCMP_PARAM( "italic" ) ) == 0 ) + aFontSlant = awt::FontSlant_ITALIC; + else if( strncmp( value, STRNCMP_PARAM( "reverse oblique" ) ) == 0 ) + aFontSlant = awt::FontSlant_REVERSE_OBLIQUE; + else if( strncmp( value, STRNCMP_PARAM( "reverse italic" ) ) == 0 ) + aFontSlant = awt::FontSlant_REVERSE_ITALIC; + else + return false; + + rAny <<= aFontSlant; + return true; +} + +/*****************************************************************************/ + +static gchar* +Weight2String(const uno::Any& rAny) +{ + return g_strdup_printf( "%g", rAny.get<float>() * 4 ); +} + +static bool +String2Weight( uno::Any& rAny, const gchar * value ) +{ + float weight; + + if( 1 != sscanf( value, "%g", &weight ) ) + return false; + + rAny <<= weight / 4; + return true; +} + +/*****************************************************************************/ + +static gchar* +Adjust2Justification(const uno::Any& rAny) +{ + const gchar * value = nullptr; + + switch( static_cast<style::ParagraphAdjust>(rAny.get<short>()) ) + { + case style::ParagraphAdjust_LEFT: + value = "left"; + break; + + case style::ParagraphAdjust_RIGHT: + value = "right"; + break; + + case style::ParagraphAdjust_BLOCK: + case style::ParagraphAdjust_STRETCH: + value = "fill"; + break; + + case style::ParagraphAdjust_CENTER: + value = "center"; + break; + + default: + break; + } + + if( value ) + return g_strdup( value ); + + return nullptr; +} + +static bool +Justification2Adjust( uno::Any& rAny, const gchar * value ) +{ + style::ParagraphAdjust nParagraphAdjust; + + if( strncmp( value, STRNCMP_PARAM( "left" ) ) == 0 ) + nParagraphAdjust = style::ParagraphAdjust_LEFT; + else if( strncmp( value, STRNCMP_PARAM( "right" ) ) == 0 ) + nParagraphAdjust = style::ParagraphAdjust_RIGHT; + else if( strncmp( value, STRNCMP_PARAM( "fill" ) ) == 0 ) + nParagraphAdjust = style::ParagraphAdjust_BLOCK; + else if( strncmp( value, STRNCMP_PARAM( "center" ) ) == 0 ) + nParagraphAdjust = style::ParagraphAdjust_CENTER; + else + return false; + + rAny <<= static_cast<short>(nParagraphAdjust); + return true; +} + +/*****************************************************************************/ + +const gchar * const font_strikethrough[] = { + "none", // FontStrikeout::NONE + "single", // FontStrikeout::SINGLE + "double", // FontStrikeout::DOUBLE + nullptr, // FontStrikeout::DONTKNOW + "bold", // FontStrikeout::BOLD + "with /", // FontStrikeout::SLASH + "with X" // FontStrikeout::X +}; + +static gchar* +Strikeout2String(const uno::Any& rAny) +{ + sal_Int16 n = rAny.get<sal_Int16>(); + + if( n >= 0 && n < sal_Int16(SAL_N_ELEMENTS(font_strikethrough)) ) + return g_strdup( font_strikethrough[n] ); + + return nullptr; +} + +static bool +String2Strikeout( uno::Any& rAny, const gchar * value ) +{ + for( sal_Int16 n=0; n < sal_Int16(SAL_N_ELEMENTS(font_strikethrough)); ++n ) + { + if( ( nullptr != font_strikethrough[n] ) && + 0 == strncmp( value, font_strikethrough[n], strlen( font_strikethrough[n] ) ) ) + { + rAny <<= n; + return true; + } + } + + return false; +} + +/*****************************************************************************/ + +static gchar* +Underline2String(const uno::Any& rAny) +{ + const gchar * value = nullptr; + + switch( rAny.get<sal_Int16>() ) + { + case awt::FontUnderline::NONE: + value = "none"; + break; + + case awt::FontUnderline::SINGLE: + value = "single"; + break; + + case awt::FontUnderline::DOUBLE: + value = "double"; + break; + + default: + break; + } + + if( value ) + return g_strdup( value ); + + return nullptr; +} + +static bool +String2Underline( uno::Any& rAny, const gchar * value ) +{ + short nUnderline; + + if( strncmp( value, STRNCMP_PARAM( "none" ) ) == 0 ) + nUnderline = awt::FontUnderline::NONE; + else if( strncmp( value, STRNCMP_PARAM( "single" ) ) == 0 ) + nUnderline = awt::FontUnderline::SINGLE; + else if( strncmp( value, STRNCMP_PARAM( "double" ) ) == 0 ) + nUnderline = awt::FontUnderline::DOUBLE; + else + return false; + + rAny <<= nUnderline; + return true; +} + +/*****************************************************************************/ + +static gchar* +GetString(const uno::Any& rAny) +{ + OString aFontName = OUStringToOString( rAny.get< OUString > (), RTL_TEXTENCODING_UTF8 ); + + if( !aFontName.isEmpty() ) + return g_strdup( aFontName.getStr() ); + + return nullptr; +} + +static bool +SetString( uno::Any& rAny, const gchar * value ) +{ + OString aFontName( value ); + + if( !aFontName.isEmpty() ) + { + rAny <<= OStringToOUString( aFontName, RTL_TEXTENCODING_UTF8 ); + return true; + } + + return false; +} + +/*****************************************************************************/ + +// @see http://developer.gnome.org/doc/API/2.0/atk/AtkText.html#AtkTextAttribute + +// CMM = 100th of mm +static gchar* +CMM2UnitString(const uno::Any& rAny) +{ + double fValue = rAny.get<sal_Int32>(); + fValue = fValue * 0.01; + + return g_strdup_printf( "%gmm", fValue ); +} + +static bool +UnitString2CMM( uno::Any& rAny, const gchar * value ) +{ + float fValue = 0.0; // pb: don't use double here because of warning on linux + + if( 1 != sscanf( value, "%gmm", &fValue ) ) + return false; + + fValue = fValue * 100; + + rAny <<= static_cast<sal_Int32>(fValue); + return true; +} + +/*****************************************************************************/ + +static const gchar * bool_values[] = { "true", "false" }; + +static gchar * +Bool2String( const uno::Any& rAny ) +{ + int n = 1; + + if( rAny.get<bool>() ) + n = 0; + + return g_strdup( bool_values[n] ); +} + +static bool +String2Bool( uno::Any& rAny, const gchar * value ) +{ + bool bValue; + + if( strncmp( value, STRNCMP_PARAM( "true" ) ) == 0 ) + bValue = true; + else if( strncmp( value, STRNCMP_PARAM( "false" ) ) == 0 ) + bValue = false; + else + return false; + + rAny <<= bValue; + return true; +} + +/*****************************************************************************/ + +static gchar* +Scale2String( const uno::Any& rAny ) +{ + return g_strdup_printf( "%g", static_cast<double>(rAny.get< sal_Int16 > ()) / 100 ); +} + +static bool +String2Scale( uno::Any& rAny, const gchar * value ) +{ + double dval; + + if( 1 != sscanf( value, "%lg", &dval ) ) + return false; + + rAny <<= static_cast<sal_Int16>(dval * 100); + return true; +} + +/*****************************************************************************/ + +static gchar * +CaseMap2String( const uno::Any& rAny ) +{ + const gchar * value; + + switch( rAny.get<short>() ) + { + case style::CaseMap::SMALLCAPS: + value = "small_caps"; + break; + + default: + value = "normal"; + break; + } + + return g_strdup(value); +} + +static bool +String2CaseMap( uno::Any& rAny, const gchar * value ) +{ + short nCaseMap; + + if( strncmp( value, STRNCMP_PARAM( "normal" ) ) == 0 ) + nCaseMap = style::CaseMap::NONE; + else if( strncmp( value, STRNCMP_PARAM( "small_caps" ) ) == 0 ) + nCaseMap = style::CaseMap::SMALLCAPS; + else + return false; + + rAny <<= nCaseMap; + return true; +} + +/*****************************************************************************/ + +const gchar * const font_stretch[] = { + "ultra_condensed", + "extra_condensed", + "condensed", + "semi_condensed", + "normal", + "semi_expanded", + "expanded", + "extra_expanded", + "ultra_expanded" +}; + +static gchar* +Kerning2Stretch(const uno::Any& rAny) +{ + sal_Int16 n = rAny.get<sal_Int16>(); + int i = 4; + + // No good idea for a mapping - just return the basic info + if( n < 0 ) + i=2; + else if( n > 0 ) + i=6; + + return g_strdup(font_stretch[i]); +} + +/*****************************************************************************/ + +static gchar* +Locale2String(const uno::Any& rAny) +{ + /* FIXME-BCP47: support language tags? And why is country lowercase? */ + lang::Locale aLocale = rAny.get<lang::Locale> (); + LanguageTag aLanguageTag( aLocale); + return g_strdup_printf( "%s-%s", + OUStringToOString( aLanguageTag.getLanguage(), RTL_TEXTENCODING_ASCII_US).getStr(), + OUStringToOString( aLanguageTag.getCountry(), RTL_TEXTENCODING_ASCII_US).toAsciiLowerCase().getStr() ); +} + +static bool +String2Locale( uno::Any& rAny, const gchar * value ) +{ + /* FIXME-BCP47: support language tags? */ + bool ret = false; + + gchar ** str_array = g_strsplit_set( value, "-.@", -1 ); + if( str_array[0] != nullptr ) + { + ret = true; + + lang::Locale aLocale; + + aLocale.Language = OUString::createFromAscii(str_array[0]); + if( str_array[1] != nullptr ) + { + gchar * country = g_ascii_strup(str_array[1], -1); + aLocale.Country = OUString::createFromAscii(country); + g_free(country); + } + + rAny <<= aLocale; + } + + g_strfreev(str_array); + return ret; +} + +/*****************************************************************************/ + +// @see http://www.w3.org/TR/2002/WD-css3-fonts-20020802/#font-effect-prop +static const gchar * relief[] = { "none", "emboss", "engrave" }; +static const gchar * const outline = "outline"; + +static gchar * +get_font_effect(const uno::Sequence< beans::PropertyValue >& rAttributeList, + sal_Int32 nContourIndex, sal_Int32 nReliefIndex) +{ + if( nContourIndex != -1 ) + { + if( rAttributeList[nContourIndex].Value.get<bool>() ) + return g_strdup(outline); + } + + if( nReliefIndex != -1 ) + { + sal_Int16 n = rAttributeList[nReliefIndex].Value.get<sal_Int16>(); + if( n < 3) + return g_strdup(relief[n]); + } + + return nullptr; +} + +/*****************************************************************************/ + +// @see http://www.w3.org/TR/REC-CSS2/text.html#lining-striking-props + +enum +{ + DECORATION_NONE = 0, + DECORATION_BLINK, + DECORATION_UNDERLINE, + DECORATION_LINE_THROUGH +}; + +static const gchar * decorations[] = { "none", "blink", "underline", "line-through" }; + +static gchar * +get_text_decoration(const uno::Sequence< beans::PropertyValue >& rAttributeList, + sal_Int32 nBlinkIndex, sal_Int32 nUnderlineIndex, + sal_Int16 nStrikeoutIndex) +{ + gchar * value_list[4] = { nullptr, nullptr, nullptr, nullptr }; + gint count = 0; + + // no property value found + if( ( nBlinkIndex == -1 ) && (nUnderlineIndex == -1 ) && (nStrikeoutIndex == -1)) + return nullptr; + + if( nBlinkIndex != -1 ) + { + if( rAttributeList[nBlinkIndex].Value.get<bool>() ) + value_list[count++] = const_cast <gchar *> (decorations[DECORATION_BLINK]); + } + if( nUnderlineIndex != -1 ) + { + sal_Int16 n = rAttributeList[nUnderlineIndex].Value.get<sal_Int16> (); + if( n != awt::FontUnderline::NONE ) + value_list[count++] = const_cast <gchar *> (decorations[DECORATION_UNDERLINE]); + } + if( nStrikeoutIndex != -1 ) + { + sal_Int16 n = rAttributeList[nStrikeoutIndex].Value.get<sal_Int16> (); + if( n != awt::FontStrikeout::NONE && n != awt::FontStrikeout::DONTKNOW ) + value_list[count++] = const_cast <gchar *> (decorations[DECORATION_LINE_THROUGH]); + } + + if( count == 0 ) + value_list[count++] = const_cast <gchar *> (decorations[DECORATION_NONE]); + + return g_strjoinv(" ", value_list); +} + +/*****************************************************************************/ + +// @see http://www.w3.org/TR/REC-CSS2/text.html#propdef-text-shadow + +static const gchar * shadow_values[] = { "none", "black" }; + +static gchar * +Bool2Shadow( const uno::Any& rAny ) +{ + int n = 0; + + if( rAny.get<bool>() ) + n = 1; + + return g_strdup( shadow_values[n] ); +} + +/*****************************************************************************/ + +static gchar * +Short2Degree( const uno::Any& rAny ) +{ + float f = rAny.get<sal_Int16>() / 10.0; + return g_strdup_printf( "%g", f ); +} + +/*****************************************************************************/ + +const gchar * const directions[] = { "ltr", "rtl", "rtl", "ltr", "none" }; + +static gchar * +WritingMode2Direction( const uno::Any& rAny ) +{ + sal_Int16 n = rAny.get<sal_Int16>(); + + if( 0 <= n && n <= text::WritingMode2::PAGE ) + return g_strdup(directions[n]); + + return nullptr; +} + +// @see http://www.w3.org/TR/2001/WD-css3-text-20010517/#PrimaryTextAdvanceDirection + +const gchar * const writing_modes[] = { "lr-tb", "rl-tb", "tb-rl", "tb-lr", "none" }; +static gchar * +WritingMode2String( const uno::Any& rAny ) +{ + sal_Int16 n = rAny.get<sal_Int16>(); + + if( 0 <= n && n <= text::WritingMode2::PAGE ) + return g_strdup(writing_modes[n]); + + return nullptr; +} + +/*****************************************************************************/ + +const char * const baseline_values[] = { "baseline", "sub", "super" }; + +// @see http://www.w3.org/TR/REC-CSS2/visudet.html#propdef-vertical-align +static gchar * +Escapement2VerticalAlign( const uno::Any& rAny ) +{ + sal_Int16 n = rAny.get<sal_Int16>(); + gchar * ret = nullptr; + + // Values are in %, 101% means "automatic" + if( n == 0 ) + ret = g_strdup(baseline_values[0]); + else if( n == 101 ) + ret = g_strdup(baseline_values[2]); + else if( n == -101 ) + ret = g_strdup(baseline_values[1]); + else + ret = g_strdup_printf( "%d%%", n ); + + return ret; +} + +/*****************************************************************************/ + +// @see http://www.w3.org/TR/REC-CSS2/visudet.html#propdef-line-height +static gchar * +LineSpacing2LineHeight( const uno::Any& rAny ) +{ + style::LineSpacing ls; + gchar * ret = nullptr; + + if( rAny >>= ls ) + { + if( ls.Mode == style::LineSpacingMode::PROP ) + ret = g_strdup_printf( "%d%%", ls.Height ); + else if( ls.Mode == style::LineSpacingMode::FIX ) + ret = g_strdup_printf( "%.3gpt", toPoint(ls.Height) ); + } + + return ret; +} + +/*****************************************************************************/ + +// @see http://www.w3.org/People/howcome/t/970224HTMLERB-CSS/WD-tabs-970117.html +static gchar * +TabStopList2String( const uno::Any& rAny, bool default_tabs ) +{ + uno::Sequence< style::TabStop > theTabStops; + gchar * ret = nullptr; + + if( rAny >>= theTabStops) + { + sal_Unicode lastFillChar = ' '; + + for( const auto& rTabStop : std::as_const(theTabStops) ) + { + bool is_default_tab = (style::TabAlign_DEFAULT == rTabStop.Alignment); + + if( is_default_tab != default_tabs ) + continue; + + double fValue = rTabStop.Position; + fValue = fValue * 0.01; + + const gchar * tab_align = ""; + switch( rTabStop.Alignment ) + { + case style::TabAlign_LEFT : + tab_align = "left "; + break; + case style::TabAlign_CENTER : + tab_align = "center "; + break; + case style::TabAlign_RIGHT : + tab_align = "right "; + break; + case style::TabAlign_DECIMAL : + tab_align = "decimal "; + break; + default: + break; + } + + const gchar * lead_char = ""; + + if( rTabStop.FillChar != lastFillChar ) + { + lastFillChar = rTabStop.FillChar; + switch (lastFillChar) + { + case ' ': + lead_char = "blank "; + break; + + case '.': + lead_char = "dotted "; + break; + + case '-': + lead_char = "dashed "; + break; + + case '_': + lead_char = "lined "; + break; + + default: + lead_char = "custom "; + break; + } + } + + gchar * tab_str = g_strdup_printf( "%s%s%gmm", lead_char, tab_align, fValue ); + + if( ret ) + { + gchar * old_tab_str = ret; + ret = g_strconcat(old_tab_str, " ", tab_str, nullptr); + g_free( tab_str ); + g_free( old_tab_str ); + } + else + ret = tab_str; + } + } + + return ret; +} + +static gchar * +TabStops2String( const uno::Any& rAny ) +{ + return TabStopList2String(rAny, false); +} + +static gchar * +DefaultTabStops2String( const uno::Any& rAny ) +{ + return TabStopList2String(rAny, true); +} + +/*****************************************************************************/ + +extern "C" { + +static int +attr_compare(const void *p1,const void *p2) +{ + const rtl_uString * pustr = static_cast<const rtl_uString *>(p1); + const char * pc = *static_cast<const char * const *>(p2); + + return rtl_ustr_ascii_compare_WithLength(pustr->buffer, pustr->length, pc); +} + +} + +static void +find_exported_attributes( sal_Int32 *pArray, + const css::uno::Sequence< css::beans::PropertyValue >& rAttributeList ) +{ + for( sal_Int32 i = 0; i < rAttributeList.getLength(); i++ ) + { + const char ** pAttr = static_cast<const char **>(bsearch(rAttributeList[i].Name.pData, + ExportedTextAttributes, TEXT_ATTRIBUTE_LAST, sizeof(const char *), + attr_compare)); + + if( pAttr ) + { + sal_Int32 nIndex = pAttr - ExportedTextAttributes; + pArray[nIndex] = i; + } + } +} + +/*****************************************************************************/ + +static AtkAttributeSet* +attribute_set_prepend( AtkAttributeSet* attribute_set, + AtkTextAttribute attribute, + gchar * value ) +{ + if( value ) + { + AtkAttribute *at = static_cast<AtkAttribute *>(g_malloc( sizeof (AtkAttribute) )); + at->name = g_strdup( atk_text_attribute_get_name( attribute ) ); + at->value = value; + + return g_slist_prepend(attribute_set, at); + } + + return attribute_set; +} + +/*****************************************************************************/ + +AtkAttributeSet* +attribute_set_new_from_property_values( + const uno::Sequence< beans::PropertyValue >& rAttributeList, + bool run_attributes_only, + AtkText *text) +{ + AtkAttributeSet* attribute_set = nullptr; + + sal_Int32 aIndexList[TEXT_ATTRIBUTE_LAST] = { -1 }; + + // Initialize index array with -1 + for(sal_Int32 & rn : aIndexList) + rn = -1; + + find_exported_attributes(aIndexList, rAttributeList); + + attribute_set = attribute_set_prepend(attribute_set, ATK_TEXT_ATTR_BG_COLOR, + get_color_value(rAttributeList, aIndexList, TEXT_ATTRIBUTE_BACKGROUND_COLOR, run_attributes_only ? nullptr : text ) ); + + attribute_set = attribute_set_prepend(attribute_set, ATK_TEXT_ATTR_FG_COLOR, + get_color_value(rAttributeList, aIndexList, TEXT_ATTRIBUTE_FOREGROUND_COLOR, run_attributes_only ? nullptr : text) ); + + attribute_set = attribute_set_prepend(attribute_set, ATK_TEXT_ATTR_INVISIBLE, + get_bool_value(rAttributeList, aIndexList[TEXT_ATTRIBUTE_HIDDEN])); + + attribute_set = attribute_set_prepend(attribute_set, ATK_TEXT_ATTR_UNDERLINE, + get_underline_value(rAttributeList, aIndexList[TEXT_ATTRIBUTE_UNDERLINE])); + + attribute_set = attribute_set_prepend(attribute_set, ATK_TEXT_ATTR_STRIKETHROUGH, + get_strikethrough_value(rAttributeList, aIndexList[TEXT_ATTRIBUTE_STRIKETHROUGH])); + + attribute_set = attribute_set_prepend(attribute_set, ATK_TEXT_ATTR_SIZE, + get_height_value(rAttributeList, aIndexList[TEXT_ATTRIBUTE_HEIGHT])); + + attribute_set = attribute_set_prepend(attribute_set, ATK_TEXT_ATTR_WEIGHT, + get_weight_value(rAttributeList, aIndexList[TEXT_ATTRIBUTE_WEIGHT])); + + attribute_set = attribute_set_prepend(attribute_set, ATK_TEXT_ATTR_FAMILY_NAME, + get_string_value(rAttributeList, aIndexList[TEXT_ATTRIBUTE_FONT_NAME])); + + attribute_set = attribute_set_prepend(attribute_set, ATK_TEXT_ATTR_VARIANT, + get_variant_value(rAttributeList, aIndexList[TEXT_ATTRIBUTE_CASEMAP])); + + attribute_set = attribute_set_prepend(attribute_set, ATK_TEXT_ATTR_STYLE, + get_style_value(rAttributeList, aIndexList[TEXT_ATTRIBUTE_POSTURE])); + + attribute_set = attribute_set_prepend(attribute_set, ATK_TEXT_ATTR_SCALE, + get_scale_width(rAttributeList, aIndexList[TEXT_ATTRIBUTE_SCALE])); + + attribute_set = attribute_set_prepend(attribute_set, ATK_TEXT_ATTR_LANGUAGE, + get_language_string(rAttributeList, aIndexList[TEXT_ATTRIBUTE_LOCALE])); + + attribute_set = attribute_set_prepend(attribute_set, ATK_TEXT_ATTR_DIRECTION, + get_value(rAttributeList, aIndexList[TEXT_ATTRIBUTE_WRITING_MODE], WritingMode2Direction)); + + attribute_set = attribute_set_prepend(attribute_set, ATK_TEXT_ATTR_STRETCH, + get_value(rAttributeList, aIndexList[TEXT_ATTRIBUTE_KERNING], Kerning2Stretch)); + + if( ATK_TEXT_ATTR_INVALID == atk_text_attribute_font_effect ) + atk_text_attribute_font_effect = atk_text_attribute_register("font-effect"); + + attribute_set = attribute_set_prepend(attribute_set, atk_text_attribute_font_effect, + get_font_effect(rAttributeList, aIndexList[TEXT_ATTRIBUTE_CONTOURED], aIndexList[TEXT_ATTRIBUTE_RELIEF])); + + if( ATK_TEXT_ATTR_INVALID == atk_text_attribute_decoration ) + atk_text_attribute_decoration = atk_text_attribute_register("text-decoration"); + + attribute_set = attribute_set_prepend(attribute_set, atk_text_attribute_decoration, + get_text_decoration(rAttributeList, aIndexList[TEXT_ATTRIBUTE_BLINKING], + aIndexList[TEXT_ATTRIBUTE_UNDERLINE], aIndexList[TEXT_ATTRIBUTE_STRIKETHROUGH])); + + if( ATK_TEXT_ATTR_INVALID == atk_text_attribute_rotation ) + atk_text_attribute_rotation = atk_text_attribute_register("text-rotation"); + + attribute_set = attribute_set_prepend(attribute_set, atk_text_attribute_rotation, + get_value(rAttributeList, aIndexList[TEXT_ATTRIBUTE_ROTATION], Short2Degree)); + + if( ATK_TEXT_ATTR_INVALID == atk_text_attribute_shadow ) + atk_text_attribute_shadow = atk_text_attribute_register("text-shadow"); + + attribute_set = attribute_set_prepend(attribute_set, atk_text_attribute_shadow, + get_value(rAttributeList, aIndexList[TEXT_ATTRIBUTE_SHADOWED], Bool2Shadow)); + + if( ATK_TEXT_ATTR_INVALID == atk_text_attribute_writing_mode ) + atk_text_attribute_writing_mode = atk_text_attribute_register("writing-mode"); + + attribute_set = attribute_set_prepend(attribute_set, atk_text_attribute_writing_mode, + get_value(rAttributeList, aIndexList[TEXT_ATTRIBUTE_WRITING_MODE], WritingMode2String)); + + if( ATK_TEXT_ATTR_INVALID == atk_text_attribute_vertical_align ) + atk_text_attribute_vertical_align = atk_text_attribute_register("vertical-align"); + + attribute_set = attribute_set_prepend(attribute_set, atk_text_attribute_vertical_align, + get_value(rAttributeList, aIndexList[TEXT_ATTRIBUTE_CHAR_ESCAPEMENT], Escapement2VerticalAlign)); + + if( run_attributes_only ) + return attribute_set; + + attribute_set = attribute_set_prepend(attribute_set, ATK_TEXT_ATTR_LEFT_MARGIN, + get_cmm_value(rAttributeList, aIndexList[TEXT_ATTRIBUTE_LEFT_MARGIN])); + + attribute_set = attribute_set_prepend(attribute_set, ATK_TEXT_ATTR_RIGHT_MARGIN, + get_cmm_value(rAttributeList, aIndexList[TEXT_ATTRIBUTE_RIGHT_MARGIN])); + + attribute_set = attribute_set_prepend(attribute_set, ATK_TEXT_ATTR_INDENT, + get_cmm_value(rAttributeList, aIndexList[TEXT_ATTRIBUTE_FIRST_LINE_INDENT])); + + attribute_set = attribute_set_prepend(attribute_set, ATK_TEXT_ATTR_PIXELS_ABOVE_LINES, + get_cmm_value(rAttributeList, aIndexList[TEXT_ATTRIBUTE_TOP_MARGIN])); + + attribute_set = attribute_set_prepend(attribute_set, ATK_TEXT_ATTR_PIXELS_BELOW_LINES, + get_cmm_value(rAttributeList, aIndexList[TEXT_ATTRIBUTE_BOTTOM_MARGIN])); + + attribute_set = attribute_set_prepend(attribute_set, ATK_TEXT_ATTR_JUSTIFICATION, + get_justification_value(rAttributeList, aIndexList[TEXT_ATTRIBUTE_JUSTIFICATION])); + + if( ATK_TEXT_ATTR_INVALID == atk_text_attribute_paragraph_style ) + atk_text_attribute_paragraph_style = atk_text_attribute_register("paragraph-style"); + + attribute_set = attribute_set_prepend(attribute_set, atk_text_attribute_paragraph_style, + get_string_value(rAttributeList, aIndexList[TEXT_ATTRIBUTE_STYLE_NAME])); + + if( ATK_TEXT_ATTR_INVALID == atk_text_attribute_line_height ) + atk_text_attribute_line_height = atk_text_attribute_register("line-height"); + + attribute_set = attribute_set_prepend(attribute_set, atk_text_attribute_line_height, + get_value(rAttributeList, aIndexList[TEXT_ATTRIBUTE_LINE_SPACING], LineSpacing2LineHeight)); + + if( ATK_TEXT_ATTR_INVALID == atk_text_attribute_tab_interval ) + atk_text_attribute_tab_interval = atk_text_attribute_register("tab-interval"); + + attribute_set = attribute_set_prepend(attribute_set, atk_text_attribute_tab_interval, + get_value(rAttributeList, aIndexList[TEXT_ATTRIBUTE_TAB_STOPS], DefaultTabStops2String)); + + if( ATK_TEXT_ATTR_INVALID == atk_text_attribute_tab_stops ) + atk_text_attribute_tab_stops = atk_text_attribute_register("tab-stops"); + + attribute_set = attribute_set_prepend(attribute_set, atk_text_attribute_tab_stops, + get_value(rAttributeList, aIndexList[TEXT_ATTRIBUTE_TAB_STOPS], TabStops2String)); + + // #i92233# + if( ATK_TEXT_ATTR_INVALID == atk_text_attribute_mm_to_pixel_ratio ) + atk_text_attribute_mm_to_pixel_ratio = atk_text_attribute_register("mm-to-pixel-ratio"); + + attribute_set = attribute_set_prepend( attribute_set, atk_text_attribute_mm_to_pixel_ratio, + get_value(rAttributeList, aIndexList[TEXT_ATTRIBUTE_MM_TO_PIXEL_RATIO], Float2String)); + + return attribute_set; +} + +AtkAttributeSet* +attribute_set_new_from_extended_attributes( + const css::uno::Reference< css::accessibility::XAccessibleExtendedAttributes >& rExtendedAttributes ) +{ + AtkAttributeSet *pSet = nullptr; + + // extended attributes is a string of colon-separated pairs of property and value, + // with pairs separated by semicolons. Example: "heading-level:2;weight:bold;" + uno::Any anyVal = rExtendedAttributes->getExtendedAttributes(); + OUString sExtendedAttrs; + anyVal >>= sExtendedAttrs; + sal_Int32 nIndex = 0; + do + { + OUString sProperty = sExtendedAttrs.getToken( 0, ';', nIndex ); + + sal_Int32 nColonPos = 0; + OString sPropertyName = OUStringToOString( sProperty.getToken( 0, ':', nColonPos ), + RTL_TEXTENCODING_UTF8 ); + OString sPropertyValue = OUStringToOString( sProperty.getToken( 0, ':', nColonPos ), + RTL_TEXTENCODING_UTF8 ); + + pSet = attribute_set_prepend( pSet, + atk_text_attribute_register( sPropertyName.getStr() ), + g_strdup_printf( "%s", sPropertyValue.getStr() ) ); + } + while ( nIndex >= 0 && nIndex < sExtendedAttrs.getLength() ); + + return pSet; +} + +AtkAttributeSet* attribute_set_prepend_misspelled( AtkAttributeSet* attribute_set ) +{ + if( ATK_TEXT_ATTR_INVALID == atk_text_attribute_misspelled ) + atk_text_attribute_misspelled = atk_text_attribute_register( "text-spelling" ); + + attribute_set = attribute_set_prepend( attribute_set, atk_text_attribute_misspelled, + g_strdup_printf( "misspelled" ) ); + + return attribute_set; +} + +// #i92232# +AtkAttributeSet* attribute_set_prepend_tracked_change_insertion( AtkAttributeSet* attribute_set ) +{ + if ( ATK_TEXT_ATTR_INVALID == atk_text_attribute_tracked_change ) + { + atk_text_attribute_tracked_change = atk_text_attribute_register( "text-tracked-change" ); + } + + attribute_set = attribute_set_prepend( attribute_set, + atk_text_attribute_tracked_change, + g_strdup_printf( "insertion" ) ); + + return attribute_set; +} + +AtkAttributeSet* attribute_set_prepend_tracked_change_deletion( AtkAttributeSet* attribute_set ) +{ + if ( ATK_TEXT_ATTR_INVALID == atk_text_attribute_tracked_change ) + { + atk_text_attribute_tracked_change = atk_text_attribute_register( "text-tracked-change" ); + } + + attribute_set = attribute_set_prepend( attribute_set, + atk_text_attribute_tracked_change, + g_strdup_printf( "deletion" ) ); + + return attribute_set; +} + +AtkAttributeSet* attribute_set_prepend_tracked_change_formatchange( AtkAttributeSet* attribute_set ) +{ + if ( ATK_TEXT_ATTR_INVALID == atk_text_attribute_tracked_change ) + { + atk_text_attribute_tracked_change = atk_text_attribute_register( "text-tracked-change" ); + } + + attribute_set = attribute_set_prepend( attribute_set, + atk_text_attribute_tracked_change, + g_strdup_printf( "attribute-change" ) ); + + return attribute_set; +} + +/*****************************************************************************/ + +namespace { + +struct AtkTextAttrMapping +{ + const char * name; + TextPropertyValueFunc const toPropertyValue; +}; + +} + +const AtkTextAttrMapping g_TextAttrMap[] = +{ + { "", InvalidValue }, // ATK_TEXT_ATTR_INVALID = 0 + { "ParaLeftMargin", UnitString2CMM }, // ATK_TEXT_ATTR_LEFT_MARGIN + { "ParaRightMargin", UnitString2CMM }, // ATK_TEXT_ATTR_RIGHT_MARGIN + { "ParaFirstLineIndent", UnitString2CMM }, // ATK_TEXT_ATTR_INDENT + { "CharHidden", String2Bool }, // ATK_TEXT_ATTR_INVISIBLE + { "", InvalidValue }, // ATK_TEXT_ATTR_EDITABLE + { "ParaTopMargin", UnitString2CMM }, // ATK_TEXT_ATTR_PIXELS_ABOVE_LINES + { "ParaBottomMargin", UnitString2CMM }, // ATK_TEXT_ATTR_PIXELS_BELOW_LINES + { "", InvalidValue }, // ATK_TEXT_ATTR_PIXELS_INSIDE_WRAP + { "", InvalidValue }, // ATK_TEXT_ATTR_BG_FULL_HEIGHT + { "", InvalidValue }, // ATK_TEXT_ATTR_RISE + { "CharUnderline", String2Underline }, // ATK_TEXT_ATTR_UNDERLINE + { "CharStrikeout", String2Strikeout }, // ATK_TEXT_ATTR_STRIKETHROUGH + { "CharHeight", String2Float }, // ATK_TEXT_ATTR_SIZE + { "CharScaleWidth", String2Scale }, // ATK_TEXT_ATTR_SCALE + { "CharWeight", String2Weight }, // ATK_TEXT_ATTR_WEIGHT + { "CharLocale", String2Locale }, // ATK_TEXT_ATTR_LANGUAGE + { "CharFontName", SetString }, // ATK_TEXT_ATTR_FAMILY_NAME + { "CharBackColor", String2Color }, // ATK_TEXT_ATTR_BG_COLOR + { "CharColor", String2Color }, // ATK_TEXT_ATTR_FG_COLOR + { "", InvalidValue }, // ATK_TEXT_ATTR_BG_STIPPLE + { "", InvalidValue }, // ATK_TEXT_ATTR_FG_STIPPLE + { "", InvalidValue }, // ATK_TEXT_ATTR_WRAP_MODE + { "", InvalidValue }, // ATK_TEXT_ATTR_DIRECTION + { "ParaAdjust", Justification2Adjust }, // ATK_TEXT_ATTR_JUSTIFICATION + { "", InvalidValue }, // ATK_TEXT_ATTR_STRETCH + { "CharCaseMap", String2CaseMap }, // ATK_TEXT_ATTR_VARIANT + { "CharPosture", Style2FontSlant } // ATK_TEXT_ATTR_STYLE +}; + +/*****************************************************************************/ + +bool +attribute_set_map_to_property_values( + AtkAttributeSet* attribute_set, + uno::Sequence< beans::PropertyValue >& rValueList ) +{ + // Ensure enough space .. + uno::Sequence< beans::PropertyValue > aAttributeList (SAL_N_ELEMENTS(g_TextAttrMap)); + + sal_Int32 nIndex = 0; + for( GSList * item = attribute_set; item != nullptr; item = g_slist_next( item ) ) + { + AtkAttribute* attribute = reinterpret_cast<AtkAttribute *>(item); + + AtkTextAttribute text_attr = atk_text_attribute_for_name( attribute->name ); + if( text_attr < SAL_N_ELEMENTS(g_TextAttrMap) ) + { + if( g_TextAttrMap[text_attr].name[0] != '\0' ) + { + if( ! g_TextAttrMap[text_attr].toPropertyValue( aAttributeList[nIndex].Value, attribute->value) ) + return false; + + aAttributeList[nIndex].Name = OUString::createFromAscii( g_TextAttrMap[text_attr].name ); + aAttributeList[nIndex].State = beans::PropertyState_DIRECT_VALUE; + ++nIndex; + } + } + else + { + // Unsupported text attribute + return false; + } + } + + aAttributeList.realloc( nIndex ); + rValueList = aAttributeList; + return true; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/a11y/gtk3atkutil.cxx b/vcl/unx/gtk3/a11y/gtk3atkutil.cxx new file mode 100644 index 000000000..1d1337d6e --- /dev/null +++ b/vcl/unx/gtk3/a11y/gtk3atkutil.cxx @@ -0,0 +1,708 @@ +/* -*- 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#ifdef AIX +#define _LINUX_SOURCE_COMPAT +#include <sys/timer.h> +#undef _LINUX_SOURCE_COMPAT +#endif + +#include <com/sun/star/accessibility/XAccessibleContext.hpp> +#include <com/sun/star/accessibility/XAccessibleEventBroadcaster.hpp> +#include <com/sun/star/accessibility/XAccessibleSelection.hpp> +#include <com/sun/star/accessibility/AccessibleEventId.hpp> +#include <com/sun/star/accessibility/AccessibleStateType.hpp> +#include <com/sun/star/accessibility/XAccessibleText.hpp> +#include <com/sun/star/lang/IndexOutOfBoundsException.hpp> +#include <cppuhelper/implbase.hxx> +#include <cppuhelper/weakref.hxx> +#include <sal/log.hxx> + +#include <vcl/svapp.hxx> +#include <vcl/window.hxx> +#include <vcl/menu.hxx> +#include <vcl/toolbox.hxx> + +#include <unx/gtk/gtkdata.hxx> +#include "atkwrapper.hxx" +#include "atkutil.hxx" + +#include <cassert> +#include <set> + +using namespace ::com::sun::star; + +namespace +{ + struct theNextFocusObject : + public rtl::Static< uno::WeakReference< accessibility::XAccessible >, theNextFocusObject> + { + }; +} + +static guint focus_notify_handler = 0; + +/*****************************************************************************/ + +extern "C" { + +static gboolean +atk_wrapper_focus_idle_handler (gpointer data) +{ + SolarMutexGuard aGuard; + + focus_notify_handler = 0; + + uno::Reference< accessibility::XAccessible > xAccessible = theNextFocusObject::get(); + if( xAccessible.get() == static_cast < accessibility::XAccessible * > (data) ) + { + AtkObject *atk_obj = xAccessible.is() ? atk_object_wrapper_ref( xAccessible ) : nullptr; + // Gail does not notify focus changes to NULL, so do we .. + if( atk_obj ) + { + SAL_WNODEPRECATED_DECLARATIONS_PUSH + atk_focus_tracker_notify(atk_obj); + SAL_WNODEPRECATED_DECLARATIONS_POP + // #i93269# + // emit text_caret_moved event for <XAccessibleText> object, + // if cursor is inside the <XAccessibleText> object. + // also emit state-changed:focused event under the same condition. + { + AtkObjectWrapper* wrapper_obj = ATK_OBJECT_WRAPPER (atk_obj); + if( wrapper_obj && !wrapper_obj->mpText.is() ) + { + wrapper_obj->mpText.set(wrapper_obj->mpContext, css::uno::UNO_QUERY); + if ( wrapper_obj->mpText.is() ) + { + gint caretPos = -1; + + try { + caretPos = wrapper_obj->mpText->getCaretPosition(); + } + catch(const uno::Exception&) { + g_warning( "Exception in getCaretPosition()" ); + } + + if ( caretPos != -1 ) + { + atk_object_notify_state_change( atk_obj, ATK_STATE_FOCUSED, true ); + g_signal_emit_by_name( atk_obj, "text_caret_moved", caretPos ); + } + } + } + } + g_object_unref(atk_obj); + } + } + + return false; +} + +} // extern "C" + +/*****************************************************************************/ + +static void +atk_wrapper_focus_tracker_notify_when_idle( const uno::Reference< accessibility::XAccessible > &xAccessible ) +{ + if( focus_notify_handler ) + g_source_remove(focus_notify_handler); + + theNextFocusObject::get() = xAccessible; + + focus_notify_handler = g_idle_add (atk_wrapper_focus_idle_handler, xAccessible.get()); +} + +/*****************************************************************************/ + +class DocumentFocusListener : + public ::cppu::WeakImplHelper< accessibility::XAccessibleEventListener > +{ + + std::set< uno::Reference< uno::XInterface > > m_aRefList; + +public: + /// @throws lang::IndexOutOfBoundsException + /// @throws uno::RuntimeException + void attachRecursive( + const uno::Reference< accessibility::XAccessible >& xAccessible + ); + + /// @throws lang::IndexOutOfBoundsException + /// @throws uno::RuntimeException + void attachRecursive( + const uno::Reference< accessibility::XAccessible >& xAccessible, + const uno::Reference< accessibility::XAccessibleContext >& xContext + ); + + /// @throws lang::IndexOutOfBoundsException + /// @throws uno::RuntimeException + void attachRecursive( + const uno::Reference< accessibility::XAccessible >& xAccessible, + const uno::Reference< accessibility::XAccessibleContext >& xContext, + const uno::Reference< accessibility::XAccessibleStateSet >& xStateSet + ); + + /// @throws lang::IndexOutOfBoundsException + /// @throws uno::RuntimeException + void detachRecursive( + const uno::Reference< accessibility::XAccessible >& xAccessible + ); + + /// @throws lang::IndexOutOfBoundsException + /// @throws uno::RuntimeException + void detachRecursive( + const uno::Reference< accessibility::XAccessibleContext >& xContext + ); + + /// @throws lang::IndexOutOfBoundsException + /// @throws uno::RuntimeException + void detachRecursive( + const uno::Reference< accessibility::XAccessibleContext >& xContext, + const uno::Reference< accessibility::XAccessibleStateSet >& xStateSet + ); + + /// @throws lang::IndexOutOfBoundsException + /// @throws uno::RuntimeException + static uno::Reference< accessibility::XAccessible > getAccessible(const lang::EventObject& aEvent ); + + // XEventListener + virtual void SAL_CALL disposing( const lang::EventObject& Source ) override; + + // XAccessibleEventListener + virtual void SAL_CALL notifyEvent( const accessibility::AccessibleEventObject& aEvent ) override; +}; + +/*****************************************************************************/ + +void DocumentFocusListener::disposing( const lang::EventObject& aEvent ) +{ + + // Unref the object here, but do not remove as listener since the object + // might no longer be in a state that safely allows this. + if( aEvent.Source.is() ) + m_aRefList.erase(aEvent.Source); + +} + +/*****************************************************************************/ + +void DocumentFocusListener::notifyEvent( const accessibility::AccessibleEventObject& aEvent ) +{ + try { + switch( aEvent.EventId ) + { + case accessibility::AccessibleEventId::STATE_CHANGED: + { + sal_Int16 nState = accessibility::AccessibleStateType::INVALID; + aEvent.NewValue >>= nState; + + if( accessibility::AccessibleStateType::FOCUSED == nState ) + atk_wrapper_focus_tracker_notify_when_idle( getAccessible(aEvent) ); + + break; + } + + case accessibility::AccessibleEventId::CHILD: + { + uno::Reference< accessibility::XAccessible > xChild; + if( (aEvent.OldValue >>= xChild) && xChild.is() ) + detachRecursive(xChild); + + if( (aEvent.NewValue >>= xChild) && xChild.is() ) + attachRecursive(xChild); + + break; + } + + case accessibility::AccessibleEventId::INVALIDATE_ALL_CHILDREN: + SAL_INFO("vcl.a11y", "Invalidate all children called"); + break; + + default: + break; + } + } + catch( const lang::IndexOutOfBoundsException& ) + { + g_warning("Focused object has invalid index in parent"); + } +} + +/*****************************************************************************/ + +uno::Reference< accessibility::XAccessible > DocumentFocusListener::getAccessible(const lang::EventObject& aEvent ) +{ + uno::Reference< accessibility::XAccessible > xAccessible(aEvent.Source, uno::UNO_QUERY); + + if( xAccessible.is() ) + return xAccessible; + + uno::Reference< accessibility::XAccessibleContext > xContext(aEvent.Source, uno::UNO_QUERY); + + if( xContext.is() ) + { + uno::Reference< accessibility::XAccessible > xParent( xContext->getAccessibleParent() ); + if( xParent.is() ) + { + uno::Reference< accessibility::XAccessibleContext > xParentContext( xParent->getAccessibleContext() ); + if( xParentContext.is() ) + { + return xParentContext->getAccessibleChild( xContext->getAccessibleIndexInParent() ); + } + } + } + + return uno::Reference< accessibility::XAccessible >(); +} + +/*****************************************************************************/ + +void DocumentFocusListener::attachRecursive( + const uno::Reference< accessibility::XAccessible >& xAccessible +) +{ + uno::Reference< accessibility::XAccessibleContext > xContext = + xAccessible->getAccessibleContext(); + + if( xContext.is() ) + attachRecursive(xAccessible, xContext); +} + +/*****************************************************************************/ + +void DocumentFocusListener::attachRecursive( + const uno::Reference< accessibility::XAccessible >& xAccessible, + const uno::Reference< accessibility::XAccessibleContext >& xContext +) +{ + uno::Reference< accessibility::XAccessibleStateSet > xStateSet = + xContext->getAccessibleStateSet(); + + if( xStateSet.is() ) + attachRecursive(xAccessible, xContext, xStateSet); +} + +/*****************************************************************************/ + +void DocumentFocusListener::attachRecursive( + const uno::Reference< accessibility::XAccessible >& xAccessible, + const uno::Reference< accessibility::XAccessibleContext >& xContext, + const uno::Reference< accessibility::XAccessibleStateSet >& xStateSet +) +{ + if( xStateSet->contains(accessibility::AccessibleStateType::FOCUSED ) ) + atk_wrapper_focus_tracker_notify_when_idle( xAccessible ); + + uno::Reference< accessibility::XAccessibleEventBroadcaster > xBroadcaster(xContext, uno::UNO_QUERY); + + if (!xBroadcaster.is()) + return; + + // If not already done, add the broadcaster to the list and attach as listener. + const uno::Reference< uno::XInterface >& xInterface = xBroadcaster; + if( m_aRefList.insert(xInterface).second ) + { + xBroadcaster->addAccessibleEventListener(static_cast< accessibility::XAccessibleEventListener *>(this)); + + if( ! xStateSet->contains(accessibility::AccessibleStateType::MANAGES_DESCENDANTS ) ) + { + sal_Int32 n, nmax = xContext->getAccessibleChildCount(); + for( n = 0; n < nmax; n++ ) + { + uno::Reference< accessibility::XAccessible > xChild( xContext->getAccessibleChild( n ) ); + + if( xChild.is() ) + attachRecursive(xChild); + } + } + } +} + +/*****************************************************************************/ + +void DocumentFocusListener::detachRecursive( + const uno::Reference< accessibility::XAccessible >& xAccessible +) +{ + uno::Reference< accessibility::XAccessibleContext > xContext = + xAccessible->getAccessibleContext(); + + if( xContext.is() ) + detachRecursive(xContext); +} + +/*****************************************************************************/ + +void DocumentFocusListener::detachRecursive( + const uno::Reference< accessibility::XAccessibleContext >& xContext +) +{ + uno::Reference< accessibility::XAccessibleStateSet > xStateSet = + xContext->getAccessibleStateSet(); + + if( xStateSet.is() ) + detachRecursive(xContext, xStateSet); +} + +/*****************************************************************************/ + +void DocumentFocusListener::detachRecursive( + const uno::Reference< accessibility::XAccessibleContext >& xContext, + const uno::Reference< accessibility::XAccessibleStateSet >& xStateSet +) +{ + uno::Reference< accessibility::XAccessibleEventBroadcaster > xBroadcaster(xContext, uno::UNO_QUERY); + + if( xBroadcaster.is() && 0 < m_aRefList.erase(xBroadcaster) ) + { + xBroadcaster->removeAccessibleEventListener(static_cast< accessibility::XAccessibleEventListener *>(this)); + + if( ! xStateSet->contains(accessibility::AccessibleStateType::MANAGES_DESCENDANTS ) ) + { + sal_Int32 n, nmax = xContext->getAccessibleChildCount(); + for( n = 0; n < nmax; n++ ) + { + uno::Reference< accessibility::XAccessible > xChild( xContext->getAccessibleChild( n ) ); + + if( xChild.is() ) + detachRecursive(xChild); + } + } + } +} + +/*****************************************************************************/ + +/* + * page tabs in gtk are widgets, so we need to simulate focus events for those + */ + +static void handle_tabpage_activated(vcl::Window *pWindow) +{ + uno::Reference< accessibility::XAccessible > xAccessible = + pWindow->GetAccessible(); + + if( ! xAccessible.is() ) + return; + + uno::Reference< accessibility::XAccessibleSelection > xSelection( + xAccessible->getAccessibleContext(), uno::UNO_QUERY); + + if( xSelection.is() ) + atk_wrapper_focus_tracker_notify_when_idle( xSelection->getSelectedAccessibleChild(0) ); +} + +/*****************************************************************************/ + +/* + * toolbar items in gtk are widgets, so we need to simulate focus events for those + */ + +static void notify_toolbox_item_focus(ToolBox *pToolBox) +{ + uno::Reference< accessibility::XAccessible > xAccessible = + pToolBox->GetAccessible(); + + if( ! xAccessible.is() ) + return; + + uno::Reference< accessibility::XAccessibleContext > xContext = + xAccessible->getAccessibleContext(); + + if( ! xContext.is() ) + return; + + ToolBox::ImplToolItems::size_type nPos = pToolBox->GetItemPos( pToolBox->GetHighlightItemId() ); + if( nPos != ToolBox::ITEM_NOTFOUND ) + atk_wrapper_focus_tracker_notify_when_idle( xContext->getAccessibleChild( nPos ) ); + //TODO: ToolBox::ImplToolItems::size_type -> sal_Int32 +} + +static void handle_toolbox_highlight(vcl::Window *pWindow) +{ + ToolBox *pToolBox = static_cast <ToolBox *> (pWindow); + + // Make sure either the toolbox or its parent toolbox has the focus + if ( ! pToolBox->HasFocus() ) + { + ToolBox* pToolBoxParent = dynamic_cast< ToolBox* >( pToolBox->GetParent() ); + if ( ! pToolBoxParent || ! pToolBoxParent->HasFocus() ) + return; + } + + notify_toolbox_item_focus(pToolBox); +} + +static void handle_toolbox_highlightoff(vcl::Window const *pWindow) +{ + ToolBox* pToolBoxParent = dynamic_cast< ToolBox* >( pWindow->GetParent() ); + + // Notify when leaving sub toolboxes + if( pToolBoxParent && pToolBoxParent->HasFocus() ) + notify_toolbox_item_focus( pToolBoxParent ); +} + +/*****************************************************************************/ + +static void create_wrapper_for_child( + const uno::Reference< accessibility::XAccessibleContext >& xContext, + sal_Int32 index) +{ + if( xContext.is() ) + { + uno::Reference< accessibility::XAccessible > xChild(xContext->getAccessibleChild(index)); + if( xChild.is() ) + { + // create the wrapper object - it will survive the unref unless it is a transient object + g_object_unref( atk_object_wrapper_ref( xChild ) ); + } + } +} + +/*****************************************************************************/ + +static void handle_toolbox_buttonchange(VclWindowEvent const *pEvent) +{ + vcl::Window* pWindow = pEvent->GetWindow(); + sal_Int32 index = static_cast<sal_Int32>(reinterpret_cast<sal_IntPtr>(pEvent->GetData())); + + if( pWindow && pWindow->IsReallyVisible() ) + { + uno::Reference< accessibility::XAccessible > xAccessible(pWindow->GetAccessible()); + if( xAccessible.is() ) + { + create_wrapper_for_child(xAccessible->getAccessibleContext(), index); + } + } +} + +/*****************************************************************************/ + +namespace { + +struct WindowList { + ~WindowList() { assert(list.empty()); }; + // needs to be empty already on DeInitVCL, but at least check it's empty + // on exit + + std::set< VclPtr<vcl::Window> > list; +}; + +WindowList g_aWindowList; + +} + +DocumentFocusListener & GtkSalData::GetDocumentFocusListener() +{ + if (!m_pDocumentFocusListener) + { + m_pDocumentFocusListener = new DocumentFocusListener; + m_xDocumentFocusListener.set(m_pDocumentFocusListener); + } + return *m_pDocumentFocusListener; +} + +static void handle_get_focus(::VclWindowEvent const * pEvent) +{ + GtkSalData *const pSalData(GetGtkSalData()); + assert(pSalData); + + DocumentFocusListener & rDocumentFocusListener(pSalData->GetDocumentFocusListener()); + + vcl::Window *pWindow = pEvent->GetWindow(); + + // The menu bar is handled through VclEventId::MenuHighlightED + if( ! pWindow || !pWindow->IsReallyVisible() || pWindow->GetType() == WindowType::MENUBARWINDOW ) + return; + + // ToolBoxes are handled through VclEventId::ToolboxHighlight + if( pWindow->GetType() == WindowType::TOOLBOX ) + return; + + if( pWindow->GetType() == WindowType::TABCONTROL ) + { + handle_tabpage_activated( pWindow ); + return; + } + + uno::Reference< accessibility::XAccessible > xAccessible = + pWindow->GetAccessible(); + + if( ! xAccessible.is() ) + return; + + uno::Reference< accessibility::XAccessibleContext > xContext = + xAccessible->getAccessibleContext(); + + if( ! xContext.is() ) + return; + + uno::Reference< accessibility::XAccessibleStateSet > xStateSet = + xContext->getAccessibleStateSet(); + + if( ! xStateSet.is() ) + return; + +/* the UNO ToolBox wrapper does not (yet?) support XAccessibleSelection, so we + * need to add listeners to the children instead of re-using the tabpage stuff + */ + if( xStateSet->contains(accessibility::AccessibleStateType::FOCUSED) && + ( pWindow->GetType() != WindowType::TREELISTBOX ) ) + { + atk_wrapper_focus_tracker_notify_when_idle( xAccessible ); + } + else + { + if( g_aWindowList.list.insert(pWindow).second ) + { + try + { + rDocumentFocusListener.attachRecursive(xAccessible, xContext, xStateSet); + } + catch (const uno::Exception&) + { + g_warning( "Exception caught processing focus events" ); + } + } + } +} + +/*****************************************************************************/ + +static void handle_menu_highlighted(::VclMenuEvent const * pEvent) +{ + try + { + Menu* pMenu = pEvent->GetMenu(); + sal_uInt16 nPos = pEvent->GetItemPos(); + + if( pMenu && nPos != 0xFFFF) + { + uno::Reference< accessibility::XAccessible > xAccessible ( pMenu->GetAccessible() ); + + if( xAccessible.is() ) + { + uno::Reference< accessibility::XAccessibleContext > xContext ( xAccessible->getAccessibleContext() ); + + if( xContext.is() ) + atk_wrapper_focus_tracker_notify_when_idle( xContext->getAccessibleChild( nPos ) ); + } + } + } + catch (const uno::Exception&) + { + g_warning( "Exception caught processing menu highlight events" ); + } +} + +/*****************************************************************************/ + +static void WindowEventHandler(void *, VclSimpleEvent& rEvent) +{ + try + { + switch (rEvent.GetId()) + { + case VclEventId::WindowShow: + break; + case VclEventId::WindowHide: + break; + case VclEventId::WindowClose: + break; + case VclEventId::WindowGetFocus: + handle_get_focus(static_cast< ::VclWindowEvent const * >(&rEvent)); + break; + case VclEventId::WindowLoseFocus: + break; + case VclEventId::WindowMinimize: + break; + case VclEventId::WindowNormalize: + break; + case VclEventId::WindowKeyInput: + case VclEventId::WindowKeyUp: + case VclEventId::WindowCommand: + case VclEventId::WindowMouseMove: + break; + + case VclEventId::MenuHighlight: + if (const VclMenuEvent* pMenuEvent = dynamic_cast<const VclMenuEvent*>(&rEvent)) + { + handle_menu_highlighted(pMenuEvent); + } + else if (const VclAccessibleEvent* pAccEvent = dynamic_cast<const VclAccessibleEvent*>(&rEvent)) + { + const uno::Reference< accessibility::XAccessible >& xAccessible = pAccEvent->GetAccessible(); + if (xAccessible.is()) + atk_wrapper_focus_tracker_notify_when_idle(xAccessible); + } + break; + + case VclEventId::ToolboxHighlight: + handle_toolbox_highlight(static_cast< ::VclWindowEvent const * >(&rEvent)->GetWindow()); + break; + + case VclEventId::ToolboxButtonStateChanged: + handle_toolbox_buttonchange(static_cast< ::VclWindowEvent const * >(&rEvent)); + break; + + case VclEventId::ObjectDying: + g_aWindowList.list.erase( static_cast< ::VclWindowEvent const * >(&rEvent)->GetWindow() ); + [[fallthrough]]; + case VclEventId::ToolboxHighlightOff: + handle_toolbox_highlightoff(static_cast< ::VclWindowEvent const * >(&rEvent)->GetWindow()); + break; + + case VclEventId::TabpageActivate: + handle_tabpage_activated(static_cast< ::VclWindowEvent const * >(&rEvent)->GetWindow()); + break; + + case VclEventId::ComboboxSetText: + // This looks quite strange to me. Stumbled over this when fixing #i104290#. + // This kicked in when leaving the combobox in the toolbar, after that the events worked. + // I guess this was a try to work around missing combobox events, which didn't do the full job, and shouldn't be necessary anymore. + // Fix for #i104290# was done in toolkit/source/awt/vclxaccessiblecomponent, FOCUSED state for compound controls in general. + // create_wrapper_for_children(static_cast< ::VclWindowEvent const * >(pEvent)->GetWindow()); + break; + + default: + break; + } + } + catch (const lang::IndexOutOfBoundsException&) + { + g_warning("Focused object has invalid index in parent"); + } +} + +static Link<VclSimpleEvent&,void> g_aEventListenerLink( nullptr, WindowEventHandler ); + +/*****************************************************************************/ + +void ooo_atk_util_ensure_event_listener() +{ + static bool bInited; + if (!bInited) + { + Application::AddEventListener( g_aEventListenerLink ); + bInited = true; + } +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/a11y/gtk3atkvalue.cxx b/vcl/unx/gtk3/a11y/gtk3atkvalue.cxx new file mode 100644 index 000000000..f5e45d3b2 --- /dev/null +++ b/vcl/unx/gtk3/a11y/gtk3atkvalue.cxx @@ -0,0 +1,138 @@ +/* -*- 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include "atkwrapper.hxx" + +#include <com/sun/star/accessibility/XAccessibleValue.hpp> + +#include <string.h> + +using namespace ::com::sun::star; + +/// @throws uno::RuntimeException +static css::uno::Reference<css::accessibility::XAccessibleValue> + getValue( AtkValue *pValue ) +{ + AtkObjectWrapper *pWrap = ATK_OBJECT_WRAPPER( pValue ); + if( pWrap ) + { + if( !pWrap->mpValue.is() ) + { + pWrap->mpValue.set(pWrap->mpContext, css::uno::UNO_QUERY); + } + + return pWrap->mpValue; + } + + return css::uno::Reference<css::accessibility::XAccessibleValue>(); +} + +static void anyToGValue( const uno::Any& aAny, GValue *pValue ) +{ + // FIXME: expand to lots of types etc. + double aDouble=0; + aAny >>= aDouble; + + memset( pValue, 0, sizeof( GValue ) ); + g_value_init( pValue, G_TYPE_DOUBLE ); + g_value_set_double( pValue, aDouble ); +} + +extern "C" { + +static void +value_wrapper_get_current_value( AtkValue *value, + GValue *gval ) +{ + try { + css::uno::Reference<css::accessibility::XAccessibleValue> pValue + = getValue( value ); + if( pValue.is() ) + anyToGValue( pValue->getCurrentValue(), gval ); + } + catch(const uno::Exception&) { + g_warning( "Exception in getCurrentValue()" ); + } +} + +static void +value_wrapper_get_maximum_value( AtkValue *value, + GValue *gval ) +{ + try { + css::uno::Reference<css::accessibility::XAccessibleValue> pValue + = getValue( value ); + if( pValue.is() ) + anyToGValue( pValue->getMaximumValue(), gval ); + } + catch(const uno::Exception&) { + g_warning( "Exception in getCurrentValue()" ); + } +} + +static void +value_wrapper_get_minimum_value( AtkValue *value, + GValue *gval ) +{ + try { + css::uno::Reference<css::accessibility::XAccessibleValue> pValue + = getValue( value ); + if( pValue.is() ) + anyToGValue( pValue->getMinimumValue(), gval ); + } + catch(const uno::Exception&) { + g_warning( "Exception in getCurrentValue()" ); + } +} + +static gboolean +value_wrapper_set_current_value( AtkValue *value, + const GValue *gval ) +{ + try { + css::uno::Reference<css::accessibility::XAccessibleValue> pValue + = getValue( value ); + if( pValue.is() ) + { + // FIXME - this needs expanding + double aDouble = g_value_get_double( gval ); + return pValue->setCurrentValue( uno::Any(aDouble) ); + } + } + catch(const uno::Exception&) { + g_warning( "Exception in getCurrentValue()" ); + } + + return FALSE; +} + +} // extern "C" + +void +valueIfaceInit (AtkValueIface *iface) +{ + g_return_if_fail (iface != nullptr); + + iface->get_current_value = value_wrapper_get_current_value; + iface->get_maximum_value = value_wrapper_get_maximum_value; + iface->get_minimum_value = value_wrapper_get_minimum_value; + iface->set_current_value = value_wrapper_set_current_value; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/a11y/gtk3atkwrapper.cxx b/vcl/unx/gtk3/a11y/gtk3atkwrapper.cxx new file mode 100644 index 000000000..ab011d9d0 --- /dev/null +++ b/vcl/unx/gtk3/a11y/gtk3atkwrapper.cxx @@ -0,0 +1,942 @@ +/* -*- 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <com/sun/star/uno/Any.hxx> +#include <com/sun/star/uno/Type.hxx> +#include <com/sun/star/uno/Sequence.hxx> +#include <com/sun/star/accessibility/AccessibleRole.hpp> +#include <com/sun/star/accessibility/AccessibleRelation.hpp> +#include <com/sun/star/accessibility/AccessibleRelationType.hpp> +#include <com/sun/star/accessibility/AccessibleStateType.hpp> +#include <com/sun/star/accessibility/XAccessible.hpp> +#include <com/sun/star/accessibility/XAccessibleText.hpp> +#include <com/sun/star/accessibility/XAccessibleValue.hpp> +#include <com/sun/star/accessibility/XAccessibleAction.hpp> +#include <com/sun/star/accessibility/XAccessibleContext.hpp> +#include <com/sun/star/accessibility/XAccessibleContext2.hpp> +#include <com/sun/star/accessibility/XAccessibleComponent.hpp> +#include <com/sun/star/accessibility/XAccessibleEventBroadcaster.hpp> +#include <com/sun/star/accessibility/XAccessibleStateSet.hpp> +#include <com/sun/star/accessibility/XAccessibleRelationSet.hpp> +#include <com/sun/star/accessibility/XAccessibleTable.hpp> +#include <com/sun/star/accessibility/XAccessibleEditableText.hpp> +#include <com/sun/star/accessibility/XAccessibleExtendedAttributes.hpp> +#include <com/sun/star/accessibility/XAccessibleImage.hpp> +#include <com/sun/star/accessibility/XAccessibleHypertext.hpp> +#include <com/sun/star/accessibility/XAccessibleSelection.hpp> + +#include <rtl/strbuf.hxx> +#include <osl/diagnose.h> + +#include "atkwrapper.hxx" +#include "atkregistry.hxx" +#include "atklistener.hxx" +#include "atktextattributes.hxx" + +#include <vector> +#include <dlfcn.h> + +using namespace ::com::sun::star; + +static GObjectClass *parent_class = nullptr; + +static AtkRelationType mapRelationType( sal_Int16 nRelation ) +{ + AtkRelationType type = ATK_RELATION_NULL; + + switch( nRelation ) + { + case accessibility::AccessibleRelationType::CONTENT_FLOWS_FROM: + type = ATK_RELATION_FLOWS_FROM; + break; + + case accessibility::AccessibleRelationType::CONTENT_FLOWS_TO: + type = ATK_RELATION_FLOWS_TO; + break; + + case accessibility::AccessibleRelationType::CONTROLLED_BY: + type = ATK_RELATION_CONTROLLED_BY; + break; + + case accessibility::AccessibleRelationType::CONTROLLER_FOR: + type = ATK_RELATION_CONTROLLER_FOR; + break; + + case accessibility::AccessibleRelationType::LABEL_FOR: + type = ATK_RELATION_LABEL_FOR; + break; + + case accessibility::AccessibleRelationType::LABELED_BY: + type = ATK_RELATION_LABELLED_BY; + break; + + case accessibility::AccessibleRelationType::MEMBER_OF: + type = ATK_RELATION_MEMBER_OF; + break; + + case accessibility::AccessibleRelationType::SUB_WINDOW_OF: + type = ATK_RELATION_SUBWINDOW_OF; + break; + + case accessibility::AccessibleRelationType::NODE_CHILD_OF: + type = ATK_RELATION_NODE_CHILD_OF; + break; + + default: + break; + } + + return type; +} + +AtkStateType mapAtkState( sal_Int16 nState ) +{ + AtkStateType type = ATK_STATE_INVALID; + + // A perfect / complete mapping ... + switch( nState ) + { +#define MAP_DIRECT( a ) \ + case accessibility::AccessibleStateType::a: \ + type = ATK_STATE_##a; break + + MAP_DIRECT( INVALID ); + MAP_DIRECT( ACTIVE ); + MAP_DIRECT( ARMED ); + MAP_DIRECT( BUSY ); + MAP_DIRECT( CHECKED ); + MAP_DIRECT( EDITABLE ); + MAP_DIRECT( ENABLED ); + MAP_DIRECT( EXPANDABLE ); + MAP_DIRECT( EXPANDED ); + MAP_DIRECT( FOCUSABLE ); + MAP_DIRECT( FOCUSED ); + MAP_DIRECT( HORIZONTAL ); + MAP_DIRECT( ICONIFIED ); + MAP_DIRECT( INDETERMINATE ); + MAP_DIRECT( MANAGES_DESCENDANTS ); + MAP_DIRECT( MODAL ); + MAP_DIRECT( MULTI_LINE ); + MAP_DIRECT( OPAQUE ); + MAP_DIRECT( PRESSED ); + MAP_DIRECT( RESIZABLE ); + MAP_DIRECT( SELECTABLE ); + MAP_DIRECT( SELECTED ); + MAP_DIRECT( SENSITIVE ); + MAP_DIRECT( SHOWING ); + MAP_DIRECT( SINGLE_LINE ); + MAP_DIRECT( STALE ); + MAP_DIRECT( TRANSIENT ); + MAP_DIRECT( VERTICAL ); + MAP_DIRECT( VISIBLE ); + MAP_DIRECT( DEFAULT ); + // a spelling error ... + case accessibility::AccessibleStateType::DEFUNC: + type = ATK_STATE_DEFUNCT; break; + case accessibility::AccessibleStateType::MULTI_SELECTABLE: + type = ATK_STATE_MULTISELECTABLE; break; + default: + //Mis-use ATK_STATE_LAST_DEFINED to check if a state is unmapped + //NOTE! Do not report it + type = ATK_STATE_LAST_DEFINED; + break; + } + + return type; +} + +static AtkRole getRoleForName( const gchar * name ) +{ + AtkRole ret = atk_role_for_name( name ); + if( ATK_ROLE_INVALID == ret ) + { + // this should only happen in old ATK versions + SAL_WNODEPRECATED_DECLARATIONS_PUSH + ret = atk_role_register( name ); + SAL_WNODEPRECATED_DECLARATIONS_POP + } + + return ret; +} + +static AtkRole mapToAtkRole( sal_Int16 nRole ) +{ + AtkRole role = ATK_ROLE_UNKNOWN; + + static AtkRole roleMap[] = { + ATK_ROLE_UNKNOWN, + ATK_ROLE_ALERT, + ATK_ROLE_COLUMN_HEADER, + ATK_ROLE_CANVAS, + ATK_ROLE_CHECK_BOX, + ATK_ROLE_CHECK_MENU_ITEM, + ATK_ROLE_COLOR_CHOOSER, + ATK_ROLE_COMBO_BOX, + ATK_ROLE_DATE_EDITOR, + ATK_ROLE_DESKTOP_ICON, + ATK_ROLE_DESKTOP_FRAME, // ? pane + ATK_ROLE_DIRECTORY_PANE, + ATK_ROLE_DIALOG, + ATK_ROLE_UNKNOWN, // DOCUMENT - registered below + ATK_ROLE_UNKNOWN, // EMBEDDED_OBJECT - registered below + ATK_ROLE_UNKNOWN, // END_NOTE - registered below + ATK_ROLE_FILE_CHOOSER, + ATK_ROLE_FILLER, + ATK_ROLE_FONT_CHOOSER, + ATK_ROLE_FOOTER, + ATK_ROLE_UNKNOWN, // FOOTNOTE - registered below + ATK_ROLE_FRAME, + ATK_ROLE_GLASS_PANE, + ATK_ROLE_IMAGE, // GRAPHIC + ATK_ROLE_UNKNOWN, // GROUP_BOX - registered below + ATK_ROLE_HEADER, + ATK_ROLE_HEADING, + ATK_ROLE_TEXT, // HYPER_LINK - registered below + ATK_ROLE_ICON, + ATK_ROLE_INTERNAL_FRAME, + ATK_ROLE_LABEL, + ATK_ROLE_LAYERED_PANE, + ATK_ROLE_LIST, + ATK_ROLE_LIST_ITEM, + ATK_ROLE_MENU, + ATK_ROLE_MENU_BAR, + ATK_ROLE_MENU_ITEM, + ATK_ROLE_OPTION_PANE, + ATK_ROLE_PAGE_TAB, + ATK_ROLE_PAGE_TAB_LIST, + ATK_ROLE_PANEL, + ATK_ROLE_PARAGRAPH, + ATK_ROLE_PASSWORD_TEXT, + ATK_ROLE_POPUP_MENU, + ATK_ROLE_PUSH_BUTTON, + ATK_ROLE_PROGRESS_BAR, + ATK_ROLE_RADIO_BUTTON, + ATK_ROLE_RADIO_MENU_ITEM, + ATK_ROLE_ROW_HEADER, + ATK_ROLE_ROOT_PANE, + ATK_ROLE_SCROLL_BAR, + ATK_ROLE_SCROLL_PANE, + ATK_ROLE_PANEL, // SHAPE + ATK_ROLE_SEPARATOR, + ATK_ROLE_SLIDER, + ATK_ROLE_SPIN_BUTTON, // SPIN_BOX ? + ATK_ROLE_SPLIT_PANE, + ATK_ROLE_STATUSBAR, + ATK_ROLE_TABLE, + ATK_ROLE_TABLE_CELL, + ATK_ROLE_TEXT, + ATK_ROLE_PANEL, // TEXT_FRAME + ATK_ROLE_TOGGLE_BUTTON, + ATK_ROLE_TOOL_BAR, + ATK_ROLE_TOOL_TIP, + ATK_ROLE_TREE, + ATK_ROLE_VIEWPORT, + ATK_ROLE_WINDOW, + ATK_ROLE_PUSH_BUTTON, // BUTTON_DROPDOWN + ATK_ROLE_PUSH_BUTTON, // BUTTON_MENU + ATK_ROLE_UNKNOWN, // CAPTION - registered below + ATK_ROLE_UNKNOWN, // CHART - registered below + ATK_ROLE_UNKNOWN, // EDIT_BAR - registered below + ATK_ROLE_UNKNOWN, // FORM - registered below + ATK_ROLE_UNKNOWN, // IMAGE_MAP - registered below + ATK_ROLE_UNKNOWN, // NOTE - registered below + ATK_ROLE_UNKNOWN, // PAGE - registered below + ATK_ROLE_RULER, + ATK_ROLE_UNKNOWN, // SECTION - registered below + ATK_ROLE_UNKNOWN, // TREE_ITEM - registered below + ATK_ROLE_TREE_TABLE, + ATK_ROLE_SCROLL_PANE, // COMMENT - mapped to atk_role_scroll_pane + ATK_ROLE_UNKNOWN // COMMENT_END - mapped to atk_role_unknown +#if defined(ATK_CHECK_VERSION) + //older ver that doesn't define ATK_CHECK_VERSION doesn't have the following + , ATK_ROLE_DOCUMENT_PRESENTATION + , ATK_ROLE_DOCUMENT_SPREADSHEET + , ATK_ROLE_DOCUMENT_TEXT +#if ATK_CHECK_VERSION(2,15,2) + , ATK_ROLE_STATIC +#else + , ATK_ROLE_LABEL +#endif +#else + //older version should fallback to DOCUMENT_FRAME role + , ATK_ROLE_DOCUMENT_FRAME + , ATK_ROLE_DOCUMENT_FRAME + , ATK_ROLE_DOCUMENT_FRAME + , ATK_ROLE_LABEL +#endif + }; + + static bool initialized = false; + + if( ! initialized ) + { + // the accessible roles below were added to ATK in later versions, + // with role_for_name we will know if they exist in runtime. + roleMap[accessibility::AccessibleRole::EDIT_BAR] = getRoleForName("edit bar"); + roleMap[accessibility::AccessibleRole::EMBEDDED_OBJECT] = getRoleForName("embedded"); + roleMap[accessibility::AccessibleRole::CHART] = getRoleForName("chart"); + roleMap[accessibility::AccessibleRole::CAPTION] = getRoleForName("caption"); + roleMap[accessibility::AccessibleRole::DOCUMENT] = getRoleForName("document frame"); + roleMap[accessibility::AccessibleRole::PAGE] = getRoleForName("page"); + roleMap[accessibility::AccessibleRole::SECTION] = getRoleForName("section"); + roleMap[accessibility::AccessibleRole::FORM] = getRoleForName("form"); + roleMap[accessibility::AccessibleRole::GROUP_BOX] = getRoleForName("grouping"); + roleMap[accessibility::AccessibleRole::COMMENT] = getRoleForName("comment"); + roleMap[accessibility::AccessibleRole::IMAGE_MAP] = getRoleForName("image map"); + roleMap[accessibility::AccessibleRole::TREE_ITEM] = getRoleForName("tree item"); + roleMap[accessibility::AccessibleRole::HYPER_LINK] = getRoleForName("link"); + roleMap[accessibility::AccessibleRole::END_NOTE] = getRoleForName("footnote"); + roleMap[accessibility::AccessibleRole::FOOTNOTE] = getRoleForName("footnote"); + roleMap[accessibility::AccessibleRole::NOTE] = getRoleForName("comment"); + + initialized = true; + } + + static const sal_Int32 nMapSize = SAL_N_ELEMENTS(roleMap); + if( 0 <= nRole && nMapSize > nRole ) + role = roleMap[nRole]; + + return role; +} + +/*****************************************************************************/ + +extern "C" { + +/*****************************************************************************/ + +static const gchar* +wrapper_get_name( AtkObject *atk_obj ) +{ + AtkObjectWrapper *obj = ATK_OBJECT_WRAPPER (atk_obj); + + if( obj->mpContext.is() ) + { + try { + OString aName = + OUStringToOString( + obj->mpContext->getAccessibleName(), + RTL_TEXTENCODING_UTF8); + + int nCmp = atk_obj->name ? rtl_str_compare( atk_obj->name, aName.getStr() ) : -1; + if( nCmp != 0 ) + { + if( atk_obj->name ) + g_free(atk_obj->name); + atk_obj->name = g_strdup(aName.getStr()); + } + } + catch(const uno::Exception&) { + g_warning( "Exception in getAccessibleName()" ); + } + } + + return ATK_OBJECT_CLASS (parent_class)->get_name(atk_obj); +} + +/*****************************************************************************/ + +static const gchar* +wrapper_get_description( AtkObject *atk_obj ) +{ + AtkObjectWrapper *obj = ATK_OBJECT_WRAPPER (atk_obj); + + if( obj->mpContext.is() ) + { + try { + OString aDescription = + OUStringToOString( + obj->mpContext->getAccessibleDescription(), + RTL_TEXTENCODING_UTF8); + + g_free(atk_obj->description); + atk_obj->description = g_strdup(aDescription.getStr()); + } + catch(const uno::Exception&) { + g_warning( "Exception in getAccessibleDescription()" ); + } + } + + return ATK_OBJECT_CLASS (parent_class)->get_description(atk_obj); + +} + +/*****************************************************************************/ + +static AtkAttributeSet * +wrapper_get_attributes( AtkObject *atk_obj ) +{ + AtkObjectWrapper *obj = ATK_OBJECT_WRAPPER( atk_obj ); + AtkAttributeSet *pSet = nullptr; + + try + { + uno::Reference< accessibility::XAccessibleExtendedAttributes > + xExtendedAttrs( obj->mpContext, uno::UNO_QUERY ); + if( xExtendedAttrs.is() ) + pSet = attribute_set_new_from_extended_attributes( xExtendedAttrs ); + } + catch(const uno::Exception&) + { + g_warning( "Exception in getAccessibleAttributes()" ); + } + + return pSet; +} + +/*****************************************************************************/ + +static gint +wrapper_get_n_children( AtkObject *atk_obj ) +{ + AtkObjectWrapper *obj = ATK_OBJECT_WRAPPER (atk_obj); + gint n = 0; + + if( obj->mpContext.is() ) + { + try { + n = obj->mpContext->getAccessibleChildCount(); + } + catch(const uno::Exception&) { + OSL_FAIL("Exception in getAccessibleChildCount()" ); + } + } + + return n; +} + +/*****************************************************************************/ + +static AtkObject * +wrapper_ref_child( AtkObject *atk_obj, + gint i ) +{ + AtkObjectWrapper *obj = ATK_OBJECT_WRAPPER (atk_obj); + AtkObject* child = nullptr; + + // see comments above atk_object_wrapper_remove_child + if( -1 < i && obj->index_of_child_about_to_be_removed == i ) + { + g_object_ref( obj->child_about_to_be_removed ); + return obj->child_about_to_be_removed; + } + + if( obj->mpContext.is() ) + { + try { + uno::Reference< accessibility::XAccessible > xAccessible = + obj->mpContext->getAccessibleChild( i ); + + child = atk_object_wrapper_ref( xAccessible ); + } + catch(const uno::Exception&) { + OSL_FAIL("Exception in getAccessibleChild"); + } + } + + return child; +} + +/*****************************************************************************/ + +static gint +wrapper_get_index_in_parent( AtkObject *atk_obj ) +{ + AtkObjectWrapper *obj = ATK_OBJECT_WRAPPER (atk_obj); + + //if we're a native GtkDrawingArea with custom a11y, use the default toolkit a11y + if (obj->mpOrig) + return atk_object_get_index_in_parent(obj->mpOrig); + + gint i = -1; + + if( obj->mpContext.is() ) + { + try { + i = obj->mpContext->getAccessibleIndexInParent(); + } + catch(const uno::Exception&) { + g_warning( "Exception in getAccessibleIndexInParent()" ); + } + } + return i; +} + +/*****************************************************************************/ + +AtkRelation* +atk_object_wrapper_relation_new(const accessibility::AccessibleRelation& rRelation) +{ + sal_uInt32 nTargetCount = rRelation.TargetSet.getLength(); + + std::vector<AtkObject*> aTargets; + + for (const auto& rTarget : rRelation.TargetSet) + { + uno::Reference< accessibility::XAccessible > xAccessible( rTarget, uno::UNO_QUERY ); + aTargets.push_back(atk_object_wrapper_ref(xAccessible)); + } + + AtkRelation *pRel = + atk_relation_new( + aTargets.data(), nTargetCount, + mapRelationType( rRelation.RelationType ) + ); + + return pRel; +} + +static AtkRelationSet * +wrapper_ref_relation_set( AtkObject *atk_obj ) +{ + AtkObjectWrapper *obj = ATK_OBJECT_WRAPPER (atk_obj); + + //if we're a native GtkDrawingArea with custom a11y, use the default toolkit relation set impl + if (obj->mpOrig) + return atk_object_ref_relation_set(obj->mpOrig); + + AtkRelationSet *pSet = atk_relation_set_new(); + + if( obj->mpContext.is() ) + { + try { + uno::Reference< accessibility::XAccessibleRelationSet > xRelationSet( + obj->mpContext->getAccessibleRelationSet() + ); + + sal_Int32 nRelations = xRelationSet.is() ? xRelationSet->getRelationCount() : 0; + for( sal_Int32 n = 0; n < nRelations; n++ ) + { + AtkRelation *pRel = atk_object_wrapper_relation_new(xRelationSet->getRelation(n)); + atk_relation_set_add(pSet, pRel); + g_object_unref(pRel); + } + } + catch(const uno::Exception &) { + g_object_unref( G_OBJECT( pSet ) ); + pSet = nullptr; + } + } + + return pSet; +} + +static AtkStateSet * +wrapper_ref_state_set( AtkObject *atk_obj ) +{ + AtkObjectWrapper *obj = ATK_OBJECT_WRAPPER (atk_obj); + AtkStateSet *pSet = atk_state_set_new(); + + if( obj->mpContext.is() ) + { + try { + uno::Reference< accessibility::XAccessibleStateSet > xStateSet( + obj->mpContext->getAccessibleStateSet()); + + if( xStateSet.is() ) + { + uno::Sequence< sal_Int16 > aStates = xStateSet->getStates(); + + for( const auto nState : aStates ) + { + // ATK_STATE_LAST_DEFINED is used to check if the state + // is unmapped, do not report it to Atk + if ( mapAtkState( nState ) != ATK_STATE_LAST_DEFINED ) + atk_state_set_add_state( pSet, mapAtkState( nState ) ); + } + + // We need to emulate FOCUS state for menus, menu-items etc. + if( atk_obj == atk_get_focus_object() ) + atk_state_set_add_state( pSet, ATK_STATE_FOCUSED ); +/* FIXME - should we do this ? + else + atk_state_set_remove_state( pSet, ATK_STATE_FOCUSED ); +*/ + } + } + + catch(const uno::Exception &) { + g_warning( "Exception in wrapper_ref_state_set" ); + atk_state_set_add_state( pSet, ATK_STATE_DEFUNCT ); + } + } + else + atk_state_set_add_state( pSet, ATK_STATE_DEFUNCT ); + + return pSet; +} + +/*****************************************************************************/ + +static void +atk_object_wrapper_finalize (GObject *obj) +{ + AtkObjectWrapper *pWrap = ATK_OBJECT_WRAPPER (obj); + + if( pWrap->mpAccessible.is() ) + { + ooo_wrapper_registry_remove( pWrap->mpAccessible ); + pWrap->mpAccessible.clear(); + } + + atk_object_wrapper_dispose( pWrap ); + + parent_class->finalize( obj ); +} + +static void +atk_object_wrapper_class_init (AtkObjectWrapperClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS( klass ); + AtkObjectClass *atk_class = ATK_OBJECT_CLASS( klass ); + + parent_class = static_cast<GObjectClass *>(g_type_class_peek_parent (klass)); + + // GObject methods + gobject_class->finalize = atk_object_wrapper_finalize; + + // AtkObject methods + atk_class->get_name = wrapper_get_name; + atk_class->get_description = wrapper_get_description; + atk_class->get_attributes = wrapper_get_attributes; + atk_class->get_n_children = wrapper_get_n_children; + atk_class->ref_child = wrapper_ref_child; + atk_class->get_index_in_parent = wrapper_get_index_in_parent; + atk_class->ref_relation_set = wrapper_ref_relation_set; + atk_class->ref_state_set = wrapper_ref_state_set; +} + +static void +atk_object_wrapper_init (AtkObjectWrapper *wrapper, + AtkObjectWrapperClass*) +{ + wrapper->mpAction = nullptr; + wrapper->mpComponent = nullptr; + wrapper->mpEditableText = nullptr; + wrapper->mpHypertext = nullptr; + wrapper->mpImage = nullptr; + wrapper->mpSelection = nullptr; + wrapper->mpTable = nullptr; + wrapper->mpText = nullptr; + wrapper->mpValue = nullptr; +} + +} // extern "C" + +GType +atk_object_wrapper_get_type() +{ + static GType type = 0; + + if (!type) + { + static const GTypeInfo typeInfo = + { + sizeof (AtkObjectWrapperClass), + nullptr, + nullptr, + reinterpret_cast<GClassInitFunc>(atk_object_wrapper_class_init), + nullptr, + nullptr, + sizeof (AtkObjectWrapper), + 0, + reinterpret_cast<GInstanceInitFunc>(atk_object_wrapper_init), + nullptr + } ; + type = g_type_register_static (ATK_TYPE_OBJECT, + "OOoAtkObj", + &typeInfo, GTypeFlags(0)) ; + } + return type; +} + +static bool +isOfType( uno::XInterface *pInterface, const uno::Type & rType ) +{ + g_return_val_if_fail( pInterface != nullptr, false ); + + bool bIs = false; + try { + uno::Any aRet = pInterface->queryInterface( rType ); + + bIs = ( ( typelib_TypeClass_INTERFACE == aRet.pType->eTypeClass ) && + ( aRet.pReserved != nullptr ) ); + } catch( const uno::Exception &) { } + + return bIs; +} + +extern "C" { +typedef GType (* GetGIfaceType ) (); +} +const struct { + const char *name; + GInterfaceInitFunc const aInit; + GetGIfaceType const aGetGIfaceType; + const uno::Type & (*aGetUnoType) (); +} aTypeTable[] = { +// re-location heaven: + { + "Comp", reinterpret_cast<GInterfaceInitFunc>(componentIfaceInit), + atk_component_get_type, + cppu::UnoType<accessibility::XAccessibleComponent>::get + }, + { + "Act", reinterpret_cast<GInterfaceInitFunc>(actionIfaceInit), + atk_action_get_type, + cppu::UnoType<accessibility::XAccessibleAction>::get + }, + { + "Txt", reinterpret_cast<GInterfaceInitFunc>(textIfaceInit), + atk_text_get_type, + cppu::UnoType<accessibility::XAccessibleText>::get + }, + { + "Val", reinterpret_cast<GInterfaceInitFunc>(valueIfaceInit), + atk_value_get_type, + cppu::UnoType<accessibility::XAccessibleValue>::get + }, + { + "Tab", reinterpret_cast<GInterfaceInitFunc>(tableIfaceInit), + atk_table_get_type, + cppu::UnoType<accessibility::XAccessibleTable>::get + }, + { + "Edt", reinterpret_cast<GInterfaceInitFunc>(editableTextIfaceInit), + atk_editable_text_get_type, + cppu::UnoType<accessibility::XAccessibleEditableText>::get + }, + { + "Img", reinterpret_cast<GInterfaceInitFunc>(imageIfaceInit), + atk_image_get_type, + cppu::UnoType<accessibility::XAccessibleImage>::get + }, + { + "Hyp", reinterpret_cast<GInterfaceInitFunc>(hypertextIfaceInit), + atk_hypertext_get_type, + cppu::UnoType<accessibility::XAccessibleHypertext>::get + }, + { + "Sel", reinterpret_cast<GInterfaceInitFunc>(selectionIfaceInit), + atk_selection_get_type, + cppu::UnoType<accessibility::XAccessibleSelection>::get + } + // AtkDocument is a nastily broken interface (so far) + // we could impl. get_document_type perhaps though. +}; + +const int aTypeTableSize = G_N_ELEMENTS( aTypeTable ); + +static GType +ensureTypeFor( uno::XInterface *pAccessible ) +{ + int i; + bool bTypes[ aTypeTableSize ] = { false, }; + OStringBuffer aTypeNameBuf( "OOoAtkObj" ); + + for( i = 0; i < aTypeTableSize; i++ ) + { + if( isOfType( pAccessible, aTypeTable[i].aGetUnoType() ) ) + { + aTypeNameBuf.append(aTypeTable[i].name); + bTypes[i] = true; + } + } + + OString aTypeName = aTypeNameBuf.makeStringAndClear(); + GType nType = g_type_from_name( aTypeName.getStr() ); + if( nType == G_TYPE_INVALID ) + { + GTypeInfo aTypeInfo = { + sizeof( AtkObjectWrapperClass ), + nullptr, nullptr, nullptr, nullptr, nullptr, + sizeof( AtkObjectWrapper ), + 0, nullptr, nullptr + } ; + nType = g_type_register_static( ATK_TYPE_OBJECT_WRAPPER, + aTypeName.getStr(), &aTypeInfo, + GTypeFlags(0) ) ; + + for( int j = 0; j < aTypeTableSize; j++ ) + if( bTypes[j] ) + { + GInterfaceInfo aIfaceInfo = { nullptr, nullptr, nullptr }; + aIfaceInfo.interface_init = aTypeTable[j].aInit; + g_type_add_interface_static (nType, aTypeTable[j].aGetGIfaceType(), + &aIfaceInfo); + } + } + return nType; +} + +AtkObject * +atk_object_wrapper_ref( const uno::Reference< accessibility::XAccessible > &rxAccessible, bool create ) +{ + g_return_val_if_fail( rxAccessible.get() != nullptr, nullptr ); + + AtkObject *obj = ooo_wrapper_registry_get(rxAccessible); + if( obj ) + { + g_object_ref( obj ); + return obj; + } + + if( create ) + return atk_object_wrapper_new( rxAccessible ); + + return nullptr; +} + +AtkObject * +atk_object_wrapper_new( const css::uno::Reference< css::accessibility::XAccessible >& rxAccessible, + AtkObject* parent, AtkObject* orig ) +{ + g_return_val_if_fail( rxAccessible.get() != nullptr, nullptr ); + + AtkObjectWrapper *pWrap = nullptr; + + try { + uno::Reference< accessibility::XAccessibleContext > xContext(rxAccessible->getAccessibleContext()); + + g_return_val_if_fail( xContext.get() != nullptr, nullptr ); + + GType nType = ensureTypeFor( xContext.get() ); + gpointer obj = g_object_new( nType, nullptr); + + pWrap = ATK_OBJECT_WRAPPER( obj ); + pWrap->mpAccessible = rxAccessible; + + pWrap->index_of_child_about_to_be_removed = -1; + pWrap->child_about_to_be_removed = nullptr; + + pWrap->mpContext = xContext; + pWrap->mpOrig = orig; + + AtkObject* atk_obj = ATK_OBJECT(pWrap); + atk_obj->role = mapToAtkRole( xContext->getAccessibleRole() ); + atk_obj->accessible_parent = parent; + + ooo_wrapper_registry_add( rxAccessible, atk_obj ); + + if( parent ) + g_object_ref( atk_obj->accessible_parent ); + else + { + /* gail_focus_tracker remembers the focused object at the first + * parent in the hierarchy that is a Gtk+ widget, but at the time the + * event gets processed (at idle), it may be too late to create the + * hierarchy, so doing it now .. + */ + uno::Reference< accessibility::XAccessible > xParent( xContext->getAccessibleParent() ); + + if( xParent.is() ) + atk_obj->accessible_parent = atk_object_wrapper_ref( xParent ); + } + + // Attach a listener to the UNO object if it's not TRANSIENT + uno::Reference< accessibility::XAccessibleStateSet > xStateSet( xContext->getAccessibleStateSet() ); + if( xStateSet.is() && ! xStateSet->contains( accessibility::AccessibleStateType::TRANSIENT ) ) + { + uno::Reference< accessibility::XAccessibleEventBroadcaster > xBroadcaster(xContext, uno::UNO_QUERY); + if( xBroadcaster.is() ) + { + uno::Reference<accessibility::XAccessibleEventListener> xListener(new AtkListener(pWrap)); + xBroadcaster->addAccessibleEventListener(xListener); + } + else + OSL_ASSERT( false ); + } + + static auto func = reinterpret_cast<void(*)(AtkObject*, const gchar*)>(dlsym(nullptr, "atk_object_set_accessible_id")); + if (func) + { + css::uno::Reference<css::accessibility::XAccessibleContext2> xContext2(xContext, css::uno::UNO_QUERY); + if( xContext2.is() ) + { + OString aId = OUStringToOString( xContext2->getAccessibleId(), RTL_TEXTENCODING_UTF8); + (*func)(atk_obj, aId.getStr()); + } + } + + return ATK_OBJECT( pWrap ); + } + catch (const uno::Exception &) + { + if( pWrap ) + g_object_unref( pWrap ); + + return nullptr; + } +} + +/*****************************************************************************/ + +void atk_object_wrapper_add_child(AtkObjectWrapper* wrapper, AtkObject *child, gint index) +{ + AtkObject *atk_obj = ATK_OBJECT( wrapper ); + + atk_object_set_parent( child, atk_obj ); + g_signal_emit_by_name( atk_obj, "children_changed::add", index, child, nullptr ); +} + +/*****************************************************************************/ + +void atk_object_wrapper_remove_child(AtkObjectWrapper* wrapper, AtkObject *child, gint index) +{ + /* + * the atk-bridge GTK+ module gets back to the event source to ref the child just + * vanishing, so we keep this reference because the semantic on OOo side is different. + */ + wrapper->child_about_to_be_removed = child; + wrapper->index_of_child_about_to_be_removed = index; + + g_signal_emit_by_name( ATK_OBJECT( wrapper ), "children_changed::remove", index, child, nullptr ); + + wrapper->index_of_child_about_to_be_removed = -1; + wrapper->child_about_to_be_removed = nullptr; +} + +/*****************************************************************************/ + +void atk_object_wrapper_set_role(AtkObjectWrapper* wrapper, sal_Int16 role) +{ + AtkObject *atk_obj = ATK_OBJECT( wrapper ); + atk_object_set_role( atk_obj, mapToAtkRole( role ) ); +} + +/*****************************************************************************/ + +void atk_object_wrapper_dispose(AtkObjectWrapper* wrapper) +{ + wrapper->mpContext.clear(); + wrapper->mpAction.clear(); + wrapper->mpComponent.clear(); + wrapper->mpEditableText.clear(); + wrapper->mpHypertext.clear(); + wrapper->mpImage.clear(); + wrapper->mpSelection.clear(); + wrapper->mpMultiLineText.clear(); + wrapper->mpTable.clear(); + wrapper->mpText.clear(); + wrapper->mpTextMarkup.clear(); + wrapper->mpTextAttributes.clear(); + wrapper->mpValue.clear(); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/cairo_gtk3_cairo.cxx b/vcl/unx/gtk3/cairo_gtk3_cairo.cxx new file mode 100644 index 000000000..2e882ded6 --- /dev/null +++ b/vcl/unx/gtk3/cairo_gtk3_cairo.cxx @@ -0,0 +1,128 @@ +/* -*- 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 "cairo_gtk3_cairo.hxx" + +#include <vcl/sysdata.hxx> +#include <vcl/virdev.hxx> + +#include <unx/gtk/gtkgdi.hxx> + +namespace +{ + Size get_surface_size(cairo_surface_t *surface) + { + cairo_t *cr = cairo_create(surface); + double x1, x2, y1, y2; + cairo_clip_extents(cr, &x1, &y1, &x2, &y2); + cairo_destroy(cr); + return Size(x2 - x1, y2 - y1); + } +} + +namespace cairo +{ + /** + * Surface::Surface: Create generic Canvas surface using given Cairo Surface + * + * @param pSurface Cairo Surface + * + * This constructor only stores data, it does no processing. + * It is used with e.g. cairo_image_surface_create_for_data() + * + * Set the mpSurface as pSurface + **/ + Gtk3Surface::Gtk3Surface(const CairoSurfaceSharedPtr& pSurface) + : mpGraphics(nullptr) + , cr(nullptr) + , mpSurface(pSurface) + {} + + /** + * Surface::Surface: Create Canvas surface from Window reference. + * @param x horizontal location of the new surface + * @param y vertical location of the new surface + * @param width width of the new surface + * @param height height of the new surface + * + * Set the mpSurface to the new surface or NULL + **/ + Gtk3Surface::Gtk3Surface(const GtkSalGraphics* pGraphics, int x, int y, int width, int height) + : mpGraphics(pGraphics) + , cr(pGraphics->getCairoContext(false)) + { + cairo_surface_t* surface = cairo_get_target(cr); + mpSurface.reset( + cairo_surface_create_for_rectangle(surface, x, y, width, height), + &cairo_surface_destroy); + } + + Gtk3Surface::~Gtk3Surface() + { + if (cr) + cairo_destroy(cr); + } + + /** + * Surface::getCairo: Create Cairo (drawing object) for the Canvas surface + * + * @return new Cairo or NULL + **/ + CairoSharedPtr Gtk3Surface::getCairo() const + { + return CairoSharedPtr( cairo_create(mpSurface.get()), + &cairo_destroy ); + } + + /** + * Surface::getSimilar: Create new similar Canvas surface + * @param cairo_content_type format of the new surface (cairo_content_t from cairo/src/cairo.h) + * @param width width of the new surface + * @param height height of the new surface + * + * Creates a new Canvas surface. This normally creates platform native surface, even though + * generic function is used. + * + * Cairo surface from cairo_content_type (cairo_content_t) + * + * @return new surface or NULL + **/ + SurfaceSharedPtr Gtk3Surface::getSimilar(int cairo_content_type, int width, int height ) const + { + return std::make_shared<Gtk3Surface>( + CairoSurfaceSharedPtr( + cairo_surface_create_similar( mpSurface.get(), + static_cast<cairo_content_t>(cairo_content_type), width, height ), + &cairo_surface_destroy )); + } + + void Gtk3Surface::flush() const + { + cairo_surface_flush(mpSurface.get()); + //Wonder if there is any benefit in using cairo_fill/stroke extents on + //every canvas call and only redrawing the union of those in a + //poor-mans-damage tracking + if (mpGraphics) + mpGraphics->WidgetQueueDraw(); + } + + VclPtr<VirtualDevice> Gtk3Surface::createVirtualDevice() const + { + SystemGraphicsData aSystemGraphicsData; + + aSystemGraphicsData.nSize = sizeof(SystemGraphicsData); + aSystemGraphicsData.pSurface = mpSurface.get(); + + return VclPtr<VirtualDevice>::Create(aSystemGraphicsData, + get_surface_size(mpSurface.get()), + DeviceFormat::DEFAULT); + } +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/cairo_gtk3_cairo.hxx b/vcl/unx/gtk3/cairo_gtk3_cairo.hxx new file mode 100644 index 000000000..59ed1437f --- /dev/null +++ b/vcl/unx/gtk3/cairo_gtk3_cairo.hxx @@ -0,0 +1,49 @@ +/* -*- 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/. + */ + +#ifndef INCLUDED_CANVAS_SOURCE_CAIRO_CAIRO_GTK3_CAIRO_HXX +#define INCLUDED_CANVAS_SOURCE_CAIRO_CAIRO_GTK3_CAIRO_HXX + +#include <sal/config.h> + +#include <vcl/cairo.hxx> + +class GtkSalGraphics; +class OutputDevice; + +namespace cairo { + + class Gtk3Surface : public Surface + { + const GtkSalGraphics* mpGraphics; + cairo_t* cr; + CairoSurfaceSharedPtr mpSurface; + public: + /// takes over ownership of passed cairo_surface + explicit Gtk3Surface(const CairoSurfaceSharedPtr& pSurface); + /// create surface on subarea of given drawable + explicit Gtk3Surface(const GtkSalGraphics* pGraphics, int x, int y, int width, int height); + + // Surface interface + virtual CairoSharedPtr getCairo() const override; + virtual CairoSurfaceSharedPtr getCairoSurface() const override { return mpSurface; } + virtual SurfaceSharedPtr getSimilar(int nContentType, int width, int height) const override; + + virtual VclPtr<VirtualDevice> createVirtualDevice() const override; + + virtual void flush() const override; + + virtual ~Gtk3Surface() override; + }; + +} + +#endif + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/fpicker/SalGtkFilePicker.cxx b/vcl/unx/gtk3/fpicker/SalGtkFilePicker.cxx new file mode 100644 index 000000000..1015fcf67 --- /dev/null +++ b/vcl/unx/gtk3/fpicker/SalGtkFilePicker.cxx @@ -0,0 +1,1991 @@ +/* -*- 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#ifdef AIX +#define _LINUX_SOURCE_COMPAT +#include <sys/timer.h> +#undef _LINUX_SOURCE_COMPAT +#endif + +#include <config_gio.h> + +#include <com/sun/star/awt/SystemDependentXWindow.hpp> +#include <com/sun/star/awt/Toolkit.hpp> +#include <com/sun/star/awt/XSystemDependentWindowPeer.hpp> +#include <com/sun/star/frame/Desktop.hpp> +#include <com/sun/star/lang/IllegalArgumentException.hpp> +#include <com/sun/star/lang/SystemDependent.hpp> +#include <com/sun/star/ui/dialogs/ExecutableDialogResults.hpp> +#include <com/sun/star/ui/dialogs/CommonFilePickerElementIds.hpp> +#include <com/sun/star/ui/dialogs/ExtendedFilePickerElementIds.hpp> +#include <osl/diagnose.h> +#include <rtl/process.h> +#include <sal/log.hxx> +#include <com/sun/star/ui/dialogs/TemplateDescription.hpp> +#include <com/sun/star/ui/dialogs/ControlActions.hpp> +#include <com/sun/star/uno/Any.hxx> +#include <unx/gtk/gtkdata.hxx> +#include <unx/gtk/gtkinst.hxx> + +#include <vcl/svapp.hxx> + +#include <tools/urlobj.hxx> + +#include <algorithm> +#include <set> +#include <string.h> + +#include "SalGtkFilePicker.hxx" + +using namespace ::com::sun::star; +using namespace ::com::sun::star::ui::dialogs; +using namespace ::com::sun::star::ui::dialogs::TemplateDescription; +using namespace ::com::sun::star::ui::dialogs::ExtendedFilePickerElementIds; +using namespace ::com::sun::star::ui::dialogs::CommonFilePickerElementIds; +using namespace ::com::sun::star::lang; +using namespace ::com::sun::star::beans; +using namespace ::com::sun::star::uno; + +void SalGtkFilePicker::dialog_mapped_cb(GtkWidget *, SalGtkFilePicker *pobjFP) +{ + pobjFP->InitialMapping(); +} + +void SalGtkFilePicker::InitialMapping() +{ + if (!mbPreviewState ) + { + gtk_widget_hide( m_pPreview ); + gtk_file_chooser_set_preview_widget_active( GTK_FILE_CHOOSER( m_pDialog ), false); + } + gtk_widget_set_size_request (m_pPreview, -1, -1); +} + +SalGtkFilePicker::SalGtkFilePicker( const uno::Reference< uno::XComponentContext >& xContext ) : + SalGtkPicker( xContext ), + SalGtkFilePicker_Base( m_rbHelperMtx ), + m_pParentWidget ( nullptr ), + m_pVBox ( nullptr ), + mnHID_FolderChange( 0 ), + mnHID_SelectionChange( 0 ), + bVersionWidthUnset( false ), + mbPreviewState( false ), + mHID_Preview( 0 ), + m_pPreview( nullptr ), + m_pPseudoFilter( nullptr ) +{ + int i; + + for( i = 0; i < TOGGLE_LAST; i++ ) + { + m_pToggles[i] = nullptr; + mbToggleVisibility[i] = false; + } + + for( i = 0; i < BUTTON_LAST; i++ ) + { + m_pButtons[i] = nullptr; + mbButtonVisibility[i] = false; + } + + for( i = 0; i < LIST_LAST; i++ ) + { + m_pHBoxs[i] = nullptr; + m_pAligns[i] = nullptr; + m_pLists[i] = nullptr; + m_pListLabels[i] = nullptr; + mbListVisibility[i] = false; + } + + OUString aFilePickerTitle = getResString( FILE_PICKER_TITLE_OPEN ); + + m_pDialog = gtk_file_chooser_dialog_new( + OUStringToOString( aFilePickerTitle, RTL_TEXTENCODING_UTF8 ).getStr(), + nullptr, + GTK_FILE_CHOOSER_ACTION_OPEN, + GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL, + GTK_STOCK_OPEN, GTK_RESPONSE_ACCEPT, + nullptr ); + + gtk_dialog_set_default_response( GTK_DIALOG (m_pDialog), GTK_RESPONSE_ACCEPT ); + +#if ENABLE_GIO + gtk_file_chooser_set_local_only( GTK_FILE_CHOOSER( m_pDialog ), false ); +#endif + gtk_file_chooser_set_select_multiple( GTK_FILE_CHOOSER( m_pDialog ), false ); + + m_pVBox = gtk_vbox_new( false, 0 ); + + // We don't want clickable items to have a huge hit-area + GtkWidget *pHBox = gtk_hbox_new( false, 0 ); + GtkWidget *pThinVBox = gtk_vbox_new( false, 0 ); + + gtk_box_pack_end (GTK_BOX( m_pVBox ), pHBox, false, false, 0); + gtk_box_pack_start (GTK_BOX( pHBox ), pThinVBox, false, false, 0); + gtk_widget_show( pHBox ); + gtk_widget_show( pThinVBox ); + + OUString aLabel; + + for( i = 0; i < TOGGLE_LAST; i++ ) + { + m_pToggles[i] = gtk_check_button_new(); + +#define LABEL_TOGGLE( elem ) \ + case elem : \ + aLabel = getResString( CHECKBOX_##elem ); \ + setLabel( CHECKBOX_##elem, aLabel ); \ + break + + switch( i ) { + LABEL_TOGGLE( AUTOEXTENSION ); + LABEL_TOGGLE( PASSWORD ); + LABEL_TOGGLE( GPGENCRYPTION ); + LABEL_TOGGLE( FILTEROPTIONS ); + LABEL_TOGGLE( READONLY ); + LABEL_TOGGLE( LINK ); + LABEL_TOGGLE( PREVIEW ); + LABEL_TOGGLE( SELECTION ); + default: + SAL_WARN( "vcl.gtk", "Handle unknown control " << i); + break; + } + + gtk_box_pack_end( GTK_BOX( pThinVBox ), m_pToggles[i], false, false, 0 ); + } + + for( i = 0; i < LIST_LAST; i++ ) + { + m_pHBoxs[i] = gtk_hbox_new( false, 0 ); + + m_pAligns[i] = gtk_alignment_new(0, 0, 0, 1); + + GtkListStore *pListStores[ LIST_LAST ]; + pListStores[i] = gtk_list_store_new (1, G_TYPE_STRING); + m_pLists[i] = gtk_combo_box_new_with_model(GTK_TREE_MODEL(pListStores[i])); + g_object_unref (pListStores[i]); // owned by the widget. + GtkCellRenderer *pCell = gtk_cell_renderer_text_new (); + gtk_cell_layout_pack_start( + GTK_CELL_LAYOUT(m_pLists[i]), pCell, true); + gtk_cell_layout_set_attributes( + GTK_CELL_LAYOUT (m_pLists[i]), pCell, "text", 0, nullptr); + + m_pListLabels[i] = gtk_label_new( "" ); + +#define LABEL_LIST( elem ) \ + case elem : \ + aLabel = getResString( LISTBOX_##elem##_LABEL ); \ + setLabel( LISTBOX_##elem##_LABEL, aLabel ); \ + break + + switch( i ) + { + LABEL_LIST( VERSION ); + LABEL_LIST( TEMPLATE ); + LABEL_LIST( IMAGE_TEMPLATE ); + LABEL_LIST( IMAGE_ANCHOR ); + default: + SAL_WARN( "vcl.gtk", "Handle unknown control " << i); + break; + } + + gtk_container_add( GTK_CONTAINER( m_pAligns[i]), m_pLists[i] ); + gtk_box_pack_end( GTK_BOX( m_pHBoxs[i] ), m_pAligns[i], false, false, 0 ); + gtk_box_pack_end( GTK_BOX( m_pHBoxs[i] ), m_pListLabels[i], false, false, 0 ); + gtk_label_set_mnemonic_widget( GTK_LABEL(m_pListLabels[i]), m_pLists[i] ); + gtk_box_set_spacing( GTK_BOX( m_pHBoxs[i] ), 12 ); + + gtk_box_pack_end( GTK_BOX( m_pVBox ), m_pHBoxs[i], false, false, 0 ); + } + + aLabel = getResString( FILE_PICKER_FILE_TYPE ); + m_pFilterExpander = gtk_expander_new_with_mnemonic( + OUStringToOString( aLabel, RTL_TEXTENCODING_UTF8 ).getStr()); + + gtk_box_pack_end( GTK_BOX( m_pVBox ), m_pFilterExpander, false, true, 0 ); + + GtkWidget *scrolled_window = gtk_scrolled_window_new (nullptr, nullptr); + gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled_window), + GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); + gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (scrolled_window), + GTK_SHADOW_IN); + gtk_container_add (GTK_CONTAINER (m_pFilterExpander), scrolled_window); + gtk_widget_show (scrolled_window); + + m_pFilterStore = gtk_list_store_new (4, G_TYPE_STRING, G_TYPE_STRING, + G_TYPE_STRING, G_TYPE_STRING); + m_pFilterView = gtk_tree_view_new_with_model (GTK_TREE_MODEL(m_pFilterStore)); + gtk_tree_view_set_headers_visible (GTK_TREE_VIEW(m_pFilterView), false); + gtk_tree_view_set_rules_hint (GTK_TREE_VIEW(m_pFilterView), true); + + GtkCellRenderer *cell = nullptr; + + for (i = 0; i < 2; ++i) + { + GtkTreeViewColumn *column = gtk_tree_view_column_new (); + cell = gtk_cell_renderer_text_new (); + gtk_tree_view_column_set_expand (column, true); + gtk_tree_view_column_pack_start (column, cell, false); + gtk_tree_view_column_set_attributes (column, cell, "text", i, nullptr); + gtk_tree_view_append_column (GTK_TREE_VIEW(m_pFilterView), column); + } + + gtk_container_add (GTK_CONTAINER (scrolled_window), m_pFilterView); + gtk_widget_show (m_pFilterView); + + gtk_file_chooser_set_extra_widget( GTK_FILE_CHOOSER( m_pDialog ), m_pVBox ); + + m_pPreview = gtk_image_new(); + gtk_file_chooser_set_preview_widget( GTK_FILE_CHOOSER( m_pDialog ), m_pPreview ); + + g_signal_connect( G_OBJECT( m_pToggles[PREVIEW] ), "toggled", + G_CALLBACK( preview_toggled_cb ), this ); + g_signal_connect (gtk_tree_view_get_selection (GTK_TREE_VIEW(m_pFilterView)), "changed", + G_CALLBACK ( type_changed_cb ), this); + g_signal_connect( G_OBJECT( m_pDialog ), "notify::filter", + G_CALLBACK( filter_changed_cb ), this); + g_signal_connect( G_OBJECT( m_pFilterExpander ), "activate", + G_CALLBACK( expander_changed_cb ), this); + g_signal_connect (G_OBJECT( m_pDialog ), "map", + G_CALLBACK (dialog_mapped_cb), this); + + gtk_widget_show( m_pVBox ); + + PangoLayout *layout = gtk_widget_create_pango_layout (m_pFilterView, nullptr); + guint ypad; + PangoRectangle row_height; + pango_layout_set_markup (layout, "All Files", -1); + pango_layout_get_pixel_extents (layout, nullptr, &row_height); + g_object_unref (layout); + + g_object_get (cell, "ypad", &ypad, nullptr); + guint height = (row_height.height + 2*ypad) * 5; + gtk_widget_set_size_request (m_pFilterView, -1, height); + gtk_widget_set_size_request (m_pPreview, 1, height); + + gtk_file_chooser_set_preview_widget_active( GTK_FILE_CHOOSER( m_pDialog ), true); +} + +// XFilePickerNotifier + +void SAL_CALL SalGtkFilePicker::addFilePickerListener( const uno::Reference<XFilePickerListener>& xListener ) +{ + SolarMutexGuard g; + + OSL_ENSURE(!m_xListener.is(), + "SalGtkFilePicker only talks with one listener at a time..."); + m_xListener = xListener; +} + +void SAL_CALL SalGtkFilePicker::removeFilePickerListener( const uno::Reference<XFilePickerListener>& ) +{ + SolarMutexGuard g; + + m_xListener.clear(); +} + +// FilePicker Event functions + +void SalGtkFilePicker::impl_fileSelectionChanged( const FilePickerEvent& aEvent ) +{ + if (m_xListener.is()) m_xListener->fileSelectionChanged( aEvent ); +} + +void SalGtkFilePicker::impl_directoryChanged( const FilePickerEvent& aEvent ) +{ + if (m_xListener.is()) m_xListener->directoryChanged( aEvent ); +} + +void SalGtkFilePicker::impl_controlStateChanged( const FilePickerEvent& aEvent ) +{ + if (m_xListener.is()) m_xListener->controlStateChanged( aEvent ); +} + +struct FilterEntry +{ +protected: + OUString m_sTitle; + OUString m_sFilter; + + css::uno::Sequence< css::beans::StringPair > m_aSubFilters; + +public: + FilterEntry( const OUString& _rTitle, const OUString& _rFilter ) + :m_sTitle( _rTitle ) + ,m_sFilter( _rFilter ) + { + } + + const OUString& getTitle() const { return m_sTitle; } + const OUString& getFilter() const { return m_sFilter; } + + /// determines if the filter has sub filter (i.e., the filter is a filter group in real) + bool hasSubFilters( ) const; + + /** retrieves the filters belonging to the entry + */ + void getSubFilters( css::uno::Sequence< css::beans::StringPair >& _rSubFilterList ); + + // helpers for iterating the sub filters + const css::beans::StringPair* beginSubFilters() const { return m_aSubFilters.begin(); } + const css::beans::StringPair* endSubFilters() const { return m_aSubFilters.end(); } +}; + +bool FilterEntry::hasSubFilters() const +{ + return m_aSubFilters.hasElements(); +} + +void FilterEntry::getSubFilters( css::uno::Sequence< css::beans::StringPair >& _rSubFilterList ) +{ + _rSubFilterList = m_aSubFilters; +} + +static bool +isFilterString( const OUString &rFilterString, const char *pMatch ) +{ + sal_Int32 nIndex = 0; + OUString aToken; + bool bIsFilter = true; + + OUString aMatch(OUString::createFromAscii(pMatch)); + + do + { + aToken = rFilterString.getToken( 0, ';', nIndex ); + if( !aToken.match( aMatch ) ) + { + bIsFilter = false; + break; + } + } + while( nIndex >= 0 ); + + return bIsFilter; +} + +static OUString +shrinkFilterName( const OUString &rFilterName, bool bAllowNoStar = false ) +{ + int i; + int nBracketLen = -1; + int nBracketEnd = -1; + const sal_Unicode *pStr = rFilterName.getStr(); + OUString aRealName = rFilterName; + + for( i = aRealName.getLength() - 1; i > 0; i-- ) + { + if( pStr[i] == ')' ) + nBracketEnd = i; + else if( pStr[i] == '(' ) + { + nBracketLen = nBracketEnd - i; + if( nBracketEnd <= 0 ) + continue; + if( isFilterString( rFilterName.copy( i + 1, nBracketLen - 1 ), "*." ) ) + aRealName = aRealName.replaceAt( i, nBracketLen + 1, OUString() ); + else if (bAllowNoStar) + { + if( isFilterString( rFilterName.copy( i + 1, nBracketLen - 1 ), ".") ) + aRealName = aRealName.replaceAt( i, nBracketLen + 1, OUString() ); + } + } + } + + return aRealName; +} + +static void +dialog_remove_buttons(GtkWidget *pActionArea) +{ + GtkContainer *pContainer = GTK_CONTAINER( pActionArea ); + + g_return_if_fail( pContainer != nullptr ); + + GList *pChildren = gtk_container_get_children( pContainer ); + + for( GList *p = pChildren; p; p = p->next ) + { + GtkWidget *pWidget = GTK_WIDGET( p->data ); + if ( GTK_IS_BUTTON( pWidget ) ) + gtk_widget_destroy( pWidget ); + } + + g_list_free( pChildren ); +} + +static void +dialog_remove_buttons( GtkDialog *pDialog ) +{ + g_return_if_fail( GTK_IS_DIALOG( pDialog ) ); + + GtkWidget *pHeaderBar = gtk_dialog_get_header_bar(pDialog); + if( pHeaderBar != nullptr ) + dialog_remove_buttons( pHeaderBar ); + else + dialog_remove_buttons(gtk_dialog_get_action_area(pDialog)); +} + +namespace { + + struct FilterTitleMatch + { + protected: + const OUString& rTitle; + + public: + explicit FilterTitleMatch( const OUString& _rTitle ) : rTitle( _rTitle ) { } + + bool operator () ( const FilterEntry& _rEntry ) + { + bool bMatch; + if( !_rEntry.hasSubFilters() ) + // a real filter + bMatch = (_rEntry.getTitle() == rTitle) + || (shrinkFilterName(_rEntry.getTitle()) == rTitle); + else + // a filter group -> search the sub filters + bMatch = + ::std::any_of( + _rEntry.beginSubFilters(), + _rEntry.endSubFilters(), + *this + ); + + return bMatch; + } + bool operator () ( const css::beans::StringPair& _rEntry ) + { + OUString aShrunkName = shrinkFilterName( _rEntry.First ); + return aShrunkName == rTitle; + } + }; +} + +bool SalGtkFilePicker::FilterNameExists( const OUString& rTitle ) +{ + bool bRet = false; + + if( m_pFilterVector ) + bRet = + ::std::any_of( + m_pFilterVector->begin(), + m_pFilterVector->end(), + FilterTitleMatch( rTitle ) + ); + + return bRet; +} + +bool SalGtkFilePicker::FilterNameExists( const css::uno::Sequence< css::beans::StringPair >& _rGroupedFilters ) +{ + bool bRet = false; + + if( m_pFilterVector ) + { + bRet = std::any_of(_rGroupedFilters.begin(), _rGroupedFilters.end(), + [&](const css::beans::StringPair& rFilter) { + return ::std::any_of( m_pFilterVector->begin(), m_pFilterVector->end(), FilterTitleMatch( rFilter.First ) ); }); + } + + return bRet; +} + +void SalGtkFilePicker::ensureFilterVector( const OUString& _rInitialCurrentFilter ) +{ + if( !m_pFilterVector ) + { + m_pFilterVector.reset( new std::vector<FilterEntry> ); + + // set the first filter to the current filter + if ( m_aCurrentFilter.isEmpty() ) + m_aCurrentFilter = _rInitialCurrentFilter; + } +} + +void SAL_CALL SalGtkFilePicker::appendFilter( const OUString& aTitle, const OUString& aFilter ) +{ + SolarMutexGuard g; + + OSL_ASSERT( m_pDialog != nullptr ); + + if( FilterNameExists( aTitle ) ) + throw IllegalArgumentException(); + + // ensure that we have a filter list + ensureFilterVector( aTitle ); + + // append the filter + m_pFilterVector->insert( m_pFilterVector->end(), FilterEntry( aTitle, aFilter ) ); +} + +void SAL_CALL SalGtkFilePicker::setCurrentFilter( const OUString& aTitle ) +{ + SolarMutexGuard g; + + OSL_ASSERT( m_pDialog != nullptr ); + + if( aTitle != m_aCurrentFilter ) + { + m_aCurrentFilter = aTitle; + SetCurFilter( m_aCurrentFilter ); + } + + // TODO m_pImpl->setCurrentFilter( aTitle ); +} + +void SalGtkFilePicker::updateCurrentFilterFromName(const gchar* filtername) +{ + OUString aFilterName(filtername, strlen(filtername), RTL_TEXTENCODING_UTF8); + if (m_pFilterVector) + { + for (auto const& filter : *m_pFilterVector) + { + if (aFilterName == shrinkFilterName(filter.getTitle())) + { + m_aCurrentFilter = filter.getTitle(); + break; + } + } + } +} + +void SalGtkFilePicker::UpdateFilterfromUI() +{ + // Update the filtername from the users selection if they have had a chance to do so. + // If the user explicitly sets a type then use that, if not then take the implicit type + // from the filter of the files glob on which he is currently searching + if (!mnHID_FolderChange || !mnHID_SelectionChange) + return; + + GtkTreeSelection* selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(m_pFilterView)); + GtkTreeIter iter; + GtkTreeModel *model; + if (gtk_tree_selection_get_selected (selection, &model, &iter)) + { + gchar *title; + gtk_tree_model_get (model, &iter, 2, &title, -1); + updateCurrentFilterFromName(title); + g_free (title); + } + else if( GtkFileFilter *filter = gtk_file_chooser_get_filter(GTK_FILE_CHOOSER(m_pDialog))) + { + if (m_pPseudoFilter != filter) + updateCurrentFilterFromName(gtk_file_filter_get_name( filter )); + else + updateCurrentFilterFromName(OUStringToOString( m_aInitialFilter, RTL_TEXTENCODING_UTF8 ).getStr()); + } +} + +OUString SAL_CALL SalGtkFilePicker::getCurrentFilter() +{ + SolarMutexGuard g; + + OSL_ASSERT( m_pDialog != nullptr ); + + UpdateFilterfromUI(); + + return m_aCurrentFilter; +} + +// XFilterGroupManager functions + +void SAL_CALL SalGtkFilePicker::appendFilterGroup( const OUString& /*sGroupTitle*/, const uno::Sequence<beans::StringPair>& aFilters ) +{ + SolarMutexGuard g; + + OSL_ASSERT( m_pDialog != nullptr ); + + // TODO m_pImpl->appendFilterGroup( sGroupTitle, aFilters ); + // check the names + if( FilterNameExists( aFilters ) ) + // TODO: a more precise exception message + throw IllegalArgumentException(); + + // ensure that we have a filter list + OUString sInitialCurrentFilter; + if( aFilters.hasElements() ) + sInitialCurrentFilter = aFilters[0].First; + + ensureFilterVector( sInitialCurrentFilter ); + + // append the filter + for( const auto& rSubFilter : aFilters ) + m_pFilterVector->insert( m_pFilterVector->end(), FilterEntry( rSubFilter.First, rSubFilter.Second ) ); + +} + +// XFilePicker functions + +void SAL_CALL SalGtkFilePicker::setMultiSelectionMode( sal_Bool bMode ) +{ + SolarMutexGuard g; + + OSL_ASSERT( m_pDialog != nullptr ); + + gtk_file_chooser_set_select_multiple( GTK_FILE_CHOOSER(m_pDialog), bMode ); +} + +void SAL_CALL SalGtkFilePicker::setDefaultName( const OUString& aName ) +{ + SolarMutexGuard g; + + OSL_ASSERT( m_pDialog != nullptr ); + + OString aStr = OUStringToOString( aName, RTL_TEXTENCODING_UTF8 ); + GtkFileChooserAction eAction = gtk_file_chooser_get_action( GTK_FILE_CHOOSER( m_pDialog ) ); + + // set_current_name launches a Gtk critical error if called for other than save + if( GTK_FILE_CHOOSER_ACTION_SAVE == eAction ) + gtk_file_chooser_set_current_name( GTK_FILE_CHOOSER( m_pDialog ), aStr.getStr() ); +} + +void SAL_CALL SalGtkFilePicker::setDisplayDirectory( const OUString& rDirectory ) +{ + SolarMutexGuard g; + + implsetDisplayDirectory(rDirectory); +} + +OUString SAL_CALL SalGtkFilePicker::getDisplayDirectory() +{ + SolarMutexGuard g; + + return implgetDisplayDirectory(); +} + +uno::Sequence<OUString> SAL_CALL SalGtkFilePicker::getFiles() +{ + // no member access => no mutex needed + + uno::Sequence< OUString > aFiles = getSelectedFiles(); + /* + The previous multiselection API design was completely broken + and unimplementable for some heterogeneous pseudo-URIs eg. search: + Thus crop unconditionally to a single selection. + */ + aFiles.realloc (1); + return aFiles; +} + +namespace +{ + +bool lcl_matchFilter( const OUString& rFilter, const OUString& rExt ) +{ + const sal_Unicode cSep {';'}; + sal_Int32 nIdx {0}; + + for (;;) + { + const sal_Int32 nBegin = rFilter.indexOf(rExt, nIdx); + + if (nBegin<0) // not found + break; + + // Let nIdx point to end of matched string, useful in order to + // check string boundaries and also for a possible next iteration + nIdx = nBegin + rExt.getLength(); + + // Check if the found occurrence is an exact match: right side + if (nIdx<rFilter.getLength() && rFilter[nIdx]!=cSep) + continue; + + // Check if the found occurrence is an exact match: left side + if (nBegin>0 && rFilter[nBegin-1]!=cSep) + continue; + + return true; + } + + return false; +} + +} + +uno::Sequence<OUString> SAL_CALL SalGtkFilePicker::getSelectedFiles() +{ + SolarMutexGuard g; + + OSL_ASSERT( m_pDialog != nullptr ); + + GSList* pPathList = gtk_file_chooser_get_uris( GTK_FILE_CHOOSER(m_pDialog) ); + + int nCount = g_slist_length( pPathList ); + int nIndex = 0; + + // get the current action setting + GtkFileChooserAction eAction = gtk_file_chooser_get_action( + GTK_FILE_CHOOSER( m_pDialog )); + + uno::Sequence< OUString > aSelectedFiles(nCount); + + // Convert to OOo + for( GSList *pElem = pPathList; pElem; pElem = pElem->next) + { + gchar *pURI = static_cast<gchar*>(pElem->data); + aSelectedFiles[ nIndex ] = uritounicode(pURI); + + if( GTK_FILE_CHOOSER_ACTION_SAVE == eAction ) + { + OUString sFilterName; + sal_Int32 nTokenIndex = 0; + bool bExtensionTypedIn = false; + + GtkTreeSelection* selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(m_pFilterView)); + GtkTreeIter iter; + GtkTreeModel *model; + if (gtk_tree_selection_get_selected (selection, &model, &iter)) + { + gchar *title = nullptr; + gtk_tree_model_get (model, &iter, 2, &title, -1); + if (title) + sFilterName = OUString( title, strlen( title ), RTL_TEXTENCODING_UTF8 ); + else + sFilterName = OUString(); + g_free (title); + } + else + { + if( aSelectedFiles[nIndex].indexOf('.') > 0 ) + { + OUString sExtension; + nTokenIndex = 0; + do + sExtension = aSelectedFiles[nIndex].getToken( 0, '.', nTokenIndex ); + while( nTokenIndex >= 0 ); + + if( sExtension.getLength() >= 3 ) // 3 = typical/minimum extension length + { + OUString aNewFilter; + OUString aOldFilter = getCurrentFilter(); + bool bChangeFilter = true; + if ( m_pFilterVector) + for (auto const& filter : *m_pFilterVector) + { + if( lcl_matchFilter( filter.getFilter(), "*." + sExtension ) ) + { + if( aNewFilter.isEmpty() ) + aNewFilter = filter.getTitle(); + + if( aOldFilter == filter.getTitle() ) + bChangeFilter = false; + + bExtensionTypedIn = true; + } + } + if( bChangeFilter && bExtensionTypedIn ) + { + gchar* pCurrentName = gtk_file_chooser_get_current_name(GTK_FILE_CHOOSER(m_pDialog)); + setCurrentFilter( aNewFilter ); + gtk_file_chooser_set_current_name(GTK_FILE_CHOOSER(m_pDialog), pCurrentName); + g_free(pCurrentName); + } + } + } + + GtkFileFilter *filter = gtk_file_chooser_get_filter(GTK_FILE_CHOOSER(m_pDialog)); + if (m_pPseudoFilter != filter) + { + const gchar* filtername = filter ? gtk_file_filter_get_name(filter) : nullptr; + if (filtername) + sFilterName = OUString(filtername, strlen( filtername ), RTL_TEXTENCODING_UTF8); + else + sFilterName.clear(); + } + else + sFilterName = m_aInitialFilter; + } + + if (m_pFilterVector) + { + auto aVectorIter = ::std::find_if( + m_pFilterVector->begin(), m_pFilterVector->end(), FilterTitleMatch(sFilterName) ); + + OUString aFilter; + if (aVectorIter != m_pFilterVector->end()) + aFilter = aVectorIter->getFilter(); + + nTokenIndex = 0; + OUString sToken; + do + { + sToken = aFilter.getToken( 0, '.', nTokenIndex ); + + if ( sToken.lastIndexOf( ';' ) != -1 ) + { + sToken = sToken.getToken(0, ';'); + break; + } + } + while( nTokenIndex >= 0 ); + + if( !bExtensionTypedIn && ( sToken != "*" ) ) + { + //if the filename does not already have the auto extension, stick it on + OUString sExtension = "." + sToken; + OUString &rBase = aSelectedFiles[nIndex]; + sal_Int32 nExtensionIdx = rBase.getLength() - sExtension.getLength(); + SAL_INFO( + "vcl.gtk", + "idx are " << rBase.lastIndexOf(sExtension) << " " + << nExtensionIdx); + + if( rBase.lastIndexOf( sExtension ) != nExtensionIdx ) + rBase += sExtension; + } + } + + } + + nIndex++; + g_free( pURI ); + } + + g_slist_free( pPathList ); + + return aSelectedFiles; +} + +// XExecutableDialog functions + +void SAL_CALL SalGtkFilePicker::setTitle( const OUString& rTitle ) +{ + SolarMutexGuard g; + + implsetTitle(rTitle); +} + +sal_Int16 SAL_CALL SalGtkFilePicker::execute() +{ + SolarMutexGuard g; + + OSL_ASSERT( m_pDialog != nullptr ); + + sal_Int16 retVal = 0; + + SetFilters(); + + // tdf#84431 - set the filter after the corresponding widget is created + if ( !m_aCurrentFilter.isEmpty() ) + SetCurFilter(m_aCurrentFilter); + + mnHID_FolderChange = + g_signal_connect( GTK_FILE_CHOOSER( m_pDialog ), "current-folder-changed", + G_CALLBACK( folder_changed_cb ), static_cast<gpointer>(this) ); + + mnHID_SelectionChange = + g_signal_connect( GTK_FILE_CHOOSER( m_pDialog ), "selection-changed", + G_CALLBACK( selection_changed_cb ), static_cast<gpointer>(this) ); + + int btn = GTK_RESPONSE_NO; + + uno::Reference< awt::XExtendedToolkit > xToolkit( + awt::Toolkit::create(m_xContext), + UNO_QUERY_THROW ); + + uno::Reference< frame::XDesktop > xDesktop( + frame::Desktop::create(m_xContext), + UNO_QUERY_THROW ); + + GtkWindow *pParent = GTK_WINDOW(m_pParentWidget); + if (!pParent) + { + SAL_WARN( "vcl.gtk", "no parent widget set"); + pParent = RunDialog::GetTransientFor(); + } + if (pParent) + gtk_window_set_transient_for(GTK_WINDOW(m_pDialog), pParent); + RunDialog* pRunDialog = new RunDialog(m_pDialog, xToolkit, xDesktop); + uno::Reference < awt::XTopWindowListener > xLifeCycle(pRunDialog); + while( GTK_RESPONSE_NO == btn ) + { + btn = GTK_RESPONSE_YES; // we don't want to repeat unless user clicks NO for file save. + + gint nStatus = pRunDialog->run(); + switch( nStatus ) + { + case GTK_RESPONSE_ACCEPT: + if( GTK_FILE_CHOOSER_ACTION_SAVE == gtk_file_chooser_get_action( GTK_FILE_CHOOSER( m_pDialog ) ) ) + { + Sequence < OUString > aPathSeq = getFiles(); + if( aPathSeq.getLength() == 1 ) + { + OString sFileName = unicodetouri( aPathSeq[0] ); + gchar *gFileName = g_filename_from_uri ( sFileName.getStr(), nullptr, nullptr ); + if( g_file_test( gFileName, G_FILE_TEST_IS_REGULAR ) ) + { + INetURLObject aFileObj( OStringToOUString(sFileName, RTL_TEXTENCODING_UTF8) ); + + OString baseName( + OUStringToOString( + aFileObj.getName( + INetURLObject::LAST_SEGMENT, + true, + INetURLObject::DecodeMechanism::WithCharset + ), + RTL_TEXTENCODING_UTF8 + ) + ); + OString aMsg( + OUStringToOString( + getResString( FILE_PICKER_OVERWRITE_PRIMARY ), + RTL_TEXTENCODING_UTF8 + ) + ); + OString toReplace("$filename$"); + + aMsg = aMsg.replaceAt( + aMsg.indexOf( toReplace ), + toReplace.getLength(), + baseName + ); + + GtkWidget *dlg = gtk_message_dialog_new( nullptr, + GTK_DIALOG_MODAL, + GTK_MESSAGE_QUESTION, + GTK_BUTTONS_YES_NO, + "%s", + aMsg.getStr() + ); + + sal_Int32 nSegmentCount = aFileObj.getSegmentCount(); + if (nSegmentCount >= 2) + { + OString dirName( + OUStringToOString( + aFileObj.getName( + nSegmentCount-2, + true, + INetURLObject::DecodeMechanism::WithCharset + ), + RTL_TEXTENCODING_UTF8 + ) + ); + + aMsg = + OUStringToOString( + getResString( FILE_PICKER_OVERWRITE_SECONDARY ), + RTL_TEXTENCODING_UTF8 + ); + + toReplace = "$dirname$"; + + aMsg = aMsg.replaceAt( + aMsg.indexOf( toReplace ), + toReplace.getLength(), + dirName + ); + + gtk_message_dialog_format_secondary_text( GTK_MESSAGE_DIALOG( dlg ), "%s", aMsg.getStr() ); + } + + gtk_window_set_title( GTK_WINDOW( dlg ), + OUStringToOString(getResString(FILE_PICKER_TITLE_SAVE ), + RTL_TEXTENCODING_UTF8 ).getStr() ); + gtk_window_set_transient_for(GTK_WINDOW(dlg), GTK_WINDOW(m_pDialog)); + RunDialog* pAnotherDialog = new RunDialog(dlg, xToolkit, xDesktop); + uno::Reference < awt::XTopWindowListener > xAnotherLifeCycle(pAnotherDialog); + btn = pAnotherDialog->run(); + + gtk_widget_destroy( dlg ); + } + g_free (gFileName); + + if( btn == GTK_RESPONSE_YES ) + retVal = ExecutableDialogResults::OK; + } + } + else + retVal = ExecutableDialogResults::OK; + break; + + case GTK_RESPONSE_CANCEL: + retVal = ExecutableDialogResults::CANCEL; + break; + + case 1: //PLAY + { + FilePickerEvent evt; + evt.ElementId = PUSHBUTTON_PLAY; + impl_controlStateChanged( evt ); + btn = GTK_RESPONSE_NO; + } + break; + + default: + retVal = 0; + break; + } + } + gtk_widget_hide(m_pDialog); + + if (mnHID_FolderChange) + g_signal_handler_disconnect(GTK_FILE_CHOOSER( m_pDialog ), mnHID_FolderChange); + if (mnHID_SelectionChange) + g_signal_handler_disconnect(GTK_FILE_CHOOSER( m_pDialog ), mnHID_SelectionChange); + + return retVal; +} + +// cf. offapi/com/sun/star/ui/dialogs/ExtendedFilePickerElementIds.idl +GtkWidget *SalGtkFilePicker::getWidget( sal_Int16 nControlId, GType *pType ) +{ + GType tType = GTK_TYPE_TOGGLE_BUTTON; //prevent warning by initializing + GtkWidget *pWidget = nullptr; + +#define MAP_TOGGLE( elem ) \ + case ExtendedFilePickerElementIds::CHECKBOX_##elem: \ + pWidget = m_pToggles[elem]; tType = GTK_TYPE_TOGGLE_BUTTON; \ + break +#define MAP_BUTTON( elem ) \ + case ExtendedFilePickerElementIds::PUSHBUTTON_##elem: \ + pWidget = m_pButtons[elem]; tType = GTK_TYPE_BUTTON; \ + break +#define MAP_LIST( elem ) \ + case ExtendedFilePickerElementIds::LISTBOX_##elem: \ + pWidget = m_pLists[elem]; tType = GTK_TYPE_COMBO_BOX; \ + break +#define MAP_LIST_LABEL( elem ) \ + case ExtendedFilePickerElementIds::LISTBOX_##elem##_LABEL: \ + pWidget = m_pListLabels[elem]; tType = GTK_TYPE_LABEL; \ + break + + switch( nControlId ) + { + MAP_TOGGLE( AUTOEXTENSION ); + MAP_TOGGLE( PASSWORD ); + MAP_TOGGLE( GPGENCRYPTION ); + MAP_TOGGLE( FILTEROPTIONS ); + MAP_TOGGLE( READONLY ); + MAP_TOGGLE( LINK ); + MAP_TOGGLE( PREVIEW ); + MAP_TOGGLE( SELECTION ); + MAP_BUTTON( PLAY ); + MAP_LIST( VERSION ); + MAP_LIST( TEMPLATE ); + MAP_LIST( IMAGE_TEMPLATE ); + MAP_LIST( IMAGE_ANCHOR ); + MAP_LIST_LABEL( VERSION ); + MAP_LIST_LABEL( TEMPLATE ); + MAP_LIST_LABEL( IMAGE_TEMPLATE ); + MAP_LIST_LABEL( IMAGE_ANCHOR ); + default: + SAL_WARN( "vcl.gtk", "Handle unknown control " << nControlId); + break; + } +#undef MAP + + if( pType ) + *pType = tType; + return pWidget; +} + +// XFilePickerControlAccess functions + +static void HackWidthToFirst(GtkComboBox *pWidget) +{ + GtkRequisition requisition; + gtk_widget_size_request(GTK_WIDGET(pWidget), &requisition); + gtk_widget_set_size_request(GTK_WIDGET(pWidget), requisition.width, -1); +} + +static void ComboBoxAppendText(GtkComboBox *pCombo, const OUString &rStr) +{ + GtkTreeIter aIter; + GtkListStore *pStore = GTK_LIST_STORE(gtk_combo_box_get_model(pCombo)); + OString aStr = OUStringToOString(rStr, RTL_TEXTENCODING_UTF8); + gtk_list_store_append(pStore, &aIter); + gtk_list_store_set(pStore, &aIter, 0, aStr.getStr(), -1); +} + +void SalGtkFilePicker::HandleSetListValue(GtkComboBox *pWidget, sal_Int16 nControlAction, const uno::Any& rValue) +{ + switch (nControlAction) + { + case ControlActions::ADD_ITEM: + { + OUString sItem; + rValue >>= sItem; + ComboBoxAppendText(pWidget, sItem); + if (!bVersionWidthUnset) + { + HackWidthToFirst(pWidget); + bVersionWidthUnset = true; + } + } + break; + case ControlActions::ADD_ITEMS: + { + Sequence< OUString > aStringList; + rValue >>= aStringList; + for (const auto& rString : std::as_const(aStringList)) + { + ComboBoxAppendText(pWidget, rString); + if (!bVersionWidthUnset) + { + HackWidthToFirst(pWidget); + bVersionWidthUnset = true; + } + } + } + break; + case ControlActions::DELETE_ITEM: + { + sal_Int32 nPos=0; + rValue >>= nPos; + + GtkTreeIter aIter; + GtkListStore *pStore = GTK_LIST_STORE( + gtk_combo_box_get_model(GTK_COMBO_BOX(pWidget))); + if(gtk_tree_model_iter_nth_child(GTK_TREE_MODEL(pStore), &aIter, nullptr, nPos)) + gtk_list_store_remove(pStore, &aIter); + } + break; + case ControlActions::DELETE_ITEMS: + { + gtk_combo_box_set_active(pWidget, -1); + GtkListStore *pStore = GTK_LIST_STORE( + gtk_combo_box_get_model(GTK_COMBO_BOX(pWidget))); + gtk_list_store_clear(pStore); + } + break; + case ControlActions::SET_SELECT_ITEM: + { + sal_Int32 nPos=0; + rValue >>= nPos; + gtk_combo_box_set_active(pWidget, nPos); + } + break; + default: + SAL_WARN( "vcl.gtk", "undocumented/unimplemented ControlAction for a list " << nControlAction); + break; + } + + //I think its best to make it insensitive unless there is the chance to + //actually select something from the list. + gint nItems = gtk_tree_model_iter_n_children( + gtk_combo_box_get_model(pWidget), nullptr); + gtk_widget_set_sensitive(GTK_WIDGET(pWidget), nItems > 1); +} + +uno::Any SalGtkFilePicker::HandleGetListValue(GtkComboBox *pWidget, sal_Int16 nControlAction) +{ + uno::Any aAny; + switch (nControlAction) + { + case ControlActions::GET_ITEMS: + { + Sequence< OUString > aItemList; + + GtkTreeModel *pTree = gtk_combo_box_get_model(pWidget); + GtkTreeIter iter; + if (gtk_tree_model_get_iter_first(pTree, &iter)) + { + sal_Int32 nSize = gtk_tree_model_iter_n_children( + pTree, nullptr); + + aItemList.realloc(nSize); + for (sal_Int32 i=0; i < nSize; ++i) + { + gchar *item; + gtk_tree_model_get(gtk_combo_box_get_model(pWidget), + &iter, 0, &item, -1); + aItemList[i] = OUString(item, strlen(item), RTL_TEXTENCODING_UTF8); + g_free(item); + (void)gtk_tree_model_iter_next(pTree, &iter); + } + } + aAny <<= aItemList; + } + break; + case ControlActions::GET_SELECTED_ITEM: + { + GtkTreeIter iter; + if (gtk_combo_box_get_active_iter(pWidget, &iter)) + { + gchar *item; + gtk_tree_model_get(gtk_combo_box_get_model(pWidget), + &iter, 0, &item, -1); + OUString sItem(item, strlen(item), RTL_TEXTENCODING_UTF8); + aAny <<= sItem; + g_free(item); + } + } + break; + case ControlActions::GET_SELECTED_ITEM_INDEX: + { + gint nActive = gtk_combo_box_get_active(pWidget); + aAny <<= static_cast< sal_Int32 >(nActive); + } + break; + default: + SAL_WARN( "vcl.gtk", "undocumented/unimplemented ControlAction for a list " << nControlAction); + break; + } + return aAny; +} + +void SAL_CALL SalGtkFilePicker::setValue( sal_Int16 nControlId, sal_Int16 nControlAction, const uno::Any& rValue ) +{ + SolarMutexGuard g; + + OSL_ASSERT( m_pDialog != nullptr ); + + GType tType; + GtkWidget *pWidget; + + if( !( pWidget = getWidget( nControlId, &tType ) ) ) + SAL_WARN( "vcl.gtk", "enable unknown control " << nControlId); + else if( tType == GTK_TYPE_TOGGLE_BUTTON ) + { + bool bChecked = false; + rValue >>= bChecked; + gtk_toggle_button_set_active( GTK_TOGGLE_BUTTON( pWidget ), bChecked ); + } + else if( tType == GTK_TYPE_COMBO_BOX ) + HandleSetListValue(GTK_COMBO_BOX(pWidget), nControlAction, rValue); + else + { + SAL_WARN( "vcl.gtk", "Can't set value on button / list " << nControlId << " " << nControlAction ); + } +} + +uno::Any SAL_CALL SalGtkFilePicker::getValue( sal_Int16 nControlId, sal_Int16 nControlAction ) +{ + SolarMutexGuard g; + + OSL_ASSERT( m_pDialog != nullptr ); + + uno::Any aRetval; + + GType tType; + GtkWidget *pWidget; + + if( !( pWidget = getWidget( nControlId, &tType ) ) ) + SAL_WARN( "vcl.gtk", "enable unknown control " << nControlId); + else if( tType == GTK_TYPE_TOGGLE_BUTTON ) + aRetval <<= bool( gtk_toggle_button_get_active( GTK_TOGGLE_BUTTON( pWidget ) ) ); + else if( tType == GTK_TYPE_COMBO_BOX ) + aRetval = HandleGetListValue(GTK_COMBO_BOX(pWidget), nControlAction); + else + SAL_WARN( "vcl.gtk", "Can't get value on button / list " << nControlId << " " << nControlAction ); + + return aRetval; +} + +void SAL_CALL SalGtkFilePicker::enableControl( sal_Int16 nControlId, sal_Bool bEnable ) +{ + SolarMutexGuard g; + + OSL_ASSERT( m_pDialog != nullptr ); + + GtkWidget *pWidget; + + if ( ( pWidget = getWidget( nControlId ) ) ) + { + if( bEnable ) + { + gtk_widget_set_sensitive( pWidget, true ); + } + else + { + gtk_widget_set_sensitive( pWidget, false ); + } + } + else + SAL_WARN( "vcl.gtk", "enable unknown control " << nControlId ); +} + +void SAL_CALL SalGtkFilePicker::setLabel( sal_Int16 nControlId, const OUString& rLabel ) +{ + SolarMutexGuard g; + + OSL_ASSERT( m_pDialog != nullptr ); + + GType tType; + GtkWidget *pWidget; + + if( !( pWidget = getWidget( nControlId, &tType ) ) ) + { + SAL_WARN( "vcl.gtk", "Set label on unknown control " << nControlId); + return; + } + + OString aTxt = OUStringToOString( rLabel.replace('~', '_'), RTL_TEXTENCODING_UTF8 ); + if (nControlId == ExtendedFilePickerElementIds::PUSHBUTTON_PLAY) + { +#ifdef GTK_STOCK_MEDIA_PLAY + if (msPlayLabel.isEmpty()) + msPlayLabel = rLabel; + if (msPlayLabel == rLabel) + gtk_button_set_label(GTK_BUTTON(pWidget), GTK_STOCK_MEDIA_PLAY); + else + gtk_button_set_label(GTK_BUTTON(pWidget), GTK_STOCK_MEDIA_STOP); +#else + gtk_button_set_label(GTK_BUTTON(pWidget), aTxt.getStr()); +#endif + } + else if( tType == GTK_TYPE_TOGGLE_BUTTON || tType == GTK_TYPE_BUTTON || tType == GTK_TYPE_LABEL ) + g_object_set( pWidget, "label", aTxt.getStr(), + "use_underline", true, nullptr ); + else + SAL_WARN( "vcl.gtk", "Can't set label on list"); +} + +OUString SAL_CALL SalGtkFilePicker::getLabel( sal_Int16 nControlId ) +{ + SolarMutexGuard g; + + OSL_ASSERT( m_pDialog != nullptr ); + + GType tType; + OString aTxt; + GtkWidget *pWidget; + + if( !( pWidget = getWidget( nControlId, &tType ) ) ) + SAL_WARN( "vcl.gtk", "Get label on unknown control " << nControlId); + else if( tType == GTK_TYPE_TOGGLE_BUTTON || tType == GTK_TYPE_BUTTON || tType == GTK_TYPE_LABEL ) + aTxt = gtk_button_get_label( GTK_BUTTON( pWidget ) ); + else + SAL_WARN( "vcl.gtk", "Can't get label on list"); + + return OStringToOUString( aTxt, RTL_TEXTENCODING_UTF8 ); +} + +// XFilePreview functions + +uno::Sequence<sal_Int16> SAL_CALL SalGtkFilePicker::getSupportedImageFormats() +{ + SolarMutexGuard g; + + OSL_ASSERT( m_pDialog != nullptr ); + + // TODO return m_pImpl->getSupportedImageFormats(); + return uno::Sequence<sal_Int16>(); +} + +sal_Int32 SAL_CALL SalGtkFilePicker::getTargetColorDepth() +{ + SolarMutexGuard g; + + OSL_ASSERT( m_pDialog != nullptr ); + + // TODO return m_pImpl->getTargetColorDepth(); + return 0; +} + +sal_Int32 SAL_CALL SalGtkFilePicker::getAvailableWidth() +{ + SolarMutexGuard g; + + OSL_ASSERT( m_pDialog != nullptr ); + + return g_PreviewImageWidth; +} + +sal_Int32 SAL_CALL SalGtkFilePicker::getAvailableHeight() +{ + SolarMutexGuard g; + + OSL_ASSERT( m_pDialog != nullptr ); + + return g_PreviewImageHeight; +} + +void SAL_CALL SalGtkFilePicker::setImage( sal_Int16 /*aImageFormat*/, const uno::Any& /*aImage*/ ) +{ + SolarMutexGuard g; + + OSL_ASSERT( m_pDialog != nullptr ); + + // TODO m_pImpl->setImage( aImageFormat, aImage ); +} + +void SalGtkFilePicker::implChangeType( GtkTreeSelection *selection ) +{ + OUString aLabel = getResString( FILE_PICKER_FILE_TYPE ); + + GtkTreeIter iter; + GtkTreeModel *model; + if (gtk_tree_selection_get_selected (selection, &model, &iter)) + { + gchar *title; + gtk_tree_model_get (model, &iter, 2, &title, -1); + aLabel += ": " + + OUString( title, strlen(title), RTL_TEXTENCODING_UTF8 ); + g_free (title); + } + gtk_expander_set_label (GTK_EXPANDER (m_pFilterExpander), + OUStringToOString( aLabel, RTL_TEXTENCODING_UTF8 ).getStr()); + FilePickerEvent evt; + evt.ElementId = LISTBOX_FILTER; + impl_controlStateChanged( evt ); +} + +void SalGtkFilePicker::type_changed_cb( GtkTreeSelection *selection, SalGtkFilePicker *pobjFP ) +{ + pobjFP->implChangeType(selection); +} + +void SalGtkFilePicker::unselect_type() +{ + gtk_tree_selection_unselect_all(gtk_tree_view_get_selection(GTK_TREE_VIEW(m_pFilterView))); +} + +void SalGtkFilePicker::expander_changed_cb( GtkExpander *expander, SalGtkFilePicker *pobjFP ) +{ + if (gtk_expander_get_expanded(expander)) + pobjFP->unselect_type(); +} + +void SalGtkFilePicker::filter_changed_cb( GtkFileChooser *, GParamSpec *, + SalGtkFilePicker *pobjFP ) +{ + FilePickerEvent evt; + evt.ElementId = LISTBOX_FILTER; + SAL_INFO( "vcl.gtk", "filter_changed, isn't it great " << pobjFP ); + pobjFP->impl_controlStateChanged( evt ); +} + +void SalGtkFilePicker::folder_changed_cb( GtkFileChooser *, SalGtkFilePicker *pobjFP ) +{ + FilePickerEvent evt; + SAL_INFO( "vcl.gtk", "folder_changed, isn't it great " << pobjFP ); + pobjFP->impl_directoryChanged( evt ); +} + +void SalGtkFilePicker::selection_changed_cb( GtkFileChooser *, SalGtkFilePicker *pobjFP ) +{ + FilePickerEvent evt; + SAL_INFO( "vcl.gtk", "selection_changed, isn't it great " << pobjFP ); + pobjFP->impl_fileSelectionChanged( evt ); +} + +void SalGtkFilePicker::update_preview_cb( GtkFileChooser *file_chooser, SalGtkFilePicker* pobjFP ) +{ + bool have_preview = false; + + GtkWidget* preview = pobjFP->m_pPreview; + char* filename = gtk_file_chooser_get_preview_filename( file_chooser ); + + if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(pobjFP->m_pToggles[PREVIEW])) && filename && g_file_test(filename, G_FILE_TEST_IS_REGULAR)) + { + GdkPixbuf *pixbuf = gdk_pixbuf_new_from_file_at_size( + filename, + g_PreviewImageWidth, + g_PreviewImageHeight, nullptr ); + + have_preview = ( pixbuf != nullptr ); + + gtk_image_set_from_pixbuf( GTK_IMAGE( preview ), pixbuf ); + if( pixbuf ) + g_object_unref( G_OBJECT( pixbuf ) ); + + } + + gtk_file_chooser_set_preview_widget_active( file_chooser, have_preview ); + + if( filename ) + g_free( filename ); +} + +sal_Bool SAL_CALL SalGtkFilePicker::setShowState( sal_Bool bShowState ) +{ + SolarMutexGuard g; + + OSL_ASSERT( m_pDialog != nullptr ); + + // TODO return m_pImpl->setShowState( bShowState ); + if( bool(bShowState) != mbPreviewState ) + { + if( bShowState ) + { + // Show + if( !mHID_Preview ) + { + mHID_Preview = g_signal_connect( + GTK_FILE_CHOOSER( m_pDialog ), "update-preview", + G_CALLBACK( update_preview_cb ), static_cast<gpointer>(this) ); + } + gtk_widget_show( m_pPreview ); + } + else + { + // Hide + gtk_widget_hide( m_pPreview ); + } + + // also emit the signal + g_signal_emit_by_name( G_OBJECT( m_pDialog ), "update-preview" ); + + mbPreviewState = bShowState; + } + return true; +} + +sal_Bool SAL_CALL SalGtkFilePicker::getShowState() +{ + SolarMutexGuard g; + + OSL_ASSERT( m_pDialog != nullptr ); + + return mbPreviewState; +} + +// XInitialization + +void SAL_CALL SalGtkFilePicker::initialize( const uno::Sequence<uno::Any>& aArguments ) +{ + // parameter checking + uno::Any aAny; + if( !aArguments.hasElements() ) + throw lang::IllegalArgumentException( + "no arguments", + static_cast<XFilePicker2*>( this ), 1 ); + + aAny = aArguments[0]; + + if( ( aAny.getValueType() != cppu::UnoType<sal_Int16>::get()) && + (aAny.getValueType() != cppu::UnoType<sal_Int8>::get()) ) + throw lang::IllegalArgumentException( + "invalid argument type", + static_cast<XFilePicker2*>( this ), 1 ); + + sal_Int16 templateId = -1; + aAny >>= templateId; + + css::uno::Reference<css::awt::XWindow> xParentWindow; + if (aArguments.getLength() > 1) + { + aArguments[1] >>= xParentWindow; + } + + if (xParentWindow.is()) + { + if (SalGtkXWindow* pGtkXWindow = dynamic_cast<SalGtkXWindow*>(xParentWindow.get())) + m_pParentWidget = pGtkXWindow->getGtkWidget(); + else + { + css::uno::Reference<css::awt::XSystemDependentWindowPeer> xSysDepWin(xParentWindow, css::uno::UNO_QUERY); + if (xSysDepWin.is()) + { + css::uno::Sequence<sal_Int8> aProcessIdent(16); + rtl_getGlobalProcessId(reinterpret_cast<sal_uInt8*>(aProcessIdent.getArray())); + aAny = xSysDepWin->getWindowHandle(aProcessIdent, css::lang::SystemDependent::SYSTEM_XWINDOW); + css::awt::SystemDependentXWindow tmp; + aAny >>= tmp; + m_pParentWidget = GetGtkSalData()->GetGtkDisplay()->findGtkWidgetForNativeHandle(tmp.WindowHandle); + } + } + } + + GtkFileChooserAction eAction = GTK_FILE_CHOOSER_ACTION_OPEN; + const gchar *first_button_text = GTK_STOCK_OPEN; + + SolarMutexGuard g; + + // TODO: extract full semantic from + // svtools/source/filepicker/filepicker.cxx (getWinBits) + switch( templateId ) + { + case FILEOPEN_SIMPLE: + eAction = GTK_FILE_CHOOSER_ACTION_OPEN; + first_button_text = GTK_STOCK_OPEN; + break; + case FILESAVE_SIMPLE: + eAction = GTK_FILE_CHOOSER_ACTION_SAVE; + first_button_text = GTK_STOCK_SAVE; + break; + case FILESAVE_AUTOEXTENSION_PASSWORD: + eAction = GTK_FILE_CHOOSER_ACTION_SAVE; + first_button_text = GTK_STOCK_SAVE; + mbToggleVisibility[PASSWORD] = true; + mbToggleVisibility[GPGENCRYPTION] = true; + // TODO + break; + case FILESAVE_AUTOEXTENSION_PASSWORD_FILTEROPTIONS: + eAction = GTK_FILE_CHOOSER_ACTION_SAVE; + first_button_text = GTK_STOCK_SAVE; + mbToggleVisibility[PASSWORD] = true; + mbToggleVisibility[GPGENCRYPTION] = true; + mbToggleVisibility[FILTEROPTIONS] = true; + // TODO + break; + case FILESAVE_AUTOEXTENSION_SELECTION: + eAction = GTK_FILE_CHOOSER_ACTION_SAVE; // SELECT_FOLDER ? + first_button_text = GTK_STOCK_SAVE; + mbToggleVisibility[SELECTION] = true; + // TODO + break; + case FILESAVE_AUTOEXTENSION_TEMPLATE: + eAction = GTK_FILE_CHOOSER_ACTION_SAVE; + first_button_text = GTK_STOCK_SAVE; + mbListVisibility[TEMPLATE] = true; + // TODO + break; + case FILEOPEN_LINK_PREVIEW_IMAGE_TEMPLATE: + eAction = GTK_FILE_CHOOSER_ACTION_OPEN; + first_button_text = GTK_STOCK_OPEN; + mbToggleVisibility[LINK] = true; + mbToggleVisibility[PREVIEW] = true; + mbListVisibility[IMAGE_TEMPLATE] = true; + // TODO + break; + case FILEOPEN_LINK_PREVIEW_IMAGE_ANCHOR: + eAction = GTK_FILE_CHOOSER_ACTION_OPEN; + first_button_text = GTK_STOCK_OPEN; + mbToggleVisibility[LINK] = true; + mbToggleVisibility[PREVIEW] = true; + mbListVisibility[IMAGE_ANCHOR] = true; + // TODO + break; + case FILEOPEN_PLAY: + eAction = GTK_FILE_CHOOSER_ACTION_OPEN; + first_button_text = GTK_STOCK_OPEN; + mbButtonVisibility[PLAY] = true; + // TODO + break; + case FILEOPEN_LINK_PLAY: + eAction = GTK_FILE_CHOOSER_ACTION_OPEN; + first_button_text = GTK_STOCK_OPEN; + mbToggleVisibility[LINK] = true; + mbButtonVisibility[PLAY] = true; + // TODO + break; + case FILEOPEN_READONLY_VERSION: + eAction = GTK_FILE_CHOOSER_ACTION_OPEN; + first_button_text = GTK_STOCK_OPEN; + mbToggleVisibility[READONLY] = true; + mbListVisibility[VERSION] = true; + break; + case FILEOPEN_LINK_PREVIEW: + eAction = GTK_FILE_CHOOSER_ACTION_OPEN; + first_button_text = GTK_STOCK_OPEN; + mbToggleVisibility[LINK] = true; + mbToggleVisibility[PREVIEW] = true; + // TODO + break; + case FILESAVE_AUTOEXTENSION: + eAction = GTK_FILE_CHOOSER_ACTION_SAVE; + first_button_text = GTK_STOCK_SAVE; + // TODO + break; + case FILEOPEN_PREVIEW: + eAction = GTK_FILE_CHOOSER_ACTION_OPEN; + first_button_text = GTK_STOCK_OPEN; + mbToggleVisibility[PREVIEW] = true; + // TODO + break; + default: + throw lang::IllegalArgumentException( + "Unknown template", + static_cast< XFilePicker2* >( this ), + 1 ); + } + + if( GTK_FILE_CHOOSER_ACTION_SAVE == eAction ) + { + OUString aFilePickerTitle(getResString( FILE_PICKER_TITLE_SAVE )); + gtk_window_set_title ( GTK_WINDOW( m_pDialog ), + OUStringToOString( aFilePickerTitle, RTL_TEXTENCODING_UTF8 ).getStr() ); + } + + gtk_file_chooser_set_action( GTK_FILE_CHOOSER( m_pDialog ), eAction); + dialog_remove_buttons( GTK_DIALOG( m_pDialog ) ); + gtk_dialog_add_button( GTK_DIALOG( m_pDialog ), GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL ); + for( int nTVIndex = 0; nTVIndex < BUTTON_LAST; nTVIndex++ ) + { + if( mbButtonVisibility[nTVIndex] ) + { +#ifdef GTK_STOCK_MEDIA_PLAY + m_pButtons[ nTVIndex ] = gtk_dialog_add_button( GTK_DIALOG( m_pDialog ), GTK_STOCK_MEDIA_PLAY, 1 ); +#else + OString aPlay = OUStringToOString( getResString( PUSHBUTTON_PLAY ), RTL_TEXTENCODING_UTF8 ); + m_pButtons[ nTVIndex ] = gtk_dialog_add_button( GTK_DIALOG( m_pDialog ), aPlay.getStr(), 1 ); +#endif + } + } + gtk_dialog_add_button( GTK_DIALOG( m_pDialog ), first_button_text, GTK_RESPONSE_ACCEPT ); + + gtk_dialog_set_default_response( GTK_DIALOG (m_pDialog), GTK_RESPONSE_ACCEPT ); + + // Setup special flags + for( int nTVIndex = 0; nTVIndex < TOGGLE_LAST; nTVIndex++ ) + { + if( mbToggleVisibility[nTVIndex] ) + gtk_widget_show( m_pToggles[ nTVIndex ] ); + } + + for( int nTVIndex = 0; nTVIndex < LIST_LAST; nTVIndex++ ) + { + if( mbListVisibility[nTVIndex] ) + { + gtk_widget_set_sensitive( m_pLists[ nTVIndex ], false ); + gtk_widget_show( m_pLists[ nTVIndex ] ); + gtk_widget_show( m_pListLabels[ nTVIndex ] ); + gtk_widget_show( m_pAligns[ nTVIndex ] ); + gtk_widget_show( m_pHBoxs[ nTVIndex ] ); + } + } +} + +void SalGtkFilePicker::preview_toggled_cb( GObject *cb, SalGtkFilePicker* pobjFP ) +{ + if( pobjFP->mbToggleVisibility[PREVIEW] ) + pobjFP->setShowState( gtk_toggle_button_get_active( GTK_TOGGLE_BUTTON( cb ) ) ); +} + +// XCancellable + +void SAL_CALL SalGtkFilePicker::cancel() +{ + SolarMutexGuard g; + + OSL_ASSERT( m_pDialog != nullptr ); + + // TODO m_pImpl->cancel(); +} + +// Misc + +void SalGtkFilePicker::SetCurFilter( const OUString& rFilter ) +{ + // Get all the filters already added + GSList *filters = gtk_file_chooser_list_filters ( GTK_FILE_CHOOSER( m_pDialog ) ); + bool bFound = false; + + for( GSList *iter = filters; !bFound && iter; iter = iter->next ) + { + GtkFileFilter* pFilter = static_cast<GtkFileFilter *>( iter->data ); + const gchar * filtername = gtk_file_filter_get_name( pFilter ); + OUString sFilterName( filtername, strlen( filtername ), RTL_TEXTENCODING_UTF8 ); + + OUString aShrunkName = shrinkFilterName( rFilter ); + if( aShrunkName == sFilterName ) + { + SAL_INFO( "vcl.gtk", "actually setting " << filtername ); + gtk_file_chooser_set_filter( GTK_FILE_CHOOSER( m_pDialog ), pFilter ); + bFound = true; + } + } + + g_slist_free( filters ); +} + +extern "C" +{ +static gboolean +case_insensitive_filter (const GtkFileFilterInfo *filter_info, gpointer data) +{ + bool bRetval = false; + const char *pFilter = static_cast<const char *>(data); + + g_return_val_if_fail( data != nullptr, false ); + g_return_val_if_fail( filter_info != nullptr, false ); + + if( !filter_info->uri ) + return false; + + const char *pExtn = strrchr( filter_info->uri, '.' ); + if( !pExtn ) + return false; + pExtn++; + + if( !g_ascii_strcasecmp( pFilter, pExtn ) ) + bRetval = true; + + SAL_INFO( "vcl.gtk", "'" << filter_info->uri << "' match extn '" << pExtn << "' vs '" << pFilter << "' yields " << bRetval ); + + return bRetval; +} +} + +GtkFileFilter* SalGtkFilePicker::implAddFilter( const OUString& rFilter, const OUString& rType ) +{ + GtkFileFilter *filter = gtk_file_filter_new(); + + OUString aShrunkName = shrinkFilterName( rFilter ); + OString aFilterName = OUStringToOString( aShrunkName, RTL_TEXTENCODING_UTF8 ); + gtk_file_filter_set_name( filter, aFilterName.getStr() ); + + OUStringBuffer aTokens; + + bool bAllGlob = rType == "*.*" || rType == "*"; + if (bAllGlob) + gtk_file_filter_add_pattern( filter, "*" ); + else + { + sal_Int32 nIndex = 0; + do + { + OUString aToken = rType.getToken( 0, ';', nIndex ); + // Assume all have the "*.<extn>" syntax + sal_Int32 nStarDot = aToken.lastIndexOf( "*." ); + if (nStarDot >= 0) + aToken = aToken.copy( nStarDot + 2 ); + if (!aToken.isEmpty()) + { + if (!aTokens.isEmpty()) + aTokens.append(","); + aTokens.append(aToken); + gtk_file_filter_add_custom (filter, GTK_FILE_FILTER_URI, + case_insensitive_filter, + g_strdup( OUStringToOString(aToken, RTL_TEXTENCODING_UTF8).getStr() ), + g_free ); + + SAL_INFO( "vcl.gtk", "fustering with " << aToken ); + } +#if OSL_DEBUG_LEVEL > 0 + else + { + g_warning( "Duff filter token '%s'\n", + OUStringToOString( + rType.getToken( 0, ';', nIndex ), RTL_TEXTENCODING_UTF8 ).getStr() ); + } +#endif + } + while( nIndex >= 0 ); + } + + gtk_file_chooser_add_filter( GTK_FILE_CHOOSER( m_pDialog ), filter ); + + if (!bAllGlob) + { + GtkTreeIter iter; + gtk_list_store_append (m_pFilterStore, &iter); + gtk_list_store_set (m_pFilterStore, &iter, + 0, OUStringToOString(shrinkFilterName( rFilter, true ), RTL_TEXTENCODING_UTF8).getStr(), + 1, OUStringToOString(aTokens.makeStringAndClear(), RTL_TEXTENCODING_UTF8).getStr(), + 2, aFilterName.getStr(), + 3, OUStringToOString(rType, RTL_TEXTENCODING_UTF8).getStr(), + -1); + } + return filter; +} + +void SalGtkFilePicker::implAddFilterGroup( const OUString& /*_rFilter*/, const Sequence< StringPair >& _rFilters ) +{ + // Gtk+ has no filter group concept I think so ... + // implAddFilter( _rFilter, String() ); + for( const auto& rSubFilter : _rFilters ) + implAddFilter( rSubFilter.First, rSubFilter.Second ); +} + +void SalGtkFilePicker::SetFilters() +{ + if (m_aInitialFilter.isEmpty()) + m_aInitialFilter = m_aCurrentFilter; + + OUString sPseudoFilter; + if( GTK_FILE_CHOOSER_ACTION_SAVE == gtk_file_chooser_get_action( GTK_FILE_CHOOSER( m_pDialog ) ) ) + { + std::set<OUString> aAllFormats; + if( m_pFilterVector ) + { + for (auto & filter : *m_pFilterVector) + { + if( filter.hasSubFilters() ) + { // it's a filter group + css::uno::Sequence< css::beans::StringPair > aSubFilters; + filter.getSubFilters( aSubFilters ); + for( const auto& rSubFilter : std::as_const(aSubFilters) ) + aAllFormats.insert(rSubFilter.Second); + } + else + aAllFormats.insert(filter.getFilter()); + } + } + if (aAllFormats.size() > 1) + { + OUStringBuffer sAllFilter; + for (auto const& format : aAllFormats) + { + if (!sAllFilter.isEmpty()) + sAllFilter.append(";"); + sAllFilter.append(format); + } + sPseudoFilter = getResString(FILE_PICKER_ALLFORMATS); + m_pPseudoFilter = implAddFilter( sPseudoFilter, sAllFilter.makeStringAndClear() ); + } + } + + if( m_pFilterVector ) + { + for (auto & filter : *m_pFilterVector) + { + if( filter.hasSubFilters() ) + { // it's a filter group + + css::uno::Sequence< css::beans::StringPair > aSubFilters; + filter.getSubFilters( aSubFilters ); + + implAddFilterGroup( filter.getTitle(), aSubFilters ); + } + else + { + // it's a single filter + + implAddFilter( filter.getTitle(), filter.getFilter() ); + } + } + } + + // We always hide the expander now and depend on the user using the glob + // list, or type a filename suffix, to select a filter by inference. + gtk_widget_hide(m_pFilterExpander); + + // set the default filter + if (!sPseudoFilter.isEmpty()) + SetCurFilter( sPseudoFilter ); + else if(!m_aCurrentFilter.isEmpty()) + SetCurFilter( m_aCurrentFilter ); + + SAL_INFO( "vcl.gtk", "end setting filters"); +} + +SalGtkFilePicker::~SalGtkFilePicker() +{ + SolarMutexGuard g; + + int i; + + for( i = 0; i < TOGGLE_LAST; i++ ) + gtk_widget_destroy( m_pToggles[i] ); + + for( i = 0; i < LIST_LAST; i++ ) + { + gtk_widget_destroy( m_pListLabels[i] ); + gtk_widget_destroy( m_pAligns[i] ); //m_pAligns[i] owns m_pLists[i] + gtk_widget_destroy( m_pHBoxs[i] ); + } + + m_pFilterVector.reset(); + + gtk_widget_destroy( m_pVBox ); +} + +uno::Reference< ui::dialogs::XFilePicker2 > +GtkInstance::createFilePicker( const css::uno::Reference< css::uno::XComponentContext > &xMSF ) +{ + return uno::Reference< ui::dialogs::XFilePicker2 >( + new SalGtkFilePicker( xMSF ) ); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/fpicker/SalGtkFilePicker.hxx b/vcl/unx/gtk3/fpicker/SalGtkFilePicker.hxx new file mode 100644 index 000000000..39bb6815b --- /dev/null +++ b/vcl/unx/gtk3/fpicker/SalGtkFilePicker.hxx @@ -0,0 +1,240 @@ +/* -*- 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#ifndef INCLUDED_VCL_UNX_GTK_FPICKER_SALGTKFILEPICKER_HXX +#define INCLUDED_VCL_UNX_GTK_FPICKER_SALGTKFILEPICKER_HXX + +#include <cppuhelper/compbase.hxx> +#include <com/sun/star/lang/XInitialization.hpp> +#include <com/sun/star/ui/dialogs/XFilePickerControlAccess.hpp> +#include <com/sun/star/ui/dialogs/XFilePreview.hpp> +#include <com/sun/star/ui/dialogs/XFilePicker3.hpp> +#include <com/sun/star/uno/XComponentContext.hpp> +#include <com/sun/star/beans/StringPair.hpp> + +#include <vector> +#include <memory> +#include <rtl/ustring.hxx> + +#include "SalGtkPicker.hxx" + +// Implementation class for the XFilePicker Interface + +struct FilterEntry; +struct ElementEntry_Impl; + + +typedef cppu::WeakComponentImplHelper< + css::ui::dialogs::XFilePickerControlAccess, + css::ui::dialogs::XFilePreview, + css::ui::dialogs::XFilePicker3, + css::lang::XInitialization + > SalGtkFilePicker_Base; + +class SalGtkFilePicker : public SalGtkPicker, public SalGtkFilePicker_Base +{ + public: + + // constructor + SalGtkFilePicker( const css::uno::Reference< css::uno::XComponentContext >& xServiceMgr ); + + // XFilePickerNotifier + + virtual void SAL_CALL addFilePickerListener( const css::uno::Reference< css::ui::dialogs::XFilePickerListener >& xListener ) override; + virtual void SAL_CALL removeFilePickerListener( const css::uno::Reference< css::ui::dialogs::XFilePickerListener >& xListener ) override; + + // XExecutableDialog functions + + virtual void SAL_CALL setTitle( const OUString& aTitle ) override; + + virtual sal_Int16 SAL_CALL execute() override; + + // XFilePicker functions + + virtual void SAL_CALL setMultiSelectionMode( sal_Bool bMode ) override; + + virtual void SAL_CALL setDefaultName( const OUString& aName ) override; + + virtual void SAL_CALL setDisplayDirectory( const OUString& aDirectory ) override; + + virtual OUString SAL_CALL getDisplayDirectory( ) override; + + virtual css::uno::Sequence< OUString > SAL_CALL getFiles( ) override; + + // XFilePicker2 functions + + virtual css::uno::Sequence< OUString > SAL_CALL getSelectedFiles() override; + + // XFilterManager functions + + virtual void SAL_CALL appendFilter( const OUString& aTitle, const OUString& aFilter ) override; + + virtual void SAL_CALL setCurrentFilter( const OUString& aTitle ) override; + + virtual OUString SAL_CALL getCurrentFilter( ) override; + + // XFilterGroupManager functions + + virtual void SAL_CALL appendFilterGroup( const OUString& sGroupTitle, const css::uno::Sequence< css::beans::StringPair >& aFilters ) override; + + // XFilePickerControlAccess functions + + virtual void SAL_CALL setValue( sal_Int16 nControlId, sal_Int16 nControlAction, const css::uno::Any& aValue ) override; + + virtual css::uno::Any SAL_CALL getValue( sal_Int16 aControlId, sal_Int16 aControlAction ) override; + + virtual void SAL_CALL enableControl( sal_Int16 nControlId, sal_Bool bEnable ) override; + + virtual void SAL_CALL setLabel( sal_Int16 nControlId, const OUString& aLabel ) override; + + virtual OUString SAL_CALL getLabel( sal_Int16 nControlId ) override; + + // XFilePreview + + virtual css::uno::Sequence< sal_Int16 > SAL_CALL getSupportedImageFormats( ) override; + + virtual sal_Int32 SAL_CALL getTargetColorDepth( ) override; + + virtual sal_Int32 SAL_CALL getAvailableWidth( ) override; + + virtual sal_Int32 SAL_CALL getAvailableHeight( ) override; + + virtual void SAL_CALL setImage( sal_Int16 aImageFormat, const css::uno::Any& aImage ) override; + + virtual sal_Bool SAL_CALL setShowState( sal_Bool bShowState ) override; + + virtual sal_Bool SAL_CALL getShowState( ) override; + + // XInitialization + + virtual void SAL_CALL initialize( const css::uno::Sequence< css::uno::Any >& aArguments ) override; + + // XCancellable + + virtual void SAL_CALL cancel( ) override; + + // FilePicker Event functions + + private: + SalGtkFilePicker( const SalGtkFilePicker& ) = delete; + SalGtkFilePicker& operator=( const SalGtkFilePicker& ) = delete; + + bool FilterNameExists( const OUString& rTitle ); + bool FilterNameExists( const css::uno::Sequence< css::beans::StringPair >& _rGroupedFilters ); + + void ensureFilterVector( const OUString& _rInitialCurrentFilter ); + + void impl_fileSelectionChanged( const css::ui::dialogs::FilePickerEvent& aEvent ); + void impl_directoryChanged( const css::ui::dialogs::FilePickerEvent& aEvent ); + void impl_controlStateChanged( const css::ui::dialogs::FilePickerEvent& aEvent ); + + private: + css::uno::Reference< css::ui::dialogs::XFilePickerListener > + m_xListener; + OUString msPlayLabel; + std::unique_ptr<std::vector<FilterEntry>> m_pFilterVector; + GtkWidget *m_pParentWidget; + GtkWidget *m_pVBox; + GtkWidget *m_pFilterExpander; + GtkWidget *m_pFilterView; + GtkListStore *m_pFilterStore; + + enum { + AUTOEXTENSION, + PASSWORD, + FILTEROPTIONS, + READONLY, + LINK, + PREVIEW, + SELECTION, + GPGENCRYPTION, + TOGGLE_LAST + }; + + GtkWidget *m_pToggles[ TOGGLE_LAST ]; + + bool mbToggleVisibility[TOGGLE_LAST]; + + enum { + PLAY, + BUTTON_LAST }; + + GtkWidget *m_pButtons[ BUTTON_LAST ]; + + enum { + VERSION, + TEMPLATE, + IMAGE_TEMPLATE, + IMAGE_ANCHOR, + LIST_LAST + }; + + GtkWidget *m_pHBoxs[ LIST_LAST ]; + GtkWidget *m_pAligns[ LIST_LAST ]; + GtkWidget *m_pLists[ LIST_LAST ]; + GtkWidget *m_pListLabels[ LIST_LAST ]; + bool mbListVisibility[ LIST_LAST ]; + bool mbButtonVisibility[ BUTTON_LAST ]; + gulong mnHID_FolderChange; + gulong mnHID_SelectionChange; + + OUString m_aCurrentFilter; + OUString m_aInitialFilter; + + bool bVersionWidthUnset; + bool mbPreviewState; + gulong mHID_Preview; + GtkWidget* m_pPreview; + GtkFileFilter* m_pPseudoFilter; + static constexpr sal_Int32 g_PreviewImageWidth = 256; + static constexpr sal_Int32 g_PreviewImageHeight = 256; + + GtkWidget *getWidget( sal_Int16 nControlId, GType *pType = nullptr); + + void SetCurFilter( const OUString& rFilter ); + void SetFilters(); + void UpdateFilterfromUI(); + + void implChangeType( GtkTreeSelection *selection ); + GtkFileFilter * implAddFilter( const OUString& rFilter, const OUString& rType ); + void implAddFilterGroup( const OUString& rFilter, + const css::uno::Sequence< css::beans::StringPair>& _rFilters ); + void updateCurrentFilterFromName(const gchar* filtername); + void unselect_type(); + void InitialMapping(); + + void HandleSetListValue(GtkComboBox *pWidget, sal_Int16 nControlAction, + const css::uno::Any& rValue); + static css::uno::Any HandleGetListValue(GtkComboBox *pWidget, sal_Int16 nControlAction); + + static void expander_changed_cb( GtkExpander *expander, SalGtkFilePicker *pobjFP ); + static void preview_toggled_cb( GObject *cb, SalGtkFilePicker *pobjFP ); + static void filter_changed_cb( GtkFileChooser *file_chooser, GParamSpec *pspec, SalGtkFilePicker *pobjFP ); + static void type_changed_cb( GtkTreeSelection *selection, SalGtkFilePicker *pobjFP ); + static void folder_changed_cb (GtkFileChooser *file_chooser, SalGtkFilePicker *pobjFP); + static void selection_changed_cb (GtkFileChooser *file_chooser, SalGtkFilePicker *pobjFP); + static void update_preview_cb (GtkFileChooser *file_chooser, SalGtkFilePicker *pobjFP); + static void dialog_mapped_cb(GtkWidget *widget, SalGtkFilePicker *pobjFP); + public: + virtual ~SalGtkFilePicker() override; + +}; +#endif // INCLUDED_VCL_UNX_GTK_FPICKER_SALGTKFILEPICKER_HXX + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/fpicker/SalGtkFolderPicker.cxx b/vcl/unx/gtk3/fpicker/SalGtkFolderPicker.cxx new file mode 100644 index 000000000..bb940cf5f --- /dev/null +++ b/vcl/unx/gtk3/fpicker/SalGtkFolderPicker.cxx @@ -0,0 +1,177 @@ +/* -*- 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#ifdef AIX +#define _LINUX_SOURCE_COMPAT +#include <sys/timer.h> +#undef _LINUX_SOURCE_COMPAT +#endif + +#include <config_gio.h> + +#include <com/sun/star/awt/Toolkit.hpp> +#include <com/sun/star/frame/Desktop.hpp> +#include <com/sun/star/ui/dialogs/ExecutableDialogResults.hpp> +#include <vcl/svapp.hxx> +#include <unx/gtk/gtkinst.hxx> +#include "SalGtkFolderPicker.hxx" +#include <sal/log.hxx> + +using namespace ::com::sun::star; +using namespace ::com::sun::star::ui::dialogs; +using namespace ::com::sun::star::lang; +using namespace ::com::sun::star::uno; + +// constructor + +SalGtkFolderPicker::SalGtkFolderPicker( const uno::Reference< uno::XComponentContext >& xContext ) : + SalGtkPicker( xContext ) +{ + m_pDialog = gtk_file_chooser_dialog_new( + OUStringToOString( getResString( FOLDERPICKER_TITLE ), RTL_TEXTENCODING_UTF8 ).getStr(), + nullptr, GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER, GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL, + GTK_STOCK_OK, GTK_RESPONSE_ACCEPT, nullptr ); + + gtk_dialog_set_default_response( GTK_DIALOG (m_pDialog), GTK_RESPONSE_ACCEPT ); +#if ENABLE_GIO + gtk_file_chooser_set_local_only( GTK_FILE_CHOOSER( m_pDialog ), false ); +#endif + gtk_file_chooser_set_select_multiple( GTK_FILE_CHOOSER( m_pDialog ), false ); +} + +void SAL_CALL SalGtkFolderPicker::setDisplayDirectory( const OUString& aDirectory ) +{ + SolarMutexGuard g; + + assert( m_pDialog != nullptr ); + + OString aTxt = unicodetouri( aDirectory ); + if( aTxt.isEmpty() ){ + aTxt = unicodetouri("file:///."); + } + + if( aTxt.endsWith("/") ) + aTxt = aTxt.copy( 0, aTxt.getLength() - 1 ); + + SAL_INFO( "vcl", "setting path to " << aTxt ); + + gtk_file_chooser_set_current_folder_uri( GTK_FILE_CHOOSER( m_pDialog ), + aTxt.getStr() ); +} + +OUString SAL_CALL SalGtkFolderPicker::getDisplayDirectory() +{ + SolarMutexGuard g; + + assert( m_pDialog != nullptr ); + + gchar* pCurrentFolder = + gtk_file_chooser_get_current_folder_uri( GTK_FILE_CHOOSER( m_pDialog ) ); + OUString aCurrentFolderName = uritounicode(pCurrentFolder); + g_free( pCurrentFolder ); + + return aCurrentFolderName; +} + +OUString SAL_CALL SalGtkFolderPicker::getDirectory() +{ + SolarMutexGuard g; + + assert( m_pDialog != nullptr ); + + gchar* pSelectedFolder = + gtk_file_chooser_get_uri( GTK_FILE_CHOOSER( m_pDialog ) ); + OUString aSelectedFolderName = uritounicode(pSelectedFolder); + g_free( pSelectedFolder ); + + return aSelectedFolderName; +} + +void SAL_CALL SalGtkFolderPicker::setDescription( const OUString& /*rDescription*/ ) +{ +} + +// XExecutableDialog functions + +void SAL_CALL SalGtkFolderPicker::setTitle( const OUString& aTitle ) +{ + SolarMutexGuard g; + + assert( m_pDialog != nullptr ); + + OString aWindowTitle = OUStringToOString( aTitle, RTL_TEXTENCODING_UTF8 ); + + gtk_window_set_title( GTK_WINDOW( m_pDialog ), aWindowTitle.getStr() ); +} + +sal_Int16 SAL_CALL SalGtkFolderPicker::execute() +{ + SolarMutexGuard g; + + assert( m_pDialog != nullptr ); + + sal_Int16 retVal = 0; + + uno::Reference< awt::XExtendedToolkit > xToolkit = + awt::Toolkit::create(m_xContext); + + uno::Reference<frame::XDesktop> xDesktop = frame::Desktop::create(m_xContext); + + GtkWindow *pParent = RunDialog::GetTransientFor(); + if (pParent) + gtk_window_set_transient_for(GTK_WINDOW(m_pDialog), pParent); + RunDialog* pRunDialog = new RunDialog(m_pDialog, xToolkit, xDesktop); + uno::Reference < awt::XTopWindowListener > xLifeCycle(pRunDialog); + gint nStatus = pRunDialog->run(); + switch( nStatus ) + { + case GTK_RESPONSE_ACCEPT: + retVal = ExecutableDialogResults::OK; + break; + case GTK_RESPONSE_CANCEL: + retVal = ExecutableDialogResults::CANCEL; + break; + default: + retVal = 0; + break; + } + gtk_widget_hide(m_pDialog); + + return retVal; +} + +// XCancellable + +void SAL_CALL SalGtkFolderPicker::cancel() +{ + SolarMutexGuard g; + + assert( m_pDialog != nullptr ); + + // TODO m_pImpl->cancel(); +} + +uno::Reference< ui::dialogs::XFolderPicker2 > +GtkInstance::createFolderPicker( const uno::Reference< uno::XComponentContext > &xMSF ) +{ + return uno::Reference< ui::dialogs::XFolderPicker2 >( + new SalGtkFolderPicker( xMSF ) ); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/fpicker/SalGtkFolderPicker.hxx b/vcl/unx/gtk3/fpicker/SalGtkFolderPicker.hxx new file mode 100644 index 000000000..229bbe8b8 --- /dev/null +++ b/vcl/unx/gtk3/fpicker/SalGtkFolderPicker.hxx @@ -0,0 +1,66 @@ +/* -*- 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#ifndef INCLUDED_VCL_UNX_GTK_FPICKER_SALGTKFOLDERPICKER_HXX +#define INCLUDED_VCL_UNX_GTK_FPICKER_SALGTKFOLDERPICKER_HXX + +#include <list> +#include <memory> +#include <rtl/ustring.hxx> +#include <cppuhelper/implbase.hxx> + +#include "SalGtkPicker.hxx" + +class SalGtkFolderPicker : + public SalGtkPicker, + public cppu::WeakImplHelper< css::ui::dialogs::XFolderPicker2 > +{ + public: + + // constructor + SalGtkFolderPicker( const css::uno::Reference< css::uno::XComponentContext >& xServiceMgr ); + + // XExecutableDialog functions + + virtual void SAL_CALL setTitle( const OUString& aTitle ) override; + + virtual sal_Int16 SAL_CALL execute( ) override; + + // XFolderPicker functions + + virtual void SAL_CALL setDisplayDirectory( const OUString& rDirectory ) override; + + virtual OUString SAL_CALL getDisplayDirectory( ) override; + + virtual OUString SAL_CALL getDirectory( ) override; + + virtual void SAL_CALL setDescription( const OUString& rDescription ) override; + + // XCancellable + + virtual void SAL_CALL cancel( ) override; + + private: + SalGtkFolderPicker( const SalGtkFolderPicker& ) = delete; + SalGtkFolderPicker& operator=( const SalGtkFolderPicker& ) = delete; +}; + +#endif // INCLUDED_VCL_UNX_GTK_FPICKER_SALGTKFOLDERPICKER_HXX + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/fpicker/SalGtkPicker.cxx b/vcl/unx/gtk3/fpicker/SalGtkPicker.cxx new file mode 100644 index 000000000..c847774d1 --- /dev/null +++ b/vcl/unx/gtk3/fpicker/SalGtkPicker.cxx @@ -0,0 +1,266 @@ +/* -*- 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#ifdef AIX +#define _LINUX_SOURCE_COMPAT +#include <sys/timer.h> +#undef _LINUX_SOURCE_COMPAT +#endif + +#include <com/sun/star/frame/TerminationVetoException.hpp> +#include <com/sun/star/lang/XMultiComponentFactory.hpp> +#include <com/sun/star/uri/ExternalUriReferenceTranslator.hpp> +#include <com/sun/star/accessibility/XAccessibleContext.hpp> +#include <com/sun/star/accessibility/AccessibleRole.hpp> +#include <osl/diagnose.h> +#include <sal/log.hxx> +#include <vcl/svapp.hxx> +#include <tools/urlobj.hxx> + +#include <vcl/window.hxx> +#include <unx/gtk/gtkframe.hxx> +#include "SalGtkPicker.hxx" + +using namespace ::rtl; +using namespace ::com::sun::star; +using namespace ::com::sun::star::lang; +using namespace ::com::sun::star::uno; + +OUString SalGtkPicker::uritounicode(const gchar* pIn) +{ + if (!pIn) + return OUString(); + + OUString sURL( pIn, strlen(pIn), + RTL_TEXTENCODING_UTF8 ); + + INetURLObject aURL(sURL); + if (INetProtocol::File == aURL.GetProtocol()) + { + // all the URLs are handled by office in UTF-8 + // so the Gnome FP related URLs should be converted accordingly + OUString aNewURL = uri::ExternalUriReferenceTranslator::create( m_xContext )->translateToInternal(sURL); + if( !aNewURL.isEmpty() ) + sURL = aNewURL; + } + return sURL; +} + +OString SalGtkPicker::unicodetouri(const OUString &rURL) +{ + // all the URLs are handled by office in UTF-8 ( and encoded with "%xx" codes based on UTF-8 ) + // so the Gnome FP related URLs should be converted accordingly + OString sURL = OUStringToOString(rURL, RTL_TEXTENCODING_UTF8); + INetURLObject aURL(rURL); + if (INetProtocol::File == aURL.GetProtocol()) + { + OUString aNewURL = uri::ExternalUriReferenceTranslator::create( m_xContext )->translateToExternal(rURL); + + if( !aNewURL.isEmpty() ) + { + // At this point the URL should contain ascii characters only actually + sURL = OUStringToOString( aNewURL, osl_getThreadTextEncoding() ); + } + } + return sURL; +} + +extern "C" +{ + static gboolean canceldialog(RunDialog *pDialog) + { + SolarMutexGuard g; + pDialog->cancel(); + return false; + } +} + +GtkWindow* RunDialog::GetTransientFor() +{ + GtkWindow *pParent = nullptr; + + vcl::Window * pWindow = ::Application::GetActiveTopWindow(); + if( pWindow ) + { + GtkSalFrame *pFrame = dynamic_cast<GtkSalFrame *>( pWindow->ImplGetFrame() ); + if( pFrame ) + pParent = GTK_WINDOW(gtk_widget_get_toplevel(pFrame->getWindow())); + } + + return pParent; +} + +RunDialog::RunDialog(GtkWidget *pDialog, const uno::Reference<awt::XExtendedToolkit>& rToolkit, + const uno::Reference<frame::XDesktop>& rDesktop) + : cppu::WeakComponentImplHelper<awt::XTopWindowListener, frame::XTerminateListener>(maLock) + , mpDialog(pDialog) + , mbTerminateDesktop(false) + , mxToolkit(rToolkit) + , mxDesktop(rDesktop) +{ +} + +RunDialog::~RunDialog() +{ + SolarMutexGuard g; + + g_source_remove_by_user_data (this); +} + +void SAL_CALL RunDialog::windowOpened(const css::lang::EventObject& e) +{ + SolarMutexGuard g; + + //Don't popdown dialogs if a tooltip appears elsewhere, that's ok, but do pop down + //if another dialog/frame is launched. + css::uno::Reference<css::accessibility::XAccessible> xAccessible(e.Source, css::uno::UNO_QUERY); + if (xAccessible.is()) + { + css::uno::Reference<css::accessibility::XAccessibleContext> xContext(xAccessible->getAccessibleContext()); + if (xContext.is() && xContext->getAccessibleRole() == css::accessibility::AccessibleRole::TOOL_TIP) + { + return; + } + } + + g_timeout_add_full(G_PRIORITY_HIGH_IDLE, 0, reinterpret_cast<GSourceFunc>(canceldialog), this, nullptr); +} + +void SAL_CALL RunDialog::queryTermination( const css::lang::EventObject& ) +{ + SolarMutexGuard g; + + g_timeout_add_full(G_PRIORITY_HIGH_IDLE, 0, reinterpret_cast<GSourceFunc>(canceldialog), this, nullptr); + + mbTerminateDesktop = true; + + throw css::frame::TerminationVetoException(); +} + +void SAL_CALL RunDialog::notifyTermination( const css::lang::EventObject& ) +{ +} + +void RunDialog::cancel() +{ + gtk_dialog_response( GTK_DIALOG( mpDialog ), GTK_RESPONSE_CANCEL ); + gtk_widget_hide( mpDialog ); +} + +namespace +{ + class ExecuteInfo + { + private: + css::uno::Reference<css::frame::XDesktop> mxDesktop; + public: + ExecuteInfo(const css::uno::Reference<css::frame::XDesktop>& rDesktop) + : mxDesktop(rDesktop) + { + } + void terminate() + { + mxDesktop->terminate(); + } + }; +} + +gint RunDialog::run() +{ + if (mxToolkit.is()) + mxToolkit->addTopWindowListener(this); + + mxDesktop->addTerminateListener(this); + gint nStatus = gtk_dialog_run(GTK_DIALOG(mpDialog)); + mxDesktop->removeTerminateListener(this); + + if (mxToolkit.is()) + mxToolkit->removeTopWindowListener(this); + + if (mbTerminateDesktop) + { + ExecuteInfo* pExecuteInfo = new ExecuteInfo(mxDesktop); + Application::PostUserEvent(LINK(nullptr, RunDialog, TerminateDesktop), pExecuteInfo); + } + + return nStatus; +} + +IMPL_STATIC_LINK(RunDialog, TerminateDesktop, void*, p, void) +{ + ExecuteInfo* pExecuteInfo = static_cast<ExecuteInfo*>(p); + pExecuteInfo->terminate(); + delete pExecuteInfo; +} + +SalGtkPicker::SalGtkPicker( const uno::Reference<uno::XComponentContext>& xContext ) + : m_pDialog( nullptr ), m_xContext( xContext ) +{ +} + +SalGtkPicker::~SalGtkPicker() +{ + SolarMutexGuard g; + + if (m_pDialog) + { + gtk_widget_destroy(m_pDialog); + } +} + +void SalGtkPicker::implsetDisplayDirectory( const OUString& aDirectory ) +{ + OSL_ASSERT( m_pDialog != nullptr ); + + OString aTxt = unicodetouri(aDirectory); + if( aTxt.isEmpty() ){ + aTxt = unicodetouri("file:///."); + } + + if( aTxt.endsWith("/") ) + aTxt = aTxt.copy( 0, aTxt.getLength() - 1 ); + + SAL_INFO( "vcl", "setting path to " << aTxt ); + + gtk_file_chooser_set_current_folder_uri( GTK_FILE_CHOOSER( m_pDialog ), + aTxt.getStr() ); +} + +OUString SalGtkPicker::implgetDisplayDirectory() +{ + OSL_ASSERT( m_pDialog != nullptr ); + + gchar* pCurrentFolder = + gtk_file_chooser_get_current_folder_uri( GTK_FILE_CHOOSER( m_pDialog ) ); + OUString aCurrentFolderName = uritounicode(pCurrentFolder); + g_free( pCurrentFolder ); + + return aCurrentFolderName; +} + +void SalGtkPicker::implsetTitle( const OUString& aTitle ) +{ + OSL_ASSERT( m_pDialog != nullptr ); + + OString aWindowTitle = OUStringToOString( aTitle, RTL_TEXTENCODING_UTF8 ); + + gtk_window_set_title( GTK_WINDOW( m_pDialog ), aWindowTitle.getStr() ); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/fpicker/SalGtkPicker.hxx b/vcl/unx/gtk3/fpicker/SalGtkPicker.hxx new file mode 100644 index 000000000..46d6d9278 --- /dev/null +++ b/vcl/unx/gtk3/fpicker/SalGtkPicker.hxx @@ -0,0 +1,117 @@ +/* -*- 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#ifndef INCLUDED_VCL_UNX_GTK_FPICKER_SALGTKPICKER_HXX +#define INCLUDED_VCL_UNX_GTK_FPICKER_SALGTKPICKER_HXX + +#include <osl/mutex.hxx> +#include <tools/link.hxx> +#include <cppuhelper/compbase.hxx> + +#include <com/sun/star/awt/XTopWindowListener.hpp> +#include <com/sun/star/awt/XExtendedToolkit.hpp> +#include <com/sun/star/frame/XDesktop.hpp> +#include <com/sun/star/frame/XTerminateListener.hpp> +#include <com/sun/star/uno/XComponentContext.hpp> + +#include <gtk/gtk.h> +#include <gdk/gdkkeysyms.h> + +#define FOLDERPICKER_TITLE 500 +#define FOLDER_PICKER_DEF_DESCRIPTION 501 +#define FILE_PICKER_TITLE_OPEN 502 +#define FILE_PICKER_TITLE_SAVE 503 +#define FILE_PICKER_FILE_TYPE 504 +#define FILE_PICKER_OVERWRITE_PRIMARY 505 +#define FILE_PICKER_OVERWRITE_SECONDARY 506 +#define FILE_PICKER_ALLFORMATS 507 + +class SalGtkPicker +{ + public: + SalGtkPicker( const css::uno::Reference<css::uno::XComponentContext>& xContext ); + virtual ~SalGtkPicker(); + protected: + osl::Mutex m_rbHelperMtx; + GtkWidget *m_pDialog; + protected: + /// @throws css::uno::RuntimeException + void implsetTitle( const OUString& aTitle ); + + /// @throws css::lang::IllegalArgumentException + /// @throws css::uno::RuntimeException + void implsetDisplayDirectory( const OUString& rDirectory ); + + /// @throws css::uno::RuntimeException + OUString implgetDisplayDirectory( ); + OUString uritounicode(const gchar *pIn); + OString unicodetouri(const OUString &rURL); + + // to instantiate own services + css::uno::Reference< css::uno::XComponentContext > m_xContext; + + static OUString getResString( sal_Int32 aId ); +}; + +//Run the Gtk Dialog. Watch for any "new windows" created while we're +//executing and consider that a CANCEL event to avoid e.g. "file cannot be opened" +//modal dialogs and this one getting locked if some other API call causes this +//to happen while we're opened waiting for user input, e.g. +//https://bugzilla.redhat.com/show_bug.cgi?id=441108 +class RunDialog : + public cppu::WeakComponentImplHelper< + css::awt::XTopWindowListener, + css::frame::XTerminateListener > +{ +private: + osl::Mutex maLock; + GtkWidget *mpDialog; + bool mbTerminateDesktop; + css::uno::Reference<css::awt::XExtendedToolkit> mxToolkit; + css::uno::Reference<css::frame::XDesktop> mxDesktop; + DECL_STATIC_LINK(RunDialog, TerminateDesktop, void*, void); +public: + + // XTopWindowListener + using cppu::WeakComponentImplHelperBase::disposing; + virtual void SAL_CALL disposing( const css::lang::EventObject& ) override {} + virtual void SAL_CALL windowOpened( const css::lang::EventObject& e ) override; + virtual void SAL_CALL windowClosing( const css::lang::EventObject& ) override {} + virtual void SAL_CALL windowClosed( const css::lang::EventObject& ) override {} + virtual void SAL_CALL windowMinimized( const css::lang::EventObject& ) override {} + virtual void SAL_CALL windowNormalized( const css::lang::EventObject& ) override {} + virtual void SAL_CALL windowActivated( const css::lang::EventObject& ) override {} + virtual void SAL_CALL windowDeactivated( const css::lang::EventObject& ) override {} + + // XTerminateListener + virtual void SAL_CALL queryTermination( const css::lang::EventObject& aEvent ) override; + virtual void SAL_CALL notifyTermination( const css::lang::EventObject& aEvent ) override; +public: + RunDialog(GtkWidget *pDialog, + const css::uno::Reference<css::awt::XExtendedToolkit>& rToolkit, + const css::uno::Reference<css::frame::XDesktop>& rDesktop); + virtual ~RunDialog() override; + gint run(); + void cancel(); + static GtkWindow* GetTransientFor(); +}; + +#endif + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/fpicker/eventnotification.hxx b/vcl/unx/gtk3/fpicker/eventnotification.hxx new file mode 100644 index 000000000..88a472e0e --- /dev/null +++ b/vcl/unx/gtk3/fpicker/eventnotification.hxx @@ -0,0 +1,43 @@ +/* -*- 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#ifndef INCLUDED_VCL_UNX_GTK_FPICKER_EVENTNOTIFICATION_HXX +#define INCLUDED_VCL_UNX_GTK_FPICKER_EVENTNOTIFICATION_HXX + +#include <com/sun/star/uno/XInterface.hpp> +#include <com/sun/star/uno/Reference.hxx> + +// encapsulate a filepicker event +// notification, because there are +// two types of filepicker notifications +// with and without parameter +// this is an application of the +// "command" pattern see GoF + +class CEventNotification +{ +public: + virtual ~CEventNotification() { }; + + virtual void SAL_CALL notifyEventListener( css::uno::Reference< css::uno::XInterface > xListener ) = 0; +}; + +#endif + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/fpicker/resourceprovider.cxx b/vcl/unx/gtk3/fpicker/resourceprovider.cxx new file mode 100644 index 000000000..5ad801a6e --- /dev/null +++ b/vcl/unx/gtk3/fpicker/resourceprovider.cxx @@ -0,0 +1,80 @@ +/* -*- 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <com/sun/star/ui/dialogs/CommonFilePickerElementIds.hpp> +#include <com/sun/star/ui/dialogs/ExtendedFilePickerElementIds.hpp> + +#include <strings.hrc> +#include <svdata.hxx> +#include "SalGtkPicker.hxx" + +using namespace ::com::sun::star::ui::dialogs::ExtendedFilePickerElementIds; +using namespace ::com::sun::star::ui::dialogs::CommonFilePickerElementIds; + +// translate control ids to resource ids + +static const struct +{ + sal_Int32 ctrlId; + const char *resId; +} CtrlIdToResIdTable[] = { + { CHECKBOX_AUTOEXTENSION, STR_FPICKER_AUTO_EXTENSION }, + { CHECKBOX_PASSWORD, STR_FPICKER_PASSWORD }, + { CHECKBOX_GPGENCRYPTION, STR_FPICKER_GPGENCRYPT }, + { CHECKBOX_FILTEROPTIONS, STR_FPICKER_FILTER_OPTIONS }, + { CHECKBOX_READONLY, STR_FPICKER_READONLY }, + { CHECKBOX_LINK, STR_FPICKER_INSERT_AS_LINK }, + { CHECKBOX_PREVIEW, STR_FPICKER_SHOW_PREVIEW }, + { PUSHBUTTON_PLAY, STR_FPICKER_PLAY }, + { LISTBOX_VERSION_LABEL, STR_FPICKER_VERSION }, + { LISTBOX_TEMPLATE_LABEL, STR_FPICKER_TEMPLATES }, + { LISTBOX_IMAGE_TEMPLATE_LABEL, STR_FPICKER_IMAGE_TEMPLATE }, + { LISTBOX_IMAGE_ANCHOR_LABEL, STR_FPICKER_IMAGE_ANCHOR }, + { CHECKBOX_SELECTION, STR_FPICKER_SELECTION }, + { FOLDERPICKER_TITLE, STR_FPICKER_FOLDER_DEFAULT_TITLE }, + { FOLDER_PICKER_DEF_DESCRIPTION, STR_FPICKER_FOLDER_DEFAULT_DESCRIPTION }, + { FILE_PICKER_OVERWRITE_PRIMARY, STR_FPICKER_ALREADYEXISTOVERWRITE_PRIMARY }, + { FILE_PICKER_OVERWRITE_SECONDARY, STR_FPICKER_ALREADYEXISTOVERWRITE_SECONDARY }, + { FILE_PICKER_ALLFORMATS, STR_FPICKER_ALLFORMATS }, + { FILE_PICKER_TITLE_OPEN, STR_FPICKER_OPEN }, + { FILE_PICKER_TITLE_SAVE, STR_FPICKER_SAVE }, + { FILE_PICKER_FILE_TYPE, STR_FPICKER_TYPE } +}; + +static const char* CtrlIdToResId( sal_Int32 aControlId ) +{ + for (auto & i : CtrlIdToResIdTable) + { + if ( i.ctrlId == aControlId ) + return i.resId; + } + return nullptr; +} + +OUString SalGtkPicker::getResString( sal_Int32 aId ) +{ + OUString aResString; + // translate the control id to a resource id + const char *pResId = CtrlIdToResId( aId ); + if (pResId) + aResString = VclResId(pResId); + return aResString.replace('~', '_'); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/gtk3gloactiongroup.cxx b/vcl/unx/gtk3/gtk3gloactiongroup.cxx new file mode 100644 index 000000000..c6d5eaf03 --- /dev/null +++ b/vcl/unx/gtk3/gtk3gloactiongroup.cxx @@ -0,0 +1,405 @@ +/* -*- 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 <unx/gtk/gtksalmenu.hxx> + +#include <unx/gtk/gloactiongroup.h> + +#include <sal/log.hxx> + +/* + * GLOAction + */ + +#define G_TYPE_LO_ACTION (g_lo_action_get_type ()) +#define G_LO_ACTION(inst) (G_TYPE_CHECK_INSTANCE_CAST ((inst), \ + G_TYPE_LO_ACTION, GLOAction)) +namespace { + +struct GLOAction +{ + GObject parent_instance; + + gint item_id; // Menu item ID. + bool submenu; // TRUE if action is a submenu action. + bool enabled; // TRUE if action is enabled. + GVariantType* parameter_type; // A GVariantType with the action parameter type. + GVariantType* state_type; // A GVariantType with item state type + GVariant* state_hint; // A GVariant with state hints. + GVariant* state; // A GVariant with current item state +}; + +} + +typedef GObjectClass GLOActionClass; + +#ifdef __GNUC__ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wunused-function" +#if defined __clang__ +#if __has_warning("-Wdeprecated-volatile") +#pragma clang diagnostic ignored "-Wdeprecated-volatile" +#endif +#endif +#endif +G_DEFINE_TYPE (GLOAction, g_lo_action, G_TYPE_OBJECT); +#ifdef __GNUC__ +#pragma GCC diagnostic pop +#endif + +static GLOAction* +g_lo_action_new() +{ + return G_LO_ACTION (g_object_new (G_TYPE_LO_ACTION, nullptr)); +} + +static void +g_lo_action_init (GLOAction *action) +{ + action->item_id = -1; + action->submenu = false; + action->enabled = true; + action->parameter_type = nullptr; + action->state_type = nullptr; + action->state_hint = nullptr; + action->state = nullptr; +} + +static void +g_lo_action_finalize (GObject *object) +{ + GLOAction* action = G_LO_ACTION(object); + + if (action->parameter_type) + g_variant_type_free (action->parameter_type); + + if (action->state_type) + g_variant_type_free (action->state_type); + + if (action->state_hint) + g_variant_unref (action->state_hint); + + if (action->state) + g_variant_unref (action->state); + + G_OBJECT_CLASS (g_lo_action_parent_class)->finalize (object); +} + +static void +g_lo_action_class_init (GLOActionClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS(klass); + + object_class->finalize = g_lo_action_finalize; +} + +/* + * GLOActionGroup + */ + +struct GLOActionGroupPrivate +{ + GHashTable *table; /* string -> GLOAction */ +}; + +static void g_lo_action_group_iface_init (GActionGroupInterface *); + +#ifdef __GNUC__ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wunused-function" +#if defined __clang__ +#if __has_warning("-Wdeprecated-volatile") +#pragma clang diagnostic ignored "-Wdeprecated-volatile" +#endif +#endif +#endif +G_DEFINE_TYPE_WITH_CODE (GLOActionGroup, + g_lo_action_group, G_TYPE_OBJECT, + G_ADD_PRIVATE(GLOActionGroup) + G_IMPLEMENT_INTERFACE (G_TYPE_ACTION_GROUP, + g_lo_action_group_iface_init)); +#ifdef __GNUC__ +#pragma GCC diagnostic pop +#endif + +static gchar ** +g_lo_action_group_list_actions (GActionGroup *group) +{ + GLOActionGroup *loGroup = G_LO_ACTION_GROUP (group); + GHashTableIter iter; + gint n, i = 0; + gchar **keys; + gpointer key; + + n = g_hash_table_size (loGroup->priv->table); + keys = g_new (gchar *, n + 1); + + g_hash_table_iter_init (&iter, loGroup->priv->table); + while (g_hash_table_iter_next (&iter, &key, nullptr)) + keys[i++] = g_strdup (static_cast<gchar*>(key)); + g_assert_cmpint (i, ==, n); + keys[n] = nullptr; + + return keys; +} + +static gboolean +g_lo_action_group_query_action (GActionGroup *group, + const gchar *action_name, + gboolean *enabled, + const GVariantType **parameter_type, + const GVariantType **state_type, + GVariant **state_hint, + GVariant **state) +{ + //SAL_INFO("vcl.unity", "g_lo_action_group_query_action on " << group); + GLOActionGroup *lo_group = G_LO_ACTION_GROUP (group); + GLOAction* action = G_LO_ACTION (g_hash_table_lookup (lo_group->priv->table, action_name)); + + if (action == nullptr) + return FALSE; + + if (enabled) + { + *enabled = action->enabled; + } + + if (parameter_type) + *parameter_type = action->parameter_type; + + if (state_type) + *state_type = action->state_type; + + if (state_hint) + *state_hint = (action->state_hint) ? g_variant_ref (action->state_hint) : nullptr; + + if (state) + *state = (action->state) ? g_variant_ref (action->state) : nullptr; + + return true; +} + +static void +g_lo_action_group_perform_submenu_action (GLOActionGroup *group, + const gchar *action_name, + GVariant *state) +{ + bool bState = g_variant_get_boolean (state); + SAL_INFO("vcl.unity", "g_lo_action_group_perform_submenu_action on " << group << " to " << bState); + + if (bState) + GtkSalMenu::Activate(action_name); + else + GtkSalMenu::Deactivate(action_name); +} + +static void +g_lo_action_group_change_state (GActionGroup *group, + const gchar *action_name, + GVariant *value) +{ + SAL_INFO("vcl.unity", "g_lo_action_group_change_state on " << group ); + g_return_if_fail (value != nullptr); + + g_variant_ref_sink (value); + + if (action_name != nullptr) + { + GLOActionGroup* lo_group = G_LO_ACTION_GROUP (group); + GLOAction* action = G_LO_ACTION (g_hash_table_lookup (lo_group->priv->table, action_name)); + + if (action != nullptr) + { + if (action->submenu) + g_lo_action_group_perform_submenu_action (lo_group, action_name, value); + else + { + bool is_new = false; + + /* If action already exists but has no state, it should be removed and added again. */ + if (action->state_type == nullptr) + { + g_action_group_action_removed (G_ACTION_GROUP (group), action_name); + action->state_type = g_variant_type_copy (g_variant_get_type(value)); + is_new = true; + } + + if (g_variant_is_of_type (value, action->state_type)) + { + if (action->state) + g_variant_unref(action->state); + + action->state = g_variant_ref (value); + + if (is_new) + g_action_group_action_added (G_ACTION_GROUP (group), action_name); + else + g_action_group_action_state_changed (group, action_name, value); + } + } + } + } + + g_variant_unref (value); +} + +static void +g_lo_action_group_activate (GActionGroup *group, + const gchar *action_name, + GVariant *parameter) +{ + if (parameter != nullptr) + g_action_group_change_action_state(group, action_name, parameter); + GtkSalMenu::DispatchCommand(action_name); +} + +void +g_lo_action_group_insert (GLOActionGroup *group, + const gchar *action_name, + gint item_id, + gboolean submenu) +{ + g_lo_action_group_insert_stateful (group, action_name, item_id, submenu, nullptr, nullptr, nullptr, nullptr); +} + +void +g_lo_action_group_insert_stateful (GLOActionGroup *group, + const gchar *action_name, + gint item_id, + gboolean submenu, + const GVariantType *parameter_type, + const GVariantType *state_type, + GVariant *state_hint, + GVariant *state) +{ + g_return_if_fail (G_IS_LO_ACTION_GROUP (group)); + + GLOAction* old_action = G_LO_ACTION (g_hash_table_lookup (group->priv->table, action_name)); + + if (old_action == nullptr || old_action->item_id != item_id) + { + if (old_action != nullptr) + g_lo_action_group_remove (group, action_name); + + GLOAction* action = g_lo_action_new(); + + g_hash_table_insert (group->priv->table, g_strdup (action_name), action); + + action->item_id = item_id; + action->submenu = submenu; + + if (parameter_type) + action->parameter_type = const_cast<GVariantType*>(parameter_type); + + if (state_type) + action->state_type = const_cast<GVariantType*>(state_type); + + if (state_hint) + action->state_hint = g_variant_ref_sink (state_hint); + + if (state) + action->state = g_variant_ref_sink (state); + + g_action_group_action_added (G_ACTION_GROUP (group), action_name); + } +} + +static void +g_lo_action_group_finalize (GObject *object) +{ + GLOActionGroup *lo_group = G_LO_ACTION_GROUP (object); + + g_hash_table_unref (lo_group->priv->table); + + G_OBJECT_CLASS (g_lo_action_group_parent_class)->finalize (object); +} + +static void +g_lo_action_group_init (GLOActionGroup *group) +{ + SAL_INFO("vcl.unity", "g_lo_action_group_init on " << group); + group->priv = static_cast<GLOActionGroupPrivate *>(g_lo_action_group_get_instance_private (group)); + group->priv->table = g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, g_object_unref); +} + +static void +g_lo_action_group_class_init (GLOActionGroupClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = g_lo_action_group_finalize; +} + +static void +g_lo_action_group_iface_init (GActionGroupInterface *iface) +{ + iface->list_actions = g_lo_action_group_list_actions; + iface->query_action = g_lo_action_group_query_action; + iface->change_action_state = g_lo_action_group_change_state; + iface->activate_action = g_lo_action_group_activate; +} + +GLOActionGroup * +g_lo_action_group_new() +{ + GLOActionGroup* group = G_LO_ACTION_GROUP (g_object_new (G_TYPE_LO_ACTION_GROUP, nullptr)); + return group; +} + +void +g_lo_action_group_set_action_enabled (GLOActionGroup *group, + const gchar *action_name, + gboolean enabled) +{ + SAL_INFO("vcl.unity", "g_lo_action_group_set_action_enabled on " << group); + g_return_if_fail (G_IS_LO_ACTION_GROUP (group)); + g_return_if_fail (action_name != nullptr); + + GLOAction* action = G_LO_ACTION (g_hash_table_lookup (group->priv->table, action_name)); + + if (action == nullptr) + return; + + action->enabled = enabled; + + g_action_group_action_enabled_changed (G_ACTION_GROUP (group), action_name, enabled); +} + +void +g_lo_action_group_remove (GLOActionGroup *group, + const gchar *action_name) +{ + SAL_INFO("vcl.unity", "g_lo_action_group_remove on " << group); + g_return_if_fail (G_IS_LO_ACTION_GROUP (group)); + + if (action_name != nullptr) + { + g_action_group_action_removed (G_ACTION_GROUP (group), action_name); + g_hash_table_remove (group->priv->table, action_name); + } +} + +void +g_lo_action_group_clear (GLOActionGroup *group) +{ + SAL_INFO("vcl.unity", "g_lo_action_group_clear on " << group); + g_return_if_fail (G_IS_LO_ACTION_GROUP (group)); + + GList* keys = g_hash_table_get_keys (group->priv->table); + + for (GList* element = g_list_first (keys); element != nullptr; element = g_list_next (element)) + { + g_lo_action_group_remove (group, static_cast<gchar*>(element->data)); + } + + g_list_free (keys); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/gtk3glomenu.cxx b/vcl/unx/gtk3/gtk3glomenu.cxx new file mode 100644 index 000000000..ca6887cb9 --- /dev/null +++ b/vcl/unx/gtk3/gtk3glomenu.cxx @@ -0,0 +1,692 @@ +/* -*- 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 <unx/gtk/glomenu.h> + +struct GLOMenu +{ + GMenuModel const parent_instance; + + GArray *items; +}; + +typedef GMenuModelClass GLOMenuClass; + +#ifdef __GNUC__ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wunused-function" +#if defined __clang__ +#if __has_warning("-Wdeprecated-volatile") +#pragma clang diagnostic ignored "-Wdeprecated-volatile" +#endif +#endif +#endif +G_DEFINE_TYPE (GLOMenu, g_lo_menu, G_TYPE_MENU_MODEL); +#ifdef __GNUC__ +#pragma GCC diagnostic pop +#endif + +namespace { + +struct item +{ + GHashTable* attributes; // Item attributes. + GHashTable* links; // Item links. +}; + +} + +static void +g_lo_menu_struct_item_init (struct item *menu_item) +{ + menu_item->attributes = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, reinterpret_cast<GDestroyNotify>(g_variant_unref)); + menu_item->links = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref); +} + +/* We treat attribute names the same as GSettings keys: + * - only lowercase ascii, digits and '-' + * - must start with lowercase + * - must not end with '-' + * - no consecutive '-' + * - not longer than 1024 chars + */ +static bool +valid_attribute_name (const gchar *name) +{ + gint i; + + if (!g_ascii_islower (name[0])) + return false; + + for (i = 1; name[i]; i++) + { + if (name[i] != '-' && + !g_ascii_islower (name[i]) && + !g_ascii_isdigit (name[i])) + return false; + + if (name[i] == '-' && name[i + 1] == '-') + return false; + } + + if (name[i - 1] == '-') + return false; + + if (i > 1024) + return false; + + return true; +} + +/* + * GLOMenu + */ + +static gboolean +g_lo_menu_is_mutable (GMenuModel*) +{ + // Menu is always mutable. + return true; +} + +static gint +g_lo_menu_get_n_items (GMenuModel *model) +{ + g_return_val_if_fail (model != nullptr, 0); + GLOMenu *menu = G_LO_MENU (model); + g_return_val_if_fail (menu->items != nullptr, 0); + + return menu->items->len; +} + +gint +g_lo_menu_get_n_items_from_section (GLOMenu *menu, + gint section) +{ + g_return_val_if_fail (0 <= section && section < static_cast<gint>(menu->items->len), 0); + + GLOMenu *model = g_lo_menu_get_section (menu, section); + + g_return_val_if_fail (model != nullptr, 0); + + gint length = model->items->len; + + g_object_unref (model); + + return length; +} + +static void +g_lo_menu_get_item_attributes (GMenuModel *model, + gint position, + GHashTable **table) +{ + GLOMenu *menu = G_LO_MENU (model); + *table = g_hash_table_ref (g_array_index (menu->items, struct item, position).attributes); +} + +static void +g_lo_menu_get_item_links (GMenuModel *model, + gint position, + GHashTable **table) +{ + GLOMenu *menu = G_LO_MENU (model); + *table = g_hash_table_ref (g_array_index (menu->items, struct item, position).links); +} + +void +g_lo_menu_insert (GLOMenu *menu, + gint position, + const gchar *label) +{ + g_lo_menu_insert_section (menu, position, label, nullptr); +} + +void +g_lo_menu_insert_in_section (GLOMenu *menu, + gint section, + gint position, + const gchar *label) +{ + g_return_if_fail (G_IS_LO_MENU (menu)); + g_return_if_fail (0 <= section && section < static_cast<gint>(menu->items->len)); + + GLOMenu *model = g_lo_menu_get_section (menu, section); + + g_return_if_fail (model != nullptr); + + g_lo_menu_insert (model, position, label); + + g_object_unref (model); +} + +GLOMenu * +g_lo_menu_new() +{ + return G_LO_MENU( g_object_new (G_TYPE_LO_MENU, nullptr) ); +} + +static void +g_lo_menu_set_attribute_value (GLOMenu *menu, + gint position, + const gchar *attribute, + GVariant *value) +{ + g_return_if_fail (G_IS_LO_MENU (menu)); + g_return_if_fail (attribute != nullptr); + g_return_if_fail (valid_attribute_name (attribute)); + + if (position >= static_cast<gint>(menu->items->len)) + return; + + struct item menu_item = g_array_index (menu->items, struct item, position); + + if (value != nullptr) + g_hash_table_insert (menu_item.attributes, g_strdup (attribute), g_variant_ref_sink (value)); + else + g_hash_table_remove (menu_item.attributes, attribute); +} + +static GVariant* +g_lo_menu_get_attribute_value_from_item_in_section (GLOMenu *menu, + gint section, + gint position, + const gchar *attribute, + const GVariantType *type) +{ + GMenuModel *model = G_MENU_MODEL (g_lo_menu_get_section (menu, section)); + + g_return_val_if_fail (model != nullptr, nullptr); + + GVariant *value = g_menu_model_get_item_attribute_value (model, + position, + attribute, + type); + + g_object_unref (model); + + return value; +} + +void +g_lo_menu_set_label (GLOMenu *menu, + gint position, + const gchar *label) +{ + g_return_if_fail (G_IS_LO_MENU (menu)); + + GVariant *value; + + if (label != nullptr) + value = g_variant_new_string (label); + else + value = nullptr; + + g_lo_menu_set_attribute_value (menu, position, G_MENU_ATTRIBUTE_LABEL, value); +} + +void +g_lo_menu_set_icon (GLOMenu *menu, + gint position, + const GIcon *icon) +{ + g_return_if_fail (G_IS_LO_MENU (menu)); + + GVariant *value; + + if (icon != nullptr) + { +#if GLIB_CHECK_VERSION(2,38,0) + value = g_icon_serialize (const_cast<GIcon*>(icon)); +#else + value = nullptr; +#endif + } + else + value = nullptr; + +#ifndef G_MENU_ATTRIBUTE_ICON +# define G_MENU_ATTRIBUTE_ICON "icon" +#endif + + g_lo_menu_set_attribute_value (menu, position, G_MENU_ATTRIBUTE_ICON, value); + if (value) + g_variant_unref (value); +} + +void +g_lo_menu_set_label_to_item_in_section (GLOMenu *menu, + gint section, + gint position, + const gchar *label) +{ + g_return_if_fail (G_IS_LO_MENU (menu)); + + GLOMenu *model = g_lo_menu_get_section (menu, section); + + g_return_if_fail (model != nullptr); + + g_lo_menu_set_label (model, position, label); + + // Notify the update. + g_menu_model_items_changed (G_MENU_MODEL (model), position, 1, 1); + + g_object_unref (model); +} + +void +g_lo_menu_set_icon_to_item_in_section (GLOMenu *menu, + gint section, + gint position, + const GIcon *icon) +{ + g_return_if_fail (G_IS_LO_MENU (menu)); + + GLOMenu *model = g_lo_menu_get_section (menu, section); + + g_return_if_fail (model != nullptr); + + g_lo_menu_set_icon (model, position, icon); + + // Notify the update. + g_menu_model_items_changed (G_MENU_MODEL (model), position, 1, 1); + + g_object_unref (model); +} + +gchar * +g_lo_menu_get_label_from_item_in_section (GLOMenu *menu, + gint section, + gint position) +{ + g_return_val_if_fail (G_IS_LO_MENU (menu), nullptr); + + GVariant *label_value = g_lo_menu_get_attribute_value_from_item_in_section (menu, + section, + position, + G_MENU_ATTRIBUTE_LABEL, + G_VARIANT_TYPE_STRING); + + gchar *label = nullptr; + + if (label_value) + { + label = g_variant_dup_string (label_value, nullptr); + g_variant_unref (label_value); + } + + return label; +} + +void +g_lo_menu_set_action_and_target_value (GLOMenu *menu, + gint position, + const gchar *action, + GVariant *target_value) +{ + g_return_if_fail (G_IS_LO_MENU (menu)); + + GVariant *action_value; + + if (action != nullptr) + { + action_value = g_variant_new_string (action); + } + else + { + action_value = nullptr; + target_value = nullptr; + } + + g_lo_menu_set_attribute_value (menu, position, G_MENU_ATTRIBUTE_ACTION, action_value); + g_lo_menu_set_attribute_value (menu, position, G_MENU_ATTRIBUTE_TARGET, target_value); + g_lo_menu_set_attribute_value (menu, position, G_LO_MENU_ATTRIBUTE_SUBMENU_ACTION, nullptr); + + g_menu_model_items_changed (G_MENU_MODEL (menu), position, 1, 1); +} + +void +g_lo_menu_set_action_and_target_value_to_item_in_section (GLOMenu *menu, + gint section, + gint position, + const gchar *command, + GVariant *target_value) +{ + g_return_if_fail (G_IS_LO_MENU (menu)); + + GLOMenu *model = g_lo_menu_get_section (menu, section); + + g_return_if_fail (model != nullptr); + + g_lo_menu_set_action_and_target_value (model, position, command, target_value); + + g_object_unref (model); +} + +void +g_lo_menu_set_accelerator_to_item_in_section (GLOMenu *menu, + gint section, + gint position, + const gchar *accelerator) +{ + g_return_if_fail (G_IS_LO_MENU (menu)); + + GLOMenu *model = g_lo_menu_get_section (menu, section); + + g_return_if_fail (model != nullptr); + + GVariant *value; + + if (accelerator != nullptr) + value = g_variant_new_string (accelerator); + else + value = nullptr; + + g_lo_menu_set_attribute_value (model, position, G_LO_MENU_ATTRIBUTE_ACCELERATOR, value); + + // Notify the update. + g_menu_model_items_changed (G_MENU_MODEL (model), position, 1, 1); + + g_object_unref (model); +} + +gchar * +g_lo_menu_get_accelerator_from_item_in_section (GLOMenu *menu, + gint section, + gint position) +{ + g_return_val_if_fail (G_IS_LO_MENU (menu), nullptr); + + GVariant *accel_value = g_lo_menu_get_attribute_value_from_item_in_section (menu, + section, + position, + G_LO_MENU_ATTRIBUTE_ACCELERATOR, + G_VARIANT_TYPE_STRING); + + gchar *accel = nullptr; + + if (accel_value != nullptr) + { + accel = g_variant_dup_string (accel_value, nullptr); + g_variant_unref (accel_value); + } + + return accel; +} + +void +g_lo_menu_set_command_to_item_in_section (GLOMenu *menu, + gint section, + gint position, + const gchar *command) +{ + g_return_if_fail (G_IS_LO_MENU (menu)); + + GLOMenu *model = g_lo_menu_get_section (menu, section); + + g_return_if_fail (model != nullptr); + + GVariant *value; + + if (command != nullptr) + value = g_variant_new_string (command); + else + value = nullptr; + + g_lo_menu_set_attribute_value (model, position, G_LO_MENU_ATTRIBUTE_COMMAND, value); + + // Notify the update. + g_menu_model_items_changed (G_MENU_MODEL (model), position, 1, 1); + + g_object_unref (model); +} + +gchar * +g_lo_menu_get_command_from_item_in_section (GLOMenu *menu, + gint section, + gint position) +{ + g_return_val_if_fail (G_IS_LO_MENU (menu), nullptr); + + GVariant *command_value = g_lo_menu_get_attribute_value_from_item_in_section (menu, + section, + position, + G_LO_MENU_ATTRIBUTE_COMMAND, + G_VARIANT_TYPE_STRING); + + gchar *command = nullptr; + + if (command_value != nullptr) + { + command = g_variant_dup_string (command_value, nullptr); + g_variant_unref (command_value); + } + + return command; +} + +static void +g_lo_menu_set_link (GLOMenu *menu, + gint position, + const gchar *link, + GMenuModel *model) +{ + g_return_if_fail (G_IS_LO_MENU (menu)); + g_return_if_fail (link != nullptr); + g_return_if_fail (valid_attribute_name (link)); + + if (position < 0 || position >= static_cast<gint>(menu->items->len)) + position = menu->items->len - 1; + + struct item menu_item = g_array_index (menu->items, struct item, position); + + if (model != nullptr) + g_hash_table_insert (menu_item.links, g_strdup (link), g_object_ref (model)); + else + g_hash_table_remove (menu_item.links, link); +} + +void +g_lo_menu_insert_section (GLOMenu *menu, + gint position, + const gchar *label, + GMenuModel *section) +{ + g_return_if_fail (G_IS_LO_MENU (menu)); + + if (position < 0 || position > static_cast<gint>(menu->items->len)) + position = menu->items->len; + + struct item menu_item; + + g_lo_menu_struct_item_init(&menu_item); + + g_array_insert_val (menu->items, position, menu_item); + + g_lo_menu_set_label (menu, position, label); + g_lo_menu_set_link (menu, position, G_MENU_LINK_SECTION, section); + + g_menu_model_items_changed (G_MENU_MODEL (menu), position, 0, 1); +} + +void +g_lo_menu_new_section (GLOMenu *menu, + gint position, + const gchar *label) +{ + GMenuModel *section = G_MENU_MODEL (g_lo_menu_new()); + + g_lo_menu_insert_section (menu, position, label, section); + + g_object_unref (section); +} + +GLOMenu * +g_lo_menu_get_section (GLOMenu *menu, + gint section) +{ + g_return_val_if_fail (G_IS_LO_MENU (menu), nullptr); + + return G_LO_MENU (G_MENU_MODEL_CLASS (g_lo_menu_parent_class) + ->get_item_link (G_MENU_MODEL (menu), section, G_MENU_LINK_SECTION)); +} + +void +g_lo_menu_new_submenu_in_item_in_section (GLOMenu *menu, + gint section, + gint position) +{ + g_return_if_fail (G_IS_LO_MENU (menu)); + g_return_if_fail (0 <= section && section < static_cast<gint>(menu->items->len)); + + GLOMenu* model = g_lo_menu_get_section (menu, section); + + g_return_if_fail (model != nullptr); + + if (0 <= position && position < static_cast<gint>(model->items->len)) { + GMenuModel* submenu = G_MENU_MODEL (g_lo_menu_new()); + + g_lo_menu_set_link (model, position, G_MENU_LINK_SUBMENU, submenu); + + g_object_unref (submenu); + + g_menu_model_items_changed (G_MENU_MODEL (model), position, 1, 1); + + g_object_unref (model); + } +} + +GLOMenu * +g_lo_menu_get_submenu_from_item_in_section (GLOMenu *menu, + gint section, + gint position) +{ + g_return_val_if_fail (G_IS_LO_MENU (menu), nullptr); + g_return_val_if_fail (0 <= section && section < static_cast<gint>(menu->items->len), nullptr); + + GLOMenu *model = g_lo_menu_get_section (menu, section); + + g_return_val_if_fail (model != nullptr, nullptr); + + GLOMenu *submenu = nullptr; + + if (0 <= position && position < static_cast<gint>(model->items->len)) + submenu = G_LO_MENU (G_MENU_MODEL_CLASS (g_lo_menu_parent_class) + ->get_item_link (G_MENU_MODEL (model), position, G_MENU_LINK_SUBMENU)); + //submenu = g_menu_model_get_item_link (G_MENU_MODEL (model), position, G_MENU_LINK_SUBMENU); + + g_object_unref (model); + + return submenu; +} + +void +g_lo_menu_set_submenu_action_to_item_in_section (GLOMenu *menu, + gint section, + gint position, + const gchar *action) +{ + g_return_if_fail (G_IS_LO_MENU (menu)); + + GMenuModel *model = G_MENU_MODEL (g_lo_menu_get_section (menu, section)); + + g_return_if_fail (model != nullptr); + + GVariant *value; + + if (action != nullptr) + value = g_variant_new_string (action); + else + value = nullptr; + + g_lo_menu_set_attribute_value (G_LO_MENU (model), position, G_LO_MENU_ATTRIBUTE_SUBMENU_ACTION, value); + + // Notify the update. + g_menu_model_items_changed (model, position, 1, 1); + + g_object_unref (model); +} + +static void +g_lo_menu_clear_item (struct item *menu_item) +{ + if (menu_item->attributes != nullptr) + g_hash_table_unref (menu_item->attributes); + if (menu_item->links != nullptr) + g_hash_table_unref (menu_item->links); +} + +void +g_lo_menu_remove (GLOMenu *menu, + gint position) +{ + g_return_if_fail (G_IS_LO_MENU (menu)); + g_return_if_fail (0 <= position && position < static_cast<gint>(menu->items->len)); + + g_lo_menu_clear_item (&g_array_index (menu->items, struct item, position)); + g_array_remove_index (menu->items, position); + g_menu_model_items_changed (G_MENU_MODEL (menu), position, 1, 0); +} + +void +g_lo_menu_remove_from_section (GLOMenu *menu, + gint section, + gint position) +{ + g_return_if_fail (G_IS_LO_MENU (menu)); + g_return_if_fail (0 <= section && section < static_cast<gint>(menu->items->len)); + + GLOMenu *model = g_lo_menu_get_section (menu, section); + + g_return_if_fail (model != nullptr); + + g_lo_menu_remove (model, position); + + g_object_unref (model); +} + +static void +g_lo_menu_finalize (GObject *object) +{ + GLOMenu *menu = G_LO_MENU (object); + struct item *items; + gint n_items; + gint i; + + n_items = menu->items->len; + items = reinterpret_cast<struct item *>(g_array_free (menu->items, FALSE)); + for (i = 0; i < n_items; i++) + g_lo_menu_clear_item (&items[i]); + g_free (items); + + G_OBJECT_CLASS (g_lo_menu_parent_class) + ->finalize (object); +} + +static void +g_lo_menu_init (GLOMenu *menu) +{ + menu->items = g_array_new (FALSE, FALSE, sizeof (struct item)); +} + +static void +g_lo_menu_class_init (GLOMenuClass *klass) +{ + GMenuModelClass *model_class = G_MENU_MODEL_CLASS (klass); + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = g_lo_menu_finalize; + + model_class->is_mutable = g_lo_menu_is_mutable; + model_class->get_n_items = g_lo_menu_get_n_items; + model_class->get_item_attributes = g_lo_menu_get_item_attributes; + model_class->get_item_links = g_lo_menu_get_item_links; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/gtk3gtkdata.cxx b/vcl/unx/gtk3/gtk3gtkdata.cxx new file mode 100644 index 000000000..3182d1551 --- /dev/null +++ b/vcl/unx/gtk3/gtk3gtkdata.cxx @@ -0,0 +1,778 @@ +/* -*- 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <unistd.h> + +#include <stdio.h> +#include <stdlib.h> +#if defined(FREEBSD) || defined(NETBSD) +#include <sys/types.h> +#include <sys/time.h> +#endif +#include <unx/gtk/gtkbackend.hxx> +#include <unx/gtk/gtkdata.hxx> +#include <unx/gtk/gtkinst.hxx> +#include <unx/gtk/gtkframe.hxx> +#include <bitmaps.hlst> +#include <cursor_hotspots.hxx> +#include <o3tl/safeint.hxx> +#include <osl/thread.h> +#include <osl/process.h> + +#include <vcl/svapp.hxx> +#include <sal/log.hxx> + +#include <chrono> + + + +using namespace vcl_sal; + +/*************************************************************** + * class GtkSalDisplay * + ***************************************************************/ +extern "C" { +static GdkFilterReturn call_filterGdkEvent( GdkXEvent* sys_event, + GdkEvent* /*event*/, + gpointer data ) +{ + GtkSalDisplay *pDisplay = static_cast<GtkSalDisplay *>(data); + return pDisplay->filterGdkEvent( sys_event ); +} +} + +GtkSalDisplay::GtkSalDisplay( GdkDisplay* pDisplay ) : + m_pSys( GtkSalSystem::GetSingleton() ), + m_pGdkDisplay( pDisplay ), + m_bStartupCompleted( false ) +{ + for(GdkCursor* & rpCsr : m_aCursors) + rpCsr = nullptr; + + // FIXME: unify this with SalInst's filter too ? + gdk_window_add_filter( nullptr, call_filterGdkEvent, this ); + + if ( getenv( "SAL_IGNOREXERRORS" ) ) + GetGenericUnixSalData()->ErrorTrapPush(); // and leak the trap + + m_bX11Display = DLSYM_GDK_IS_X11_DISPLAY( m_pGdkDisplay ); + + gtk_widget_set_default_direction(AllSettings::GetLayoutRTL() ? GTK_TEXT_DIR_RTL : GTK_TEXT_DIR_LTR); +} + +GtkSalDisplay::~GtkSalDisplay() +{ + gdk_window_remove_filter( nullptr, call_filterGdkEvent, this ); + + if( !m_bStartupCompleted ) + gdk_notify_startup_complete(); + + for(GdkCursor* & rpCsr : m_aCursors) + if( rpCsr ) + gdk_cursor_unref( rpCsr ); +} + +extern "C" { + +static void signalScreenSizeChanged( GdkScreen* pScreen, gpointer data ) +{ + GtkSalDisplay* pDisp = static_cast<GtkSalDisplay*>(data); + pDisp->screenSizeChanged( pScreen ); +} + +static void signalMonitorsChanged( GdkScreen* pScreen, gpointer data ) +{ + GtkSalDisplay* pDisp = static_cast<GtkSalDisplay*>(data); + pDisp->monitorsChanged( pScreen ); +} + +} + +GdkFilterReturn GtkSalDisplay::filterGdkEvent( GdkXEvent* ) +{ + (void) this; // loplugin:staticmethods + //FIXME: implement filterGdkEvent ... + return GDK_FILTER_CONTINUE; +} + +void GtkSalDisplay::screenSizeChanged( GdkScreen const * pScreen ) +{ + m_pSys->countScreenMonitors(); + if (pScreen) + emitDisplayChanged(); +} + +void GtkSalDisplay::monitorsChanged( GdkScreen const * pScreen ) +{ + m_pSys->countScreenMonitors(); + if (pScreen) + emitDisplayChanged(); +} + +GdkCursor* GtkSalDisplay::getFromSvg(OUString const & name, int nXHot, int nYHot) +{ + GdkPixbuf* pPixBuf = load_icon_by_name(name); + assert(pPixBuf && "missing image?"); + if (!pPixBuf) + return nullptr; + + guint nDefaultCursorSize = gdk_display_get_default_cursor_size( m_pGdkDisplay ); + int nPixWidth = gdk_pixbuf_get_width(pPixBuf); + int nPixHeight = gdk_pixbuf_get_height(pPixBuf); + double fScalefactor = static_cast<double>(nDefaultCursorSize) / std::max(nPixWidth, nPixHeight); + GdkPixbuf* pScaledPixBuf = gdk_pixbuf_scale_simple(pPixBuf, + nPixWidth * fScalefactor, + nPixHeight * fScalefactor, + GDK_INTERP_HYPER); + g_object_unref(pPixBuf); + GdkCursor* pCursor = gdk_cursor_new_from_pixbuf(m_pGdkDisplay, pScaledPixBuf, + nXHot * fScalefactor, nYHot * fScalefactor); + return pCursor; +} + +#define MAKE_CURSOR( vcl_name, name, name2 ) \ + case vcl_name: \ + pCursor = getFromSvg(name2, name##curs_x_hot, name##curs_y_hot); \ + break +#define MAP_BUILTIN( vcl_name, gdk_name ) \ + case vcl_name: \ + pCursor = gdk_cursor_new_for_display( m_pGdkDisplay, gdk_name ); \ + break + +GdkCursor *GtkSalDisplay::getCursor( PointerStyle ePointerStyle ) +{ + if ( !m_aCursors[ ePointerStyle ] ) + { + GdkCursor *pCursor = nullptr; + + switch( ePointerStyle ) + { + MAP_BUILTIN( PointerStyle::Arrow, GDK_LEFT_PTR ); + MAP_BUILTIN( PointerStyle::Text, GDK_XTERM ); + MAP_BUILTIN( PointerStyle::Help, GDK_QUESTION_ARROW ); + MAP_BUILTIN( PointerStyle::Cross, GDK_CROSSHAIR ); + MAP_BUILTIN( PointerStyle::Wait, GDK_WATCH ); + + MAP_BUILTIN( PointerStyle::NSize, GDK_SB_V_DOUBLE_ARROW ); + MAP_BUILTIN( PointerStyle::SSize, GDK_SB_V_DOUBLE_ARROW ); + MAP_BUILTIN( PointerStyle::WSize, GDK_SB_H_DOUBLE_ARROW ); + MAP_BUILTIN( PointerStyle::ESize, GDK_SB_H_DOUBLE_ARROW ); + + MAP_BUILTIN( PointerStyle::NWSize, GDK_TOP_LEFT_CORNER ); + MAP_BUILTIN( PointerStyle::NESize, GDK_TOP_RIGHT_CORNER ); + MAP_BUILTIN( PointerStyle::SWSize, GDK_BOTTOM_LEFT_CORNER ); + MAP_BUILTIN( PointerStyle::SESize, GDK_BOTTOM_RIGHT_CORNER ); + + MAP_BUILTIN( PointerStyle::WindowNSize, GDK_TOP_SIDE ); + MAP_BUILTIN( PointerStyle::WindowSSize, GDK_BOTTOM_SIDE ); + MAP_BUILTIN( PointerStyle::WindowWSize, GDK_LEFT_SIDE ); + MAP_BUILTIN( PointerStyle::WindowESize, GDK_RIGHT_SIDE ); + + MAP_BUILTIN( PointerStyle::WindowNWSize, GDK_TOP_LEFT_CORNER ); + MAP_BUILTIN( PointerStyle::WindowNESize, GDK_TOP_RIGHT_CORNER ); + MAP_BUILTIN( PointerStyle::WindowSWSize, GDK_BOTTOM_LEFT_CORNER ); + MAP_BUILTIN( PointerStyle::WindowSESize, GDK_BOTTOM_RIGHT_CORNER ); + + MAP_BUILTIN( PointerStyle::HSizeBar, GDK_SB_H_DOUBLE_ARROW ); + MAP_BUILTIN( PointerStyle::VSizeBar, GDK_SB_V_DOUBLE_ARROW ); + + MAP_BUILTIN( PointerStyle::RefHand, GDK_HAND2 ); + MAP_BUILTIN( PointerStyle::Hand, GDK_HAND2 ); + MAP_BUILTIN( PointerStyle::Pen, GDK_PENCIL ); + + MAP_BUILTIN( PointerStyle::HSplit, GDK_SB_H_DOUBLE_ARROW ); + MAP_BUILTIN( PointerStyle::VSplit, GDK_SB_V_DOUBLE_ARROW ); + + MAP_BUILTIN( PointerStyle::Move, GDK_FLEUR ); + + MAKE_CURSOR( PointerStyle::Null, null, RID_CURSOR_NULL ); + MAKE_CURSOR( PointerStyle::Magnify, magnify_, RID_CURSOR_MAGNIFY ); + MAKE_CURSOR( PointerStyle::Fill, fill_, RID_CURSOR_FILL ); + MAKE_CURSOR( PointerStyle::MoveData, movedata_, RID_CURSOR_MOVE_DATA ); + MAKE_CURSOR( PointerStyle::CopyData, copydata_, RID_CURSOR_COPY_DATA ); + MAKE_CURSOR( PointerStyle::MoveFile, movefile_, RID_CURSOR_MOVE_FILE ); + MAKE_CURSOR( PointerStyle::CopyFile, copyfile_, RID_CURSOR_COPY_FILE ); + MAKE_CURSOR( PointerStyle::MoveFiles, movefiles_, RID_CURSOR_MOVE_FILES ); + MAKE_CURSOR( PointerStyle::CopyFiles, copyfiles_, RID_CURSOR_COPY_FILES ); + MAKE_CURSOR( PointerStyle::NotAllowed, nodrop_, RID_CURSOR_NOT_ALLOWED ); + MAKE_CURSOR( PointerStyle::Rotate, rotate_, RID_CURSOR_ROTATE ); + MAKE_CURSOR( PointerStyle::HShear, hshear_, RID_CURSOR_H_SHEAR ); + MAKE_CURSOR( PointerStyle::VShear, vshear_, RID_CURSOR_V_SHEAR ); + MAKE_CURSOR( PointerStyle::DrawLine, drawline_, RID_CURSOR_DRAW_LINE ); + MAKE_CURSOR( PointerStyle::DrawRect, drawrect_, RID_CURSOR_DRAW_RECT ); + MAKE_CURSOR( PointerStyle::DrawPolygon, drawpolygon_, RID_CURSOR_DRAW_POLYGON ); + MAKE_CURSOR( PointerStyle::DrawBezier, drawbezier_, RID_CURSOR_DRAW_BEZIER ); + MAKE_CURSOR( PointerStyle::DrawArc, drawarc_, RID_CURSOR_DRAW_ARC ); + MAKE_CURSOR( PointerStyle::DrawPie, drawpie_, RID_CURSOR_DRAW_PIE ); + MAKE_CURSOR( PointerStyle::DrawCircleCut, drawcirclecut_, RID_CURSOR_DRAW_CIRCLE_CUT ); + MAKE_CURSOR( PointerStyle::DrawEllipse, drawellipse_, RID_CURSOR_DRAW_ELLIPSE ); + MAKE_CURSOR( PointerStyle::DrawConnect, drawconnect_, RID_CURSOR_DRAW_CONNECT ); + MAKE_CURSOR( PointerStyle::DrawText, drawtext_, RID_CURSOR_DRAW_TEXT ); + MAKE_CURSOR( PointerStyle::Mirror, mirror_, RID_CURSOR_MIRROR ); + MAKE_CURSOR( PointerStyle::Crook, crook_, RID_CURSOR_CROOK ); + MAKE_CURSOR( PointerStyle::Crop, crop_, RID_CURSOR_CROP ); + MAKE_CURSOR( PointerStyle::MovePoint, movepoint_, RID_CURSOR_MOVE_POINT ); + MAKE_CURSOR( PointerStyle::MoveBezierWeight, movebezierweight_, RID_CURSOR_MOVE_BEZIER_WEIGHT ); + MAKE_CURSOR( PointerStyle::DrawFreehand, drawfreehand_, RID_CURSOR_DRAW_FREEHAND ); + MAKE_CURSOR( PointerStyle::DrawCaption, drawcaption_, RID_CURSOR_DRAW_CAPTION ); + MAKE_CURSOR( PointerStyle::LinkData, linkdata_, RID_CURSOR_LINK_DATA ); + MAKE_CURSOR( PointerStyle::MoveDataLink, movedlnk_, RID_CURSOR_MOVE_DATA_LINK ); + MAKE_CURSOR( PointerStyle::CopyDataLink, copydlnk_, RID_CURSOR_COPY_DATA_LINK ); + MAKE_CURSOR( PointerStyle::LinkFile, linkfile_, RID_CURSOR_LINK_FILE ); + MAKE_CURSOR( PointerStyle::MoveFileLink, moveflnk_, RID_CURSOR_MOVE_FILE_LINK ); + MAKE_CURSOR( PointerStyle::CopyFileLink, copyflnk_, RID_CURSOR_COPY_FILE_LINK ); + MAKE_CURSOR( PointerStyle::Chart, chart_, RID_CURSOR_CHART ); + MAKE_CURSOR( PointerStyle::Detective, detective_, RID_CURSOR_DETECTIVE ); + MAKE_CURSOR( PointerStyle::PivotCol, pivotcol_, RID_CURSOR_PIVOT_COLUMN ); + MAKE_CURSOR( PointerStyle::PivotRow, pivotrow_, RID_CURSOR_PIVOT_ROW ); + MAKE_CURSOR( PointerStyle::PivotField, pivotfld_, RID_CURSOR_PIVOT_FIELD ); + MAKE_CURSOR( PointerStyle::PivotDelete, pivotdel_, RID_CURSOR_PIVOT_DELETE ); + MAKE_CURSOR( PointerStyle::Chain, chain_, RID_CURSOR_CHAIN ); + MAKE_CURSOR( PointerStyle::ChainNotAllowed, chainnot_, RID_CURSOR_CHAIN_NOT_ALLOWED ); + MAKE_CURSOR( PointerStyle::AutoScrollN, asn_, RID_CURSOR_AUTOSCROLL_N ); + MAKE_CURSOR( PointerStyle::AutoScrollS, ass_, RID_CURSOR_AUTOSCROLL_S ); + MAKE_CURSOR( PointerStyle::AutoScrollW, asw_, RID_CURSOR_AUTOSCROLL_W ); + MAKE_CURSOR( PointerStyle::AutoScrollE, ase_, RID_CURSOR_AUTOSCROLL_E ); + MAKE_CURSOR( PointerStyle::AutoScrollNW, asnw_, RID_CURSOR_AUTOSCROLL_NW ); + MAKE_CURSOR( PointerStyle::AutoScrollNE, asne_, RID_CURSOR_AUTOSCROLL_NE ); + MAKE_CURSOR( PointerStyle::AutoScrollSW, assw_, RID_CURSOR_AUTOSCROLL_SW ); + MAKE_CURSOR( PointerStyle::AutoScrollSE, asse_, RID_CURSOR_AUTOSCROLL_SE ); + MAKE_CURSOR( PointerStyle::AutoScrollNS, asns_, RID_CURSOR_AUTOSCROLL_NS ); + MAKE_CURSOR( PointerStyle::AutoScrollWE, aswe_, RID_CURSOR_AUTOSCROLL_WE ); + MAKE_CURSOR( PointerStyle::AutoScrollNSWE, asnswe_, RID_CURSOR_AUTOSCROLL_NSWE ); + MAKE_CURSOR( PointerStyle::TextVertical, vertcurs_, RID_CURSOR_TEXT_VERTICAL ); + + // #i32329# + MAKE_CURSOR( PointerStyle::TabSelectS, tblsels_, RID_CURSOR_TAB_SELECT_S ); + MAKE_CURSOR( PointerStyle::TabSelectE, tblsele_, RID_CURSOR_TAB_SELECT_E ); + MAKE_CURSOR( PointerStyle::TabSelectSE, tblselse_, RID_CURSOR_TAB_SELECT_SE ); + MAKE_CURSOR( PointerStyle::TabSelectW, tblselw_, RID_CURSOR_TAB_SELECT_W ); + MAKE_CURSOR( PointerStyle::TabSelectSW, tblselsw_, RID_CURSOR_TAB_SELECT_SW ); + + MAKE_CURSOR( PointerStyle::HideWhitespace, hidewhitespace_, RID_CURSOR_HIDE_WHITESPACE ); + MAKE_CURSOR( PointerStyle::ShowWhitespace, showwhitespace_, RID_CURSOR_SHOW_WHITESPACE ); + + default: + SAL_WARN( "vcl.gtk", "pointer " << static_cast<int>(ePointerStyle) << "not implemented" ); + break; + } + if( !pCursor ) + pCursor = gdk_cursor_new_for_display( m_pGdkDisplay, GDK_LEFT_PTR ); + + m_aCursors[ ePointerStyle ] = pCursor; + } + + return m_aCursors[ ePointerStyle ]; +} + +int GtkSalDisplay::CaptureMouse( SalFrame* pSFrame ) +{ + GtkSalFrame* pFrame = static_cast<GtkSalFrame*>(pSFrame); + + if( !pFrame ) + { + if( m_pCapture ) + static_cast<GtkSalFrame*>(m_pCapture)->grabPointer( false, false, false ); + m_pCapture = nullptr; + return 0; + } + + if( m_pCapture ) + { + if( pFrame == m_pCapture ) + return 1; + static_cast<GtkSalFrame*>(m_pCapture)->grabPointer( false, false, false ); + } + + m_pCapture = pFrame; + pFrame->grabPointer( true, false, false ); + return 1; +} + +/********************************************************************** + * class GtkSalData * + **********************************************************************/ + +GtkSalData::GtkSalData( SalInstance *pInstance ) + : GenericUnixSalData( SAL_DATA_GTK3, pInstance ) + , m_aDispatchMutex() + , m_aDispatchCondition() + , m_pDocumentFocusListener(nullptr) +{ + m_pUserEvent = nullptr; +} + +#if defined(GDK_WINDOWING_X11) +static XIOErrorHandler aOrigXIOErrorHandler = nullptr; + +extern "C" { + +static int XIOErrorHdl(Display *) +{ + fprintf(stderr, "X IO Error\n"); + _exit(1); + // avoid crashes in unrelated threads that still run while atexit + // handlers are in progress +} + +} +#endif + +GtkSalData::~GtkSalData() +{ + Yield( true, true ); + g_warning ("TESTME: We used to have a stop-timer here, but the central code should do this"); + + // sanity check: at this point nobody should be yielding, but wake them + // up anyway before the condition they're waiting on gets destroyed. + m_aDispatchCondition.set(); + + osl::MutexGuard g( m_aDispatchMutex ); + if (m_pUserEvent) + { + g_source_destroy (m_pUserEvent); + g_source_unref (m_pUserEvent); + m_pUserEvent = nullptr; + } +#if defined(GDK_WINDOWING_X11) + if (DLSYM_GDK_IS_X11_DISPLAY(gdk_display_get_default())) + XSetIOErrorHandler(aOrigXIOErrorHandler); +#endif +} + +void GtkSalData::Dispose() +{ + deInitNWF(); +} + +/// Allows events to be processed, returns true if we processed an event. +bool GtkSalData::Yield( bool bWait, bool bHandleAllCurrentEvents ) +{ + /* #i33212# only enter g_main_context_iteration in one thread at any one + * time, else one of them potentially will never end as long as there is + * another thread in there. Having only one yielding thread actually dispatch + * fits the vcl event model (see e.g. the generic plugin). + */ + bool bDispatchThread = false; + bool bWasEvent = false; + { + // release YieldMutex (and re-acquire at block end) + SolarMutexReleaser aReleaser; + if( m_aDispatchMutex.tryToAcquire() ) + bDispatchThread = true; + else if( ! bWait ) + { + return false; // someone else is waiting already, return + } + + if( bDispatchThread ) + { + int nMaxEvents = bHandleAllCurrentEvents ? 100 : 1; + bool wasOneEvent = true; + while( nMaxEvents-- && wasOneEvent ) + { + wasOneEvent = g_main_context_iteration( nullptr, bWait && !bWasEvent ); + if( wasOneEvent ) + bWasEvent = true; + } + if (m_aException) + std::rethrow_exception(m_aException); + } + else if( bWait ) + { + /* #i41693# in case the dispatch thread hangs in join + * for this thread the condition will never be set + * workaround: timeout of 1 second an emergency exit + */ + // we are the dispatch thread + m_aDispatchCondition.reset(); + m_aDispatchCondition.wait(std::chrono::seconds(1)); + } + } + + if( bDispatchThread ) + { + m_aDispatchMutex.release(); + if( bWasEvent ) + m_aDispatchCondition.set(); // trigger non dispatch thread yields + } + + return bWasEvent; +} + +void GtkSalData::Init() +{ + SAL_INFO( "vcl.gtk", "GtkMainloop::Init()" ); + + /* + * open connection to X11 Display + * try in this order: + * o -display command line parameter, + * o $DISPLAY environment variable + * o default display + */ + + GdkDisplay *pGdkDisp = nullptr; + + // is there a -display command line parameter? + rtl_TextEncoding aEnc = osl_getThreadTextEncoding(); + int nParams = osl_getCommandArgCount(); + OString aDisplay; + OUString aParam, aBin; + char** pCmdLineAry = new char*[ nParams+1 ]; + osl_getExecutableFile( &aParam.pData ); + osl_getSystemPathFromFileURL( aParam.pData, &aBin.pData ); + pCmdLineAry[0] = g_strdup( OUStringToOString( aBin, aEnc ).getStr() ); + for (int i = 0; i < nParams; ++i) + { + osl_getCommandArg(i, &aParam.pData ); + OString aBParam( OUStringToOString( aParam, aEnc ) ); + + if( aParam == "-display" || aParam == "--display" ) + { + pCmdLineAry[i+1] = g_strdup( "--display" ); + osl_getCommandArg(i+1, &aParam.pData ); + aDisplay = OUStringToOString( aParam, aEnc ); + } + else + pCmdLineAry[i+1] = g_strdup( aBParam.getStr() ); + } + // add executable + nParams++; + + g_set_application_name(SalGenericSystem::getFrameClassName()); + + // Set consistent name of the root accessible + OUString aAppName = Application::GetAppName(); + if( !aAppName.isEmpty() ) + { + OString aPrgName = OUStringToOString(aAppName, aEnc); + g_set_prgname(aPrgName.getStr()); + } + + // init gtk/gdk + gtk_init_check( &nParams, &pCmdLineAry ); + gdk_error_trap_push(); + + for (int i = 0; i < nParams; ++i) + g_free( pCmdLineAry[i] ); + delete [] pCmdLineAry; + +#if OSL_DEBUG_LEVEL > 1 + if (g_getenv("SAL_DEBUG_UPDATES")) + gdk_window_set_debug_updates (TRUE); +#endif + + pGdkDisp = gdk_display_get_default(); + if ( !pGdkDisp ) + { + OUString aProgramFileURL; + osl_getExecutableFile( &aProgramFileURL.pData ); + OUString aProgramSystemPath; + osl_getSystemPathFromFileURL (aProgramFileURL.pData, &aProgramSystemPath.pData); + OString aProgramName = OUStringToOString( + aProgramSystemPath, + osl_getThreadTextEncoding() ); + fprintf( stderr, "%s X11 error: Can't open display: %s\n", + aProgramName.getStr(), aDisplay.getStr()); + fprintf( stderr, " Set DISPLAY environment variable, use -display option\n"); + fprintf( stderr, " or check permissions of your X-Server\n"); + fprintf( stderr, " (See \"man X\" resp. \"man xhost\" for details)\n"); + fflush( stderr ); + exit(0); + } + +#if defined(GDK_WINDOWING_X11) + if (DLSYM_GDK_IS_X11_DISPLAY(pGdkDisp)) + aOrigXIOErrorHandler = XSetIOErrorHandler(XIOErrorHdl); +#endif + + GtkSalDisplay *pDisplay = new GtkSalDisplay( pGdkDisp ); + SetDisplay( pDisplay ); + + //FIXME: unwind keyboard extension bits + + // add signal handler to notify screen size changes + int nScreens = gdk_display_get_n_screens( pGdkDisp ); + for( int n = 0; n < nScreens; n++ ) + { + GdkScreen *pScreen = gdk_display_get_screen( pGdkDisp, n ); + if( pScreen ) + { + pDisplay->screenSizeChanged( pScreen ); + pDisplay->monitorsChanged( pScreen ); + g_signal_connect( G_OBJECT(pScreen), "size-changed", + G_CALLBACK(signalScreenSizeChanged), pDisplay ); + g_signal_connect( G_OBJECT(pScreen), "monitors-changed", + G_CALLBACK(signalMonitorsChanged), GetGtkDisplay() ); + } + } +} + +void GtkSalData::ErrorTrapPush() +{ + gdk_error_trap_push (); +} + +bool GtkSalData::ErrorTrapPop( bool bIgnoreError ) +{ + if (bIgnoreError) + { + gdk_error_trap_pop_ignored (); // faster + return false; + } + return gdk_error_trap_pop () != 0; +} + +#if !GLIB_CHECK_VERSION(2,32,0) +#define G_SOURCE_REMOVE FALSE +#endif + +extern "C" { + + struct SalGtkTimeoutSource { + GSource aParent; + GTimeVal aFireTime; + GtkSalTimer *pInstance; + }; + + static void sal_gtk_timeout_defer( SalGtkTimeoutSource *pTSource ) + { + g_get_current_time( &pTSource->aFireTime ); + g_time_val_add( &pTSource->aFireTime, pTSource->pInstance->m_nTimeoutMS * 1000 ); + } + + static gboolean sal_gtk_timeout_expired( SalGtkTimeoutSource *pTSource, + gint *nTimeoutMS, GTimeVal const *pTimeNow ) + { + glong nDeltaSec = pTSource->aFireTime.tv_sec - pTimeNow->tv_sec; + glong nDeltaUSec = pTSource->aFireTime.tv_usec - pTimeNow->tv_usec; + if( nDeltaSec < 0 || ( nDeltaSec == 0 && nDeltaUSec < 0) ) + { + *nTimeoutMS = 0; + return true; + } + if( nDeltaUSec < 0 ) + { + nDeltaUSec += 1000000; + nDeltaSec -= 1; + } + // if the clock changes backwards we need to cope ... + if( o3tl::make_unsigned(nDeltaSec) > 1 + ( pTSource->pInstance->m_nTimeoutMS / 1000 ) ) + { + sal_gtk_timeout_defer( pTSource ); + return true; + } + + *nTimeoutMS = MIN( G_MAXINT, ( nDeltaSec * 1000 + (nDeltaUSec + 999) / 1000 ) ); + + return *nTimeoutMS == 0; + } + + static gboolean sal_gtk_timeout_prepare( GSource *pSource, gint *nTimeoutMS ) + { + SalGtkTimeoutSource *pTSource = reinterpret_cast<SalGtkTimeoutSource *>(pSource); + + GTimeVal aTimeNow; + g_get_current_time( &aTimeNow ); + + return sal_gtk_timeout_expired( pTSource, nTimeoutMS, &aTimeNow ); + } + + static gboolean sal_gtk_timeout_check( GSource *pSource ) + { + SalGtkTimeoutSource *pTSource = reinterpret_cast<SalGtkTimeoutSource *>(pSource); + + GTimeVal aTimeNow; + g_get_current_time( &aTimeNow ); + + return ( pTSource->aFireTime.tv_sec < aTimeNow.tv_sec || + ( pTSource->aFireTime.tv_sec == aTimeNow.tv_sec && + pTSource->aFireTime.tv_usec < aTimeNow.tv_usec ) ); + } + + static gboolean sal_gtk_timeout_dispatch( GSource *pSource, GSourceFunc, gpointer ) + { + SalGtkTimeoutSource *pTSource = reinterpret_cast<SalGtkTimeoutSource *>(pSource); + + if( !pTSource->pInstance ) + return FALSE; + + SolarMutexGuard aGuard; + + sal_gtk_timeout_defer( pTSource ); + + ImplSVData* pSVData = ImplGetSVData(); + if( pSVData->maSchedCtx.mpSalTimer ) + pSVData->maSchedCtx.mpSalTimer->CallCallback(); + + return G_SOURCE_REMOVE; + } + + static GSourceFuncs sal_gtk_timeout_funcs = + { + sal_gtk_timeout_prepare, + sal_gtk_timeout_check, + sal_gtk_timeout_dispatch, + nullptr, nullptr, nullptr + }; +} + +static SalGtkTimeoutSource * +create_sal_gtk_timeout( GtkSalTimer *pTimer ) +{ + GSource *pSource = g_source_new( &sal_gtk_timeout_funcs, sizeof( SalGtkTimeoutSource ) ); + SalGtkTimeoutSource *pTSource = reinterpret_cast<SalGtkTimeoutSource *>(pSource); + pTSource->pInstance = pTimer; + + // #i36226# timers should be executed with lower priority + // than XEvents like in generic plugin + g_source_set_priority( pSource, G_PRIORITY_LOW ); + g_source_set_can_recurse( pSource, true ); + g_source_set_callback( pSource, + /* unused dummy */ g_idle_remove_by_data, + nullptr, nullptr ); + g_source_attach( pSource, g_main_context_default() ); +#ifdef DBG_UTIL + g_source_set_name( pSource, "VCL timeout source" ); +#endif + + sal_gtk_timeout_defer( pTSource ); + + return pTSource; +} + +GtkSalTimer::GtkSalTimer() + : m_pTimeout(nullptr) + , m_nTimeoutMS(0) +{ +} + +GtkSalTimer::~GtkSalTimer() +{ + GtkInstance *pInstance = static_cast<GtkInstance *>(GetSalData()->m_pInstance); + pInstance->RemoveTimer(); + Stop(); +} + +bool GtkSalTimer::Expired() +{ + if( !m_pTimeout || g_source_is_destroyed( &m_pTimeout->aParent ) ) + return false; + + gint nDummy = 0; + GTimeVal aTimeNow; + g_get_current_time( &aTimeNow ); + return !!sal_gtk_timeout_expired( m_pTimeout, &nDummy, &aTimeNow); +} + +void GtkSalTimer::Start( sal_uInt64 nMS ) +{ + // glib is not 64bit safe in this regard. + assert( nMS <= G_MAXINT ); + if ( nMS > G_MAXINT ) + nMS = G_MAXINT; + m_nTimeoutMS = nMS; // for restarting + Stop(); // FIXME: ideally re-use an existing m_pTimeout + m_pTimeout = create_sal_gtk_timeout( this ); +} + +void GtkSalTimer::Stop() +{ + if( m_pTimeout ) + { + g_source_destroy( &m_pTimeout->aParent ); + g_source_unref( &m_pTimeout->aParent ); + m_pTimeout = nullptr; + } +} + +extern "C" { + static gboolean call_userEventFn( void *data ) + { + SolarMutexGuard aGuard; + const SalGenericDisplay *pDisplay = GetGenericUnixSalData()->GetDisplay(); + if ( pDisplay ) + { + GtkSalDisplay *pThisDisplay = static_cast<GtkSalData *>(data)->GetGtkDisplay(); + assert(static_cast<const SalGenericDisplay *>(pThisDisplay) == pDisplay); + pThisDisplay->DispatchInternalEvent(); + } + return true; + } +} + +void GtkSalData::TriggerUserEventProcessing() +{ + if (m_pUserEvent) + g_main_context_wakeup (nullptr); // really needed ? + else // nothing pending anyway + { + m_pUserEvent = g_idle_source_new(); + // tdf#110737 set user-events to a lower priority than system redraw + // events, which is G_PRIORITY_HIGH_IDLE + 20, so presentations + // queue-redraw has a chance to be fulfilled + g_source_set_priority (m_pUserEvent, G_PRIORITY_HIGH_IDLE + 30); + g_source_set_can_recurse (m_pUserEvent, true); + g_source_set_callback (m_pUserEvent, call_userEventFn, + static_cast<gpointer>(this), nullptr); + g_source_attach (m_pUserEvent, g_main_context_default ()); + } +} + +void GtkSalData::TriggerAllUserEventsProcessed() +{ + assert( m_pUserEvent ); + g_source_destroy( m_pUserEvent ); + g_source_unref( m_pUserEvent ); + m_pUserEvent = nullptr; +} + +void GtkSalDisplay::TriggerUserEventProcessing() +{ + GetGtkSalData()->TriggerUserEventProcessing(); +} + +void GtkSalDisplay::TriggerAllUserEventsProcessed() +{ + GetGtkSalData()->TriggerAllUserEventsProcessed(); +} + +GtkWidget* GtkSalDisplay::findGtkWidgetForNativeHandle(sal_uIntPtr hWindow) const +{ + for (auto pSalFrame : m_aFrames ) + { + const SystemEnvData* pEnvData = pSalFrame->GetSystemData(); + if (pEnvData->aWindow == hWindow) + return GTK_WIDGET(pEnvData->pWidget); + } + return nullptr; +} + +void GtkSalDisplay::deregisterFrame( SalFrame* pFrame ) +{ + if( m_pCapture == pFrame ) + { + static_cast<GtkSalFrame*>(m_pCapture)->grabPointer( false, false, false ); + m_pCapture = nullptr; + } + SalGenericDisplay::deregisterFrame( pFrame ); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/gtk3gtkframe.cxx b/vcl/unx/gtk3/gtk3gtkframe.cxx new file mode 100644 index 000000000..69a554108 --- /dev/null +++ b/vcl/unx/gtk3/gtk3gtkframe.cxx @@ -0,0 +1,4609 @@ +/* -*- 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <unx/gtk/gtkframe.hxx> +#include <unx/gtk/gtkdata.hxx> +#include <unx/gtk/gtkinst.hxx> +#include <unx/gtk/gtkgdi.hxx> +#include <unx/gtk/gtksalmenu.hxx> +#include <unx/gtk/hudawareness.h> +#include <vcl/event.hxx> +#include <vcl/keycodes.hxx> +#include <unx/geninst.h> +#include <headless/svpgdi.hxx> +#include <sal/log.hxx> +#include <tools/diagnose_ex.h> +#include <vcl/floatwin.hxx> +#include <vcl/svapp.hxx> +#include <vcl/weld.hxx> +#include <vcl/window.hxx> +#include <vcl/settings.hxx> + +#include <gtk/gtk.h> + +#include <X11/Xlib.h> +#include <X11/Xutil.h> +#include <unx/gtk/gtkbackend.hxx> + +#include <window.h> + +#include <basegfx/vector/b2ivector.hxx> + +#include <dlfcn.h> + +#include <algorithm> + +#if OSL_DEBUG_LEVEL > 1 +# include <cstdio> +#endif + +#include <i18nlangtag/mslangid.hxx> + +#include <cstdlib> +#include <cmath> + +#include <com/sun/star/accessibility/XAccessibleEditableText.hpp> +#include <com/sun/star/awt/MouseButton.hpp> +#include <com/sun/star/datatransfer/dnd/DNDConstants.hpp> + +using namespace com::sun::star; + +int GtkSalFrame::m_nFloats = 0; + +static GDBusConnection* pSessionBus = nullptr; + +sal_uInt16 GtkSalFrame::GetKeyModCode( guint state ) +{ + sal_uInt16 nCode = 0; + if( state & GDK_SHIFT_MASK ) + nCode |= KEY_SHIFT; + if( state & GDK_CONTROL_MASK ) + nCode |= KEY_MOD1; + if( state & GDK_MOD1_MASK ) + nCode |= KEY_MOD2; + if( state & GDK_SUPER_MASK ) + nCode |= KEY_MOD3; + return nCode; +} + +sal_uInt16 GtkSalFrame::GetMouseModCode( guint state ) +{ + sal_uInt16 nCode = GetKeyModCode( state ); + if( state & GDK_BUTTON1_MASK ) + nCode |= MOUSE_LEFT; + if( state & GDK_BUTTON2_MASK ) + nCode |= MOUSE_MIDDLE; + if( state & GDK_BUTTON3_MASK ) + nCode |= MOUSE_RIGHT; + + return nCode; +} + +sal_uInt16 GtkSalFrame::GetKeyCode(guint keyval) +{ + sal_uInt16 nCode = 0; + if( keyval >= GDK_KEY_0 && keyval <= GDK_KEY_9 ) + nCode = KEY_0 + (keyval-GDK_KEY_0); + else if( keyval >= GDK_KEY_KP_0 && keyval <= GDK_KEY_KP_9 ) + nCode = KEY_0 + (keyval-GDK_KEY_KP_0); + else if( keyval >= GDK_KEY_A && keyval <= GDK_KEY_Z ) + nCode = KEY_A + (keyval-GDK_KEY_A ); + else if( keyval >= GDK_KEY_a && keyval <= GDK_KEY_z ) + nCode = KEY_A + (keyval-GDK_KEY_a ); + else if( keyval >= GDK_KEY_F1 && keyval <= GDK_KEY_F26 ) + // KEY_F26 is the last function key known to keycodes.hxx + { + switch( keyval ) + { + // - - - - - Sun keyboard, see vcl/unx/source/app/saldisp.cxx + // although GDK_KEY_F1 ... GDK_KEY_L10 are known to + // gdk/gdkkeysyms.h, they are unlikely to be generated + // except possibly by Solaris systems + // this whole section needs review + case GDK_KEY_L2: + nCode = KEY_F12; + break; + case GDK_KEY_L3: nCode = KEY_PROPERTIES; break; + case GDK_KEY_L4: nCode = KEY_UNDO; break; + case GDK_KEY_L6: nCode = KEY_COPY; break; // KEY_F16 + case GDK_KEY_L8: nCode = KEY_PASTE; break; // KEY_F18 + case GDK_KEY_L10: nCode = KEY_CUT; break; // KEY_F20 + default: + nCode = KEY_F1 + (keyval-GDK_KEY_F1); break; + } + } + else + { + switch( keyval ) + { + case GDK_KEY_KP_Down: + case GDK_KEY_Down: nCode = KEY_DOWN; break; + case GDK_KEY_KP_Up: + case GDK_KEY_Up: nCode = KEY_UP; break; + case GDK_KEY_KP_Left: + case GDK_KEY_Left: nCode = KEY_LEFT; break; + case GDK_KEY_KP_Right: + case GDK_KEY_Right: nCode = KEY_RIGHT; break; + case GDK_KEY_KP_Begin: + case GDK_KEY_KP_Home: + case GDK_KEY_Begin: + case GDK_KEY_Home: nCode = KEY_HOME; break; + case GDK_KEY_KP_End: + case GDK_KEY_End: nCode = KEY_END; break; + case GDK_KEY_KP_Page_Up: + case GDK_KEY_Page_Up: nCode = KEY_PAGEUP; break; + case GDK_KEY_KP_Page_Down: + case GDK_KEY_Page_Down: nCode = KEY_PAGEDOWN; break; + case GDK_KEY_KP_Enter: + case GDK_KEY_Return: nCode = KEY_RETURN; break; + case GDK_KEY_Escape: nCode = KEY_ESCAPE; break; + case GDK_KEY_ISO_Left_Tab: + case GDK_KEY_KP_Tab: + case GDK_KEY_Tab: nCode = KEY_TAB; break; + case GDK_KEY_BackSpace: nCode = KEY_BACKSPACE; break; + case GDK_KEY_KP_Space: + case GDK_KEY_space: nCode = KEY_SPACE; break; + case GDK_KEY_KP_Insert: + case GDK_KEY_Insert: nCode = KEY_INSERT; break; + case GDK_KEY_KP_Delete: + case GDK_KEY_Delete: nCode = KEY_DELETE; break; + case GDK_KEY_plus: + case GDK_KEY_KP_Add: nCode = KEY_ADD; break; + case GDK_KEY_minus: + case GDK_KEY_KP_Subtract: nCode = KEY_SUBTRACT; break; + case GDK_KEY_asterisk: + case GDK_KEY_KP_Multiply: nCode = KEY_MULTIPLY; break; + case GDK_KEY_slash: + case GDK_KEY_KP_Divide: nCode = KEY_DIVIDE; break; + case GDK_KEY_period: nCode = KEY_POINT; break; + case GDK_KEY_decimalpoint: nCode = KEY_POINT; break; + case GDK_KEY_comma: nCode = KEY_COMMA; break; + case GDK_KEY_less: nCode = KEY_LESS; break; + case GDK_KEY_greater: nCode = KEY_GREATER; break; + case GDK_KEY_KP_Equal: + case GDK_KEY_equal: nCode = KEY_EQUAL; break; + case GDK_KEY_Find: nCode = KEY_FIND; break; + case GDK_KEY_Menu: nCode = KEY_CONTEXTMENU;break; + case GDK_KEY_Help: nCode = KEY_HELP; break; + case GDK_KEY_Undo: nCode = KEY_UNDO; break; + case GDK_KEY_Redo: nCode = KEY_REPEAT; break; + // on a sun keyboard this actually is usually SunXK_Stop = 0x0000FF69 (XK_Cancel), + // but VCL doesn't have a key definition for that + case GDK_KEY_Cancel: nCode = KEY_F11; break; + case GDK_KEY_KP_Decimal: + case GDK_KEY_KP_Separator: nCode = KEY_DECIMAL; break; + case GDK_KEY_asciitilde: nCode = KEY_TILDE; break; + case GDK_KEY_leftsinglequotemark: + case GDK_KEY_quoteleft: nCode = KEY_QUOTELEFT; break; + case GDK_KEY_bracketleft: nCode = KEY_BRACKETLEFT; break; + case GDK_KEY_bracketright: nCode = KEY_BRACKETRIGHT; break; + case GDK_KEY_semicolon: nCode = KEY_SEMICOLON; break; + case GDK_KEY_quoteright: nCode = KEY_QUOTERIGHT; break; + // some special cases, also see saldisp.cxx + // - - - - - - - - - - - - - Apollo - - - - - - - - - - - - - 0x1000 + // These can be found in ap_keysym.h + case 0x1000FF02: // apXK_Copy + nCode = KEY_COPY; + break; + case 0x1000FF03: // apXK_Cut + nCode = KEY_CUT; + break; + case 0x1000FF04: // apXK_Paste + nCode = KEY_PASTE; + break; + case 0x1000FF14: // apXK_Repeat + nCode = KEY_REPEAT; + break; + // Exit, Save + // - - - - - - - - - - - - - - D E C - - - - - - - - - - - - - 0x1000 + // These can be found in DECkeysym.h + case 0x1000FF00: + nCode = KEY_DELETE; + break; + // - - - - - - - - - - - - - - H P - - - - - - - - - - - - - 0x1000 + // These can be found in HPkeysym.h + case 0x1000FF73: // hpXK_DeleteChar + nCode = KEY_DELETE; + break; + case 0x1000FF74: // hpXK_BackTab + case 0x1000FF75: // hpXK_KP_BackTab + nCode = KEY_TAB; + break; + // - - - - - - - - - - - - - - I B M - - - - - - - - - - - - - + // - - - - - - - - - - - - - - O S F - - - - - - - - - - - - - 0x1004 + // These also can be found in HPkeysym.h + case 0x1004FF02: // osfXK_Copy + nCode = KEY_COPY; + break; + case 0x1004FF03: // osfXK_Cut + nCode = KEY_CUT; + break; + case 0x1004FF04: // osfXK_Paste + nCode = KEY_PASTE; + break; + case 0x1004FF07: // osfXK_BackTab + nCode = KEY_TAB; + break; + case 0x1004FF08: // osfXK_BackSpace + nCode = KEY_BACKSPACE; + break; + case 0x1004FF1B: // osfXK_Escape + nCode = KEY_ESCAPE; + break; + // Up, Down, Left, Right, PageUp, PageDown + // - - - - - - - - - - - - - - S C O - - - - - - - - - - - - - + // - - - - - - - - - - - - - - S G I - - - - - - - - - - - - - 0x1007 + // - - - - - - - - - - - - - - S N I - - - - - - - - - - - - - + // - - - - - - - - - - - - - - S U N - - - - - - - - - - - - - 0x1005 + // These can be found in Sunkeysym.h + case 0x1005FF10: // SunXK_F36 + nCode = KEY_F11; + break; + case 0x1005FF11: // SunXK_F37 + nCode = KEY_F12; + break; + case 0x1005FF70: // SunXK_Props + nCode = KEY_PROPERTIES; + break; + case 0x1005FF71: // SunXK_Front + nCode = KEY_FRONT; + break; + case 0x1005FF72: // SunXK_Copy + nCode = KEY_COPY; + break; + case 0x1005FF73: // SunXK_Open + nCode = KEY_OPEN; + break; + case 0x1005FF74: // SunXK_Paste + nCode = KEY_PASTE; + break; + case 0x1005FF75: // SunXK_Cut + nCode = KEY_CUT; + break; + // - - - - - - - - - - - - - X F 8 6 - - - - - - - - - - - - - 0x1008 + // These can be found in XF86keysym.h + // but more importantly they are also available GTK/Gdk version 3 + // and hence are already provided in gdk/gdkkeysyms.h, and hence + // in gdk/gdk.h + case GDK_KEY_Copy: nCode = KEY_COPY; break; // 0x1008ff57 + case GDK_KEY_Cut: nCode = KEY_CUT; break; // 0x1008ff58 + case GDK_KEY_Open: nCode = KEY_OPEN; break; // 0x1008ff6b + case GDK_KEY_Paste: nCode = KEY_PASTE; break; // 0x1008ff6d + } + } + + return nCode; +} + +guint GtkSalFrame::GetKeyValFor(GdkKeymap* pKeyMap, guint16 hardware_keycode, guint8 group) +{ + guint updated_keyval = 0; + gdk_keymap_translate_keyboard_state(pKeyMap, hardware_keycode, + GdkModifierType(0), group, &updated_keyval, nullptr, nullptr, nullptr); + return updated_keyval; +} + +namespace { + +// F10 means either KEY_F10 or KEY_MENU, which has to be decided +// in the independent part. +struct KeyAlternate +{ + sal_uInt16 nKeyCode; + sal_Unicode nCharCode; + KeyAlternate() : nKeyCode( 0 ), nCharCode( 0 ) {} + KeyAlternate( sal_uInt16 nKey, sal_Unicode nChar = 0 ) : nKeyCode( nKey ), nCharCode( nChar ) {} +}; + +} + +static KeyAlternate +GetAlternateKeyCode( const sal_uInt16 nKeyCode ) +{ + KeyAlternate aAlternate; + + switch( nKeyCode ) + { + case KEY_F10: aAlternate = KeyAlternate( KEY_MENU );break; + case KEY_F24: aAlternate = KeyAlternate( KEY_SUBTRACT, '-' );break; + } + + return aAlternate; +} + +#if OSL_DEBUG_LEVEL > 0 +static bool dumpframes = false; +#endif + +bool GtkSalFrame::doKeyCallback( guint state, + guint keyval, + guint16 hardware_keycode, + guint8 group, + sal_Unicode aOrigCode, + bool bDown, + bool bSendRelease + ) +{ + SalKeyEvent aEvent; + + aEvent.mnCharCode = aOrigCode; + aEvent.mnRepeat = 0; + + vcl::DeletionListener aDel( this ); + +#if OSL_DEBUG_LEVEL > 0 + const char* pKeyDebug = getenv("VCL_GTK3_PAINTDEBUG"); + + if (pKeyDebug && *pKeyDebug == '1') + { + if (bDown) + { + // shift-zero forces a re-draw and event is swallowed + if (keyval == GDK_KEY_0) + { + SAL_INFO("vcl.gtk3", "force widget_queue_draw."); + gtk_widget_queue_draw(GTK_WIDGET(m_pFixedContainer)); + return false; + } + else if (keyval == GDK_KEY_1) + { + SAL_INFO("vcl.gtk3", "force repaint all."); + TriggerPaintEvent(); + return false; + } + else if (keyval == GDK_KEY_2) + { + dumpframes = !dumpframes; + SAL_INFO("vcl.gtk3", "toggle dump frames to " << dumpframes); + return false; + } + } + } +#endif + + /* + * #i42122# translate all keys with Ctrl and/or Alt to group 0 else + * shortcuts (e.g. Ctrl-o) will not work but be inserted by the + * application + * + * #i52338# do this for all keys that the independent part has no key code + * for + * + * fdo#41169 rather than use group 0, detect if there is a group which can + * be used to input Latin text and use that if possible + */ + aEvent.mnCode = GetKeyCode( keyval ); + if( aEvent.mnCode == 0 ) + { + gint best_group = SAL_MAX_INT32; + + // Try and find Latin layout + GdkKeymap* keymap = gdk_keymap_get_default(); + GdkKeymapKey *keys; + gint n_keys; + if (gdk_keymap_get_entries_for_keyval(keymap, GDK_KEY_A, &keys, &n_keys)) + { + // Find the lowest group that supports Latin layout + for (gint i = 0; i < n_keys; ++i) + { + if (keys[i].level != 0 && keys[i].level != 1) + continue; + best_group = std::min(best_group, keys[i].group); + if (best_group == 0) + break; + } + g_free(keys); + } + + //Unavailable, go with original group then I suppose + if (best_group == SAL_MAX_INT32) + best_group = group; + + guint updated_keyval = GetKeyValFor(keymap, hardware_keycode, best_group); + aEvent.mnCode = GetKeyCode(updated_keyval); + } + + aEvent.mnCode |= GetKeyModCode( state ); + + bool bStopProcessingKey; + if (bDown) + { + bStopProcessingKey = CallCallbackExc(SalEvent::KeyInput, &aEvent); + // #i46889# copy AlternateKeyCode handling from generic plugin + if (!bStopProcessingKey) + { + KeyAlternate aAlternate = GetAlternateKeyCode( aEvent.mnCode ); + if( aAlternate.nKeyCode ) + { + aEvent.mnCode = aAlternate.nKeyCode; + if( aAlternate.nCharCode ) + aEvent.mnCharCode = aAlternate.nCharCode; + bStopProcessingKey = CallCallbackExc(SalEvent::KeyInput, &aEvent); + } + } + if( bSendRelease && ! aDel.isDeleted() ) + { + CallCallbackExc(SalEvent::KeyUp, &aEvent); + } + } + else + bStopProcessingKey = CallCallbackExc(SalEvent::KeyUp, &aEvent); + return bStopProcessingKey; +} + +GtkSalFrame::GtkSalFrame( SalFrame* pParent, SalFrameStyleFlags nStyle ) + : m_nXScreen( getDisplay()->GetDefaultXScreen() ) + , m_pHeaderBar(nullptr) + , m_bGraphics(false) +{ + getDisplay()->registerFrame( this ); + m_bDefaultPos = true; + m_bDefaultSize = ( (nStyle & SalFrameStyleFlags::SIZEABLE) && ! pParent ); + Init( pParent, nStyle ); +} + +GtkSalFrame::GtkSalFrame( SystemParentData* pSysData ) + : m_nXScreen( getDisplay()->GetDefaultXScreen() ) + , m_pHeaderBar(nullptr) + , m_bGraphics(false) +{ + getDisplay()->registerFrame( this ); + // permanently ignore errors from our unruly children ... + GetGenericUnixSalData()->ErrorTrapPush(); + m_bDefaultPos = true; + m_bDefaultSize = true; + Init( pSysData ); +} + +// AppMenu watch functions. + +static void ObjectDestroyedNotify( gpointer data ) +{ + if ( data ) { + g_object_unref( data ); + } +} + +static void hud_activated( gboolean hud_active, gpointer user_data ) +{ + if ( hud_active ) + { + SolarMutexGuard aGuard; + GtkSalFrame* pSalFrame = static_cast< GtkSalFrame* >( user_data ); + GtkSalMenu* pSalMenu = reinterpret_cast< GtkSalMenu* >( pSalFrame->GetMenu() ); + + if ( pSalMenu ) + pSalMenu->UpdateFull(); + } +} + +static bool ensure_dbus_setup( gpointer data ) +{ + GtkSalFrame* pSalFrame = static_cast< GtkSalFrame* >( data ); + GdkWindow* gdkWindow = gtk_widget_get_window( pSalFrame->getWindow() ); + + if ( gdkWindow != nullptr && g_object_get_data( G_OBJECT( gdkWindow ), "g-lo-menubar" ) == nullptr ) + { + // Get a DBus session connection. + if(!pSessionBus) + pSessionBus = g_bus_get_sync (G_BUS_TYPE_SESSION, nullptr, nullptr); + if( !pSessionBus ) + { + return false; + } + + // Create menu model and action group attached to this frame. + GMenuModel* pMenuModel = G_MENU_MODEL( g_lo_menu_new() ); + GActionGroup* pActionGroup = reinterpret_cast<GActionGroup*>(g_lo_action_group_new()); + + // Generate menu paths. + sal_uIntPtr windowId = pSalFrame->GetNativeWindowHandle(pSalFrame->getWindow()); + gchar* aDBusWindowPath = g_strdup_printf( "/org/libreoffice/window/%lu", windowId ); + gchar* aDBusMenubarPath = g_strdup_printf( "/org/libreoffice/window/%lu/menus/menubar", windowId ); + + // Set window properties. + g_object_set_data_full( G_OBJECT( gdkWindow ), "g-lo-menubar", pMenuModel, ObjectDestroyedNotify ); + g_object_set_data_full( G_OBJECT( gdkWindow ), "g-lo-action-group", pActionGroup, ObjectDestroyedNotify ); + + GdkDisplay *pDisplay = GtkSalFrame::getGdkDisplay(); +#if defined(GDK_WINDOWING_X11) + if (DLSYM_GDK_IS_X11_DISPLAY(pDisplay)) + { + gdk_x11_window_set_utf8_property( gdkWindow, "_GTK_APPLICATION_ID", "org.libreoffice" ); + gdk_x11_window_set_utf8_property( gdkWindow, "_GTK_MENUBAR_OBJECT_PATH", aDBusMenubarPath ); + gdk_x11_window_set_utf8_property( gdkWindow, "_GTK_WINDOW_OBJECT_PATH", aDBusWindowPath ); + gdk_x11_window_set_utf8_property( gdkWindow, "_GTK_APPLICATION_OBJECT_PATH", "/org/libreoffice" ); + gdk_x11_window_set_utf8_property( gdkWindow, "_GTK_UNIQUE_BUS_NAME", g_dbus_connection_get_unique_name( pSessionBus ) ); + } +#endif +#if defined(GDK_WINDOWING_WAYLAND) + if (DLSYM_GDK_IS_WAYLAND_DISPLAY(pDisplay)) + { + gdk_wayland_window_set_dbus_properties_libgtk_only(gdkWindow, "org.libreoffice", + nullptr, + aDBusMenubarPath, + aDBusWindowPath, + "/org/libreoffice", + g_dbus_connection_get_unique_name( pSessionBus )); + } +#endif + // Publish the menu model and the action group. + SAL_INFO("vcl.unity", "exporting menu model at " << pMenuModel << " for window " << windowId); + pSalFrame->m_nMenuExportId = g_dbus_connection_export_menu_model (pSessionBus, aDBusMenubarPath, pMenuModel, nullptr); + SAL_INFO("vcl.unity", "exporting action group at " << pActionGroup << " for window " << windowId); + pSalFrame->m_nActionGroupExportId = g_dbus_connection_export_action_group( pSessionBus, aDBusWindowPath, pActionGroup, nullptr); + pSalFrame->m_nHudAwarenessId = hud_awareness_register( pSessionBus, aDBusMenubarPath, hud_activated, pSalFrame, nullptr, nullptr ); + + g_free( aDBusWindowPath ); + g_free( aDBusMenubarPath ); + } + + return false; +} + +void on_registrar_available( GDBusConnection * /*connection*/, + const gchar * /*name*/, + const gchar * /*name_owner*/, + gpointer user_data ) +{ + SolarMutexGuard aGuard; + + GtkSalFrame* pSalFrame = static_cast< GtkSalFrame* >( user_data ); + + SalMenu* pSalMenu = pSalFrame->GetMenu(); + + if ( pSalMenu != nullptr ) + { + GtkSalMenu* pGtkSalMenu = static_cast<GtkSalMenu*>(pSalMenu); + pGtkSalMenu->EnableUnity(true); + } +} + +// This is called when the registrar becomes unavailable. It shows the menubar. +void on_registrar_unavailable( GDBusConnection * /*connection*/, + const gchar * /*name*/, + gpointer user_data ) +{ + SolarMutexGuard aGuard; + + SAL_INFO("vcl.unity", "on_registrar_unavailable"); + + //pSessionBus = NULL; + GtkSalFrame* pSalFrame = static_cast< GtkSalFrame* >( user_data ); + + SalMenu* pSalMenu = pSalFrame->GetMenu(); + + if ( pSalMenu ) { + GtkSalMenu* pGtkSalMenu = static_cast< GtkSalMenu* >( pSalMenu ); + pGtkSalMenu->EnableUnity(false); + } +} + +void GtkSalFrame::EnsureAppMenuWatch() +{ + if ( !m_nWatcherId ) + { + // Get a DBus session connection. + if ( pSessionBus == nullptr ) + { + pSessionBus = g_bus_get_sync( G_BUS_TYPE_SESSION, nullptr, nullptr ); + + if ( pSessionBus == nullptr ) + return; + } + + // Publish the menu only if AppMenu registrar is available. + m_nWatcherId = g_bus_watch_name_on_connection( pSessionBus, + "com.canonical.AppMenu.Registrar", + G_BUS_NAME_WATCHER_FLAGS_NONE, + on_registrar_available, + on_registrar_unavailable, + this, + nullptr ); + } +} + +void GtkSalFrame::InvalidateGraphics() +{ + if( m_pGraphics ) + { + m_bGraphics = false; + } +} + +GtkSalFrame::~GtkSalFrame() +{ + m_aSmoothScrollIdle.Stop(); + m_aSmoothScrollIdle.ClearInvokeHandler(); + + if (m_pDropTarget) + { + m_pDropTarget->deinitialize(); + m_pDropTarget = nullptr; + } + + if (m_pDragSource) + { + m_pDragSource->deinitialize(); + m_pDragSource= nullptr; + } + + InvalidateGraphics(); + + if (m_pParent) + { + m_pParent->m_aChildren.remove( this ); + } + + getDisplay()->deregisterFrame( this ); + + if( m_pRegion ) + { + cairo_region_destroy( m_pRegion ); + } + + m_pIMHandler.reset(); + + //tdf#108705 remove grabs on event widget before + //destroying event widget + while (m_nGrabLevel) + removeGrabLevel(); + + GtkWidget *pEventWidget = getMouseEventWidget(); + for (auto handler_id : m_aMouseSignalIds) + g_signal_handler_disconnect(G_OBJECT(pEventWidget), handler_id); + if( m_pFixedContainer ) + gtk_widget_destroy( GTK_WIDGET( m_pFixedContainer ) ); + if( m_pEventBox ) + gtk_widget_destroy( GTK_WIDGET(m_pEventBox) ); + if( m_pTopLevelGrid ) + gtk_widget_destroy( GTK_WIDGET(m_pTopLevelGrid) ); + { + SolarMutexGuard aGuard; + + if(m_nWatcherId) + g_bus_unwatch_name(m_nWatcherId); + + if( m_pWindow ) + { + g_object_set_data( G_OBJECT( m_pWindow ), "SalFrame", nullptr ); + + if ( pSessionBus ) + { + if ( m_nHudAwarenessId ) + hud_awareness_unregister( pSessionBus, m_nHudAwarenessId ); + if ( m_nMenuExportId ) + g_dbus_connection_unexport_menu_model( pSessionBus, m_nMenuExportId ); + if ( m_nActionGroupExportId ) + g_dbus_connection_unexport_action_group( pSessionBus, m_nActionGroupExportId ); + } + gtk_widget_destroy( m_pWindow ); + } + } + if( m_pForeignParent ) + g_object_unref( G_OBJECT( m_pForeignParent ) ); + if( m_pForeignTopLevel ) + g_object_unref( G_OBJECT( m_pForeignTopLevel) ); + + m_pGraphics.reset(); + + if (m_pSurface) + cairo_surface_destroy(m_pSurface); +} + +void GtkSalFrame::moveWindow( long nX, long nY ) +{ + if( isChild( false ) ) + { + GtkWidget* pParent = m_pParent ? gtk_widget_get_parent(m_pWindow) : nullptr; + // tdf#130414 its possible that we were reparented and are no longer inside + // our original GtkFixed parent + if (pParent && GTK_IS_FIXED(pParent)) + { + gtk_fixed_move( GTK_FIXED(pParent), + m_pWindow, + nX - m_pParent->maGeometry.nX, nY - m_pParent->maGeometry.nY ); + } + } + else + gtk_window_move( GTK_WINDOW(m_pWindow), nX, nY ); +} + +void GtkSalFrame::widget_set_size_request(long nWidth, long nHeight) +{ + gtk_widget_set_size_request(GTK_WIDGET(m_pFixedContainer), nWidth, nHeight ); +} + +void GtkSalFrame::window_resize(long nWidth, long nHeight) +{ + m_nWidthRequest = nWidth; + m_nHeightRequest = nHeight; + gtk_window_set_default_size(GTK_WINDOW(m_pWindow), nWidth, nHeight); + if (gtk_widget_get_visible(m_pWindow)) + gtk_window_resize(GTK_WINDOW(m_pWindow), nWidth, nHeight); +} + +void GtkSalFrame::resizeWindow( long nWidth, long nHeight ) +{ + if( isChild( false ) ) + { + widget_set_size_request(nWidth, nHeight); + } + else if( ! isChild( true, false ) ) + window_resize(nWidth, nHeight); +} + +// tdf#124694 GtkFixed takes the max size of all its children as its +// preferred size, causing it to not clip its child, but grow instead. + +static void +ooo_fixed_get_preferred_height(GtkWidget*, gint *minimum, gint *natural) +{ + *minimum = 0; + *natural = 0; +} + +static void +ooo_fixed_get_preferred_width(GtkWidget*, gint *minimum, gint *natural) +{ + *minimum = 0; + *natural = 0; +} + +static void +ooo_fixed_class_init(GtkFixedClass *klass) +{ + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass); + widget_class->get_accessible = ooo_fixed_get_accessible; + widget_class->get_preferred_height = ooo_fixed_get_preferred_height; + widget_class->get_preferred_width = ooo_fixed_get_preferred_width; +} + +/* + * Always use a sub-class of GtkFixed we can tag for a11y. This allows us to + * utilize GAIL for the toplevel window and toolkit implementation incl. + * key event listener support .. + */ + +GType +ooo_fixed_get_type() +{ + static GType type = 0; + + if (!type) { + static const GTypeInfo tinfo = + { + sizeof (GtkFixedClass), + nullptr, /* base init */ + nullptr, /* base finalize */ + reinterpret_cast<GClassInitFunc>(ooo_fixed_class_init), /* class init */ + nullptr, /* class finalize */ + nullptr, /* class data */ + sizeof (GtkFixed), /* instance size */ + 0, /* nb preallocs */ + nullptr, /* instance init */ + nullptr /* value table */ + }; + + type = g_type_register_static( GTK_TYPE_FIXED, "OOoFixed", + &tinfo, GTypeFlags(0)); + } + + return type; +} + +void GtkSalFrame::updateScreenNumber() +{ + int nScreen = 0; + GdkScreen *pScreen = gtk_widget_get_screen( m_pWindow ); + if( pScreen ) + nScreen = getDisplay()->getSystem()->getScreenMonitorIdx( pScreen, maGeometry.nX, maGeometry.nY ); + maGeometry.nDisplayScreenNumber = nScreen; +} + +GtkWidget *GtkSalFrame::getMouseEventWidget() const +{ + return GTK_WIDGET(m_pEventBox); +} + +static void damaged(void *handle, + sal_Int32 nExtentsX, sal_Int32 nExtentsY, + sal_Int32 nExtentsWidth, sal_Int32 nExtentsHeight) +{ + GtkSalFrame* pThis = static_cast<GtkSalFrame*>(handle); + pThis->damaged(nExtentsX, nExtentsY, nExtentsWidth, nExtentsHeight); +} + +void GtkSalFrame::InitCommon() +{ + m_pSurface = nullptr; + m_nGrabLevel = 0; + m_bSalObjectSetPosSize = false; + + m_aDamageHandler.handle = this; + m_aDamageHandler.damaged = ::damaged; + + m_aSmoothScrollIdle.SetInvokeHandler(LINK(this, GtkSalFrame, AsyncScroll)); + + m_pTopLevelGrid = GTK_GRID(gtk_grid_new()); + gtk_container_add(GTK_CONTAINER(m_pWindow), GTK_WIDGET(m_pTopLevelGrid)); + + m_pEventBox = GTK_EVENT_BOX(gtk_event_box_new()); + gtk_widget_add_events( GTK_WIDGET(m_pEventBox), + GDK_ALL_EVENTS_MASK ); + gtk_widget_set_vexpand(GTK_WIDGET(m_pEventBox), true); + gtk_widget_set_hexpand(GTK_WIDGET(m_pEventBox), true); + gtk_grid_attach(m_pTopLevelGrid, GTK_WIDGET(m_pEventBox), 0, 0, 1, 1); + + // add the fixed container child, + // fixed is needed since we have to position plugin windows + m_pFixedContainer = GTK_FIXED(g_object_new( ooo_fixed_get_type(), nullptr )); + gtk_widget_set_can_focus(GTK_WIDGET(m_pFixedContainer), true); + gtk_widget_set_size_request(GTK_WIDGET(m_pFixedContainer), 1, 1); + gtk_container_add( GTK_CONTAINER(m_pEventBox), GTK_WIDGET(m_pFixedContainer) ); + + GtkWidget *pEventWidget = getMouseEventWidget(); + + gtk_widget_set_app_paintable(GTK_WIDGET(m_pFixedContainer), true); + gtk_widget_set_redraw_on_allocate(GTK_WIDGET(m_pFixedContainer), false); + + // connect signals + // use pEventWidget instead of m_pWindow to avoid infinite event loop under Linux Mint Mate 18.3 + g_signal_connect( G_OBJECT(pEventWidget), "style-updated", G_CALLBACK(signalStyleUpdated), this ); + gtk_widget_set_has_tooltip(pEventWidget, true); + m_aMouseSignalIds.push_back(g_signal_connect( G_OBJECT(pEventWidget), "query-tooltip", G_CALLBACK(signalTooltipQuery), this )); + m_aMouseSignalIds.push_back(g_signal_connect( G_OBJECT(pEventWidget), "button-press-event", G_CALLBACK(signalButton), this )); + m_aMouseSignalIds.push_back(g_signal_connect( G_OBJECT(pEventWidget), "motion-notify-event", G_CALLBACK(signalMotion), this )); + m_aMouseSignalIds.push_back(g_signal_connect( G_OBJECT(pEventWidget), "button-release-event", G_CALLBACK(signalButton), this )); + m_aMouseSignalIds.push_back(g_signal_connect( G_OBJECT(pEventWidget), "leave-notify-event", G_CALLBACK(signalCrossing), this )); + m_aMouseSignalIds.push_back(g_signal_connect( G_OBJECT(pEventWidget), "enter-notify-event", G_CALLBACK(signalCrossing), this )); + + + //Drop Target Stuff + gtk_drag_dest_set(GTK_WIDGET(pEventWidget), GtkDestDefaults(0), nullptr, 0, GdkDragAction(0)); + gtk_drag_dest_set_track_motion(GTK_WIDGET(pEventWidget), true); + m_aMouseSignalIds.push_back(g_signal_connect( G_OBJECT(pEventWidget), "drag-motion", G_CALLBACK(signalDragMotion), this )); + m_aMouseSignalIds.push_back(g_signal_connect( G_OBJECT(pEventWidget), "drag-drop", G_CALLBACK(signalDragDrop), this )); + m_aMouseSignalIds.push_back(g_signal_connect( G_OBJECT(pEventWidget), "drag-data-received", G_CALLBACK(signalDragDropReceived), this )); + m_aMouseSignalIds.push_back(g_signal_connect( G_OBJECT(pEventWidget), "drag-leave", G_CALLBACK(signalDragLeave), this )); + + //Drag Source Stuff + m_aMouseSignalIds.push_back(g_signal_connect( G_OBJECT(pEventWidget), "drag-end", G_CALLBACK(signalDragEnd), this )); + m_aMouseSignalIds.push_back(g_signal_connect( G_OBJECT(pEventWidget), "drag-failed", G_CALLBACK(signalDragFailed), this )); + m_aMouseSignalIds.push_back(g_signal_connect( G_OBJECT(pEventWidget), "drag-data-delete", G_CALLBACK(signalDragDelete), this )); + m_aMouseSignalIds.push_back(g_signal_connect( G_OBJECT(pEventWidget), "drag-data-get", G_CALLBACK(signalDragDataGet), this )); + m_aMouseSignalIds.push_back(g_signal_connect( G_OBJECT(pEventWidget), "scroll-event", G_CALLBACK(signalScroll), this )); + + g_signal_connect( G_OBJECT(m_pFixedContainer), "draw", G_CALLBACK(signalDraw), this ); + g_signal_connect( G_OBJECT(m_pFixedContainer), "realize", G_CALLBACK(signalRealize), this ); + g_signal_connect( G_OBJECT(m_pFixedContainer), "size-allocate", G_CALLBACK(sizeAllocated), this ); + + GtkGesture *pSwipe = gtk_gesture_swipe_new(pEventWidget); + g_signal_connect(pSwipe, "swipe", G_CALLBACK(gestureSwipe), this); + gtk_event_controller_set_propagation_phase(GTK_EVENT_CONTROLLER (pSwipe), GTK_PHASE_TARGET); + g_object_weak_ref(G_OBJECT(pEventWidget), reinterpret_cast<GWeakNotify>(g_object_unref), pSwipe); + + GtkGesture *pLongPress = gtk_gesture_long_press_new(pEventWidget); + g_signal_connect(pLongPress, "pressed", G_CALLBACK(gestureLongPress), this); + gtk_event_controller_set_propagation_phase(GTK_EVENT_CONTROLLER (pLongPress), GTK_PHASE_TARGET); + g_object_weak_ref(G_OBJECT(pEventWidget), reinterpret_cast<GWeakNotify>(g_object_unref), pLongPress); + + g_signal_connect_after( G_OBJECT(m_pWindow), "focus-in-event", G_CALLBACK(signalFocus), this ); + g_signal_connect_after( G_OBJECT(m_pWindow), "focus-out-event", G_CALLBACK(signalFocus), this ); + if (GTK_IS_WINDOW(m_pWindow)) // i.e. not if it's a GtkEventBox which doesn't have the signal + g_signal_connect( G_OBJECT(m_pWindow), "set-focus", G_CALLBACK(signalSetFocus), this ); + g_signal_connect( G_OBJECT(m_pWindow), "map-event", G_CALLBACK(signalMap), this ); + g_signal_connect( G_OBJECT(m_pWindow), "unmap-event", G_CALLBACK(signalUnmap), this ); + g_signal_connect( G_OBJECT(m_pWindow), "configure-event", G_CALLBACK(signalConfigure), this ); + g_signal_connect( G_OBJECT(m_pWindow), "key-press-event", G_CALLBACK(signalKey), this ); + g_signal_connect( G_OBJECT(m_pWindow), "key-release-event", G_CALLBACK(signalKey), this ); + g_signal_connect( G_OBJECT(m_pWindow), "delete-event", G_CALLBACK(signalDelete), this ); + g_signal_connect( G_OBJECT(m_pWindow), "window-state-event", G_CALLBACK(signalWindowState), this ); + g_signal_connect( G_OBJECT(m_pWindow), "visibility-notify-event", G_CALLBACK(signalVisibility), this ); + g_signal_connect( G_OBJECT(m_pWindow), "destroy", G_CALLBACK(signalDestroy), this ); + + // init members + m_pCurrentCursor = nullptr; + m_nKeyModifiers = ModKeyFlags::NONE; + m_bFullscreen = false; + m_bSpanMonitorsWhenFullscreen = false; + m_nState = GDK_WINDOW_STATE_WITHDRAWN; + m_pIMHandler = nullptr; + m_pRegion = nullptr; + m_pDropTarget = nullptr; + m_pDragSource = nullptr; + m_bGeometryIsProvisional = false; + m_bIconSetWhileUnmapped = false; + m_bTooltipBlocked = false; + m_ePointerStyle = static_cast<PointerStyle>(0xffff); + m_pSalMenu = nullptr; + m_nWatcherId = 0; + m_nMenuExportId = 0; + m_nActionGroupExportId = 0; + m_nHudAwarenessId = 0; + + gtk_widget_add_events( m_pWindow, + GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | + GDK_VISIBILITY_NOTIFY_MASK | GDK_SCROLL_MASK + ); + + // show the widgets + gtk_widget_show_all(GTK_WIDGET(m_pTopLevelGrid)); + + // realize the window, we need an XWindow id + gtk_widget_realize( m_pWindow ); + + //system data + m_aSystemData.aWindow = GetNativeWindowHandle(m_pWindow); + m_aSystemData.aShellWindow = reinterpret_cast<sal_IntPtr>(this); + m_aSystemData.pSalFrame = this; + m_aSystemData.pWidget = m_pWindow; + m_aSystemData.nScreen = m_nXScreen.getXScreen(); + m_aSystemData.toolkit = SystemEnvData::Toolkit::Gtk3; + +#if defined(GDK_WINDOWING_X11) + GdkDisplay *pDisplay = getGdkDisplay(); + if (DLSYM_GDK_IS_X11_DISPLAY(pDisplay)) + { + m_aSystemData.pDisplay = gdk_x11_display_get_xdisplay(pDisplay); + m_aSystemData.platform = SystemEnvData::Platform::Xcb; + GdkScreen* pScreen = gtk_widget_get_screen(m_pWindow); + GdkVisual* pVisual = gdk_screen_get_system_visual(pScreen); + m_aSystemData.pVisual = gdk_x11_visual_get_xvisual(pVisual); + } +#endif +#if defined(GDK_WINDOWING_WAYLAND) + if (DLSYM_GDK_IS_WAYLAND_DISPLAY(pDisplay)) + { + m_aSystemData.pDisplay = gdk_wayland_display_get_wl_display(pDisplay); + m_aSystemData.platform = SystemEnvData::Platform::Wayland; + } +#endif + + m_bGraphics = false; + m_pGraphics = nullptr; + + m_nFloatFlags = FloatWinPopupFlags::NONE; + m_bFloatPositioned = false; + + m_nWidthRequest = 0; + m_nHeightRequest = 0; + + // fake an initial geometry, gets updated via configure event or SetPosSize + if (m_bDefaultPos || m_bDefaultSize) + { + Size aDefSize = calcDefaultSize(); + maGeometry.nX = -1; + maGeometry.nY = -1; + maGeometry.nWidth = aDefSize.Width(); + maGeometry.nHeight = aDefSize.Height(); + maGeometry.nTopDecoration = 0; + maGeometry.nBottomDecoration = 0; + maGeometry.nLeftDecoration = 0; + maGeometry.nRightDecoration = 0; + } + updateScreenNumber(); + + SetIcon(SV_ICON_ID_OFFICE); +} + +GtkSalFrame *GtkSalFrame::getFromWindow( GtkWidget *pWindow ) +{ + return static_cast<GtkSalFrame *>(g_object_get_data( G_OBJECT( pWindow ), "SalFrame" )); +} + +void GtkSalFrame::Init( SalFrame* pParent, SalFrameStyleFlags nStyle ) +{ + if( nStyle & SalFrameStyleFlags::DEFAULT ) // ensure default style + { + nStyle |= SalFrameStyleFlags::MOVEABLE | SalFrameStyleFlags::SIZEABLE | SalFrameStyleFlags::CLOSEABLE; + nStyle &= ~SalFrameStyleFlags::FLOAT; + } + + m_pParent = static_cast<GtkSalFrame*>(pParent); + m_pForeignParent = nullptr; + m_aForeignParentWindow = None; + m_pForeignTopLevel = nullptr; + m_aForeignTopLevelWindow = None; + m_nStyle = nStyle; + + GtkWindowType eWinType = ( (nStyle & SalFrameStyleFlags::FLOAT) && + ! (nStyle & SalFrameStyleFlags::OWNERDRAWDECORATION) + ) + ? GTK_WINDOW_POPUP : GTK_WINDOW_TOPLEVEL; + + if( nStyle & SalFrameStyleFlags::SYSTEMCHILD ) + { + m_pWindow = gtk_event_box_new(); + if( m_pParent ) + { + // insert into container + gtk_fixed_put( m_pParent->getFixedContainer(), + m_pWindow, 0, 0 ); + } + } + else + { + m_pWindow = gtk_window_new(eWinType); + + // hook up F1 to show help for embedded native gtk widgets + GtkAccelGroup *pGroup = gtk_accel_group_new(); + GClosure* closure = g_cclosure_new(G_CALLBACK(GtkSalFrame::NativeWidgetHelpPressed), GTK_WINDOW(m_pWindow), nullptr); + gtk_accel_group_connect(pGroup, GDK_KEY_F1, static_cast<GdkModifierType>(0), GTK_ACCEL_LOCKED, closure); + gtk_window_add_accel_group(GTK_WINDOW(m_pWindow), pGroup); + } + + g_object_set_data( G_OBJECT( m_pWindow ), "SalFrame", this ); + g_object_set_data( G_OBJECT( m_pWindow ), "libo-version", const_cast<char *>(LIBO_VERSION_DOTTED)); + + // force wm class hint + if (!isChild()) + { + if (m_pParent) + m_sWMClass = m_pParent->m_sWMClass; + updateWMClass(); + } + + if (GTK_IS_WINDOW(m_pWindow)) + { + if (m_pParent) + { + GtkWidget* pTopLevel = gtk_widget_get_toplevel(m_pParent->m_pWindow); + if (!isChild()) + gtk_window_set_screen(GTK_WINDOW(m_pWindow), gtk_widget_get_screen(pTopLevel)); + + if (!(m_pParent->m_nStyle & SalFrameStyleFlags::PLUG)) + gtk_window_set_transient_for(GTK_WINDOW(m_pWindow), GTK_WINDOW(pTopLevel)); + m_pParent->m_aChildren.push_back( this ); + gtk_window_group_add_window(gtk_window_get_group(GTK_WINDOW(pTopLevel)), GTK_WINDOW(m_pWindow)); + } + else + { + gtk_window_group_add_window(gtk_window_group_new(), GTK_WINDOW(m_pWindow)); + g_object_unref(gtk_window_get_group(GTK_WINDOW(m_pWindow))); + } + } + + // set window type + bool bDecoHandling = + ! isChild() && + ( ! (nStyle & SalFrameStyleFlags::FLOAT) || + (nStyle & SalFrameStyleFlags::OWNERDRAWDECORATION) ); + + if( bDecoHandling ) + { + GdkWindowTypeHint eType = GDK_WINDOW_TYPE_HINT_NORMAL; + if( (nStyle & SalFrameStyleFlags::DIALOG) && m_pParent != nullptr ) + eType = GDK_WINDOW_TYPE_HINT_DIALOG; + if( nStyle & SalFrameStyleFlags::INTRO ) + { + gtk_window_set_role( GTK_WINDOW(m_pWindow), "splashscreen" ); + eType = GDK_WINDOW_TYPE_HINT_SPLASHSCREEN; + } + else if( nStyle & SalFrameStyleFlags::TOOLWINDOW ) + { + eType = GDK_WINDOW_TYPE_HINT_DIALOG; + gtk_window_set_skip_taskbar_hint( GTK_WINDOW(m_pWindow), true ); + } + else if( nStyle & SalFrameStyleFlags::OWNERDRAWDECORATION ) + { + eType = GDK_WINDOW_TYPE_HINT_TOOLBAR; + gtk_window_set_focus_on_map(GTK_WINDOW(m_pWindow), false); + gtk_window_set_decorated(GTK_WINDOW(m_pWindow), false); + } + gtk_window_set_type_hint( GTK_WINDOW(m_pWindow), eType ); + gtk_window_set_gravity( GTK_WINDOW(m_pWindow), GDK_GRAVITY_STATIC ); + gtk_window_set_resizable( GTK_WINDOW(m_pWindow), bool(nStyle & SalFrameStyleFlags::SIZEABLE) ); + +#if defined(GDK_WINDOWING_WAYLAND) + //rhbz#1392145 under wayland/csd if we've overridden the default widget direction in order to set LibreOffice's + //UI to the configured ui language but the system ui locale is a different text direction, then the toplevel + //built-in close button of the titlebar follows the overridden direction rather than continue in the same + //direction as every other titlebar on the user's desktop. So if they don't match set an explicit + //header bar with the desired 'outside' direction + if ((eType == GDK_WINDOW_TYPE_HINT_NORMAL || eType == GDK_WINDOW_TYPE_HINT_DIALOG) && DLSYM_GDK_IS_WAYLAND_DISPLAY(GtkSalFrame::getGdkDisplay())) + { + const bool bDesktopIsRTL = MsLangId::isRightToLeft(MsLangId::getSystemUILanguage()); + const bool bAppIsRTL = gtk_widget_get_default_direction() == GTK_TEXT_DIR_RTL; + if (bDesktopIsRTL != bAppIsRTL) + { + m_pHeaderBar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_set_direction(GTK_WIDGET(m_pHeaderBar), bDesktopIsRTL ? GTK_TEXT_DIR_RTL : GTK_TEXT_DIR_LTR); + gtk_header_bar_set_show_close_button(m_pHeaderBar, true); + gtk_window_set_titlebar(GTK_WINDOW(m_pWindow), GTK_WIDGET(m_pHeaderBar)); + gtk_widget_show(GTK_WIDGET(m_pHeaderBar)); + } + } +#endif + } + else if( nStyle & SalFrameStyleFlags::FLOAT ) + gtk_window_set_type_hint( GTK_WINDOW(m_pWindow), GDK_WINDOW_TYPE_HINT_POPUP_MENU ); + + InitCommon(); + + if( eWinType == GTK_WINDOW_TOPLEVEL ) + { + // Enable DBus native menu if available. + ensure_dbus_setup( this ); + + } +} + +GdkNativeWindow GtkSalFrame::findTopLevelSystemWindow( GdkNativeWindow ) +{ + //FIXME: no findToplevelSystemWindow + return 0; +} + +void GtkSalFrame::Init( SystemParentData* pSysData ) +{ + m_pParent = nullptr; + m_aForeignParentWindow = pSysData->aWindow; + m_pForeignParent = nullptr; + m_aForeignTopLevelWindow = findTopLevelSystemWindow(pSysData->aWindow); + m_pForeignTopLevel = gdk_window_foreign_new_for_display( getGdkDisplay(), m_aForeignTopLevelWindow ); + gdk_window_set_events( m_pForeignTopLevel, GDK_STRUCTURE_MASK ); + + if( pSysData->nSize > sizeof(pSysData->nSize)+sizeof(pSysData->aWindow) && pSysData->bXEmbedSupport ) + { + m_pWindow = gtk_plug_new_for_display( getGdkDisplay(), pSysData->aWindow ); + gtk_widget_set_can_default(m_pWindow, true); + gtk_widget_set_can_focus(m_pWindow, true); + gtk_widget_set_sensitive(m_pWindow, true); + } + else + { + m_pWindow = gtk_window_new( GTK_WINDOW_POPUP ); + } + m_nStyle = SalFrameStyleFlags::PLUG; + InitCommon(); + + m_pForeignParent = gdk_window_foreign_new_for_display( getGdkDisplay(), m_aForeignParentWindow ); + gdk_window_set_events( m_pForeignParent, GDK_STRUCTURE_MASK ); + + //FIXME: Handling embedded windows, is going to be fun ... +} + +void GtkSalFrame::SetExtendedFrameStyle(SalExtStyle) +{ +} + +SalGraphics* GtkSalFrame::AcquireGraphics() +{ + if( m_bGraphics ) + return nullptr; + + if( !m_pGraphics ) + { + m_pGraphics.reset( new GtkSalGraphics( this, m_pWindow ) ); + if (!m_pSurface) + { + AllocateFrame(); + TriggerPaintEvent(); + } + m_pGraphics->setSurface(m_pSurface, m_aFrameSize); + } + m_bGraphics = true; + return m_pGraphics.get(); +} + +void GtkSalFrame::ReleaseGraphics( SalGraphics* pGraphics ) +{ + (void) pGraphics; + assert( pGraphics == m_pGraphics.get() ); + m_bGraphics = false; +} + +bool GtkSalFrame::PostEvent(std::unique_ptr<ImplSVEvent> pData) +{ + getDisplay()->SendInternalEvent( this, pData.release() ); + return true; +} + +void GtkSalFrame::SetTitle( const OUString& rTitle ) +{ + if( m_pWindow && ! isChild() ) + { + OString sTitle(OUStringToOString(rTitle, RTL_TEXTENCODING_UTF8)); + gtk_window_set_title(GTK_WINDOW(m_pWindow), sTitle.getStr()); + if (m_pHeaderBar) + gtk_header_bar_set_title(m_pHeaderBar, sTitle.getStr()); + } +} + +void GtkSalFrame::SetIcon(const char* appicon) +{ + gtk_window_set_icon_name(GTK_WINDOW(m_pWindow), appicon); + +#if defined(GDK_WINDOWING_WAYLAND) + if (DLSYM_GDK_IS_WAYLAND_DISPLAY(getGdkDisplay())) + { + static auto set_application_id = reinterpret_cast<void (*) (GdkWindow*, const char*)>( + dlsym(nullptr, "gdk_wayland_window_set_application_id")); + if (set_application_id) + { + GdkWindow* gdkWindow = gtk_widget_get_window(m_pWindow); + set_application_id(gdkWindow, appicon); + + // gdk_wayland_window_set_application_id doesn't seem to work before + // the window is mapped, so set this for real when/if we are mapped + m_bIconSetWhileUnmapped = !gtk_widget_get_mapped(m_pWindow); + } + } +#endif +} + +void GtkSalFrame::SetIcon( sal_uInt16 nIcon ) +{ + if( (m_nStyle & (SalFrameStyleFlags::PLUG|SalFrameStyleFlags::SYSTEMCHILD|SalFrameStyleFlags::FLOAT|SalFrameStyleFlags::INTRO|SalFrameStyleFlags::OWNERDRAWDECORATION)) + || ! m_pWindow ) + return; + + gchar* appicon; + + if (nIcon == SV_ICON_ID_TEXT) + appicon = g_strdup ("libreoffice-writer"); + else if (nIcon == SV_ICON_ID_SPREADSHEET) + appicon = g_strdup ("libreoffice-calc"); + else if (nIcon == SV_ICON_ID_DRAWING) + appicon = g_strdup ("libreoffice-draw"); + else if (nIcon == SV_ICON_ID_PRESENTATION) + appicon = g_strdup ("libreoffice-impress"); + else if (nIcon == SV_ICON_ID_DATABASE) + appicon = g_strdup ("libreoffice-base"); + else if (nIcon == SV_ICON_ID_FORMULA) + appicon = g_strdup ("libreoffice-math"); + else + appicon = g_strdup ("libreoffice-startcenter"); + + SetIcon(appicon); + + g_free (appicon); +} + +void GtkSalFrame::SetMenu( SalMenu* pSalMenu ) +{ + m_pSalMenu = static_cast<GtkSalMenu*>(pSalMenu); +} + +SalMenu* GtkSalFrame::GetMenu() +{ + return m_pSalMenu; +} + +void GtkSalFrame::DrawMenuBar() +{ +} + +void GtkSalFrame::Center() +{ + if (!GTK_IS_WINDOW(m_pWindow)) + return; + if (m_pParent) + gtk_window_set_position(GTK_WINDOW(m_pWindow), GTK_WIN_POS_CENTER_ON_PARENT); + else + gtk_window_set_position(GTK_WINDOW(m_pWindow), GTK_WIN_POS_CENTER); +} + +Size GtkSalFrame::calcDefaultSize() +{ + Size aScreenSize(getDisplay()->GetScreenSize(GetDisplayScreen())); + int scale = gtk_widget_get_scale_factor(m_pWindow); + aScreenSize.setWidth( aScreenSize.Width() / scale ); + aScreenSize.setHeight( aScreenSize.Height() / scale ); + return bestmaxFrameSizeForScreenSize(aScreenSize); +} + +void GtkSalFrame::SetDefaultSize() +{ + Size aDefSize = calcDefaultSize(); + + SetPosSize( 0, 0, aDefSize.Width(), aDefSize.Height(), + SAL_FRAME_POSSIZE_WIDTH | SAL_FRAME_POSSIZE_HEIGHT ); + + if( (m_nStyle & SalFrameStyleFlags::DEFAULT) && m_pWindow ) + gtk_window_maximize( GTK_WINDOW(m_pWindow) ); +} + +void GtkSalFrame::Show( bool bVisible, bool /*bNoActivate*/ ) +{ + if( m_pWindow ) + { + if( bVisible ) + { + getDisplay()->startupNotificationCompleted(); + + if( m_bDefaultPos ) + Center(); + if( m_bDefaultSize ) + SetDefaultSize(); + setMinMaxSize(); + + if (isFloatGrabWindow() && !getDisplay()->GetCaptureFrame()) + { + m_pParent->grabPointer(true, true, true); + m_pParent->addGrabLevel(); + } + +#if defined(GDK_WINDOWING_WAYLAND) + /* + rhbz#1334915, gnome#779143, tdf#100158 + https://gitlab.gnome.org/GNOME/gtk/-/issues/767 + + before gdk_wayland_window_set_application_id was available gtk + under wayland lacked a way to change the app_id of a window, so + brute force everything as a startcenter when initially shown to at + least get the default LibreOffice icon and not the broken app icon + */ + static bool bAppIdImmutable = DLSYM_GDK_IS_WAYLAND_DISPLAY(getGdkDisplay()) && + !dlsym(nullptr, "gdk_wayland_window_set_application_id"); + if (bAppIdImmutable) + { + OString sOrigName(g_get_prgname()); + g_set_prgname("libreoffice-startcenter"); + gtk_widget_show(m_pWindow); + g_set_prgname(sOrigName.getStr()); + } + else + { + gtk_widget_show(m_pWindow); + } +#else + gtk_widget_show(m_pWindow); +#endif + + if( isFloatGrabWindow() ) + { + m_nFloats++; + if (!getDisplay()->GetCaptureFrame()) + { + grabPointer(true, true, true); + addGrabLevel(); + } + // #i44068# reset parent's IM context + if( m_pParent ) + m_pParent->EndExtTextInput(EndExtTextInputFlags::NONE); + } + } + else + { + if( isFloatGrabWindow() ) + { + m_nFloats--; + if (!getDisplay()->GetCaptureFrame()) + { + removeGrabLevel(); + grabPointer(false, true, false); + m_pParent->removeGrabLevel(); + m_pParent->grabPointer(false, true, false); + } + } + gtk_widget_hide( m_pWindow ); + if( m_pIMHandler ) + m_pIMHandler->focusChanged( false ); + } + } +} + +void GtkSalFrame::setMinMaxSize() +{ + /* #i34504# metacity (and possibly others) do not treat + * _NET_WM_STATE_FULLSCREEN and max_width/height independently; + * whether they should is undefined. So don't set the max size hint + * for a full screen window. + */ + if( m_pWindow && ! isChild() ) + { + GdkGeometry aGeo; + int aHints = 0; + if( m_nStyle & SalFrameStyleFlags::SIZEABLE ) + { + if( m_aMinSize.Width() && m_aMinSize.Height() && ! m_bFullscreen ) + { + aGeo.min_width = m_aMinSize.Width(); + aGeo.min_height = m_aMinSize.Height(); + aHints |= GDK_HINT_MIN_SIZE; + } + if( m_aMaxSize.Width() && m_aMaxSize.Height() && ! m_bFullscreen ) + { + aGeo.max_width = m_aMaxSize.Width(); + aGeo.max_height = m_aMaxSize.Height(); + aHints |= GDK_HINT_MAX_SIZE; + } + } + else + { + if (!m_bFullscreen && m_nWidthRequest && m_nHeightRequest) + { + aGeo.min_width = m_nWidthRequest; + aGeo.min_height = m_nHeightRequest; + aHints |= GDK_HINT_MIN_SIZE; + + aGeo.max_width = m_nWidthRequest; + aGeo.max_height = m_nHeightRequest; + aHints |= GDK_HINT_MAX_SIZE; + } + } + + if( m_bFullscreen && m_aMaxSize.Width() && m_aMaxSize.Height() ) + { + aGeo.max_width = m_aMaxSize.Width(); + aGeo.max_height = m_aMaxSize.Height(); + aHints |= GDK_HINT_MAX_SIZE; + } + if( aHints ) + { + gtk_window_set_geometry_hints( GTK_WINDOW(m_pWindow), + nullptr, + &aGeo, + GdkWindowHints( aHints ) ); + } + } +} + +void GtkSalFrame::SetMaxClientSize( long nWidth, long nHeight ) +{ + if( ! isChild() ) + { + m_aMaxSize = Size( nWidth, nHeight ); + setMinMaxSize(); + } +} +void GtkSalFrame::SetMinClientSize( long nWidth, long nHeight ) +{ + if( ! isChild() ) + { + m_aMinSize = Size( nWidth, nHeight ); + if( m_pWindow ) + { + widget_set_size_request(nWidth, nHeight); + setMinMaxSize(); + } + } +} + +void GtkSalFrame::AllocateFrame() +{ + basegfx::B2IVector aFrameSize( maGeometry.nWidth, maGeometry.nHeight ); + if (!m_pSurface || m_aFrameSize.getX() != aFrameSize.getX() || + m_aFrameSize.getY() != aFrameSize.getY() ) + { + if( aFrameSize.getX() == 0 ) + aFrameSize.setX( 1 ); + if( aFrameSize.getY() == 0 ) + aFrameSize.setY( 1 ); + + if (m_pSurface) + cairo_surface_destroy(m_pSurface); + + m_pSurface = gdk_window_create_similar_surface(gtk_widget_get_window(m_pWindow), + CAIRO_CONTENT_COLOR_ALPHA, + aFrameSize.getX(), + aFrameSize.getY()); + m_aFrameSize = aFrameSize; + + cairo_surface_set_user_data(m_pSurface, SvpSalGraphics::getDamageKey(), &m_aDamageHandler, nullptr); + SAL_INFO("vcl.gtk3", "allocated Frame size of " << maGeometry.nWidth << " x " << maGeometry.nHeight); + + if (m_pGraphics) + m_pGraphics->setSurface(m_pSurface, m_aFrameSize); + } +} + +void GtkSalFrame::SetPosSize( long nX, long nY, long nWidth, long nHeight, sal_uInt16 nFlags ) +{ + if( !m_pWindow || isChild( true, false ) ) + return; + + if( (nFlags & ( SAL_FRAME_POSSIZE_WIDTH | SAL_FRAME_POSSIZE_HEIGHT )) && + (nWidth > 0 && nHeight > 0 ) // sometimes stupid things happen + ) + { + m_bDefaultSize = false; + + maGeometry.nWidth = nWidth; + maGeometry.nHeight = nHeight; + + if( isChild( false ) ) + widget_set_size_request(nWidth, nHeight); + else if( ! ( m_nState & GDK_WINDOW_STATE_MAXIMIZED ) ) + window_resize(nWidth, nHeight); + + setMinMaxSize(); + } + else if( m_bDefaultSize ) + SetDefaultSize(); + + m_bDefaultSize = false; + + if( nFlags & ( SAL_FRAME_POSSIZE_X | SAL_FRAME_POSSIZE_Y ) ) + { + if( m_pParent ) + { + if( AllSettings::GetLayoutRTL() ) + nX = m_pParent->maGeometry.nWidth-m_nWidthRequest-1-nX; + nX += m_pParent->maGeometry.nX; + nY += m_pParent->maGeometry.nY; + } + + if (nFlags & SAL_FRAME_POSSIZE_X) + maGeometry.nX = nX; + if (nFlags & SAL_FRAME_POSSIZE_Y) + maGeometry.nY = nY; + m_bGeometryIsProvisional = true; + + m_bDefaultPos = false; + + moveWindow(maGeometry.nX, maGeometry.nY); + + updateScreenNumber(); + } + else if( m_bDefaultPos ) + Center(); + + m_bDefaultPos = false; +} + +void GtkSalFrame::GetClientSize( long& rWidth, long& rHeight ) +{ + if( m_pWindow && !(m_nState & GDK_WINDOW_STATE_ICONIFIED) ) + { + rWidth = maGeometry.nWidth; + rHeight = maGeometry.nHeight; + } + else + rWidth = rHeight = 0; +} + +void GtkSalFrame::GetWorkArea( tools::Rectangle& rRect ) +{ + GdkScreen *pScreen = gtk_widget_get_screen(m_pWindow); + tools::Rectangle aRetRect; + int max = gdk_screen_get_n_monitors (pScreen); + for (int i = 0; i < max; ++i) + { + GdkRectangle aRect; + gdk_screen_get_monitor_workarea(pScreen, i, &aRect); + tools::Rectangle aMonitorRect(aRect.x, aRect.y, aRect.x+aRect.width, aRect.y+aRect.height); + aRetRect.Union(aMonitorRect); + } + rRect = aRetRect; +} + +SalFrame* GtkSalFrame::GetParent() const +{ + return m_pParent; +} + +void GtkSalFrame::SetWindowState( const SalFrameState* pState ) +{ + if( ! m_pWindow || ! pState || isChild( true, false ) ) + return; + + const WindowStateMask nMaxGeometryMask = + WindowStateMask::X | WindowStateMask::Y | + WindowStateMask::Width | WindowStateMask::Height | + WindowStateMask::MaximizedX | WindowStateMask::MaximizedY | + WindowStateMask::MaximizedWidth | WindowStateMask::MaximizedHeight; + + if( (pState->mnMask & WindowStateMask::State) && + ! ( m_nState & GDK_WINDOW_STATE_MAXIMIZED ) && + (pState->mnState & WindowStateState::Maximized) && + (pState->mnMask & nMaxGeometryMask) == nMaxGeometryMask ) + { + resizeWindow( pState->mnWidth, pState->mnHeight ); + moveWindow( pState->mnX, pState->mnY ); + m_bDefaultPos = m_bDefaultSize = false; + + updateScreenNumber(); + + m_nState = GdkWindowState( m_nState | GDK_WINDOW_STATE_MAXIMIZED ); + m_aRestorePosSize = tools::Rectangle( Point( pState->mnX, pState->mnY ), + Size( pState->mnWidth, pState->mnHeight ) ); + } + else if( pState->mnMask & (WindowStateMask::X | WindowStateMask::Y | + WindowStateMask::Width | WindowStateMask::Height ) ) + { + sal_uInt16 nPosSizeFlags = 0; + long nX = pState->mnX - (m_pParent ? m_pParent->maGeometry.nX : 0); + long nY = pState->mnY - (m_pParent ? m_pParent->maGeometry.nY : 0); + if( pState->mnMask & WindowStateMask::X ) + nPosSizeFlags |= SAL_FRAME_POSSIZE_X; + else + nX = maGeometry.nX - (m_pParent ? m_pParent->maGeometry.nX : 0); + if( pState->mnMask & WindowStateMask::Y ) + nPosSizeFlags |= SAL_FRAME_POSSIZE_Y; + else + nY = maGeometry.nY - (m_pParent ? m_pParent->maGeometry.nY : 0); + if( pState->mnMask & WindowStateMask::Width ) + nPosSizeFlags |= SAL_FRAME_POSSIZE_WIDTH; + if( pState->mnMask & WindowStateMask::Height ) + nPosSizeFlags |= SAL_FRAME_POSSIZE_HEIGHT; + SetPosSize( nX, nY, pState->mnWidth, pState->mnHeight, nPosSizeFlags ); + } + if( pState->mnMask & WindowStateMask::State && ! isChild() ) + { + if( pState->mnState & WindowStateState::Maximized ) + gtk_window_maximize( GTK_WINDOW(m_pWindow) ); + else + gtk_window_unmaximize( GTK_WINDOW(m_pWindow) ); + /* #i42379# there is no rollup state in GDK; and rolled up windows are + * (probably depending on the WM) reported as iconified. If we iconify a + * window here that was e.g. a dialog, then it will be unmapped but still + * not be displayed in the task list, so it's an iconified window that + * the user cannot get out of this state. So do not set the iconified state + * on windows with a parent (that is transient frames) since these tend + * to not be represented in an icon task list. + */ + if( (pState->mnState & WindowStateState::Minimized) + && ! m_pParent ) + gtk_window_iconify( GTK_WINDOW(m_pWindow) ); + else + gtk_window_deiconify( GTK_WINDOW(m_pWindow) ); + } + TriggerPaintEvent(); +} + +namespace +{ + void GetPosAndSize(GtkWindow *pWindow, long& rX, long &rY, long &rWidth, long &rHeight) + { + gint root_x, root_y; + gtk_window_get_position(GTK_WINDOW(pWindow), &root_x, &root_y); + rX = root_x; + rY = root_y; + gint width, height; + gtk_window_get_size(GTK_WINDOW(pWindow), &width, &height); + rWidth = width; + rHeight = height; + } + + tools::Rectangle GetPosAndSize(GtkWindow *pWindow) + { + long nX, nY, nWidth, nHeight; + GetPosAndSize(pWindow, nX, nY, nWidth, nHeight); + return tools::Rectangle(nX, nY, nX + nWidth, nY + nHeight); + } +} + +bool GtkSalFrame::GetWindowState( SalFrameState* pState ) +{ + pState->mnState = WindowStateState::Normal; + pState->mnMask = WindowStateMask::State; + // rollup ? gtk 2.2 does not seem to support the shaded state + if( m_nState & GDK_WINDOW_STATE_ICONIFIED ) + pState->mnState |= WindowStateState::Minimized; + if( m_nState & GDK_WINDOW_STATE_MAXIMIZED ) + { + pState->mnState |= WindowStateState::Maximized; + pState->mnX = m_aRestorePosSize.Left(); + pState->mnY = m_aRestorePosSize.Top(); + pState->mnWidth = m_aRestorePosSize.GetWidth(); + pState->mnHeight = m_aRestorePosSize.GetHeight(); + GetPosAndSize(GTK_WINDOW(m_pWindow), pState->mnMaximizedX, pState->mnMaximizedY, + pState->mnMaximizedWidth, pState->mnMaximizedHeight); + pState->mnMask |= WindowStateMask::MaximizedX | + WindowStateMask::MaximizedY | + WindowStateMask::MaximizedWidth | + WindowStateMask::MaximizedHeight; + } + else + { + GetPosAndSize(GTK_WINDOW(m_pWindow), pState->mnX, pState->mnY, + pState->mnWidth, pState->mnHeight); + } + pState->mnMask |= WindowStateMask::X | + WindowStateMask::Y | + WindowStateMask::Width | + WindowStateMask::Height; + + return true; +} + +void GtkSalFrame::SetScreen( unsigned int nNewScreen, SetType eType, tools::Rectangle const *pSize ) +{ + if( !m_pWindow ) + return; + + if (maGeometry.nDisplayScreenNumber == nNewScreen && eType == SetType::RetainSize) + return; + + int nX = maGeometry.nX, nY = maGeometry.nY, + nWidth = maGeometry.nWidth, nHeight = maGeometry.nHeight; + GdkScreen *pScreen = nullptr; + GdkRectangle aNewMonitor; + + bool bSpanAllScreens = nNewScreen == static_cast<unsigned int>(-1); + m_bSpanMonitorsWhenFullscreen = bSpanAllScreens && getDisplay()->getSystem()->GetDisplayScreenCount() > 1; + gint nMonitor = -1; + if (m_bSpanMonitorsWhenFullscreen) //span all screens + { + pScreen = gtk_widget_get_screen( m_pWindow ); + aNewMonitor.x = 0; + aNewMonitor.y = 0; + aNewMonitor.width = gdk_screen_get_width(pScreen); + aNewMonitor.height = gdk_screen_get_height(pScreen); + } + else + { + bool bSameMonitor = false; + + if (!bSpanAllScreens) + { + pScreen = getDisplay()->getSystem()->getScreenMonitorFromIdx( nNewScreen, nMonitor ); + if (!pScreen) + { + g_warning ("Attempt to move GtkSalFrame to invalid screen %d => " + "fallback to current\n", nNewScreen); + } + } + + if (!pScreen) + { + pScreen = gtk_widget_get_screen( m_pWindow ); + bSameMonitor = true; + } + + // Heavy lifting, need to move screen ... + if( pScreen != gtk_widget_get_screen( m_pWindow )) + gtk_window_set_screen( GTK_WINDOW( m_pWindow ), pScreen ); + + gint nOldMonitor = gdk_screen_get_monitor_at_window( + pScreen, gtk_widget_get_window( m_pWindow ) ); + if (bSameMonitor) + nMonitor = nOldMonitor; + + #if OSL_DEBUG_LEVEL > 1 + if( nMonitor == nOldMonitor ) + g_warning( "An apparently pointless SetScreen - should we elide it ?" ); + #endif + + GdkRectangle aOldMonitor; + gdk_screen_get_monitor_geometry( pScreen, nOldMonitor, &aOldMonitor ); + gdk_screen_get_monitor_geometry( pScreen, nMonitor, &aNewMonitor ); + + nX = aNewMonitor.x + nX - aOldMonitor.x; + nY = aNewMonitor.y + nY - aOldMonitor.y; + } + + bool bResize = false; + bool bVisible = gtk_widget_get_mapped( m_pWindow ); + if( bVisible ) + Show( false ); + + if( eType == SetType::Fullscreen ) + { + nX = aNewMonitor.x; + nY = aNewMonitor.y; + nWidth = aNewMonitor.width; + nHeight = aNewMonitor.height; + m_nStyle |= SalFrameStyleFlags::PARTIAL_FULLSCREEN; + bResize = true; + + // #i110881# for the benefit of compiz set a max size here + // else setting to fullscreen fails for unknown reasons + m_aMaxSize.setWidth( aNewMonitor.width ); + m_aMaxSize.setHeight( aNewMonitor.height ); + } + + if( pSize && eType == SetType::UnFullscreen ) + { + nX = pSize->Left(); + nY = pSize->Top(); + nWidth = pSize->GetWidth(); + nHeight = pSize->GetHeight(); + m_nStyle &= ~SalFrameStyleFlags::PARTIAL_FULLSCREEN; + bResize = true; + } + + if (bResize) + { + // temporarily re-sizeable + if( !(m_nStyle & SalFrameStyleFlags::SIZEABLE) ) + gtk_window_set_resizable( GTK_WINDOW(m_pWindow), true ); + window_resize(nWidth, nHeight); + } + + gtk_window_move(GTK_WINDOW(m_pWindow), nX, nY); + + gdk_window_set_fullscreen_mode( gtk_widget_get_window(m_pWindow), m_bSpanMonitorsWhenFullscreen + ? GDK_FULLSCREEN_ON_ALL_MONITORS : GDK_FULLSCREEN_ON_CURRENT_MONITOR ); + + GtkWidget* pMenuBarContainerWidget = m_pSalMenu ? m_pSalMenu->GetMenuBarContainerWidget() : nullptr; + if( eType == SetType::Fullscreen ) + { + if (pMenuBarContainerWidget) + gtk_widget_hide(pMenuBarContainerWidget); + if (m_bSpanMonitorsWhenFullscreen) + gtk_window_fullscreen(GTK_WINDOW(m_pWindow)); + else + { + gtk_window_fullscreen_on_monitor(GTK_WINDOW(m_pWindow), pScreen, nMonitor); + } + + } + else if( eType == SetType::UnFullscreen ) + { + if (pMenuBarContainerWidget) + gtk_widget_show(pMenuBarContainerWidget); + gtk_window_unfullscreen( GTK_WINDOW( m_pWindow ) ); + } + + if( eType == SetType::UnFullscreen && + !(m_nStyle & SalFrameStyleFlags::SIZEABLE) ) + gtk_window_set_resizable( GTK_WINDOW( m_pWindow ), FALSE ); + + // FIXME: we should really let gtk+ handle our widget hierarchy ... + if( m_pParent && gtk_widget_get_screen( m_pParent->m_pWindow ) != pScreen ) + SetParent( nullptr ); + std::list< GtkSalFrame* > aChildren = m_aChildren; + for (auto const& child : aChildren) + child->SetScreen( nNewScreen, SetType::RetainSize ); + + m_bDefaultPos = m_bDefaultSize = false; + updateScreenNumber(); + + if( bVisible ) + Show( true ); +} + +void GtkSalFrame::SetScreenNumber( unsigned int nNewScreen ) +{ + SetScreen( nNewScreen, SetType::RetainSize ); +} + +void GtkSalFrame::updateWMClass() +{ + OString aResClass = OUStringToOString(m_sWMClass, RTL_TEXTENCODING_ASCII_US); + const char *pResClass = !aResClass.isEmpty() ? aResClass.getStr() : + SalGenericSystem::getFrameClassName(); + Display *display; + + if (!getDisplay()->IsX11Display()) + return; + + display = GDK_DISPLAY_XDISPLAY(getGdkDisplay()); + + if( gtk_widget_get_realized( m_pWindow ) ) + { + XClassHint* pClass = XAllocClassHint(); + OString aResName = SalGenericSystem::getFrameResName(); + pClass->res_name = const_cast<char*>(aResName.getStr()); + pClass->res_class = const_cast<char*>(pResClass); + XSetClassHint( display, + widget_get_xid(m_pWindow), + pClass ); + XFree( pClass ); + } +} + +void GtkSalFrame::SetApplicationID( const OUString &rWMClass ) +{ + if( rWMClass != m_sWMClass && ! isChild() ) + { + m_sWMClass = rWMClass; + updateWMClass(); + + for (auto const& child : m_aChildren) + child->SetApplicationID(rWMClass); + } +} + +void GtkSalFrame::ShowFullScreen( bool bFullScreen, sal_Int32 nScreen ) +{ + m_bFullscreen = bFullScreen; + + if( !m_pWindow || isChild() ) + return; + + if( bFullScreen ) + { + m_aRestorePosSize = GetPosAndSize(GTK_WINDOW(m_pWindow)); + SetScreen( nScreen, SetType::Fullscreen ); + } + else + { + SetScreen( nScreen, SetType::UnFullscreen, + !m_aRestorePosSize.IsEmpty() ? &m_aRestorePosSize : nullptr ); + m_aRestorePosSize = tools::Rectangle(); + } +} + +void GtkSalFrame::StartPresentation( bool bStart ) +{ + std::optional<guint> aWindow; + std::optional<Display*> aDisplay; + if( getDisplay()->IsX11Display() ) + { + aWindow = widget_get_xid(m_pWindow); + aDisplay = GDK_DISPLAY_XDISPLAY( getGdkDisplay() ); + } + + m_ScreenSaverInhibitor.inhibit( bStart, + "presentation", + getDisplay()->IsX11Display(), + aWindow, + aDisplay ); +} + +void GtkSalFrame::SetAlwaysOnTop( bool bOnTop ) +{ + if( m_pWindow ) + gtk_window_set_keep_above( GTK_WINDOW( m_pWindow ), bOnTop ); +} + +static guint32 nLastUserInputTime = GDK_CURRENT_TIME; + +guint32 GtkSalFrame::GetLastInputEventTime() +{ + return nLastUserInputTime; +} + +void GtkSalFrame::UpdateLastInputEventTime(guint32 nUserInputTime) +{ + //gtk3 can generate a synthetic crossing event with a useless 0 + //(GDK_CURRENT_TIME) timestamp on showing a menu from the main + //menubar, which is unhelpful, so ignore the 0 timestamps + if (nUserInputTime == GDK_CURRENT_TIME) + return; + nLastUserInputTime = nUserInputTime; +} + +void GtkSalFrame::ToTop( SalFrameToTop nFlags ) +{ + if( m_pWindow ) + { + if( isChild( false ) ) + GrabFocus(); + else if( gtk_widget_get_mapped( m_pWindow ) ) + { + if (!(nFlags & SalFrameToTop::GrabFocusOnly)) + gtk_window_present_with_time(GTK_WINDOW(m_pWindow), GetLastInputEventTime()); + else + gdk_window_focus(gtk_widget_get_window(m_pWindow), GetLastInputEventTime()); + GrabFocus(); + } + else + { + if( nFlags & SalFrameToTop::RestoreWhenMin ) + gtk_window_present( GTK_WINDOW(m_pWindow) ); + } + } +} + +void GtkSalFrame::SetPointer( PointerStyle ePointerStyle ) +{ + if( m_pWindow && ePointerStyle != m_ePointerStyle ) + { + m_ePointerStyle = ePointerStyle; + GdkCursor *pCursor = getDisplay()->getCursor( ePointerStyle ); + gdk_window_set_cursor( gtk_widget_get_window(m_pWindow), pCursor ); + m_pCurrentCursor = pCursor; + + // #i80791# use grabPointer the same way as CaptureMouse, respective float grab + if( getDisplay()->MouseCaptured( this ) ) + grabPointer( true, false, false ); + else if( m_nFloats > 0 ) + grabPointer( true, false, true ); + } +} + +void GtkSalFrame::grabPointer( bool bGrab, bool bKeyboardAlso, bool bOwnerEvents ) +{ + if (bGrab) + { + // tdf#135779 move focus back inside usual input window out of any + // other gtk widgets before grabbing the pointer + GrabFocus(); + } + + static const char* pEnv = getenv( "SAL_NO_MOUSEGRABS" ); + if (pEnv && *pEnv) + return; + + if (!m_pWindow) + return; + +#if GTK_CHECK_VERSION(3, 20, 0) + if (gtk_check_version(3, 20, 0) == nullptr) + { + GdkSeat* pSeat = gdk_display_get_default_seat(getGdkDisplay()); + if (bGrab) + { + GdkSeatCapabilities eCapability = bKeyboardAlso ? GDK_SEAT_CAPABILITY_ALL : GDK_SEAT_CAPABILITY_ALL_POINTING; + gdk_seat_grab(pSeat, gtk_widget_get_window(getMouseEventWidget()), eCapability, + bOwnerEvents, nullptr, nullptr, nullptr, nullptr); + } + else + { + gdk_seat_ungrab(pSeat); + } + return; + } +#endif + + //else older gtk3 + GdkDeviceManager* pDeviceManager = gdk_display_get_device_manager(getGdkDisplay()); + GdkDevice* pPointer = gdk_device_manager_get_client_pointer(pDeviceManager); + GdkDevice* pKeyboard = bKeyboardAlso ? gdk_device_get_associated_device(pPointer) : nullptr; + GdkWindow* pWindow = gtk_widget_get_window(getMouseEventWidget()); + guint32 nCurrentTime = gtk_get_current_event_time(); + if (bGrab) + { + gdk_device_grab(pPointer, pWindow, GDK_OWNERSHIP_NONE, + bOwnerEvents, GDK_ALL_EVENTS_MASK, m_pCurrentCursor, nCurrentTime); + if (pKeyboard) + gdk_device_grab(pKeyboard, pWindow, GDK_OWNERSHIP_NONE, true, GDK_ALL_EVENTS_MASK, nullptr, nCurrentTime); + } + else + { + gdk_device_ungrab(pPointer, nCurrentTime); + if (pKeyboard) + gdk_device_ungrab(pKeyboard, nCurrentTime); + } +} + +void GtkSalFrame::CaptureMouse( bool bCapture ) +{ + getDisplay()->CaptureMouse( bCapture ? this : nullptr ); +} + +void GtkSalFrame::SetPointerPos( long nX, long nY ) +{ + GtkSalFrame* pFrame = this; + while( pFrame && pFrame->isChild( false ) ) + pFrame = pFrame->m_pParent; + if( ! pFrame ) + return; + + GdkScreen *pScreen = gtk_widget_get_screen(pFrame->m_pWindow); + GdkDisplay *pDisplay = gdk_screen_get_display( pScreen ); + + /* when the application tries to center the mouse in the dialog the + * window isn't mapped already. So use coordinates relative to the root window. + */ + unsigned int nWindowLeft = maGeometry.nX + nX; + unsigned int nWindowTop = maGeometry.nY + nY; + + GdkDeviceManager* pManager = gdk_display_get_device_manager(pDisplay); + gdk_device_warp(gdk_device_manager_get_client_pointer(pManager), pScreen, nWindowLeft, nWindowTop); + + // #i38648# ask for the next motion hint + gint x, y; + GdkModifierType mask; + gdk_window_get_pointer( gtk_widget_get_window(pFrame->m_pWindow) , &x, &y, &mask ); +} + +void GtkSalFrame::Flush() +{ + gdk_display_flush( getGdkDisplay() ); +} + +void GtkSalFrame::KeyCodeToGdkKey(const vcl::KeyCode& rKeyCode, + guint* pGdkKeyCode, GdkModifierType *pGdkModifiers) +{ + if ( pGdkKeyCode == nullptr || pGdkModifiers == nullptr ) + return; + + // Get GDK key modifiers + GdkModifierType nModifiers = GdkModifierType(0); + + if ( rKeyCode.IsShift() ) + nModifiers = static_cast<GdkModifierType>( nModifiers | GDK_SHIFT_MASK ); + + if ( rKeyCode.IsMod1() ) + nModifiers = static_cast<GdkModifierType>( nModifiers | GDK_CONTROL_MASK ); + + if ( rKeyCode.IsMod2() ) + nModifiers = static_cast<GdkModifierType>( nModifiers | GDK_MOD1_MASK ); + + *pGdkModifiers = nModifiers; + + // Get GDK keycode. + guint nKeyCode = 0; + + guint nCode = rKeyCode.GetCode(); + + if ( nCode >= KEY_0 && nCode <= KEY_9 ) + nKeyCode = ( nCode - KEY_0 ) + GDK_KEY_0; + else if ( nCode >= KEY_A && nCode <= KEY_Z ) + nKeyCode = ( nCode - KEY_A ) + GDK_KEY_A; + else if ( nCode >= KEY_F1 && nCode <= KEY_F26 ) + nKeyCode = ( nCode - KEY_F1 ) + GDK_KEY_F1; + else + { + switch (nCode) + { + case KEY_DOWN: nKeyCode = GDK_KEY_Down; break; + case KEY_UP: nKeyCode = GDK_KEY_Up; break; + case KEY_LEFT: nKeyCode = GDK_KEY_Left; break; + case KEY_RIGHT: nKeyCode = GDK_KEY_Right; break; + case KEY_HOME: nKeyCode = GDK_KEY_Home; break; + case KEY_END: nKeyCode = GDK_KEY_End; break; + case KEY_PAGEUP: nKeyCode = GDK_KEY_Page_Up; break; + case KEY_PAGEDOWN: nKeyCode = GDK_KEY_Page_Down; break; + case KEY_RETURN: nKeyCode = GDK_KEY_Return; break; + case KEY_ESCAPE: nKeyCode = GDK_KEY_Escape; break; + case KEY_TAB: nKeyCode = GDK_KEY_Tab; break; + case KEY_BACKSPACE: nKeyCode = GDK_KEY_BackSpace; break; + case KEY_SPACE: nKeyCode = GDK_KEY_space; break; + case KEY_INSERT: nKeyCode = GDK_KEY_Insert; break; + case KEY_DELETE: nKeyCode = GDK_KEY_Delete; break; + case KEY_ADD: nKeyCode = GDK_KEY_plus; break; + case KEY_SUBTRACT: nKeyCode = GDK_KEY_minus; break; + case KEY_MULTIPLY: nKeyCode = GDK_KEY_asterisk; break; + case KEY_DIVIDE: nKeyCode = GDK_KEY_slash; break; + case KEY_POINT: nKeyCode = GDK_KEY_period; break; + case KEY_COMMA: nKeyCode = GDK_KEY_comma; break; + case KEY_LESS: nKeyCode = GDK_KEY_less; break; + case KEY_GREATER: nKeyCode = GDK_KEY_greater; break; + case KEY_EQUAL: nKeyCode = GDK_KEY_equal; break; + case KEY_FIND: nKeyCode = GDK_KEY_Find; break; + case KEY_CONTEXTMENU: nKeyCode = GDK_KEY_Menu; break; + case KEY_HELP: nKeyCode = GDK_KEY_Help; break; + case KEY_UNDO: nKeyCode = GDK_KEY_Undo; break; + case KEY_REPEAT: nKeyCode = GDK_KEY_Redo; break; + case KEY_DECIMAL: nKeyCode = GDK_KEY_KP_Decimal; break; + case KEY_TILDE: nKeyCode = GDK_KEY_asciitilde; break; + case KEY_QUOTELEFT: nKeyCode = GDK_KEY_quoteleft; break; + case KEY_BRACKETLEFT: nKeyCode = GDK_KEY_bracketleft; break; + case KEY_BRACKETRIGHT: nKeyCode = GDK_KEY_bracketright; break; + case KEY_SEMICOLON: nKeyCode = GDK_KEY_semicolon; break; + case KEY_QUOTERIGHT: nKeyCode = GDK_KEY_quoteright; break; + + // Special cases + case KEY_COPY: nKeyCode = GDK_KEY_Copy; break; + case KEY_CUT: nKeyCode = GDK_KEY_Cut; break; + case KEY_PASTE: nKeyCode = GDK_KEY_Paste; break; + case KEY_OPEN: nKeyCode = GDK_KEY_Open; break; + } + } + + *pGdkKeyCode = nKeyCode; +} + +OUString GtkSalFrame::GetKeyName( sal_uInt16 nKeyCode ) +{ + guint nGtkKeyCode; + GdkModifierType nGtkModifiers; + KeyCodeToGdkKey(nKeyCode, &nGtkKeyCode, &nGtkModifiers ); + + gchar* pName = gtk_accelerator_get_label(nGtkKeyCode, nGtkModifiers); + OUString aRet(pName, rtl_str_getLength(pName), RTL_TEXTENCODING_UTF8); + g_free(pName); + return aRet; +} + +GdkDisplay *GtkSalFrame::getGdkDisplay() +{ + return GetGtkSalData()->GetGdkDisplay(); +} + +GtkSalDisplay *GtkSalFrame::getDisplay() +{ + return GetGtkSalData()->GetGtkDisplay(); +} + +SalFrame::SalPointerState GtkSalFrame::GetPointerState() +{ + SalPointerState aState; + GdkScreen* pScreen; + gint x, y; + GdkModifierType aMask; + gdk_display_get_pointer( getGdkDisplay(), &pScreen, &x, &y, &aMask ); + aState.maPos = Point( x - maGeometry.nX, y - maGeometry.nY ); + aState.mnState = GetMouseModCode( aMask ); + return aState; +} + +KeyIndicatorState GtkSalFrame::GetIndicatorState() +{ + KeyIndicatorState nState = KeyIndicatorState::NONE; + + GdkKeymap *pKeyMap = gdk_keymap_get_for_display(getGdkDisplay()); + + if (gdk_keymap_get_caps_lock_state(pKeyMap)) + nState |= KeyIndicatorState::CAPSLOCK; + if (gdk_keymap_get_num_lock_state(pKeyMap)) + nState |= KeyIndicatorState::NUMLOCK; + if (gdk_keymap_get_scroll_lock_state(pKeyMap)) + nState |= KeyIndicatorState::SCROLLLOCK; + + return nState; +} + +void GtkSalFrame::SimulateKeyPress( sal_uInt16 nKeyCode ) +{ + g_warning ("missing simulate keypress %d", nKeyCode); +} + +void GtkSalFrame::SetInputContext( SalInputContext* pContext ) +{ + if( ! pContext ) + return; + + if( ! (pContext->mnOptions & InputContextFlags::Text) ) + return; + + // create a new im context + if( ! m_pIMHandler ) + m_pIMHandler.reset( new IMHandler( this ) ); +} + +void GtkSalFrame::EndExtTextInput( EndExtTextInputFlags nFlags ) +{ + if( m_pIMHandler ) + m_pIMHandler->endExtTextInput( nFlags ); +} + +bool GtkSalFrame::MapUnicodeToKeyCode( sal_Unicode , LanguageType , vcl::KeyCode& ) +{ + // not supported yet + return false; +} + +LanguageType GtkSalFrame::GetInputLanguage() +{ + return LANGUAGE_DONTKNOW; +} + +void GtkSalFrame::UpdateSettings( AllSettings& rSettings ) +{ + if( ! m_pWindow ) + return; + + GtkSalGraphics* pGraphics = m_pGraphics.get(); + bool bFreeGraphics = false; + if( ! pGraphics ) + { + pGraphics = static_cast<GtkSalGraphics*>(AcquireGraphics()); + if ( !pGraphics ) + { + SAL_WARN("vcl.gtk3", "Could not get graphics - unable to update settings"); + return; + } + bFreeGraphics = true; + } + + pGraphics->UpdateSettings( rSettings ); + + if( bFreeGraphics ) + ReleaseGraphics( pGraphics ); +} + +void GtkSalFrame::Beep() +{ + gdk_display_beep( getGdkDisplay() ); +} + +const SystemEnvData* GtkSalFrame::GetSystemData() const +{ + return &m_aSystemData; +} + +void GtkSalFrame::SetParent( SalFrame* pNewParent ) +{ + GtkWindow* pWindow = GTK_IS_WINDOW(m_pWindow) ? GTK_WINDOW(m_pWindow) : nullptr; + if (m_pParent) + { + if (pWindow && GTK_IS_WINDOW(m_pParent->m_pWindow)) + gtk_window_group_remove_window(gtk_window_get_group(GTK_WINDOW(m_pParent->m_pWindow)), pWindow); + m_pParent->m_aChildren.remove(this); + } + m_pParent = static_cast<GtkSalFrame*>(pNewParent); + if (m_pParent) + { + m_pParent->m_aChildren.push_back(this); + if (pWindow && GTK_IS_WINDOW(m_pParent->m_pWindow)) + gtk_window_group_add_window(gtk_window_get_group(GTK_WINDOW(m_pParent->m_pWindow)), pWindow); + } + if (!isChild() && pWindow) + gtk_window_set_transient_for( pWindow, + (m_pParent && ! m_pParent->isChild(true,false)) ? GTK_WINDOW(m_pParent->m_pWindow) : nullptr + ); +} + +bool GtkSalFrame::SetPluginParent( SystemParentData* ) +{ + //FIXME: no SetPluginParent impl. for gtk3 + return false; +} + +void GtkSalFrame::ResetClipRegion() +{ + if( m_pWindow ) + gdk_window_shape_combine_region( gtk_widget_get_window( m_pWindow ), nullptr, 0, 0 ); +} + +void GtkSalFrame::BeginSetClipRegion( sal_uInt32 ) +{ + if( m_pRegion ) + cairo_region_destroy( m_pRegion ); + m_pRegion = cairo_region_create(); +} + +void GtkSalFrame::UnionClipRegion( long nX, long nY, long nWidth, long nHeight ) +{ + if( m_pRegion ) + { + GdkRectangle aRect; + aRect.x = nX; + aRect.y = nY; + aRect.width = nWidth; + aRect.height = nHeight; + cairo_region_union_rectangle( m_pRegion, &aRect ); + } +} + +void GtkSalFrame::EndSetClipRegion() +{ + if( m_pWindow && m_pRegion ) + gdk_window_shape_combine_region( gtk_widget_get_window(m_pWindow), m_pRegion, 0, 0 ); +} + +void GtkSalFrame::PositionByToolkit(const tools::Rectangle& rRect, FloatWinPopupFlags nFlags) +{ + if (ImplGetSVData()->maNWFData.mbCanDetermineWindowPosition) + return; + + m_aFloatRect = rRect; + m_nFloatFlags = nFlags; + m_bFloatPositioned = true; +} + +void GtkSalFrame::SetModal(bool bModal) +{ + if (!m_pWindow) + return; + gtk_window_set_modal(GTK_WINDOW(m_pWindow), bModal); +} + +bool GtkSalFrame::GetModal() const +{ + if (!m_pWindow) + return false; + return gtk_window_get_modal(GTK_WINDOW(m_pWindow)); +} + +gboolean GtkSalFrame::signalTooltipQuery(GtkWidget*, gint /*x*/, gint /*y*/, + gboolean /*keyboard_mode*/, GtkTooltip *tooltip, + gpointer frame) +{ + GtkSalFrame* pThis = static_cast<GtkSalFrame*>(frame); + if (pThis->m_aTooltip.isEmpty() || pThis->m_bTooltipBlocked) + return false; + gtk_tooltip_set_text(tooltip, + OUStringToOString(pThis->m_aTooltip, RTL_TEXTENCODING_UTF8).getStr()); + GdkRectangle aHelpArea; + aHelpArea.x = pThis->m_aHelpArea.Left(); + aHelpArea.y = pThis->m_aHelpArea.Top(); + aHelpArea.width = pThis->m_aHelpArea.GetWidth(); + aHelpArea.height = pThis->m_aHelpArea.GetHeight(); + if (AllSettings::GetLayoutRTL()) + aHelpArea.x = pThis->maGeometry.nWidth-aHelpArea.width-1-aHelpArea.x; + gtk_tooltip_set_tip_area(tooltip, &aHelpArea); + return true; +} + +bool GtkSalFrame::ShowTooltip(const OUString& rHelpText, const tools::Rectangle& rHelpArea) +{ + m_aTooltip = rHelpText; + m_aHelpArea = rHelpArea; + gtk_widget_trigger_tooltip_query(getMouseEventWidget()); + return true; +} + +void GtkSalFrame::BlockTooltip() +{ + m_bTooltipBlocked = true; +} + +void GtkSalFrame::UnblockTooltip() +{ + m_bTooltipBlocked = false; +} + +void GtkSalFrame::HideTooltip() +{ + m_aTooltip.clear(); + GtkWidget* pEventWidget = getMouseEventWidget(); + gtk_widget_trigger_tooltip_query(pEventWidget); +} + +namespace +{ + void set_pointing_to(GtkPopover *pPopOver, vcl::Window* pParent, const tools::Rectangle& rHelpArea, const SalFrameGeometry& rGeometry) + { + GdkRectangle aRect; + aRect.x = FloatingWindow::ImplConvertToAbsPos(pParent, rHelpArea).Left() - rGeometry.nX; + aRect.y = rHelpArea.Top(); + aRect.width = 1; + aRect.height = 1; + + GtkPositionType ePos = gtk_popover_get_position(pPopOver); + switch (ePos) + { + case GTK_POS_BOTTOM: + case GTK_POS_TOP: + aRect.width = rHelpArea.GetWidth(); + break; + case GTK_POS_RIGHT: + case GTK_POS_LEFT: + aRect.height = rHelpArea.GetHeight(); + break; + } + + gtk_popover_set_pointing_to(pPopOver, &aRect); + } +} + +void* GtkSalFrame::ShowPopover(const OUString& rHelpText, vcl::Window* pParent, const tools::Rectangle& rHelpArea, QuickHelpFlags nFlags) +{ + GtkWidget *pWidget = gtk_popover_new(getMouseEventWidget()); + OString sUTF = OUStringToOString(rHelpText, RTL_TEXTENCODING_UTF8); + GtkWidget *pLabel = gtk_label_new(sUTF.getStr()); + gtk_container_add(GTK_CONTAINER(pWidget), pLabel); + + if (nFlags & QuickHelpFlags::Top) + gtk_popover_set_position(GTK_POPOVER(pWidget), GTK_POS_BOTTOM); + else if (nFlags & QuickHelpFlags::Bottom) + gtk_popover_set_position(GTK_POPOVER(pWidget), GTK_POS_TOP); + else if (nFlags & QuickHelpFlags::Left) + gtk_popover_set_position(GTK_POPOVER(pWidget), GTK_POS_RIGHT); + else if (nFlags & QuickHelpFlags::Right) + gtk_popover_set_position(GTK_POPOVER(pWidget), GTK_POS_LEFT); + + set_pointing_to(GTK_POPOVER(pWidget), pParent, rHelpArea, maGeometry); + + gtk_popover_set_modal(GTK_POPOVER(pWidget), false); + + gtk_widget_show_all(pWidget); + + return pWidget; +} + +bool GtkSalFrame::UpdatePopover(void* nId, const OUString& rHelpText, vcl::Window* pParent, const tools::Rectangle& rHelpArea) +{ + GtkWidget *pWidget = static_cast<GtkWidget*>(nId); + + set_pointing_to(GTK_POPOVER(pWidget), pParent, rHelpArea, maGeometry); + + GtkWidget *pLabel = gtk_bin_get_child(GTK_BIN(pWidget)); + OString sUTF = OUStringToOString(rHelpText, RTL_TEXTENCODING_UTF8); + gtk_label_set_text(GTK_LABEL(pLabel), sUTF.getStr()); + + return true; +} + +bool GtkSalFrame::HidePopover(void* nId) +{ + GtkWidget *pWidget = static_cast<GtkWidget*>(nId); + gtk_widget_destroy(pWidget); + return true; +} + +void GtkSalFrame::addGrabLevel() +{ + if (m_nGrabLevel == 0) + gtk_grab_add(getMouseEventWidget()); + ++m_nGrabLevel; +} + +void GtkSalFrame::removeGrabLevel() +{ + if (m_nGrabLevel > 0) + { + --m_nGrabLevel; + if (m_nGrabLevel == 0) + gtk_grab_remove(getMouseEventWidget()); + } +} + +void GtkSalFrame::closePopup() +{ + if (!m_nFloats) + return; + ImplSVData* pSVData = ImplGetSVData(); + if (!pSVData->mpWinData->mpFirstFloat) + return; + if (pSVData->mpWinData->mpFirstFloat->ImplGetFrame() != this) + return; + pSVData->mpWinData->mpFirstFloat->EndPopupMode(FloatWinPopupEndFlags::Cancel | FloatWinPopupEndFlags::CloseAll); +} + +namespace +{ + //tdf#117981 translate embedded video window mouse events to parent coordinates + void translate_coords(GdkWindow* pSourceWindow, GtkWidget* pTargetWidget, int& rEventX, int& rEventY) + { + gpointer user_data=nullptr; + gdk_window_get_user_data(pSourceWindow, &user_data); + GtkWidget* pRealEventWidget = static_cast<GtkWidget*>(user_data); + if (pRealEventWidget) + { + gtk_widget_translate_coordinates(pRealEventWidget, pTargetWidget, rEventX, rEventY, &rEventX, &rEventY); + } + } +} + +void GtkSalFrame::GrabFocus() +{ + GtkWidget* pGrabWidget; + if (GTK_IS_EVENT_BOX(m_pWindow)) + pGrabWidget = GTK_WIDGET(m_pWindow); + else + pGrabWidget = GTK_WIDGET(m_pFixedContainer); + if (!gtk_widget_get_can_focus(pGrabWidget)) + gtk_widget_set_can_focus(pGrabWidget, true); + if (!gtk_widget_has_focus(pGrabWidget)) + gtk_widget_grab_focus(pGrabWidget); +} + +gboolean GtkSalFrame::signalButton(GtkWidget*, GdkEventButton* pEvent, gpointer frame) +{ + UpdateLastInputEventTime(pEvent->time); + + GtkSalFrame* pThis = static_cast<GtkSalFrame*>(frame); + GtkWidget* pEventWidget = pThis->getMouseEventWidget(); + bool bDifferentEventWindow = pEvent->window != gtk_widget_get_window(pEventWidget); + + if (pEvent->type == GDK_BUTTON_PRESS) + { + // tdf#120764 It isn't allowed under wayland to have two visible popups that share + // the same top level parent. The problem is that since gtk 3.24 tooltips are also + // implemented as popups, which means that we cannot show any popup if there is a + // visible tooltip. In fact, gtk will hide the tooltip by itself after this handler, + // in case of a button press event. But if we intend to show a popup on button press + // it will be too late, so just do it here: + pThis->HideTooltip(); + + // focus on click + if (!bDifferentEventWindow) + pThis->GrabFocus(); + } + + SalMouseEvent aEvent; + SalEvent nEventType = SalEvent::NONE; + switch( pEvent->type ) + { + case GDK_BUTTON_PRESS: + nEventType = SalEvent::MouseButtonDown; + break; + case GDK_BUTTON_RELEASE: + nEventType = SalEvent::MouseButtonUp; + break; + default: + return false; + } + switch( pEvent->button ) + { + case 1: aEvent.mnButton = MOUSE_LEFT; break; + case 2: aEvent.mnButton = MOUSE_MIDDLE; break; + case 3: aEvent.mnButton = MOUSE_RIGHT; break; + default: return false; + } + + vcl::DeletionListener aDel( pThis ); + + if (pThis->isFloatGrabWindow()) + { + //rhbz#1505379 if the window that got the event isn't our one, or there's none + //of our windows under the mouse then close this popup window + if (bDifferentEventWindow || + gdk_device_get_window_at_position(pEvent->device, nullptr, nullptr) == nullptr) + { + if (pEvent->type == GDK_BUTTON_PRESS) + pThis->closePopup(); + else if (pEvent->type == GDK_BUTTON_RELEASE) + return true; + } + } + + int nEventX = pEvent->x; + int nEventY = pEvent->y; + + if (bDifferentEventWindow) + translate_coords(pEvent->window, pEventWidget, nEventX, nEventY); + + if (!aDel.isDeleted()) + { + int frame_x = static_cast<int>(pEvent->x_root - nEventX); + int frame_y = static_cast<int>(pEvent->y_root - nEventY); + if (pThis->m_bGeometryIsProvisional || frame_x != pThis->maGeometry.nX || frame_y != pThis->maGeometry.nY) + { + pThis->m_bGeometryIsProvisional = false; + pThis->maGeometry.nX = frame_x; + pThis->maGeometry.nY = frame_y; + ImplSVData* pSVData = ImplGetSVData(); + if (pSVData->maNWFData.mbCanDetermineWindowPosition) + pThis->CallCallbackExc(SalEvent::Move, nullptr); + } + } + + if (!aDel.isDeleted()) + { + aEvent.mnTime = pEvent->time; + aEvent.mnX = static_cast<long>(pEvent->x_root) - pThis->maGeometry.nX; + aEvent.mnY = static_cast<long>(pEvent->y_root) - pThis->maGeometry.nY; + aEvent.mnCode = GetMouseModCode( pEvent->state ); + + if( AllSettings::GetLayoutRTL() ) + aEvent.mnX = pThis->maGeometry.nWidth-1-aEvent.mnX; + + pThis->CallCallbackExc( nEventType, &aEvent ); + } + + return true; +} + +void GtkSalFrame::LaunchAsyncScroll(GdkEvent const * pEvent) +{ + //if we don't match previous pending states, flush that queue now + if (!m_aPendingScrollEvents.empty() && pEvent->scroll.state != m_aPendingScrollEvents.back()->scroll.state) + { + m_aSmoothScrollIdle.Stop(); + m_aSmoothScrollIdle.Invoke(); + assert(m_aPendingScrollEvents.empty()); + } + //add scroll event to queue + m_aPendingScrollEvents.push_back(gdk_event_copy(pEvent)); + if (!m_aSmoothScrollIdle.IsActive()) + m_aSmoothScrollIdle.Start(); +} + +IMPL_LINK_NOARG(GtkSalFrame, AsyncScroll, Timer *, void) +{ + assert(!m_aPendingScrollEvents.empty()); + + SalWheelMouseEvent aEvent; + + GdkEvent* pEvent = m_aPendingScrollEvents.back(); + + aEvent.mnTime = pEvent->scroll.time; + aEvent.mnX = static_cast<sal_uLong>(pEvent->scroll.x); + // --- RTL --- (mirror mouse pos) + if (AllSettings::GetLayoutRTL()) + aEvent.mnX = maGeometry.nWidth - 1 - aEvent.mnX; + aEvent.mnY = static_cast<sal_uLong>(pEvent->scroll.y); + aEvent.mnCode = GetMouseModCode( pEvent->scroll.state ); + + double delta_x(0.0), delta_y(0.0); + for (auto pSubEvent : m_aPendingScrollEvents) + { + delta_x += pSubEvent->scroll.delta_x; + delta_y += pSubEvent->scroll.delta_y; + gdk_event_free(pSubEvent); + } + m_aPendingScrollEvents.clear(); + + // rhbz#1344042 "Traditionally" in gtk3 we tool a single up/down event as + // equating to 3 scroll lines and a delta of 120. So scale the delta here + // by 120 where a single mouse wheel click is an incoming delta_x of 1 + // and divide that by 40 to get the number of scroll lines + if (delta_x != 0.0) + { + aEvent.mnDelta = -delta_x * 120; + aEvent.mnNotchDelta = aEvent.mnDelta < 0 ? -1 : +1; + if (aEvent.mnDelta == 0) + aEvent.mnDelta = aEvent.mnNotchDelta; + aEvent.mbHorz = true; + aEvent.mnScrollLines = std::abs(aEvent.mnDelta) / 40.0; + CallCallbackExc(SalEvent::WheelMouse, &aEvent); + } + + if (delta_y != 0.0) + { + aEvent.mnDelta = -delta_y * 120; + aEvent.mnNotchDelta = aEvent.mnDelta < 0 ? -1 : +1; + if (aEvent.mnDelta == 0) + aEvent.mnDelta = aEvent.mnNotchDelta; + aEvent.mbHorz = false; + aEvent.mnScrollLines = std::abs(aEvent.mnDelta) / 40.0; + CallCallbackExc(SalEvent::WheelMouse, &aEvent); + } +} + +SalWheelMouseEvent GtkSalFrame::GetWheelEvent(GdkEventScroll& rEvent) +{ + SalWheelMouseEvent aEvent; + + aEvent.mnTime = rEvent.time; + aEvent.mnX = static_cast<sal_uLong>(rEvent.x); + aEvent.mnY = static_cast<sal_uLong>(rEvent.y); + aEvent.mnCode = GetMouseModCode(rEvent.state); + + switch (rEvent.direction) + { + case GDK_SCROLL_UP: + aEvent.mnDelta = 120; + aEvent.mnNotchDelta = 1; + aEvent.mnScrollLines = 3; + aEvent.mbHorz = false; + break; + + case GDK_SCROLL_DOWN: + aEvent.mnDelta = -120; + aEvent.mnNotchDelta = -1; + aEvent.mnScrollLines = 3; + aEvent.mbHorz = false; + break; + + case GDK_SCROLL_LEFT: + aEvent.mnDelta = 120; + aEvent.mnNotchDelta = 1; + aEvent.mnScrollLines = 3; + aEvent.mbHorz = true; + break; + + case GDK_SCROLL_RIGHT: + aEvent.mnDelta = -120; + aEvent.mnNotchDelta = -1; + aEvent.mnScrollLines = 3; + aEvent.mbHorz = true; + break; + default: + break; + } + + return aEvent; +} + +gboolean GtkSalFrame::signalScroll(GtkWidget*, GdkEvent* pInEvent, gpointer frame) +{ + GdkEventScroll& rEvent = pInEvent->scroll; + + UpdateLastInputEventTime(rEvent.time); + + GtkSalFrame* pThis = static_cast<GtkSalFrame*>(frame); + + if (rEvent.direction == GDK_SCROLL_SMOOTH) + { + pThis->LaunchAsyncScroll(pInEvent); + return true; + } + + //if we have smooth scrolling previous pending states, flush that queue now + if (!pThis->m_aPendingScrollEvents.empty()) + { + pThis->m_aSmoothScrollIdle.Stop(); + pThis->m_aSmoothScrollIdle.Invoke(); + assert(pThis->m_aPendingScrollEvents.empty()); + } + + SalWheelMouseEvent aEvent(GetWheelEvent(rEvent)); + + // --- RTL --- (mirror mouse pos) + if (AllSettings::GetLayoutRTL()) + aEvent.mnX = pThis->maGeometry.nWidth - 1 - aEvent.mnX; + + pThis->CallCallbackExc(SalEvent::WheelMouse, &aEvent); + + return true; +} + +void GtkSalFrame::gestureSwipe(GtkGestureSwipe* gesture, gdouble velocity_x, gdouble velocity_y, gpointer frame) +{ + gdouble x, y; + GdkEventSequence *sequence = gtk_gesture_single_get_current_sequence(GTK_GESTURE_SINGLE(gesture)); + //I feel I want the first point of the sequence, not the last point which + //the docs say this gives, but for the moment assume we start and end + //within the same vcl window + if (gtk_gesture_get_point(GTK_GESTURE(gesture), sequence, &x, &y)) + { + GtkSalFrame* pThis = static_cast<GtkSalFrame*>(frame); + + SalSwipeEvent aEvent; + aEvent.mnVelocityX = velocity_x; + aEvent.mnVelocityY = velocity_y; + aEvent.mnX = x; + aEvent.mnY = y; + + pThis->CallCallbackExc(SalEvent::Swipe, &aEvent); + } +} + +void GtkSalFrame::gestureLongPress(GtkGestureLongPress* gesture, gdouble x, gdouble y, gpointer frame) +{ + GdkEventSequence *sequence = gtk_gesture_single_get_current_sequence(GTK_GESTURE_SINGLE(gesture)); + if (gtk_gesture_get_point(GTK_GESTURE(gesture), sequence, &x, &y)) + { + GtkSalFrame* pThis = static_cast<GtkSalFrame*>(frame); + + SalLongPressEvent aEvent; + aEvent.mnX = x; + aEvent.mnY = y; + + pThis->CallCallbackExc(SalEvent::LongPress, &aEvent); + } +} + +gboolean GtkSalFrame::signalMotion( GtkWidget*, GdkEventMotion* pEvent, gpointer frame ) +{ + UpdateLastInputEventTime(pEvent->time); + + GtkSalFrame* pThis = static_cast<GtkSalFrame*>(frame); + GtkWidget* pEventWidget = pThis->getMouseEventWidget(); + bool bDifferentEventWindow = pEvent->window != gtk_widget_get_window(pEventWidget); + + //If a menu, e.g. font name dropdown, is open, then under wayland moving the + //mouse in the top left corner of the toplevel window in a + //0,0,float-width,float-height area generates motion events which are + //delivered to the dropdown + if (pThis->isFloatGrabWindow() && bDifferentEventWindow) + return true; + + vcl::DeletionListener aDel( pThis ); + + int nEventX = pEvent->x; + int nEventY = pEvent->y; + + if (bDifferentEventWindow) + translate_coords(pEvent->window, pEventWidget, nEventX, nEventY); + + int frame_x = static_cast<int>(pEvent->x_root - nEventX); + int frame_y = static_cast<int>(pEvent->y_root - nEventY); + + if (pThis->m_bGeometryIsProvisional || frame_x != pThis->maGeometry.nX || frame_y != pThis->maGeometry.nY) + { + pThis->m_bGeometryIsProvisional = false; + pThis->maGeometry.nX = frame_x; + pThis->maGeometry.nY = frame_y; + ImplSVData* pSVData = ImplGetSVData(); + if (pSVData->maNWFData.mbCanDetermineWindowPosition) + pThis->CallCallbackExc(SalEvent::Move, nullptr); + } + + if (!aDel.isDeleted()) + { + SalMouseEvent aEvent; + aEvent.mnTime = pEvent->time; + aEvent.mnX = static_cast<long>(pEvent->x_root) - pThis->maGeometry.nX; + aEvent.mnY = static_cast<long>(pEvent->y_root) - pThis->maGeometry.nY; + aEvent.mnCode = GetMouseModCode( pEvent->state ); + aEvent.mnButton = 0; + + if( AllSettings::GetLayoutRTL() ) + aEvent.mnX = pThis->maGeometry.nWidth-1-aEvent.mnX; + + pThis->CallCallbackExc( SalEvent::MouseMove, &aEvent ); + } + + if (!aDel.isDeleted()) + { + // ask for the next hint + gint x, y; + GdkModifierType mask; + gdk_window_get_pointer( gtk_widget_get_window(GTK_WIDGET(pThis->m_pWindow)), &x, &y, &mask ); + } + + return true; +} + +gboolean GtkSalFrame::signalCrossing( GtkWidget*, GdkEventCrossing* pEvent, gpointer frame ) +{ + UpdateLastInputEventTime(pEvent->time); + + GtkSalFrame* pThis = static_cast<GtkSalFrame*>(frame); + SalMouseEvent aEvent; + aEvent.mnTime = pEvent->time; + aEvent.mnX = static_cast<long>(pEvent->x_root) - pThis->maGeometry.nX; + aEvent.mnY = static_cast<long>(pEvent->y_root) - pThis->maGeometry.nY; + aEvent.mnCode = GetMouseModCode( pEvent->state ); + aEvent.mnButton = 0; + + if (AllSettings::GetLayoutRTL()) + aEvent.mnX = pThis->maGeometry.nWidth-1-aEvent.mnX; + + pThis->CallCallbackExc( (pEvent->type == GDK_ENTER_NOTIFY) ? SalEvent::MouseMove : SalEvent::MouseLeave, &aEvent ); + + return true; +} + +cairo_t* GtkSalFrame::getCairoContext() const +{ + cairo_t* cr = cairo_create(m_pSurface); + assert(cr); + return cr; +} + +void GtkSalFrame::damaged(sal_Int32 nExtentsX, sal_Int32 nExtentsY, + sal_Int32 nExtentsWidth, sal_Int32 nExtentsHeight) const +{ +#if OSL_DEBUG_LEVEL > 0 + if (dumpframes) + { + static int frame; + OString tmp("/tmp/frame" + OString::number(frame++) + ".png"); + cairo_t* cr = getCairoContext(); + cairo_surface_write_to_png(cairo_get_target(cr), tmp.getStr()); + cairo_destroy(cr); + } +#endif + + gtk_widget_queue_draw_area(GTK_WIDGET(m_pFixedContainer), + nExtentsX, nExtentsY, + nExtentsWidth, nExtentsHeight); +} + +// blit our backing cairo surface to the target cairo context +gboolean GtkSalFrame::signalDraw(GtkWidget*, cairo_t *cr, gpointer frame) +{ + GtkSalFrame* pThis = static_cast<GtkSalFrame*>(frame); + + cairo_set_source_surface(cr, pThis->m_pSurface, 0, 0); + cairo_paint(cr); + + return false; +} + +void GtkSalFrame::sizeAllocated(GtkWidget* pWidget, GdkRectangle *pAllocation, gpointer frame) +{ + GtkSalFrame* pThis = static_cast<GtkSalFrame*>(frame); + // ignore size-allocations that occur during configuring an embedded SalObject + if (pThis->m_bSalObjectSetPosSize) + return; + pThis->maGeometry.nWidth = pAllocation->width; + pThis->maGeometry.nHeight = pAllocation->height; + bool bRealized = gtk_widget_get_realized(pWidget); + if (bRealized) + pThis->AllocateFrame(); + pThis->CallCallbackExc( SalEvent::Resize, nullptr ); + if (bRealized) + pThis->TriggerPaintEvent(); +} + +namespace { + +void swapDirection(GdkGravity& gravity) +{ + if (gravity == GDK_GRAVITY_NORTH_WEST) + gravity = GDK_GRAVITY_NORTH_EAST; + else if (gravity == GDK_GRAVITY_NORTH_EAST) + gravity = GDK_GRAVITY_NORTH_WEST; + else if (gravity == GDK_GRAVITY_SOUTH_WEST) + gravity = GDK_GRAVITY_SOUTH_EAST; + else if (gravity == GDK_GRAVITY_SOUTH_EAST) + gravity = GDK_GRAVITY_SOUTH_WEST; +} + +} + +void GtkSalFrame::signalRealize(GtkWidget*, gpointer frame) +{ + GtkSalFrame* pThis = static_cast<GtkSalFrame*>(frame); + pThis->AllocateFrame(); + if (pThis->m_bSalObjectSetPosSize) + return; + pThis->TriggerPaintEvent(); + + if (!pThis->m_bFloatPositioned) + return; + + static auto window_move_to_rect = reinterpret_cast<void (*) (GdkWindow*, const GdkRectangle*, GdkGravity, + GdkGravity, GdkAnchorHints, gint, gint)>( + dlsym(nullptr, "gdk_window_move_to_rect")); + if (window_move_to_rect) + { + GdkGravity rect_anchor = GDK_GRAVITY_SOUTH_WEST, menu_anchor = GDK_GRAVITY_NORTH_WEST; + + if (pThis->m_nFloatFlags & FloatWinPopupFlags::Left) + { + rect_anchor = GDK_GRAVITY_NORTH_WEST; + menu_anchor = GDK_GRAVITY_NORTH_EAST; + } + else if (pThis->m_nFloatFlags & FloatWinPopupFlags::Up) + { + rect_anchor = GDK_GRAVITY_NORTH_WEST; + menu_anchor = GDK_GRAVITY_SOUTH_WEST; + } + else if (pThis->m_nFloatFlags & FloatWinPopupFlags::Right) + { + rect_anchor = GDK_GRAVITY_NORTH_EAST; + } + + VclPtr<vcl::Window> pVclParent = pThis->GetWindow()->GetParent(); + if (pVclParent->HasMirroredGraphics() && pVclParent->IsRTLEnabled()) + { + swapDirection(rect_anchor); + swapDirection(menu_anchor); + } + + tools::Rectangle aFloatRect = FloatingWindow::ImplConvertToAbsPos(pVclParent, pThis->m_aFloatRect); + if (gdk_window_get_window_type(gtk_widget_get_window(pThis->m_pParent->m_pWindow)) != GDK_WINDOW_TOPLEVEL) + aFloatRect.Move(-pThis->m_pParent->maGeometry.nX, -pThis->m_pParent->maGeometry.nY); + + GdkRectangle rect {static_cast<int>(aFloatRect.Left()), static_cast<int>(aFloatRect.Top()), + static_cast<int>(aFloatRect.GetWidth()), static_cast<int>(aFloatRect.GetHeight())}; + + GdkWindow* gdkWindow = gtk_widget_get_window(pThis->m_pWindow); + window_move_to_rect(gdkWindow, &rect, rect_anchor, menu_anchor, static_cast<GdkAnchorHints>(GDK_ANCHOR_FLIP | GDK_ANCHOR_SLIDE), 0, 0); + } +} + +gboolean GtkSalFrame::signalConfigure(GtkWidget*, GdkEventConfigure* pEvent, gpointer frame) +{ + GtkSalFrame* pThis = static_cast<GtkSalFrame*>(frame); + + bool bMoved = false; + int x = pEvent->x, y = pEvent->y; + + /* #i31785# claims we cannot trust the x,y members of the event; + * they are e.g. not set correctly on maximize/demaximize; + * yet the gdkdisplay-x11.c code handling configure_events has + * done this XTranslateCoordinates work since the day ~zero. + */ + if (pThis->m_bGeometryIsProvisional || x != pThis->maGeometry.nX || y != pThis->maGeometry.nY ) + { + bMoved = true; + pThis->m_bGeometryIsProvisional = false; + pThis->maGeometry.nX = x; + pThis->maGeometry.nY = y; + } + + // update decoration hints + GdkRectangle aRect; + gdk_window_get_frame_extents( gtk_widget_get_window(GTK_WIDGET(pThis->m_pWindow)), &aRect ); + pThis->maGeometry.nTopDecoration = y - aRect.y; + pThis->maGeometry.nBottomDecoration = aRect.y + aRect.height - y - pEvent->height; + pThis->maGeometry.nLeftDecoration = x - aRect.x; + pThis->maGeometry.nRightDecoration = aRect.x + aRect.width - x - pEvent->width; + pThis->updateScreenNumber(); + + if (bMoved) + { + ImplSVData* pSVData = ImplGetSVData(); + if (pSVData->maNWFData.mbCanDetermineWindowPosition) + pThis->CallCallbackExc(SalEvent::Move, nullptr); + } + + return false; +} + +void GtkSalFrame::TriggerPaintEvent() +{ + //Under gtk2 we can basically paint directly into the XWindow and on + //additional "expose-event" events we can re-render the missing pieces + // + //Under gtk3 we have to keep our own buffer up to date and flush it into + //the given cairo context on "draw". So we emit a paint event on + //opportune resize trigger events to initially fill our backbuffer and then + //keep it up to date with our direct paints and tell gtk those regions + //have changed and then blit them into the provided cairo context when + //we get the "draw" + // + //The other alternative was to always paint everything on "draw", but + //that duplicates the amount of drawing and is hideously slow + SAL_INFO("vcl.gtk3", "force painting" << 0 << "," << 0 << " " << maGeometry.nWidth << "x" << maGeometry.nHeight); + SalPaintEvent aPaintEvt(0, 0, maGeometry.nWidth, maGeometry.nHeight, true); + CallCallbackExc(SalEvent::Paint, &aPaintEvt); + gtk_widget_queue_draw(GTK_WIDGET(m_pFixedContainer)); +} + +gboolean GtkSalFrame::signalFocus( GtkWidget*, GdkEventFocus* pEvent, gpointer frame ) +{ + GtkSalFrame* pThis = static_cast<GtkSalFrame*>(frame); + + SalGenericInstance *pSalInstance = + static_cast< SalGenericInstance* >(GetSalData()->m_pInstance); + + // check if printers have changed (analogous to salframe focus handler) + pSalInstance->updatePrinterUpdate(); + + if( !pEvent->in ) + pThis->m_nKeyModifiers = ModKeyFlags::NONE; + + if( pThis->m_pIMHandler ) + pThis->m_pIMHandler->focusChanged( pEvent->in != 0 ); + + // ask for changed printers like generic implementation + if( pEvent->in && pSalInstance->isPrinterInit() ) + pSalInstance->updatePrinterUpdate(); + + // FIXME: find out who the hell steals the focus from our frame + // while we have the pointer grabbed, this should not come from + // the window manager. Is this an event that was still queued ? + // The focus does not seem to get set inside our process + + // in the meantime do not propagate focus get/lose if floats are open + if( m_nFloats == 0 ) + { + GtkWidget* pGrabWidget; + if (GTK_IS_EVENT_BOX(pThis->m_pWindow)) + pGrabWidget = GTK_WIDGET(pThis->m_pWindow); + else + pGrabWidget = GTK_WIDGET(pThis->m_pFixedContainer); + bool bHasFocus = gtk_widget_has_focus(pGrabWidget); + pThis->CallCallbackExc(bHasFocus ? SalEvent::GetFocus : SalEvent::LoseFocus, nullptr); + } + + return false; +} + +void GtkSalFrame::signalSetFocus(GtkWindow*, GtkWidget* pWidget, gpointer frame) +{ + // do not propagate focus get/lose if floats are open + if (m_nFloats) + return; + // change of focus between native widgets within the toplevel + GtkSalFrame* pThis = static_cast<GtkSalFrame*>(frame); + // tdf#129634 ignore floating toolbars + if (pThis->m_nStyle & SalFrameStyleFlags::OWNERDRAWDECORATION) + return; + + // tdf#129634 interpret losing focus as focus passing explicitly to another widget + bool bLoseFocus = pWidget && pWidget != GTK_WIDGET(pThis->m_pFixedContainer); + pThis->CallCallbackExc(bLoseFocus ? SalEvent::LoseFocus : SalEvent::GetFocus, nullptr); + gtk_widget_set_can_focus(GTK_WIDGET(pThis->m_pFixedContainer), !bLoseFocus); +} + +gboolean GtkSalFrame::signalMap(GtkWidget *, GdkEvent*, gpointer frame) +{ + GtkSalFrame* pThis = static_cast<GtkSalFrame*>(frame); + + if (pThis->m_bIconSetWhileUnmapped) + pThis->SetIcon(gtk_window_get_icon_name(GTK_WINDOW(pThis->m_pWindow))); + + pThis->CallCallbackExc( SalEvent::Resize, nullptr ); + pThis->TriggerPaintEvent(); + + return false; +} + +gboolean GtkSalFrame::signalUnmap( GtkWidget*, GdkEvent*, gpointer frame ) +{ + GtkSalFrame* pThis = static_cast<GtkSalFrame*>(frame); + + pThis->CallCallbackExc( SalEvent::Resize, nullptr ); + + if (pThis->m_bFloatPositioned) + { + // Unrealize is needed for cases where we reuse the same popup + // (e.g. the font name control), making the realize signal fire + // again on next show. + gtk_widget_unrealize(pThis->m_pWindow); + pThis->m_bFloatPositioned = false; + } + + return false; +} + +gboolean GtkSalFrame::signalKey(GtkWidget* pWidget, GdkEventKey* pEvent, gpointer frame) +{ + UpdateLastInputEventTime(pEvent->time); + + GtkSalFrame* pThis = static_cast<GtkSalFrame*>(frame); + + bool bFocusInAnotherGtkWidget = false; + + if (GTK_IS_WINDOW(pThis->m_pWindow)) + { + GtkWidget* pFocusWindow = gtk_window_get_focus(GTK_WINDOW(pThis->m_pWindow)); + bFocusInAnotherGtkWidget = pFocusWindow && pFocusWindow != GTK_WIDGET(pThis->m_pFixedContainer); + if (bFocusInAnotherGtkWidget) + { + gpointer pClass = g_type_class_ref(GTK_TYPE_WINDOW); + GtkWidgetClass* pWindowClass = GTK_WIDGET_CLASS(pClass); + // if the focus is not in our main widget, see if there is a handler + // for this key stroke in GtkWindow first + bool bHandled = pEvent->type == GDK_KEY_PRESS + ? pWindowClass->key_press_event(pThis->m_pWindow, pEvent) + : pWindowClass->key_release_event(pThis->m_pWindow, pEvent); + g_type_class_unref(pClass); + if (bHandled) + return true; + } + } + + if (pThis->isFloatGrabWindow()) + return signalKey(pWidget, pEvent, pThis->m_pParent); + + vcl::DeletionListener aDel( pThis ); + + if (!bFocusInAnotherGtkWidget && pThis->m_pIMHandler && pThis->m_pIMHandler->handleKeyEvent(pEvent)) + return true; + + bool bStopProcessingKey = false; + + // handle modifiers + if( pEvent->keyval == GDK_KEY_Shift_L || pEvent->keyval == GDK_KEY_Shift_R || + pEvent->keyval == GDK_KEY_Control_L || pEvent->keyval == GDK_KEY_Control_R || + pEvent->keyval == GDK_KEY_Alt_L || pEvent->keyval == GDK_KEY_Alt_R || + pEvent->keyval == GDK_KEY_Meta_L || pEvent->keyval == GDK_KEY_Meta_R || + pEvent->keyval == GDK_KEY_Super_L || pEvent->keyval == GDK_KEY_Super_R ) + { + sal_uInt16 nModCode = GetKeyModCode( pEvent->state ); + ModKeyFlags nExtModMask = ModKeyFlags::NONE; + sal_uInt16 nModMask = 0; + // pressing just the ctrl key leads to a keysym of XK_Control but + // the event state does not contain ControlMask. In the release + // event it's the other way round: it does contain the Control mask. + // The modifier mode therefore has to be adapted manually. + switch( pEvent->keyval ) + { + case GDK_KEY_Control_L: + nExtModMask = ModKeyFlags::LeftMod1; + nModMask = KEY_MOD1; + break; + case GDK_KEY_Control_R: + nExtModMask = ModKeyFlags::RightMod1; + nModMask = KEY_MOD1; + break; + case GDK_KEY_Alt_L: + nExtModMask = ModKeyFlags::LeftMod2; + nModMask = KEY_MOD2; + break; + case GDK_KEY_Alt_R: + nExtModMask = ModKeyFlags::RightMod2; + nModMask = KEY_MOD2; + break; + case GDK_KEY_Shift_L: + nExtModMask = ModKeyFlags::LeftShift; + nModMask = KEY_SHIFT; + break; + case GDK_KEY_Shift_R: + nExtModMask = ModKeyFlags::RightShift; + nModMask = KEY_SHIFT; + break; + // Map Meta/Super to MOD3 modifier on all Unix systems + // except macOS + case GDK_KEY_Meta_L: + case GDK_KEY_Super_L: + nExtModMask = ModKeyFlags::LeftMod3; + nModMask = KEY_MOD3; + break; + case GDK_KEY_Meta_R: + case GDK_KEY_Super_R: + nExtModMask = ModKeyFlags::RightMod3; + nModMask = KEY_MOD3; + break; + } + + SalKeyModEvent aModEvt; + aModEvt.mbDown = pEvent->type == GDK_KEY_PRESS; + + if( pEvent->type == GDK_KEY_RELEASE ) + { + aModEvt.mnModKeyCode = pThis->m_nKeyModifiers; + aModEvt.mnCode = nModCode & ~nModMask; + pThis->m_nKeyModifiers &= ~nExtModMask; + } + else + { + aModEvt.mnCode = nModCode | nModMask; + pThis->m_nKeyModifiers |= nExtModMask; + aModEvt.mnModKeyCode = pThis->m_nKeyModifiers; + } + + pThis->CallCallbackExc( SalEvent::KeyModChange, &aModEvt ); + } + else + { + bStopProcessingKey = pThis->doKeyCallback(pEvent->state, + pEvent->keyval, + pEvent->hardware_keycode, + pEvent->group, + sal_Unicode(gdk_keyval_to_unicode( pEvent->keyval )), + (pEvent->type == GDK_KEY_PRESS), + false); + if( ! aDel.isDeleted() ) + pThis->m_nKeyModifiers = ModKeyFlags::NONE; + } + + if (!bFocusInAnotherGtkWidget && !aDel.isDeleted() && pThis->m_pIMHandler) + pThis->m_pIMHandler->updateIMSpotLocation(); + + return bStopProcessingKey; +} + +gboolean GtkSalFrame::signalDelete( GtkWidget*, GdkEvent*, gpointer frame ) +{ + GtkSalFrame* pThis = static_cast<GtkSalFrame*>(frame); + pThis->CallCallbackExc( SalEvent::Close, nullptr ); + return true; +} + +void GtkSalFrame::signalStyleUpdated(GtkWidget*, gpointer frame) +{ + GtkSalFrame* pThis = static_cast<GtkSalFrame*>(frame); + + // note: settings changed for multiple frames is avoided in winproc.cxx ImplHandleSettings + GtkSalFrame::getDisplay()->SendInternalEvent( pThis, nullptr, SalEvent::SettingsChanged ); + + // fire off font-changed when the system cairo font hints change + GtkInstance *pInstance = static_cast<GtkInstance*>(GetSalData()->m_pInstance); + const cairo_font_options_t* pLastCairoFontOptions = pInstance->GetLastSeenCairoFontOptions(); + const cairo_font_options_t* pCurrentCairoFontOptions = gdk_screen_get_font_options(gdk_screen_get_default()); + bool bFontSettingsChanged = true; + if (pLastCairoFontOptions && pCurrentCairoFontOptions) + bFontSettingsChanged = !cairo_font_options_equal(pLastCairoFontOptions, pCurrentCairoFontOptions); + else if (!pLastCairoFontOptions && !pCurrentCairoFontOptions) + bFontSettingsChanged = false; + if (bFontSettingsChanged) + { + pInstance->ResetLastSeenCairoFontOptions(pCurrentCairoFontOptions); + GtkSalFrame::getDisplay()->SendInternalEvent( pThis, nullptr, SalEvent::FontChanged ); + } +} + +gboolean GtkSalFrame::signalWindowState( GtkWidget*, GdkEvent* pEvent, gpointer frame ) +{ + GtkSalFrame* pThis = static_cast<GtkSalFrame*>(frame); + if( (pThis->m_nState & GDK_WINDOW_STATE_ICONIFIED) != (pEvent->window_state.new_window_state & GDK_WINDOW_STATE_ICONIFIED ) ) + { + GtkSalFrame::getDisplay()->SendInternalEvent( pThis, nullptr, SalEvent::Resize ); + pThis->TriggerPaintEvent(); + } + + if ((pEvent->window_state.new_window_state & GDK_WINDOW_STATE_MAXIMIZED) && + !(pThis->m_nState & GDK_WINDOW_STATE_MAXIMIZED)) + { + pThis->m_aRestorePosSize = GetPosAndSize(GTK_WINDOW(pThis->m_pWindow)); + } + + if ((pEvent->window_state.new_window_state & GDK_WINDOW_STATE_WITHDRAWN) && + !(pThis->m_nState & GDK_WINDOW_STATE_WITHDRAWN)) + { + if (pThis->isFloatGrabWindow()) + pThis->closePopup(); + } + + pThis->m_nState = pEvent->window_state.new_window_state; + +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO_IF((pEvent->window_state.changed_mask & + GDK_WINDOW_STATE_FULLSCREEN), + "vcl.gtk3", "window " + << pThis + << " " + << ((pEvent->window_state.new_window_state & + GDK_WINDOW_STATE_FULLSCREEN) ? + "enters" : + "leaves") + << " full screen state."); +#endif + + return false; +} + +gboolean GtkSalFrame::signalVisibility( GtkWidget*, GdkEventVisibility* /*pEvent*/, gpointer /*frame*/ ) +{ + return true; +} + +namespace +{ + GdkDragAction VclToGdk(sal_Int8 dragOperation) + { + GdkDragAction eRet(static_cast<GdkDragAction>(0)); + if (dragOperation & css::datatransfer::dnd::DNDConstants::ACTION_COPY) + eRet = static_cast<GdkDragAction>(eRet | GDK_ACTION_COPY); + if (dragOperation & css::datatransfer::dnd::DNDConstants::ACTION_MOVE) + eRet = static_cast<GdkDragAction>(eRet | GDK_ACTION_MOVE); + if (dragOperation & css::datatransfer::dnd::DNDConstants::ACTION_LINK) + eRet = static_cast<GdkDragAction>(eRet | GDK_ACTION_LINK); + return eRet; + } + + sal_Int8 GdkToVcl(GdkDragAction dragOperation) + { + sal_Int8 nRet(0); + if (dragOperation & GDK_ACTION_COPY) + nRet |= css::datatransfer::dnd::DNDConstants::ACTION_COPY; + if (dragOperation & GDK_ACTION_MOVE) + nRet |= css::datatransfer::dnd::DNDConstants::ACTION_MOVE; + if (dragOperation & GDK_ACTION_LINK) + nRet |= css::datatransfer::dnd::DNDConstants::ACTION_LINK; + return nRet; + } +} + +namespace +{ + GdkDragAction getPreferredDragAction(sal_Int8 dragOperation) + { + GdkDragAction eAct(static_cast<GdkDragAction>(0)); + + if (dragOperation & css::datatransfer::dnd::DNDConstants::ACTION_MOVE) + eAct = GDK_ACTION_MOVE; + else if (dragOperation & css::datatransfer::dnd::DNDConstants::ACTION_COPY) + eAct = GDK_ACTION_COPY; + else if (dragOperation & css::datatransfer::dnd::DNDConstants::ACTION_LINK) + eAct = GDK_ACTION_LINK; + + return eAct; + } +} + +static bool g_DropSuccessSet = false; +static bool g_DropSuccess = false; + +namespace { + +class GtkDropTargetDropContext : public cppu::WeakImplHelper<css::datatransfer::dnd::XDropTargetDropContext> +{ + GdkDragContext *m_pContext; + guint m_nTime; +public: + GtkDropTargetDropContext(GdkDragContext *pContext, guint nTime) + : m_pContext(pContext) + , m_nTime(nTime) + { + } + + // XDropTargetDropContext + virtual void SAL_CALL acceptDrop(sal_Int8 dragOperation) override + { + gdk_drag_status(m_pContext, getPreferredDragAction(dragOperation), m_nTime); + } + + virtual void SAL_CALL rejectDrop() override + { + gdk_drag_status(m_pContext, static_cast<GdkDragAction>(0), m_nTime); + } + + virtual void SAL_CALL dropComplete(sal_Bool bSuccess) override + { + gtk_drag_finish(m_pContext, bSuccess, false, m_nTime); + if (GtkDragSource::g_ActiveDragSource) + { + g_DropSuccessSet = true; + g_DropSuccess = bSuccess; + } + } +}; + +} + +class GtkDnDTransferable : public GtkTransferable +{ + GdkDragContext *m_pContext; + guint m_nTime; + GtkWidget *m_pWidget; + GtkDropTarget* m_pDropTarget; + GMainLoop *m_pLoop; + GtkSelectionData *m_pData; +public: + GtkDnDTransferable(GdkDragContext *pContext, guint nTime, GtkWidget *pWidget, GtkDropTarget *pDropTarget) + : m_pContext(pContext) + , m_nTime(nTime) + , m_pWidget(pWidget) + , m_pDropTarget(pDropTarget) + , m_pLoop(nullptr) + , m_pData(nullptr) + { + } + + virtual css::uno::Any SAL_CALL getTransferData(const css::datatransfer::DataFlavor& rFlavor) override + { + css::datatransfer::DataFlavor aFlavor(rFlavor); + if (aFlavor.MimeType == "text/plain;charset=utf-16") + aFlavor.MimeType = "text/plain;charset=utf-8"; + + auto it = m_aMimeTypeToAtom.find(aFlavor.MimeType); + if (it == m_aMimeTypeToAtom.end()) + return css::uno::Any(); + + /* like gtk_clipboard_wait_for_contents run a sub loop + * waiting for drag-data-received triggered from + * gtk_drag_get_data + */ + { + m_pLoop = g_main_loop_new(nullptr, true); + m_pDropTarget->SetFormatConversionRequest(this); + + gtk_drag_get_data(m_pWidget, m_pContext, it->second, m_nTime); + + if (g_main_loop_is_running(m_pLoop)) + { + gdk_threads_leave(); + g_main_loop_run(m_pLoop); + gdk_threads_enter(); + } + + g_main_loop_unref(m_pLoop); + m_pLoop = nullptr; + m_pDropTarget->SetFormatConversionRequest(nullptr); + } + + css::uno::Any aRet; + + if (aFlavor.MimeType == "text/plain;charset=utf-8") + { + OUString aStr; + gchar *pText = reinterpret_cast<gchar*>(gtk_selection_data_get_text(m_pData)); + if (pText) + aStr = OUString(pText, rtl_str_getLength(pText), RTL_TEXTENCODING_UTF8); + g_free(pText); + aRet <<= aStr.replaceAll("\r\n", "\n"); + } + else + { + gint length(0); + const guchar *rawdata = gtk_selection_data_get_data_with_length(m_pData, + &length); + // seen here was rawhide == nullptr and length set to -1 + if (rawdata) + { + css::uno::Sequence<sal_Int8> aSeq(reinterpret_cast<const sal_Int8*>(rawdata), length); + aRet <<= aSeq; + } + } + + gtk_selection_data_free(m_pData); + + return aRet; + } + + virtual std::vector<css::datatransfer::DataFlavor> getTransferDataFlavorsAsVector() override + { + std::vector<GdkAtom> targets; + for (GList* l = gdk_drag_context_list_targets(m_pContext); l; l = l->next) + targets.push_back(static_cast<GdkAtom>(l->data)); + return GtkTransferable::getTransferDataFlavorsAsVector(targets.data(), targets.size()); + } + + void LoopEnd(GtkSelectionData *pData) + { + m_pData = pData; + g_main_loop_quit(m_pLoop); + } +}; + +// For LibreOffice internal D&D we provide the Transferable without Gtk +// intermediaries as a shortcut, see tdf#100097 for how dbaccess depends on this +GtkDragSource* GtkDragSource::g_ActiveDragSource; + +gboolean GtkSalFrame::signalDragDrop(GtkWidget* pWidget, GdkDragContext* context, gint x, gint y, guint time, gpointer frame) +{ + GtkSalFrame* pThis = static_cast<GtkSalFrame*>(frame); + if (!pThis->m_pDropTarget) + return false; + return pThis->m_pDropTarget->signalDragDrop(pWidget, context, x, y, time); +} + +gboolean GtkDropTarget::signalDragDrop(GtkWidget* pWidget, GdkDragContext* context, gint x, gint y, guint time) +{ + // remove the deferred dragExit, as we'll do a drop +#ifndef NDEBUG + bool res = +#endif + g_idle_remove_by_data(this); + assert(res); + + css::datatransfer::dnd::DropTargetDropEvent aEvent; + aEvent.Source = static_cast<css::datatransfer::dnd::XDropTarget*>(this); + aEvent.Context = new GtkDropTargetDropContext(context, time); + aEvent.LocationX = x; + aEvent.LocationY = y; + aEvent.DropAction = GdkToVcl(gdk_drag_context_get_selected_action(context)); + // ACTION_DEFAULT is documented as... + // 'This means the user did not press any key during the Drag and Drop operation + // and the action that was combined with ACTION_DEFAULT is the system default action' + // in tdf#107031 writer won't insert a link when a heading is dragged from the + // navigator unless this is set. Its unclear really what ACTION_DEFAULT means, + // there is a deprecated 'GDK_ACTION_DEFAULT Means nothing, and should not be used' + // possible equivalent in gtk. + // So (tdf#109227) set ACTION_DEFAULT if no modifier key is held down + GdkModifierType mask; + gdk_window_get_pointer(gtk_widget_get_window(pWidget), nullptr, nullptr, &mask); + if (!(mask & (GDK_CONTROL_MASK | GDK_SHIFT_MASK))) + aEvent.DropAction |= css::datatransfer::dnd::DNDConstants::ACTION_DEFAULT; + aEvent.SourceActions = GdkToVcl(gdk_drag_context_get_actions(context)); + css::uno::Reference<css::datatransfer::XTransferable> xTransferable; + // For LibreOffice internal D&D we provide the Transferable without Gtk + // intermediaries as a shortcut, see tdf#100097 for how dbaccess depends on this + if (GtkDragSource::g_ActiveDragSource) + xTransferable = GtkDragSource::g_ActiveDragSource->GetTransferrable(); + else + xTransferable = new GtkDnDTransferable(context, time, pWidget, this); + aEvent.Transferable = xTransferable; + + fire_drop(aEvent); + + return true; +} + +namespace { + +class GtkDropTargetDragContext : public cppu::WeakImplHelper<css::datatransfer::dnd::XDropTargetDragContext> +{ + GdkDragContext *m_pContext; + guint m_nTime; +public: + GtkDropTargetDragContext(GdkDragContext *pContext, guint nTime) + : m_pContext(pContext) + , m_nTime(nTime) + { + } + + virtual void SAL_CALL acceptDrag(sal_Int8 dragOperation) override + { + gdk_drag_status(m_pContext, getPreferredDragAction(dragOperation), m_nTime); + } + + virtual void SAL_CALL rejectDrag() override + { + gdk_drag_status(m_pContext, static_cast<GdkDragAction>(0), m_nTime); + } +}; + +} + +void GtkSalFrame::signalDragDropReceived(GtkWidget* pWidget, GdkDragContext* context, gint x, gint y, GtkSelectionData* data, guint ttype, guint time, gpointer frame) +{ + GtkSalFrame* pThis = static_cast<GtkSalFrame*>(frame); + if (!pThis->m_pDropTarget) + return; + pThis->m_pDropTarget->signalDragDropReceived(pWidget, context, x, y, data, ttype, time); +} + +void GtkDropTarget::signalDragDropReceived(GtkWidget* /*pWidget*/, GdkDragContext * /*context*/, gint /*x*/, gint /*y*/, GtkSelectionData* data, guint /*ttype*/, guint /*time*/) +{ + /* + * If we get a drop, then we will call like gtk_clipboard_wait_for_contents + * with a loop inside a loop to get the right format, so if this is the + * case return to the outer loop here with a copy of the desired data + * + * don't look at me like that. + */ + if (!m_pFormatConversionRequest) + return; + + m_pFormatConversionRequest->LoopEnd(gtk_selection_data_copy(data)); +} + +gboolean GtkSalFrame::signalDragMotion(GtkWidget *pWidget, GdkDragContext *context, gint x, gint y, guint time, gpointer frame) +{ + GtkSalFrame* pThis = static_cast<GtkSalFrame*>(frame); + if (!pThis->m_pDropTarget) + return false; + return pThis->m_pDropTarget->signalDragMotion(pWidget, context, x, y, time); +} + + +gboolean GtkDropTarget::signalDragMotion(GtkWidget *pWidget, GdkDragContext *context, gint x, gint y, guint time) +{ + if (!m_bInDrag) + gtk_drag_highlight(pWidget); + + css::datatransfer::dnd::DropTargetDragEnterEvent aEvent; + aEvent.Source = static_cast<css::datatransfer::dnd::XDropTarget*>(this); + GtkDropTargetDragContext* pContext = new GtkDropTargetDragContext(context, time); + //preliminary accept the Drag and select the preferred action, the fire_* will + //inform the original caller of our choice and the callsite can decide + //to overrule this choice. i.e. typically here we default to ACTION_MOVE + sal_Int8 nSourceActions = GdkToVcl(gdk_drag_context_get_actions(context)); + GdkModifierType mask; + gdk_window_get_pointer(gtk_widget_get_window(pWidget), nullptr, nullptr, &mask); + + // tdf#124411 default to move if drag originates within LO itself, default + // to copy if it comes from outside, this is similar to srcAndDestEqual + // in macosx DropTarget::determineDropAction equivalent + sal_Int8 nNewDropAction = GtkDragSource::g_ActiveDragSource ? + css::datatransfer::dnd::DNDConstants::ACTION_MOVE : + css::datatransfer::dnd::DNDConstants::ACTION_COPY; + + // tdf#109227 if a modifier is held down, default to the matching + // action for that modifier combo, otherwise pick the preferred + // default from the possible source actions + if ((mask & GDK_SHIFT_MASK) && !(mask & GDK_CONTROL_MASK)) + nNewDropAction = css::datatransfer::dnd::DNDConstants::ACTION_MOVE; + else if ((mask & GDK_CONTROL_MASK) && !(mask & GDK_SHIFT_MASK)) + nNewDropAction = css::datatransfer::dnd::DNDConstants::ACTION_COPY; + else if ((mask & GDK_SHIFT_MASK) && (mask & GDK_CONTROL_MASK) ) + nNewDropAction = css::datatransfer::dnd::DNDConstants::ACTION_LINK; + nNewDropAction &= nSourceActions; + + GdkDragAction eAction; + if (!(mask & (GDK_CONTROL_MASK | GDK_SHIFT_MASK)) && !nNewDropAction) + eAction = getPreferredDragAction(nSourceActions); + else + eAction = getPreferredDragAction(nNewDropAction); + + gdk_drag_status(context, eAction, time); + aEvent.Context = pContext; + aEvent.LocationX = x; + aEvent.LocationY = y; + //under wayland at least, the action selected by gdk_drag_status on the + //context is not immediately available via gdk_drag_context_get_selected_action + //so here we set the DropAction from what we selected on the context, not + //what the context says is selected + aEvent.DropAction = GdkToVcl(eAction); + aEvent.SourceActions = nSourceActions; + + if (!m_bInDrag) + { + css::uno::Reference<css::datatransfer::XTransferable> xTransferable; + // For LibreOffice internal D&D we provide the Transferable without Gtk + // intermediaries as a shortcut, see tdf#100097 for how dbaccess depends on this + if (GtkDragSource::g_ActiveDragSource) + xTransferable = GtkDragSource::g_ActiveDragSource->GetTransferrable(); + else + xTransferable = new GtkDnDTransferable(context, time, pWidget, this); + css::uno::Sequence<css::datatransfer::DataFlavor> aFormats = xTransferable->getTransferDataFlavors(); + aEvent.SupportedDataFlavors = aFormats; + fire_dragEnter(aEvent); + m_bInDrag = true; + } + else + { + fire_dragOver(aEvent); + } + + return true; +} + +void GtkSalFrame::signalDragLeave(GtkWidget *pWidget, GdkDragContext *context, guint time, gpointer frame) +{ + GtkSalFrame* pThis = static_cast<GtkSalFrame*>(frame); + if (!pThis->m_pDropTarget) + return; + pThis->m_pDropTarget->signalDragLeave(pWidget, context, time); +} + +static gboolean lcl_deferred_dragExit(gpointer user_data) +{ + GtkDropTarget* pThis = static_cast<GtkDropTarget*>(user_data); + css::datatransfer::dnd::DropTargetEvent aEvent; + aEvent.Source = static_cast<css::datatransfer::dnd::XDropTarget*>(pThis); + pThis->fire_dragExit(aEvent); + return FALSE; +} + +void GtkDropTarget::signalDragLeave(GtkWidget* pWidget, GdkDragContext* /*context*/, guint /*time*/) +{ + m_bInDrag = false; + gtk_drag_unhighlight(pWidget); + // defer fire_dragExit, since gtk also sends a drag-leave before the drop, while + // LO expect to either handle the drop or the exit... at least in Writer. + // but since we don't know there will be a drop following the leave, defer the + // exit handling to an idle. + g_idle_add(lcl_deferred_dragExit, this); +} + +void GtkSalFrame::signalDestroy( GtkWidget* pObj, gpointer frame ) +{ + GtkSalFrame* pThis = static_cast<GtkSalFrame*>(frame); + if( pObj == pThis->m_pWindow ) + { + pThis->m_aDamageHandler.damaged = nullptr; + pThis->m_aDamageHandler.handle = nullptr; + if (pThis->m_pSurface) + cairo_surface_set_user_data(pThis->m_pSurface, SvpSalGraphics::getDamageKey(), nullptr, nullptr); + pThis->m_pFixedContainer = nullptr; + pThis->m_pEventBox = nullptr; + pThis->m_pTopLevelGrid = nullptr; + pThis->m_pWindow = nullptr; + pThis->m_xFrameWeld.reset(); + pThis->InvalidateGraphics(); + } +} + +// GtkSalFrame::IMHandler + +GtkSalFrame::IMHandler::IMHandler( GtkSalFrame* pFrame ) +: m_pFrame(pFrame), + m_nPrevKeyPresses( 0 ), + m_pIMContext( nullptr ), + m_bFocused( true ), + m_bPreeditJustChanged( false ) +{ + m_aInputEvent.mpTextAttr = nullptr; + createIMContext(); +} + +GtkSalFrame::IMHandler::~IMHandler() +{ + // cancel an eventual event posted to begin preedit again + GtkSalFrame::getDisplay()->CancelInternalEvent( m_pFrame, &m_aInputEvent, SalEvent::ExtTextInput ); + deleteIMContext(); +} + +void GtkSalFrame::IMHandler::createIMContext() +{ + if( m_pIMContext ) + return; + + m_pIMContext = gtk_im_multicontext_new (); + g_signal_connect( m_pIMContext, "commit", + G_CALLBACK (signalIMCommit), this ); + g_signal_connect( m_pIMContext, "preedit_changed", + G_CALLBACK (signalIMPreeditChanged), this ); + g_signal_connect( m_pIMContext, "retrieve_surrounding", + G_CALLBACK (signalIMRetrieveSurrounding), this ); + g_signal_connect( m_pIMContext, "delete_surrounding", + G_CALLBACK (signalIMDeleteSurrounding), this ); + g_signal_connect( m_pIMContext, "preedit_start", + G_CALLBACK (signalIMPreeditStart), this ); + g_signal_connect( m_pIMContext, "preedit_end", + G_CALLBACK (signalIMPreeditEnd), this ); + + GetGenericUnixSalData()->ErrorTrapPush(); + gtk_im_context_set_client_window(m_pIMContext, gtk_widget_get_window(m_pFrame->getMouseEventWidget())); + gtk_im_context_focus_in( m_pIMContext ); + GetGenericUnixSalData()->ErrorTrapPop(); + m_bFocused = true; + +} + +void GtkSalFrame::IMHandler::deleteIMContext() +{ + if( m_pIMContext ) + { + // first give IC a chance to deinitialize + GetGenericUnixSalData()->ErrorTrapPush(); + gtk_im_context_set_client_window( m_pIMContext, nullptr ); + GetGenericUnixSalData()->ErrorTrapPop(); + // destroy old IC + g_object_unref( m_pIMContext ); + m_pIMContext = nullptr; + } +} + +void GtkSalFrame::IMHandler::doCallEndExtTextInput() +{ + m_aInputEvent.mpTextAttr = nullptr; + m_pFrame->CallCallbackExc( SalEvent::EndExtTextInput, nullptr ); +} + +void GtkSalFrame::IMHandler::updateIMSpotLocation() +{ + SalExtTextInputPosEvent aPosEvent; + m_pFrame->CallCallbackExc( SalEvent::ExtTextInputPos, static_cast<void*>(&aPosEvent) ); + GdkRectangle aArea; + aArea.x = aPosEvent.mnX; + aArea.y = aPosEvent.mnY; + aArea.width = aPosEvent.mnWidth; + aArea.height = aPosEvent.mnHeight; + GetGenericUnixSalData()->ErrorTrapPush(); + gtk_im_context_set_cursor_location( m_pIMContext, &aArea ); + GetGenericUnixSalData()->ErrorTrapPop(); +} + +void GtkSalFrame::IMHandler::sendEmptyCommit() +{ + vcl::DeletionListener aDel( m_pFrame ); + + SalExtTextInputEvent aEmptyEv; + aEmptyEv.mpTextAttr = nullptr; + aEmptyEv.maText.clear(); + aEmptyEv.mnCursorPos = 0; + aEmptyEv.mnCursorFlags = 0; + m_pFrame->CallCallbackExc( SalEvent::ExtTextInput, static_cast<void*>(&aEmptyEv) ); + if( ! aDel.isDeleted() ) + m_pFrame->CallCallbackExc( SalEvent::EndExtTextInput, nullptr ); +} + +void GtkSalFrame::IMHandler::endExtTextInput( EndExtTextInputFlags /*nFlags*/ ) +{ + gtk_im_context_reset ( m_pIMContext ); + + if( m_aInputEvent.mpTextAttr ) + { + vcl::DeletionListener aDel( m_pFrame ); + // delete preedit in sal (commit an empty string) + sendEmptyCommit(); + if( ! aDel.isDeleted() ) + { + // mark previous preedit state again (will e.g. be sent at focus gain) + m_aInputEvent.mpTextAttr = m_aInputFlags.data(); + if( m_bFocused ) + { + // begin preedit again + GtkSalFrame::getDisplay()->SendInternalEvent( m_pFrame, &m_aInputEvent, SalEvent::ExtTextInput ); + } + } + } +} + +void GtkSalFrame::IMHandler::focusChanged( bool bFocusIn ) +{ + m_bFocused = bFocusIn; + if( bFocusIn ) + { + GetGenericUnixSalData()->ErrorTrapPush(); + gtk_im_context_focus_in( m_pIMContext ); + GetGenericUnixSalData()->ErrorTrapPop(); + if( m_aInputEvent.mpTextAttr ) + { + sendEmptyCommit(); + // begin preedit again + GtkSalFrame::getDisplay()->SendInternalEvent( m_pFrame, &m_aInputEvent, SalEvent::ExtTextInput ); + } + } + else + { + GetGenericUnixSalData()->ErrorTrapPush(); + gtk_im_context_focus_out( m_pIMContext ); + GetGenericUnixSalData()->ErrorTrapPop(); + // cancel an eventual event posted to begin preedit again + GtkSalFrame::getDisplay()->CancelInternalEvent( m_pFrame, &m_aInputEvent, SalEvent::ExtTextInput ); + } +} + +bool GtkSalFrame::IMHandler::handleKeyEvent( GdkEventKey* pEvent ) +{ + vcl::DeletionListener aDel( m_pFrame ); + + if( pEvent->type == GDK_KEY_PRESS ) + { + // Add this key press event to the list of previous key presses + // to which we compare key release events. If a later key release + // event has a matching key press event in this list, we swallow + // the key release because some GTK Input Methods don't swallow it + // for us. + m_aPrevKeyPresses.emplace_back(pEvent ); + m_nPrevKeyPresses++; + + // Also pop off the earliest key press event if there are more than 10 + // already. + while (m_nPrevKeyPresses > 10) + { + m_aPrevKeyPresses.pop_front(); + m_nPrevKeyPresses--; + } + + GObject* pRef = G_OBJECT( g_object_ref( G_OBJECT( m_pIMContext ) ) ); + + // #i51353# update spot location on every key input since we cannot + // know which key may activate a preedit choice window + updateIMSpotLocation(); + if( aDel.isDeleted() ) + return true; + + bool bResult = gtk_im_context_filter_keypress( m_pIMContext, pEvent ); + g_object_unref( pRef ); + + if( aDel.isDeleted() ) + return true; + + m_bPreeditJustChanged = false; + + if( bResult ) + return true; + else + { + SAL_WARN_IF( m_nPrevKeyPresses <= 0, "vcl.gtk3", "key press has vanished !" ); + if( ! m_aPrevKeyPresses.empty() ) // sanity check + { + // event was not swallowed, do not filter a following + // key release event + // note: this relies on gtk_im_context_filter_keypress + // returning without calling a handler (in the "not swallowed" + // case ) which might change the previous key press list so + // we would pop the wrong event here + m_aPrevKeyPresses.pop_back(); + m_nPrevKeyPresses--; + } + } + } + + // Determine if we got an earlier key press event corresponding to this key release + if (pEvent->type == GDK_KEY_RELEASE) + { + GObject* pRef = G_OBJECT( g_object_ref( G_OBJECT( m_pIMContext ) ) ); + bool bResult = gtk_im_context_filter_keypress( m_pIMContext, pEvent ); + g_object_unref( pRef ); + + if( aDel.isDeleted() ) + return true; + + m_bPreeditJustChanged = false; + + auto iter = std::find(m_aPrevKeyPresses.begin(), m_aPrevKeyPresses.end(), pEvent); + // If we found a corresponding previous key press event, swallow the release + // and remove the earlier key press from our list + if (iter != m_aPrevKeyPresses.end()) + { + m_aPrevKeyPresses.erase(iter); + m_nPrevKeyPresses--; + return true; + } + + if( bResult ) + return true; + } + + return false; +} + +/* FIXME: +* #122282# still more hacking: some IMEs never start a preedit but simply commit +* in this case we cannot commit a single character. Workaround: do not do the +* single key hack for enter or space if the unicode committed does not match +*/ + +static bool checkSingleKeyCommitHack( guint keyval, sal_Unicode cCode ) +{ + bool bRet = true; + switch( keyval ) + { + case GDK_KEY_KP_Enter: + case GDK_KEY_Return: + if( cCode != '\n' && cCode != '\r' ) + bRet = false; + break; + case GDK_KEY_space: + case GDK_KEY_KP_Space: + if( cCode != ' ' ) + bRet = false; + break; + default: + break; + } + return bRet; +} + +void GtkSalFrame::IMHandler::signalIMCommit( GtkIMContext* /*pContext*/, gchar* pText, gpointer im_handler ) +{ + GtkSalFrame::IMHandler* pThis = static_cast<GtkSalFrame::IMHandler*>(im_handler); + + SolarMutexGuard aGuard; + vcl::DeletionListener aDel( pThis->m_pFrame ); + { + const bool bWasPreedit = + (pThis->m_aInputEvent.mpTextAttr != nullptr) || + pThis->m_bPreeditJustChanged; + + pThis->m_aInputEvent.mpTextAttr = nullptr; + pThis->m_aInputEvent.maText = OUString( pText, strlen(pText), RTL_TEXTENCODING_UTF8 ); + pThis->m_aInputEvent.mnCursorPos = pThis->m_aInputEvent.maText.getLength(); + pThis->m_aInputEvent.mnCursorFlags = 0; + + pThis->m_aInputFlags.clear(); + + /* necessary HACK: all keyboard input comes in here as soon as an IMContext is set + * which is logical and consequent. But since even simple input like + * <space> comes through the commit signal instead of signalKey + * and all kinds of windows only implement KeyInput (e.g. PushButtons, + * RadioButtons and a lot of other Controls), will send a single + * KeyInput/KeyUp sequence instead of an ExtText event if there + * never was a preedit and the text is only one character. + * + * In this case there the last ExtText event must have been + * SalEvent::EndExtTextInput, either because of a regular commit + * or because there never was a preedit. + */ + bool bSingleCommit = false; + if( ! bWasPreedit + && pThis->m_aInputEvent.maText.getLength() == 1 + && ! pThis->m_aPrevKeyPresses.empty() + ) + { + const PreviousKeyPress& rKP = pThis->m_aPrevKeyPresses.back(); + sal_Unicode aOrigCode = pThis->m_aInputEvent.maText[0]; + + if( checkSingleKeyCommitHack( rKP.keyval, aOrigCode ) ) + { + pThis->m_pFrame->doKeyCallback( rKP.state, rKP.keyval, rKP.hardware_keycode, rKP.group, aOrigCode, true, true ); + bSingleCommit = true; + } + } + if( ! bSingleCommit ) + { + pThis->m_pFrame->CallCallbackExc( SalEvent::ExtTextInput, static_cast<void*>(&pThis->m_aInputEvent)); + if( ! aDel.isDeleted() ) + pThis->doCallEndExtTextInput(); + } + if( ! aDel.isDeleted() ) + { + // reset input event + pThis->m_aInputEvent.maText.clear(); + pThis->m_aInputEvent.mnCursorPos = 0; + pThis->updateIMSpotLocation(); + } + } +} + +OUString GtkSalFrame::GetPreeditDetails(GtkIMContext* pIMContext, std::vector<ExtTextInputAttr>& rInputFlags, sal_Int32& rCursorPos, sal_uInt8& rCursorFlags) +{ + char* pText = nullptr; + PangoAttrList* pAttrs = nullptr; + gint nCursorPos = 0; + + gtk_im_context_get_preedit_string( pIMContext, + &pText, + &pAttrs, + &nCursorPos ); + + gint nUtf8Len = pText ? strlen(pText) : 0; + OUString sText = pText ? OUString(pText, nUtf8Len, RTL_TEXTENCODING_UTF8) : OUString(); + + std::vector<sal_Int32> aUtf16Offsets; + for (sal_Int32 nUtf16Offset = 0; nUtf16Offset < sText.getLength(); sText.iterateCodePoints(&nUtf16Offset)) + aUtf16Offsets.push_back(nUtf16Offset); + + sal_Int32 nUtf32Len = aUtf16Offsets.size(); + // from the above loop filling aUtf16Offsets, we know that its size() fits into sal_Int32 + aUtf16Offsets.push_back(sText.getLength()); + + // sanitize the CurPos which is in utf-32 + if (nCursorPos < 0) + nCursorPos = 0; + else if (nCursorPos > nUtf32Len) + nCursorPos = nUtf32Len; + + rCursorPos = aUtf16Offsets[nCursorPos]; + rCursorFlags = 0; + + rInputFlags.resize(std::max(1, static_cast<int>(sText.getLength())), ExtTextInputAttr::NONE); + + PangoAttrIterator *iter = pango_attr_list_get_iterator(pAttrs); + do + { + GSList *attr_list = nullptr; + GSList *tmp_list = nullptr; + gint nUtf8Start, nUtf8End; + ExtTextInputAttr sal_attr = ExtTextInputAttr::NONE; + + // docs say... "Get the range of the current segment ... the stored + // return values are signed, not unsigned like the values in + // PangoAttribute", which implies that the units are otherwise the same + // as that of PangoAttribute whose docs state these units are "in + // bytes" + // so this is the utf8 range + pango_attr_iterator_range(iter, &nUtf8Start, &nUtf8End); + + // sanitize the utf8 range + nUtf8Start = std::min(nUtf8Start, nUtf8Len); + nUtf8End = std::min(nUtf8End, nUtf8Len); + if (nUtf8Start >= nUtf8End) + continue; + + // get the utf32 range + sal_Int32 nUtf32Start = g_utf8_pointer_to_offset(pText, pText + nUtf8Start); + sal_Int32 nUtf32End = g_utf8_pointer_to_offset(pText, pText + nUtf8End); + + // sanitize the utf32 range + nUtf32Start = std::min(nUtf32Start, nUtf32Len); + nUtf32End = std::min(nUtf32End, nUtf32Len); + if (nUtf32Start >= nUtf32End) + continue; + + tmp_list = attr_list = pango_attr_iterator_get_attrs (iter); + while (tmp_list) + { + PangoAttribute *pango_attr = static_cast<PangoAttribute *>(tmp_list->data); + + switch (pango_attr->klass->type) + { + case PANGO_ATTR_BACKGROUND: + sal_attr |= ExtTextInputAttr::Highlight; + rCursorFlags |= EXTTEXTINPUT_CURSOR_INVISIBLE; + break; + case PANGO_ATTR_UNDERLINE: + sal_attr |= ExtTextInputAttr::Underline; + break; + case PANGO_ATTR_STRIKETHROUGH: + sal_attr |= ExtTextInputAttr::RedText; + break; + default: + break; + } + pango_attribute_destroy (pango_attr); + tmp_list = tmp_list->next; + } + if (sal_attr == ExtTextInputAttr::NONE) + sal_attr |= ExtTextInputAttr::Underline; + g_slist_free (attr_list); + + // Set the sal attributes on our text + // rhbz#1648281 apply over our utf-16 range derived from the input utf-32 range + for (sal_Int32 i = aUtf16Offsets[nUtf32Start]; i < aUtf16Offsets[nUtf32End]; ++i) + { + SAL_WARN_IF(i >= static_cast<int>(rInputFlags.size()), + "vcl.gtk3", "pango attrib out of range. Broken range: " + << aUtf16Offsets[nUtf32Start] << "," << aUtf16Offsets[nUtf32End] << " Legal range: 0," + << rInputFlags.size()); + if (i >= static_cast<int>(rInputFlags.size())) + continue; + rInputFlags[i] |= sal_attr; + } + } while (pango_attr_iterator_next (iter)); + pango_attr_iterator_destroy(iter); + + g_free( pText ); + pango_attr_list_unref( pAttrs ); + + return sText; +} + +void GtkSalFrame::IMHandler::signalIMPreeditChanged( GtkIMContext* pIMContext, gpointer im_handler ) +{ + GtkSalFrame::IMHandler* pThis = static_cast<GtkSalFrame::IMHandler*>(im_handler); + + sal_Int32 nCursorPos(0); + sal_uInt8 nCursorFlags(0); + std::vector<ExtTextInputAttr> aInputFlags; + OUString sText = GtkSalFrame::GetPreeditDetails(pIMContext, aInputFlags, nCursorPos, nCursorFlags); + if (sText.isEmpty() && pThis->m_aInputEvent.maText.isEmpty()) + { + // change from nothing to nothing -> do not start preedit + // e.g. this will activate input into a calc cell without + // user input + return; + } + + pThis->m_bPreeditJustChanged = true; + + bool bEndPreedit = sText.isEmpty() && pThis->m_aInputEvent.mpTextAttr != nullptr; + pThis->m_aInputEvent.maText = sText; + pThis->m_aInputEvent.mnCursorPos = nCursorPos; + pThis->m_aInputEvent.mnCursorFlags = nCursorFlags; + pThis->m_aInputFlags = aInputFlags; + pThis->m_aInputEvent.mpTextAttr = pThis->m_aInputFlags.data(); + + SolarMutexGuard aGuard; + vcl::DeletionListener aDel( pThis->m_pFrame ); + + pThis->m_pFrame->CallCallbackExc( SalEvent::ExtTextInput, static_cast<void*>(&pThis->m_aInputEvent)); + if( bEndPreedit && ! aDel.isDeleted() ) + pThis->doCallEndExtTextInput(); + if( ! aDel.isDeleted() ) + pThis->updateIMSpotLocation(); +} + +void GtkSalFrame::IMHandler::signalIMPreeditStart( GtkIMContext*, gpointer /*im_handler*/ ) +{ +} + +void GtkSalFrame::IMHandler::signalIMPreeditEnd( GtkIMContext*, gpointer im_handler ) +{ + GtkSalFrame::IMHandler* pThis = static_cast<GtkSalFrame::IMHandler*>(im_handler); + + pThis->m_bPreeditJustChanged = true; + + SolarMutexGuard aGuard; + vcl::DeletionListener aDel( pThis->m_pFrame ); + pThis->doCallEndExtTextInput(); + if( ! aDel.isDeleted() ) + pThis->updateIMSpotLocation(); +} + +static uno::Reference<accessibility::XAccessibleEditableText> lcl_GetxText(vcl::Window *pFocusWin) +{ + uno::Reference<accessibility::XAccessibleEditableText> xText; + try + { + uno::Reference< accessibility::XAccessible > xAccessible( pFocusWin->GetAccessible() ); + if (xAccessible.is()) + xText = FindFocusedEditableText(xAccessible->getAccessibleContext()); + } + catch(const uno::Exception&) + { + TOOLS_WARN_EXCEPTION( "vcl.gtk3", "Exception in getting input method surrounding text"); + } + return xText; +} + +gboolean GtkSalFrame::IMHandler::signalIMRetrieveSurrounding( GtkIMContext* pContext, gpointer /*im_handler*/ ) +{ + vcl::Window *pFocusWin = Application::GetFocusWindow(); + if (!pFocusWin) + return true; + + uno::Reference<accessibility::XAccessibleEditableText> xText = lcl_GetxText(pFocusWin); + if (xText.is()) + { + sal_Int32 nPosition = xText->getCaretPosition(); + if (nPosition != -1) + { + OUString sAllText = xText->getText(); + OString sUTF = OUStringToOString(sAllText, RTL_TEXTENCODING_UTF8); + OUString sCursorText(sAllText.copy(0, nPosition)); + gtk_im_context_set_surrounding(pContext, sUTF.getStr(), sUTF.getLength(), + OUStringToOString(sCursorText, RTL_TEXTENCODING_UTF8).getLength()); + return true; + } + } + + return false; +} + +gboolean GtkSalFrame::IMHandler::signalIMDeleteSurrounding( GtkIMContext*, gint offset, gint nchars, + gpointer /*im_handler*/ ) +{ + vcl::Window *pFocusWin = Application::GetFocusWindow(); + if (!pFocusWin) + return true; + + uno::Reference<accessibility::XAccessibleEditableText> xText = lcl_GetxText(pFocusWin); + if (xText.is()) + { + sal_Int32 nPosition = xText->getCaretPosition(); + // #i111768# range checking + sal_Int32 nDeletePos = nPosition + offset; + sal_Int32 nDeleteEnd = nDeletePos + nchars; + if (nDeletePos < 0) + nDeletePos = 0; + if (nDeleteEnd < 0) + nDeleteEnd = 0; + if (nDeleteEnd > xText->getCharacterCount()) + nDeleteEnd = xText->getCharacterCount(); + + xText->deleteText(nDeletePos, nDeleteEnd); + //tdf91641 adjust cursor if deleted chars shift it forward (normal case) + if (nDeletePos < nPosition) + { + if (nDeleteEnd <= nPosition) + nPosition = nPosition - (nDeleteEnd - nDeletePos); + else + nPosition = nDeletePos; + + if (xText->getCharacterCount() >= nPosition) + xText->setCaretPosition( nPosition ); + } + return true; + } + + return false; +} + +Size GtkSalDisplay::GetScreenSize( int nDisplayScreen ) +{ + tools::Rectangle aRect = m_pSys->GetDisplayScreenPosSizePixel( nDisplayScreen ); + return Size( aRect.GetWidth(), aRect.GetHeight() ); +} + +sal_uIntPtr GtkSalFrame::GetNativeWindowHandle(GtkWidget *pWidget) +{ + (void) this; // Silence loplugin:staticmethods + GdkDisplay *pDisplay = getGdkDisplay(); + GdkWindow *pWindow = gtk_widget_get_window(pWidget); + +#if defined(GDK_WINDOWING_X11) + if (DLSYM_GDK_IS_X11_DISPLAY(pDisplay)) + { + return GDK_WINDOW_XID(pWindow); + } +#endif +#if defined(GDK_WINDOWING_WAYLAND) + if (DLSYM_GDK_IS_WAYLAND_DISPLAY(pDisplay)) + { + return reinterpret_cast<sal_uIntPtr>(gdk_wayland_window_get_wl_surface(pWindow)); + } +#endif + return 0; +} + +sal_uIntPtr GtkSalFrame::GetNativeWindowHandle() +{ + return GetNativeWindowHandle(m_pWindow); +} + +void GtkDragSource::set_datatransfer(const css::uno::Reference<css::datatransfer::XTransferable>& rTrans, + const css::uno::Reference<css::datatransfer::dnd::XDragSourceListener>& rListener) +{ + m_xListener = rListener; + m_xTrans = rTrans; +} + +void GtkDragSource::setActiveDragSource() +{ + // For LibreOffice internal D&D we provide the Transferable without Gtk + // intermediaries as a shortcut, see tdf#100097 for how dbaccess depends on this + g_ActiveDragSource = this; + g_DropSuccessSet = false; + g_DropSuccess = false; +} + +std::vector<GtkTargetEntry> GtkDragSource::FormatsToGtk(const css::uno::Sequence<css::datatransfer::DataFlavor> &rFormats) +{ + return m_aConversionHelper.FormatsToGtk(rFormats); +} + +void GtkDragSource::startDrag(const datatransfer::dnd::DragGestureEvent& rEvent, + sal_Int8 sourceActions, sal_Int32 /*cursor*/, sal_Int32 /*image*/, + const css::uno::Reference<css::datatransfer::XTransferable>& rTrans, + const css::uno::Reference<css::datatransfer::dnd::XDragSourceListener>& rListener) +{ + set_datatransfer(rTrans, rListener); + + if (m_pFrame) + { + auto aFormats = m_xTrans->getTransferDataFlavors(); + std::vector<GtkTargetEntry> aGtkTargets(FormatsToGtk(aFormats)); + GtkTargetList *pTargetList = gtk_target_list_new(aGtkTargets.data(), aGtkTargets.size()); + + gint nDragButton = 1; // default to left button + css::awt::MouseEvent aEvent; + if (rEvent.Event >>= aEvent) + { + if (aEvent.Buttons & css::awt::MouseButton::LEFT ) + nDragButton = 1; + else if (aEvent.Buttons & css::awt::MouseButton::RIGHT) + nDragButton = 3; + else if (aEvent.Buttons & css::awt::MouseButton::MIDDLE) + nDragButton = 2; + } + + setActiveDragSource(); + + m_pFrame->startDrag(nDragButton, rEvent.DragOriginX, rEvent.DragOriginY, + VclToGdk(sourceActions), pTargetList); + + gtk_target_list_unref(pTargetList); + for (auto &a : aGtkTargets) + g_free(a.target); + } + else + dragFailed(); +} + +void GtkSalFrame::startDrag(gint nButton, gint nDragOriginX, gint nDragOriginY, + GdkDragAction sourceActions, GtkTargetList* pTargetList) +{ + SolarMutexGuard aGuard; + + assert(m_pDragSource); + + GdkEvent aFakeEvent; + memset(&aFakeEvent, 0, sizeof(GdkEvent)); + aFakeEvent.type = GDK_BUTTON_PRESS; + aFakeEvent.button.window = gtk_widget_get_window(getMouseEventWidget()); + aFakeEvent.button.time = GDK_CURRENT_TIME; + GdkDeviceManager* pDeviceManager = gdk_display_get_device_manager(getGdkDisplay()); + aFakeEvent.button.device = gdk_device_manager_get_client_pointer(pDeviceManager); + + GdkDragContext *pContext = gtk_drag_begin_with_coordinates(getMouseEventWidget(), + pTargetList, + sourceActions, + nButton, + &aFakeEvent, + nDragOriginX, + nDragOriginY); + + if (!pContext) + m_pDragSource->dragFailed(); +} + +void GtkDragSource::dragFailed() +{ + if (m_xListener.is()) + { + datatransfer::dnd::DragSourceDropEvent aEv; + aEv.DropAction = datatransfer::dnd::DNDConstants::ACTION_NONE; + aEv.DropSuccess = false; + auto xListener = m_xListener; + m_xListener.clear(); + xListener->dragDropEnd(aEv); + } +} + +gboolean GtkSalFrame::signalDragFailed(GtkWidget* /*widget*/, GdkDragContext* /*context*/, GtkDragResult /*result*/, gpointer frame) +{ + GtkSalFrame* pThis = static_cast<GtkSalFrame*>(frame); + if (!pThis->m_pDragSource) + return false; + pThis->m_pDragSource->dragFailed(); + return false; +} + +void GtkDragSource::dragDelete() +{ + if (m_xListener.is()) + { + datatransfer::dnd::DragSourceDropEvent aEv; + aEv.DropAction = datatransfer::dnd::DNDConstants::ACTION_MOVE; + aEv.DropSuccess = true; + auto xListener = m_xListener; + m_xListener.clear(); + xListener->dragDropEnd(aEv); + } +} + +void GtkSalFrame::signalDragDelete(GtkWidget* /*widget*/, GdkDragContext* /*context*/, gpointer frame) +{ + GtkSalFrame* pThis = static_cast<GtkSalFrame*>(frame); + if (!pThis->m_pDragSource) + return; + pThis->m_pDragSource->dragDelete(); +} + +void GtkDragSource::dragEnd(GdkDragContext* context) +{ + if (m_xListener.is()) + { + datatransfer::dnd::DragSourceDropEvent aEv; + aEv.DropAction = GdkToVcl(gdk_drag_context_get_selected_action(context)); + // an internal drop can accept the drop but fail with dropComplete( false ) + // this is different than the GTK API + if (g_DropSuccessSet) + aEv.DropSuccess = g_DropSuccess; + else + aEv.DropSuccess = true; + auto xListener = m_xListener; + m_xListener.clear(); + xListener->dragDropEnd(aEv); + } + g_ActiveDragSource = nullptr; +} + +void GtkSalFrame::signalDragEnd(GtkWidget* /*widget*/, GdkDragContext* context, gpointer frame) +{ + GtkSalFrame* pThis = static_cast<GtkSalFrame*>(frame); + if (!pThis->m_pDragSource) + return; + pThis->m_pDragSource->dragEnd(context); +} + +void GtkDragSource::dragDataGet(GtkSelectionData *data, guint info) +{ + m_aConversionHelper.setSelectionData(m_xTrans, data, info); +} + +void GtkSalFrame::signalDragDataGet(GtkWidget* /*widget*/, GdkDragContext* /*context*/, GtkSelectionData *data, guint info, + guint /*time*/, gpointer frame) +{ + GtkSalFrame* pThis = static_cast<GtkSalFrame*>(frame); + if (!pThis->m_pDragSource) + return; + pThis->m_pDragSource->dragDataGet(data, info); +} + +bool GtkSalFrame::CallCallbackExc(SalEvent nEvent, const void* pEvent) const +{ + bool nRet = false; + try + { + nRet = CallCallback(nEvent, pEvent); + } + catch (...) + { + GtkSalData *pSalData = static_cast<GtkSalData*>(GetSalData()); + pSalData->setException(std::current_exception()); + } + return nRet; +} + +void GtkSalFrame::nopaint_container_resize_children(GtkContainer *pContainer) +{ + bool bOrigSalObjectSetPosSize = m_bSalObjectSetPosSize; + m_bSalObjectSetPosSize = true; + gtk_container_resize_children(pContainer); + m_bSalObjectSetPosSize = bOrigSalObjectSetPosSize; +} + +GdkEvent* GtkSalFrame::makeFakeKeyPress(GtkWidget* pWidget) +{ + GdkEvent *event = gdk_event_new(GDK_KEY_PRESS); + event->key.window = GDK_WINDOW(g_object_ref(gtk_widget_get_window(pWidget))); + +#if GTK_CHECK_VERSION(3, 20, 0) + if (gtk_check_version(3, 20, 0) == nullptr) + { + GdkSeat *seat = gdk_display_get_default_seat(gtk_widget_get_display(pWidget)); + gdk_event_set_device(event, gdk_seat_get_keyboard(seat)); + } +#endif + + event->key.send_event = 1 /* TRUE */; + event->key.time = gtk_get_current_event_time(); + event->key.state = 0; + event->key.keyval = 0; + event->key.length = 0; + event->key.string = nullptr; + event->key.hardware_keycode = 0; + event->key.group = 0; + event->key.is_modifier = false; + return event; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/gtk3gtkinst.cxx b/vcl/unx/gtk3/gtk3gtkinst.cxx new file mode 100644 index 000000000..0290359be --- /dev/null +++ b/vcl/unx/gtk3/gtk3gtkinst.cxx @@ -0,0 +1,16272 @@ +/* -*- 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 <sal/config.h> + +#include <deque> +#include <stack> +#include <string.h> +#include <osl/process.h> +#include <unx/gtk/gtkdata.hxx> +#include <unx/gtk/gtkinst.hxx> +#include <unx/salobj.h> +#include <unx/gtk/gtkgdi.hxx> +#include <unx/gtk/gtkframe.hxx> +#include <unx/gtk/gtkobject.hxx> +#include <unx/gtk/atkbridge.hxx> +#include <unx/gtk/gtkprn.hxx> +#include <unx/gtk/gtksalmenu.hxx> +#include <headless/svpvd.hxx> +#include <headless/svpbmp.hxx> +#include <vcl/inputtypes.hxx> +#include <vcl/transfer.hxx> +#include <unx/genpspgraphics.h> +#include <rtl/strbuf.hxx> +#include <sal/log.hxx> +#include <rtl/uri.hxx> + +#include <vcl/settings.hxx> + +#include <dlfcn.h> +#include <fcntl.h> +#include <unistd.h> + +#include <unx/gtk/gtkprintwrapper.hxx> + +#include "a11y/atkwrapper.hxx" +#include <com/sun/star/lang/IllegalArgumentException.hpp> +#include <com/sun/star/lang/XMultiServiceFactory.hpp> +#include <com/sun/star/lang/XServiceInfo.hpp> +#include <com/sun/star/lang/XSingleServiceFactory.hpp> +#include <com/sun/star/lang/XInitialization.hpp> +#include <com/sun/star/datatransfer/XTransferable.hpp> +#include <com/sun/star/datatransfer/clipboard/XClipboard.hpp> +#include <com/sun/star/datatransfer/clipboard/XClipboardEx.hpp> +#include <com/sun/star/datatransfer/clipboard/XClipboardNotifier.hpp> +#include <com/sun/star/datatransfer/clipboard/XClipboardListener.hpp> +#include <com/sun/star/datatransfer/clipboard/XFlushableClipboard.hpp> +#include <com/sun/star/datatransfer/clipboard/XSystemClipboard.hpp> +#include <com/sun/star/datatransfer/dnd/DNDConstants.hpp> +#include <comphelper/lok.hxx> +#include <comphelper/processfactory.hxx> +#include <comphelper/sequence.hxx> +#include <cppuhelper/compbase.hxx> +#include <comphelper/string.hxx> +#include <cppuhelper/implbase.hxx> +#include <cppuhelper/supportsservice.hxx> +#include <officecfg/Office/Common.hxx> +#include <rtl/bootstrap.hxx> +#include <o3tl/unreachable.hxx> +#include <svl/zforlist.hxx> +#include <svl/zformat.hxx> +#include <tools/helpers.hxx> +#include <tools/fract.hxx> +#include <tools/stream.hxx> +#include <unotools/resmgr.hxx> +#include <unx/gstsink.hxx> +#include <vcl/ImageTree.hxx> +#include <vcl/abstdlg.hxx> +#include <vcl/event.hxx> +#include <vcl/i18nhelp.hxx> +#include <vcl/quickselectionengine.hxx> +#include <vcl/mnemonic.hxx> +#include <vcl/pngwrite.hxx> +#include <vcl/stdtext.hxx> +#include <vcl/syswin.hxx> +#include <vcl/virdev.hxx> +#include <vcl/weld.hxx> +#include <vcl/wrkwin.hxx> +#include <strings.hrc> +#include <window.h> +#include <numeric> + +#include <boost/property_tree/ptree.hpp> + +using namespace com::sun::star; +using namespace com::sun::star::uno; +using namespace com::sun::star::lang; + +extern "C" +{ + #define GET_YIELD_MUTEX() static_cast<GtkYieldMutex*>(GetSalData()->m_pInstance->GetYieldMutex()) + static void GdkThreadsEnter() + { + GtkYieldMutex *pYieldMutex = GET_YIELD_MUTEX(); + pYieldMutex->ThreadsEnter(); + } + static void GdkThreadsLeave() + { + GtkYieldMutex *pYieldMutex = GET_YIELD_MUTEX(); + pYieldMutex->ThreadsLeave(); + } + + VCLPLUG_GTK_PUBLIC SalInstance* create_SalInstance() + { + SAL_INFO( + "vcl.gtk", + "create vcl plugin instance with gtk version " << gtk_major_version + << " " << gtk_minor_version << " " << gtk_micro_version); + + if (gtk_major_version == 3 && gtk_minor_version < 18) + { + g_warning("require gtk >= 3.18 for theme expectations"); + return nullptr; + } + + // for gtk2 it is always built with X support, so this is always called + // for gtk3 it is normally built with X and Wayland support, if + // X is supported GDK_WINDOWING_X11 is defined and this is always + // called, regardless of if we're running under X or Wayland. + // We can't use (DLSYM_GDK_IS_X11_DISPLAY(pDisplay)) to only do it under + // X, because we need to do it earlier than we have a display +#if defined(GDK_WINDOWING_X11) + /* #i92121# workaround deadlocks in the X11 implementation + */ + static const char* pNoXInitThreads = getenv( "SAL_NO_XINITTHREADS" ); + /* #i90094# + from now on we know that an X connection will be + established, so protect X against itself + */ + if( ! ( pNoXInitThreads && *pNoXInitThreads ) ) + XInitThreads(); +#endif + + // init gdk thread protection + bool const sup = g_thread_supported(); + // extracted from the 'if' to avoid Clang -Wunreachable-code + if ( !sup ) + g_thread_init( nullptr ); + + gdk_threads_set_lock_functions (GdkThreadsEnter, GdkThreadsLeave); + SAL_INFO("vcl.gtk", "Hooked gdk threads locks"); + + auto pYieldMutex = std::make_unique<GtkYieldMutex>(); + + gdk_threads_init(); + + GtkInstance* pInstance = new GtkInstance( std::move(pYieldMutex) ); + SAL_INFO("vcl.gtk", "creating GtkInstance " << pInstance); + + // Create SalData, this does not leak + new GtkSalData( pInstance ); + + return pInstance; + } +} + +static VclInputFlags categorizeEvent(const GdkEvent *pEvent) +{ + VclInputFlags nType = VclInputFlags::NONE; + switch( pEvent->type ) + { + case GDK_MOTION_NOTIFY: + case GDK_BUTTON_PRESS: + case GDK_2BUTTON_PRESS: + case GDK_3BUTTON_PRESS: + case GDK_BUTTON_RELEASE: + case GDK_ENTER_NOTIFY: + case GDK_LEAVE_NOTIFY: + case GDK_SCROLL: + nType = VclInputFlags::MOUSE; + break; + case GDK_KEY_PRESS: + // case GDK_KEY_RELEASE: //similar to the X11SalInstance one + nType = VclInputFlags::KEYBOARD; + break; + case GDK_EXPOSE: + nType = VclInputFlags::PAINT; + break; + default: + nType = VclInputFlags::OTHER; + break; + } + return nType; +} + +GtkInstance::GtkInstance( std::unique_ptr<SalYieldMutex> pMutex ) + : SvpSalInstance( std::move(pMutex) ) + , m_pTimer(nullptr) + , bNeedsInit(true) + , m_pLastCairoFontOptions(nullptr) +{ +} + +//We want to defer initializing gtk until we are after uno has been +//bootstrapped so we can ask the config what the UI language is so that we can +//force that in as $LANGUAGE to get gtk to render widgets RTL if we have a RTL +//UI in a LTR locale +void GtkInstance::AfterAppInit() +{ + EnsureInit(); +} + +void GtkInstance::EnsureInit() +{ + if (!bNeedsInit) + return; + // initialize SalData + GtkSalData *pSalData = GetGtkSalData(); + pSalData->Init(); + GtkSalData::initNWF(); + + InitAtkBridge(); + + ImplSVData* pSVData = ImplGetSVData(); +#ifdef GTK_TOOLKIT_NAME + pSVData->maAppData.mxToolkitName = OUString(GTK_TOOLKIT_NAME); +#else + pSVData->maAppData.mxToolkitName = OUString("gtk3"); +#endif + + bNeedsInit = false; +} + +GtkInstance::~GtkInstance() +{ + assert( nullptr == m_pTimer ); + DeInitAtkBridge(); + ResetLastSeenCairoFontOptions(nullptr); +} + +SalFrame* GtkInstance::CreateFrame( SalFrame* pParent, SalFrameStyleFlags nStyle ) +{ + EnsureInit(); + return new GtkSalFrame( pParent, nStyle ); +} + +SalFrame* GtkInstance::CreateChildFrame( SystemParentData* pParentData, SalFrameStyleFlags ) +{ + EnsureInit(); + return new GtkSalFrame( pParentData ); +} + +SalObject* GtkInstance::CreateObject( SalFrame* pParent, SystemWindowData* pWindowData, bool bShow ) +{ + EnsureInit(); + //FIXME: Missing CreateObject functionality ... + if (pWindowData && pWindowData->bClipUsingNativeWidget) + return new GtkSalObjectWidgetClip(static_cast<GtkSalFrame*>(pParent), bShow); + return new GtkSalObject(static_cast<GtkSalFrame*>(pParent), bShow); +} + +extern "C" +{ + typedef void*(* getDefaultFnc)(); + typedef void(* addItemFnc)(void *, const char *); +} + +void GtkInstance::AddToRecentDocumentList(const OUString& rFileUrl, const OUString&, const OUString&) +{ + EnsureInit(); + OString sGtkURL; + rtl_TextEncoding aSystemEnc = osl_getThreadTextEncoding(); + if ((aSystemEnc == RTL_TEXTENCODING_UTF8) || !rFileUrl.startsWith( "file://" )) + sGtkURL = OUStringToOString(rFileUrl, RTL_TEXTENCODING_UTF8); + else + { + //Non-utf8 locales are a bad idea if trying to work with non-ascii filenames + //Decode %XX components + OUString sDecodedUri = rtl::Uri::decode(rFileUrl.copy(7), rtl_UriDecodeToIuri, RTL_TEXTENCODING_UTF8); + //Convert back to system locale encoding + OString sSystemUrl = OUStringToOString(sDecodedUri, aSystemEnc); + //Encode to an escaped ASCII-encoded URI + gchar *g_uri = g_filename_to_uri(sSystemUrl.getStr(), nullptr, nullptr); + sGtkURL = OString(g_uri); + g_free(g_uri); + } + GtkRecentManager *manager = gtk_recent_manager_get_default (); + gtk_recent_manager_add_item (manager, sGtkURL.getStr()); +} + +SalInfoPrinter* GtkInstance::CreateInfoPrinter( SalPrinterQueueInfo* pQueueInfo, + ImplJobSetup* pSetupData ) +{ + EnsureInit(); + mbPrinterInit = true; + // create and initialize SalInfoPrinter + PspSalInfoPrinter* pPrinter = new GtkSalInfoPrinter; + configurePspInfoPrinter(pPrinter, pQueueInfo, pSetupData); + return pPrinter; +} + +std::unique_ptr<SalPrinter> GtkInstance::CreatePrinter( SalInfoPrinter* pInfoPrinter ) +{ + EnsureInit(); + mbPrinterInit = true; + return std::unique_ptr<SalPrinter>(new GtkSalPrinter( pInfoPrinter )); +} + +/* + * These methods always occur in pairs + * A ThreadsEnter is followed by a ThreadsLeave + * We need to queue up the recursive lock count + * for each pair, so we can accurately restore + * it later. + */ +thread_local std::stack<sal_uInt32> GtkYieldMutex::yieldCounts; + +void GtkYieldMutex::ThreadsEnter() +{ + acquire(); + if (!yieldCounts.empty()) { + auto n = yieldCounts.top(); + yieldCounts.pop(); + assert(n > 0); + n--; + if (n > 0) + acquire(n); + } +} + +void GtkYieldMutex::ThreadsLeave() +{ + assert(m_nCount != 0); + yieldCounts.push(m_nCount); + release(true); +} + +std::unique_ptr<SalVirtualDevice> GtkInstance::CreateVirtualDevice( SalGraphics *pG, + long &nDX, long &nDY, + DeviceFormat eFormat, + const SystemGraphicsData* pGd ) +{ + EnsureInit(); + SvpSalGraphics *pSvpSalGraphics = dynamic_cast<SvpSalGraphics*>(pG); + assert(pSvpSalGraphics); + // tdf#127529 see SvpSalInstance::CreateVirtualDevice for the rare case of a non-null pPreExistingTarget + cairo_surface_t* pPreExistingTarget = pGd ? static_cast<cairo_surface_t*>(pGd->pSurface) : nullptr; + std::unique_ptr<SalVirtualDevice> pNew(new SvpSalVirtualDevice(eFormat, pSvpSalGraphics->getSurface(), pPreExistingTarget)); + pNew->SetSize( nDX, nDY ); + return pNew; +} + +std::shared_ptr<SalBitmap> GtkInstance::CreateSalBitmap() +{ + EnsureInit(); + return SvpSalInstance::CreateSalBitmap(); +} + +std::unique_ptr<SalMenu> GtkInstance::CreateMenu( bool bMenuBar, Menu* pVCLMenu ) +{ + EnsureInit(); + GtkSalMenu* pSalMenu = new GtkSalMenu( bMenuBar ); + pSalMenu->SetMenu( pVCLMenu ); + return std::unique_ptr<SalMenu>(pSalMenu); +} + +std::unique_ptr<SalMenuItem> GtkInstance::CreateMenuItem( const SalItemParams & rItemData ) +{ + EnsureInit(); + return std::unique_ptr<SalMenuItem>(new GtkSalMenuItem( &rItemData )); +} + +SalTimer* GtkInstance::CreateSalTimer() +{ + EnsureInit(); + assert( nullptr == m_pTimer ); + if ( nullptr == m_pTimer ) + m_pTimer = new GtkSalTimer(); + return m_pTimer; +} + +void GtkInstance::RemoveTimer () +{ + EnsureInit(); + m_pTimer = nullptr; +} + +bool GtkInstance::DoYield(bool bWait, bool bHandleAllCurrentEvents) +{ + EnsureInit(); + return GetGtkSalData()->Yield( bWait, bHandleAllCurrentEvents ); +} + +bool GtkInstance::IsTimerExpired() +{ + EnsureInit(); + return (m_pTimer && m_pTimer->Expired()); +} + +bool GtkInstance::AnyInput( VclInputFlags nType ) +{ + EnsureInit(); + if( (nType & VclInputFlags::TIMER) && IsTimerExpired() ) + return true; + if (!gdk_events_pending()) + return false; + + if (nType == VCL_INPUT_ANY) + return true; + + bool bRet = false; + std::deque<GdkEvent*> aEvents; + GdkEvent *pEvent = nullptr; + while ((pEvent = gdk_event_get())) + { + aEvents.push_back(pEvent); + VclInputFlags nEventType = categorizeEvent(pEvent); + if ( (nEventType & nType) || ( nEventType == VclInputFlags::NONE && (nType & VclInputFlags::OTHER) ) ) + { + bRet = true; + } + } + + while (!aEvents.empty()) + { + pEvent = aEvents.front(); + gdk_event_put(pEvent); + gdk_event_free(pEvent); + aEvents.pop_front(); + } + return bRet; +} + +std::unique_ptr<GenPspGraphics> GtkInstance::CreatePrintGraphics() +{ + EnsureInit(); + return std::make_unique<GenPspGraphics>(); +} + +std::shared_ptr<vcl::unx::GtkPrintWrapper> const & +GtkInstance::getPrintWrapper() const +{ + if (!m_xPrintWrapper) + m_xPrintWrapper = std::make_shared<vcl::unx::GtkPrintWrapper>(); + return m_xPrintWrapper; +} + +const cairo_font_options_t* GtkInstance::GetCairoFontOptions() +{ + const cairo_font_options_t* pCairoFontOptions = gdk_screen_get_font_options(gdk_screen_get_default()); + if (!m_pLastCairoFontOptions && pCairoFontOptions) + m_pLastCairoFontOptions = cairo_font_options_copy(pCairoFontOptions); + return pCairoFontOptions; +} + +const cairo_font_options_t* GtkInstance::GetLastSeenCairoFontOptions() const +{ + return m_pLastCairoFontOptions; +} + +void GtkInstance::ResetLastSeenCairoFontOptions(const cairo_font_options_t* pCairoFontOptions) +{ + if (m_pLastCairoFontOptions) + cairo_font_options_destroy(m_pLastCairoFontOptions); + if (pCairoFontOptions) + m_pLastCairoFontOptions = cairo_font_options_copy(pCairoFontOptions); + else + m_pLastCairoFontOptions = nullptr; +} + + +namespace +{ + struct TypeEntry + { + const char* pNativeType; // string corresponding to nAtom for the case of nAtom being uninitialized + const char* pType; // Mime encoding on our side + }; + + static const TypeEntry aConversionTab[] = + { + { "ISO10646-1", "text/plain;charset=utf-16" }, + { "UTF8_STRING", "text/plain;charset=utf-8" }, + { "UTF-8", "text/plain;charset=utf-8" }, + { "text/plain;charset=UTF-8", "text/plain;charset=utf-8" }, + // ISO encodings + { "ISO8859-2", "text/plain;charset=iso8859-2" }, + { "ISO8859-3", "text/plain;charset=iso8859-3" }, + { "ISO8859-4", "text/plain;charset=iso8859-4" }, + { "ISO8859-5", "text/plain;charset=iso8859-5" }, + { "ISO8859-6", "text/plain;charset=iso8859-6" }, + { "ISO8859-7", "text/plain;charset=iso8859-7" }, + { "ISO8859-8", "text/plain;charset=iso8859-8" }, + { "ISO8859-9", "text/plain;charset=iso8859-9" }, + { "ISO8859-10", "text/plain;charset=iso8859-10" }, + { "ISO8859-13", "text/plain;charset=iso8859-13" }, + { "ISO8859-14", "text/plain;charset=iso8859-14" }, + { "ISO8859-15", "text/plain;charset=iso8859-15" }, + // asian encodings + { "JISX0201.1976-0", "text/plain;charset=jisx0201.1976-0" }, + { "JISX0208.1983-0", "text/plain;charset=jisx0208.1983-0" }, + { "JISX0208.1990-0", "text/plain;charset=jisx0208.1990-0" }, + { "JISX0212.1990-0", "text/plain;charset=jisx0212.1990-0" }, + { "GB2312.1980-0", "text/plain;charset=gb2312.1980-0" }, + { "KSC5601.1992-0", "text/plain;charset=ksc5601.1992-0" }, + // eastern european encodings + { "KOI8-R", "text/plain;charset=koi8-r" }, + { "KOI8-U", "text/plain;charset=koi8-u" }, + // String (== iso8859-1) + { "STRING", "text/plain;charset=iso8859-1" }, + // special for compound text + { "COMPOUND_TEXT", "text/plain;charset=compound_text" }, + + // PIXMAP + { "PIXMAP", "image/bmp" } + }; + + class DataFlavorEq + { + private: + const css::datatransfer::DataFlavor& m_rData; + public: + explicit DataFlavorEq(const css::datatransfer::DataFlavor& rData) : m_rData(rData) {} + bool operator() (const css::datatransfer::DataFlavor& rData) const + { + return rData.MimeType == m_rData.MimeType && + rData.DataType == m_rData.DataType; + } + }; +} + +std::vector<css::datatransfer::DataFlavor> GtkTransferable::getTransferDataFlavorsAsVector(GdkAtom *targets, gint n_targets) +{ + std::vector<css::datatransfer::DataFlavor> aVector; + + bool bHaveText = false, bHaveUTF16 = false; + + for (gint i = 0; i < n_targets; ++i) + { + gchar* pName = gdk_atom_name(targets[i]); + const char* pFinalName = pName; + css::datatransfer::DataFlavor aFlavor; + + // omit text/plain;charset=unicode since it is not well defined + if (rtl_str_compare(pName, "text/plain;charset=unicode") == 0) + { + g_free(pName); + continue; + } + + for (size_t j = 0; j < SAL_N_ELEMENTS(aConversionTab); ++j) + { + if (rtl_str_compare(pName, aConversionTab[j].pNativeType) == 0) + { + pFinalName = aConversionTab[j].pType; + break; + } + } + + // There are more non-MIME-types reported that are not translated by + // aConversionTab, like "SAVE_TARGETS", "INTEGER", "ATOM"; just filter + // them out for now before they confuse this code's clients: + if (rtl_str_indexOfChar(pFinalName, '/') == -1) + { + g_free(pName); + continue; + } + + aFlavor.MimeType = OUString(pFinalName, + strlen(pFinalName), + RTL_TEXTENCODING_UTF8); + + m_aMimeTypeToAtom[aFlavor.MimeType] = targets[i]; + + aFlavor.DataType = cppu::UnoType<Sequence< sal_Int8 >>::get(); + + sal_Int32 nIndex(0); + if (aFlavor.MimeType.getToken(0, ';', nIndex) == "text/plain") + { + bHaveText = true; + OUString aToken(aFlavor.MimeType.getToken(0, ';', nIndex)); + if (aToken == "charset=utf-16") + { + bHaveUTF16 = true; + aFlavor.DataType = cppu::UnoType<OUString>::get(); + } + } + aVector.push_back(aFlavor); + g_free(pName); + } + + //If we have text, but no UTF-16 format which is basically the only + //text-format LibreOffice supports for cnp then claim we do and we + //will convert on demand + if (bHaveText && !bHaveUTF16) + { + css::datatransfer::DataFlavor aFlavor; + aFlavor.MimeType = "text/plain;charset=utf-16"; + aFlavor.DataType = cppu::UnoType<OUString>::get(); + aVector.push_back(aFlavor); + } + + return aVector; +} + + +css::uno::Sequence<css::datatransfer::DataFlavor> SAL_CALL GtkTransferable::getTransferDataFlavors() +{ + return comphelper::containerToSequence(getTransferDataFlavorsAsVector()); +} + +sal_Bool SAL_CALL GtkTransferable::isDataFlavorSupported(const css::datatransfer::DataFlavor& rFlavor) +{ + const std::vector<css::datatransfer::DataFlavor> aAll = + getTransferDataFlavorsAsVector(); + + return std::any_of(aAll.begin(), aAll.end(), DataFlavorEq(rFlavor)); +} + +namespace { + +class GtkClipboardTransferable : public GtkTransferable +{ +private: + GdkAtom m_nSelection; +public: + + explicit GtkClipboardTransferable(GdkAtom nSelection) + : m_nSelection(nSelection) + { + } + + /* + * XTransferable + */ + + virtual css::uno::Any SAL_CALL getTransferData(const css::datatransfer::DataFlavor& rFlavor) override + { + GtkClipboard* clipboard = gtk_clipboard_get(m_nSelection); + if (rFlavor.MimeType == "text/plain;charset=utf-16") + { + OUString aStr; + gchar *pText = gtk_clipboard_wait_for_text(clipboard); + if (pText) + aStr = OUString(pText, strlen(pText), RTL_TEXTENCODING_UTF8); + g_free(pText); + css::uno::Any aRet; + aRet <<= aStr.replaceAll("\r\n", "\n"); + return aRet; + } + + auto it = m_aMimeTypeToAtom.find(rFlavor.MimeType); + if (it == m_aMimeTypeToAtom.end()) + return css::uno::Any(); + + css::uno::Any aRet; + GtkSelectionData* data = gtk_clipboard_wait_for_contents(clipboard, + it->second); + if (!data) + { + return css::uno::Any(); + } + gint length; + const guchar *rawdata = gtk_selection_data_get_data_with_length(data, + &length); + Sequence<sal_Int8> aSeq(reinterpret_cast<const sal_Int8*>(rawdata), length); + gtk_selection_data_free(data); + aRet <<= aSeq; + return aRet; + } + + std::vector<css::datatransfer::DataFlavor> getTransferDataFlavorsAsVector() + override + { + std::vector<css::datatransfer::DataFlavor> aVector; + + GtkClipboard* clipboard = gtk_clipboard_get(m_nSelection); + + GdkAtom *targets; + gint n_targets; + if (gtk_clipboard_wait_for_targets(clipboard, &targets, &n_targets)) + { + aVector = GtkTransferable::getTransferDataFlavorsAsVector(targets, n_targets); + g_free(targets); + } + + return aVector; + } +}; + +class VclGtkClipboard : + public cppu::WeakComponentImplHelper< + datatransfer::clipboard::XSystemClipboard, + datatransfer::clipboard::XFlushableClipboard, + XServiceInfo> +{ + GdkAtom m_nSelection; + osl::Mutex m_aMutex; + gulong m_nOwnerChangedSignalId; + ImplSVEvent* m_pSetClipboardEvent; + Reference<css::datatransfer::XTransferable> m_aContents; + Reference<css::datatransfer::clipboard::XClipboardOwner> m_aOwner; + std::vector< Reference<css::datatransfer::clipboard::XClipboardListener> > m_aListeners; + std::vector<GtkTargetEntry> m_aGtkTargets; + VclToGtkHelper m_aConversionHelper; + + DECL_LINK(AsyncSetGtkClipboard, void*, void); +public: + + explicit VclGtkClipboard(GdkAtom nSelection); + virtual ~VclGtkClipboard() override; + + /* + * XServiceInfo + */ + + virtual OUString SAL_CALL getImplementationName() override; + virtual sal_Bool SAL_CALL supportsService( const OUString& ServiceName ) override; + virtual Sequence< OUString > SAL_CALL getSupportedServiceNames() override; + + /* + * XClipboard + */ + + virtual Reference< css::datatransfer::XTransferable > SAL_CALL getContents() override; + + virtual void SAL_CALL setContents( + const Reference< css::datatransfer::XTransferable >& xTrans, + const Reference< css::datatransfer::clipboard::XClipboardOwner >& xClipboardOwner ) override; + + virtual OUString SAL_CALL getName() override; + + /* + * XClipboardEx + */ + + virtual sal_Int8 SAL_CALL getRenderingCapabilities() override; + + /* + * XFlushableClipboard + */ + virtual void SAL_CALL flushClipboard() override; + + /* + * XClipboardNotifier + */ + virtual void SAL_CALL addClipboardListener( + const Reference< css::datatransfer::clipboard::XClipboardListener >& listener ) override; + + virtual void SAL_CALL removeClipboardListener( + const Reference< css::datatransfer::clipboard::XClipboardListener >& listener ) override; + + void ClipboardGet(GtkSelectionData *selection_data, guint info); + void ClipboardClear(); + void OwnerPossiblyChanged(GtkClipboard *clipboard); + void SetGtkClipboard(); + void SyncGtkClipboard(); +}; + +} + +OUString VclGtkClipboard::getImplementationName() +{ + return "com.sun.star.datatransfer.VclGtkClipboard"; +} + +Sequence< OUString > VclGtkClipboard::getSupportedServiceNames() +{ + Sequence<OUString> aRet { "com.sun.star.datatransfer.clipboard.SystemClipboard" }; + return aRet; +} + +sal_Bool VclGtkClipboard::supportsService( const OUString& ServiceName ) +{ + return cppu::supportsService(this, ServiceName); +} + +Reference< css::datatransfer::XTransferable > VclGtkClipboard::getContents() +{ + if (!m_aContents.is()) + { + //tdf#93887 This is the system clipboard/selection. We fetch it when we are not + //the owner of the clipboard and have not already fetched it. + m_aContents = new GtkClipboardTransferable(m_nSelection); + } + + return m_aContents; +} + +void VclGtkClipboard::ClipboardGet(GtkSelectionData *selection_data, guint info) +{ + if (!m_aContents.is()) + return; + // tdf#129809 take a reference in case m_aContents is replaced during this + // call + Reference<datatransfer::XTransferable> xCurrentContents(m_aContents); + m_aConversionHelper.setSelectionData(xCurrentContents, selection_data, info); +} + +namespace +{ + const OString& getPID() + { + static OString sPID; + if (!sPID.getLength()) + { + oslProcessIdentifier aProcessId = 0; + oslProcessInfo info; + info.Size = sizeof (oslProcessInfo); + if (osl_getProcessInfo(nullptr, osl_Process_IDENTIFIER, &info) == osl_Process_E_None) + aProcessId = info.Ident; + sPID = OString::number(aProcessId); + } + return sPID; + } +} + +namespace +{ + void ClipboardGetFunc(GtkClipboard* /*clipboard*/, GtkSelectionData *selection_data, + guint info, + gpointer user_data_or_owner) + { + VclGtkClipboard* pThis = static_cast<VclGtkClipboard*>(user_data_or_owner); + pThis->ClipboardGet(selection_data, info); + } + + void ClipboardClearFunc(GtkClipboard* /*clipboard*/, gpointer user_data_or_owner) + { + VclGtkClipboard* pThis = static_cast<VclGtkClipboard*>(user_data_or_owner); + pThis->ClipboardClear(); + } + + void handle_owner_change(GtkClipboard *clipboard, GdkEvent* /*event*/, gpointer user_data) + { + VclGtkClipboard* pThis = static_cast<VclGtkClipboard*>(user_data); + pThis->OwnerPossiblyChanged(clipboard); + } +} + +void VclGtkClipboard::OwnerPossiblyChanged(GtkClipboard* clipboard) +{ + SyncGtkClipboard(); // tdf#138183 do any pending SetGtkClipboard calls + if (!m_aContents.is()) + return; + + //if gdk_display_supports_selection_notification is not supported, e.g. like + //right now under wayland, then you only get owner-changed notifications at + //opportune times when the selection might have changed. So here + //we see if the selection supports a dummy selection type identifying + //our pid, in which case it's us. + bool bSelf = false; + + //disconnect and reconnect after gtk_clipboard_wait_for_targets to + //avoid possible recursion + g_signal_handler_disconnect(clipboard, m_nOwnerChangedSignalId); + + OString sTunnel = "application/x-libreoffice-internal-id-" + getPID(); + GdkAtom *targets; + gint n_targets; + if (gtk_clipboard_wait_for_targets(clipboard, &targets, &n_targets)) + { + for (gint i = 0; i < n_targets && !bSelf; ++i) + { + gchar* pName = gdk_atom_name(targets[i]); + if (strcmp(pName, sTunnel.getStr()) == 0) + { + bSelf = true; + } + g_free(pName); + } + + g_free(targets); + } + + m_nOwnerChangedSignalId = g_signal_connect(clipboard, "owner-change", + G_CALLBACK(handle_owner_change), this); + + if (!bSelf) + { + //null out m_aContents to return control to the system-one which + //will be retrieved if getContents is called again + setContents(Reference<css::datatransfer::XTransferable>(), + Reference<css::datatransfer::clipboard::XClipboardOwner>()); + } +} + +void VclGtkClipboard::ClipboardClear() +{ + if (m_pSetClipboardEvent) + { + Application::RemoveUserEvent(m_pSetClipboardEvent); + m_pSetClipboardEvent = nullptr; + } + for (auto &a : m_aGtkTargets) + g_free(a.target); + m_aGtkTargets.clear(); +} + +GtkTargetEntry VclToGtkHelper::makeGtkTargetEntry(const css::datatransfer::DataFlavor& rFlavor) +{ + GtkTargetEntry aEntry; + aEntry.target = + g_strdup(OUStringToOString(rFlavor.MimeType, RTL_TEXTENCODING_UTF8).getStr()); + aEntry.flags = 0; + auto it = std::find_if(aInfoToFlavor.begin(), aInfoToFlavor.end(), + DataFlavorEq(rFlavor)); + if (it != aInfoToFlavor.end()) + aEntry.info = std::distance(aInfoToFlavor.begin(), it); + else + { + aEntry.info = aInfoToFlavor.size(); + aInfoToFlavor.push_back(rFlavor); + } + return aEntry; +} + +void VclToGtkHelper::setSelectionData(const Reference<css::datatransfer::XTransferable> &rTrans, + GtkSelectionData *selection_data, guint info) +{ + GdkAtom type(gdk_atom_intern(OUStringToOString(aInfoToFlavor[info].MimeType, + RTL_TEXTENCODING_UTF8).getStr(), + false)); + + css::datatransfer::DataFlavor aFlavor(aInfoToFlavor[info]); + if (aFlavor.MimeType == "UTF8_STRING" || aFlavor.MimeType == "STRING") + aFlavor.MimeType = "text/plain;charset=utf-8"; + + Sequence<sal_Int8> aData; + Any aValue; + + try + { + aValue = rTrans->getTransferData(aFlavor); + } + catch (...) + { + } + + if (aValue.getValueTypeClass() == TypeClass_STRING) + { + OUString aString; + aValue >>= aString; + aData = Sequence< sal_Int8 >( reinterpret_cast<sal_Int8 const *>(aString.getStr()), aString.getLength() * sizeof( sal_Unicode ) ); + } + else if (aValue.getValueType() == cppu::UnoType<Sequence< sal_Int8 >>::get()) + { + aValue >>= aData; + } + else if (aFlavor.MimeType == "text/plain;charset=utf-8") + { + //didn't have utf-8, try utf-16 and convert + aFlavor.MimeType = "text/plain;charset=utf-16"; + aFlavor.DataType = cppu::UnoType<OUString>::get(); + try + { + aValue = rTrans->getTransferData(aFlavor); + } + catch (...) + { + } + OUString aString; + aValue >>= aString; + OString aUTF8String(OUStringToOString(aString, RTL_TEXTENCODING_UTF8)); + gtk_selection_data_set(selection_data, type, 8, + reinterpret_cast<const guchar *>(aUTF8String.getStr()), + aUTF8String.getLength()); + return; + } + + gtk_selection_data_set(selection_data, type, 8, + reinterpret_cast<const guchar *>(aData.getArray()), + aData.getLength()); +} + +VclGtkClipboard::VclGtkClipboard(GdkAtom nSelection) + : cppu::WeakComponentImplHelper<datatransfer::clipboard::XSystemClipboard, + datatransfer::clipboard::XFlushableClipboard, XServiceInfo> + (m_aMutex) + , m_nSelection(nSelection) + , m_pSetClipboardEvent(nullptr) +{ + GtkClipboard* clipboard = gtk_clipboard_get(m_nSelection); + m_nOwnerChangedSignalId = g_signal_connect(clipboard, "owner-change", + G_CALLBACK(handle_owner_change), this); +} + +void VclGtkClipboard::flushClipboard() +{ + SolarMutexGuard aGuard; + + if (GDK_SELECTION_CLIPBOARD != m_nSelection) + return; + + GtkClipboard* clipboard = gtk_clipboard_get(m_nSelection); + gtk_clipboard_store(clipboard); +} + +VclGtkClipboard::~VclGtkClipboard() +{ + GtkClipboard* clipboard = gtk_clipboard_get(m_nSelection); + g_signal_handler_disconnect(clipboard, m_nOwnerChangedSignalId); + if (!m_aGtkTargets.empty()) + { + gtk_clipboard_clear(clipboard); + ClipboardClear(); + } + assert(!m_pSetClipboardEvent); + assert(m_aGtkTargets.empty()); +} + +std::vector<GtkTargetEntry> VclToGtkHelper::FormatsToGtk(const css::uno::Sequence<css::datatransfer::DataFlavor> &rFormats) +{ + std::vector<GtkTargetEntry> aGtkTargets; + + bool bHaveText(false), bHaveUTF8(false); + for (const css::datatransfer::DataFlavor& rFlavor : rFormats) + { + sal_Int32 nIndex(0); + if (rFlavor.MimeType.getToken(0, ';', nIndex) == "text/plain") + { + bHaveText = true; + OUString aToken(rFlavor.MimeType.getToken(0, ';', nIndex)); + if (aToken == "charset=utf-8") + { + bHaveUTF8 = true; + } + } + GtkTargetEntry aEntry(makeGtkTargetEntry(rFlavor)); + aGtkTargets.push_back(aEntry); + } + + if (bHaveText) + { + css::datatransfer::DataFlavor aFlavor; + aFlavor.DataType = cppu::UnoType<Sequence< sal_Int8 >>::get(); + if (!bHaveUTF8) + { + aFlavor.MimeType = "text/plain;charset=utf-8"; + aGtkTargets.push_back(makeGtkTargetEntry(aFlavor)); + } + aFlavor.MimeType = "UTF8_STRING"; + aGtkTargets.push_back(makeGtkTargetEntry(aFlavor)); + aFlavor.MimeType = "STRING"; + aGtkTargets.push_back(makeGtkTargetEntry(aFlavor)); + } + + return aGtkTargets; +} + +IMPL_LINK_NOARG(VclGtkClipboard, AsyncSetGtkClipboard, void*, void) +{ + osl::ClearableMutexGuard aGuard( m_aMutex ); + m_pSetClipboardEvent = nullptr; + SetGtkClipboard(); +} + +void VclGtkClipboard::SyncGtkClipboard() +{ + osl::ClearableMutexGuard aGuard(m_aMutex); + if (m_pSetClipboardEvent) + { + Application::RemoveUserEvent(m_pSetClipboardEvent); + m_pSetClipboardEvent = nullptr; + SetGtkClipboard(); + } +} + +void VclGtkClipboard::SetGtkClipboard() +{ + GtkClipboard* clipboard = gtk_clipboard_get(m_nSelection); + gtk_clipboard_set_with_data(clipboard, m_aGtkTargets.data(), m_aGtkTargets.size(), + ClipboardGetFunc, ClipboardClearFunc, this); + gtk_clipboard_set_can_store(clipboard, m_aGtkTargets.data(), m_aGtkTargets.size()); +} + +void VclGtkClipboard::setContents( + const Reference< css::datatransfer::XTransferable >& xTrans, + const Reference< css::datatransfer::clipboard::XClipboardOwner >& xClipboardOwner ) +{ + css::uno::Sequence<css::datatransfer::DataFlavor> aFormats; + if (xTrans.is()) + { + aFormats = xTrans->getTransferDataFlavors(); + } + + osl::ClearableMutexGuard aGuard( m_aMutex ); + Reference< datatransfer::clipboard::XClipboardOwner > xOldOwner( m_aOwner ); + Reference< datatransfer::XTransferable > xOldContents( m_aContents ); + m_aContents = xTrans; + m_aOwner = xClipboardOwner; + + std::vector< Reference< datatransfer::clipboard::XClipboardListener > > aListeners( m_aListeners ); + datatransfer::clipboard::ClipboardEvent aEv; + + GtkClipboard* clipboard = gtk_clipboard_get(m_nSelection); + if (!m_aGtkTargets.empty()) + { + gtk_clipboard_clear(clipboard); + ClipboardClear(); + } + assert(m_aGtkTargets.empty()); + if (m_aContents.is()) + { + std::vector<GtkTargetEntry> aGtkTargets(m_aConversionHelper.FormatsToGtk(aFormats)); + if (!aGtkTargets.empty()) + { + GtkTargetEntry aEntry; + OString sTunnel = "application/x-libreoffice-internal-id-" + getPID(); + aEntry.target = g_strdup(sTunnel.getStr()); + aEntry.flags = 0; + aEntry.info = 0; + aGtkTargets.push_back(aEntry); + + m_aGtkTargets = aGtkTargets; + + if (!m_pSetClipboardEvent) + m_pSetClipboardEvent = Application::PostUserEvent(LINK(this, VclGtkClipboard, AsyncSetGtkClipboard)); + } + } + + aEv.Contents = getContents(); + + aGuard.clear(); + + if (xOldOwner.is() && xOldOwner != xClipboardOwner) + xOldOwner->lostOwnership( this, xOldContents ); + for (auto const& listener : aListeners) + { + listener->changedContents( aEv ); + } +} + +OUString VclGtkClipboard::getName() +{ + return (m_nSelection == GDK_SELECTION_CLIPBOARD) ? OUString("CLIPBOARD") : OUString("PRIMARY"); +} + +sal_Int8 VclGtkClipboard::getRenderingCapabilities() +{ + return 0; +} + +void VclGtkClipboard::addClipboardListener( const Reference< datatransfer::clipboard::XClipboardListener >& listener ) +{ + osl::ClearableMutexGuard aGuard( m_aMutex ); + + m_aListeners.push_back( listener ); +} + +void VclGtkClipboard::removeClipboardListener( const Reference< datatransfer::clipboard::XClipboardListener >& listener ) +{ + osl::ClearableMutexGuard aGuard( m_aMutex ); + + m_aListeners.erase(std::remove(m_aListeners.begin(), m_aListeners.end(), listener), m_aListeners.end()); +} + +Reference< XInterface > GtkInstance::CreateClipboard(const Sequence< Any >& arguments) +{ + OUString sel; + if (!arguments.hasElements()) { + sel = "CLIPBOARD"; + } else if (arguments.getLength() != 1 || !(arguments[0] >>= sel)) { + throw css::lang::IllegalArgumentException( + "bad GtkInstance::CreateClipboard arguments", + css::uno::Reference<css::uno::XInterface>(), -1); + } + + GdkAtom nSelection = (sel == "CLIPBOARD") ? GDK_SELECTION_CLIPBOARD : GDK_SELECTION_PRIMARY; + + auto it = m_aClipboards.find(nSelection); + if (it != m_aClipboards.end()) + return it->second; + + Reference<XInterface> xClipboard(static_cast<cppu::OWeakObject *>(new VclGtkClipboard(nSelection))); + m_aClipboards[nSelection] = xClipboard; + + return xClipboard; +} + +GtkDropTarget::GtkDropTarget() + : WeakComponentImplHelper(m_aMutex) + , m_pFrame(nullptr) + , m_pFormatConversionRequest(nullptr) + , m_bActive(false) + , m_bInDrag(false) + , m_nDefaultActions(0) +{ +} + +OUString SAL_CALL GtkDropTarget::getImplementationName() +{ + return "com.sun.star.datatransfer.dnd.VclGtkDropTarget"; +} + +sal_Bool SAL_CALL GtkDropTarget::supportsService(OUString const & ServiceName) +{ + return cppu::supportsService(this, ServiceName); +} + +css::uno::Sequence<OUString> SAL_CALL GtkDropTarget::getSupportedServiceNames() +{ + Sequence<OUString> aRet { "com.sun.star.datatransfer.dnd.GtkDropTarget" }; + return aRet; +} + +GtkDropTarget::~GtkDropTarget() +{ + if (m_pFrame) + m_pFrame->deregisterDropTarget(this); +} + +void GtkDropTarget::deinitialize() +{ + m_pFrame = nullptr; + m_bActive = false; +} + +void GtkDropTarget::initialize(const Sequence<Any>& rArguments) +{ + if (rArguments.getLength() < 2) + { + throw RuntimeException("DropTarget::initialize: Cannot install window event handler", + static_cast<OWeakObject*>(this)); + } + + sal_IntPtr nFrame = 0; + rArguments.getConstArray()[1] >>= nFrame; + + if (!nFrame) + { + throw RuntimeException("DropTarget::initialize: missing SalFrame", + static_cast<OWeakObject*>(this)); + } + + m_pFrame = reinterpret_cast<GtkSalFrame*>(nFrame); + m_pFrame->registerDropTarget(this); + m_bActive = true; +} + +void GtkDropTarget::addDropTargetListener( const Reference< css::datatransfer::dnd::XDropTargetListener >& xListener) +{ + ::osl::Guard< ::osl::Mutex > aGuard( m_aMutex ); + + m_aListeners.push_back( xListener ); +} + +void GtkDropTarget::removeDropTargetListener( const Reference< css::datatransfer::dnd::XDropTargetListener >& xListener) +{ + ::osl::Guard< ::osl::Mutex > aGuard( m_aMutex ); + + m_aListeners.erase(std::remove(m_aListeners.begin(), m_aListeners.end(), xListener), m_aListeners.end()); +} + +void GtkDropTarget::fire_drop(const css::datatransfer::dnd::DropTargetDropEvent& dtde) +{ + osl::ClearableGuard<osl::Mutex> aGuard( m_aMutex ); + std::vector<css::uno::Reference<css::datatransfer::dnd::XDropTargetListener>> aListeners(m_aListeners); + aGuard.clear(); + + for (auto const& listener : aListeners) + { + listener->drop( dtde ); + } +} + +void GtkDropTarget::fire_dragEnter(const css::datatransfer::dnd::DropTargetDragEnterEvent& dtde) +{ + osl::ClearableGuard< ::osl::Mutex > aGuard( m_aMutex ); + std::vector<css::uno::Reference<css::datatransfer::dnd::XDropTargetListener>> aListeners(m_aListeners); + aGuard.clear(); + + for (auto const& listener : aListeners) + { + listener->dragEnter( dtde ); + } +} + +void GtkDropTarget::fire_dragOver(const css::datatransfer::dnd::DropTargetDragEvent& dtde) +{ + osl::ClearableGuard< ::osl::Mutex > aGuard( m_aMutex ); + std::vector<css::uno::Reference<css::datatransfer::dnd::XDropTargetListener>> aListeners(m_aListeners); + aGuard.clear(); + + for (auto const& listener : aListeners) + { + listener->dragOver( dtde ); + } +} + +void GtkDropTarget::fire_dragExit(const css::datatransfer::dnd::DropTargetEvent& dte) +{ + osl::ClearableGuard< ::osl::Mutex > aGuard( m_aMutex ); + std::vector<css::uno::Reference<css::datatransfer::dnd::XDropTargetListener>> aListeners(m_aListeners); + aGuard.clear(); + + for (auto const& listener : aListeners) + { + listener->dragExit( dte ); + } +} + +sal_Bool GtkDropTarget::isActive() +{ + return m_bActive; +} + +void GtkDropTarget::setActive(sal_Bool bActive) +{ + m_bActive = bActive; +} + +sal_Int8 GtkDropTarget::getDefaultActions() +{ + return m_nDefaultActions; +} + +void GtkDropTarget::setDefaultActions(sal_Int8 nDefaultActions) +{ + m_nDefaultActions = nDefaultActions; +} + +Reference< XInterface > GtkInstance::CreateDropTarget() +{ + return Reference<XInterface>(static_cast<cppu::OWeakObject*>(new GtkDropTarget)); +} + +GtkDragSource::~GtkDragSource() +{ + if (m_pFrame) + m_pFrame->deregisterDragSource(this); + + if (GtkDragSource::g_ActiveDragSource == this) + { + SAL_WARN( "vcl.gtk", "dragEnd should have been called on GtkDragSource before dtor"); + GtkDragSource::g_ActiveDragSource = nullptr; + } +} + +void GtkDragSource::deinitialize() +{ + m_pFrame = nullptr; +} + +sal_Bool GtkDragSource::isDragImageSupported() +{ + return true; +} + +sal_Int32 GtkDragSource::getDefaultCursor( sal_Int8 ) +{ + return 0; +} + +void GtkDragSource::initialize(const css::uno::Sequence<css::uno::Any >& rArguments) +{ + if (rArguments.getLength() < 2) + { + throw RuntimeException("DragSource::initialize: Cannot install window event handler", + static_cast<OWeakObject*>(this)); + } + + sal_IntPtr nFrame = 0; + rArguments.getConstArray()[1] >>= nFrame; + + if (!nFrame) + { + throw RuntimeException("DragSource::initialize: missing SalFrame", + static_cast<OWeakObject*>(this)); + } + + m_pFrame = reinterpret_cast<GtkSalFrame*>(nFrame); + m_pFrame->registerDragSource(this); +} + +OUString SAL_CALL GtkDragSource::getImplementationName() +{ + return "com.sun.star.datatransfer.dnd.VclGtkDragSource"; +} + +sal_Bool SAL_CALL GtkDragSource::supportsService(OUString const & ServiceName) +{ + return cppu::supportsService(this, ServiceName); +} + +css::uno::Sequence<OUString> SAL_CALL GtkDragSource::getSupportedServiceNames() +{ + Sequence<OUString> aRet { "com.sun.star.datatransfer.dnd.GtkDragSource" }; + return aRet; +} + +Reference< XInterface > GtkInstance::CreateDragSource() +{ + return Reference< XInterface >( static_cast<cppu::OWeakObject *>(new GtkDragSource()) ); +} + +namespace { + +class GtkOpenGLContext : public OpenGLContext +{ + GLWindow m_aGLWin; + GtkWidget *m_pGLArea; + GdkGLContext *m_pContext; + guint m_nAreaFrameBuffer; + guint m_nFrameBuffer; + guint m_nRenderBuffer; + guint m_nDepthBuffer; + guint m_nFrameScratchBuffer; + guint m_nRenderScratchBuffer; + guint m_nDepthScratchBuffer; + +public: + GtkOpenGLContext() + : OpenGLContext() + , m_pGLArea(nullptr) + , m_pContext(nullptr) + , m_nAreaFrameBuffer(0) + , m_nFrameBuffer(0) + , m_nRenderBuffer(0) + , m_nDepthBuffer(0) + , m_nFrameScratchBuffer(0) + , m_nRenderScratchBuffer(0) + , m_nDepthScratchBuffer(0) + { + } + + virtual void initWindow() override + { + if( !m_pChildWindow ) + { + SystemWindowData winData = generateWinData(mpWindow, mbRequestLegacyContext); + m_pChildWindow = VclPtr<SystemChildWindow>::Create(mpWindow, 0, &winData, false); + } + + if (m_pChildWindow) + { + InitChildWindow(m_pChildWindow.get()); + } + } + +private: + virtual const GLWindow& getOpenGLWindow() const override { return m_aGLWin; } + virtual GLWindow& getModifiableOpenGLWindow() override { return m_aGLWin; } + + static void signalDestroy(GtkWidget*, gpointer context) + { + GtkOpenGLContext* pThis = static_cast<GtkOpenGLContext*>(context); + pThis->m_pGLArea = nullptr; + } + + static gboolean signalRender(GtkGLArea*, GdkGLContext*, gpointer window) + { + GtkOpenGLContext* pThis = static_cast<GtkOpenGLContext*>(window); + + int scale = gtk_widget_get_scale_factor(pThis->m_pGLArea); + int width = pThis->m_aGLWin.Width * scale; + int height = pThis->m_aGLWin.Height * scale; + + glDrawBuffer(GL_COLOR_ATTACHMENT0_EXT); + + glBindFramebuffer(GL_READ_FRAMEBUFFER, pThis->m_nAreaFrameBuffer); + glReadBuffer(GL_COLOR_ATTACHMENT0_EXT); + + glBlitFramebuffer(0, 0, width, height, 0, 0, width, height, + GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT, GL_NEAREST); + + gdk_gl_context_make_current(pThis->m_pContext); + return true; + } + + virtual void adjustToNewSize() override + { + if (!m_pGLArea) + return; + + int scale = gtk_widget_get_scale_factor(m_pGLArea); + int width = m_aGLWin.Width * scale; + int height = m_aGLWin.Height * scale; + + // seen in tdf#124729 width/height of 0 leading to GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT + int allocwidth = std::max(width, 1); + int allocheight = std::max(height, 1); + + gtk_gl_area_make_current(GTK_GL_AREA(m_pGLArea)); + if (GError *pError = gtk_gl_area_get_error(GTK_GL_AREA(m_pGLArea))) + { + SAL_WARN("vcl.gtk", "gtk gl area error: " << pError->message); + return; + } + + glBindRenderbuffer(GL_RENDERBUFFER, m_nRenderBuffer); + glRenderbufferStorage(GL_RENDERBUFFER, GL_RGB8, allocwidth, allocheight); + glBindRenderbuffer(GL_RENDERBUFFER, m_nDepthBuffer); + glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, allocwidth, allocheight); + glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, m_nAreaFrameBuffer); + + glFramebufferRenderbufferEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, + GL_RENDERBUFFER_EXT, m_nRenderBuffer); + glFramebufferRenderbufferEXT(GL_FRAMEBUFFER_EXT, GL_DEPTH_ATTACHMENT_EXT, + GL_RENDERBUFFER_EXT, m_nDepthBuffer); + + gdk_gl_context_make_current(m_pContext); + glBindRenderbuffer(GL_RENDERBUFFER, m_nRenderBuffer); + glBindRenderbuffer(GL_RENDERBUFFER, m_nDepthBuffer); + glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, m_nFrameBuffer); + + glFramebufferRenderbufferEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, + GL_RENDERBUFFER_EXT, m_nRenderBuffer); + glFramebufferRenderbufferEXT(GL_FRAMEBUFFER_EXT, GL_DEPTH_ATTACHMENT_EXT, + GL_RENDERBUFFER_EXT, m_nDepthBuffer); + glViewport(0, 0, width, height); + + glBindRenderbuffer(GL_RENDERBUFFER, m_nRenderScratchBuffer); + glRenderbufferStorage(GL_RENDERBUFFER, GL_RGB8, allocwidth, allocheight); + glBindRenderbuffer(GL_RENDERBUFFER, m_nDepthScratchBuffer); + glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, allocwidth, allocheight); + glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, m_nFrameScratchBuffer); + + glFramebufferRenderbufferEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, + GL_RENDERBUFFER_EXT, m_nRenderScratchBuffer); + glFramebufferRenderbufferEXT(GL_FRAMEBUFFER_EXT, GL_DEPTH_ATTACHMENT_EXT, + GL_RENDERBUFFER_EXT, m_nDepthScratchBuffer); + + glViewport(0, 0, width, height); + } + + virtual bool ImplInit() override + { + const SystemEnvData* pEnvData = m_pChildWindow->GetSystemData(); + GtkWidget *pParent = static_cast<GtkWidget*>(pEnvData->pWidget); + m_pGLArea = gtk_gl_area_new(); + g_signal_connect(G_OBJECT(m_pGLArea), "destroy", G_CALLBACK(signalDestroy), this); + g_signal_connect(G_OBJECT(m_pGLArea), "render", G_CALLBACK(signalRender), this); + gtk_gl_area_set_has_depth_buffer(GTK_GL_AREA(m_pGLArea), true); + gtk_gl_area_set_auto_render(GTK_GL_AREA(m_pGLArea), false); + gtk_widget_set_hexpand(m_pGLArea, true); + gtk_widget_set_vexpand(m_pGLArea, true); + gtk_container_add(GTK_CONTAINER(pParent), m_pGLArea); + gtk_widget_show_all(pParent); + + gtk_gl_area_make_current(GTK_GL_AREA(m_pGLArea)); + if (GError *pError = gtk_gl_area_get_error(GTK_GL_AREA(m_pGLArea))) + { + SAL_WARN("vcl.gtk", "gtk gl area error: " << pError->message); + return false; + } + + gtk_gl_area_attach_buffers(GTK_GL_AREA(m_pGLArea)); + glGenFramebuffersEXT(1, &m_nAreaFrameBuffer); + + GdkWindow *pWindow = gtk_widget_get_window(pParent); + m_pContext = gdk_window_create_gl_context(pWindow, nullptr); + if (!m_pContext) + return false; + + if (!gdk_gl_context_realize(m_pContext, nullptr)) + return false; + + gdk_gl_context_make_current(m_pContext); + glGenFramebuffersEXT(1, &m_nFrameBuffer); + glGenRenderbuffersEXT(1, &m_nRenderBuffer); + glGenRenderbuffersEXT(1, &m_nDepthBuffer); + glGenFramebuffersEXT(1, &m_nFrameScratchBuffer); + glGenRenderbuffersEXT(1, &m_nRenderScratchBuffer); + glGenRenderbuffersEXT(1, &m_nDepthScratchBuffer); + + bool bRet = InitGL(); + InitGLDebugging(); + return bRet; + } + + virtual void restoreDefaultFramebuffer() override + { + OpenGLContext::restoreDefaultFramebuffer(); + glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, m_nFrameScratchBuffer); + glFramebufferRenderbufferEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, + GL_RENDERBUFFER_EXT, m_nRenderScratchBuffer); + } + + virtual void makeCurrent() override + { + if (isCurrent()) + return; + + clearCurrent(); + + if (m_pGLArea) + { + int scale = gtk_widget_get_scale_factor(m_pGLArea); + int width = m_aGLWin.Width * scale; + int height = m_aGLWin.Height * scale; + + gdk_gl_context_make_current(m_pContext); + + glBindRenderbuffer(GL_RENDERBUFFER, m_nRenderScratchBuffer); + glBindRenderbuffer(GL_RENDERBUFFER, m_nDepthScratchBuffer); + glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, m_nFrameScratchBuffer); + glFramebufferRenderbufferEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, + GL_RENDERBUFFER_EXT, m_nRenderScratchBuffer); + glFramebufferRenderbufferEXT(GL_FRAMEBUFFER_EXT, GL_DEPTH_ATTACHMENT_EXT, + GL_RENDERBUFFER_EXT, m_nDepthScratchBuffer); + glViewport(0, 0, width, height); + } + + registerAsCurrent(); + } + + virtual void destroyCurrentContext() override + { + gdk_gl_context_clear_current(); + } + + virtual bool isCurrent() override + { + return m_pGLArea && gdk_gl_context_get_current() == m_pContext; + } + + virtual void sync() override + { + } + + virtual void resetCurrent() override + { + clearCurrent(); + gdk_gl_context_clear_current(); + } + + virtual void swapBuffers() override + { + int scale = gtk_widget_get_scale_factor(m_pGLArea); + int width = m_aGLWin.Width * scale; + int height = m_aGLWin.Height * scale; + + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, m_nFrameBuffer); + glDrawBuffer(GL_COLOR_ATTACHMENT0_EXT); + + glBindFramebuffer(GL_READ_FRAMEBUFFER, m_nFrameScratchBuffer); + glReadBuffer(GL_COLOR_ATTACHMENT0_EXT); + + glBlitFramebuffer(0, 0, width, height, 0, 0, width, height, + GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT, GL_NEAREST); + + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, m_nFrameScratchBuffer); + glDrawBuffer(GL_COLOR_ATTACHMENT0_EXT); + + gtk_gl_area_queue_render(GTK_GL_AREA(m_pGLArea)); + BuffersSwapped(); + } + + virtual ~GtkOpenGLContext() override + { + if (m_pContext) + { + g_clear_object(&m_pContext); + } + } +}; + +} + +OpenGLContext* GtkInstance::CreateOpenGLContext() +{ + return new GtkOpenGLContext; +} + +// tdf#123800 avoid requiring wayland at runtime just because it existed at buildtime +bool DLSYM_GDK_IS_WAYLAND_DISPLAY(GdkDisplay* pDisplay) +{ + static auto get_type = reinterpret_cast<GType (*) (void)>(dlsym(nullptr, "gdk_wayland_display_get_type")); + if (!get_type) + return false; + return G_TYPE_CHECK_INSTANCE_TYPE(pDisplay, get_type()); +} + +bool DLSYM_GDK_IS_X11_DISPLAY(GdkDisplay* pDisplay) +{ + static auto get_type = reinterpret_cast<GType (*) (void)>(dlsym(nullptr, "gdk_x11_display_get_type")); + if (!get_type) + return false; + return G_TYPE_CHECK_INSTANCE_TYPE(pDisplay, get_type()); +} + +namespace +{ + +class GtkInstanceBuilder; + + void set_help_id(const GtkWidget *pWidget, const OString& rHelpId) + { + gchar *helpid = g_strdup(rHelpId.getStr()); + g_object_set_data_full(G_OBJECT(pWidget), "g-lo-helpid", helpid, g_free); + } + + OString get_help_id(const GtkWidget *pWidget) + { + void* pData = g_object_get_data(G_OBJECT(pWidget), "g-lo-helpid"); + const gchar* pStr = static_cast<const gchar*>(pData); + return OString(pStr, pStr ? strlen(pStr) : 0); + } + + KeyEvent GtkToVcl(const GdkEventKey& rEvent) + { + sal_uInt16 nKeyCode = GtkSalFrame::GetKeyCode(rEvent.keyval); + if (nKeyCode == 0) + { + guint updated_keyval = GtkSalFrame::GetKeyValFor(gdk_keymap_get_default(), rEvent.hardware_keycode, rEvent.group); + nKeyCode = GtkSalFrame::GetKeyCode(updated_keyval); + } + nKeyCode |= GtkSalFrame::GetKeyModCode(rEvent.state); + return KeyEvent(gdk_keyval_to_unicode(rEvent.keyval), nKeyCode, 0); + } +} + +static MouseEventModifiers ImplGetMouseButtonMode(sal_uInt16 nButton, sal_uInt16 nCode) +{ + MouseEventModifiers nMode = MouseEventModifiers::NONE; + if ( nButton == MOUSE_LEFT ) + nMode |= MouseEventModifiers::SIMPLECLICK; + if ( (nButton == MOUSE_LEFT) && !(nCode & (MOUSE_MIDDLE | MOUSE_RIGHT)) ) + nMode |= MouseEventModifiers::SELECT; + if ( (nButton == MOUSE_LEFT) && (nCode & KEY_MOD1) && + !(nCode & (MOUSE_MIDDLE | MOUSE_RIGHT | KEY_SHIFT)) ) + nMode |= MouseEventModifiers::MULTISELECT; + if ( (nButton == MOUSE_LEFT) && (nCode & KEY_SHIFT) && + !(nCode & (MOUSE_MIDDLE | MOUSE_RIGHT | KEY_MOD1)) ) + nMode |= MouseEventModifiers::RANGESELECT; + return nMode; +} + +static MouseEventModifiers ImplGetMouseMoveMode(sal_uInt16 nCode) +{ + MouseEventModifiers nMode = MouseEventModifiers::NONE; + if ( !nCode ) + nMode |= MouseEventModifiers::SIMPLEMOVE; + if ( (nCode & MOUSE_LEFT) && !(nCode & KEY_MOD1) ) + nMode |= MouseEventModifiers::DRAGMOVE; + if ( (nCode & MOUSE_LEFT) && (nCode & KEY_MOD1) ) + nMode |= MouseEventModifiers::DRAGCOPY; + return nMode; +} + +namespace +{ + bool SwapForRTL(GtkWidget* pWidget) + { + GtkTextDirection eDir = gtk_widget_get_direction(pWidget); + if (eDir == GTK_TEXT_DIR_RTL) + return true; + if (eDir == GTK_TEXT_DIR_LTR) + return false; + return AllSettings::GetLayoutRTL(); + } + + void replaceWidget(GtkWidget* pWidget, GtkWidget* pReplacement) + { + // remove the widget and replace it with pReplacement + GtkWidget* pParent = gtk_widget_get_parent(pWidget); + + // if pWidget was un-parented then don't bother + if (!pParent) + return; + + g_object_ref(pWidget); + + gint nTopAttach(0), nLeftAttach(0), nHeight(1), nWidth(1); + if (GTK_IS_GRID(pParent)) + { + gtk_container_child_get(GTK_CONTAINER(pParent), pWidget, + "left-attach", &nTopAttach, + "top-attach", &nLeftAttach, + "width", &nWidth, + "height", &nHeight, + nullptr); + } + + gboolean bExpand(false), bFill(false); + GtkPackType ePackType(GTK_PACK_START); + guint nPadding(0); + gint nPosition(0); + if (GTK_IS_BOX(pParent)) + { + gtk_container_child_get(GTK_CONTAINER(pParent), pWidget, + "expand", &bExpand, + "fill", &bFill, + "pack-type", &ePackType, + "padding", &nPadding, + "position", &nPosition, + nullptr); + } + + gtk_container_remove(GTK_CONTAINER(pParent), pWidget); + + gtk_widget_set_visible(pReplacement, gtk_widget_get_visible(pWidget)); + gtk_widget_set_no_show_all(pReplacement, gtk_widget_get_no_show_all(pWidget)); + + int nReqWidth, nReqHeight; + gtk_widget_get_size_request(pWidget, &nReqWidth, &nReqHeight); + gtk_widget_set_size_request(pReplacement, nReqWidth, nReqHeight); + + static GQuark quark_size_groups = g_quark_from_static_string("gtk-widget-size-groups"); + GSList* pSizeGroups = static_cast<GSList*>(g_object_get_qdata(G_OBJECT(pWidget), quark_size_groups)); + while (pSizeGroups) + { + GtkSizeGroup *pSizeGroup = static_cast<GtkSizeGroup*>(pSizeGroups->data); + pSizeGroups = pSizeGroups->next; + gtk_size_group_remove_widget(pSizeGroup, pWidget); + gtk_size_group_add_widget(pSizeGroup, pReplacement); + } + + // tdf#135368 change the mnemonic to point to our replacement + GList* pLabels = gtk_widget_list_mnemonic_labels(pWidget); + for (GList* pLabel = g_list_first(pLabels); pLabel; pLabel = g_list_next(pLabel)) + { + GtkWidget* pLabelWidget = static_cast<GtkWidget*>(pLabel->data); + if (!GTK_IS_LABEL(pLabelWidget)) + continue; + gtk_label_set_mnemonic_widget(GTK_LABEL(pLabelWidget), pReplacement); + } + g_list_free(pLabels); + + gtk_container_add(GTK_CONTAINER(pParent), pReplacement); + + if (GTK_IS_GRID(pParent)) + { + gtk_container_child_set(GTK_CONTAINER(pParent), pReplacement, + "left-attach", nTopAttach, + "top-attach", nLeftAttach, + "width", nWidth, + "height", nHeight, + nullptr); + } + + if (GTK_IS_BOX(pParent)) + { + gtk_container_child_set(GTK_CONTAINER(pParent), pReplacement, + "expand", bExpand, + "fill", bFill, + "pack-type", ePackType, + "padding", nPadding, + "position", nPosition, + nullptr); + } + + if (gtk_widget_get_hexpand_set(pWidget)) + gtk_widget_set_hexpand(pReplacement, gtk_widget_get_hexpand(pWidget)); + + if (gtk_widget_get_vexpand_set(pWidget)) + gtk_widget_set_vexpand(pReplacement, gtk_widget_get_vexpand(pWidget)); + + gtk_widget_set_halign(pReplacement, gtk_widget_get_halign(pWidget)); + gtk_widget_set_valign(pReplacement, gtk_widget_get_valign(pWidget)); + + g_object_unref(pWidget); + } + + void insertAsParent(GtkWidget* pWidget, GtkWidget* pReplacement) + { + g_object_ref(pWidget); + + replaceWidget(pWidget, pReplacement); + + gtk_container_add(GTK_CONTAINER(pReplacement), pWidget); + + g_object_unref(pWidget); + } + + GtkWidget* ensureEventWidget(GtkWidget* pWidget) + { + if (!pWidget) + return nullptr; + + GtkWidget* pMouseEventBox; + // not every widget has a GdkWindow and can get any event, so if we + // want an event it doesn't have, insert a GtkEventBox so we can get + // those + if (gtk_widget_get_has_window(pWidget)) + pMouseEventBox = pWidget; + else + { + // remove the widget and replace it with an eventbox and put the old + // widget into it + pMouseEventBox = gtk_event_box_new(); + gtk_event_box_set_above_child(GTK_EVENT_BOX(pMouseEventBox), false); + gtk_event_box_set_visible_window(GTK_EVENT_BOX(pMouseEventBox), false); + insertAsParent(pWidget, pMouseEventBox); + } + + return pMouseEventBox; + } +} + +namespace { + +GdkDragAction VclToGdk(sal_Int8 dragOperation) +{ + GdkDragAction eRet(static_cast<GdkDragAction>(0)); + if (dragOperation & css::datatransfer::dnd::DNDConstants::ACTION_COPY) + eRet = static_cast<GdkDragAction>(eRet | GDK_ACTION_COPY); + if (dragOperation & css::datatransfer::dnd::DNDConstants::ACTION_MOVE) + eRet = static_cast<GdkDragAction>(eRet | GDK_ACTION_MOVE); + if (dragOperation & css::datatransfer::dnd::DNDConstants::ACTION_LINK) + eRet = static_cast<GdkDragAction>(eRet | GDK_ACTION_LINK); + return eRet; +} + +class GtkInstanceWidget : public virtual weld::Widget +{ +protected: + GtkWidget* m_pWidget; + GtkWidget* m_pMouseEventBox; + GtkInstanceBuilder* m_pBuilder; + + DECL_LINK(async_signal_focus_in, void*, void); + DECL_LINK(async_signal_focus_out, void*, void); + DECL_LINK(async_drag_cancel, void*, void); + + void launch_signal_focus_in() + { + // in e.g. function wizard RefEdits we want to select all when we get focus + // but there are pending gtk handlers which change selection after our handler + // post our focus in event to happen after those finish + if (m_pFocusInEvent) + Application::RemoveUserEvent(m_pFocusInEvent); + m_pFocusInEvent = Application::PostUserEvent(LINK(this, GtkInstanceWidget, async_signal_focus_in)); + } + + static gboolean signalFocusIn(GtkWidget*, GdkEvent*, gpointer widget) + { + GtkInstanceWidget* pThis = static_cast<GtkInstanceWidget*>(widget); + pThis->launch_signal_focus_in(); + return false; + } + + void signal_focus_in() + { + m_aFocusInHdl.Call(*this); + } + + static gboolean signalMnemonicActivate(GtkWidget*, gboolean, gpointer widget) + { + GtkInstanceWidget* pThis = static_cast<GtkInstanceWidget*>(widget); + SolarMutexGuard aGuard; + return pThis->signal_mnemonic_activate(); + } + + bool signal_mnemonic_activate() + { + return m_aMnemonicActivateHdl.Call(*this); + } + + void launch_signal_focus_out() + { + // tdf#127262 because focus in is async, focus out must not appear out + // of sequence to focus in + if (m_pFocusOutEvent) + Application::RemoveUserEvent(m_pFocusOutEvent); + m_pFocusOutEvent = Application::PostUserEvent(LINK(this, GtkInstanceWidget, async_signal_focus_out)); + } + + static gboolean signalFocusOut(GtkWidget*, GdkEvent*, gpointer widget) + { + GtkInstanceWidget* pThis = static_cast<GtkInstanceWidget*>(widget); + SolarMutexGuard aGuard; + pThis->launch_signal_focus_out(); + return false; + } + + void launch_drag_cancel(GdkDragContext* context) + { + // post our drag cancel to happen at the next available event cycle + if (m_pDragCancelEvent) + return; + g_object_ref(context); + m_pDragCancelEvent = Application::PostUserEvent(LINK(this, GtkInstanceWidget, async_drag_cancel), context); + } + + void signal_focus_out() + { + m_aFocusOutHdl.Call(*this); + } + + void ensureEventWidget() + { + if (!m_pMouseEventBox) + m_pMouseEventBox = ::ensureEventWidget(m_pWidget); + } + + void ensureButtonPressSignal() + { + if (!m_nButtonPressSignalId) + { + ensureEventWidget(); + m_nButtonPressSignalId = g_signal_connect(m_pMouseEventBox, "button-press-event", G_CALLBACK(signalButton), this); + } + } + + static gboolean signalPopupMenu(GtkWidget* pWidget, gpointer widget) + { + GtkInstanceWidget* pThis = static_cast<GtkInstanceWidget*>(widget); + SolarMutexGuard aGuard; + //center it when we don't know where else to use + Point aPos(gtk_widget_get_allocated_width(pWidget) / 2, + gtk_widget_get_allocated_height(pWidget) / 2); + CommandEvent aCEvt(aPos, CommandEventId::ContextMenu, false); + return pThis->signal_popup_menu(aCEvt); + } + + bool SwapForRTL() const + { + return ::SwapForRTL(m_pWidget); + } + + void do_enable_drag_source(rtl::Reference<TransferDataContainer>& rHelper, sal_uInt8 eDNDConstants) + { + css::uno::Reference<css::datatransfer::XTransferable> xTrans(rHelper.get()); + css::uno::Reference<css::datatransfer::dnd::XDragSourceListener> xListener(rHelper.get()); + + ensure_drag_source(); + + auto aFormats = xTrans->getTransferDataFlavors(); + std::vector<GtkTargetEntry> aGtkTargets(m_xDragSource->FormatsToGtk(aFormats)); + + m_eDragAction = VclToGdk(eDNDConstants); + drag_source_set(aGtkTargets, m_eDragAction); + + for (auto &a : aGtkTargets) + g_free(a.target); + + m_xDragSource->set_datatransfer(xTrans, xListener); + } + + void localizeDecimalSeparator() + { + // tdf#128867 if localize decimal separator is active we will always + // need to be able to change the output of the decimal key press + if (!m_nKeyPressSignalId && Application::GetSettings().GetMiscSettings().GetEnableLocalizedDecimalSep()) + m_nKeyPressSignalId = g_signal_connect(m_pWidget, "key-press-event", G_CALLBACK(signalKey), this); + } + + void ensure_drag_begin_end() + { + if (!m_nDragBeginSignalId) + { + // using "after" due to https://gitlab.gnome.org/GNOME/pygobject/issues/251 + m_nDragBeginSignalId = g_signal_connect_after(m_pWidget, "drag-begin", G_CALLBACK(signalDragBegin), this); + } + if (!m_nDragEndSignalId) + m_nDragEndSignalId = g_signal_connect(m_pWidget, "drag-end", G_CALLBACK(signalDragEnd), this); + } + +private: + bool m_bTakeOwnership; + bool m_bDraggedOver; + sal_uInt16 m_nLastMouseButton; + sal_uInt16 m_nLastMouseClicks; + int m_nPressedButton; + int m_nPressStartX; + int m_nPressStartY; + ImplSVEvent* m_pFocusInEvent; + ImplSVEvent* m_pFocusOutEvent; + ImplSVEvent* m_pDragCancelEvent; + GtkCssProvider* m_pBgCssProvider; + GdkDragAction m_eDragAction; + gulong m_nFocusInSignalId; + gulong m_nMnemonicActivateSignalId; + gulong m_nFocusOutSignalId; + gulong m_nKeyPressSignalId; + gulong m_nKeyReleaseSignalId; + gulong m_nSizeAllocateSignalId; + gulong m_nButtonPressSignalId; + gulong m_nMotionSignalId; + gulong m_nLeaveSignalId; + gulong m_nEnterSignalId; + gulong m_nButtonReleaseSignalId; + gulong m_nDragMotionSignalId; + gulong m_nDragDropSignalId; + gulong m_nDragDropReceivedSignalId; + gulong m_nDragLeaveSignalId; + gulong m_nDragBeginSignalId; + gulong m_nDragEndSignalId; + gulong m_nDragFailedSignalId; + gulong m_nDragDataDeleteignalId; + gulong m_nDragGetSignalId; + + rtl::Reference<GtkDropTarget> m_xDropTarget; + rtl::Reference<GtkDragSource> m_xDragSource; + + static void signalSizeAllocate(GtkWidget*, GdkRectangle* allocation, gpointer widget) + { + GtkInstanceWidget* pThis = static_cast<GtkInstanceWidget*>(widget); + SolarMutexGuard aGuard; + pThis->signal_size_allocate(allocation->width, allocation->height); + } + + static gboolean signalKey(GtkWidget*, GdkEventKey* pEvent, gpointer widget) + { + // #i1820# use locale specific decimal separator + if (pEvent->keyval == GDK_KEY_KP_Decimal && Application::GetSettings().GetMiscSettings().GetEnableLocalizedDecimalSep()) + { + OUString aSep(Application::GetSettings().GetLocaleDataWrapper().getNumDecimalSep()); + pEvent->keyval = aSep[0]; + } + + GtkInstanceWidget* pThis = static_cast<GtkInstanceWidget*>(widget); + return pThis->signal_key(pEvent); + } + + virtual bool signal_popup_menu(const CommandEvent&) + { + return false; + } + + static gboolean signalButton(GtkWidget*, GdkEventButton* pEvent, gpointer widget) + { + GtkInstanceWidget* pThis = static_cast<GtkInstanceWidget*>(widget); + SolarMutexGuard aGuard; + return pThis->signal_button(pEvent); + } + + bool signal_button(GdkEventButton* pEvent) + { + m_nPressedButton = -1; + + Point aPos(pEvent->x, pEvent->y); + if (SwapForRTL()) + aPos.setX(gtk_widget_get_allocated_width(m_pWidget) - 1 - aPos.X()); + + if (gdk_event_triggers_context_menu(reinterpret_cast<GdkEvent*>(pEvent)) && pEvent->type == GDK_BUTTON_PRESS) + { + //if handled for context menu, stop processing + CommandEvent aCEvt(aPos, CommandEventId::ContextMenu, true); + if (signal_popup_menu(aCEvt)) + return true; + } + + if (!m_aMousePressHdl.IsSet() && !m_aMouseReleaseHdl.IsSet()) + return false; + + SalEvent nEventType = SalEvent::NONE; + switch (pEvent->type) + { + case GDK_BUTTON_PRESS: + if (GdkEvent* pPeekEvent = gdk_event_peek()) + { + bool bSkip = pPeekEvent->type == GDK_2BUTTON_PRESS || + pPeekEvent->type == GDK_3BUTTON_PRESS; + gdk_event_free(pPeekEvent); + if (bSkip) + { + return false; + } + } + nEventType = SalEvent::MouseButtonDown; + m_nLastMouseClicks = 1; + break; + case GDK_2BUTTON_PRESS: + m_nLastMouseClicks = 2; + nEventType = SalEvent::MouseButtonDown; + break; + case GDK_3BUTTON_PRESS: + m_nLastMouseClicks = 3; + nEventType = SalEvent::MouseButtonDown; + break; + case GDK_BUTTON_RELEASE: + nEventType = SalEvent::MouseButtonUp; + break; + default: + return false; + } + + switch (pEvent->button) + { + case 1: + m_nLastMouseButton = MOUSE_LEFT; + break; + case 2: + m_nLastMouseButton = MOUSE_MIDDLE; + break; + case 3: + m_nLastMouseButton = MOUSE_RIGHT; + break; + default: + return false; + } + + /* Save press to possibly begin a drag */ + if (pEvent->type != GDK_BUTTON_RELEASE) + { + m_nPressedButton = pEvent->button; + m_nPressStartX = pEvent->x; + m_nPressStartY = pEvent->y; + } + + sal_uInt32 nModCode = GtkSalFrame::GetMouseModCode(pEvent->state); + // strip out which buttons are involved from the nModCode and replace with m_nLastMouseButton + sal_uInt16 nCode = m_nLastMouseButton | (nModCode & (KEY_SHIFT | KEY_MOD1 | KEY_MOD2)); + MouseEvent aMEvt(aPos, m_nLastMouseClicks, ImplGetMouseButtonMode(m_nLastMouseButton, nModCode), nCode, nCode); + + if (nEventType == SalEvent::MouseButtonDown) + { + if (!m_aMousePressHdl.IsSet()) + return false; + return m_aMousePressHdl.Call(aMEvt); + } + + if (!m_aMouseReleaseHdl.IsSet()) + return false; + return m_aMouseReleaseHdl.Call(aMEvt); + } + + static gboolean signalMotion(GtkWidget*, GdkEventMotion* pEvent, gpointer widget) + { + GtkInstanceWidget* pThis = static_cast<GtkInstanceWidget*>(widget); + SolarMutexGuard aGuard; + return pThis->signal_motion(pEvent); + } + + bool signal_motion(const GdkEventMotion* pEvent) + { + GtkTargetList* pDragData = (m_eDragAction != 0 && m_nPressedButton != -1 && m_xDragSource.is()) ? gtk_drag_source_get_target_list(m_pWidget) : nullptr; + bool bUnsetDragIcon(false); + if (pDragData && gtk_drag_check_threshold(m_pWidget, m_nPressStartX, m_nPressStartY, pEvent->x, pEvent->y) && !do_signal_drag_begin(bUnsetDragIcon)) + { + GdkDragContext* pContext = gtk_drag_begin_with_coordinates(m_pWidget, + pDragData, + m_eDragAction, + m_nPressedButton, + const_cast<GdkEvent*>(reinterpret_cast<const GdkEvent*>(pEvent)), + m_nPressStartX, m_nPressStartY); + + if (pContext && bUnsetDragIcon) + { + cairo_surface_t *surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, 0, 0); + gtk_drag_set_icon_surface(pContext, surface); + } + + m_nPressedButton = -1; + return false; + } + + if (!m_aMouseMotionHdl.IsSet()) + return false; + + Point aPos(pEvent->x, pEvent->y); + if (SwapForRTL()) + aPos.setX(gtk_widget_get_allocated_width(m_pWidget) - 1 - aPos.X()); + sal_uInt32 nModCode = GtkSalFrame::GetMouseModCode(pEvent->state); + MouseEvent aMEvt(aPos, 0, ImplGetMouseMoveMode(nModCode), nModCode, nModCode); + + m_aMouseMotionHdl.Call(aMEvt); + return true; + } + + static gboolean signalCrossing(GtkWidget*, GdkEventCrossing* pEvent, gpointer widget) + { + GtkInstanceWidget* pThis = static_cast<GtkInstanceWidget*>(widget); + SolarMutexGuard aGuard; + return pThis->signal_crossing(pEvent); + } + + bool signal_crossing(const GdkEventCrossing* pEvent) + { + if (!m_aMouseMotionHdl.IsSet()) + return false; + + Point aPos(pEvent->x, pEvent->y); + if (SwapForRTL()) + aPos.setX(gtk_widget_get_allocated_width(m_pWidget) - 1 - aPos.X()); + sal_uInt32 nModCode = GtkSalFrame::GetMouseModCode(pEvent->state); + MouseEventModifiers eModifiers = ImplGetMouseMoveMode(nModCode); + eModifiers = eModifiers | (pEvent->type == GDK_ENTER_NOTIFY ? MouseEventModifiers::ENTERWINDOW : MouseEventModifiers::LEAVEWINDOW); + MouseEvent aMEvt(aPos, 0, eModifiers, nModCode, nModCode); + + m_aMouseMotionHdl.Call(aMEvt); + return true; + } + + virtual void drag_started() + { + } + + static gboolean signalDragMotion(GtkWidget *pWidget, GdkDragContext *context, gint x, gint y, guint time, gpointer widget) + { + GtkInstanceWidget* pThis = static_cast<GtkInstanceWidget*>(widget); + if (!pThis->m_bDraggedOver) + { + pThis->m_bDraggedOver = true; + pThis->drag_started(); + } + return pThis->m_xDropTarget->signalDragMotion(pWidget, context, x, y, time); + } + + static gboolean signalDragDrop(GtkWidget* pWidget, GdkDragContext* context, gint x, gint y, guint time, gpointer widget) + { + GtkInstanceWidget* pThis = static_cast<GtkInstanceWidget*>(widget); + return pThis->m_xDropTarget->signalDragDrop(pWidget, context, x, y, time); + } + + static void signalDragDropReceived(GtkWidget* pWidget, GdkDragContext* context, gint x, gint y, GtkSelectionData* data, guint ttype, guint time, gpointer widget) + { + GtkInstanceWidget* pThis = static_cast<GtkInstanceWidget*>(widget); + pThis->m_xDropTarget->signalDragDropReceived(pWidget, context, x, y, data, ttype, time); + } + + virtual void drag_ended() + { + } + + static void signalDragLeave(GtkWidget *pWidget, GdkDragContext *context, guint time, gpointer widget) + { + GtkInstanceWidget* pThis = static_cast<GtkInstanceWidget*>(widget); + pThis->m_xDropTarget->signalDragLeave(pWidget, context, time); + if (pThis->m_bDraggedOver) + { + pThis->m_bDraggedOver = false; + pThis->drag_ended(); + } + } + + static void signalDragBegin(GtkWidget*, GdkDragContext* context, gpointer widget) + { + GtkInstanceWidget* pThis = static_cast<GtkInstanceWidget*>(widget); + pThis->signal_drag_begin(context); + } + + void ensure_drag_source() + { + if (!m_xDragSource) + { + m_xDragSource.set(new GtkDragSource); + + m_nDragFailedSignalId = g_signal_connect(m_pWidget, "drag-failed", G_CALLBACK(signalDragFailed), this); + m_nDragDataDeleteignalId = g_signal_connect(m_pWidget, "drag-data-delete", G_CALLBACK(signalDragDelete), this); + m_nDragGetSignalId = g_signal_connect(m_pWidget, "drag-data-get", G_CALLBACK(signalDragDataGet), this); + + ensure_drag_begin_end(); + } + } + + virtual bool do_signal_drag_begin(bool& rUnsetDragIcon) + { + rUnsetDragIcon = false; + return false; + } + + void signal_drag_begin(GdkDragContext* context) + { + bool bUnsetDragIcon(false); + if (do_signal_drag_begin(bUnsetDragIcon)) + { + launch_drag_cancel(context); + return; + } + if (bUnsetDragIcon) + { + cairo_surface_t *surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, 0, 0); + gtk_drag_set_icon_surface(context, surface); + } + if (!m_xDragSource) + return; + m_xDragSource->setActiveDragSource(); + } + + virtual void do_signal_drag_end() + { + } + + static void signalDragEnd(GtkWidget* /*widget*/, GdkDragContext* context, gpointer widget) + { + GtkInstanceWidget* pThis = static_cast<GtkInstanceWidget*>(widget); + pThis->do_signal_drag_end(); + if (pThis->m_xDragSource.is()) + pThis->m_xDragSource->dragEnd(context); + } + + static gboolean signalDragFailed(GtkWidget* /*widget*/, GdkDragContext* /*context*/, GtkDragResult /*result*/, gpointer widget) + { + GtkInstanceWidget* pThis = static_cast<GtkInstanceWidget*>(widget); + pThis->m_xDragSource->dragFailed(); + return false; + } + + static void signalDragDelete(GtkWidget* /*widget*/, GdkDragContext* /*context*/, gpointer widget) + { + GtkInstanceWidget* pThis = static_cast<GtkInstanceWidget*>(widget); + pThis->m_xDragSource->dragDelete(); + } + + static void signalDragDataGet(GtkWidget* /*widget*/, GdkDragContext* /*context*/, GtkSelectionData *data, guint info, + guint /*time*/, gpointer widget) + { + GtkInstanceWidget* pThis = static_cast<GtkInstanceWidget*>(widget); + pThis->m_xDragSource->dragDataGet(data, info); + } + + virtual void drag_source_set(const std::vector<GtkTargetEntry>& rGtkTargets, GdkDragAction eDragAction) + { + gtk_drag_source_set(m_pWidget, GDK_BUTTON1_MASK, rGtkTargets.data(), rGtkTargets.size(), eDragAction); + } + + void set_background(const OUString* pColor) + { + if (!pColor && !m_pBgCssProvider) + return; + GtkStyleContext *pWidgetContext = gtk_widget_get_style_context(GTK_WIDGET(m_pWidget)); + if (m_pBgCssProvider) + { + gtk_style_context_remove_provider(pWidgetContext, GTK_STYLE_PROVIDER(m_pBgCssProvider)); + m_pBgCssProvider = nullptr; + } + if (!pColor) + return; + m_pBgCssProvider = gtk_css_provider_new(); + OUString aBuffer = "* { background-color: #" + *pColor + "; }"; + OString aResult = OUStringToOString(aBuffer, RTL_TEXTENCODING_UTF8); + gtk_css_provider_load_from_data(m_pBgCssProvider, aResult.getStr(), aResult.getLength(), nullptr); + gtk_style_context_add_provider(pWidgetContext, GTK_STYLE_PROVIDER(m_pBgCssProvider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + } + +public: + GtkInstanceWidget(GtkWidget* pWidget, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : m_pWidget(pWidget) + , m_pMouseEventBox(nullptr) + , m_pBuilder(pBuilder) + , m_bTakeOwnership(bTakeOwnership) + , m_bDraggedOver(false) + , m_nLastMouseButton(0) + , m_nLastMouseClicks(0) + , m_nPressedButton(-1) + , m_nPressStartX(-1) + , m_nPressStartY(-1) + , m_pFocusInEvent(nullptr) + , m_pFocusOutEvent(nullptr) + , m_pDragCancelEvent(nullptr) + , m_pBgCssProvider(nullptr) + , m_eDragAction(GdkDragAction(0)) + , m_nFocusInSignalId(0) + , m_nMnemonicActivateSignalId(0) + , m_nFocusOutSignalId(0) + , m_nKeyPressSignalId(0) + , m_nKeyReleaseSignalId(0) + , m_nSizeAllocateSignalId(0) + , m_nButtonPressSignalId(0) + , m_nMotionSignalId(0) + , m_nLeaveSignalId(0) + , m_nEnterSignalId(0) + , m_nButtonReleaseSignalId(0) + , m_nDragMotionSignalId(0) + , m_nDragDropSignalId(0) + , m_nDragDropReceivedSignalId(0) + , m_nDragLeaveSignalId(0) + , m_nDragBeginSignalId(0) + , m_nDragEndSignalId(0) + , m_nDragFailedSignalId(0) + , m_nDragDataDeleteignalId(0) + , m_nDragGetSignalId(0) + { + if (!bTakeOwnership) + g_object_ref(m_pWidget); + + localizeDecimalSeparator(); + } + + virtual void connect_key_press(const Link<const KeyEvent&, bool>& rLink) override + { + if (!m_nKeyPressSignalId) + m_nKeyPressSignalId = g_signal_connect(m_pWidget, "key-press-event", G_CALLBACK(signalKey), this); + weld::Widget::connect_key_press(rLink); + } + + virtual void connect_key_release(const Link<const KeyEvent&, bool>& rLink) override + { + if (!m_nKeyReleaseSignalId) + m_nKeyReleaseSignalId = g_signal_connect(m_pWidget, "key-release-event", G_CALLBACK(signalKey), this); + weld::Widget::connect_key_release(rLink); + } + + virtual void connect_mouse_press(const Link<const MouseEvent&, bool>& rLink) override + { + ensureButtonPressSignal(); + weld::Widget::connect_mouse_press(rLink); + } + + virtual void connect_mouse_move(const Link<const MouseEvent&, bool>& rLink) override + { + ensureEventWidget(); + if (!m_nMotionSignalId) + m_nMotionSignalId = g_signal_connect(m_pMouseEventBox, "motion-notify-event", G_CALLBACK(signalMotion), this); + if (!m_nLeaveSignalId) + m_nLeaveSignalId = g_signal_connect(m_pMouseEventBox, "leave-notify-event", G_CALLBACK(signalCrossing), this); + if (!m_nEnterSignalId) + m_nEnterSignalId = g_signal_connect(m_pMouseEventBox, "enter-notify-event", G_CALLBACK(signalCrossing), this); + weld::Widget::connect_mouse_move(rLink); + } + + virtual void connect_mouse_release(const Link<const MouseEvent&, bool>& rLink) override + { + ensureEventWidget(); + if (!m_nButtonReleaseSignalId) + m_nButtonReleaseSignalId = g_signal_connect(m_pMouseEventBox, "button-release-event", G_CALLBACK(signalButton), this); + weld::Widget::connect_mouse_release(rLink); + } + + virtual void set_sensitive(bool sensitive) override + { + gtk_widget_set_sensitive(m_pWidget, sensitive); + } + + virtual bool get_sensitive() const override + { + return gtk_widget_get_sensitive(m_pWidget); + } + + virtual bool get_visible() const override + { + return gtk_widget_get_visible(m_pWidget); + } + + virtual bool is_visible() const override + { + return gtk_widget_is_visible(m_pWidget); + } + + virtual void set_can_focus(bool bCanFocus) override + { + gtk_widget_set_can_focus(m_pWidget, bCanFocus); + } + + virtual void grab_focus() override + { + disable_notify_events(); + gtk_widget_grab_focus(m_pWidget); + enable_notify_events(); + } + + virtual bool has_focus() const override + { + return gtk_widget_has_focus(m_pWidget); + } + + virtual bool is_active() const override + { + GtkWindow* pTopLevel = GTK_WINDOW(gtk_widget_get_toplevel(m_pWidget)); + return pTopLevel && gtk_window_is_active(pTopLevel) && has_focus(); + } + + virtual void set_has_default(bool has_default) override + { + g_object_set(G_OBJECT(m_pWidget), "has-default", has_default, nullptr); + } + + virtual bool get_has_default() const override + { + gboolean has_default(false); + g_object_get(G_OBJECT(m_pWidget), "has-default", &has_default, nullptr); + return has_default; + } + + virtual void show() override + { + gtk_widget_show(m_pWidget); + } + + virtual void hide() override + { + gtk_widget_hide(m_pWidget); + } + + virtual void set_size_request(int nWidth, int nHeight) override + { + GtkWidget* pParent = gtk_widget_get_parent(m_pWidget); + if (GTK_IS_VIEWPORT(pParent)) + pParent = gtk_widget_get_parent(pParent); + if (GTK_IS_SCROLLED_WINDOW(pParent)) + { + gtk_scrolled_window_set_min_content_width(GTK_SCROLLED_WINDOW(pParent), nWidth); + gtk_scrolled_window_set_min_content_height(GTK_SCROLLED_WINDOW(pParent), nHeight); + } + gtk_widget_set_size_request(m_pWidget, nWidth, nHeight); + } + + virtual Size get_size_request() const override + { + int nWidth, nHeight; + gtk_widget_get_size_request(m_pWidget, &nWidth, &nHeight); + return Size(nWidth, nHeight); + } + + virtual Size get_preferred_size() const override + { + GtkRequisition size; + gtk_widget_get_preferred_size(m_pWidget, nullptr, &size); + return Size(size.width, size.height); + } + + virtual float get_approximate_digit_width() const override + { + PangoContext* pContext = gtk_widget_get_pango_context(m_pWidget); + PangoFontMetrics* pMetrics = pango_context_get_metrics(pContext, + pango_context_get_font_description(pContext), + pango_context_get_language(pContext)); + float nDigitWidth = pango_font_metrics_get_approximate_digit_width(pMetrics); + pango_font_metrics_unref(pMetrics); + + return nDigitWidth / PANGO_SCALE; + } + + virtual int get_text_height() const override + { + PangoContext* pContext = gtk_widget_get_pango_context(m_pWidget); + PangoFontMetrics* pMetrics = pango_context_get_metrics(pContext, + pango_context_get_font_description(pContext), + pango_context_get_language(pContext)); + int nLineHeight = pango_font_metrics_get_ascent(pMetrics) + pango_font_metrics_get_descent(pMetrics); + pango_font_metrics_unref(pMetrics); + return nLineHeight / PANGO_SCALE; + } + + virtual Size get_pixel_size(const OUString& rText) const override + { + OString aStr(OUStringToOString(rText, RTL_TEXTENCODING_UTF8)); + PangoLayout* pLayout = gtk_widget_create_pango_layout(m_pWidget, aStr.getStr()); + gint nWidth, nHeight; + pango_layout_get_pixel_size(pLayout, &nWidth, &nHeight); + g_object_unref(pLayout); + return Size(nWidth, nHeight); + } + + virtual vcl::Font get_font() override + { + PangoContext* pContext = gtk_widget_get_pango_context(m_pWidget); + return pango_to_vcl(pango_context_get_font_description(pContext), + Application::GetSettings().GetUILanguageTag().getLocale()); + } + + virtual void set_grid_left_attach(int nAttach) override + { + GtkContainer* pParent = GTK_CONTAINER(gtk_widget_get_parent(m_pWidget)); + gtk_container_child_set(pParent, m_pWidget, "left-attach", nAttach, nullptr); + } + + virtual int get_grid_left_attach() const override + { + GtkContainer* pParent = GTK_CONTAINER(gtk_widget_get_parent(m_pWidget)); + gint nAttach(0); + gtk_container_child_get(pParent, m_pWidget, "left-attach", &nAttach, nullptr); + return nAttach; + } + + virtual void set_grid_width(int nCols) override + { + GtkContainer* pParent = GTK_CONTAINER(gtk_widget_get_parent(m_pWidget)); + gtk_container_child_set(pParent, m_pWidget, "width", nCols, nullptr); + } + + virtual void set_grid_top_attach(int nAttach) override + { + GtkContainer* pParent = GTK_CONTAINER(gtk_widget_get_parent(m_pWidget)); + gtk_container_child_set(pParent, m_pWidget, "top-attach", nAttach, nullptr); + } + + virtual int get_grid_top_attach() const override + { + GtkContainer* pParent = GTK_CONTAINER(gtk_widget_get_parent(m_pWidget)); + gint nAttach(0); + gtk_container_child_get(pParent, m_pWidget, "top-attach", &nAttach, nullptr); + return nAttach; + } + + virtual void set_hexpand(bool bExpand) override + { + gtk_widget_set_hexpand(m_pWidget, bExpand); + } + + virtual bool get_hexpand() const override + { + return gtk_widget_get_hexpand(m_pWidget); + } + + virtual void set_vexpand(bool bExpand) override + { + gtk_widget_set_vexpand(m_pWidget, bExpand); + } + + virtual bool get_vexpand() const override + { + return gtk_widget_get_vexpand(m_pWidget); + } + + virtual void set_secondary(bool bSecondary) override + { + GtkWidget* pParent = gtk_widget_get_parent(m_pWidget); + if (pParent && GTK_IS_BUTTON_BOX(pParent)) + gtk_button_box_set_child_secondary(GTK_BUTTON_BOX(pParent), m_pWidget, bSecondary); + } + + virtual void set_margin_top(int nMargin) override + { + gtk_widget_set_margin_top(m_pWidget, nMargin); + } + + virtual void set_margin_bottom(int nMargin) override + { + gtk_widget_set_margin_bottom(m_pWidget, nMargin); + } + + virtual void set_margin_left(int nMargin) override + { + gtk_widget_set_margin_left(m_pWidget, nMargin); + } + + virtual void set_margin_right(int nMargin) override + { + gtk_widget_set_margin_right(m_pWidget, nMargin); + } + + virtual int get_margin_top() const override + { + return gtk_widget_get_margin_top(m_pWidget); + } + + virtual int get_margin_bottom() const override + { + return gtk_widget_get_margin_bottom(m_pWidget); + } + + virtual int get_margin_left() const override + { + return gtk_widget_get_margin_left(m_pWidget); + } + + virtual int get_margin_right() const override + { + return gtk_widget_get_margin_right(m_pWidget); + } + + virtual void set_accessible_name(const OUString& rName) override + { + AtkObject* pAtkObject = gtk_widget_get_accessible(m_pWidget); + if (!pAtkObject) + return; + atk_object_set_name(pAtkObject, OUStringToOString(rName, RTL_TEXTENCODING_UTF8).getStr()); + } + + virtual OUString get_accessible_name() const override + { + AtkObject* pAtkObject = gtk_widget_get_accessible(m_pWidget); + const char* pStr = pAtkObject ? atk_object_get_name(pAtkObject) : nullptr; + return OUString(pStr, pStr ? strlen(pStr) : 0, RTL_TEXTENCODING_UTF8); + } + + virtual OUString get_accessible_description() const override + { + AtkObject* pAtkObject = gtk_widget_get_accessible(m_pWidget); + const char* pStr = pAtkObject ? atk_object_get_description(pAtkObject) : nullptr; + return OUString(pStr, pStr ? strlen(pStr) : 0, RTL_TEXTENCODING_UTF8); + } + + virtual void set_accessible_relation_labeled_by(weld::Widget* pLabel) override + { + AtkObject* pAtkObject = gtk_widget_get_accessible(m_pWidget); + if (!pAtkObject) + return; + AtkObject *pAtkLabel = pLabel ? gtk_widget_get_accessible(dynamic_cast<GtkInstanceWidget&>(*pLabel).getWidget()) : nullptr; + AtkRelationSet *pRelationSet = atk_object_ref_relation_set(pAtkObject); + AtkRelation *pRelation = atk_relation_set_get_relation_by_type(pRelationSet, ATK_RELATION_LABELLED_BY); + if (pRelation) + atk_relation_set_remove(pRelationSet, pRelation); + if (pAtkLabel) + { + AtkObject *obj_array[1]; + obj_array[0] = pAtkLabel; + pRelation = atk_relation_new(obj_array, 1, ATK_RELATION_LABELLED_BY); + atk_relation_set_add(pRelationSet, pRelation); + } + g_object_unref(pRelationSet); + } + + virtual void set_accessible_relation_label_for(weld::Widget* pLabeled) override + { + AtkObject* pAtkObject = gtk_widget_get_accessible(m_pWidget); + if (!pAtkObject) + return; + AtkObject *pAtkLabeled = pLabeled ? gtk_widget_get_accessible(dynamic_cast<GtkInstanceWidget&>(*pLabeled).getWidget()) : nullptr; + AtkRelationSet *pRelationSet = atk_object_ref_relation_set(pAtkObject); + AtkRelation *pRelation = atk_relation_set_get_relation_by_type(pRelationSet, ATK_RELATION_LABEL_FOR); + if (pRelation) + atk_relation_set_remove(pRelationSet, pRelation); + if (pAtkLabeled) + { + AtkObject *obj_array[1]; + obj_array[0] = pAtkLabeled; + pRelation = atk_relation_new(obj_array, 1, ATK_RELATION_LABEL_FOR); + atk_relation_set_add(pRelationSet, pRelation); + } + g_object_unref(pRelationSet); + } + + virtual bool get_extents_relative_to(weld::Widget& rRelative, int& x, int &y, int& width, int &height) override + { + //for toplevel windows this is sadly futile under wayland, so we can't tell where a dialog is in order to allow + //the document underneath to auto-scroll to place content in a visible location + bool ret = gtk_widget_translate_coordinates(m_pWidget, + dynamic_cast<GtkInstanceWidget&>(rRelative).getWidget(), + 0, 0, &x, &y); + width = gtk_widget_get_allocated_width(m_pWidget); + height = gtk_widget_get_allocated_height(m_pWidget); + return ret; + } + + virtual void set_tooltip_text(const OUString& rTip) override + { + gtk_widget_set_tooltip_text(m_pWidget, OUStringToOString(rTip, RTL_TEXTENCODING_UTF8).getStr()); + } + + virtual OUString get_tooltip_text() const override + { + const gchar* pStr = gtk_widget_get_tooltip_text(m_pWidget); + return OUString(pStr, pStr ? strlen(pStr) : 0, RTL_TEXTENCODING_UTF8); + } + + virtual std::unique_ptr<weld::Container> weld_parent() const override; + + virtual OString get_buildable_name() const override + { + const gchar* pStr = gtk_buildable_get_name(GTK_BUILDABLE(m_pWidget)); + return OString(pStr, pStr ? strlen(pStr) : 0); + } + + virtual void set_help_id(const OString& rHelpId) override + { + ::set_help_id(m_pWidget, rHelpId); + } + + virtual OString get_help_id() const override + { + OString sRet = ::get_help_id(m_pWidget); + if (sRet.isEmpty()) + sRet = OString("null"); + return sRet; + } + + GtkWidget* getWidget() + { + return m_pWidget; + } + + GtkWindow* getWindow() + { + return GTK_WINDOW(gtk_widget_get_toplevel(m_pWidget)); + } + + virtual void connect_focus_in(const Link<Widget&, void>& rLink) override + { + if (!m_nFocusInSignalId) + m_nFocusInSignalId = g_signal_connect(m_pWidget, "focus-in-event", G_CALLBACK(signalFocusIn), this); + weld::Widget::connect_focus_in(rLink); + } + + virtual void connect_mnemonic_activate(const Link<Widget&, bool>& rLink) override + { + if (!m_nMnemonicActivateSignalId) + m_nMnemonicActivateSignalId = g_signal_connect(m_pWidget, "mnemonic-activate", G_CALLBACK(signalMnemonicActivate), this); + weld::Widget::connect_mnemonic_activate(rLink); + } + + virtual void connect_focus_out(const Link<Widget&, void>& rLink) override + { + if (!m_nFocusOutSignalId) + m_nFocusOutSignalId = g_signal_connect(m_pWidget, "focus-out-event", G_CALLBACK(signalFocusOut), this); + weld::Widget::connect_focus_out(rLink); + } + + virtual void connect_size_allocate(const Link<const Size&, void>& rLink) override + { + m_nSizeAllocateSignalId = g_signal_connect(m_pWidget, "size_allocate", G_CALLBACK(signalSizeAllocate), this); + weld::Widget::connect_size_allocate(rLink); + } + + virtual void signal_size_allocate(guint nWidth, guint nHeight) + { + m_aSizeAllocateHdl.Call(Size(nWidth, nHeight)); + } + + bool signal_key(const GdkEventKey* pEvent) + { + if (pEvent->type == GDK_KEY_PRESS && m_aKeyPressHdl.IsSet()) + { + SolarMutexGuard aGuard; + return m_aKeyPressHdl.Call(GtkToVcl(*pEvent)); + } + if (pEvent->type == GDK_KEY_RELEASE && m_aKeyReleaseHdl.IsSet()) + { + SolarMutexGuard aGuard; + return m_aKeyReleaseHdl.Call(GtkToVcl(*pEvent)); + } + return false; + } + + virtual void grab_add() override + { + gtk_grab_add(m_pWidget); + } + + virtual bool has_grab() const override + { + return gtk_widget_has_grab(m_pWidget); + } + + virtual void grab_remove() override + { + gtk_grab_remove(m_pWidget); + } + + virtual bool get_direction() const override + { + return gtk_widget_get_direction(m_pWidget) == GTK_TEXT_DIR_RTL; + } + + virtual void set_direction(bool bRTL) override + { + gtk_widget_set_direction(m_pWidget, bRTL ? GTK_TEXT_DIR_RTL : GTK_TEXT_DIR_LTR); + } + + virtual void freeze() override + { + gtk_widget_freeze_child_notify(m_pWidget); + } + + virtual void thaw() override + { + gtk_widget_thaw_child_notify(m_pWidget); + } + + virtual css::uno::Reference<css::datatransfer::dnd::XDropTarget> get_drop_target() override + { + if (!m_xDropTarget) + { + m_xDropTarget.set(new GtkDropTarget); + if (!gtk_drag_dest_get_track_motion(m_pWidget)) + { + gtk_drag_dest_set(m_pWidget, GtkDestDefaults(0), nullptr, 0, GdkDragAction(0)); + gtk_drag_dest_set_track_motion(m_pWidget, true); + } + m_nDragMotionSignalId = g_signal_connect(m_pWidget, "drag-motion", G_CALLBACK(signalDragMotion), this); + m_nDragDropSignalId = g_signal_connect(m_pWidget, "drag-drop", G_CALLBACK(signalDragDrop), this); + m_nDragDropReceivedSignalId = g_signal_connect(m_pWidget, "drag-data-received", G_CALLBACK(signalDragDropReceived), this); + m_nDragLeaveSignalId = g_signal_connect(m_pWidget, "drag-leave", G_CALLBACK(signalDragLeave), this); + } + return m_xDropTarget.get(); + } + + virtual void connect_get_property_tree(const Link<boost::property_tree::ptree&, void>& /*rLink*/) override + { + //not implemented for the gtk variant + } + + virtual void set_stack_background() override + { + OUString sColor = Application::GetSettings().GetStyleSettings().GetWindowColor().AsRGBHexString(); + set_background(&sColor); + } + + virtual void set_highlight_background() override + { + OUString sColor = Application::GetSettings().GetStyleSettings().GetHighlightColor().AsRGBHexString(); + set_background(&sColor); + } + + virtual void set_toolbar_background() override + { + // no-op + } + + virtual ~GtkInstanceWidget() override + { + if (m_pFocusInEvent) + Application::RemoveUserEvent(m_pFocusInEvent); + if (m_pFocusOutEvent) + Application::RemoveUserEvent(m_pFocusOutEvent); + if (m_pDragCancelEvent) + Application::RemoveUserEvent(m_pDragCancelEvent); + if (m_nDragMotionSignalId) + g_signal_handler_disconnect(m_pWidget, m_nDragMotionSignalId); + if (m_nDragDropSignalId) + g_signal_handler_disconnect(m_pWidget, m_nDragDropSignalId); + if (m_nDragDropReceivedSignalId) + g_signal_handler_disconnect(m_pWidget, m_nDragDropReceivedSignalId); + if (m_nDragLeaveSignalId) + g_signal_handler_disconnect(m_pWidget, m_nDragLeaveSignalId); + if (m_nDragEndSignalId) + g_signal_handler_disconnect(m_pWidget, m_nDragEndSignalId); + if (m_nDragBeginSignalId) + g_signal_handler_disconnect(m_pWidget, m_nDragBeginSignalId); + if (m_nDragFailedSignalId) + g_signal_handler_disconnect(m_pWidget, m_nDragFailedSignalId); + if (m_nDragDataDeleteignalId) + g_signal_handler_disconnect(m_pWidget, m_nDragDataDeleteignalId); + if (m_nDragGetSignalId) + g_signal_handler_disconnect(m_pWidget, m_nDragGetSignalId); + if (m_nKeyPressSignalId) + g_signal_handler_disconnect(m_pWidget, m_nKeyPressSignalId); + if (m_nKeyReleaseSignalId) + g_signal_handler_disconnect(m_pWidget, m_nKeyReleaseSignalId); + if (m_nButtonPressSignalId) + g_signal_handler_disconnect(m_pMouseEventBox, m_nButtonPressSignalId); + if (m_nMotionSignalId) + g_signal_handler_disconnect(m_pMouseEventBox, m_nMotionSignalId); + if (m_nLeaveSignalId) + g_signal_handler_disconnect(m_pMouseEventBox, m_nLeaveSignalId); + if (m_nEnterSignalId) + g_signal_handler_disconnect(m_pMouseEventBox, m_nEnterSignalId); + if (m_nButtonReleaseSignalId) + g_signal_handler_disconnect(m_pMouseEventBox, m_nButtonReleaseSignalId); + if (m_nFocusInSignalId) + g_signal_handler_disconnect(m_pWidget, m_nFocusInSignalId); + if (m_nMnemonicActivateSignalId) + g_signal_handler_disconnect(m_pWidget, m_nMnemonicActivateSignalId); + if (m_nFocusOutSignalId) + g_signal_handler_disconnect(m_pWidget, m_nFocusOutSignalId); + if (m_nSizeAllocateSignalId) + g_signal_handler_disconnect(m_pWidget, m_nSizeAllocateSignalId); + + set_background(nullptr); + + if (m_pMouseEventBox && m_pMouseEventBox != m_pWidget) + { + // put things back they way we found them + GtkWidget* pParent = gtk_widget_get_parent(m_pMouseEventBox); + + g_object_ref(m_pWidget); + gtk_container_remove(GTK_CONTAINER(m_pMouseEventBox), m_pWidget); + + gtk_widget_destroy(m_pMouseEventBox); + + gtk_container_add(GTK_CONTAINER(pParent), m_pWidget); + g_object_unref(m_pWidget); + } + + if (m_bTakeOwnership) + gtk_widget_destroy(m_pWidget); + else + g_object_unref(m_pWidget); + } + + virtual void disable_notify_events() + { + if (m_nFocusInSignalId) + g_signal_handler_block(m_pWidget, m_nFocusInSignalId); + if (m_nMnemonicActivateSignalId) + g_signal_handler_block(m_pWidget, m_nMnemonicActivateSignalId); + if (m_nFocusOutSignalId) + g_signal_handler_block(m_pWidget, m_nFocusOutSignalId); + if (m_nSizeAllocateSignalId) + g_signal_handler_block(m_pWidget, m_nSizeAllocateSignalId); + } + + virtual void enable_notify_events() + { + if (m_nSizeAllocateSignalId) + g_signal_handler_unblock(m_pWidget, m_nSizeAllocateSignalId); + if (m_nFocusOutSignalId) + g_signal_handler_unblock(m_pWidget, m_nFocusOutSignalId); + if (m_nMnemonicActivateSignalId) + g_signal_handler_unblock(m_pWidget, m_nMnemonicActivateSignalId); + if (m_nFocusInSignalId) + g_signal_handler_unblock(m_pWidget, m_nFocusInSignalId); + } + + virtual void help_hierarchy_foreach(const std::function<bool(const OString&)>& func) override; + + virtual OUString strip_mnemonic(const OUString &rLabel) const override + { + return rLabel.replaceFirst("_", ""); + } + + virtual VclPtr<VirtualDevice> create_virtual_device() const override + { + // create with no separate alpha layer like everything sane does + auto xRet = VclPtr<VirtualDevice>::Create(); + xRet->SetBackground(COL_TRANSPARENT); + return xRet; + } + + virtual void draw(VirtualDevice& rOutput) override + { + // detect if we have to manually setup its size + bool bAlreadyRealized = gtk_widget_get_realized(m_pWidget); + // has to be visible for draw to work + bool bAlreadyVisible = gtk_widget_get_visible(m_pWidget); + // has to be mapped for draw to work + bool bAlreadyMapped = gtk_widget_get_mapped(m_pWidget); + if (!bAlreadyVisible) + gtk_widget_show(m_pWidget); + + GtkAllocation allocation; + + if (!bAlreadyRealized) + gtk_widget_realize(m_pWidget); + + if (!bAlreadyMapped) + gtk_widget_map(m_pWidget); + + if (GTK_IS_CONTAINER(m_pWidget)) + gtk_container_resize_children(GTK_CONTAINER(m_pWidget)); + + gtk_widget_get_allocation(m_pWidget, &allocation); + + rOutput.SetOutputSizePixel(Size(allocation.width, allocation.height)); + cairo_surface_t* pSurface = get_underlying_cairo_surface(rOutput); + cairo_t* cr = cairo_create(pSurface); + + gtk_widget_draw(m_pWidget, cr); + + cairo_destroy(cr); + + if (!bAlreadyVisible) + gtk_widget_hide(m_pWidget); + if (!bAlreadyMapped) + gtk_widget_unmap(m_pWidget); + if (!bAlreadyRealized) + gtk_widget_unrealize(m_pWidget); + } +}; + +} + +IMPL_LINK_NOARG(GtkInstanceWidget, async_signal_focus_in, void*, void) +{ + m_pFocusInEvent = nullptr; + signal_focus_in(); +} + +IMPL_LINK_NOARG(GtkInstanceWidget, async_signal_focus_out, void*, void) +{ + m_pFocusOutEvent = nullptr; + signal_focus_out(); +} + +IMPL_LINK(GtkInstanceWidget, async_drag_cancel, void*, arg, void) +{ + m_pDragCancelEvent = nullptr; + GdkDragContext* context = static_cast<GdkDragContext*>(arg); + + // tdf#132477 simply calling gtk_drag_cancel on the treeview dnd under X + // doesn't seem to work as hoped for (though under wayland all is well). + // Under X the next (allowed) drag effort doesn't work to drop anything, + // but a then repeated attempt does. + // emitting cancel to get gtk to cancel the drag for us does work as hoped for. + g_signal_emit_by_name(context, "cancel", 0, GDK_DRAG_CANCEL_USER_CANCELLED); + + g_object_unref(context); +} + +namespace +{ + OString MapToGtkAccelerator(const OUString &rStr) + { + return OUStringToOString(rStr.replaceFirst("~", "_"), RTL_TEXTENCODING_UTF8); + } + + OUString get_label(GtkLabel* pLabel) + { + const gchar* pStr = gtk_label_get_label(pLabel); + return OUString(pStr, pStr ? strlen(pStr) : 0, RTL_TEXTENCODING_UTF8); + } + + void set_label(GtkLabel* pLabel, const OUString& rText) + { + gtk_label_set_label(pLabel, MapToGtkAccelerator(rText).getStr()); + } + + OUString get_label(GtkButton* pButton) + { + const gchar* pStr = gtk_button_get_label(pButton); + return OUString(pStr, pStr ? strlen(pStr) : 0, RTL_TEXTENCODING_UTF8); + } + + void set_label(GtkButton* pButton, const OUString& rText) + { + gtk_button_set_label(pButton, MapToGtkAccelerator(rText).getStr()); + } + + OUString get_title(GtkWindow* pWindow) + { + const gchar* pStr = gtk_window_get_title(pWindow); + return OUString(pStr, pStr ? strlen(pStr) : 0, RTL_TEXTENCODING_UTF8); + } + + void set_title(GtkWindow* pWindow, const OUString& rTitle) + { + gtk_window_set_title(pWindow, OUStringToOString(rTitle, RTL_TEXTENCODING_UTF8).getStr()); + } + + OUString get_primary_text(GtkMessageDialog* pMessageDialog) + { + gchar* pText = nullptr; + g_object_get(G_OBJECT(pMessageDialog), "text", &pText, nullptr); + return OUString(pText, pText ? strlen(pText) : 0, RTL_TEXTENCODING_UTF8); + } + + void set_primary_text(GtkMessageDialog* pMessageDialog, const OUString& rText) + { + g_object_set(G_OBJECT(pMessageDialog), "text", + OUStringToOString(rText, RTL_TEXTENCODING_UTF8).getStr(), + nullptr); + } + + void set_secondary_text(GtkMessageDialog* pMessageDialog, const OUString& rText) + { + g_object_set(G_OBJECT(pMessageDialog), "secondary-text", + OUStringToOString(rText, RTL_TEXTENCODING_UTF8).getStr(), + nullptr); + } + + OUString get_secondary_text(GtkMessageDialog* pMessageDialog) + { + gchar* pText = nullptr; + g_object_get(G_OBJECT(pMessageDialog), "secondary-text", &pText, nullptr); + return OUString(pText, pText ? strlen(pText) : 0, RTL_TEXTENCODING_UTF8); + } +} + +namespace +{ + GdkPixbuf* load_icon_from_stream(SvMemoryStream& rStream) + { + auto nLength = rStream.TellEnd(); + if (!nLength) + return nullptr; + const guchar* pData = static_cast<const guchar*>(rStream.GetData()); + assert((*pData == 137 || *pData == '<') && "if we want to support more than png or svg this function must change"); + // if we know the image type, it's a little faster to hand the type over and skip the type detection. + GdkPixbufLoader *pixbuf_loader = gdk_pixbuf_loader_new_with_type(*pData == 137 ? "png" : "svg", nullptr); + gdk_pixbuf_loader_write(pixbuf_loader, pData, nLength, nullptr); + gdk_pixbuf_loader_close(pixbuf_loader, nullptr); + GdkPixbuf* pixbuf = gdk_pixbuf_loader_get_pixbuf(pixbuf_loader); + if (pixbuf) + g_object_ref(pixbuf); + g_object_unref(pixbuf_loader); + return pixbuf; + } + + GdkPixbuf* load_icon_by_name_theme_lang(const OUString& rIconName, const OUString& rIconTheme, const OUString& rUILang) + { + auto xMemStm = ImageTree::get().getImageStream(rIconName, rIconTheme, rUILang); + if (!xMemStm) + return nullptr; + return load_icon_from_stream(*xMemStm); + } +} + +GdkPixbuf* load_icon_by_name(const OUString& rIconName) +{ + OUString sIconTheme = Application::GetSettings().GetStyleSettings().DetermineIconTheme(); + OUString sUILang = Application::GetSettings().GetUILanguageTag().getBcp47(); + return load_icon_by_name_theme_lang(rIconName, sIconTheme, sUILang); +} + +namespace +{ + GdkPixbuf* getPixbuf(const css::uno::Reference<css::graphic::XGraphic>& rImage) + { + Image aImage(rImage); + + OUString sStock(aImage.GetStock()); + if (!sStock.isEmpty()) + return load_icon_by_name(sStock); + + std::unique_ptr<SvMemoryStream> xMemStm(new SvMemoryStream); + + css::uno::Sequence<css::beans::PropertyValue> aFilterData(1); + aFilterData[0].Name = "Compression"; + // We "know" that this gets passed to zlib's deflateInit2_(). 1 means best speed. + aFilterData[0].Value <<= sal_Int32(1); + + vcl::PNGWriter aWriter(aImage.GetBitmapEx(), &aFilterData); + aWriter.Write(*xMemStm); + + return load_icon_from_stream(*xMemStm); + } + + GdkPixbuf* getPixbuf(const VirtualDevice& rDevice) + { + Size aSize(rDevice.GetOutputSizePixel()); + cairo_surface_t* orig_surface = get_underlying_cairo_surface(rDevice); + double m_fXScale, m_fYScale; + dl_cairo_surface_get_device_scale(orig_surface, &m_fXScale, &m_fYScale); + + cairo_surface_t* surface; + if (m_fXScale != 1.0 || m_fYScale != -1) + { + surface = cairo_surface_create_similar_image(orig_surface, + CAIRO_FORMAT_ARGB32, + aSize.Width(), + aSize.Height()); + cairo_t* cr = cairo_create(surface); + cairo_set_source_surface(cr, orig_surface, 0, 0); + cairo_paint(cr); + cairo_destroy(cr); + } + else + surface = orig_surface; + + GdkPixbuf* pRet = gdk_pixbuf_get_from_surface(surface, 0, 0, aSize.Width(), aSize.Height()); + + if (surface != orig_surface) + cairo_surface_destroy(surface); + + return pRet; + } + + GdkPixbuf* getPixbuf(const OUString& rIconName) + { + if (rIconName.isEmpty()) + return nullptr; + + GdkPixbuf* pixbuf = nullptr; + + if (rIconName.lastIndexOf('.') != rIconName.getLength() - 4) + { + assert((rIconName== "dialog-warning" || rIconName== "dialog-error" || rIconName== "dialog-information") && + "unknown stock image"); + + GError *error = nullptr; + GtkIconTheme *icon_theme = gtk_icon_theme_get_default(); + pixbuf = gtk_icon_theme_load_icon(icon_theme, OUStringToOString(rIconName, RTL_TEXTENCODING_UTF8).getStr(), + 16, GTK_ICON_LOOKUP_USE_BUILTIN, &error); + } + else + { + const AllSettings& rSettings = Application::GetSettings(); + pixbuf = load_icon_by_name_theme_lang(rIconName, + rSettings.GetStyleSettings().DetermineIconTheme(), + rSettings.GetUILanguageTag().getBcp47()); + } + + return pixbuf; + } + + GtkWidget* image_new_from_virtual_device(const VirtualDevice& rImageSurface) + { + GtkWidget* pImage = nullptr; + if (gtk_check_version(3, 20, 0) == nullptr) + { + cairo_surface_t* surface = get_underlying_cairo_surface(rImageSurface); + + Size aSize(rImageSurface.GetOutputSizePixel()); + cairo_surface_t* target = cairo_surface_create_similar(surface, + cairo_surface_get_content(surface), + aSize.Width(), + aSize.Height()); + + cairo_t* cr = cairo_create(target); + cairo_set_source_surface(cr, surface, 0, 0); + cairo_paint(cr); + cairo_destroy(cr); + pImage = gtk_image_new_from_surface(target); + cairo_surface_destroy(target); + } + else + { + GdkPixbuf* pixbuf = getPixbuf(rImageSurface); + pImage = gtk_image_new_from_pixbuf(pixbuf); + g_object_unref(pixbuf); + } + return pImage; + } + +class MenuHelper +{ +protected: + GtkMenu* m_pMenu; + bool m_bTakeOwnership; + std::map<OString, GtkMenuItem*> m_aMap; +private: + + static void collect(GtkWidget* pItem, gpointer widget) + { + GtkMenuItem* pMenuItem = GTK_MENU_ITEM(pItem); + if (GtkWidget* pSubMenu = gtk_menu_item_get_submenu(pMenuItem)) + gtk_container_foreach(GTK_CONTAINER(pSubMenu), collect, widget); + MenuHelper* pThis = static_cast<MenuHelper*>(widget); + pThis->add_to_map(pMenuItem); + } + + static void signalActivate(GtkMenuItem* pItem, gpointer widget) + { + MenuHelper* pThis = static_cast<MenuHelper*>(widget); + SolarMutexGuard aGuard; + pThis->signal_activate(pItem); + } + + virtual void signal_activate(GtkMenuItem* pItem) = 0; + +public: + MenuHelper(GtkMenu* pMenu, bool bTakeOwnership) + : m_pMenu(pMenu) + , m_bTakeOwnership(bTakeOwnership) + { + if (!m_pMenu) + return; + gtk_container_foreach(GTK_CONTAINER(m_pMenu), collect, this); + } + + void add_to_map(GtkMenuItem* pMenuItem) + { + const gchar* pStr = gtk_buildable_get_name(GTK_BUILDABLE(pMenuItem)); + OString id(pStr, pStr ? strlen(pStr) : 0); + m_aMap[id] = pMenuItem; + g_signal_connect(pMenuItem, "activate", G_CALLBACK(signalActivate), this); + } + + void remove_from_map(GtkMenuItem* pMenuItem) + { + const gchar* pStr = gtk_buildable_get_name(GTK_BUILDABLE(pMenuItem)); + OString id(pStr, pStr ? strlen(pStr) : 0); + auto iter = m_aMap.find(id); + g_signal_handlers_disconnect_by_data(pMenuItem, this); + m_aMap.erase(iter); + } + + void disable_item_notify_events() + { + for (auto& a : m_aMap) + g_signal_handlers_block_by_func(a.second, reinterpret_cast<void*>(signalActivate), this); + } + + void enable_item_notify_events() + { + for (auto& a : m_aMap) + g_signal_handlers_unblock_by_func(a.second, reinterpret_cast<void*>(signalActivate), this); + } + + void insert_item(int pos, const OUString& rId, const OUString& rStr, + const OUString* pIconName, const VirtualDevice* pImageSurface, + TriState eCheckRadioFalse) + { + GtkWidget* pImage = nullptr; + if (pIconName && !pIconName->isEmpty()) + { + GdkPixbuf* pixbuf = load_icon_by_name(*pIconName); + if (!pixbuf) + { + pImage = gtk_image_new_from_pixbuf(pixbuf); + g_object_unref(pixbuf); + } + } + else if (pImageSurface) + pImage = image_new_from_virtual_device(*pImageSurface); + + GtkWidget *pItem; + if (pImage) + { + GtkWidget *pBox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6); + GtkWidget *pLabel = gtk_label_new(MapToGtkAccelerator(rStr).getStr()); + pItem = eCheckRadioFalse != TRISTATE_INDET ? gtk_check_menu_item_new() : gtk_menu_item_new(); + gtk_container_add(GTK_CONTAINER(pBox), pImage); + gtk_container_add(GTK_CONTAINER(pBox), pLabel); + gtk_container_add(GTK_CONTAINER(pItem), pBox); + gtk_widget_show_all(pItem); + } + else + { + pItem = eCheckRadioFalse != TRISTATE_INDET ? gtk_check_menu_item_new_with_mnemonic(MapToGtkAccelerator(rStr).getStr()) + : gtk_menu_item_new_with_mnemonic(MapToGtkAccelerator(rStr).getStr()); + } + + if (eCheckRadioFalse == TRISTATE_FALSE) + gtk_check_menu_item_set_draw_as_radio(GTK_CHECK_MENU_ITEM(pItem), true); + + gtk_buildable_set_name(GTK_BUILDABLE(pItem), OUStringToOString(rId, RTL_TEXTENCODING_UTF8).getStr()); + gtk_menu_shell_append(GTK_MENU_SHELL(m_pMenu), pItem); + gtk_widget_show(pItem); + add_to_map(GTK_MENU_ITEM(pItem)); + if (pos != -1) + gtk_menu_reorder_child(m_pMenu, pItem, pos); + } + + void insert_separator(int pos, const OUString& rId) + { + GtkWidget* pItem = gtk_separator_menu_item_new(); + gtk_buildable_set_name(GTK_BUILDABLE(pItem), OUStringToOString(rId, RTL_TEXTENCODING_UTF8).getStr()); + gtk_menu_shell_append(GTK_MENU_SHELL(m_pMenu), pItem); + gtk_widget_show(pItem); + add_to_map(GTK_MENU_ITEM(pItem)); + if (pos != -1) + gtk_menu_reorder_child(m_pMenu, pItem, pos); + } + + void remove_item(const OString& rIdent) + { + GtkMenuItem* pMenuItem = m_aMap[rIdent]; + remove_from_map(pMenuItem); + gtk_widget_destroy(GTK_WIDGET(pMenuItem)); + } + + void set_item_sensitive(const OString& rIdent, bool bSensitive) + { + gtk_widget_set_sensitive(GTK_WIDGET(m_aMap[rIdent]), bSensitive); + } + + void set_item_active(const OString& rIdent, bool bActive) + { + disable_item_notify_events(); + gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(m_aMap[rIdent]), bActive); + enable_item_notify_events(); + } + + bool get_item_active(const OString& rIdent) const + { + return gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(m_aMap.find(rIdent)->second)); + } + + void set_item_label(const OString& rIdent, const OUString& rText) + { + gtk_menu_item_set_label(m_aMap[rIdent], MapToGtkAccelerator(rText).getStr()); + } + + OUString get_item_label(const OString& rIdent) const + { + const gchar* pText = gtk_menu_item_get_label(m_aMap.find(rIdent)->second); + return OUString(pText, pText ? strlen(pText) : 0, RTL_TEXTENCODING_UTF8); + } + + void set_item_help_id(const OString& rIdent, const OString& rHelpId) + { + set_help_id(GTK_WIDGET(m_aMap[rIdent]), rHelpId); + } + + OString get_item_help_id(const OString& rIdent) const + { + return get_help_id(GTK_WIDGET(m_aMap.find(rIdent)->second)); + } + + void set_item_visible(const OString& rIdent, bool bShow) + { + GtkWidget* pWidget = GTK_WIDGET(m_aMap[rIdent]); + if (bShow) + gtk_widget_show(pWidget); + else + gtk_widget_hide(pWidget); + } + + void clear_items() + { + for (const auto& a : m_aMap) + { + GtkMenuItem* pMenuItem = a.second; + g_signal_handlers_disconnect_by_data(pMenuItem, this); + gtk_widget_destroy(GTK_WIDGET(pMenuItem)); + } + m_aMap.clear(); + } + + GtkMenu* getMenu() const + { + return m_pMenu; + } + + virtual ~MenuHelper() + { + for (auto& a : m_aMap) + g_signal_handlers_disconnect_by_data(a.second, this); + if (m_bTakeOwnership) + gtk_widget_destroy(GTK_WIDGET(m_pMenu)); + } +}; + +class GtkInstanceSizeGroup : public weld::SizeGroup +{ +private: + GtkSizeGroup* m_pGroup; +public: + GtkInstanceSizeGroup() + : m_pGroup(gtk_size_group_new(GTK_SIZE_GROUP_HORIZONTAL)) + { + } + virtual void add_widget(weld::Widget* pWidget) override + { + GtkInstanceWidget* pVclWidget = dynamic_cast<GtkInstanceWidget*>(pWidget); + assert(pVclWidget); + gtk_size_group_add_widget(m_pGroup, pVclWidget->getWidget()); + } + virtual void set_mode(VclSizeGroupMode eVclMode) override + { + GtkSizeGroupMode eGtkMode(GTK_SIZE_GROUP_NONE); + switch (eVclMode) + { + case VclSizeGroupMode::NONE: + eGtkMode = GTK_SIZE_GROUP_NONE; + break; + case VclSizeGroupMode::Horizontal: + eGtkMode = GTK_SIZE_GROUP_HORIZONTAL; + break; + case VclSizeGroupMode::Vertical: + eGtkMode = GTK_SIZE_GROUP_VERTICAL; + break; + case VclSizeGroupMode::Both: + eGtkMode = GTK_SIZE_GROUP_BOTH; + break; + } + gtk_size_group_set_mode(m_pGroup, eGtkMode); + } + virtual ~GtkInstanceSizeGroup() override + { + g_object_unref(m_pGroup); + } +}; + +class ChildFrame : public WorkWindow +{ +private: + Idle maLayoutIdle; + + DECL_LINK(ImplHandleLayoutTimerHdl, Timer*, void); +public: + ChildFrame(vcl::Window* pParent, WinBits nStyle) + : WorkWindow(pParent, nStyle) + { + maLayoutIdle.SetPriority(TaskPriority::RESIZE); + maLayoutIdle.SetInvokeHandler( LINK( this, ChildFrame, ImplHandleLayoutTimerHdl ) ); + maLayoutIdle.SetDebugName( "ChildFrame maLayoutIdle" ); + } + + virtual void dispose() override + { + maLayoutIdle.Stop(); + WorkWindow::dispose(); + } + + virtual void queue_resize(StateChangedType eReason = StateChangedType::Layout) override + { + WorkWindow::queue_resize(eReason); + if (maLayoutIdle.IsActive()) + return; + maLayoutIdle.Start(); + } + + virtual void Resize() override + { + WorkWindow::Resize(); + queue_resize(); + } +}; + +IMPL_LINK_NOARG(ChildFrame, ImplHandleLayoutTimerHdl, Timer*, void) +{ + if (vcl::Window *pChild = GetWindow(GetWindowType::FirstChild)) + pChild->SetPosSizePixel(Point(0, 0), GetSizePixel()); +} + +class GtkInstanceContainer : public GtkInstanceWidget, public virtual weld::Container +{ +private: + GtkContainer* m_pContainer; + + static void implResetDefault(GtkWidget *pWidget, gpointer user_data) + { + if (GTK_IS_BUTTON(pWidget)) + g_object_set(G_OBJECT(pWidget), "has-default", false, nullptr); + if (GTK_IS_CONTAINER(pWidget)) + gtk_container_forall(GTK_CONTAINER(pWidget), implResetDefault, user_data); + } + +public: + GtkInstanceContainer(GtkContainer* pContainer, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : GtkInstanceWidget(GTK_WIDGET(pContainer), pBuilder, bTakeOwnership) + , m_pContainer(pContainer) + { + } + + GtkContainer* getContainer() { return m_pContainer; } + + virtual void move(weld::Widget* pWidget, weld::Container* pNewParent) override + { + GtkInstanceWidget* pGtkWidget = dynamic_cast<GtkInstanceWidget*>(pWidget); + assert(pGtkWidget); + GtkWidget* pChild = pGtkWidget->getWidget(); + g_object_ref(pChild); + gtk_container_remove(getContainer(), pChild); + + GtkInstanceContainer* pNewGtkParent = dynamic_cast<GtkInstanceContainer*>(pNewParent); + assert(!pNewParent || pNewGtkParent); + if (pNewGtkParent) + gtk_container_add(pNewGtkParent->getContainer(), pChild); + g_object_unref(pChild); + } + + virtual void recursively_unset_default_buttons() override + { + implResetDefault(GTK_WIDGET(m_pContainer), nullptr); + } + + virtual css::uno::Reference<css::awt::XWindow> CreateChildFrame() override + { + // This will cause a GtkSalFrame to be created. With WB_SYSTEMCHILDWINDOW set it + // will create a toplevel GtkEventBox window + auto xEmbedWindow = VclPtr<ChildFrame>::Create(ImplGetDefaultWindow(), WB_SYSTEMCHILDWINDOW | WB_DIALOGCONTROL | WB_CHILDDLGCTRL); + SalFrame* pFrame = xEmbedWindow->ImplGetFrame(); + GtkSalFrame* pGtkFrame = dynamic_cast<GtkSalFrame*>(pFrame); + assert(pGtkFrame); + + // relocate that toplevel GtkEventBox into this widget + GtkWidget* pWindow = pGtkFrame->getWindow(); + + GtkWidget* pParent = gtk_widget_get_parent(pWindow); + + g_object_ref(pWindow); + gtk_container_remove(GTK_CONTAINER(pParent), pWindow); + gtk_container_add(m_pContainer, pWindow); + gtk_container_child_set(m_pContainer, pWindow, "expand", true, "fill", true, nullptr); + gtk_widget_set_hexpand(pWindow, true); + gtk_widget_set_vexpand(pWindow, true); + gtk_widget_realize(pWindow); + gtk_widget_set_can_focus(pWindow, true); + g_object_unref(pWindow); + + // NoActivate otherwise Show grab focus to this widget + xEmbedWindow->Show(true, ShowFlags::NoActivate); + css::uno::Reference<css::awt::XWindow> xWindow(xEmbedWindow->GetComponentInterface(), css::uno::UNO_QUERY); + return xWindow; + } +}; + +} + +std::unique_ptr<weld::Container> GtkInstanceWidget::weld_parent() const +{ + GtkWidget* pParent = gtk_widget_get_parent(m_pWidget); + if (!pParent) + return nullptr; + return std::make_unique<GtkInstanceContainer>(GTK_CONTAINER(pParent), m_pBuilder, false); +} + +namespace { + +class GtkInstanceBox : public GtkInstanceContainer, public virtual weld::Box +{ +private: + GtkBox* m_pBox; + +public: + GtkInstanceBox(GtkBox* pBox, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : GtkInstanceContainer(GTK_CONTAINER(pBox), pBuilder, bTakeOwnership) + , m_pBox(pBox) + { + } + + virtual void reorder_child(weld::Widget* pWidget, int nNewPosition) override + { + GtkInstanceWidget* pGtkWidget = dynamic_cast<GtkInstanceWidget*>(pWidget); + assert(pGtkWidget); + GtkWidget* pChild = pGtkWidget->getWidget(); + gtk_box_reorder_child(m_pBox, pChild, nNewPosition); + } +}; + + void set_cursor(GtkWidget* pWidget, const char *pName) + { + if (!gtk_widget_get_realized(pWidget)) + gtk_widget_realize(pWidget); + GdkDisplay *pDisplay = gtk_widget_get_display(pWidget); + GdkCursor *pCursor = pName ? gdk_cursor_new_from_name(pDisplay, pName) : nullptr; + gdk_window_set_cursor(gtk_widget_get_window(pWidget), pCursor); + gdk_display_flush(pDisplay); + if (pCursor) + g_object_unref(pCursor); + } +} + +namespace +{ + struct ButtonOrder + { + const char * m_aType; + int m_nPriority; + }; + + int getButtonPriority(const OString &rType) + { + static const size_t N_TYPES = 7; + static const ButtonOrder aDiscardCancelSave[N_TYPES] = + { + { "/discard", 0 }, + { "/cancel", 1 }, + { "/no", 2 }, + { "/open", 3 }, + { "/save", 3 }, + { "/yes", 3 }, + { "/ok", 3 } + }; + + static const ButtonOrder aSaveDiscardCancel[N_TYPES] = + { + { "/open", 0 }, + { "/save", 0 }, + { "/yes", 0 }, + { "/ok", 0 }, + { "/discard", 1 }, + { "/no", 1 }, + { "/cancel", 2 } + }; + + const ButtonOrder* pOrder = &aDiscardCancelSave[0]; + + const OUString &rEnv = Application::GetDesktopEnvironment(); + + if (rEnv.equalsIgnoreAsciiCase("windows") || + rEnv.equalsIgnoreAsciiCase("tde") || + rEnv.startsWithIgnoreAsciiCase("kde")) + { + pOrder = &aSaveDiscardCancel[0]; + } + + for (size_t i = 0; i < N_TYPES; ++i, ++pOrder) + { + if (rType.endsWith(pOrder->m_aType)) + return pOrder->m_nPriority; + } + + return -1; + } + + bool sortButtons(const GtkWidget* pA, const GtkWidget* pB) + { + //order within groups according to platform rules + return getButtonPriority(::get_help_id(pA)) < getButtonPriority(::get_help_id(pB)); + } + + void sort_native_button_order(GtkBox* pContainer) + { + std::vector<GtkWidget*> aChildren; + GList* pChildren = gtk_container_get_children(GTK_CONTAINER(pContainer)); + for (GList* pChild = g_list_first(pChildren); pChild; pChild = g_list_next(pChild)) + aChildren.push_back(static_cast<GtkWidget*>(pChild->data)); + g_list_free(pChildren); + + //sort child order within parent so that we match the platform button order + std::stable_sort(aChildren.begin(), aChildren.end(), sortButtons); + + for (size_t pos = 0; pos < aChildren.size(); ++pos) + gtk_box_reorder_child(pContainer, aChildren[pos], pos); + } + + Point get_csd_offset(GtkWidget* pTopLevel) + { + // try and omit drawing CSD under wayland + GList* pChildren = gtk_container_get_children(GTK_CONTAINER(pTopLevel)); + GList* pChild = g_list_first(pChildren); + + int x, y; + gtk_widget_translate_coordinates(GTK_WIDGET(pChild->data), + GTK_WIDGET(pTopLevel), + 0, 0, &x, &y); + + int innerborder = gtk_container_get_border_width(GTK_CONTAINER(pChild->data)); + g_list_free(pChildren); + + int outerborder = gtk_container_get_border_width(GTK_CONTAINER(pTopLevel)); + int totalborder = outerborder + innerborder; + x -= totalborder; + y -= totalborder; + + return Point(x, y); + } + + void do_collect_screenshot_data(GtkWidget* pItem, gpointer data) + { + GtkWidget* pTopLevel = gtk_widget_get_toplevel(pItem); + + int x, y; + gtk_widget_translate_coordinates(pItem, pTopLevel, 0, 0, &x, &y); + + Point aOffset = get_csd_offset(pTopLevel); + + GtkAllocation alloc; + gtk_widget_get_allocation(pItem, &alloc); + + const basegfx::B2IPoint aCurrentTopLeft(x - aOffset.X(), y - aOffset.Y()); + const basegfx::B2IRange aCurrentRange(aCurrentTopLeft, aCurrentTopLeft + basegfx::B2IPoint(alloc.width, alloc.height)); + + if (!aCurrentRange.isEmpty()) + { + weld::ScreenShotCollection* pCollection = static_cast<weld::ScreenShotCollection*>(data); + pCollection->emplace_back(::get_help_id(pItem), aCurrentRange); + } + + if (GTK_IS_CONTAINER(pItem)) + gtk_container_forall(GTK_CONTAINER(pItem), do_collect_screenshot_data, data); + } + + tools::Rectangle get_monitor_workarea(GtkWidget* pWindow) + { + GdkScreen* pScreen = gtk_widget_get_screen(pWindow); + gint nMonitor = gdk_screen_get_monitor_at_window(pScreen, gtk_widget_get_window(pWindow)); + GdkRectangle aRect; + gdk_screen_get_monitor_workarea(pScreen, nMonitor, &aRect); + return tools::Rectangle(aRect.x, aRect.y, aRect.x + aRect.width, aRect.y + aRect.height); + } + + +class GtkInstanceWindow : public GtkInstanceContainer, public virtual weld::Window +{ +private: + GtkWindow* m_pWindow; + rtl::Reference<SalGtkXWindow> m_xWindow; //uno api + gulong m_nToplevelFocusChangedSignalId; + + static gboolean help_pressed(GtkAccelGroup*, GObject*, guint, GdkModifierType, gpointer widget) + { + GtkInstanceWindow* pThis = static_cast<GtkInstanceWindow*>(widget); + pThis->help(); + return true; + } + + static void signalToplevelFocusChanged(GtkWindow*, GParamSpec*, gpointer widget) + { + GtkInstanceWindow* pThis = static_cast<GtkInstanceWindow*>(widget); + pThis->signal_toplevel_focus_changed(); + } + +protected: + void help(); +public: + GtkInstanceWindow(GtkWindow* pWindow, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : GtkInstanceContainer(GTK_CONTAINER(pWindow), pBuilder, bTakeOwnership) + , m_pWindow(pWindow) + , m_nToplevelFocusChangedSignalId(0) + { + const bool bIsFrameWeld = pBuilder == nullptr; + if (!bIsFrameWeld) + { + //hook up F1 to show help + GtkAccelGroup *pGroup = gtk_accel_group_new(); + GClosure* closure = g_cclosure_new(G_CALLBACK(help_pressed), this, nullptr); + gtk_accel_group_connect(pGroup, GDK_KEY_F1, static_cast<GdkModifierType>(0), GTK_ACCEL_LOCKED, closure); + gtk_window_add_accel_group(pWindow, pGroup); + } + } + + virtual void set_title(const OUString& rTitle) override + { + ::set_title(m_pWindow, rTitle); + } + + virtual OUString get_title() const override + { + return ::get_title(m_pWindow); + } + + virtual css::uno::Reference<css::awt::XWindow> GetXWindow() override + { + if (!m_xWindow.is()) + m_xWindow.set(new SalGtkXWindow(this, m_pWidget)); + return css::uno::Reference<css::awt::XWindow>(m_xWindow.get()); + } + + virtual void set_busy_cursor(bool bBusy) override + { + set_cursor(m_pWidget, bBusy ? "progress" : nullptr); + } + + virtual void set_modal(bool bModal) override + { + gtk_window_set_modal(m_pWindow, bModal); + } + + virtual bool get_modal() const override + { + return gtk_window_get_modal(m_pWindow); + } + + virtual void resize_to_request() override + { + gtk_window_resize(m_pWindow, 1, 1); + } + + virtual void window_move(int x, int y) override + { + gtk_window_move(m_pWindow, x, y); + } + + virtual SystemEnvData get_system_data() const override + { + assert(false && "nothing should call this impl, yet anyway, if ever"); + return SystemEnvData(); + } + + virtual Size get_size() const override + { + int current_width, current_height; + gtk_window_get_size(m_pWindow, ¤t_width, ¤t_height); + return Size(current_width, current_height); + } + + virtual Point get_position() const override + { + int current_x, current_y; + gtk_window_get_position(m_pWindow, ¤t_x, ¤t_y); + return Point(current_x, current_y); + } + + virtual tools::Rectangle get_monitor_workarea() const override + { + return ::get_monitor_workarea(GTK_WIDGET(m_pWindow)); + } + + virtual void set_centered_on_parent(bool bTrackGeometryRequests) override + { + if (bTrackGeometryRequests) + gtk_window_set_position(m_pWindow, GTK_WIN_POS_CENTER_ALWAYS); + else + gtk_window_set_position(m_pWindow, GTK_WIN_POS_CENTER_ON_PARENT); + } + + virtual bool get_resizable() const override + { + return gtk_window_get_resizable(m_pWindow); + } + + virtual bool has_toplevel_focus() const override + { + return gtk_window_has_toplevel_focus(m_pWindow); + } + + virtual void present() override + { + gtk_window_present(m_pWindow); + } + + virtual void set_window_state(const OString& rStr) override + { + WindowStateData aData; + ImplWindowStateFromStr( aData, rStr ); + + auto nMask = aData.GetMask(); + auto nState = aData.GetState() & WindowStateState::SystemMask; + + if (nMask & WindowStateMask::Width && nMask & WindowStateMask::Height) + { + gtk_window_set_default_size(m_pWindow, aData.GetWidth(), aData.GetHeight()); + } + if (nMask & WindowStateMask::State) + { + if (nState & WindowStateState::Maximized) + gtk_window_maximize(m_pWindow); + else + gtk_window_unmaximize(m_pWindow); + } + } + + virtual OString get_window_state(WindowStateMask nMask) const override + { + bool bPositioningAllowed = true; +#if defined(GDK_WINDOWING_WAYLAND) + // drop x/y when under wayland + GdkDisplay *pDisplay = gtk_widget_get_display(m_pWidget); + bPositioningAllowed = !DLSYM_GDK_IS_WAYLAND_DISPLAY(pDisplay); +#endif + + WindowStateData aData; + WindowStateMask nAvailable = WindowStateMask::State | + WindowStateMask::Width | WindowStateMask::Height; + if (bPositioningAllowed) + nAvailable |= WindowStateMask::X | WindowStateMask::Y; + aData.SetMask(nMask & nAvailable); + + if (nMask & WindowStateMask::State) + { + WindowStateState nState = WindowStateState::Normal; + if (gtk_window_is_maximized(m_pWindow)) + nState |= WindowStateState::Maximized; + aData.SetState(nState); + } + + if (bPositioningAllowed && (nMask & (WindowStateMask::X | WindowStateMask::Y))) + { + auto aPos = get_position(); + aData.SetX(aPos.X()); + aData.SetY(aPos.Y()); + } + + if (nMask & (WindowStateMask::Width | WindowStateMask::Height)) + { + auto aSize = get_size(); + aData.SetWidth(aSize.Width()); + aData.SetHeight(aSize.Height()); + } + + return aData.ToStr(); + } + + virtual void connect_toplevel_focus_changed(const Link<weld::Widget&, void>& rLink) override + { + assert(!m_nToplevelFocusChangedSignalId); + m_nToplevelFocusChangedSignalId = g_signal_connect(m_pWindow, "notify::has-toplevel-focus", G_CALLBACK(signalToplevelFocusChanged), this); + weld::Window::connect_toplevel_focus_changed(rLink); + } + + virtual void disable_notify_events() override + { + if (m_nToplevelFocusChangedSignalId) + g_signal_handler_block(m_pWidget, m_nToplevelFocusChangedSignalId); + GtkInstanceContainer::disable_notify_events(); + } + + virtual void enable_notify_events() override + { + GtkInstanceContainer::enable_notify_events(); + if (m_nToplevelFocusChangedSignalId) + g_signal_handler_unblock(m_pWidget, m_nToplevelFocusChangedSignalId); + } + + virtual void draw(VirtualDevice& rOutput) override + { + // detect if we have to manually setup its size + bool bAlreadyRealized = gtk_widget_get_realized(GTK_WIDGET(m_pWindow)); + // has to be visible for draw to work + bool bAlreadyVisible = gtk_widget_get_visible(GTK_WIDGET(m_pWindow)); + if (!bAlreadyVisible) + { + if (GTK_IS_DIALOG(m_pWindow)) + sort_native_button_order(GTK_BOX(gtk_dialog_get_action_area(GTK_DIALOG(m_pWindow)))); + gtk_widget_show(GTK_WIDGET(m_pWindow)); + } + + if (!bAlreadyRealized) + { + GtkAllocation allocation; + gtk_widget_realize(GTK_WIDGET(m_pWindow)); + gtk_widget_get_allocation(GTK_WIDGET(m_pWindow), &allocation); + gtk_widget_size_allocate(GTK_WIDGET(m_pWindow), &allocation); + } + + rOutput.SetOutputSizePixel(get_size()); + cairo_surface_t* pSurface = get_underlying_cairo_surface(rOutput); + cairo_t* cr = cairo_create(pSurface); + + Point aOffset = get_csd_offset(GTK_WIDGET(m_pWindow)); + +#if defined(GDK_WINDOWING_X11) + GdkDisplay *pDisplay = gtk_widget_get_display(GTK_WIDGET(m_pWindow)); + if (DLSYM_GDK_IS_X11_DISPLAY(pDisplay)) + assert(aOffset.X() == 0 && aOffset.Y() == 0 && "expected offset of 0 under X"); +#endif + + cairo_translate(cr, -aOffset.X(), -aOffset.Y()); + + gtk_widget_draw(GTK_WIDGET(m_pWindow), cr); + + cairo_destroy(cr); + + if (!bAlreadyVisible) + gtk_widget_hide(GTK_WIDGET(m_pWindow)); + if (!bAlreadyRealized) + gtk_widget_unrealize(GTK_WIDGET(m_pWindow)); + } + + virtual weld::ScreenShotCollection collect_screenshot_data() override + { + weld::ScreenShotCollection aRet; + + gtk_container_foreach(GTK_CONTAINER(m_pWindow), do_collect_screenshot_data, &aRet); + + return aRet; + } + + virtual ~GtkInstanceWindow() override + { + if (m_nToplevelFocusChangedSignalId) + g_signal_handler_disconnect(m_pWindow, m_nToplevelFocusChangedSignalId); + if (m_xWindow.is()) + m_xWindow->clear(); + } +}; + +class GtkInstanceDialog; + +struct DialogRunner +{ + GtkWindow* m_pDialog; + GtkInstanceDialog *m_pInstance; + gint m_nResponseId; + GMainLoop *m_pLoop; + VclPtr<vcl::Window> m_xFrameWindow; + int m_nModalDepth; + + DialogRunner(GtkWindow* pDialog, GtkInstanceDialog* pInstance) + : m_pDialog(pDialog) + , m_pInstance(pInstance) + , m_nResponseId(GTK_RESPONSE_NONE) + , m_pLoop(nullptr) + , m_nModalDepth(0) + { + GtkWindow* pParent = gtk_window_get_transient_for(m_pDialog); + GtkSalFrame* pFrame = pParent ? GtkSalFrame::getFromWindow(GTK_WIDGET(pParent)) : nullptr; + m_xFrameWindow = pFrame ? pFrame->GetWindow() : nullptr; + } + + bool loop_is_running() const + { + return m_pLoop && g_main_loop_is_running(m_pLoop); + } + + void loop_quit() + { + if (g_main_loop_is_running(m_pLoop)) + g_main_loop_quit(m_pLoop); + } + + static void signal_response(GtkDialog*, gint nResponseId, gpointer data); + static void signal_cancel(GtkAssistant*, gpointer data); + + static gboolean signal_delete(GtkDialog* pDialog, GdkEventAny*, gpointer data) + { + DialogRunner* pThis = static_cast<DialogRunner*>(data); + if (GTK_IS_ASSISTANT(pThis->m_pDialog)) + { + // An assistant isn't a dialog, but we want to treat it like one + signal_response(pDialog, GTK_RESPONSE_DELETE_EVENT, data); + } + else + pThis->loop_quit(); + return true; /* Do not destroy */ + } + + static void signal_destroy(GtkDialog*, gpointer data) + { + DialogRunner* pThis = static_cast<DialogRunner*>(data); + pThis->loop_quit(); + } + + void inc_modal_count() + { + if (m_xFrameWindow) + { + m_xFrameWindow->IncModalCount(); + if (m_nModalDepth == 0) + m_xFrameWindow->ImplGetFrame()->NotifyModalHierarchy(true); + ++m_nModalDepth; + } + } + + void dec_modal_count() + { + if (m_xFrameWindow) + { + m_xFrameWindow->DecModalCount(); + --m_nModalDepth; + if (m_nModalDepth == 0) + m_xFrameWindow->ImplGetFrame()->NotifyModalHierarchy(false); + } + } + + // same as gtk_dialog_run except that unmap doesn't auto-respond + // so we can hide the dialog and restore it without a response getting + // triggered + gint run() + { + g_object_ref(m_pDialog); + + inc_modal_count(); + + bool bWasModal = gtk_window_get_modal(m_pDialog); + if (!bWasModal) + gtk_window_set_modal(m_pDialog, true); + + if (!gtk_widget_get_visible(GTK_WIDGET(m_pDialog))) + gtk_widget_show(GTK_WIDGET(m_pDialog)); + + gulong nSignalResponseId = GTK_IS_DIALOG(m_pDialog) ? g_signal_connect(m_pDialog, "response", G_CALLBACK(signal_response), this) : 0; + gulong nSignalCancelId = GTK_IS_ASSISTANT(m_pDialog) ? g_signal_connect(m_pDialog, "cancel", G_CALLBACK(signal_cancel), this) : 0; + gulong nSignalDeleteId = g_signal_connect(m_pDialog, "delete-event", G_CALLBACK(signal_delete), this); + gulong nSignalDestroyId = g_signal_connect(m_pDialog, "destroy", G_CALLBACK(signal_destroy), this); + + m_pLoop = g_main_loop_new(nullptr, false); + m_nResponseId = GTK_RESPONSE_NONE; + + gdk_threads_leave(); + g_main_loop_run(m_pLoop); + gdk_threads_enter(); + + g_main_loop_unref(m_pLoop); + + m_pLoop = nullptr; + + if (!bWasModal) + gtk_window_set_modal(m_pDialog, false); + + if (nSignalResponseId) + g_signal_handler_disconnect(m_pDialog, nSignalResponseId); + if (nSignalCancelId) + g_signal_handler_disconnect(m_pDialog, nSignalCancelId); + g_signal_handler_disconnect(m_pDialog, nSignalDeleteId); + g_signal_handler_disconnect(m_pDialog, nSignalDestroyId); + + dec_modal_count(); + + g_object_unref(m_pDialog); + + return m_nResponseId; + } + + ~DialogRunner() + { + if (m_xFrameWindow && m_nModalDepth) + { + // if, like the calc validation dialog does, the modality was + // toggled off during execution ensure that on cleanup the parent + // is left in the state it was found + while (m_nModalDepth++ < 0) + m_xFrameWindow->IncModalCount(); + } + } +}; + +} + +typedef std::set<GtkWidget*> winset; + +namespace +{ + void hideUnless(GtkContainer *pTop, const winset& rVisibleWidgets, + std::vector<GtkWidget*> &rWasVisibleWidgets) + { + GList* pChildren = gtk_container_get_children(pTop); + for (GList* pEntry = g_list_first(pChildren); pEntry; pEntry = g_list_next(pEntry)) + { + GtkWidget* pChild = static_cast<GtkWidget*>(pEntry->data); + if (!gtk_widget_get_visible(pChild)) + continue; + if (rVisibleWidgets.find(pChild) == rVisibleWidgets.end()) + { + g_object_ref(pChild); + rWasVisibleWidgets.emplace_back(pChild); + gtk_widget_hide(pChild); + } + else if (GTK_IS_CONTAINER(pChild)) + { + hideUnless(GTK_CONTAINER(pChild), rVisibleWidgets, rWasVisibleWidgets); + } + } + g_list_free(pChildren); + } + +class GtkInstanceButton; + +class GtkInstanceDialog : public GtkInstanceWindow, public virtual weld::Dialog +{ +private: + GtkWindow* m_pDialog; + DialogRunner m_aDialogRun; + std::shared_ptr<weld::DialogController> m_xDialogController; + // Used to keep ourself alive during a runAsync(when doing runAsync without a DialogController) + std::shared_ptr<weld::Dialog> m_xRunAsyncSelf; + std::function<void(sal_Int32)> m_aFunc; + gulong m_nCloseSignalId; + gulong m_nResponseSignalId; + gulong m_nCancelSignalId; + gulong m_nSignalDeleteId; + + // for calc ref dialog that shrink to range selection widgets and resize back + GtkWidget* m_pRefEdit; + std::vector<GtkWidget*> m_aHiddenWidgets; // vector of hidden Controls + int m_nOldEditWidth; // Original width of the input field + int m_nOldEditWidthReq; // Original width request of the input field + int m_nOldBorderWidth; // border width for expanded dialog + + void signal_close() + { + close(true); + } + + static void signalClose(GtkWidget*, gpointer widget) + { + GtkInstanceDialog* pThis = static_cast<GtkInstanceDialog*>(widget); + pThis->signal_close(); + } + + static void signalAsyncResponse(GtkWidget*, gint ret, gpointer widget) + { + GtkInstanceDialog* pThis = static_cast<GtkInstanceDialog*>(widget); + pThis->asyncresponse(ret); + } + + static void signalAsyncCancel(GtkAssistant*, gpointer widget) + { + GtkInstanceDialog* pThis = static_cast<GtkInstanceDialog*>(widget); + // make esc in an assistant act as if cancel button was pressed + pThis->close(false); + } + + static gboolean signalAsyncDelete(GtkWidget* pDialog, GdkEventAny*, gpointer widget) + { + GtkInstanceDialog* pThis = static_cast<GtkInstanceDialog*>(widget); + if (GTK_IS_ASSISTANT(pThis->m_pDialog)) + { + // An assistant isn't a dialog, but we want to treat it like one + signalAsyncResponse(pDialog, GTK_RESPONSE_DELETE_EVENT, widget); + } + return true; /* Do not destroy */ + } + + static int GtkToVcl(int ret) + { + if (ret == GTK_RESPONSE_OK) + ret = RET_OK; + else if (ret == GTK_RESPONSE_CANCEL) + ret = RET_CANCEL; + else if (ret == GTK_RESPONSE_DELETE_EVENT) + ret = RET_CANCEL; + else if (ret == GTK_RESPONSE_CLOSE) + ret = RET_CLOSE; + else if (ret == GTK_RESPONSE_YES) + ret = RET_YES; + else if (ret == GTK_RESPONSE_NO) + ret = RET_NO; + else if (ret == GTK_RESPONSE_HELP) + ret = RET_HELP; + return ret; + } + + static int VclToGtk(int nResponse) + { + if (nResponse == RET_OK) + return GTK_RESPONSE_OK; + else if (nResponse == RET_CANCEL) + return GTK_RESPONSE_CANCEL; + else if (nResponse == RET_CLOSE) + return GTK_RESPONSE_CLOSE; + else if (nResponse == RET_YES) + return GTK_RESPONSE_YES; + else if (nResponse == RET_NO) + return GTK_RESPONSE_NO; + else if (nResponse == RET_HELP) + return GTK_RESPONSE_HELP; + return nResponse; + } + + void asyncresponse(gint ret); + + static void signalActivate(GtkMenuItem*, gpointer data) + { + bool* pActivate = static_cast<bool*>(data); + *pActivate = true; + } + + bool signal_screenshot_popup_menu(GdkEventButton* pEvent) + { + GtkWidget *pMenu = gtk_menu_new(); + + GtkWidget* pMenuItem = gtk_menu_item_new_with_mnemonic(MapToGtkAccelerator(VclResId(SV_BUTTONTEXT_SCREENSHOT)).getStr()); + gtk_menu_shell_append(GTK_MENU_SHELL(pMenu), pMenuItem); + bool bActivate(false); + g_signal_connect(pMenuItem, "activate", G_CALLBACK(signalActivate), &bActivate); + gtk_widget_show(pMenuItem); + + int button, event_time; + if (pEvent) + { + button = pEvent->button; + event_time = pEvent->time; + } + else + { + button = 0; + event_time = gtk_get_current_event_time(); + } + + gtk_menu_attach_to_widget(GTK_MENU(pMenu), GTK_WIDGET(m_pDialog), nullptr); + + GMainLoop* pLoop = g_main_loop_new(nullptr, true); + gulong nSignalId = g_signal_connect_swapped(G_OBJECT(pMenu), "deactivate", G_CALLBACK(g_main_loop_quit), pLoop); + + gtk_menu_popup(GTK_MENU(pMenu), nullptr, nullptr, nullptr, nullptr, button, event_time); + + if (g_main_loop_is_running(pLoop)) + { + gdk_threads_leave(); + g_main_loop_run(pLoop); + gdk_threads_enter(); + } + + g_main_loop_unref(pLoop); + g_signal_handler_disconnect(pMenu, nSignalId); + gtk_menu_detach(GTK_MENU(pMenu)); + + if (bActivate) + { + // open screenshot annotation dialog + VclAbstractDialogFactory* pFact = VclAbstractDialogFactory::Create(); + VclPtr<AbstractScreenshotAnnotationDlg> xTmp = pFact->CreateScreenshotAnnotationDlg(*this); + ScopedVclPtr<AbstractScreenshotAnnotationDlg> xDialog(xTmp); + xDialog->Execute(); + } + + return false; + } + + static gboolean signalScreenshotPopupMenu(GtkWidget*, gpointer widget) + { + GtkInstanceDialog* pThis = static_cast<GtkInstanceDialog*>(widget); + return pThis->signal_screenshot_popup_menu(nullptr); + } + + static gboolean signalScreenshotButton(GtkWidget*, GdkEventButton* pEvent, gpointer widget) + { + GtkInstanceDialog* pThis = static_cast<GtkInstanceDialog*>(widget); + SolarMutexGuard aGuard; + return pThis->signal_screenshot_button(pEvent); + } + + bool signal_screenshot_button(GdkEventButton* pEvent) + { + if (gdk_event_triggers_context_menu(reinterpret_cast<GdkEvent*>(pEvent)) && pEvent->type == GDK_BUTTON_PRESS) + { + //if handled for context menu, stop processing + return signal_screenshot_popup_menu(pEvent); + } + return false; + } + +public: + GtkInstanceDialog(GtkWindow* pDialog, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : GtkInstanceWindow(pDialog, pBuilder, bTakeOwnership) + , m_pDialog(pDialog) + , m_aDialogRun(pDialog, this) + , m_nResponseSignalId(0) + , m_nCancelSignalId(0) + , m_nSignalDeleteId(0) + , m_pRefEdit(nullptr) + , m_nOldEditWidth(0) + , m_nOldEditWidthReq(0) + , m_nOldBorderWidth(0) + { + if (GTK_IS_DIALOG(m_pDialog) || GTK_IS_ASSISTANT(m_pDialog)) + m_nCloseSignalId = g_signal_connect(m_pDialog, "close", G_CALLBACK(signalClose), this); + else + m_nCloseSignalId = 0; + const bool bScreenshotMode(officecfg::Office::Common::Misc::ScreenshotMode::get()); + if (bScreenshotMode) + { + g_signal_connect(m_pDialog, "popup-menu", G_CALLBACK(signalScreenshotPopupMenu), this); + g_signal_connect(m_pDialog, "button-press-event", G_CALLBACK(signalScreenshotButton), this); + } + } + + virtual bool runAsync(std::shared_ptr<weld::DialogController> rDialogController, const std::function<void(sal_Int32)>& func) override + { + assert(!m_nResponseSignalId && !m_nCancelSignalId && !m_nSignalDeleteId); + + m_xDialogController = rDialogController; + m_aFunc = func; + + if (get_modal()) + m_aDialogRun.inc_modal_count(); + show(); + + m_nResponseSignalId = GTK_IS_DIALOG(m_pDialog) ? g_signal_connect(m_pDialog, "response", G_CALLBACK(signalAsyncResponse), this) : 0; + m_nCancelSignalId = GTK_IS_ASSISTANT(m_pDialog) ? g_signal_connect(m_pDialog, "cancel", G_CALLBACK(signalAsyncCancel), this) : 0; + m_nSignalDeleteId = g_signal_connect(m_pDialog, "delete-event", G_CALLBACK(signalAsyncDelete), this); + + return true; + } + + virtual bool runAsync(std::shared_ptr<Dialog> const & rxSelf, const std::function<void(sal_Int32)>& func) override + { + assert( rxSelf.get() == this ); + assert(!m_nResponseSignalId && !m_nCancelSignalId && !m_nSignalDeleteId); + + // In order to store a shared_ptr to ourself, we have to have been constructed by make_shared, + // which is that rxSelf enforces. + m_xRunAsyncSelf = rxSelf; + m_aFunc = func; + + if (get_modal()) + m_aDialogRun.inc_modal_count(); + show(); + + m_nResponseSignalId = GTK_IS_DIALOG(m_pDialog) ? g_signal_connect(m_pDialog, "response", G_CALLBACK(signalAsyncResponse), this) : 0; + m_nCancelSignalId = GTK_IS_ASSISTANT(m_pDialog) ? g_signal_connect(m_pDialog, "cancel", G_CALLBACK(signalAsyncCancel), this) : 0; + m_nSignalDeleteId = g_signal_connect(m_pDialog, "delete-event", G_CALLBACK(signalAsyncDelete), this); + + return true; + } + + GtkInstanceButton* has_click_handler(int nResponse); + + virtual int run() override; + + virtual void show() override + { + if (gtk_widget_get_visible(m_pWidget)) + return; + if (GTK_IS_DIALOG(m_pDialog)) + sort_native_button_order(GTK_BOX(gtk_dialog_get_action_area(GTK_DIALOG(m_pDialog)))); + GtkInstanceWindow::show(); + } + + virtual void set_modal(bool bModal) override + { + if (get_modal() == bModal) + return; + GtkInstanceWindow::set_modal(bModal); + /* if change the dialog modality while its running, then also change the parent LibreOffice window + modal count, we typically expect the dialog modality to be restored to its original state + + This change modality while running case is for... + + a) the calc/chart dialogs which put up an extra range chooser + dialog, hides the original, the user can select a range of cells and + on completion the original dialog is restored + + b) the validity dialog in calc + */ + // tdf#135567 we know we are running in the sync case if loop_is_running is true + // but for the async case we instead check for m_xDialogController which is set in + // runAsync and cleared in asyncresponse + if (m_aDialogRun.loop_is_running() || m_xDialogController) + { + if (bModal) + m_aDialogRun.inc_modal_count(); + else + m_aDialogRun.dec_modal_count(); + } + } + + virtual void response(int nResponse) override; + + virtual void add_button(const OUString& rText, int nResponse, const OString& rHelpId) override + { + GtkWidget* pWidget = gtk_dialog_add_button(GTK_DIALOG(m_pDialog), MapToGtkAccelerator(rText).getStr(), VclToGtk(nResponse)); + if (!rHelpId.isEmpty()) + ::set_help_id(pWidget, rHelpId); + } + + virtual void set_default_response(int nResponse) override + { + gtk_dialog_set_default_response(GTK_DIALOG(m_pDialog), VclToGtk(nResponse)); + } + + virtual GtkButton* get_widget_for_response(int nGtkResponse) + { + return GTK_BUTTON(gtk_dialog_get_widget_for_response(GTK_DIALOG(m_pDialog), nGtkResponse)); + } + + virtual weld::Button* weld_widget_for_response(int nVclResponse) override; + + virtual Container* weld_content_area() override + { + return new GtkInstanceContainer(GTK_CONTAINER(gtk_dialog_get_content_area(GTK_DIALOG(m_pDialog))), m_pBuilder, false); + } + + virtual void collapse(weld::Widget* pEdit, weld::Widget* pButton) override + { + GtkInstanceWidget* pVclEdit = dynamic_cast<GtkInstanceWidget*>(pEdit); + assert(pVclEdit); + GtkInstanceWidget* pVclButton = dynamic_cast<GtkInstanceWidget*>(pButton); + + GtkWidget* pRefEdit = pVclEdit->getWidget(); + GtkWidget* pRefBtn = pVclButton ? pVclButton->getWidget() : nullptr; + + m_nOldEditWidth = gtk_widget_get_allocated_width(pRefEdit); + + gtk_widget_get_size_request(pRefEdit, &m_nOldEditWidthReq, nullptr); + + //We want just pRefBtn and pRefEdit to be shown + //mark widgets we want to be visible, starting with pRefEdit + //and all its direct parents. + winset aVisibleWidgets; + GtkWidget *pContentArea = gtk_dialog_get_content_area(GTK_DIALOG(m_pDialog)); + for (GtkWidget *pCandidate = pRefEdit; + pCandidate && pCandidate != pContentArea && gtk_widget_get_visible(pCandidate); + pCandidate = gtk_widget_get_parent(pCandidate)) + { + aVisibleWidgets.insert(pCandidate); + } + //same again with pRefBtn, except stop if there's a + //shared parent in the existing widgets + for (GtkWidget *pCandidate = pRefBtn; + pCandidate && pCandidate != pContentArea && gtk_widget_get_visible(pCandidate); + pCandidate = gtk_widget_get_parent(pCandidate)) + { + if (aVisibleWidgets.insert(pCandidate).second) + break; + } + + //hide everything except the aVisibleWidgets + hideUnless(GTK_CONTAINER(pContentArea), aVisibleWidgets, m_aHiddenWidgets); + + gtk_widget_set_size_request(pRefEdit, m_nOldEditWidth, -1); + m_nOldBorderWidth = gtk_container_get_border_width(GTK_CONTAINER(m_pDialog)); + gtk_container_set_border_width(GTK_CONTAINER(m_pDialog), 0); + if (GtkWidget* pActionArea = gtk_dialog_get_action_area(GTK_DIALOG(m_pDialog))) + gtk_widget_hide(pActionArea); + + // calc's insert->function is springing back to its original size if the ref-button + // is used to shrink the dialog down and then the user clicks in the calc area to do + // the selection +#if defined(GDK_WINDOWING_WAYLAND) + bool bWorkaroundSizeSpringingBack = DLSYM_GDK_IS_WAYLAND_DISPLAY(gtk_widget_get_display(m_pWidget)); + if (bWorkaroundSizeSpringingBack) + gtk_widget_unmap(GTK_WIDGET(m_pDialog)); +#endif + + resize_to_request(); + +#if defined(GDK_WINDOWING_WAYLAND) + if (bWorkaroundSizeSpringingBack) + gtk_widget_map(GTK_WIDGET(m_pDialog)); +#endif + + m_pRefEdit = pRefEdit; + } + + virtual void undo_collapse() override + { + // All others: Show(); + for (GtkWidget* pWindow : m_aHiddenWidgets) + { + gtk_widget_show(pWindow); + g_object_unref(pWindow); + } + m_aHiddenWidgets.clear(); + + gtk_widget_set_size_request(m_pRefEdit, m_nOldEditWidthReq, -1); + m_pRefEdit = nullptr; + gtk_container_set_border_width(GTK_CONTAINER(m_pDialog), m_nOldBorderWidth); + if (GtkWidget* pActionArea = gtk_dialog_get_action_area(GTK_DIALOG(m_pDialog))) + gtk_widget_show(pActionArea); + resize_to_request(); + present(); + } + + void close(bool bCloseSignal); + + virtual void SetInstallLOKNotifierHdl(const Link<void*, vcl::ILibreOfficeKitNotifier*>&) override + { + //not implemented for the gtk variant + } + + virtual ~GtkInstanceDialog() override + { + if (!m_aHiddenWidgets.empty()) + { + for (GtkWidget* pWindow : m_aHiddenWidgets) + g_object_unref(pWindow); + m_aHiddenWidgets.clear(); + } + + if (m_nCloseSignalId) + g_signal_handler_disconnect(m_pDialog, m_nCloseSignalId); + assert(!m_nResponseSignalId && !m_nCancelSignalId && !m_nSignalDeleteId); + } +}; + +} + +void DialogRunner::signal_response(GtkDialog*, gint nResponseId, gpointer data) +{ + DialogRunner* pThis = static_cast<DialogRunner*>(data); + + // make GTK_RESPONSE_DELETE_EVENT act as if cancel button was pressed + if (nResponseId == GTK_RESPONSE_DELETE_EVENT) + { + pThis->m_pInstance->close(false); + return; + } + + pThis->m_nResponseId = nResponseId; + pThis->loop_quit(); +} + +void DialogRunner::signal_cancel(GtkAssistant*, gpointer data) +{ + DialogRunner* pThis = static_cast<DialogRunner*>(data); + + // make esc in an assistant act as if cancel button was pressed + pThis->m_pInstance->close(false); +} + +namespace { + +class GtkInstanceMessageDialog : public GtkInstanceDialog, public virtual weld::MessageDialog +{ +private: + GtkMessageDialog* m_pMessageDialog; +public: + GtkInstanceMessageDialog(GtkMessageDialog* pMessageDialog, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : GtkInstanceDialog(GTK_WINDOW(pMessageDialog), pBuilder, bTakeOwnership) + , m_pMessageDialog(pMessageDialog) + { + } + + virtual void set_primary_text(const OUString& rText) override + { + ::set_primary_text(m_pMessageDialog, rText); + } + + virtual OUString get_primary_text() const override + { + return ::get_primary_text(m_pMessageDialog); + } + + virtual void set_secondary_text(const OUString& rText) override + { + ::set_secondary_text(m_pMessageDialog, rText); + } + + virtual OUString get_secondary_text() const override + { + return ::get_secondary_text(m_pMessageDialog); + } + + virtual Container* weld_message_area() override + { + return new GtkInstanceContainer(GTK_CONTAINER(gtk_message_dialog_get_message_area(m_pMessageDialog)), m_pBuilder, false); + } +}; + +class GtkInstanceAssistant : public GtkInstanceDialog, public virtual weld::Assistant +{ +private: + GtkAssistant* m_pAssistant; + GtkWidget* m_pSidebar; + GtkWidget* m_pSidebarEventBox; + GtkButtonBox* m_pButtonBox; + GtkButton* m_pHelp; + GtkButton* m_pBack; + GtkButton* m_pNext; + GtkButton* m_pFinish; + GtkButton* m_pCancel; + gulong m_nButtonPressSignalId; + std::vector<std::unique_ptr<GtkInstanceContainer>> m_aPages; + std::map<OString, bool> m_aNotClickable; + + int find_page(const OString& rIdent) const + { + int nPages = gtk_assistant_get_n_pages(m_pAssistant); + for (int i = 0; i < nPages; ++i) + { + GtkWidget* pPage = gtk_assistant_get_nth_page(m_pAssistant, i); + const gchar* pStr = gtk_buildable_get_name(GTK_BUILDABLE(pPage)); + if (g_strcmp0(pStr, rIdent.getStr()) == 0) + return i; + } + return -1; + } + + static void wrap_sidebar_label(GtkWidget *pWidget, gpointer /*user_data*/) + { + if (GTK_IS_LABEL(pWidget)) + { + gtk_label_set_line_wrap(GTK_LABEL(pWidget), true); + gtk_label_set_width_chars(GTK_LABEL(pWidget), 22); + gtk_label_set_max_width_chars(GTK_LABEL(pWidget), 22); + } + } + + static void find_sidebar(GtkWidget *pWidget, gpointer user_data) + { + if (g_strcmp0(gtk_buildable_get_name(GTK_BUILDABLE(pWidget)), "sidebar") == 0) + { + GtkWidget **ppSidebar = static_cast<GtkWidget**>(user_data); + *ppSidebar = pWidget; + } + if (GTK_IS_CONTAINER(pWidget)) + gtk_container_forall(GTK_CONTAINER(pWidget), find_sidebar, user_data); + } + + static void signalHelpClicked(GtkButton*, gpointer widget) + { + GtkInstanceAssistant* pThis = static_cast<GtkInstanceAssistant*>(widget); + pThis->signal_help_clicked(); + } + + void signal_help_clicked() + { + help(); + } + + static gboolean signalButton(GtkWidget*, GdkEventButton* pEvent, gpointer widget) + { + GtkInstanceAssistant* pThis = static_cast<GtkInstanceAssistant*>(widget); + SolarMutexGuard aGuard; + return pThis->signal_button(pEvent); + } + + bool signal_button(GdkEventButton* pEvent) + { + int nNewCurrentPage = -1; + + GtkAllocation allocation; + + int nPageIndex = 0; + GList* pChildren = gtk_container_get_children(GTK_CONTAINER(m_pSidebar)); + for (GList* pChild = g_list_first(pChildren); pChild; pChild = g_list_next(pChild)) + { + GtkWidget* pWidget = static_cast<GtkWidget*>(pChild->data); + if (!gtk_widget_get_visible(pWidget)) + continue; + + gtk_widget_get_allocation(pWidget, &allocation); + + gint dest_x1, dest_y1; + gtk_widget_translate_coordinates(pWidget, + m_pSidebarEventBox, + 0, + 0, + &dest_x1, + &dest_y1); + + gint dest_x2, dest_y2; + gtk_widget_translate_coordinates(pWidget, + m_pSidebarEventBox, + allocation.width, + allocation.height, + &dest_x2, + &dest_y2); + + + if (pEvent->x >= dest_x1 && pEvent->x <= dest_x2 && pEvent->y >= dest_y1 && pEvent->y <= dest_y2) + { + nNewCurrentPage = nPageIndex; + break; + } + + ++nPageIndex; + } + g_list_free(pChildren); + + if (nNewCurrentPage != -1 && nNewCurrentPage != get_current_page()) + { + OString sIdent = get_page_ident(nNewCurrentPage); + if (!m_aNotClickable[sIdent] && !signal_jump_page(sIdent)) + set_current_page(nNewCurrentPage); + } + + return false; + } + +public: + GtkInstanceAssistant(GtkAssistant* pAssistant, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : GtkInstanceDialog(GTK_WINDOW(pAssistant), pBuilder, bTakeOwnership) + , m_pAssistant(pAssistant) + , m_pSidebar(nullptr) + { + m_pButtonBox = GTK_BUTTON_BOX(gtk_button_box_new(GTK_ORIENTATION_HORIZONTAL)); + gtk_button_box_set_layout(m_pButtonBox, GTK_BUTTONBOX_END); + gtk_box_set_spacing(GTK_BOX(m_pButtonBox), 6); + + m_pBack = GTK_BUTTON(gtk_button_new_with_mnemonic(MapToGtkAccelerator(GetStandardText(StandardButtonType::Back)).getStr())); + gtk_widget_set_can_default(GTK_WIDGET(m_pBack), true); + gtk_buildable_set_name(GTK_BUILDABLE(m_pBack), "previous"); + gtk_box_pack_end(GTK_BOX(m_pButtonBox), GTK_WIDGET(m_pBack), false, false, 0); + + m_pNext = GTK_BUTTON(gtk_button_new_with_mnemonic(MapToGtkAccelerator(GetStandardText(StandardButtonType::Next)).getStr())); + gtk_widget_set_can_default(GTK_WIDGET(m_pNext), true); + gtk_buildable_set_name(GTK_BUILDABLE(m_pNext), "next"); + gtk_box_pack_end(GTK_BOX(m_pButtonBox), GTK_WIDGET(m_pNext), false, false, 0); + + m_pCancel = GTK_BUTTON(gtk_button_new_with_mnemonic(MapToGtkAccelerator(GetStandardText(StandardButtonType::Cancel)).getStr())); + gtk_widget_set_can_default(GTK_WIDGET(m_pCancel), true); + gtk_box_pack_end(GTK_BOX(m_pButtonBox), GTK_WIDGET(m_pCancel), false, false, 0); + + m_pFinish = GTK_BUTTON(gtk_button_new_with_mnemonic(MapToGtkAccelerator(GetStandardText(StandardButtonType::Finish)).getStr())); + gtk_widget_set_can_default(GTK_WIDGET(m_pFinish), true); + gtk_buildable_set_name(GTK_BUILDABLE(m_pFinish), "finish"); + gtk_box_pack_end(GTK_BOX(m_pButtonBox), GTK_WIDGET(m_pFinish), false, false, 0); + + m_pHelp = GTK_BUTTON(gtk_button_new_with_mnemonic(MapToGtkAccelerator(GetStandardText(StandardButtonType::Help)).getStr())); + gtk_widget_set_can_default(GTK_WIDGET(m_pHelp), true); + g_signal_connect(m_pHelp, "clicked", G_CALLBACK(signalHelpClicked), this); + gtk_box_pack_end(GTK_BOX(m_pButtonBox), GTK_WIDGET(m_pHelp), false, false, 0); + + gtk_assistant_add_action_widget(pAssistant, GTK_WIDGET(m_pButtonBox)); + gtk_button_box_set_child_secondary(m_pButtonBox, GTK_WIDGET(m_pHelp), true); + gtk_widget_set_hexpand(GTK_WIDGET(m_pButtonBox), true); + + GtkWidget* pParent = gtk_widget_get_parent(GTK_WIDGET(m_pButtonBox)); + gtk_container_child_set(GTK_CONTAINER(pParent), GTK_WIDGET(m_pButtonBox), "expand", true, "fill", true, nullptr); + gtk_widget_set_halign(pParent, GTK_ALIGN_FILL); + + // Hide the built-in ones early so we get a nice optimal size for the width without + // including the unused contents + GList* pChildren = gtk_container_get_children(GTK_CONTAINER(pParent)); + for (GList* pChild = g_list_first(pChildren); pChild; pChild = g_list_next(pChild)) + { + GtkWidget* pWidget = static_cast<GtkWidget*>(pChild->data); + gtk_widget_hide(pWidget); + } + g_list_free(pChildren); + + gtk_widget_show_all(GTK_WIDGET(m_pButtonBox)); + + find_sidebar(GTK_WIDGET(m_pAssistant), &m_pSidebar); + + m_pSidebarEventBox = ::ensureEventWidget(m_pSidebar); + m_nButtonPressSignalId = m_pSidebarEventBox ? g_signal_connect(m_pSidebarEventBox, "button-press-event", G_CALLBACK(signalButton), this) : 0; + } + + virtual int get_current_page() const override + { + return gtk_assistant_get_current_page(m_pAssistant); + } + + virtual int get_n_pages() const override + { + return gtk_assistant_get_n_pages(m_pAssistant); + } + + virtual OString get_page_ident(int nPage) const override + { + const GtkWidget* pWidget = gtk_assistant_get_nth_page(m_pAssistant, nPage); + const gchar* pStr = gtk_buildable_get_name(GTK_BUILDABLE(pWidget)); + return OString(pStr, pStr ? strlen(pStr) : 0); + } + + virtual OString get_current_page_ident() const override + { + return get_page_ident(get_current_page()); + } + + virtual void set_current_page(int nPage) override + { + OString sDialogTitle(gtk_window_get_title(GTK_WINDOW(m_pAssistant))); + + gtk_assistant_set_current_page(m_pAssistant, nPage); + + // if the page doesn't have a title, then the dialog will now have no + // title, so restore the original title as a fallback + GtkWidget* pPage = gtk_assistant_get_nth_page(m_pAssistant, nPage); + if (!gtk_assistant_get_page_title(m_pAssistant, pPage)) + gtk_window_set_title(GTK_WINDOW(m_pAssistant), sDialogTitle.getStr()); + } + + virtual void set_current_page(const OString& rIdent) override + { + int nPage = find_page(rIdent); + if (nPage == -1) + return; + set_current_page(nPage); + } + + virtual void set_page_title(const OString& rIdent, const OUString& rTitle) override + { + int nIndex = find_page(rIdent); + if (nIndex == -1) + return; + GtkWidget* pPage = gtk_assistant_get_nth_page(m_pAssistant, nIndex); + gtk_assistant_set_page_title(m_pAssistant, pPage, + OUStringToOString(rTitle, RTL_TEXTENCODING_UTF8).getStr()); + gtk_container_forall(GTK_CONTAINER(m_pSidebar), wrap_sidebar_label, nullptr); + } + + virtual OUString get_page_title(const OString& rIdent) const override + { + int nIndex = find_page(rIdent); + if (nIndex == -1) + return OUString(); + GtkWidget* pPage = gtk_assistant_get_nth_page(m_pAssistant, nIndex); + const gchar* pStr = gtk_assistant_get_page_title(m_pAssistant, pPage); + return OUString(pStr, pStr ? strlen(pStr) : 0, RTL_TEXTENCODING_UTF8); + } + + virtual void set_page_sensitive(const OString& rIdent, bool bSensitive) override + { + m_aNotClickable[rIdent] = !bSensitive; + } + + virtual void set_page_index(const OString& rIdent, int nNewIndex) override + { + int nOldIndex = find_page(rIdent); + if (nOldIndex == -1) + return; + + if (nOldIndex == nNewIndex) + return; + + GtkWidget* pPage = gtk_assistant_get_nth_page(m_pAssistant, nOldIndex); + + g_object_ref(pPage); + OString sTitle(gtk_assistant_get_page_title(m_pAssistant, pPage)); + gtk_assistant_remove_page(m_pAssistant, nOldIndex); + gtk_assistant_insert_page(m_pAssistant, pPage, nNewIndex); + gtk_assistant_set_page_type(m_pAssistant, pPage, GTK_ASSISTANT_PAGE_CUSTOM); + gtk_assistant_set_page_title(m_pAssistant, pPage, sTitle.getStr()); + gtk_container_forall(GTK_CONTAINER(m_pSidebar), wrap_sidebar_label, nullptr); + g_object_unref(pPage); + } + + virtual weld::Container* append_page(const OString& rIdent) override + { + disable_notify_events(); + + GtkWidget *pChild = gtk_grid_new(); + gtk_buildable_set_name(GTK_BUILDABLE(pChild), rIdent.getStr()); + gtk_assistant_append_page(m_pAssistant, pChild); + gtk_assistant_set_page_type(m_pAssistant, pChild, GTK_ASSISTANT_PAGE_CUSTOM); + gtk_widget_show(pChild); + + enable_notify_events(); + + m_aPages.emplace_back(new GtkInstanceContainer(GTK_CONTAINER(pChild), m_pBuilder, false)); + + return m_aPages.back().get(); + } + + virtual void set_page_side_help_id(const OString& rHelpId) override + { + if (!m_pSidebar) + return; + ::set_help_id(m_pSidebar, rHelpId); + } + + virtual GtkButton* get_widget_for_response(int nGtkResponse) override + { + GtkButton* pButton = nullptr; + if (nGtkResponse == GTK_RESPONSE_YES) + pButton = m_pNext; + else if (nGtkResponse == GTK_RESPONSE_NO) + pButton = m_pBack; + else if (nGtkResponse == GTK_RESPONSE_OK) + pButton = m_pFinish; + else if (nGtkResponse == GTK_RESPONSE_CANCEL) + pButton = m_pCancel; + else if (nGtkResponse == GTK_RESPONSE_HELP) + pButton = m_pHelp; + return pButton; + } + + virtual ~GtkInstanceAssistant() override + { + if (m_nButtonPressSignalId) + g_signal_handler_disconnect(m_pSidebarEventBox, m_nButtonPressSignalId); + } +}; + +class GtkInstanceFrame : public GtkInstanceContainer, public virtual weld::Frame +{ +private: + GtkFrame* m_pFrame; +public: + GtkInstanceFrame(GtkFrame* pFrame, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : GtkInstanceContainer(GTK_CONTAINER(pFrame), pBuilder, bTakeOwnership) + , m_pFrame(pFrame) + { + } + + virtual void set_label(const OUString& rText) override + { + gtk_label_set_label(GTK_LABEL(gtk_frame_get_label_widget(m_pFrame)), rText.replaceFirst("~", "").toUtf8().getStr()); + } + + virtual OUString get_label() const override + { + const gchar* pStr = gtk_frame_get_label(m_pFrame); + return OUString(pStr, pStr ? strlen(pStr) : 0, RTL_TEXTENCODING_UTF8); + } + + virtual std::unique_ptr<weld::Label> weld_label_widget() const override; +}; + +class GtkInstancePaned : public GtkInstanceContainer, public virtual weld::Paned +{ +private: + GtkPaned* m_pPaned; +public: + GtkInstancePaned(GtkPaned* pPaned, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : GtkInstanceContainer(GTK_CONTAINER(pPaned), pBuilder, bTakeOwnership) + , m_pPaned(pPaned) + { + } + + virtual void set_position(int nPos) override + { + gtk_paned_set_position(m_pPaned, nPos); + } + + virtual int get_position() const override + { + return gtk_paned_get_position(m_pPaned); + } +}; + +} + +static GType crippled_viewport_get_type(); + +#define CRIPPLED_TYPE_VIEWPORT (crippled_viewport_get_type ()) +#define CRIPPLED_VIEWPORT(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), CRIPPLED_TYPE_VIEWPORT, CrippledViewport)) +#ifndef NDEBUG +# define CRIPPLED_IS_VIEWPORT(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), CRIPPLED_TYPE_VIEWPORT)) +#endif + +namespace { + +struct CrippledViewport +{ + GtkViewport viewport; + + GtkAdjustment *hadjustment; + GtkAdjustment *vadjustment; +}; + +} + +enum +{ + PROP_0, + PROP_HADJUSTMENT, + PROP_VADJUSTMENT, + PROP_HSCROLL_POLICY, + PROP_VSCROLL_POLICY, + PROP_SHADOW_TYPE +}; + +static void viewport_set_adjustment(CrippledViewport *viewport, + GtkOrientation orientation, + GtkAdjustment *adjustment) +{ + if (!adjustment) + adjustment = gtk_adjustment_new(0.0, 0.0, 0.0, 0.0, 0.0, 0.0); + + if (orientation == GTK_ORIENTATION_HORIZONTAL) + { + if (viewport->hadjustment) + g_object_unref(viewport->hadjustment); + viewport->hadjustment = adjustment; + } + else + { + if (viewport->vadjustment) + g_object_unref(viewport->vadjustment); + viewport->vadjustment = adjustment; + } + + g_object_ref_sink(adjustment); +} + +static void +crippled_viewport_set_property(GObject* object, + guint prop_id, + const GValue* value, + GParamSpec* /*pspec*/) +{ + CrippledViewport *viewport = CRIPPLED_VIEWPORT(object); + + switch (prop_id) + { + case PROP_HADJUSTMENT: + viewport_set_adjustment(viewport, GTK_ORIENTATION_HORIZONTAL, GTK_ADJUSTMENT(g_value_get_object(value))); + break; + case PROP_VADJUSTMENT: + viewport_set_adjustment(viewport, GTK_ORIENTATION_VERTICAL, GTK_ADJUSTMENT(g_value_get_object(value))); + break; + case PROP_HSCROLL_POLICY: + case PROP_VSCROLL_POLICY: + break; + default: + SAL_WARN( "vcl.gtk", "unknown property\n"); + break; + } +} + +static void +crippled_viewport_get_property(GObject* object, + guint prop_id, + GValue* value, + GParamSpec* /*pspec*/) +{ + CrippledViewport *viewport = CRIPPLED_VIEWPORT(object); + + switch (prop_id) + { + case PROP_HADJUSTMENT: + g_value_set_object(value, viewport->hadjustment); + break; + case PROP_VADJUSTMENT: + g_value_set_object(value, viewport->vadjustment); + break; + case PROP_HSCROLL_POLICY: + g_value_set_enum(value, GTK_SCROLL_MINIMUM); + break; + case PROP_VSCROLL_POLICY: + g_value_set_enum(value, GTK_SCROLL_MINIMUM); + break; + default: + SAL_WARN( "vcl.gtk", "unknown property\n"); + break; + } +} + +static void crippled_viewport_class_init(GtkViewportClass *klass) +{ + GObjectClass* o_class = G_OBJECT_CLASS(klass); + + /* GObject signals */ + o_class->set_property = crippled_viewport_set_property; + o_class->get_property = crippled_viewport_get_property; + + /* Properties */ + g_object_class_override_property(o_class, PROP_HADJUSTMENT, "hadjustment"); + g_object_class_override_property(o_class, PROP_VADJUSTMENT, "vadjustment"); + g_object_class_override_property(o_class, PROP_HSCROLL_POLICY, "hscroll-policy"); + g_object_class_override_property(o_class, PROP_VSCROLL_POLICY, "vscroll-policy"); +} + +GType crippled_viewport_get_type() +{ + static GType type = 0; + + if (!type) + { + static const GTypeInfo tinfo = + { + sizeof (GtkViewportClass), + nullptr, /* base init */ + nullptr, /* base finalize */ + reinterpret_cast<GClassInitFunc>(crippled_viewport_class_init), /* class init */ + nullptr, /* class finalize */ + nullptr, /* class data */ + sizeof (CrippledViewport), /* instance size */ + 0, /* nb preallocs */ + nullptr, /* instance init */ + nullptr /* value table */ + }; + + type = g_type_register_static( GTK_TYPE_VIEWPORT, "CrippledViewport", + &tinfo, GTypeFlags(0)); + } + + return type; +} + +#define CUSTOM_TYPE_CELL_RENDERER_SURFACE (custom_cell_renderer_surface_get_type()) +#define CUSTOM_CELL_RENDERER_SURFACE(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), CUSTOM_TYPE_CELL_RENDERER_SURFACE, CustomCellRendererSurface)) +#define CUSTOM_IS_CELL_RENDERER_SURFACE(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), CUSTOM_TYPE_CELL_RENDERER_SURFACE)) + +namespace { + + struct CustomCellRendererSurface + { + GtkCellRendererText parent; + VclPtr<VirtualDevice> device; + gchar *id; + gpointer instance; + }; + + struct CustomCellRendererSurfaceClass + { + GtkCellRendererTextClass parent_class; + }; + + enum + { + PROP_ID = 10000, + PROP_INSTANCE_TREE_VIEW = 10001 + }; +} + +static gpointer custom_cell_renderer_surface_parent_class; + +static GType custom_cell_renderer_surface_get_type(); +static void custom_cell_renderer_surface_class_init(CustomCellRendererSurfaceClass *klass); + +GType custom_cell_renderer_surface_get_type() +{ + static GType type = 0; + + if (!type) + { + static const GTypeInfo tinfo = + { + sizeof (CustomCellRendererSurfaceClass), + nullptr, /* base init */ + nullptr, /* base finalize */ + reinterpret_cast<GClassInitFunc>(custom_cell_renderer_surface_class_init), /* class init */ + nullptr, /* class finalize */ + nullptr, /* class data */ + sizeof (CustomCellRendererSurface), /* instance size */ + 0, /* nb preallocs */ + nullptr, /* instance init */ + nullptr /* value table */ + }; + + // inherit from GtkCellRendererText so we can set the "text" property and get a11y support for that + type = g_type_register_static(GTK_TYPE_CELL_RENDERER_TEXT, "CustomCellRendererSurface", + &tinfo, GTypeFlags(0)); + } + + return type; +} + +static void custom_cell_renderer_surface_get_property(GObject *object, + guint param_id, + GValue *value, + GParamSpec *pspec) +{ + CustomCellRendererSurface *cellsurface = CUSTOM_CELL_RENDERER_SURFACE(object); + + switch (param_id) + { + case PROP_ID: + g_value_set_string(value, cellsurface->id); + break; + case PROP_INSTANCE_TREE_VIEW: + g_value_set_pointer(value, cellsurface->instance); + break; + default: + G_OBJECT_CLASS(custom_cell_renderer_surface_parent_class)->get_property(object, param_id, value, pspec); + break; + } +} + +static void custom_cell_renderer_surface_set_property(GObject *object, + guint param_id, + const GValue *value, + GParamSpec *pspec) +{ + CustomCellRendererSurface *cellsurface = CUSTOM_CELL_RENDERER_SURFACE(object); + + switch (param_id) + { + case PROP_ID: + g_free(cellsurface->id); + cellsurface->id = g_value_dup_string(value); + break; + case PROP_INSTANCE_TREE_VIEW: + cellsurface->instance = g_value_get_pointer(value); + break; + default: + G_OBJECT_CLASS(custom_cell_renderer_surface_parent_class)->set_property(object, param_id, value, pspec); + break; + } +} + +static bool custom_cell_renderer_surface_get_preferred_size(GtkCellRenderer *cell, + GtkOrientation orientation, + gint *minimum_size, + gint *natural_size); + +static void custom_cell_renderer_surface_render(GtkCellRenderer* cell, + cairo_t* cr, + GtkWidget* widget, + const GdkRectangle* background_area, + const GdkRectangle* cell_area, + GtkCellRendererState flags); + +static void custom_cell_renderer_surface_finalize(GObject *object) +{ + CustomCellRendererSurface *cellsurface = CUSTOM_CELL_RENDERER_SURFACE(object); + + g_free(cellsurface->id); + cellsurface->device.disposeAndClear(); + + G_OBJECT_CLASS(custom_cell_renderer_surface_parent_class)->finalize(object); +} + +static void custom_cell_renderer_surface_get_preferred_width(GtkCellRenderer *cell, + GtkWidget *widget, + gint *minimum_size, + gint *natural_size) +{ + if (!custom_cell_renderer_surface_get_preferred_size(cell, GTK_ORIENTATION_HORIZONTAL, + minimum_size, natural_size)) + { + // fallback to parent if we're empty + GTK_CELL_RENDERER_CLASS(custom_cell_renderer_surface_parent_class)->get_preferred_width(cell, + widget, minimum_size, natural_size); + } +} + +static void custom_cell_renderer_surface_get_preferred_height(GtkCellRenderer *cell, + GtkWidget *widget, + gint *minimum_size, + gint *natural_size) +{ + if (!custom_cell_renderer_surface_get_preferred_size(cell, GTK_ORIENTATION_VERTICAL, + minimum_size, natural_size)) + { + // fallback to parent if we're empty + GTK_CELL_RENDERER_CLASS(custom_cell_renderer_surface_parent_class)->get_preferred_height(cell, + widget, minimum_size, natural_size); + } + +} + +static void custom_cell_renderer_surface_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 custom_cell_renderer_surface_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 custom_cell_renderer_surface_class_init(CustomCellRendererSurfaceClass *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 = custom_cell_renderer_surface_get_property; + object_class->set_property = custom_cell_renderer_surface_set_property; + + custom_cell_renderer_surface_parent_class = g_type_class_peek_parent(klass); + object_class->finalize = custom_cell_renderer_surface_finalize; + + cell_class->get_preferred_width = custom_cell_renderer_surface_get_preferred_width; + cell_class->get_preferred_height = custom_cell_renderer_surface_get_preferred_height; + cell_class->get_preferred_width_for_height = custom_cell_renderer_surface_get_preferred_width_for_height; + cell_class->get_preferred_height_for_width = custom_cell_renderer_surface_get_preferred_height_for_width; + + cell_class->render = custom_cell_renderer_surface_render; + + g_object_class_install_property(object_class, + PROP_ID, + g_param_spec_string("id", + "ID", + "The ID of the custom data", + nullptr, + G_PARAM_READWRITE)); + + g_object_class_install_property(object_class, + PROP_INSTANCE_TREE_VIEW, + g_param_spec_pointer("instance", + "Instance", + "The GtkInstanceTreeView", + G_PARAM_READWRITE)); + + gtk_cell_renderer_class_set_accessible_type(cell_class, GTK_TYPE_TEXT_CELL_ACCESSIBLE); +} + +static GtkCellRenderer* custom_cell_renderer_surface_new() +{ + return GTK_CELL_RENDERER(g_object_new(CUSTOM_TYPE_CELL_RENDERER_SURFACE, nullptr)); +} + +static VclPolicyType GtkToVcl(GtkPolicyType eType) +{ + VclPolicyType eRet(VclPolicyType::NEVER); + switch (eType) + { + case GTK_POLICY_ALWAYS: + eRet = VclPolicyType::ALWAYS; + break; + case GTK_POLICY_AUTOMATIC: + eRet = VclPolicyType::AUTOMATIC; + break; + case GTK_POLICY_EXTERNAL: + case GTK_POLICY_NEVER: + eRet = VclPolicyType::NEVER; + break; + } + return eRet; +} + +static GtkPolicyType VclToGtk(VclPolicyType eType) +{ + GtkPolicyType eRet(GTK_POLICY_ALWAYS); + switch (eType) + { + case VclPolicyType::ALWAYS: + eRet = GTK_POLICY_ALWAYS; + break; + case VclPolicyType::AUTOMATIC: + eRet = GTK_POLICY_AUTOMATIC; + break; + case VclPolicyType::NEVER: + eRet = GTK_POLICY_NEVER; + break; + } + return eRet; +} + +static GtkMessageType VclToGtk(VclMessageType eType) +{ + GtkMessageType eRet(GTK_MESSAGE_INFO); + switch (eType) + { + case VclMessageType::Info: + eRet = GTK_MESSAGE_INFO; + break; + case VclMessageType::Warning: + eRet = GTK_MESSAGE_WARNING; + break; + case VclMessageType::Question: + eRet = GTK_MESSAGE_QUESTION; + break; + case VclMessageType::Error: + eRet = GTK_MESSAGE_ERROR; + break; + case VclMessageType::Other: + eRet = GTK_MESSAGE_OTHER; + break; + } + return eRet; +} + +static GtkButtonsType VclToGtk(VclButtonsType eType) +{ + GtkButtonsType eRet(GTK_BUTTONS_NONE); + switch (eType) + { + case VclButtonsType::NONE: + eRet = GTK_BUTTONS_NONE; + break; + case VclButtonsType::Ok: + eRet = GTK_BUTTONS_OK; + break; + case VclButtonsType::Close: + eRet = GTK_BUTTONS_CLOSE; + break; + case VclButtonsType::Cancel: + eRet = GTK_BUTTONS_CANCEL; + break; + case VclButtonsType::YesNo: + eRet = GTK_BUTTONS_YES_NO; + break; + case VclButtonsType::OkCancel: + eRet = GTK_BUTTONS_OK_CANCEL; + break; + } + return eRet; +} + +static GtkSelectionMode VclToGtk(SelectionMode eType) +{ + GtkSelectionMode eRet(GTK_SELECTION_NONE); + switch (eType) + { + case SelectionMode::NONE: + eRet = GTK_SELECTION_NONE; + break; + case SelectionMode::Single: + eRet = GTK_SELECTION_SINGLE; + break; + case SelectionMode::Range: + eRet = GTK_SELECTION_BROWSE; + break; + case SelectionMode::Multiple: + eRet = GTK_SELECTION_MULTIPLE; + break; + } + return eRet; +} + +namespace { + +class GtkInstanceScrolledWindow final : public GtkInstanceContainer, public virtual weld::ScrolledWindow +{ +private: + GtkScrolledWindow* m_pScrolledWindow; + GtkWidget *m_pOrigViewport; + GtkAdjustment* m_pVAdjustment; + GtkAdjustment* m_pHAdjustment; + gulong m_nVAdjustChangedSignalId; + gulong m_nHAdjustChangedSignalId; + + static void signalVAdjustValueChanged(GtkAdjustment*, gpointer widget) + { + GtkInstanceScrolledWindow* pThis = static_cast<GtkInstanceScrolledWindow*>(widget); + SolarMutexGuard aGuard; + pThis->signal_vadjustment_changed(); + } + + static void signalHAdjustValueChanged(GtkAdjustment*, gpointer widget) + { + GtkInstanceScrolledWindow* pThis = static_cast<GtkInstanceScrolledWindow*>(widget); + SolarMutexGuard aGuard; + pThis->signal_hadjustment_changed(); + } + +public: + GtkInstanceScrolledWindow(GtkScrolledWindow* pScrolledWindow, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : GtkInstanceContainer(GTK_CONTAINER(pScrolledWindow), pBuilder, bTakeOwnership) + , m_pScrolledWindow(pScrolledWindow) + , m_pOrigViewport(nullptr) + , m_pVAdjustment(gtk_scrolled_window_get_vadjustment(m_pScrolledWindow)) + , m_pHAdjustment(gtk_scrolled_window_get_hadjustment(m_pScrolledWindow)) + , m_nVAdjustChangedSignalId(g_signal_connect(m_pVAdjustment, "value-changed", G_CALLBACK(signalVAdjustValueChanged), this)) + , m_nHAdjustChangedSignalId(g_signal_connect(m_pHAdjustment, "value-changed", G_CALLBACK(signalHAdjustValueChanged), this)) + { + } + + virtual void set_user_managed_scrolling() override + { + disable_notify_events(); + //remove the original viewport and replace it with our bodged one which + //doesn't do any scrolling and expects its child to figure it out somehow + assert(!m_pOrigViewport); + GtkWidget *pViewport = gtk_bin_get_child(GTK_BIN(m_pScrolledWindow)); + assert(GTK_IS_VIEWPORT(pViewport)); + GtkWidget *pChild = gtk_bin_get_child(GTK_BIN(pViewport)); + g_object_ref(pChild); + gtk_container_remove(GTK_CONTAINER(pViewport), pChild); + g_object_ref(pViewport); + gtk_container_remove(GTK_CONTAINER(m_pScrolledWindow), pViewport); + GtkWidget* pNewViewport = GTK_WIDGET(g_object_new(crippled_viewport_get_type(), nullptr)); + gtk_widget_show(pNewViewport); + gtk_container_add(GTK_CONTAINER(m_pScrolledWindow), pNewViewport); + gtk_container_add(GTK_CONTAINER(pNewViewport), pChild); + g_object_unref(pChild); + m_pOrigViewport = pViewport; + enable_notify_events(); + } + + virtual void hadjustment_configure(int value, int lower, int upper, + int step_increment, int page_increment, + int page_size) override + { + disable_notify_events(); + if (SwapForRTL()) + value = upper - (value - lower + page_size); + gtk_adjustment_configure(m_pHAdjustment, value, lower, upper, step_increment, page_increment, page_size); + enable_notify_events(); + } + + virtual int hadjustment_get_value() const override + { + int value = gtk_adjustment_get_value(m_pHAdjustment); + + if (SwapForRTL()) + { + int upper = gtk_adjustment_get_upper(m_pHAdjustment); + int lower = gtk_adjustment_get_lower(m_pHAdjustment); + int page_size = gtk_adjustment_get_page_size(m_pHAdjustment); + value = lower + (upper - value - page_size); + } + + return value; + } + + virtual void hadjustment_set_value(int value) override + { + disable_notify_events(); + + if (SwapForRTL()) + { + int upper = gtk_adjustment_get_upper(m_pHAdjustment); + int lower = gtk_adjustment_get_lower(m_pHAdjustment); + int page_size = gtk_adjustment_get_page_size(m_pHAdjustment); + value = upper - (value - lower + page_size); + } + + gtk_adjustment_set_value(m_pHAdjustment, value); + enable_notify_events(); + } + + virtual int hadjustment_get_upper() const override + { + return gtk_adjustment_get_upper(m_pHAdjustment); + } + + virtual void hadjustment_set_upper(int upper) override + { + disable_notify_events(); + gtk_adjustment_set_upper(m_pHAdjustment, upper); + enable_notify_events(); + } + + virtual int hadjustment_get_page_size() const override + { + return gtk_adjustment_get_page_size(m_pHAdjustment); + } + + virtual void hadjustment_set_page_size(int size) override + { + gtk_adjustment_set_page_size(m_pHAdjustment, size); + } + + virtual void hadjustment_set_page_increment(int size) override + { + gtk_adjustment_set_page_increment(m_pHAdjustment, size); + } + + virtual void hadjustment_set_step_increment(int size) override + { + gtk_adjustment_set_step_increment(m_pHAdjustment, size); + } + + virtual void set_hpolicy(VclPolicyType eHPolicy) override + { + GtkPolicyType eGtkVPolicy; + gtk_scrolled_window_get_policy(m_pScrolledWindow, nullptr, &eGtkVPolicy); + gtk_scrolled_window_set_policy(m_pScrolledWindow, eGtkVPolicy, VclToGtk(eHPolicy)); + } + + virtual VclPolicyType get_hpolicy() const override + { + GtkPolicyType eGtkHPolicy; + gtk_scrolled_window_get_policy(m_pScrolledWindow, &eGtkHPolicy, nullptr); + return GtkToVcl(eGtkHPolicy); + } + + virtual int get_hscroll_height() const override + { + if (gtk_scrolled_window_get_overlay_scrolling(m_pScrolledWindow)) + return 0; + return gtk_widget_get_allocated_height(gtk_scrolled_window_get_hscrollbar(m_pScrolledWindow)); + } + + virtual void vadjustment_configure(int value, int lower, int upper, + int step_increment, int page_increment, + int page_size) override + { + disable_notify_events(); + gtk_adjustment_configure(m_pVAdjustment, value, lower, upper, step_increment, page_increment, page_size); + enable_notify_events(); + } + + virtual int vadjustment_get_value() const override + { + return gtk_adjustment_get_value(m_pVAdjustment); + } + + virtual void vadjustment_set_value(int value) override + { + disable_notify_events(); + gtk_adjustment_set_value(m_pVAdjustment, value); + enable_notify_events(); + } + + virtual int vadjustment_get_upper() const override + { + return gtk_adjustment_get_upper(m_pVAdjustment); + } + + virtual void vadjustment_set_upper(int upper) override + { + disable_notify_events(); + gtk_adjustment_set_upper(m_pVAdjustment, upper); + enable_notify_events(); + } + + virtual int vadjustment_get_lower() const override + { + return gtk_adjustment_get_lower(m_pVAdjustment); + } + + virtual void vadjustment_set_lower(int lower) override + { + disable_notify_events(); + gtk_adjustment_set_lower(m_pVAdjustment, lower); + enable_notify_events(); + } + + virtual int vadjustment_get_page_size() const override + { + return gtk_adjustment_get_page_size(m_pVAdjustment); + } + + virtual void vadjustment_set_page_size(int size) override + { + gtk_adjustment_set_page_size(m_pVAdjustment, size); + } + + virtual void vadjustment_set_page_increment(int size) override + { + gtk_adjustment_set_page_increment(m_pVAdjustment, size); + } + + virtual void vadjustment_set_step_increment(int size) override + { + gtk_adjustment_set_step_increment(m_pVAdjustment, size); + } + + virtual void set_vpolicy(VclPolicyType eVPolicy) override + { + GtkPolicyType eGtkHPolicy; + gtk_scrolled_window_get_policy(m_pScrolledWindow, &eGtkHPolicy, nullptr); + gtk_scrolled_window_set_policy(m_pScrolledWindow, eGtkHPolicy, VclToGtk(eVPolicy)); + } + + virtual VclPolicyType get_vpolicy() const override + { + GtkPolicyType eGtkVPolicy; + gtk_scrolled_window_get_policy(m_pScrolledWindow, nullptr, &eGtkVPolicy); + return GtkToVcl(eGtkVPolicy); + } + + virtual int get_vscroll_width() const override + { + if (gtk_scrolled_window_get_overlay_scrolling(m_pScrolledWindow)) + return 0; + return gtk_widget_get_allocated_width(gtk_scrolled_window_get_vscrollbar(m_pScrolledWindow)); + } + + virtual void disable_notify_events() override + { + g_signal_handler_block(m_pVAdjustment, m_nVAdjustChangedSignalId); + g_signal_handler_block(m_pHAdjustment, m_nHAdjustChangedSignalId); + GtkInstanceContainer::disable_notify_events(); + } + + virtual void enable_notify_events() override + { + GtkInstanceContainer::enable_notify_events(); + g_signal_handler_unblock(m_pVAdjustment, m_nVAdjustChangedSignalId); + g_signal_handler_unblock(m_pHAdjustment, m_nHAdjustChangedSignalId); + } + + virtual ~GtkInstanceScrolledWindow() override + { + // we use GtkInstanceContainer::[disable|enable]_notify_events later on + // to avoid touching these removed handlers + g_signal_handler_disconnect(m_pVAdjustment, m_nVAdjustChangedSignalId); + g_signal_handler_disconnect(m_pHAdjustment, m_nHAdjustChangedSignalId); + + //put it back the way it was + if (m_pOrigViewport) + { + GtkInstanceContainer::disable_notify_events(); + + // force in new adjustment to drop the built-in handlers on value-changed + // which are getting called eventually by the gtk_container_add call + // and which access the scrolled window indicators which, in the case + // of user-managed scrolling windows in toolbar popups during popdown + // are nullptr causing crashes when the scrolling windows is not at its + // initial 0,0 position + GtkAdjustment *pVAdjustment = gtk_adjustment_new(0.0, 0.0, 0.0, 0.0, 0.0, 0.0); + gtk_scrolled_window_set_vadjustment(m_pScrolledWindow, pVAdjustment); + GtkAdjustment *pHAdjustment = gtk_adjustment_new(0.0, 0.0, 0.0, 0.0, 0.0, 0.0); + gtk_scrolled_window_set_hadjustment(m_pScrolledWindow, pHAdjustment); + + GtkWidget *pViewport = gtk_bin_get_child(GTK_BIN(m_pScrolledWindow)); + assert(CRIPPLED_IS_VIEWPORT(pViewport)); + GtkWidget *pChild = gtk_bin_get_child(GTK_BIN(pViewport)); + g_object_ref(pChild); + gtk_container_remove(GTK_CONTAINER(pViewport), pChild); + g_object_ref(pViewport); + gtk_container_remove(GTK_CONTAINER(m_pScrolledWindow), pViewport); + + gtk_container_add(GTK_CONTAINER(m_pScrolledWindow), m_pOrigViewport); + g_object_unref(m_pOrigViewport); + gtk_container_add(GTK_CONTAINER(m_pOrigViewport), pChild); + g_object_unref(pChild); + gtk_widget_destroy(pViewport); + g_object_unref(pViewport); + m_pOrigViewport = nullptr; + GtkInstanceContainer::enable_notify_events(); + } + } +}; + +class GtkInstanceNotebook : public GtkInstanceContainer, public virtual weld::Notebook +{ +private: + GtkNotebook* m_pNotebook; + GtkBox* m_pOverFlowBox; + GtkNotebook* m_pOverFlowNotebook; + gulong m_nSwitchPageSignalId; + gulong m_nOverFlowSwitchPageSignalId; + gulong m_nSizeAllocateSignalId; + gulong m_nFocusSignalId; + gulong m_nChangeCurrentPageId; + guint m_nLaunchSplitTimeoutId; + bool m_bOverFlowBoxActive; + bool m_bOverFlowBoxIsStart; + bool m_bInternalPageChange; + int m_nStartTabCount; + int m_nEndTabCount; + mutable std::vector<std::unique_ptr<GtkInstanceContainer>> m_aPages; + + static void signalSwitchPage(GtkNotebook*, GtkWidget*, guint nNewPage, gpointer widget) + { + GtkInstanceNotebook* pThis = static_cast<GtkInstanceNotebook*>(widget); + SolarMutexGuard aGuard; + pThis->signal_switch_page(nNewPage); + } + + static gboolean launch_overflow_switch_page(GtkInstanceNotebook* pThis) + { + SolarMutexGuard aGuard; + pThis->signal_overflow_switch_page(); + return false; + } + + static void signalOverFlowSwitchPage(GtkNotebook*, GtkWidget*, guint, gpointer widget) + { + g_timeout_add_full(G_PRIORITY_HIGH_IDLE, 0, reinterpret_cast<GSourceFunc>(launch_overflow_switch_page), widget, nullptr); + } + + void signal_switch_page(int nNewPage) + { + if (m_bOverFlowBoxIsStart) + { + auto nOverFlowLen = m_bOverFlowBoxActive ? gtk_notebook_get_n_pages(m_pOverFlowNotebook) - 1 : 0; + // add count of overflow pages, minus the extra tab + nNewPage += nOverFlowLen; + } + + bool bAllow = m_bInternalPageChange || !m_aLeavePageHdl.IsSet() || m_aLeavePageHdl.Call(get_current_page_ident()); + if (!bAllow) + { + g_signal_stop_emission_by_name(m_pNotebook, "switch-page"); + return; + } + if (m_bOverFlowBoxActive) + gtk_notebook_set_current_page(m_pOverFlowNotebook, gtk_notebook_get_n_pages(m_pOverFlowNotebook) - 1); + OString sNewIdent(get_page_ident(nNewPage)); + if (!m_bInternalPageChange) + m_aEnterPageHdl.Call(sNewIdent); + } + + void unsplit_notebooks() + { + int nOverFlowPages = gtk_notebook_get_n_pages(m_pOverFlowNotebook) - 1; + int nMainPages = gtk_notebook_get_n_pages(m_pNotebook); + int nPageIndex = 0; + if (!m_bOverFlowBoxIsStart) + nPageIndex += nMainPages; + + // take the overflow pages, and put them back at the end of the normal one + int i = nMainPages; + while (nOverFlowPages) + { + OString sIdent(get_page_ident(m_pOverFlowNotebook, 0)); + OUString sLabel(get_tab_label_text(m_pOverFlowNotebook, 0)); + remove_page(m_pOverFlowNotebook, sIdent); + + GtkWidget* pPage = m_aPages[nPageIndex]->getWidget(); + insert_page(m_pNotebook, sIdent, sLabel, pPage, -1); + + GtkWidget* pTabWidget = gtk_notebook_get_tab_label(m_pNotebook, + gtk_notebook_get_nth_page(m_pNotebook, i)); + gtk_widget_set_hexpand(pTabWidget, true); + --nOverFlowPages; + ++i; + ++nPageIndex; + } + + // remove the dangling placeholder tab page + remove_page(m_pOverFlowNotebook, "useless"); + } + + // a tab has been selected on the overflow notebook + void signal_overflow_switch_page() + { + int nNewPage = gtk_notebook_get_current_page(m_pOverFlowNotebook); + int nOverFlowPages = gtk_notebook_get_n_pages(m_pOverFlowNotebook) - 1; + if (nNewPage == nOverFlowPages) + { + // the useless tab which is there because there has to be an active tab + return; + } + + // check if we are allowed leave before attempting to resplit the notebooks + bool bAllow = !m_aLeavePageHdl.IsSet() || m_aLeavePageHdl.Call(get_current_page_ident()); + if (!bAllow) + return; + + disable_notify_events(); + + // take the overflow pages, and put them back at the end of the normal one + unsplit_notebooks(); + + // now redo the split, the pages will be split the other way around this time + std::swap(m_nStartTabCount, m_nEndTabCount); + split_notebooks(); + + gtk_notebook_set_current_page(m_pNotebook, nNewPage); + + enable_notify_events(); + + // trigger main notebook switch-page callback + OString sNewIdent(get_page_ident(m_pNotebook, nNewPage)); + m_aEnterPageHdl.Call(sNewIdent); + } + + static OString get_page_ident(GtkNotebook *pNotebook, guint nPage) + { + const GtkWidget* pTabWidget = gtk_notebook_get_tab_label(pNotebook, gtk_notebook_get_nth_page(pNotebook, nPage)); + const gchar* pStr = gtk_buildable_get_name(GTK_BUILDABLE(pTabWidget)); + return OString(pStr, pStr ? strlen(pStr) : 0); + } + + static gint get_page_number(GtkNotebook *pNotebook, const OString& rIdent) + { + gint nPages = gtk_notebook_get_n_pages(pNotebook); + for (gint i = 0; i < nPages; ++i) + { + const GtkWidget* pTabWidget = gtk_notebook_get_tab_label(pNotebook, gtk_notebook_get_nth_page(pNotebook, i)); + const gchar* pStr = gtk_buildable_get_name(GTK_BUILDABLE(pTabWidget)); + if (pStr && strcmp(pStr, rIdent.getStr()) == 0) + return i; + } + return -1; + } + + int remove_page(GtkNotebook *pNotebook, const OString& rIdent) + { + disable_notify_events(); + int nPageNumber = get_page_number(pNotebook, rIdent); + gtk_notebook_remove_page(pNotebook, nPageNumber); + enable_notify_events(); + return nPageNumber; + } + + static OUString get_tab_label_text(GtkNotebook *pNotebook, guint nPage) + { + const gchar* pStr = gtk_notebook_get_tab_label_text(pNotebook, gtk_notebook_get_nth_page(pNotebook, nPage)); + return OUString(pStr, pStr ? strlen(pStr) : 0, RTL_TEXTENCODING_UTF8); + } + + static void set_tab_label_text(GtkNotebook *pNotebook, guint nPage, const OUString& rText) + { + OString sUtf8(rText.toUtf8()); + + GtkWidget* pPage = gtk_notebook_get_nth_page(pNotebook, nPage); + + // tdf#128241 if there's already a label here, reuse it so the buildable + // name remains the same, gtk_notebook_set_tab_label_text will replace + // the label widget with a new one + GtkWidget* pTabWidget = gtk_notebook_get_tab_label(pNotebook, pPage); + if (pTabWidget && GTK_IS_LABEL(pTabWidget)) + { + gtk_label_set_label(GTK_LABEL(pTabWidget), sUtf8.getStr()); + return; + } + + gtk_notebook_set_tab_label_text(pNotebook, pPage, sUtf8.getStr()); + } + + void append_useless_page(GtkNotebook *pNotebook) + { + disable_notify_events(); + + GtkWidget *pTabWidget = gtk_fixed_new(); + gtk_buildable_set_name(GTK_BUILDABLE(pTabWidget), "useless"); + + GtkWidget *pChild = gtk_grid_new(); + gtk_notebook_append_page(pNotebook, pChild, pTabWidget); + gtk_widget_show(pChild); + gtk_widget_show(pTabWidget); + + enable_notify_events(); + } + + void insert_page(GtkNotebook *pNotebook, const OString& rIdent, const OUString& rLabel, GtkWidget *pChild, int nPos) + { + disable_notify_events(); + + GtkWidget *pTabWidget = gtk_label_new(MapToGtkAccelerator(rLabel).getStr()); + gtk_buildable_set_name(GTK_BUILDABLE(pTabWidget), rIdent.getStr()); + + gtk_notebook_insert_page(pNotebook, pChild, pTabWidget, nPos); + gtk_widget_show(pChild); + gtk_widget_show(pTabWidget); + + enable_notify_events(); + } + + gint get_page_number(const OString& rIdent) const + { + auto nMainIndex = get_page_number(m_pNotebook, rIdent); + auto nOverFlowIndex = get_page_number(m_pOverFlowNotebook, rIdent); + + if (nMainIndex == -1 && nOverFlowIndex == -1) + return -1; + + if (m_bOverFlowBoxIsStart) + { + if (nOverFlowIndex != -1) + return nOverFlowIndex; + else + { + auto nOverFlowLen = m_bOverFlowBoxActive ? gtk_notebook_get_n_pages(m_pOverFlowNotebook) - 1 : 0; + return nMainIndex + nOverFlowLen; + } + } + else + { + if (nMainIndex != -1) + return nMainIndex; + else + { + auto nMainLen = gtk_notebook_get_n_pages(m_pNotebook); + return nOverFlowIndex + nMainLen; + } + } + } + + void make_overflow_boxes() + { + m_pOverFlowBox = GTK_BOX(gtk_box_new(GTK_ORIENTATION_VERTICAL, 0)); + GtkWidget* pParent = gtk_widget_get_parent(GTK_WIDGET(m_pNotebook)); + gtk_container_add(GTK_CONTAINER(pParent), GTK_WIDGET(m_pOverFlowBox)); + gtk_box_pack_start(m_pOverFlowBox, GTK_WIDGET(m_pOverFlowNotebook), false, false, 0); + g_object_ref(m_pNotebook); + gtk_container_remove(GTK_CONTAINER(pParent), GTK_WIDGET(m_pNotebook)); + gtk_box_pack_start(m_pOverFlowBox, GTK_WIDGET(m_pNotebook), true, true, 0); + g_object_unref(m_pNotebook); + gtk_widget_show(GTK_WIDGET(m_pOverFlowBox)); + } + + void split_notebooks() + { + // get the original preferred size for the notebook, the sane width + // expected here depends on the notebooks all initially having + // scrollable tabs enabled + GtkAllocation alloc; + gtk_widget_get_allocation(GTK_WIDGET(m_pNotebook), &alloc); + + // toggle the direction of the split since the last time + m_bOverFlowBoxIsStart = !m_bOverFlowBoxIsStart; + if (!m_pOverFlowBox) + make_overflow_boxes(); + + // don't scroll the tabs anymore + gtk_notebook_set_scrollable(m_pNotebook, false); + + gtk_widget_freeze_child_notify(GTK_WIDGET(m_pNotebook)); + gtk_widget_freeze_child_notify(GTK_WIDGET(m_pOverFlowNotebook)); + + gtk_widget_show(GTK_WIDGET(m_pOverFlowNotebook)); + + gint nPages; + + GtkRequisition size1, size2; + + if (!m_nStartTabCount && !m_nEndTabCount) + { + nPages = gtk_notebook_get_n_pages(m_pNotebook); + + std::vector<int> aLabelWidths; + //move tabs to the overflow notebook + for (int i = 0; i < nPages; ++i) + { + OUString sLabel(get_tab_label_text(m_pNotebook, i)); + aLabelWidths.push_back(get_pixel_size(sLabel).Width()); + } + int row_width = std::accumulate(aLabelWidths.begin(), aLabelWidths.end(), 0) / 2; + int count = 0; + for (int i = 0; i < nPages; ++i) + { + count += aLabelWidths[i]; + if (count >= row_width) + { + m_nStartTabCount = i; + break; + } + } + + m_nEndTabCount = nPages - m_nStartTabCount; + } + + //move the tabs to the overflow notebook + int i = 0; + int nOverFlowPages = m_nStartTabCount; + while (nOverFlowPages) + { + OString sIdent(get_page_ident(m_pNotebook, 0)); + OUString sLabel(get_tab_label_text(m_pNotebook, 0)); + remove_page(m_pNotebook, sIdent); + insert_page(m_pOverFlowNotebook, sIdent, sLabel, gtk_grid_new(), -1); + GtkWidget* pTabWidget = gtk_notebook_get_tab_label(m_pOverFlowNotebook, + gtk_notebook_get_nth_page(m_pOverFlowNotebook, i)); + gtk_widget_set_hexpand(pTabWidget, true); + + --nOverFlowPages; + ++i; + } + + for (i = 0; i < m_nEndTabCount; ++i) + { + GtkWidget* pTabWidget = gtk_notebook_get_tab_label(m_pNotebook, + gtk_notebook_get_nth_page(m_pNotebook, i)); + gtk_widget_set_hexpand(pTabWidget, true); + } + + // have to have some tab as the active tab of the overflow notebook + append_useless_page(m_pOverFlowNotebook); + gtk_notebook_set_current_page(m_pOverFlowNotebook, gtk_notebook_get_n_pages(m_pOverFlowNotebook) - 1); + if (gtk_widget_has_focus(GTK_WIDGET(m_pOverFlowNotebook))) + gtk_widget_grab_focus(GTK_WIDGET(m_pNotebook)); + + // add this temporarily to the normal notebook to measure how wide + // the row would be if switched to the other notebook + append_useless_page(m_pNotebook); + + gtk_widget_get_preferred_size(GTK_WIDGET(m_pNotebook), nullptr, &size1); + gtk_widget_get_preferred_size(GTK_WIDGET(m_pOverFlowNotebook), nullptr, &size2); + + auto nWidth = std::max(size1.width, size2.width); + gtk_widget_set_size_request(GTK_WIDGET(m_pNotebook), nWidth, alloc.height); + gtk_widget_set_size_request(GTK_WIDGET(m_pOverFlowNotebook), nWidth, -1); + + // remove it once we've measured it + remove_page(m_pNotebook, "useless"); + + gtk_widget_thaw_child_notify(GTK_WIDGET(m_pOverFlowNotebook)); + gtk_widget_thaw_child_notify(GTK_WIDGET(m_pNotebook)); + + m_bOverFlowBoxActive = true; + } + + static gboolean launch_split_notebooks(GtkInstanceNotebook* pThis) + { + int nCurrentPage = pThis->get_current_page(); + pThis->split_notebooks(); + pThis->set_current_page(nCurrentPage); + pThis->m_nLaunchSplitTimeoutId = 0; + return false; + } + + // tdf#120371 + // https://developer.gnome.org/hig-book/unstable/controls-notebooks.html.en#controls-too-many-tabs + // if no of tabs > 6, but only if the notebook would auto-scroll, then split the tabs over + // two notebooks. Checking for the auto-scroll allows themes like Ambience under Ubuntu 16.04 to keep + // tabs in a single row when they would fit + void signal_notebook_size_allocate() + { + if (m_bOverFlowBoxActive || m_nLaunchSplitTimeoutId) + return; + disable_notify_events(); + gint nPages = gtk_notebook_get_n_pages(m_pNotebook); + if (nPages > 6 && gtk_notebook_get_tab_pos(m_pNotebook) == GTK_POS_TOP) + { + for (gint i = 0; i < nPages; ++i) + { + GtkWidget* pTabWidget = gtk_notebook_get_tab_label(m_pNotebook, gtk_notebook_get_nth_page(m_pNotebook, i)); + if (!gtk_widget_get_child_visible(pTabWidget)) + { + m_nLaunchSplitTimeoutId = g_timeout_add_full(G_PRIORITY_HIGH_IDLE, 0, reinterpret_cast<GSourceFunc>(launch_split_notebooks), this, nullptr); + break; + } + } + } + enable_notify_events(); + } + + static void signalSizeAllocate(GtkWidget*, GdkRectangle*, gpointer widget) + { + GtkInstanceNotebook* pThis = static_cast<GtkInstanceNotebook*>(widget); + pThis->signal_notebook_size_allocate(); + } + + bool signal_focus(GtkDirectionType direction) + { + if (!m_bOverFlowBoxActive) + return false; + + int nPage = gtk_notebook_get_current_page(m_pNotebook); + if (direction == GTK_DIR_LEFT && nPage == 0) + { + auto nOverFlowLen = gtk_notebook_get_n_pages(m_pOverFlowNotebook) - 1; + gtk_notebook_set_current_page(m_pOverFlowNotebook, nOverFlowLen - 1); + return true; + } + else if (direction == GTK_DIR_RIGHT && nPage == gtk_notebook_get_n_pages(m_pNotebook) - 1) + { + gtk_notebook_set_current_page(m_pOverFlowNotebook, 0); + return true; + } + + return false; + } + + static gboolean signalFocus(GtkNotebook* notebook, GtkDirectionType direction, gpointer widget) + { + // if the notebook widget itself has focus + if (gtk_widget_is_focus(GTK_WIDGET(notebook))) + { + GtkInstanceNotebook* pThis = static_cast<GtkInstanceNotebook*>(widget); + return pThis->signal_focus(direction); + } + return false; + } + + // ctrl + page_up/ page_down + bool signal_change_current_page(gint arg1) + { + bool bHandled = signal_focus(arg1 < 0 ? GTK_DIR_LEFT : GTK_DIR_RIGHT); + if (bHandled) + g_signal_stop_emission_by_name(m_pNotebook, "change-current-page"); + return false; + } + + static gboolean signalChangeCurrentPage(GtkNotebook*, gint arg1, gpointer widget) + { + if (arg1 == 0) + return true; + GtkInstanceNotebook* pThis = static_cast<GtkInstanceNotebook*>(widget); + return pThis->signal_change_current_page(arg1); + } + +public: + GtkInstanceNotebook(GtkNotebook* pNotebook, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : GtkInstanceContainer(GTK_CONTAINER(pNotebook), pBuilder, bTakeOwnership) + , m_pNotebook(pNotebook) + , m_pOverFlowBox(nullptr) + , m_pOverFlowNotebook(GTK_NOTEBOOK(gtk_notebook_new())) + , m_nSwitchPageSignalId(g_signal_connect(pNotebook, "switch-page", G_CALLBACK(signalSwitchPage), this)) + , m_nOverFlowSwitchPageSignalId(g_signal_connect(m_pOverFlowNotebook, "switch-page", G_CALLBACK(signalOverFlowSwitchPage), this)) + , m_nFocusSignalId(g_signal_connect(pNotebook, "focus", G_CALLBACK(signalFocus), this)) + , m_nChangeCurrentPageId(g_signal_connect(pNotebook, "change-current-page", G_CALLBACK(signalChangeCurrentPage), this)) + , m_nLaunchSplitTimeoutId(0) + , m_bOverFlowBoxActive(false) + , m_bOverFlowBoxIsStart(false) + , m_bInternalPageChange(false) + , m_nStartTabCount(0) + , m_nEndTabCount(0) + { + gtk_widget_add_events(GTK_WIDGET(pNotebook), GDK_SCROLL_MASK); + if (get_n_pages() > 6) + m_nSizeAllocateSignalId = g_signal_connect_after(pNotebook, "size-allocate", G_CALLBACK(signalSizeAllocate), this); + else + m_nSizeAllocateSignalId = 0; + gtk_notebook_set_show_border(m_pOverFlowNotebook, false); + + // tdf#122623 it's nigh impossible to have a GtkNotebook without an active (checked) tab, so try and theme + // the unwanted tab into invisibility + GtkStyleContext *pNotebookContext = gtk_widget_get_style_context(GTK_WIDGET(m_pOverFlowNotebook)); + GtkCssProvider *pProvider = gtk_css_provider_new(); + static const gchar data[] = "header.top > tabs > tab:checked { box-shadow: none; padding: 0 0 0 0; margin: 0 0 0 0; border-image: none; border-image-width: 0 0 0 0; background-image: none; background-color: transparent; border-radius: 0 0 0 0; border-width: 0 0 0 0; border-style: none; border-color: transparent; opacity: 0; min-height: 0; min-width: 0; }"; + static const gchar olddata[] = "tab.top:active { box-shadow: none; padding: 0 0 0 0; margin: 0 0 0 0; border-image: none; border-image-width: 0 0 0 0; background-image: none; background-color: transparent; border-radius: 0 0 0 0; border-width: 0 0 0 0; border-style: none; border-color: transparent; opacity: 0; }"; + gtk_css_provider_load_from_data(pProvider, gtk_check_version(3, 20, 0) == nullptr ? data : olddata, -1, nullptr); + gtk_style_context_add_provider(pNotebookContext, GTK_STYLE_PROVIDER(pProvider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + } + + virtual int get_current_page() const override + { + int nPage = gtk_notebook_get_current_page(m_pNotebook); + if (nPage == -1) + return nPage; + if (m_bOverFlowBoxIsStart) + { + auto nOverFlowLen = m_bOverFlowBoxActive ? gtk_notebook_get_n_pages(m_pOverFlowNotebook) - 1 : 0; + // add count of overflow pages, minus the extra tab + nPage += nOverFlowLen; + } + return nPage; + } + + virtual OString get_page_ident(int nPage) const override + { + auto nMainLen = gtk_notebook_get_n_pages(m_pNotebook); + auto nOverFlowLen = m_bOverFlowBoxActive ? gtk_notebook_get_n_pages(m_pOverFlowNotebook) - 1 : 0; + if (m_bOverFlowBoxIsStart) + { + if (nPage < nOverFlowLen) + return get_page_ident(m_pOverFlowNotebook, nPage); + nPage -= nOverFlowLen; + return get_page_ident(m_pNotebook, nPage); + } + else + { + if (nPage < nMainLen) + return get_page_ident(m_pNotebook, nPage); + nPage -= nMainLen; + return get_page_ident(m_pOverFlowNotebook, nPage); + } + } + + virtual OString get_current_page_ident() const override + { + const int nPage = get_current_page(); + return nPage != -1 ? get_page_ident(nPage) : OString(); + } + + virtual weld::Container* get_page(const OString& rIdent) const override + { + int nPage = get_page_number(rIdent); + if (nPage < 0) + return nullptr; + + GtkContainer* pChild; + if (m_bOverFlowBoxIsStart) + { + auto nOverFlowLen = m_bOverFlowBoxActive ? gtk_notebook_get_n_pages(m_pOverFlowNotebook) - 1 : 0; + if (nPage < nOverFlowLen) + pChild = GTK_CONTAINER(gtk_notebook_get_nth_page(m_pOverFlowNotebook, nPage)); + else + { + nPage -= nOverFlowLen; + pChild = GTK_CONTAINER(gtk_notebook_get_nth_page(m_pNotebook, nPage)); + } + } + else + { + auto nMainLen = gtk_notebook_get_n_pages(m_pNotebook); + if (nPage < nMainLen) + pChild = GTK_CONTAINER(gtk_notebook_get_nth_page(m_pNotebook, nPage)); + else + { + nPage -= nMainLen; + pChild = GTK_CONTAINER(gtk_notebook_get_nth_page(m_pOverFlowNotebook, nPage)); + } + } + + unsigned int nPageIndex = static_cast<unsigned int>(nPage); + if (m_aPages.size() < nPageIndex + 1) + m_aPages.resize(nPageIndex + 1); + if (!m_aPages[nPageIndex]) + m_aPages[nPageIndex].reset(new GtkInstanceContainer(pChild, m_pBuilder, false)); + return m_aPages[nPageIndex].get(); + } + + virtual void set_current_page(int nPage) override + { + // normally we'd call disable_notify_events/enable_notify_events here, + // but the notebook is complicated by the need to support the + // double-decker hackery so for simplicity just flag that the page + // change is not a directly user-triggered one + bool bInternalPageChange = m_bInternalPageChange; + m_bInternalPageChange = true; + + if (m_bOverFlowBoxIsStart) + { + auto nOverFlowLen = m_bOverFlowBoxActive ? gtk_notebook_get_n_pages(m_pOverFlowNotebook) - 1 : 0; + if (nPage < nOverFlowLen) + gtk_notebook_set_current_page(m_pOverFlowNotebook, nPage); + else + { + nPage -= nOverFlowLen; + gtk_notebook_set_current_page(m_pNotebook, nPage); + } + } + else + { + auto nMainLen = gtk_notebook_get_n_pages(m_pNotebook); + if (nPage < nMainLen) + gtk_notebook_set_current_page(m_pNotebook, nPage); + else + { + nPage -= nMainLen; + gtk_notebook_set_current_page(m_pOverFlowNotebook, nPage); + } + } + + m_bInternalPageChange = bInternalPageChange; + } + + virtual void set_current_page(const OString& rIdent) override + { + gint nPage = get_page_number(rIdent); + set_current_page(nPage); + } + + virtual int get_n_pages() const override + { + int nLen = gtk_notebook_get_n_pages(m_pNotebook); + if (m_bOverFlowBoxActive) + nLen += gtk_notebook_get_n_pages(m_pOverFlowNotebook) - 1; + return nLen; + } + + virtual OUString get_tab_label_text(const OString& rIdent) const override + { + gint nPageNum = get_page_number(m_pNotebook, rIdent); + if (nPageNum != -1) + return get_tab_label_text(m_pNotebook, nPageNum); + nPageNum = get_page_number(m_pOverFlowNotebook, rIdent); + if (nPageNum != -1) + return get_tab_label_text(m_pOverFlowNotebook, nPageNum); + return OUString(); + } + + virtual void set_tab_label_text(const OString& rIdent, const OUString& rText) override + { + gint nPageNum = get_page_number(m_pNotebook, rIdent); + if (nPageNum != -1) + { + set_tab_label_text(m_pNotebook, nPageNum, rText); + return; + } + nPageNum = get_page_number(m_pOverFlowNotebook, rIdent); + if (nPageNum != -1) + { + set_tab_label_text(m_pOverFlowNotebook, nPageNum, rText); + } + } + + virtual void disable_notify_events() override + { + g_signal_handler_block(m_pNotebook, m_nSwitchPageSignalId); + g_signal_handler_block(m_pNotebook, m_nFocusSignalId); + g_signal_handler_block(m_pNotebook, m_nChangeCurrentPageId); + g_signal_handler_block(m_pOverFlowNotebook, m_nOverFlowSwitchPageSignalId); + gtk_widget_freeze_child_notify(GTK_WIDGET(m_pOverFlowNotebook)); + GtkInstanceContainer::disable_notify_events(); + } + + virtual void enable_notify_events() override + { + GtkInstanceContainer::enable_notify_events(); + gtk_widget_thaw_child_notify(GTK_WIDGET(m_pOverFlowNotebook)); + g_signal_handler_unblock(m_pOverFlowNotebook, m_nOverFlowSwitchPageSignalId); + g_signal_handler_unblock(m_pNotebook, m_nSwitchPageSignalId); + g_signal_handler_unblock(m_pNotebook, m_nFocusSignalId); + g_signal_handler_unblock(m_pNotebook, m_nChangeCurrentPageId); + } + + void reset_split_data() + { + // reset overflow and allow it to be recalculated if necessary + gtk_widget_hide(GTK_WIDGET(m_pOverFlowNotebook)); + m_bOverFlowBoxActive = false; + m_nStartTabCount = 0; + m_nEndTabCount = 0; + } + + virtual void remove_page(const OString& rIdent) override + { + if (m_bOverFlowBoxActive) + { + unsplit_notebooks(); + reset_split_data(); + } + + unsigned int nPageIndex = remove_page(m_pNotebook, rIdent); + if (nPageIndex < m_aPages.size()) + m_aPages.erase(m_aPages.begin() + nPageIndex); + } + + virtual void insert_page(const OString& rIdent, const OUString& rLabel, int nPos) override + { + if (m_bOverFlowBoxActive) + { + unsplit_notebooks(); + reset_split_data(); + } + + // reset overflow and allow it to be recalculated if necessary + gtk_widget_hide(GTK_WIDGET(m_pOverFlowNotebook)); + m_bOverFlowBoxActive = false; + + insert_page(m_pNotebook, rIdent, rLabel, gtk_grid_new(), nPos); + } + + virtual ~GtkInstanceNotebook() override + { + if (m_nLaunchSplitTimeoutId) + g_source_remove(m_nLaunchSplitTimeoutId); + if (m_nSizeAllocateSignalId) + g_signal_handler_disconnect(m_pNotebook, m_nSizeAllocateSignalId); + g_signal_handler_disconnect(m_pNotebook, m_nSwitchPageSignalId); + g_signal_handler_disconnect(m_pNotebook, m_nFocusSignalId); + g_signal_handler_disconnect(m_pNotebook, m_nChangeCurrentPageId); + g_signal_handler_disconnect(m_pOverFlowNotebook, m_nOverFlowSwitchPageSignalId); + gtk_widget_destroy(GTK_WIDGET(m_pOverFlowNotebook)); + if (m_pOverFlowBox) + { + // put it back to how we found it initially + GtkWidget* pParent = gtk_widget_get_parent(GTK_WIDGET(m_pOverFlowBox)); + g_object_ref(m_pNotebook); + gtk_container_remove(GTK_CONTAINER(m_pOverFlowBox), GTK_WIDGET(m_pNotebook)); + gtk_container_add(GTK_CONTAINER(pParent), GTK_WIDGET(m_pNotebook)); + g_object_unref(m_pNotebook); + + gtk_widget_destroy(GTK_WIDGET(m_pOverFlowBox)); + } + } +}; + +class GtkInstanceButton : public GtkInstanceContainer, public virtual weld::Button +{ +private: + GtkButton* m_pButton; + gulong m_nSignalId; + + static void signalClicked(GtkButton*, gpointer widget) + { + GtkInstanceButton* pThis = static_cast<GtkInstanceButton*>(widget); + SolarMutexGuard aGuard; + pThis->signal_clicked(); + } + +public: + GtkInstanceButton(GtkButton* pButton, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : GtkInstanceContainer(GTK_CONTAINER(pButton), pBuilder, bTakeOwnership) + , m_pButton(pButton) + , m_nSignalId(g_signal_connect(pButton, "clicked", G_CALLBACK(signalClicked), this)) + { + g_object_set_data(G_OBJECT(m_pButton), "g-lo-GtkInstanceButton", this); + } + + virtual void set_label(const OUString& rText) override + { + ::set_label(m_pButton, rText); + } + + virtual void set_image(VirtualDevice* pDevice) override + { + gtk_button_set_always_show_image(m_pButton, true); + gtk_button_set_image_position(m_pButton, GTK_POS_LEFT); + if (pDevice) + gtk_button_set_image(m_pButton, image_new_from_virtual_device(*pDevice)); + else + gtk_button_set_image(m_pButton, nullptr); + } + + virtual void set_from_icon_name(const OUString& rIconName) override + { + GdkPixbuf* pixbuf = load_icon_by_name(rIconName); + if (!pixbuf) + gtk_button_set_image(m_pButton, nullptr); + else + { + gtk_button_set_image(m_pButton, gtk_image_new_from_pixbuf(pixbuf)); + g_object_unref(pixbuf); + } + } + + virtual void set_image(const css::uno::Reference<css::graphic::XGraphic>& rImage) override + { + GdkPixbuf* pixbuf = getPixbuf(rImage); + if (!pixbuf) + gtk_button_set_image(m_pButton, nullptr); + else + { + gtk_button_set_image(m_pButton, gtk_image_new_from_pixbuf(pixbuf)); + g_object_unref(pixbuf); + } + } + + virtual OUString get_label() const override + { + return ::get_label(m_pButton); + } + + virtual void set_label_line_wrap(bool wrap) override + { + GtkWidget* pChild = gtk_bin_get_child(GTK_BIN(m_pButton)); + gtk_label_set_line_wrap(GTK_LABEL(pChild), wrap); + } + + // allow us to block buttons with click handlers making dialogs return a response + bool has_click_handler() const + { + return m_aClickHdl.IsSet(); + } + + void clear_click_handler() + { + m_aClickHdl = Link<Button&, void>(); + } + + virtual void disable_notify_events() override + { + g_signal_handler_block(m_pButton, m_nSignalId); + GtkInstanceContainer::disable_notify_events(); + } + + virtual void enable_notify_events() override + { + GtkInstanceContainer::enable_notify_events(); + g_signal_handler_unblock(m_pButton, m_nSignalId); + } + + virtual ~GtkInstanceButton() override + { + g_object_steal_data(G_OBJECT(m_pButton), "g-lo-GtkInstanceButton"); + g_signal_handler_disconnect(m_pButton, m_nSignalId); + } +}; + +} + +void GtkInstanceDialog::asyncresponse(gint ret) +{ + if (ret == GTK_RESPONSE_HELP) + { + help(); + return; + } + + GtkInstanceButton* pClickHandler = has_click_handler(ret); + if (pClickHandler) + { + // make GTK_RESPONSE_DELETE_EVENT act as if cancel button was pressed + if (ret == GTK_RESPONSE_DELETE_EVENT) + close(false); + return; + } + + if (get_modal()) + m_aDialogRun.dec_modal_count(); + hide(); + + // move the self pointer, otherwise it might be de-allocated by time we try to reset it + auto xRunAsyncSelf = std::move(m_xRunAsyncSelf); + auto xDialogController = std::move(m_xDialogController); + auto aFunc = std::move(m_aFunc); + + auto nResponseSignalId = m_nResponseSignalId; + auto nCancelSignalId = m_nCancelSignalId; + auto nSignalDeleteId = m_nSignalDeleteId; + m_nResponseSignalId = 0; + m_nCancelSignalId = 0; + m_nSignalDeleteId = 0; + + aFunc(GtkToVcl(ret)); + + if (nResponseSignalId) + g_signal_handler_disconnect(m_pDialog, nResponseSignalId); + if (nCancelSignalId) + g_signal_handler_disconnect(m_pDialog, nCancelSignalId); + if (nSignalDeleteId) + g_signal_handler_disconnect(m_pDialog, nSignalDeleteId); + + xDialogController.reset(); + xRunAsyncSelf.reset(); +} + +int GtkInstanceDialog::run() +{ + if (GTK_IS_DIALOG(m_pDialog)) + sort_native_button_order(GTK_BOX(gtk_dialog_get_action_area(GTK_DIALOG(m_pDialog)))); + int ret; + while (true) + { + ret = m_aDialogRun.run(); + if (ret == GTK_RESPONSE_HELP) + { + help(); + continue; + } + else if (has_click_handler(ret)) + continue; + break; + } + hide(); + return GtkToVcl(ret); +} + +weld::Button* GtkInstanceDialog::weld_widget_for_response(int nVclResponse) +{ + GtkButton* pButton = get_widget_for_response(VclToGtk(nVclResponse)); + if (!pButton) + return nullptr; + return new GtkInstanceButton(pButton, m_pBuilder, false); +} + +void GtkInstanceDialog::response(int nResponse) +{ + int nGtkResponse = VclToGtk(nResponse); + //unblock this response now when activated through code + if (GtkButton* pWidget = get_widget_for_response(nGtkResponse)) + { + void* pData = g_object_get_data(G_OBJECT(pWidget), "g-lo-GtkInstanceButton"); + GtkInstanceButton* pButton = static_cast<GtkInstanceButton*>(pData); + if (pButton) + pButton->clear_click_handler(); + } + if (GTK_IS_DIALOG(m_pDialog)) + gtk_dialog_response(GTK_DIALOG(m_pDialog), nGtkResponse); + else if (GTK_IS_ASSISTANT(m_pDialog)) + { + if (!m_aDialogRun.loop_is_running()) + asyncresponse(nGtkResponse); + else + { + m_aDialogRun.m_nResponseId = nGtkResponse; + m_aDialogRun.loop_quit(); + } + } +} + +void GtkInstanceDialog::close(bool bCloseSignal) +{ + GtkInstanceButton* pClickHandler = has_click_handler(GTK_RESPONSE_CANCEL); + if (pClickHandler) + { + if (bCloseSignal) + g_signal_stop_emission_by_name(m_pDialog, "close"); + // make esc (bCloseSignal == true) or window-delete (bCloseSignal == false) + // act as if cancel button was pressed + pClickHandler->clicked(); + return; + } + response(RET_CANCEL); +} + +GtkInstanceButton* GtkInstanceDialog::has_click_handler(int nResponse) +{ + GtkInstanceButton* pButton = nullptr; + // e.g. map GTK_RESPONSE_DELETE_EVENT to GTK_RESPONSE_CANCEL + nResponse = VclToGtk(GtkToVcl(nResponse)); + if (GtkButton* pWidget = get_widget_for_response(nResponse)) + { + void* pData = g_object_get_data(G_OBJECT(pWidget), "g-lo-GtkInstanceButton"); + pButton = static_cast<GtkInstanceButton*>(pData); + if (pButton && !pButton->has_click_handler()) + pButton = nullptr; + } + return pButton; +} + +namespace { + +class GtkInstanceToggleButton : public GtkInstanceButton, public virtual weld::ToggleButton +{ +private: + GtkToggleButton* m_pToggleButton; + gulong m_nSignalId; + + static void signalToggled(GtkToggleButton*, gpointer widget) + { + GtkInstanceToggleButton* pThis = static_cast<GtkInstanceToggleButton*>(widget); + SolarMutexGuard aGuard; + pThis->signal_toggled(); + } +public: + GtkInstanceToggleButton(GtkToggleButton* pButton, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : GtkInstanceButton(GTK_BUTTON(pButton), pBuilder, bTakeOwnership) + , m_pToggleButton(pButton) + , m_nSignalId(g_signal_connect(m_pToggleButton, "toggled", G_CALLBACK(signalToggled), this)) + { + } + + virtual void set_active(bool active) override + { + disable_notify_events(); + gtk_toggle_button_set_inconsistent(m_pToggleButton, false); + gtk_toggle_button_set_active(m_pToggleButton, active); + enable_notify_events(); + } + + virtual bool get_active() const override + { + return gtk_toggle_button_get_active(m_pToggleButton); + } + + virtual void set_inconsistent(bool inconsistent) override + { + gtk_toggle_button_set_inconsistent(m_pToggleButton, inconsistent); + } + + virtual bool get_inconsistent() const override + { + return gtk_toggle_button_get_inconsistent(m_pToggleButton); + } + + virtual void disable_notify_events() override + { + g_signal_handler_block(m_pToggleButton, m_nSignalId); + GtkInstanceButton::disable_notify_events(); + } + + virtual void enable_notify_events() override + { + GtkInstanceButton::enable_notify_events(); + g_signal_handler_unblock(m_pToggleButton, m_nSignalId); + } + + virtual ~GtkInstanceToggleButton() override + { + g_signal_handler_disconnect(m_pToggleButton, m_nSignalId); + } +}; + +void do_grab(GtkWidget* pWidget) +{ + GdkDisplay *pDisplay = gtk_widget_get_display(pWidget); +#if GTK_CHECK_VERSION(3, 20, 0) + if (gtk_check_version(3, 20, 0) == nullptr) + { + GdkSeat* pSeat = gdk_display_get_default_seat(pDisplay); + gdk_seat_grab(pSeat, gtk_widget_get_window(pWidget), + GDK_SEAT_CAPABILITY_ALL, true, nullptr, nullptr, nullptr, nullptr); + return; + } +#endif + //else older gtk3 + GdkDeviceManager* pDeviceManager = gdk_display_get_device_manager(pDisplay); + GdkDevice* pPointer = gdk_device_manager_get_client_pointer(pDeviceManager); + GdkWindow* pWindow = gtk_widget_get_window(pWidget); + guint32 nCurrentTime = gtk_get_current_event_time(); + gdk_device_grab(pPointer, pWindow, GDK_OWNERSHIP_NONE, true, GDK_ALL_EVENTS_MASK, nullptr, nCurrentTime); + if (GdkDevice* pKeyboard = gdk_device_get_associated_device(pPointer)) + gdk_device_grab(pKeyboard, pWindow, GDK_OWNERSHIP_NONE, true, GDK_ALL_EVENTS_MASK, nullptr, nCurrentTime); +} + +void do_ungrab(GtkWidget* pWidget) +{ + GdkDisplay *pDisplay = gtk_widget_get_display(pWidget); +#if GTK_CHECK_VERSION(3, 20, 0) + if (gtk_check_version(3, 20, 0) == nullptr) + { + GdkSeat* pSeat = gdk_display_get_default_seat(pDisplay); + gdk_seat_ungrab(pSeat); + return; + } +#endif + //else older gtk3 + GdkDeviceManager* pDeviceManager = gdk_display_get_device_manager(pDisplay); + GdkDevice* pPointer = gdk_device_manager_get_client_pointer(pDeviceManager); + guint32 nCurrentTime = gtk_get_current_event_time(); + gdk_device_ungrab(pPointer, nCurrentTime); + if (GdkDevice* pKeyboard = gdk_device_get_associated_device(pPointer)) + gdk_device_ungrab(pKeyboard, nCurrentTime); +} + +GtkPositionType show_menu_older_gtk(GtkWidget* pMenuButton, GtkWindow* pMenu) +{ + //place the toplevel just below its launcher button + GtkWidget* pToplevel = gtk_widget_get_toplevel(pMenuButton); + gint x, y, absx, absy; + gtk_widget_translate_coordinates(pMenuButton, pToplevel, 0, 0, &x, &y); + GdkWindow *pWindow = gtk_widget_get_window(pToplevel); + gdk_window_get_position(pWindow, &absx, &absy); + + x += absx; + y += absy; + + gint nButtonHeight = gtk_widget_get_allocated_height(pMenuButton); + y += nButtonHeight; + + gtk_window_group_add_window(gtk_window_get_group(GTK_WINDOW(pToplevel)), pMenu); + gtk_window_set_transient_for(pMenu, GTK_WINDOW(pToplevel)); + + GtkRequisition req; + gtk_widget_get_preferred_size(GTK_WIDGET(pMenu), nullptr, &req); + gint nMenuWidth = req.width; + gint nMenuHeight = req.height; + + tools::Rectangle aWorkArea(::get_monitor_workarea(pMenuButton)); + + // shrink it a little, I find it reassuring to see a little margin with a + // long menu to know the menu is fully on screen + aWorkArea.AdjustTop(8); + aWorkArea.AdjustBottom(-8); + gint endx = x + nMenuWidth; + if (endx > aWorkArea.Right()) + { + x -= endx - aWorkArea.Right(); + if (x < 0) + x = 0; + } + + GtkPositionType ePosUsed = GTK_POS_BOTTOM; + + gint endy = y + nMenuHeight; + gint nMissingBelow = endy - aWorkArea.Bottom(); + if (nMissingBelow > 0) + { + gint nNewY = y - (nButtonHeight + nMenuHeight); + if (nNewY < aWorkArea.Top()) + { + gint nMissingAbove = aWorkArea.Top() - nNewY; + if (nMissingBelow <= nMissingAbove) + nMenuHeight -= nMissingBelow; + else + { + nMenuHeight -= nMissingAbove; + y = aWorkArea.Top(); + ePosUsed = GTK_POS_TOP; + } + gtk_widget_set_size_request(GTK_WIDGET(pMenu), nMenuWidth, nMenuHeight); + } + else + { + y = nNewY; + ePosUsed = GTK_POS_TOP; + } + } + + gtk_window_move(pMenu, x, y); + + return ePosUsed; +} + +bool show_menu_newer_gtk(GtkWidget* pComboBox, GtkWindow* pMenu) +{ + static auto window_move_to_rect = reinterpret_cast<void (*) (GdkWindow*, const GdkRectangle*, GdkGravity, + GdkGravity, GdkAnchorHints, gint, gint)>( + dlsym(nullptr, "gdk_window_move_to_rect")); + if (!window_move_to_rect) + return false; + +#if defined(GDK_WINDOWING_X11) + // under wayland gdk_window_move_to_rect works great for me, but in my current + // gtk 3.24 under X it leaves part of long menus outside the work area + GdkDisplay *pDisplay = gtk_widget_get_display(pComboBox); + if (DLSYM_GDK_IS_X11_DISPLAY(pDisplay)) + return false; +#endif + + //place the toplevel just below its launcher button + GtkWidget* pToplevel = gtk_widget_get_toplevel(pComboBox); + gint x, y; + gtk_widget_translate_coordinates(pComboBox, pToplevel, 0, 0, &x, &y); + + gtk_widget_realize(GTK_WIDGET(pMenu)); + gtk_window_group_add_window(gtk_window_get_group(GTK_WINDOW(pToplevel)), pMenu); + gtk_window_set_transient_for(pMenu, GTK_WINDOW(pToplevel)); + + gint nComboWidth = gtk_widget_get_allocated_width(pComboBox); + gint nComboHeight = gtk_widget_get_allocated_height(pComboBox); + + bool bSwapForRTL = SwapForRTL(GTK_WIDGET(pComboBox)); + + GdkGravity rect_anchor = !bSwapForRTL ? GDK_GRAVITY_SOUTH_WEST : GDK_GRAVITY_SOUTH_EAST; + GdkGravity menu_anchor = !bSwapForRTL ? GDK_GRAVITY_NORTH_WEST : GDK_GRAVITY_NORTH_EAST; + GdkRectangle rect {x, y, nComboWidth, nComboHeight }; + GdkWindow* toplevel = gtk_widget_get_window(GTK_WIDGET(pMenu)); + + window_move_to_rect(toplevel, &rect, rect_anchor, menu_anchor, + static_cast<GdkAnchorHints>(GDK_ANCHOR_FLIP_Y | GDK_ANCHOR_RESIZE_Y | + GDK_ANCHOR_SLIDE_X | GDK_ANCHOR_RESIZE_X), + 0, 0); + + return true; +} + +GtkPositionType show_menu(GtkWidget* pMenuButton, GtkWindow* pMenu) +{ + // we only use ePosUsed in the replacement-for-X-popover case of a + // MenuButton, so we only need it when show_menu_older_gtk is used + GtkPositionType ePosUsed = GTK_POS_BOTTOM; + + // tdf#120764 It isn't allowed under wayland to have two visible popups that share + // the same top level parent. The problem is that since gtk 3.24 tooltips are also + // implemented as popups, which means that we cannot show any popup if there is a + // visible tooltip. + GtkWidget* pParent = gtk_widget_get_toplevel(pMenuButton); + GtkSalFrame* pFrame = pParent ? GtkSalFrame::getFromWindow(pParent) : nullptr; + if (pFrame) + { + // hide any current tooltip + pFrame->HideTooltip(); + // don't allow any more to appear until menu is dismissed + pFrame->BlockTooltip(); + } + + // try with gdk_window_move_to_rect, but if that's not available, try without + if (!show_menu_newer_gtk(pMenuButton, pMenu)) + ePosUsed = show_menu_older_gtk(pMenuButton, pMenu); + gtk_widget_show_all(GTK_WIDGET(pMenu)); + gtk_widget_grab_focus(GTK_WIDGET(pMenu)); + do_grab(GTK_WIDGET(pMenu)); + + return ePosUsed; +} + +class GtkInstanceMenuButton : public GtkInstanceToggleButton, public MenuHelper, public virtual weld::MenuButton +{ +private: + GtkMenuButton* m_pMenuButton; + GtkBox* m_pBox; + GtkImage* m_pImage; + GtkWidget* m_pLabel; + //popover cannot escape dialog under X so stick up own window instead + GtkWindow* m_pMenuHack; + //when doing so, if it's a toolbar menubutton align the menu to the full toolitem + GtkWidget* m_pMenuHackAlign; + GtkWidget* m_pPopover; + gulong m_nSignalId; + + static void signalToggled(GtkWidget*, gpointer widget) + { + GtkInstanceMenuButton* pThis = static_cast<GtkInstanceMenuButton*>(widget); + SolarMutexGuard aGuard; + pThis->toggle_menu(); + } + + void toggle_menu() + { + if (!m_pMenuHack) + return; + if (!get_active()) + { + do_ungrab(GTK_WIDGET(m_pMenuHack)); + + gtk_widget_hide(GTK_WIDGET(m_pMenuHack)); + //put contents back from where the came from + GtkWidget* pChild = gtk_bin_get_child(GTK_BIN(m_pMenuHack)); + g_object_ref(pChild); + gtk_container_remove(GTK_CONTAINER(m_pMenuHack), pChild); + gtk_container_add(GTK_CONTAINER(m_pPopover), pChild); + g_object_unref(pChild); + + // so gdk_window_move_to_rect will work again the next time + gtk_widget_unrealize(GTK_WIDGET(m_pMenuHack)); + + gtk_widget_set_size_request(GTK_WIDGET(m_pMenuHack), -1, -1); + + // undo show_menu tooltip blocking + GtkWidget* pParent = gtk_widget_get_toplevel(GTK_WIDGET(m_pMenuButton)); + GtkSalFrame* pFrame = pParent ? GtkSalFrame::getFromWindow(pParent) : nullptr; + if (pFrame) + pFrame->UnblockTooltip(); + } + else + { + //set border width + gtk_container_set_border_width(GTK_CONTAINER(m_pMenuHack), gtk_container_get_border_width(GTK_CONTAINER(m_pPopover))); + + //steal popover contents and smuggle into toplevel display window + GtkWidget* pChild = gtk_bin_get_child(GTK_BIN(m_pPopover)); + g_object_ref(pChild); + gtk_container_remove(GTK_CONTAINER(m_pPopover), pChild); + gtk_container_add(GTK_CONTAINER(m_pMenuHack), pChild); + g_object_unref(pChild); + + GtkPositionType ePosUsed = show_menu(m_pMenuHackAlign ? m_pMenuHackAlign : GTK_WIDGET(m_pMenuButton), m_pMenuHack); + // tdf#132540 keep the placeholder popover on this same side as the replacement menu + gtk_popover_set_position(gtk_menu_button_get_popover(m_pMenuButton), ePosUsed); + } + } + + static void signalGrabBroken(GtkWidget*, GdkEventGrabBroken *pEvent, gpointer widget) + { + GtkInstanceMenuButton* pThis = static_cast<GtkInstanceMenuButton*>(widget); + pThis->grab_broken(pEvent); + } + + void grab_broken(const GdkEventGrabBroken *event) + { + if (event->grab_window == nullptr) + { + set_active(false); + } + else + { + //try and regrab, so when we lose the grab to the menu of the color palette + //combobox we regain it so the color palette doesn't itself disappear on next + //click on the color palette combobox + do_grab(GTK_WIDGET(m_pMenuHack)); + } + } + + static gboolean signalButtonRelease(GtkWidget* pWidget, GdkEventButton* pEvent, gpointer widget) + { + GtkInstanceMenuButton* pThis = static_cast<GtkInstanceMenuButton*>(widget); + return pThis->button_release(pWidget, pEvent); + } + + bool button_release(GtkWidget* pWidget, GdkEventButton* pEvent) + { + //we want to pop down if the button was released outside our popup + gdouble x = pEvent->x_root; + gdouble y = pEvent->y_root; + gint xoffset, yoffset; + gdk_window_get_root_origin(gtk_widget_get_window(pWidget), &xoffset, &yoffset); + + GtkAllocation alloc; + gtk_widget_get_allocation(pWidget, &alloc); + xoffset += alloc.x; + yoffset += alloc.y; + + gtk_widget_get_allocation(GTK_WIDGET(m_pMenuHack), &alloc); + gint x1 = alloc.x + xoffset; + gint y1 = alloc.y + yoffset; + gint x2 = x1 + alloc.width; + gint y2 = y1 + alloc.height; + + if (x > x1 && x < x2 && y > y1 && y < y2) + return false; + + set_active(false); + + return false; + } + + static gboolean keyPress(GtkWidget*, GdkEventKey* pEvent, gpointer widget) + { + GtkInstanceMenuButton* pThis = static_cast<GtkInstanceMenuButton*>(widget); + return pThis->key_press(pEvent); + } + + bool key_press(const GdkEventKey* pEvent) + { + if (pEvent->keyval == GDK_KEY_Escape) + { + set_active(false); + return true; + } + return false; + } + + void ensure_image_widget() + { + if (!m_pImage) + { + m_pImage = GTK_IMAGE(gtk_image_new()); + gtk_box_pack_start(m_pBox, GTK_WIDGET(m_pImage), false, false, 0); + gtk_box_reorder_child(m_pBox, GTK_WIDGET(m_pImage), 0); + gtk_widget_show(GTK_WIDGET(m_pImage)); + } + } + +public: + GtkInstanceMenuButton(GtkMenuButton* pMenuButton, GtkWidget* pMenuAlign, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : GtkInstanceToggleButton(GTK_TOGGLE_BUTTON(pMenuButton), pBuilder, bTakeOwnership) + , MenuHelper(gtk_menu_button_get_popup(pMenuButton), false) + , m_pMenuButton(pMenuButton) + , m_pImage(nullptr) + , m_pMenuHack(nullptr) + , m_pMenuHackAlign(pMenuAlign) + , m_pPopover(nullptr) + , m_nSignalId(0) + { + m_pLabel = gtk_bin_get_child(GTK_BIN(m_pMenuButton)); + //do it "manually" so we can have the dropdown image in GtkMenuButtons shown + //on the right at the same time as this image is shown on the left + g_object_ref(m_pLabel); + gtk_container_remove(GTK_CONTAINER(m_pMenuButton), m_pLabel); + + gint nImageSpacing(2); + GtkStyleContext *pContext = gtk_widget_get_style_context(GTK_WIDGET(m_pMenuButton)); + gtk_style_context_get_style(pContext, "image-spacing", &nImageSpacing, nullptr); + m_pBox = GTK_BOX(gtk_box_new(GTK_ORIENTATION_HORIZONTAL, nImageSpacing)); + + gtk_box_pack_start(m_pBox, m_pLabel, false, false, 0); + g_object_unref(m_pLabel); + + if (gtk_toggle_button_get_mode(GTK_TOGGLE_BUTTON(m_pMenuButton))) + gtk_box_pack_end(m_pBox, gtk_image_new_from_icon_name("pan-down-symbolic", GTK_ICON_SIZE_BUTTON), false, false, 0); + + gtk_container_add(GTK_CONTAINER(m_pMenuButton), GTK_WIDGET(m_pBox)); + gtk_widget_show_all(GTK_WIDGET(m_pBox)); + } + + virtual void set_size_request(int nWidth, int nHeight) override + { + // tweak the label to get a narrower size to stick + if (GTK_IS_LABEL(m_pLabel)) + gtk_label_set_ellipsize(GTK_LABEL(m_pLabel), PANGO_ELLIPSIZE_MIDDLE); + gtk_widget_set_size_request(m_pWidget, nWidth, nHeight); + } + + virtual void set_label(const OUString& rText) override + { + ::set_label(GTK_LABEL(m_pLabel), rText); + } + + virtual OUString get_label() const override + { + return ::get_label(GTK_LABEL(m_pLabel)); + } + + virtual void set_image(VirtualDevice* pDevice) override + { + ensure_image_widget(); + if (pDevice) + { + if (gtk_check_version(3, 20, 0) == nullptr) + gtk_image_set_from_surface(m_pImage, get_underlying_cairo_surface(*pDevice)); + else + { + GdkPixbuf* pixbuf = getPixbuf(*pDevice); + gtk_image_set_from_pixbuf(m_pImage, pixbuf); + g_object_unref(pixbuf); + } + } + else + gtk_image_set_from_surface(m_pImage, nullptr); + } + + virtual void set_image(const css::uno::Reference<css::graphic::XGraphic>& rImage) override + { + ensure_image_widget(); + GdkPixbuf* pixbuf = getPixbuf(rImage); + if (pixbuf) + { + gtk_image_set_from_pixbuf(m_pImage, pixbuf); + g_object_unref(pixbuf); + } + else + gtk_image_set_from_surface(m_pImage, nullptr); + } + + virtual void insert_item(int pos, const OUString& rId, const OUString& rStr, + const OUString* pIconName, VirtualDevice* pImageSurface, TriState eCheckRadioFalse) override + { + MenuHelper::insert_item(pos, rId, rStr, pIconName, pImageSurface, eCheckRadioFalse); + } + + virtual void insert_separator(int pos, const OUString& rId) override + { + MenuHelper::insert_separator(pos, rId); + } + + virtual void remove_item(const OString& rId) override + { + MenuHelper::remove_item(rId); + } + + virtual void clear() override + { + clear_items(); + } + + virtual void set_item_active(const OString& rIdent, bool bActive) override + { + MenuHelper::set_item_active(rIdent, bActive); + } + + virtual void set_item_sensitive(const OString& rIdent, bool bSensitive) override + { + MenuHelper::set_item_sensitive(rIdent, bSensitive); + } + + virtual void set_item_label(const OString& rIdent, const OUString& rLabel) override + { + MenuHelper::set_item_label(rIdent, rLabel); + } + + virtual OUString get_item_label(const OString& rIdent) const override + { + return MenuHelper::get_item_label(rIdent); + } + + virtual void set_item_visible(const OString& rIdent, bool bVisible) override + { + MenuHelper::set_item_visible(rIdent, bVisible); + } + + virtual void set_item_help_id(const OString& rIdent, const OString& rHelpId) override + { + MenuHelper::set_item_help_id(rIdent, rHelpId); + } + + virtual OString get_item_help_id(const OString& rIdent) const override + { + return MenuHelper::get_item_help_id(rIdent); + } + + virtual void signal_activate(GtkMenuItem* pItem) override + { + const gchar* pStr = gtk_buildable_get_name(GTK_BUILDABLE(pItem)); + signal_selected(OString(pStr, pStr ? strlen(pStr) : 0)); + } + + virtual void set_popover(weld::Widget* pPopover) override + { + GtkInstanceWidget* pPopoverWidget = dynamic_cast<GtkInstanceWidget*>(pPopover); + m_pPopover = pPopoverWidget ? pPopoverWidget->getWidget() : nullptr; + +#if defined(GDK_WINDOWING_X11) + if (!m_pMenuHack) + { + //under wayland a Popover will work to "escape" the parent dialog, not + //so under X, so come up with this hack to use a raw GtkWindow + GdkDisplay *pDisplay = gtk_widget_get_display(m_pWidget); + if (DLSYM_GDK_IS_X11_DISPLAY(pDisplay)) + { + m_pMenuHack = GTK_WINDOW(gtk_window_new(GTK_WINDOW_POPUP)); + gtk_window_set_type_hint(m_pMenuHack, GDK_WINDOW_TYPE_HINT_COMBO); + gtk_window_set_modal(m_pMenuHack, true); + gtk_window_set_resizable(m_pMenuHack, false); + m_nSignalId = g_signal_connect(GTK_TOGGLE_BUTTON(m_pMenuButton), "toggled", G_CALLBACK(signalToggled), this); + g_signal_connect(m_pMenuHack, "grab-broken-event", G_CALLBACK(signalGrabBroken), this); + g_signal_connect(m_pMenuHack, "button-release-event", G_CALLBACK(signalButtonRelease), this); + g_signal_connect(m_pMenuHack, "key-press-event", G_CALLBACK(keyPress), this); + } + } +#endif + + if (m_pMenuHack) + { + GtkWidget* pPlaceHolder = gtk_popover_new(GTK_WIDGET(m_pMenuButton)); + gtk_popover_set_transitions_enabled(GTK_POPOVER(pPlaceHolder), false); + + // tdf#132540 theme the unwanted popover into invisibility + GtkStyleContext *pPopoverContext = gtk_widget_get_style_context(pPlaceHolder); + GtkCssProvider *pProvider = gtk_css_provider_new(); + static const gchar data[] = "popover { box-shadow: none; padding: 0 0 0 0; margin: 0 0 0 0; border-image: none; border-image-width: 0 0 0 0; background-image: none; background-color: transparent; border-radius: 0 0 0 0; border-width: 0 0 0 0; border-style: none; border-color: transparent; opacity: 0; min-height: 0; min-width: 0; }"; + gtk_css_provider_load_from_data(pProvider, data, -1, nullptr); + gtk_style_context_add_provider(pPopoverContext, GTK_STYLE_PROVIDER(pProvider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + + gtk_menu_button_set_popover(m_pMenuButton, pPlaceHolder); + } + else + { + gtk_menu_button_set_popover(m_pMenuButton, m_pPopover); + if (m_pPopover) + gtk_widget_show_all(m_pPopover); + } + } + + void set_menu(weld::Menu* pMenu); + + virtual ~GtkInstanceMenuButton() override + { + if (m_pMenuHack) + { + g_signal_handler_disconnect(m_pMenuButton, m_nSignalId); + gtk_menu_button_set_popover(m_pMenuButton, nullptr); + gtk_widget_destroy(GTK_WIDGET(m_pMenuHack)); + } + } +}; + +class GtkInstanceMenu : public MenuHelper, public virtual weld::Menu +{ +protected: + std::vector<GtkMenuItem*> m_aExtraItems; + OString m_sActivated; + MenuHelper* m_pTopLevelMenuHelper; + +private: + virtual void signal_activate(GtkMenuItem* pItem) override + { + const gchar* pStr = gtk_buildable_get_name(GTK_BUILDABLE(pItem)); + m_sActivated = OString(pStr, pStr ? strlen(pStr) : 0); + weld::Menu::signal_activate(m_sActivated); + } + + void clear_extras() + { + if (m_aExtraItems.empty()) + return; + if (m_pTopLevelMenuHelper) + { + for (auto a : m_aExtraItems) + m_pTopLevelMenuHelper->remove_from_map(a); + } + m_aExtraItems.clear(); + } + +public: + GtkInstanceMenu(GtkMenu* pMenu, bool bTakeOwnership) + : MenuHelper(pMenu, bTakeOwnership) + , m_pTopLevelMenuHelper(nullptr) + { + g_object_set_data(G_OBJECT(m_pMenu), "g-lo-GtkInstanceMenu", this); + // tdf#122527 if we're welding a submenu of a menu of a MenuButton, + // then find that MenuButton parent so that when adding items to this + // menu we can inform the MenuButton of their addition + GtkMenu* pTopLevelMenu = pMenu; + while (true) + { + GtkWidget* pAttached = gtk_menu_get_attach_widget(pTopLevelMenu); + if (!pAttached || !GTK_IS_MENU_ITEM(pAttached)) + break; + GtkWidget* pParent = gtk_widget_get_parent(pAttached); + if (!pParent || !GTK_IS_MENU(pParent)) + break; + pTopLevelMenu = GTK_MENU(pParent); + } + if (pTopLevelMenu != pMenu) + { + // maybe the toplevel is a menubutton + GtkWidget* pAttached = gtk_menu_get_attach_widget(pTopLevelMenu); + if (pAttached && GTK_IS_MENU_BUTTON(pAttached)) + { + void* pData = g_object_get_data(G_OBJECT(pAttached), "g-lo-GtkInstanceButton"); + m_pTopLevelMenuHelper = dynamic_cast<GtkInstanceMenuButton*>(static_cast<GtkInstanceButton*>(pData)); + } + // or maybe a menu + if (!m_pTopLevelMenuHelper) + { + void* pData = g_object_get_data(G_OBJECT(pTopLevelMenu), "g-lo-GtkInstanceMenu"); + m_pTopLevelMenuHelper = static_cast<GtkInstanceMenu*>(pData); + } + } + } + + virtual OString popup_at_rect(weld::Widget* pParent, const tools::Rectangle &rRect) override + { + m_sActivated.clear(); + + GtkInstanceWidget* pGtkWidget = dynamic_cast<GtkInstanceWidget*>(pParent); + assert(pGtkWidget); + + GtkWidget* pWidget = pGtkWidget->getWidget(); + gtk_menu_attach_to_widget(m_pMenu, pWidget, nullptr); + + //run in a sub main loop because we need to keep vcl PopupMenu alive to use + //it during DispatchCommand, returning now to the outer loop causes the + //launching PopupMenu to be destroyed, instead run the subloop here + //until the gtk menu is destroyed + GMainLoop* pLoop = g_main_loop_new(nullptr, true); + gulong nSignalId = g_signal_connect_swapped(G_OBJECT(m_pMenu), "deactivate", G_CALLBACK(g_main_loop_quit), pLoop); + +#if GTK_CHECK_VERSION(3,22,0) + if (gtk_check_version(3, 22, 0) == nullptr) + { + GdkRectangle aRect{static_cast<int>(rRect.Left()), static_cast<int>(rRect.Top()), + static_cast<int>(rRect.GetWidth()), static_cast<int>(rRect.GetHeight())}; + if (SwapForRTL(pWidget)) + aRect.x = gtk_widget_get_allocated_width(pWidget) - aRect.width - 1 - aRect.x; + + // Send a keyboard event through gtk_main_do_event to toggle any active tooltip offs + // before trying to launch the menu + // https://gitlab.gnome.org/GNOME/gtk/issues/1785 + GdkEvent *event = GtkSalFrame::makeFakeKeyPress(pWidget); + gtk_main_do_event(event); + gdk_event_free(event); + + gtk_menu_popup_at_rect(m_pMenu, gtk_widget_get_window(pWidget), &aRect, GDK_GRAVITY_NORTH_WEST, GDK_GRAVITY_NORTH_WEST, nullptr); + } + else +#else + (void) rRect; +#endif + { + guint nButton; + guint32 nTime; + + //typically there is an event, and we can then distinguish if this was + //launched from the keyboard (gets auto-mnemoniced) or the mouse (which + //doesn't) + GdkEvent *pEvent = gtk_get_current_event(); + if (pEvent) + { + gdk_event_get_button(pEvent, &nButton); + nTime = gdk_event_get_time(pEvent); + } + else + { + nButton = 0; + nTime = GtkSalFrame::GetLastInputEventTime(); + } + + gtk_menu_popup(m_pMenu, nullptr, nullptr, nullptr, nullptr, nButton, nTime); + } + + if (g_main_loop_is_running(pLoop)) + { + gdk_threads_leave(); + g_main_loop_run(pLoop); + gdk_threads_enter(); + } + g_main_loop_unref(pLoop); + g_signal_handler_disconnect(m_pMenu, nSignalId); + gtk_menu_detach(m_pMenu); + + return m_sActivated; + } + + virtual void set_sensitive(const OString& rIdent, bool bSensitive) override + { + set_item_sensitive(rIdent, bSensitive); + } + + virtual void set_active(const OString& rIdent, bool bActive) override + { + set_item_active(rIdent, bActive); + } + + virtual bool get_active(const OString& rIdent) const override + { + return get_item_active(rIdent); + } + + virtual void set_visible(const OString& rIdent, bool bShow) override + { + set_item_visible(rIdent, bShow); + } + + virtual void set_label(const OString& rIdent, const OUString& rLabel) override + { + set_item_label(rIdent, rLabel); + } + + virtual OUString get_label(const OString& rIdent) const override + { + return get_item_label(rIdent); + } + + virtual void insert_separator(int pos, const OUString& rId) override + { + MenuHelper::insert_separator(pos, rId); + } + + virtual void clear() override + { + clear_extras(); + clear_items(); + } + + virtual void insert(int pos, const OUString& rId, const OUString& rStr, + const OUString* pIconName, VirtualDevice* pImageSurface, + TriState eCheckRadioFalse) override + { + GtkWidget* pImage = nullptr; + if (pIconName) + { + if (GdkPixbuf* pixbuf = load_icon_by_name(*pIconName)) + { + pImage = gtk_image_new_from_pixbuf(pixbuf); + g_object_unref(pixbuf); + } + } + else if (pImageSurface) + { + pImage = image_new_from_virtual_device(*pImageSurface); + } + + GtkWidget *pItem; + if (pImage) + { + GtkWidget *pBox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6); + GtkWidget *pLabel = gtk_label_new(MapToGtkAccelerator(rStr).getStr()); + pItem = eCheckRadioFalse != TRISTATE_INDET ? gtk_check_menu_item_new() : gtk_menu_item_new(); + gtk_container_add(GTK_CONTAINER(pBox), pImage); + gtk_container_add(GTK_CONTAINER(pBox), pLabel); + gtk_container_add(GTK_CONTAINER(pItem), pBox); + gtk_widget_show_all(pItem); + } + else + { + pItem = eCheckRadioFalse != TRISTATE_INDET ? gtk_check_menu_item_new_with_mnemonic(MapToGtkAccelerator(rStr).getStr()) + : gtk_menu_item_new_with_mnemonic(MapToGtkAccelerator(rStr).getStr()); + } + + if (eCheckRadioFalse == TRISTATE_FALSE) + gtk_check_menu_item_set_draw_as_radio(GTK_CHECK_MENU_ITEM(pItem), true); + + gtk_buildable_set_name(GTK_BUILDABLE(pItem), OUStringToOString(rId, RTL_TEXTENCODING_UTF8).getStr()); + gtk_menu_shell_append(GTK_MENU_SHELL(m_pMenu), pItem); + gtk_widget_show(pItem); + GtkMenuItem* pMenuItem = GTK_MENU_ITEM(pItem); + m_aExtraItems.push_back(pMenuItem); + add_to_map(pMenuItem); + if (m_pTopLevelMenuHelper) + m_pTopLevelMenuHelper->add_to_map(pMenuItem); + if (pos != -1) + gtk_menu_reorder_child(m_pMenu, pItem, pos); + } + + virtual int n_children() const override + { + GList* pChildren = gtk_container_get_children(GTK_CONTAINER(m_pMenu)); + int nLen = g_list_length(pChildren); + g_list_free(pChildren); + return nLen; + } + + void remove(const OString& rIdent) override + { + if (!m_aExtraItems.empty()) + { + GtkMenuItem* pMenuItem = m_aMap[rIdent]; + auto iter = std::find(m_aExtraItems.begin(), m_aExtraItems.end(), pMenuItem); + if (iter != m_aExtraItems.end()) + { + m_pTopLevelMenuHelper->remove_from_map(pMenuItem); + m_aExtraItems.erase(iter); + } + } + MenuHelper::remove_item(rIdent); + } + + virtual ~GtkInstanceMenu() override + { + clear_extras(); + g_object_steal_data(G_OBJECT(m_pMenu), "g-lo-GtkInstanceMenu"); + } +}; + + vcl::ImageType GtkToVcl(GtkIconSize eSize) + { + vcl::ImageType eRet; + switch (eSize) + { + case GTK_ICON_SIZE_MENU: + case GTK_ICON_SIZE_SMALL_TOOLBAR: + case GTK_ICON_SIZE_BUTTON: + eRet = vcl::ImageType::Size16; + break; + case GTK_ICON_SIZE_LARGE_TOOLBAR: + eRet = vcl::ImageType::Size26; + break; + case GTK_ICON_SIZE_DND: + case GTK_ICON_SIZE_DIALOG: + eRet = vcl::ImageType::Size32; + break; + default: + case GTK_ICON_SIZE_INVALID: + eRet = vcl::ImageType::Small; + break; + } + return eRet; + } + + GtkIconSize VclToGtk(vcl::ImageType eSize) + { + GtkIconSize eRet; + switch (eSize) + { + case vcl::ImageType::Size16: + eRet = GTK_ICON_SIZE_SMALL_TOOLBAR; + break; + case vcl::ImageType::Size26: + eRet = GTK_ICON_SIZE_LARGE_TOOLBAR; + break; + case vcl::ImageType::Size32: + eRet = GTK_ICON_SIZE_DIALOG; + break; + default: + O3TL_UNREACHABLE; + } + return eRet; + } +} + +void GtkInstanceMenuButton::set_menu(weld::Menu* pMenu) +{ + GtkInstanceMenu* pPopoverWidget = dynamic_cast<GtkInstanceMenu*>(pMenu); + m_pPopover = nullptr; + GtkWidget* pMenuWidget = GTK_WIDGET(pPopoverWidget ? pPopoverWidget->getMenu() : nullptr); + gtk_menu_button_set_popup(m_pMenuButton, pMenuWidget); +} + +namespace { + +class GtkInstanceToolbar : public GtkInstanceWidget, public virtual weld::Toolbar +{ +private: + GtkToolbar* m_pToolbar; + GtkCssProvider *m_pMenuButtonProvider; + + std::map<OString, GtkToolItem*> m_aMap; + std::map<OString, std::unique_ptr<GtkInstanceMenuButton>> m_aMenuButtonMap; + + // at the time of writing there is no gtk_menu_tool_button_set_popover available + // though there will be in the future + // https://gitlab.gnome.org/GNOME/gtk/commit/03e30431a8af9a947a0c4ccab545f24da16bfe17?w=1 + static void find_menu_button(GtkWidget *pWidget, gpointer user_data) + { + if (g_strcmp0(gtk_widget_get_name(pWidget), "GtkMenuButton") == 0) + { + GtkWidget **ppToggleButton = static_cast<GtkWidget**>(user_data); + *ppToggleButton = pWidget; + } + else if (GTK_IS_CONTAINER(pWidget)) + gtk_container_forall(GTK_CONTAINER(pWidget), find_menu_button, user_data); + } + + static void find_menupeer_button(GtkWidget *pWidget, gpointer user_data) + { + if (g_strcmp0(gtk_widget_get_name(pWidget), "GtkButton") == 0) + { + GtkWidget **ppButton = static_cast<GtkWidget**>(user_data); + *ppButton = pWidget; + } + else if (GTK_IS_CONTAINER(pWidget)) + gtk_container_forall(GTK_CONTAINER(pWidget), find_menupeer_button, user_data); + } + + static void collect(GtkWidget* pItem, gpointer widget) + { + if (GTK_IS_TOOL_ITEM(pItem)) + { + GtkToolItem* pToolItem = GTK_TOOL_ITEM(pItem); + GtkInstanceToolbar* pThis = static_cast<GtkInstanceToolbar*>(widget); + + GtkMenuButton* pMenuButton = nullptr; + if (GTK_IS_MENU_TOOL_BUTTON(pItem)) + find_menu_button(pItem, &pMenuButton); + + pThis->add_to_map(pToolItem, pMenuButton); + } + } + + void add_to_map(GtkToolItem* pToolItem, GtkMenuButton* pMenuButton) + { + const gchar* pStr = gtk_buildable_get_name(GTK_BUILDABLE(pToolItem)); + OString id(pStr, pStr ? strlen(pStr) : 0); + m_aMap[id] = pToolItem; + if (pMenuButton) + { + m_aMenuButtonMap[id] = std::make_unique<GtkInstanceMenuButton>(pMenuButton, GTK_WIDGET(pToolItem), m_pBuilder, false); + // so that, e.g. with focus initially in writer main document then + // after clicking the heading menu in the writer navigator focus is + // left in the main document and not in the toolbar + gtk_button_set_focus_on_click(GTK_BUTTON(pMenuButton), false); + g_signal_connect(pMenuButton, "toggled", G_CALLBACK(signalItemToggled), this); + + if (pMenuButton) + { + // by default the GtkMenuButton down arrow button is as wide as + // a normal button and LibreOffice's original ones are very + // narrow, that assumption is fairly baked into the toolbar and + // sidebar designs, try and minimize the width of the dropdown + // zone. + GtkStyleContext *pButtonContext = gtk_widget_get_style_context(GTK_WIDGET(pMenuButton)); + + if (!m_pMenuButtonProvider) + { + m_pMenuButtonProvider = gtk_css_provider_new(); + static const gchar data[] = "* { " + "padding: 0;" + "margin-left: 0px;" + "margin-right: 0px;" + "min-width: 4px;" + "}"; + const gchar olddata[] = "* { " + "padding: 0;" + "margin-left: 0px;" + "margin-right: 0px;" + "}"; + gtk_css_provider_load_from_data(m_pMenuButtonProvider, gtk_check_version(3, 20, 0) == nullptr ? data : olddata, -1, nullptr); + } + + gtk_style_context_add_provider(pButtonContext, + GTK_STYLE_PROVIDER(m_pMenuButtonProvider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + gtk_style_context_add_class(pButtonContext, "small-button"); + } + + } + if (!GTK_IS_TOOL_BUTTON(pToolItem)) + return; + g_signal_connect(pToolItem, "clicked", G_CALLBACK(signalItemClicked), this); + } + + static void signalItemClicked(GtkToolButton* pItem, gpointer widget) + { + GtkInstanceToolbar* pThis = static_cast<GtkInstanceToolbar*>(widget); + SolarMutexGuard aGuard; + pThis->signal_item_clicked(pItem); + } + + void signal_item_clicked(GtkToolButton* pItem) + { + const gchar* pStr = gtk_buildable_get_name(GTK_BUILDABLE(pItem)); + signal_clicked(OString(pStr, pStr ? strlen(pStr) : 0)); + } + + static void signalItemToggled(GtkToggleButton* pItem, gpointer widget) + { + GtkInstanceToolbar* pThis = static_cast<GtkInstanceToolbar*>(widget); + SolarMutexGuard aGuard; + pThis->signal_item_toggled(pItem); + } + + void signal_item_toggled(GtkToggleButton* pItem) + { + for (auto& a : m_aMenuButtonMap) + { + if (a.second->getWidget() == GTK_WIDGET(pItem)) + { + signal_toggle_menu(a.first); + break; + } + } + } + + static void set_item_image(GtkToolButton* pItem, const css::uno::Reference<css::graphic::XGraphic>& rIcon) + { + GtkWidget* pImage = nullptr; + + if (GdkPixbuf* pixbuf = getPixbuf(rIcon)) + { + pImage = gtk_image_new_from_pixbuf(pixbuf); + g_object_unref(pixbuf); + gtk_widget_show(pImage); + } + + gtk_tool_button_set_icon_widget(pItem, pImage); + } + + void set_item_image(GtkToolButton* pItem, VirtualDevice* pDevice) + { + GtkWidget* pImage = nullptr; + + if (pDevice) + { + pImage = image_new_from_virtual_device(*pDevice); + gtk_widget_show(pImage); + } + + gtk_tool_button_set_icon_widget(pItem, pImage); + gtk_widget_queue_draw(GTK_WIDGET(m_pToolbar)); + } + +public: + GtkInstanceToolbar(GtkToolbar* pToolbar, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : GtkInstanceWidget(GTK_WIDGET(pToolbar), pBuilder, bTakeOwnership) + , m_pToolbar(pToolbar) + , m_pMenuButtonProvider(nullptr) + { + gtk_container_foreach(GTK_CONTAINER(pToolbar), collect, this); + } + + void disable_item_notify_events() + { + for (auto& a : m_aMap) + { + g_signal_handlers_block_by_func(a.second, reinterpret_cast<void*>(signalItemClicked), this); + } + } + + void enable_item_notify_events() + { + for (auto& a : m_aMap) + { + g_signal_handlers_unblock_by_func(a.second, reinterpret_cast<void*>(signalItemClicked), this); + } + } + + virtual void set_item_sensitive(const OString& rIdent, bool bSensitive) override + { + disable_item_notify_events(); + gtk_widget_set_sensitive(GTK_WIDGET(m_aMap[rIdent]), bSensitive); + enable_item_notify_events(); + } + + virtual bool get_item_sensitive(const OString& rIdent) const override + { + return gtk_widget_get_sensitive(GTK_WIDGET(m_aMap.find(rIdent)->second)); + } + + virtual void set_item_visible(const OString& rIdent, bool bVisible) override + { + disable_item_notify_events(); + gtk_widget_set_visible(GTK_WIDGET(m_aMap[rIdent]), bVisible); + enable_item_notify_events(); + } + + virtual void set_item_help_id(const OString& rIdent, const OString& rHelpId) override + { + ::set_help_id(GTK_WIDGET(m_aMap[rIdent]), rHelpId); + } + + virtual bool get_item_visible(const OString& rIdent) const override + { + return gtk_widget_get_visible(GTK_WIDGET(m_aMap.find(rIdent)->second)); + } + + virtual void set_item_active(const OString& rIdent, bool bActive) override + { + disable_item_notify_events(); + + GtkToolItem* pToolButton = m_aMap.find(rIdent)->second; + + if (GTK_IS_TOGGLE_TOOL_BUTTON(pToolButton)) + gtk_toggle_tool_button_set_active(GTK_TOGGLE_TOOL_BUTTON(pToolButton), bActive); + else + { + GtkButton* pButton = nullptr; + // there is no GtkMenuToggleToolButton so abuse the CHECKED state of the GtkMenuToolButton button + // to emulate one + find_menupeer_button(GTK_WIDGET(pToolButton), &pButton); + if (pButton) + { + auto eState = gtk_widget_get_state_flags(GTK_WIDGET(pButton)) & ~GTK_STATE_FLAG_CHECKED; + if (bActive) + eState |= GTK_STATE_FLAG_CHECKED; + gtk_widget_set_state_flags(GTK_WIDGET(pButton), static_cast<GtkStateFlags>(eState), true); + } + } + + enable_item_notify_events(); + } + + virtual bool get_item_active(const OString& rIdent) const override + { + GtkToolItem* pToolButton = m_aMap.find(rIdent)->second; + + if (GTK_IS_TOGGLE_TOOL_BUTTON(pToolButton)) + return gtk_toggle_tool_button_get_active(GTK_TOGGLE_TOOL_BUTTON(pToolButton)); + else + { + GtkButton* pButton = nullptr; + // there is no GtkMenuToggleToolButton so abuse the CHECKED state of the GtkMenuToolButton button + // to emulate one + find_menupeer_button(GTK_WIDGET(pToolButton), &pButton); + if (pButton) + { + return gtk_widget_get_state_flags(GTK_WIDGET(pButton)) & GTK_STATE_FLAG_CHECKED; + } + } + + return false; + } + + virtual void set_menu_item_active(const OString& rIdent, bool bActive) override + { + disable_item_notify_events(); + + auto aFind = m_aMenuButtonMap.find(rIdent); + assert (aFind != m_aMenuButtonMap.end()); + aFind->second->set_active(bActive); + + enable_item_notify_events(); + } + + virtual bool get_menu_item_active(const OString& rIdent) const override + { + auto aFind = m_aMenuButtonMap.find(rIdent); + assert (aFind != m_aMenuButtonMap.end()); + return aFind->second->get_active(); + } + + virtual void insert_separator(int pos, const OUString& rId) override + { + GtkToolItem* pItem = gtk_separator_tool_item_new(); + gtk_buildable_set_name(GTK_BUILDABLE(pItem), OUStringToOString(rId, RTL_TEXTENCODING_UTF8).getStr()); + gtk_toolbar_insert(m_pToolbar, pItem, pos); + gtk_widget_show(GTK_WIDGET(pItem)); + } + + virtual void set_item_popover(const OString& rIdent, weld::Widget* pPopover) override + { + m_aMenuButtonMap[rIdent]->set_popover(pPopover); + } + + virtual void set_item_menu(const OString& rIdent, weld::Menu* pMenu) override + { + m_aMenuButtonMap[rIdent]->set_menu(pMenu); + } + + virtual int get_n_items() const override + { + return gtk_toolbar_get_n_items(m_pToolbar); + } + + virtual OString get_item_ident(int nIndex) const override + { + GtkToolItem* pItem = gtk_toolbar_get_nth_item(m_pToolbar, nIndex); + const gchar* pStr = gtk_buildable_get_name(GTK_BUILDABLE(pItem)); + return OString(pStr, pStr ? strlen(pStr) : 0); + } + + virtual void set_item_ident(int nIndex, const OString& rIdent) override + { + GtkToolItem* pItem = gtk_toolbar_get_nth_item(m_pToolbar, nIndex); + gtk_buildable_set_name(GTK_BUILDABLE(pItem), rIdent.getStr()); + } + + virtual void set_item_label(int nIndex, const OUString& rLabel) override + { + GtkToolItem* pItem = gtk_toolbar_get_nth_item(m_pToolbar, nIndex); + if (!GTK_IS_TOOL_BUTTON(pItem)) + return; + gtk_tool_button_set_label(GTK_TOOL_BUTTON(pItem), MapToGtkAccelerator(rLabel).getStr()); + } + + virtual void set_item_label(const OString& rIdent, const OUString& rLabel) override + { + GtkToolItem* pItem = m_aMap[rIdent]; + if (!pItem || !GTK_IS_TOOL_BUTTON(pItem)) + return; + gtk_tool_button_set_label(GTK_TOOL_BUTTON(pItem), MapToGtkAccelerator(rLabel).getStr()); + } + + OUString get_item_label(const OString& rIdent) const override + { + const gchar* pText = gtk_tool_button_get_label(GTK_TOOL_BUTTON(m_aMap.find(rIdent)->second)); + return OUString(pText, pText ? strlen(pText) : 0, RTL_TEXTENCODING_UTF8); + } + + virtual void set_item_icon_name(const OString& rIdent, const OUString& rIconName) override + { + GtkToolItem* pItem = m_aMap[rIdent]; + if (!pItem || !GTK_IS_TOOL_BUTTON(pItem)) + return; + + GtkWidget* pImage = nullptr; + + if (GdkPixbuf* pixbuf = getPixbuf(rIconName)) + { + pImage = gtk_image_new_from_pixbuf(pixbuf); + g_object_unref(pixbuf); + gtk_widget_show(pImage); + } + + gtk_tool_button_set_icon_widget(GTK_TOOL_BUTTON(pItem), pImage); + } + + virtual void set_item_image(const OString& rIdent, const css::uno::Reference<css::graphic::XGraphic>& rIcon) override + { + GtkToolItem* pItem = m_aMap[rIdent]; + if (!pItem || !GTK_IS_TOOL_BUTTON(pItem)) + return; + set_item_image(GTK_TOOL_BUTTON(pItem), rIcon); + } + + virtual void set_item_image(const OString& rIdent, VirtualDevice* pDevice) override + { + GtkToolItem* pItem = m_aMap[rIdent]; + if (!pItem || !GTK_IS_TOOL_BUTTON(pItem)) + return; + set_item_image(GTK_TOOL_BUTTON(pItem), pDevice); + } + + virtual void set_item_image(int nIndex, const css::uno::Reference<css::graphic::XGraphic>& rIcon) override + { + GtkToolItem* pItem = gtk_toolbar_get_nth_item(m_pToolbar, nIndex); + if (!GTK_IS_TOOL_BUTTON(pItem)) + return; + set_item_image(GTK_TOOL_BUTTON(pItem), rIcon); + } + + virtual void set_item_tooltip_text(int nIndex, const OUString& rTip) override + { + GtkToolItem* pItem = gtk_toolbar_get_nth_item(m_pToolbar, nIndex); + gtk_widget_set_tooltip_text(GTK_WIDGET(pItem), OUStringToOString(rTip, RTL_TEXTENCODING_UTF8).getStr()); + } + + virtual void set_item_tooltip_text(const OString& rIdent, const OUString& rTip) override + { + GtkToolItem* pItem = m_aMap[rIdent]; + gtk_widget_set_tooltip_text(GTK_WIDGET(pItem), OUStringToOString(rTip, RTL_TEXTENCODING_UTF8).getStr()); + } + + virtual OUString get_item_tooltip_text(const OString& rIdent) const override + { + GtkToolItem* pItem = m_aMap.find(rIdent)->second; + const gchar* pStr = gtk_widget_get_tooltip_text(GTK_WIDGET(pItem)); + return OUString(pStr, pStr ? strlen(pStr) : 0, RTL_TEXTENCODING_UTF8); + } + + virtual vcl::ImageType get_icon_size() const override + { + return GtkToVcl(gtk_toolbar_get_icon_size(m_pToolbar)); + } + + virtual void set_icon_size(vcl::ImageType eType) override + { + return gtk_toolbar_set_icon_size(m_pToolbar, VclToGtk(eType)); + } + + virtual sal_uInt16 get_modifier_state() const override + { + GdkKeymap* pKeymap = gdk_keymap_get_default(); + guint nState = gdk_keymap_get_modifier_state(pKeymap); + return GtkSalFrame::GetKeyModCode(nState); + } + + int get_drop_index(const Point& rPoint) const override + { + return gtk_toolbar_get_drop_index(m_pToolbar, rPoint.X(), rPoint.Y()); + } + + virtual ~GtkInstanceToolbar() override + { + for (auto& a : m_aMap) + g_signal_handlers_disconnect_by_data(a.second, this); + } +}; + +class GtkInstanceLinkButton : public GtkInstanceContainer, public virtual weld::LinkButton +{ +private: + GtkLinkButton* m_pButton; + gulong m_nSignalId; + + static bool signalActivateLink(GtkButton*, gpointer widget) + { + GtkInstanceLinkButton* pThis = static_cast<GtkInstanceLinkButton*>(widget); + SolarMutexGuard aGuard; + return pThis->signal_activate_link(); + } + +public: + GtkInstanceLinkButton(GtkLinkButton* pButton, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : GtkInstanceContainer(GTK_CONTAINER(pButton), pBuilder, bTakeOwnership) + , m_pButton(pButton) + , m_nSignalId(g_signal_connect(pButton, "activate-link", G_CALLBACK(signalActivateLink), this)) + { + } + + virtual void set_label(const OUString& rText) override + { + ::set_label(GTK_BUTTON(m_pButton), rText); + } + + virtual OUString get_label() const override + { + return ::get_label(GTK_BUTTON(m_pButton)); + } + + virtual void set_uri(const OUString& rText) override + { + gtk_link_button_set_uri(m_pButton, OUStringToOString(rText, RTL_TEXTENCODING_UTF8).getStr()); + } + + virtual OUString get_uri() const override + { + const gchar* pStr = gtk_link_button_get_uri(m_pButton); + return OUString(pStr, pStr ? strlen(pStr) : 0, RTL_TEXTENCODING_UTF8); + } + + virtual void disable_notify_events() override + { + g_signal_handler_block(m_pButton, m_nSignalId); + GtkInstanceContainer::disable_notify_events(); + } + + virtual void enable_notify_events() override + { + GtkInstanceContainer::enable_notify_events(); + g_signal_handler_unblock(m_pButton, m_nSignalId); + } + + virtual ~GtkInstanceLinkButton() override + { + g_signal_handler_disconnect(m_pButton, m_nSignalId); + } +}; + +class GtkInstanceRadioButton : public GtkInstanceToggleButton, public virtual weld::RadioButton +{ +public: + GtkInstanceRadioButton(GtkRadioButton* pButton, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : GtkInstanceToggleButton(GTK_TOGGLE_BUTTON(pButton), pBuilder, bTakeOwnership) + { + } +}; + +class GtkInstanceCheckButton : public GtkInstanceToggleButton, public virtual weld::CheckButton +{ +public: + GtkInstanceCheckButton(GtkCheckButton* pButton, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : GtkInstanceToggleButton(GTK_TOGGLE_BUTTON(pButton), pBuilder, bTakeOwnership) + { + } +}; + +class GtkInstanceScale : public GtkInstanceWidget, public virtual weld::Scale +{ +private: + GtkScale* m_pScale; + gulong m_nValueChangedSignalId; + + static void signalValueChanged(GtkScale*, gpointer widget) + { + GtkInstanceScale* pThis = static_cast<GtkInstanceScale*>(widget); + SolarMutexGuard aGuard; + pThis->signal_value_changed(); + } + +public: + GtkInstanceScale(GtkScale* pScale, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : GtkInstanceWidget(GTK_WIDGET(pScale), pBuilder, bTakeOwnership) + , m_pScale(pScale) + , m_nValueChangedSignalId(g_signal_connect(m_pScale, "value-changed", G_CALLBACK(signalValueChanged), this)) + { + } + + virtual void disable_notify_events() override + { + g_signal_handler_block(m_pScale, m_nValueChangedSignalId); + GtkInstanceWidget::disable_notify_events(); + } + + virtual void enable_notify_events() override + { + GtkInstanceWidget::enable_notify_events(); + g_signal_handler_unblock(m_pScale, m_nValueChangedSignalId); + } + + virtual void set_value(int value) override + { + disable_notify_events(); + gtk_range_set_value(GTK_RANGE(m_pScale), value); + enable_notify_events(); + } + + virtual void set_range(int min, int max) override + { + disable_notify_events(); + gtk_range_set_range(GTK_RANGE(m_pScale), min, max); + enable_notify_events(); + } + + virtual void set_increments(int step, int page) override + { + disable_notify_events(); + gtk_range_set_increments(GTK_RANGE(m_pScale), step, page); + enable_notify_events(); + } + + virtual void get_increments(int& step, int& page) const override + { + GtkAdjustment* pAdjustment = gtk_range_get_adjustment(GTK_RANGE(m_pScale)); + step = gtk_adjustment_get_step_increment(pAdjustment); + page = gtk_adjustment_get_page_increment(pAdjustment); + } + + virtual int get_value() const override + { + return gtk_range_get_value(GTK_RANGE(m_pScale)); + } + + virtual ~GtkInstanceScale() override + { + g_signal_handler_disconnect(m_pScale, m_nValueChangedSignalId); + } +}; + +class GtkInstanceProgressBar : public GtkInstanceWidget, public virtual weld::ProgressBar +{ +private: + GtkProgressBar* m_pProgressBar; + +public: + GtkInstanceProgressBar(GtkProgressBar* pProgressBar, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : GtkInstanceWidget(GTK_WIDGET(pProgressBar), pBuilder, bTakeOwnership) + , m_pProgressBar(pProgressBar) + { + } + + virtual void set_percentage(int value) override + { + gtk_progress_bar_set_fraction(m_pProgressBar, value / 100.0); + } + + virtual OUString get_text() const override + { + const gchar* pText = gtk_progress_bar_get_text(m_pProgressBar); + OUString sRet(pText, pText ? strlen(pText) : 0, RTL_TEXTENCODING_UTF8); + return sRet; + } + + virtual void set_text(const OUString& rText) override + { + gtk_progress_bar_set_text(m_pProgressBar, OUStringToOString(rText, RTL_TEXTENCODING_UTF8).getStr()); + } +}; + +class GtkInstanceSpinner : public GtkInstanceWidget, public virtual weld::Spinner +{ +private: + GtkSpinner* m_pSpinner; + +public: + GtkInstanceSpinner(GtkSpinner* pSpinner, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : GtkInstanceWidget(GTK_WIDGET(pSpinner), pBuilder, bTakeOwnership) + , m_pSpinner(pSpinner) + { + } + + virtual void start() override + { + gtk_spinner_start(m_pSpinner); + } + + virtual void stop() override + { + gtk_spinner_stop(m_pSpinner); + } +}; + +class GtkInstanceImage : public GtkInstanceWidget, public virtual weld::Image +{ +private: + GtkImage* m_pImage; + +public: + GtkInstanceImage(GtkImage* pImage, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : GtkInstanceWidget(GTK_WIDGET(pImage), pBuilder, bTakeOwnership) + , m_pImage(pImage) + { + } + + virtual void set_from_icon_name(const OUString& rIconName) override + { + GdkPixbuf* pixbuf = load_icon_by_name(rIconName); + if (!pixbuf) + return; + gtk_image_set_from_pixbuf(m_pImage, pixbuf); + g_object_unref(pixbuf); + } + + virtual void set_image(VirtualDevice* pDevice) override + { + if (gtk_check_version(3, 20, 0) == nullptr) + { + if (pDevice) + gtk_image_set_from_surface(m_pImage, get_underlying_cairo_surface(*pDevice)); + else + gtk_image_set_from_surface(m_pImage, nullptr); + return; + } + + GdkPixbuf* pixbuf = pDevice ? getPixbuf(*pDevice) : nullptr; + gtk_image_set_from_pixbuf(m_pImage, pixbuf); + if (pixbuf) + g_object_unref(pixbuf); + } + + virtual void set_image(const css::uno::Reference<css::graphic::XGraphic>& rImage) override + { + GdkPixbuf* pixbuf = getPixbuf(rImage); + gtk_image_set_from_pixbuf(m_pImage, pixbuf); + if (pixbuf) + g_object_unref(pixbuf); + } +}; + +class GtkInstanceCalendar : public GtkInstanceWidget, public virtual weld::Calendar +{ +private: + GtkCalendar* m_pCalendar; + gulong m_nDaySelectedSignalId; + gulong m_nDaySelectedDoubleClickSignalId; + gulong m_nKeyPressEventSignalId; + + static void signalDaySelected(GtkCalendar*, gpointer widget) + { + GtkInstanceCalendar* pThis = static_cast<GtkInstanceCalendar*>(widget); + pThis->signal_selected(); + } + + static void signalDaySelectedDoubleClick(GtkCalendar*, gpointer widget) + { + GtkInstanceCalendar* pThis = static_cast<GtkInstanceCalendar*>(widget); + pThis->signal_activated(); + } + + bool signal_key_press(GdkEventKey* pEvent) + { + if (pEvent->keyval == GDK_KEY_Return || pEvent->keyval == GDK_KEY_KP_Enter) + { + signal_activated(); + return true; + } + return false; + } + + static gboolean signalKeyPress(GtkWidget*, GdkEventKey* pEvent, gpointer widget) + { + GtkInstanceCalendar* pThis = static_cast<GtkInstanceCalendar*>(widget); + return pThis->signal_key_press(pEvent); + } + +public: + GtkInstanceCalendar(GtkCalendar* pCalendar, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : GtkInstanceWidget(GTK_WIDGET(pCalendar), pBuilder, bTakeOwnership) + , m_pCalendar(pCalendar) + , m_nDaySelectedSignalId(g_signal_connect(pCalendar, "day-selected", G_CALLBACK(signalDaySelected), this)) + , m_nDaySelectedDoubleClickSignalId(g_signal_connect(pCalendar, "day-selected-double-click", G_CALLBACK(signalDaySelectedDoubleClick), this)) + , m_nKeyPressEventSignalId(g_signal_connect(pCalendar, "key-press-event", G_CALLBACK(signalKeyPress), this)) + { + } + + virtual void set_date(const Date& rDate) override + { + if (!rDate.IsValidAndGregorian()) + return; + + disable_notify_events(); + gtk_calendar_select_month(m_pCalendar, rDate.GetMonth() - 1, rDate.GetYear()); + gtk_calendar_select_day(m_pCalendar, rDate.GetDay()); + enable_notify_events(); + } + + virtual Date get_date() const override + { + guint year, month, day; + gtk_calendar_get_date(m_pCalendar, &year, &month, &day); + return Date(day, month + 1, year); + } + + virtual void disable_notify_events() override + { + g_signal_handler_block(m_pCalendar, m_nDaySelectedDoubleClickSignalId); + g_signal_handler_block(m_pCalendar, m_nDaySelectedSignalId); + GtkInstanceWidget::disable_notify_events(); + } + + virtual void enable_notify_events() override + { + GtkInstanceWidget::enable_notify_events(); + g_signal_handler_unblock(m_pCalendar, m_nDaySelectedSignalId); + g_signal_handler_unblock(m_pCalendar, m_nDaySelectedDoubleClickSignalId); + } + + virtual ~GtkInstanceCalendar() override + { + g_signal_handler_disconnect(m_pCalendar, m_nKeyPressEventSignalId); + g_signal_handler_disconnect(m_pCalendar, m_nDaySelectedDoubleClickSignalId); + g_signal_handler_disconnect(m_pCalendar, m_nDaySelectedSignalId); + } +}; + + PangoAttrList* create_attr_list(const vcl::Font& rFont) + { + PangoAttrList* pAttrList = pango_attr_list_new(); + pango_attr_list_insert(pAttrList, pango_attr_family_new(OUStringToOString(rFont.GetFamilyName(), RTL_TEXTENCODING_UTF8).getStr())); + pango_attr_list_insert(pAttrList, pango_attr_size_new(rFont.GetFontSize().Height() * PANGO_SCALE)); + switch (rFont.GetItalic()) + { + case ITALIC_NONE: + pango_attr_list_insert(pAttrList, pango_attr_style_new(PANGO_STYLE_NORMAL)); + break; + case ITALIC_NORMAL: + pango_attr_list_insert(pAttrList, pango_attr_style_new(PANGO_STYLE_ITALIC)); + break; + case ITALIC_OBLIQUE: + pango_attr_list_insert(pAttrList, pango_attr_style_new(PANGO_STYLE_OBLIQUE)); + break; + default: + break; + } + switch (rFont.GetWeight()) + { + case WEIGHT_ULTRALIGHT: + pango_attr_list_insert(pAttrList, pango_attr_weight_new(PANGO_WEIGHT_ULTRALIGHT)); + break; + case WEIGHT_LIGHT: + pango_attr_list_insert(pAttrList, pango_attr_weight_new(PANGO_WEIGHT_LIGHT)); + break; + case WEIGHT_NORMAL: + pango_attr_list_insert(pAttrList, pango_attr_weight_new(PANGO_WEIGHT_NORMAL)); + break; + case WEIGHT_BOLD: + pango_attr_list_insert(pAttrList, pango_attr_weight_new(PANGO_WEIGHT_BOLD)); + break; + case WEIGHT_ULTRABOLD: + pango_attr_list_insert(pAttrList, pango_attr_weight_new(PANGO_WEIGHT_ULTRABOLD)); + break; + default: + break; + } + switch (rFont.GetWidthType()) + { + case WIDTH_ULTRA_CONDENSED: + pango_attr_list_insert(pAttrList, pango_attr_stretch_new(PANGO_STRETCH_ULTRA_CONDENSED)); + break; + case WIDTH_EXTRA_CONDENSED: + pango_attr_list_insert(pAttrList, pango_attr_stretch_new(PANGO_STRETCH_EXTRA_CONDENSED)); + break; + case WIDTH_CONDENSED: + pango_attr_list_insert(pAttrList, pango_attr_stretch_new(PANGO_STRETCH_CONDENSED)); + break; + case WIDTH_SEMI_CONDENSED: + pango_attr_list_insert(pAttrList, pango_attr_stretch_new(PANGO_STRETCH_SEMI_CONDENSED)); + break; + case WIDTH_NORMAL: + pango_attr_list_insert(pAttrList, pango_attr_stretch_new(PANGO_STRETCH_NORMAL)); + break; + case WIDTH_SEMI_EXPANDED: + pango_attr_list_insert(pAttrList, pango_attr_stretch_new(PANGO_STRETCH_SEMI_EXPANDED)); + break; + case WIDTH_EXPANDED: + pango_attr_list_insert(pAttrList, pango_attr_stretch_new(PANGO_STRETCH_EXPANDED)); + break; + case WIDTH_EXTRA_EXPANDED: + pango_attr_list_insert(pAttrList, pango_attr_stretch_new(PANGO_STRETCH_EXTRA_EXPANDED)); + break; + case WIDTH_ULTRA_EXPANDED: + pango_attr_list_insert(pAttrList, pango_attr_stretch_new(PANGO_STRETCH_ULTRA_EXPANDED)); + break; + default: + break; + } + return pAttrList; + } +} + +namespace +{ + void set_entry_message_type(GtkEntry* pEntry, weld::EntryMessageType eType) + { + if (eType == weld::EntryMessageType::Error) + gtk_entry_set_icon_from_icon_name(pEntry, GTK_ENTRY_ICON_SECONDARY, "dialog-error"); + else if (eType == weld::EntryMessageType::Warning) + gtk_entry_set_icon_from_icon_name(pEntry, GTK_ENTRY_ICON_SECONDARY, "dialog-warning"); + else + gtk_entry_set_icon_from_icon_name(pEntry, GTK_ENTRY_ICON_SECONDARY, nullptr); + } + +class GtkInstanceEntry : public GtkInstanceWidget, public virtual weld::Entry +{ +private: + GtkEntry* m_pEntry; + std::unique_ptr<vcl::Font> m_xFont; + gulong m_nChangedSignalId; + gulong m_nInsertTextSignalId; + gulong m_nCursorPosSignalId; + gulong m_nSelectionPosSignalId; + gulong m_nActivateSignalId; + + static void signalChanged(GtkEntry*, gpointer widget) + { + GtkInstanceEntry* pThis = static_cast<GtkInstanceEntry*>(widget); + SolarMutexGuard aGuard; + pThis->signal_changed(); + } + + static void signalInsertText(GtkEntry* pEntry, const gchar* pNewText, gint nNewTextLength, + gint* position, gpointer widget) + { + GtkInstanceEntry* pThis = static_cast<GtkInstanceEntry*>(widget); + SolarMutexGuard aGuard; + pThis->signal_insert_text(pEntry, pNewText, nNewTextLength, position); + } + + void signal_insert_text(GtkEntry* pEntry, const gchar* pNewText, gint nNewTextLength, gint* position) + { + if (!m_aInsertTextHdl.IsSet()) + return; + OUString sText(pNewText, nNewTextLength, RTL_TEXTENCODING_UTF8); + const bool bContinue = m_aInsertTextHdl.Call(sText); + if (bContinue && !sText.isEmpty()) + { + OString sFinalText(OUStringToOString(sText, RTL_TEXTENCODING_UTF8)); + g_signal_handlers_block_by_func(pEntry, reinterpret_cast<gpointer>(signalInsertText), this); + gtk_editable_insert_text(GTK_EDITABLE(pEntry), sFinalText.getStr(), sFinalText.getLength(), position); + g_signal_handlers_unblock_by_func(pEntry, reinterpret_cast<gpointer>(signalInsertText), this); + } + g_signal_stop_emission_by_name(pEntry, "insert-text"); + } + + static void signalCursorPosition(GtkEntry*, GParamSpec*, gpointer widget) + { + GtkInstanceEntry* pThis = static_cast<GtkInstanceEntry*>(widget); + pThis->signal_cursor_position(); + } + + static void signalActivate(GtkEntry*, gpointer widget) + { + GtkInstanceEntry* pThis = static_cast<GtkInstanceEntry*>(widget); + pThis->signal_activate(); + } + +protected: + + virtual void signal_activate() + { + if (m_aActivateHdl.IsSet()) + { + SolarMutexGuard aGuard; + if (m_aActivateHdl.Call(*this)) + g_signal_stop_emission_by_name(m_pEntry, "activate"); + } + } + +public: + GtkInstanceEntry(GtkEntry* pEntry, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : GtkInstanceWidget(GTK_WIDGET(pEntry), pBuilder, bTakeOwnership) + , m_pEntry(pEntry) + , m_nChangedSignalId(g_signal_connect(pEntry, "changed", G_CALLBACK(signalChanged), this)) + , m_nInsertTextSignalId(g_signal_connect(pEntry, "insert-text", G_CALLBACK(signalInsertText), this)) + , m_nCursorPosSignalId(g_signal_connect(pEntry, "notify::cursor-position", G_CALLBACK(signalCursorPosition), this)) + , m_nSelectionPosSignalId(g_signal_connect(pEntry, "notify::selection-bound", G_CALLBACK(signalCursorPosition), this)) + , m_nActivateSignalId(g_signal_connect(pEntry, "activate", G_CALLBACK(signalActivate), this)) + { + } + + virtual void set_text(const OUString& rText) override + { + disable_notify_events(); + gtk_entry_set_text(m_pEntry, OUStringToOString(rText, RTL_TEXTENCODING_UTF8).getStr()); + enable_notify_events(); + } + + virtual OUString get_text() const override + { + const gchar* pText = gtk_entry_get_text(m_pEntry); + OUString sRet(pText, pText ? strlen(pText) : 0, RTL_TEXTENCODING_UTF8); + return sRet; + } + + virtual void set_width_chars(int nChars) override + { + disable_notify_events(); + gtk_entry_set_width_chars(m_pEntry, nChars); + gtk_entry_set_max_width_chars(m_pEntry, nChars); + enable_notify_events(); + } + + virtual int get_width_chars() const override + { + return gtk_entry_get_width_chars(m_pEntry); + } + + virtual void set_max_length(int nChars) override + { + disable_notify_events(); + gtk_entry_set_max_length(m_pEntry, nChars); + enable_notify_events(); + } + + virtual void select_region(int nStartPos, int nEndPos) override + { + disable_notify_events(); + gtk_editable_select_region(GTK_EDITABLE(m_pEntry), nStartPos, nEndPos); + enable_notify_events(); + } + + bool get_selection_bounds(int& rStartPos, int& rEndPos) override + { + return gtk_editable_get_selection_bounds(GTK_EDITABLE(m_pEntry), &rStartPos, &rEndPos); + } + + virtual void replace_selection(const OUString& rText) override + { + disable_notify_events(); + gtk_editable_delete_selection(GTK_EDITABLE(m_pEntry)); + OString sText(OUStringToOString(rText, RTL_TEXTENCODING_UTF8)); + gint position = gtk_editable_get_position(GTK_EDITABLE(m_pEntry)); + gtk_editable_insert_text(GTK_EDITABLE(m_pEntry), sText.getStr(), sText.getLength(), + &position); + enable_notify_events(); + } + + virtual void set_position(int nCursorPos) override + { + disable_notify_events(); + gtk_editable_set_position(GTK_EDITABLE(m_pEntry), nCursorPos); + enable_notify_events(); + } + + virtual int get_position() const override + { + return gtk_editable_get_position(GTK_EDITABLE(m_pEntry)); + } + + virtual void set_editable(bool bEditable) override + { + gtk_editable_set_editable(GTK_EDITABLE(m_pEntry), bEditable); + } + + virtual bool get_editable() const override + { + return gtk_editable_get_editable(GTK_EDITABLE(m_pEntry)); + } + + virtual void set_message_type(weld::EntryMessageType eType) override + { + ::set_entry_message_type(m_pEntry, eType); + } + + virtual void disable_notify_events() override + { + g_signal_handler_block(m_pEntry, m_nActivateSignalId); + g_signal_handler_block(m_pEntry, m_nSelectionPosSignalId); + g_signal_handler_block(m_pEntry, m_nCursorPosSignalId); + g_signal_handler_block(m_pEntry, m_nInsertTextSignalId); + g_signal_handler_block(m_pEntry, m_nChangedSignalId); + GtkInstanceWidget::disable_notify_events(); + } + + virtual void enable_notify_events() override + { + GtkInstanceWidget::enable_notify_events(); + g_signal_handler_unblock(m_pEntry, m_nChangedSignalId); + g_signal_handler_unblock(m_pEntry, m_nInsertTextSignalId); + g_signal_handler_unblock(m_pEntry, m_nCursorPosSignalId); + g_signal_handler_unblock(m_pEntry, m_nSelectionPosSignalId); + g_signal_handler_unblock(m_pEntry, m_nActivateSignalId); + } + + virtual void set_font(const vcl::Font& rFont) override + { + m_xFont.reset(new vcl::Font(rFont)); + PangoAttrList* pAttrList = create_attr_list(rFont); + gtk_entry_set_attributes(m_pEntry, pAttrList); + pango_attr_list_unref(pAttrList); + } + + virtual vcl::Font get_font() override + { + if (m_xFont) + return *m_xFont; + return GtkInstanceWidget::get_font(); + } + + void fire_signal_changed() + { + signal_changed(); + } + + virtual void cut_clipboard() override + { + gtk_editable_cut_clipboard(GTK_EDITABLE(m_pEntry)); + } + + virtual void copy_clipboard() override + { + gtk_editable_copy_clipboard(GTK_EDITABLE(m_pEntry)); + } + + virtual void paste_clipboard() override + { + gtk_editable_paste_clipboard(GTK_EDITABLE(m_pEntry)); + } + + virtual void set_placeholder_text(const OUString& rText) override + { + gtk_entry_set_placeholder_text(m_pEntry, rText.toUtf8().getStr()); + } + + virtual void grab_focus() override + { + disable_notify_events(); + gtk_entry_grab_focus_without_selecting(m_pEntry); + enable_notify_events(); + } + + virtual ~GtkInstanceEntry() override + { + g_signal_handler_disconnect(m_pEntry, m_nActivateSignalId); + g_signal_handler_disconnect(m_pEntry, m_nSelectionPosSignalId); + g_signal_handler_disconnect(m_pEntry, m_nCursorPosSignalId); + g_signal_handler_disconnect(m_pEntry, m_nInsertTextSignalId); + g_signal_handler_disconnect(m_pEntry, m_nChangedSignalId); + } +}; + + struct Search + { + OString str; + int index; + int col; + Search(const OUString& rText, int nCol) + : str(OUStringToOString(rText, RTL_TEXTENCODING_UTF8)) + , index(-1) + , col(nCol) + { + } + }; + + gboolean foreach_find(GtkTreeModel* model, GtkTreePath* path, GtkTreeIter* iter, gpointer data) + { + Search* search = static_cast<Search*>(data); + gchar *pStr = nullptr; + gtk_tree_model_get(model, iter, search->col, &pStr, -1); + bool found = strcmp(pStr, search->str.getStr()) == 0; + if (found) + { + gint depth; + gint* indices = gtk_tree_path_get_indices_with_depth(path, &depth); + search->index = indices[depth-1]; + } + g_free(pStr); + return found; + } + + void insert_row(GtkListStore* pListStore, GtkTreeIter& iter, int pos, const OUString* pId, const OUString& rText, const OUString* pIconName, const VirtualDevice* pDevice) + { + if (!pIconName && !pDevice) + { + gtk_list_store_insert_with_values(pListStore, &iter, pos, + 0, OUStringToOString(rText, RTL_TEXTENCODING_UTF8).getStr(), + 1, !pId ? nullptr : OUStringToOString(*pId, RTL_TEXTENCODING_UTF8).getStr(), + -1); + } + else + { + if (pIconName) + { + GdkPixbuf* pixbuf = getPixbuf(*pIconName); + + gtk_list_store_insert_with_values(pListStore, &iter, pos, + 0, OUStringToOString(rText, RTL_TEXTENCODING_UTF8).getStr(), + 1, !pId ? nullptr : OUStringToOString(*pId, RTL_TEXTENCODING_UTF8).getStr(), + 2, pixbuf, + -1); + + if (pixbuf) + g_object_unref(pixbuf); + } + else + { + cairo_surface_t* surface = get_underlying_cairo_surface(*pDevice); + + Size aSize(pDevice->GetOutputSizePixel()); + cairo_surface_t* target = cairo_surface_create_similar(surface, + cairo_surface_get_content(surface), + aSize.Width(), + aSize.Height()); + + cairo_t* cr = cairo_create(target); + cairo_set_source_surface(cr, surface, 0, 0); + cairo_paint(cr); + cairo_destroy(cr); + + gtk_list_store_insert_with_values(pListStore, &iter, pos, + 0, OUStringToOString(rText, RTL_TEXTENCODING_UTF8).getStr(), + 1, !pId ? nullptr : OUStringToOString(*pId, RTL_TEXTENCODING_UTF8).getStr(), + 3, target, + -1); + cairo_surface_destroy(target); + } + } + } +} + +namespace +{ + gint default_sort_func(GtkTreeModel* pModel, GtkTreeIter* a, GtkTreeIter* b, gpointer data) + { + comphelper::string::NaturalStringSorter* pSorter = static_cast<comphelper::string::NaturalStringSorter*>(data); + gchar* pName1; + gchar* pName2; + GtkTreeSortable* pSortable = GTK_TREE_SORTABLE(pModel); + gint sort_column_id(0); + gtk_tree_sortable_get_sort_column_id(pSortable, &sort_column_id, nullptr); + gtk_tree_model_get(pModel, a, sort_column_id, &pName1, -1); + gtk_tree_model_get(pModel, b, sort_column_id, &pName2, -1); + gint ret = pSorter->compare(OUString(pName1, pName1 ? strlen(pName1) : 0, RTL_TEXTENCODING_UTF8), + OUString(pName2, pName2 ? strlen(pName2) : 0, RTL_TEXTENCODING_UTF8)); + g_free(pName1); + g_free(pName2); + return ret; + } + + int starts_with(GtkTreeModel* pTreeModel, const OUString& rStr, int col, int nStartRow, bool bCaseSensitive) + { + GtkTreeIter iter; + if (!gtk_tree_model_iter_nth_child(pTreeModel, &iter, nullptr, nStartRow)) + return -1; + + const vcl::I18nHelper& rI18nHelper = Application::GetSettings().GetUILocaleI18nHelper(); + int nRet = nStartRow; + do + { + gchar* pStr; + gtk_tree_model_get(pTreeModel, &iter, col, &pStr, -1); + OUString aStr(pStr, pStr ? strlen(pStr) : 0, RTL_TEXTENCODING_UTF8); + g_free(pStr); + const bool bMatch = !bCaseSensitive ? rI18nHelper.MatchString(rStr, aStr) : aStr.startsWith(rStr); + if (bMatch) + return nRet; + ++nRet; + } while (gtk_tree_model_iter_next(pTreeModel, &iter)); + + return -1; + } + +struct GtkInstanceTreeIter : public weld::TreeIter +{ + GtkInstanceTreeIter(const GtkInstanceTreeIter* pOrig) + { + if (pOrig) + iter = pOrig->iter; + else + memset(&iter, 0, sizeof(iter)); + } + GtkInstanceTreeIter(const GtkTreeIter& rOrig) + { + memcpy(&iter, &rOrig, sizeof(iter)); + } + virtual bool equal(const TreeIter& rOther) const override + { + return memcmp(&iter, &static_cast<const GtkInstanceTreeIter&>(rOther).iter, sizeof(GtkTreeIter)) == 0; + } + GtkTreeIter iter; +}; + +class GtkInstanceTreeView; + +} + +static GtkInstanceTreeView* g_DragSource; + +namespace { + +struct CompareGtkTreePath +{ + bool operator()(const GtkTreePath* lhs, const GtkTreePath* rhs) const + { + return gtk_tree_path_compare(lhs, rhs) < 0; + } +}; + +int get_height_row(GtkTreeView* pTreeView, GList* pColumns) +{ + gint nMaxRowHeight = 0; + for (GList* pEntry = g_list_first(pColumns); pEntry; pEntry = g_list_next(pEntry)) + { + GtkTreeViewColumn* pColumn = GTK_TREE_VIEW_COLUMN(pEntry->data); + GList *pRenderers = gtk_cell_layout_get_cells(GTK_CELL_LAYOUT(pColumn)); + for (GList* pRenderer = g_list_first(pRenderers); pRenderer; pRenderer = g_list_next(pRenderer)) + { + GtkCellRenderer* pCellRenderer = GTK_CELL_RENDERER(pRenderer->data); + gint nRowHeight; + gtk_cell_renderer_get_preferred_height(pCellRenderer, GTK_WIDGET(pTreeView), nullptr, &nRowHeight); + nMaxRowHeight = std::max(nMaxRowHeight, nRowHeight); + } + g_list_free(pRenderers); + } + return nMaxRowHeight; +} + +int get_height_row_separator(GtkTreeView* pTreeView) +{ + gint nVerticalSeparator; + gtk_widget_style_get(GTK_WIDGET(pTreeView), "vertical-separator", &nVerticalSeparator, nullptr); + return nVerticalSeparator; +} + +int get_height_rows(GtkTreeView* pTreeView, GList* pColumns, int nRows) +{ + gint nMaxRowHeight = get_height_row(pTreeView, pColumns); + gint nVerticalSeparator = get_height_row_separator(pTreeView); + return (nMaxRowHeight * nRows) + (nVerticalSeparator * (nRows + 1)); +} + +int get_height_rows(int nRowHeight, int nSeparatorHeight, int nRows) +{ + return (nRowHeight * nRows) + (nSeparatorHeight * (nRows + 1)); +} + +tools::Rectangle get_row_area(GtkTreeView* pTreeView, GList* pColumns, GtkTreePath* pPath) +{ + tools::Rectangle aRet; + + GdkRectangle aRect; + for (GList* pEntry = g_list_last(pColumns); pEntry; pEntry = g_list_previous(pEntry)) + { + GtkTreeViewColumn* pColumn = GTK_TREE_VIEW_COLUMN(pEntry->data); + gtk_tree_view_get_cell_area(pTreeView, pPath, pColumn, &aRect); + aRet.Union(tools::Rectangle(aRect.x, aRect.y, aRect.x + aRect.width, aRect.y + aRect.height)); + } + + return aRet; +} + +class GtkInstanceTreeView : public GtkInstanceContainer, public virtual weld::TreeView +{ +private: + GtkTreeView* m_pTreeView; + GtkTreeStore* m_pTreeStore; + std::unique_ptr<comphelper::string::NaturalStringSorter> m_xSorter; + GList *m_pColumns; + std::vector<gulong> m_aColumnSignalIds; + // map from toggle column to toggle visibility column + std::map<int, int> m_aToggleVisMap; + // map from toggle column to tristate column + std::map<int, int> m_aToggleTriStateMap; + // map from text column to text weight column + std::map<int, int> m_aWeightMap; + // map from text column to sensitive column + std::map<int, int> m_aSensitiveMap; + // map from text column to indent column + std::map<int, int> m_aIndentMap; + // map from text column to text align column + std::map<int, int> m_aAlignMap; + // currently expanding parent that logically, but not currently physically, + // contain placeholders + o3tl::sorted_vector<GtkTreePath*, CompareGtkTreePath> m_aExpandingPlaceHolderParents; + std::vector<GtkSortType> m_aSavedSortTypes; + std::vector<int> m_aSavedSortColumns; + std::vector<int> m_aViewColToModelCol; + std::vector<int> m_aModelColToViewCol; + bool m_bWorkAroundBadDragRegion; + bool m_bInDrag; + gint m_nTextCol; + gint m_nImageCol; + gint m_nExpanderImageCol; + gint m_nIdCol; + int m_nPendingVAdjustment; + gulong m_nChangedSignalId; + gulong m_nRowActivatedSignalId; + gulong m_nTestExpandRowSignalId; + gulong m_nTestCollapseRowSignalId; + gulong m_nVAdjustmentChangedSignalId; + gulong m_nRowDeletedSignalId; + gulong m_nRowInsertedSignalId; + gulong m_nPopupMenuSignalId; + gulong m_nKeyPressSignalId; + gulong m_nQueryTooltipSignalId; + GtkAdjustment* m_pVAdjustment; + ImplSVEvent* m_pChangeEvent; + + DECL_LINK(async_signal_changed, void*, void); + + void launch_signal_changed() + { + //tdf#117991 selection change is sent before the focus change, and focus change + //is what will cause a spinbutton that currently has the focus to set its contents + //as the spin button value. So any LibreOffice callbacks on + //signal-change would happen before the spinbutton value-change occurs. + //To avoid this, send the signal-change to LibreOffice to occur after focus-change + //has been processed + if (m_pChangeEvent) + Application::RemoveUserEvent(m_pChangeEvent); + m_pChangeEvent = Application::PostUserEvent(LINK(this, GtkInstanceTreeView, async_signal_changed)); + } + + static void signalChanged(GtkTreeView*, gpointer widget) + { + GtkInstanceTreeView* pThis = static_cast<GtkInstanceTreeView*>(widget); + pThis->launch_signal_changed(); + } + + void handle_row_activated() + { + if (signal_row_activated()) + return; + GtkInstanceTreeIter aIter(nullptr); + if (!get_cursor(&aIter)) + return; + if (gtk_tree_model_iter_has_child(GTK_TREE_MODEL(m_pTreeStore), &aIter.iter)) + get_row_expanded(aIter) ? collapse_row(aIter) : expand_row(aIter); + } + + static void signalRowActivated(GtkTreeView*, GtkTreePath*, GtkTreeViewColumn*, gpointer widget) + { + GtkInstanceTreeView* pThis = static_cast<GtkInstanceTreeView*>(widget); + SolarMutexGuard aGuard; + pThis->handle_row_activated(); + } + + virtual bool signal_popup_menu(const CommandEvent& rCEvt) override + { + return m_aPopupMenuHdl.Call(rCEvt); + } + + void insert_row(GtkTreeIter& iter, const GtkTreeIter* parent, int pos, const OUString* pId, const OUString* pText, + const OUString* pIconName, const VirtualDevice* pDevice, const OUString* pExpanderName) + { + gtk_tree_store_insert_with_values(m_pTreeStore, &iter, const_cast<GtkTreeIter*>(parent), pos, + m_nTextCol, !pText ? nullptr : OUStringToOString(*pText, RTL_TEXTENCODING_UTF8).getStr(), + m_nIdCol, !pId ? nullptr : OUStringToOString(*pId, RTL_TEXTENCODING_UTF8).getStr(), + -1); + if (pIconName) + { + GdkPixbuf* pixbuf = getPixbuf(*pIconName); + gtk_tree_store_set(m_pTreeStore, &iter, m_nImageCol, pixbuf, -1); + if (pixbuf) + g_object_unref(pixbuf); + } + else if (pDevice) + { + cairo_surface_t* surface = get_underlying_cairo_surface(*pDevice); + + Size aSize(pDevice->GetOutputSizePixel()); + cairo_surface_t* target = cairo_surface_create_similar(surface, + cairo_surface_get_content(surface), + aSize.Width(), + aSize.Height()); + + cairo_t* cr = cairo_create(target); + cairo_set_source_surface(cr, surface, 0, 0); + cairo_paint(cr); + cairo_destroy(cr); + + gtk_tree_store_set(m_pTreeStore, &iter, m_nImageCol, target, -1); + cairo_surface_destroy(target); + } + + if (pExpanderName) + { + GdkPixbuf* pixbuf = getPixbuf(*pExpanderName); + gtk_tree_store_set(m_pTreeStore, &iter, m_nExpanderImageCol, pixbuf, -1); + if (pixbuf) + g_object_unref(pixbuf); + } + } + + OUString get(const GtkTreeIter& iter, int col) const + { + GtkTreeModel *pModel = GTK_TREE_MODEL(m_pTreeStore); + gchar* pStr; + gtk_tree_model_get(pModel, const_cast<GtkTreeIter*>(&iter), col, &pStr, -1); + OUString sRet(pStr, pStr ? strlen(pStr) : 0, RTL_TEXTENCODING_UTF8); + g_free(pStr); + return sRet; + } + + OUString get(int pos, int col) const + { + OUString sRet; + GtkTreeModel *pModel = GTK_TREE_MODEL(m_pTreeStore); + GtkTreeIter iter; + if (gtk_tree_model_iter_nth_child(pModel, &iter, nullptr, pos)) + sRet = get(iter, col); + return sRet; + } + + gint get_int(const GtkTreeIter& iter, int col) const + { + gint nRet(-1); + GtkTreeModel *pModel = GTK_TREE_MODEL(m_pTreeStore); + gtk_tree_model_get(pModel, const_cast<GtkTreeIter*>(&iter), col, &nRet, -1); + return nRet; + } + + gint get_int(int pos, int col) const + { + gint nRet(-1); + GtkTreeModel *pModel = GTK_TREE_MODEL(m_pTreeStore); + GtkTreeIter iter; + if (gtk_tree_model_iter_nth_child(pModel, &iter, nullptr, pos)) + nRet = get_int(iter, col); + gtk_tree_model_get(pModel, &iter, col, &nRet, -1); + return nRet; + } + + bool get_bool(const GtkTreeIter& iter, int col) const + { + gboolean bRet(false); + GtkTreeModel *pModel = GTK_TREE_MODEL(m_pTreeStore); + gtk_tree_model_get(pModel, const_cast<GtkTreeIter*>(&iter), col, &bRet, -1); + return bRet; + } + + bool get_bool(int pos, int col) const + { + bool bRet(false); + GtkTreeModel *pModel = GTK_TREE_MODEL(m_pTreeStore); + GtkTreeIter iter; + if (gtk_tree_model_iter_nth_child(pModel, &iter, nullptr, pos)) + bRet = get_bool(iter, col); + return bRet; + } + + void set_toggle(const GtkTreeIter& iter, TriState eState, int col) + { + col = get_model_col(col); + if (eState == TRISTATE_INDET) + { + gtk_tree_store_set(m_pTreeStore, const_cast<GtkTreeIter*>(&iter), + m_aToggleVisMap[col], true, // checkbuttons are invisible until toggled on or off + m_aToggleTriStateMap[col], true, // tristate on + -1); + } + else + { + gtk_tree_store_set(m_pTreeStore, const_cast<GtkTreeIter*>(&iter), + m_aToggleVisMap[col], true, // checkbuttons are invisible until toggled on or off + m_aToggleTriStateMap[col], false, // tristate off + col, eState == TRISTATE_TRUE, // set toggle state + -1); + } + } + + void set(const GtkTreeIter& iter, int col, const OUString& rText) + { + OString aStr(OUStringToOString(rText, RTL_TEXTENCODING_UTF8)); + gtk_tree_store_set(m_pTreeStore, const_cast<GtkTreeIter*>(&iter), col, aStr.getStr(), -1); + } + + void set(int pos, int col, const OUString& rText) + { + GtkTreeModel *pModel = GTK_TREE_MODEL(m_pTreeStore); + GtkTreeIter iter; + if (gtk_tree_model_iter_nth_child(pModel, &iter, nullptr, pos)) + set(iter, col, rText); + } + + void set(const GtkTreeIter& iter, int col, bool bOn) + { + gtk_tree_store_set(m_pTreeStore, const_cast<GtkTreeIter*>(&iter), col, bOn, -1); + } + + void set(int pos, int col, bool bOn) + { + GtkTreeModel *pModel = GTK_TREE_MODEL(m_pTreeStore); + GtkTreeIter iter; + if (gtk_tree_model_iter_nth_child(pModel, &iter, nullptr, pos)) + set(iter, col, bOn); + } + + void set(const GtkTreeIter& iter, int col, gint bInt) + { + gtk_tree_store_set(m_pTreeStore, const_cast<GtkTreeIter*>(&iter), col, bInt, -1); + } + + void set(int pos, int col, gint bInt) + { + GtkTreeModel *pModel = GTK_TREE_MODEL(m_pTreeStore); + GtkTreeIter iter; + if (gtk_tree_model_iter_nth_child(pModel, &iter, nullptr, pos)) + set(iter, col, bInt); + } + + void set(const GtkTreeIter& iter, int col, double fValue) + { + gtk_tree_store_set(m_pTreeStore, const_cast<GtkTreeIter*>(&iter), col, fValue, -1); + } + + void set(int pos, int col, double fValue) + { + GtkTreeModel *pModel = GTK_TREE_MODEL(m_pTreeStore); + GtkTreeIter iter; + if (gtk_tree_model_iter_nth_child(pModel, &iter, nullptr, pos)) + set(iter, col, fValue); + } + + static gboolean signalTestExpandRow(GtkTreeView*, GtkTreeIter* iter, GtkTreePath*, gpointer widget) + { + GtkInstanceTreeView* pThis = static_cast<GtkInstanceTreeView*>(widget); + return !pThis->signal_test_expand_row(*iter); + } + + static gboolean signalTestCollapseRow(GtkTreeView*, GtkTreeIter* iter, GtkTreePath*, gpointer widget) + { + GtkInstanceTreeView* pThis = static_cast<GtkInstanceTreeView*>(widget); + return !pThis->signal_test_collapse_row(*iter); + } + + bool child_is_placeholder(GtkInstanceTreeIter& rGtkIter) const + { + GtkTreeModel *pModel = GTK_TREE_MODEL(m_pTreeStore); + + GtkTreePath* pPath = gtk_tree_model_get_path(pModel, &rGtkIter.iter); + bool bExpanding = m_aExpandingPlaceHolderParents.count(pPath); + gtk_tree_path_free(pPath); + if (bExpanding) + return true; + + bool bPlaceHolder = false; + GtkTreeIter tmp; + if (gtk_tree_model_iter_children(pModel, &tmp, &rGtkIter.iter)) + { + rGtkIter.iter = tmp; + if (get_text(rGtkIter, -1) == "<dummy>") + { + bPlaceHolder = true; + } + } + return bPlaceHolder; + } + + bool signal_test_expand_row(GtkTreeIter& iter) + { + disable_notify_events(); + + // if there's a preexisting placeholder child, required to make this + // potentially expandable in the first place, now we remove it + GtkInstanceTreeIter aIter(iter); + GtkTreePath* pPlaceHolderPath = nullptr; + bool bPlaceHolder = child_is_placeholder(aIter); + if (bPlaceHolder) + { + gtk_tree_store_remove(m_pTreeStore, &aIter.iter); + + GtkTreeModel *pModel = GTK_TREE_MODEL(m_pTreeStore); + pPlaceHolderPath = gtk_tree_model_get_path(pModel, &iter); + m_aExpandingPlaceHolderParents.insert(pPlaceHolderPath); + } + + aIter.iter = iter; + bool bRet = signal_expanding(aIter); + + if (bPlaceHolder) + { + //expand disallowed, restore placeholder + if (!bRet) + { + GtkTreeIter subiter; + OUString sDummy("<dummy>"); + insert_row(subiter, &iter, -1, nullptr, &sDummy, nullptr, nullptr, nullptr); + } + m_aExpandingPlaceHolderParents.erase(pPlaceHolderPath); + gtk_tree_path_free(pPlaceHolderPath); + } + + enable_notify_events(); + return bRet; + } + + bool signal_test_collapse_row(GtkTreeIter& iter) + { + disable_notify_events(); + + GtkInstanceTreeIter aIter(iter); + bool bRet = signal_collapsing(aIter); + + enable_notify_events(); + return bRet; + } + + static void signalCellToggled(GtkCellRendererToggle* pCell, const gchar *path, gpointer widget) + { + GtkInstanceTreeView* pThis = static_cast<GtkInstanceTreeView*>(widget); + void* pData = g_object_get_data(G_OBJECT(pCell), "g-lo-CellIndex"); + pThis->signal_cell_toggled(path, reinterpret_cast<sal_IntPtr>(pData)); + } + + void signal_cell_toggled(const gchar *path, int nCol) + { + GtkTreePath *tree_path = gtk_tree_path_new_from_string(path); + + // toggled signal handlers can query get_cursor to get which + // node was clicked + gtk_tree_view_set_cursor(m_pTreeView, tree_path, nullptr, false); + + GtkTreeModel *pModel = GTK_TREE_MODEL(m_pTreeStore); + GtkTreeIter iter; + gtk_tree_model_get_iter(pModel, &iter, tree_path); + + gboolean bRet(false); + gtk_tree_model_get(pModel, &iter, nCol, &bRet, -1); + bRet = !bRet; + gtk_tree_store_set(m_pTreeStore, &iter, nCol, bRet, -1); + + gint depth; + gint* indices = gtk_tree_path_get_indices_with_depth(tree_path, &depth); + int nRow = indices[depth-1]; + + set(iter, m_aToggleTriStateMap[nCol], false); + + signal_toggled(std::make_pair(nRow, nCol)); + + gtk_tree_path_free(tree_path); + } + + DECL_LINK(async_stop_cell_editing, void*, void); + + static void signalCellEditingStarted(GtkCellRenderer*, GtkCellEditable*, const gchar *path, gpointer widget) + { + GtkInstanceTreeView* pThis = static_cast<GtkInstanceTreeView*>(widget); + if (!pThis->signal_cell_editing_started(path)) + Application::PostUserEvent(LINK(pThis, GtkInstanceTreeView, async_stop_cell_editing)); + } + + bool signal_cell_editing_started(const gchar *path) + { + GtkTreePath *tree_path = gtk_tree_path_new_from_string(path); + + GtkTreeModel *pModel = GTK_TREE_MODEL(m_pTreeStore); + GtkInstanceTreeIter aGtkIter(nullptr); + gtk_tree_model_get_iter(pModel, &aGtkIter.iter, tree_path); + gtk_tree_path_free(tree_path); + + return signal_editing_started(aGtkIter); + } + + static void signalCellEdited(GtkCellRendererText* pCell, const gchar *path, const gchar *pNewText, gpointer widget) + { + GtkInstanceTreeView* pThis = static_cast<GtkInstanceTreeView*>(widget); + pThis->signal_cell_edited(pCell, path, pNewText); + } + + static void restoreNonEditable(GObject* pCell) + { + if (g_object_get_data(pCell, "g-lo-RestoreNonEditable")) + { + g_object_set(pCell, "editable", false, "editable-set", false, nullptr); + g_object_set_data(pCell, "g-lo-RestoreNonEditable", reinterpret_cast<gpointer>(false)); + } + } + + void signal_cell_edited(GtkCellRendererText* pCell, const gchar *path, const gchar* pNewText) + { + GtkTreePath *tree_path = gtk_tree_path_new_from_string(path); + + GtkTreeModel *pModel = GTK_TREE_MODEL(m_pTreeStore); + GtkInstanceTreeIter aGtkIter(nullptr); + gtk_tree_model_get_iter(pModel, &aGtkIter.iter, tree_path); + gtk_tree_path_free(tree_path); + + OUString sText(pNewText, pNewText ? strlen(pNewText) : 0, RTL_TEXTENCODING_UTF8); + if (signal_editing_done(std::pair<const weld::TreeIter&, OUString>(aGtkIter, sText))) + { + void* pData = g_object_get_data(G_OBJECT(pCell), "g-lo-CellIndex"); + set(aGtkIter.iter, reinterpret_cast<sal_IntPtr>(pData), sText); + } + + restoreNonEditable(G_OBJECT(pCell)); + } + + static void signalCellEditingCanceled(GtkCellRenderer* pCell, gpointer /*widget*/) + { + restoreNonEditable(G_OBJECT(pCell)); + } + + void signal_column_clicked(GtkTreeViewColumn* pClickedColumn) + { + int nIndex(0); + for (GList* pEntry = g_list_first(m_pColumns); pEntry; pEntry = g_list_next(pEntry)) + { + GtkTreeViewColumn* pColumn = GTK_TREE_VIEW_COLUMN(pEntry->data); + if (pColumn == pClickedColumn) + { + TreeView::signal_column_clicked(nIndex); + break; + } + ++nIndex; + } + } + + static void signalColumnClicked(GtkTreeViewColumn* pColumn, gpointer widget) + { + GtkInstanceTreeView* pThis = static_cast<GtkInstanceTreeView*>(widget); + pThis->signal_column_clicked(pColumn); + } + + static void signalVAdjustmentChanged(GtkAdjustment*, gpointer widget) + { + GtkInstanceTreeView* pThis = static_cast<GtkInstanceTreeView*>(widget); + pThis->signal_visible_range_changed(); + } + + int get_model_col(int viewcol) const + { + return m_aViewColToModelCol[viewcol]; + } + + int get_view_col(int modelcol) const + { + return m_aModelColToViewCol[modelcol]; + } + + void set_column_editable(int nCol, bool bEditable) + { + for (GList* pEntry = g_list_first(m_pColumns); pEntry; pEntry = g_list_next(pEntry)) + { + GtkTreeViewColumn* pColumn = GTK_TREE_VIEW_COLUMN(pEntry->data); + GList *pRenderers = gtk_cell_layout_get_cells(GTK_CELL_LAYOUT(pColumn)); + for (GList* pRenderer = g_list_first(pRenderers); pRenderer; pRenderer = g_list_next(pRenderer)) + { + GtkCellRenderer* pCellRenderer = GTK_CELL_RENDERER(pRenderer->data); + void* pData = g_object_get_data(G_OBJECT(pCellRenderer), "g-lo-CellIndex"); + if (reinterpret_cast<sal_IntPtr>(pData) == nCol) + { + g_object_set(G_OBJECT(pCellRenderer), "editable", bEditable, "editable-set", true, nullptr); + break; + } + } + g_list_free(pRenderers); + } + } + + static void signalRowDeleted(GtkTreeModel*, GtkTreePath*, gpointer widget) + { + GtkInstanceTreeView* pThis = static_cast<GtkInstanceTreeView*>(widget); + pThis->signal_model_changed(); + } + + static void signalRowInserted(GtkTreeModel*, GtkTreePath*, GtkTreeIter*, gpointer widget) + { + GtkInstanceTreeView* pThis = static_cast<GtkInstanceTreeView*>(widget); + pThis->signal_model_changed(); + } + + static gint sortFunc(GtkTreeModel* pModel, GtkTreeIter* a, GtkTreeIter* b, gpointer widget) + { + GtkInstanceTreeView* pThis = static_cast<GtkInstanceTreeView*>(widget); + return pThis->sort_func(pModel, a, b); + } + + gint sort_func(GtkTreeModel* pModel, GtkTreeIter* a, GtkTreeIter* b) + { + if (m_aCustomSort) + return m_aCustomSort(GtkInstanceTreeIter(*a), GtkInstanceTreeIter(*b)); + return default_sort_func(pModel, a, b, m_xSorter.get()); + } + + bool signal_key_press(GdkEventKey* pEvent) + { + if (pEvent->keyval != GDK_KEY_Left && pEvent->keyval != GDK_KEY_Right) + return false; + + GtkInstanceTreeIter aIter(nullptr); + if (!get_cursor(&aIter)) + return false; + + bool bHasChild = gtk_tree_model_iter_has_child(GTK_TREE_MODEL(m_pTreeStore), &aIter.iter); + + if (pEvent->keyval == GDK_KEY_Right) + { + if (bHasChild && !get_row_expanded(aIter)) + { + expand_row(aIter); + return true; + } + return false; + } + + if (bHasChild && get_row_expanded(aIter)) + { + collapse_row(aIter); + return true; + } + + if (iter_parent(aIter)) + { + unselect_all(); + set_cursor(aIter); + select(aIter); + return true; + } + + return false; + } + + static gboolean signalKeyPress(GtkWidget*, GdkEventKey* pEvent, gpointer widget) + { + GtkInstanceTreeView* pThis = static_cast<GtkInstanceTreeView*>(widget); + return pThis->signal_key_press(pEvent); + } + + static gboolean signalQueryTooltip(GtkWidget* /*pGtkWidget*/, gint x, gint y, + gboolean keyboard_tip, GtkTooltip *tooltip, + gpointer widget) + { + GtkInstanceTreeView* pThis = static_cast<GtkInstanceTreeView*>(widget); + GtkTreeIter iter; + GtkTreeView *pTreeView = pThis->m_pTreeView; + GtkTreeModel *pModel = gtk_tree_view_get_model(pTreeView); + GtkTreePath *pPath = nullptr; + if (!gtk_tree_view_get_tooltip_context(pTreeView, &x, &y, keyboard_tip, &pModel, &pPath, &iter)) + return false; + OUString aTooltip = pThis->signal_query_tooltip(GtkInstanceTreeIter(iter)); + if (aTooltip.isEmpty()) + return false; + gtk_tooltip_set_text(tooltip, OUStringToOString(aTooltip, RTL_TEXTENCODING_UTF8).getStr()); + gtk_tree_view_set_tooltip_row(pTreeView, tooltip, pPath); + gtk_tree_path_free(pPath); + return true; + } + + void last_child(GtkTreeModel* pModel, GtkTreeIter* result, GtkTreeIter* pParent, int nChildren) const + { + gtk_tree_model_iter_nth_child(pModel, result, pParent, nChildren - 1); + nChildren = gtk_tree_model_iter_n_children(pModel, result); + if (nChildren) + { + GtkTreeIter newparent(*result); + last_child(pModel, result, &newparent, nChildren); + } + } + + GtkTreePath* get_path_of_last_entry(GtkTreeModel *pModel) + { + GtkTreePath *lastpath; + // find the last entry in the model for comparison + int nChildren = gtk_tree_model_iter_n_children(pModel, nullptr); + if (!nChildren) + lastpath = gtk_tree_path_new_from_indices(0, -1); + else + { + GtkTreeIter iter; + last_child(pModel, &iter, nullptr, nChildren); + lastpath = gtk_tree_model_get_path(pModel, &iter); + } + return lastpath; + } + + void set_font_color(const GtkTreeIter& iter, const Color& rColor) + { + if (rColor == COL_AUTO) + gtk_tree_store_set(m_pTreeStore, const_cast<GtkTreeIter*>(&iter), m_nIdCol + 1, nullptr, -1); + else + { + GdkRGBA aColor{rColor.GetRed()/255.0, rColor.GetGreen()/255.0, rColor.GetBlue()/255.0, 0}; + gtk_tree_store_set(m_pTreeStore, const_cast<GtkTreeIter*>(&iter), m_nIdCol + 1, &aColor, -1); + } + } + + int get_expander_size() const + { + gint nExpanderSize; + gint nHorizontalSeparator; + + gtk_widget_style_get(GTK_WIDGET(m_pTreeView), + "expander-size", &nExpanderSize, + "horizontal-separator", &nHorizontalSeparator, + nullptr); + + return nExpanderSize + (nHorizontalSeparator/ 2); + } + + void real_vadjustment_set_value(int value) + { + disable_notify_events(); + gtk_adjustment_set_value(m_pVAdjustment, value); + enable_notify_events(); + } + + static gboolean setAdjustmentCallback(GtkWidget*, GdkFrameClock*, gpointer widget) + { + GtkInstanceTreeView* pThis = static_cast<GtkInstanceTreeView*>(widget); + if (pThis->m_nPendingVAdjustment != -1) + { + pThis->real_vadjustment_set_value(pThis->m_nPendingVAdjustment); + pThis->m_nPendingVAdjustment = -1; + } + return false; + } + + bool iter_next(weld::TreeIter& rIter, bool bOnlyExpanded) const + { + GtkInstanceTreeIter& rGtkIter = static_cast<GtkInstanceTreeIter&>(rIter); + GtkTreeModel *pModel = GTK_TREE_MODEL(m_pTreeStore); + GtkTreeIter tmp; + GtkTreeIter iter = rGtkIter.iter; + + bool ret = gtk_tree_model_iter_children(pModel, &tmp, &iter); + if (ret && bOnlyExpanded && !get_row_expanded(rGtkIter)) + ret = false; + rGtkIter.iter = tmp; + if (ret) + { + //on-demand dummy entry doesn't count + if (get_text(rGtkIter, -1) == "<dummy>") + return iter_next(rGtkIter, bOnlyExpanded); + return true; + } + + tmp = iter; + if (gtk_tree_model_iter_next(pModel, &tmp)) + { + rGtkIter.iter = tmp; + //on-demand dummy entry doesn't count + if (get_text(rGtkIter, -1) == "<dummy>") + return iter_next(rGtkIter, bOnlyExpanded); + return true; + } + // Move up level(s) until we find the level where the next node exists. + while (gtk_tree_model_iter_parent(pModel, &tmp, &iter)) + { + iter = tmp; + if (gtk_tree_model_iter_next(pModel, &tmp)) + { + rGtkIter.iter = tmp; + //on-demand dummy entry doesn't count + if (get_text(rGtkIter, -1) == "<dummy>") + return iter_next(rGtkIter, bOnlyExpanded); + return true; + } + } + return false; + } + +public: + GtkInstanceTreeView(GtkTreeView* pTreeView, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : GtkInstanceContainer(GTK_CONTAINER(pTreeView), pBuilder, bTakeOwnership) + , m_pTreeView(pTreeView) + , m_pTreeStore(GTK_TREE_STORE(gtk_tree_view_get_model(m_pTreeView))) + , m_bWorkAroundBadDragRegion(false) + , m_bInDrag(false) + , m_nTextCol(-1) + , m_nImageCol(-1) + , m_nExpanderImageCol(-1) + , m_nPendingVAdjustment(-1) + , m_nChangedSignalId(g_signal_connect(gtk_tree_view_get_selection(pTreeView), "changed", + G_CALLBACK(signalChanged), this)) + , m_nRowActivatedSignalId(g_signal_connect(pTreeView, "row-activated", G_CALLBACK(signalRowActivated), this)) + , m_nTestExpandRowSignalId(g_signal_connect(pTreeView, "test-expand-row", G_CALLBACK(signalTestExpandRow), this)) + , m_nTestCollapseRowSignalId(g_signal_connect(pTreeView, "test-collapse-row", G_CALLBACK(signalTestCollapseRow), this)) + , m_nVAdjustmentChangedSignalId(0) + , m_nPopupMenuSignalId(g_signal_connect(pTreeView, "popup-menu", G_CALLBACK(signalPopupMenu), this)) + , m_nKeyPressSignalId(g_signal_connect(pTreeView, "key-press-event", G_CALLBACK(signalKeyPress), this)) + , m_nQueryTooltipSignalId(0) + , m_pVAdjustment(gtk_scrollable_get_vadjustment(GTK_SCROLLABLE(pTreeView))) + , m_pChangeEvent(nullptr) + { + m_pColumns = gtk_tree_view_get_columns(m_pTreeView); + int nIndex(0); + for (GList* pEntry = g_list_first(m_pColumns); pEntry; pEntry = g_list_next(pEntry)) + { + GtkTreeViewColumn* pColumn = GTK_TREE_VIEW_COLUMN(pEntry->data); + m_aColumnSignalIds.push_back(g_signal_connect(pColumn, "clicked", G_CALLBACK(signalColumnClicked), this)); + GList *pRenderers = gtk_cell_layout_get_cells(GTK_CELL_LAYOUT(pColumn)); + for (GList* pRenderer = g_list_first(pRenderers); pRenderer; pRenderer = g_list_next(pRenderer)) + { + GtkCellRenderer* pCellRenderer = GTK_CELL_RENDERER(pRenderer->data); + g_object_set_data(G_OBJECT(pCellRenderer), "g-lo-CellIndex", reinterpret_cast<gpointer>(nIndex)); + if (GTK_IS_CELL_RENDERER_TEXT(pCellRenderer)) + { + if (m_nTextCol == -1) + m_nTextCol = nIndex; + m_aWeightMap[nIndex] = -1; + m_aSensitiveMap[nIndex] = -1; + m_aIndentMap[nIndex] = -1; + m_aAlignMap[nIndex] = -1; + g_signal_connect(G_OBJECT(pCellRenderer), "editing-started", G_CALLBACK(signalCellEditingStarted), this); + g_signal_connect(G_OBJECT(pCellRenderer), "editing-canceled", G_CALLBACK(signalCellEditingCanceled), this); + g_signal_connect(G_OBJECT(pCellRenderer), "edited", G_CALLBACK(signalCellEdited), this); + } + else if (GTK_IS_CELL_RENDERER_TOGGLE(pCellRenderer)) + { + g_signal_connect(G_OBJECT(pCellRenderer), "toggled", G_CALLBACK(signalCellToggled), this); + m_aToggleVisMap[nIndex] = -1; + m_aToggleTriStateMap[nIndex] = -1; + } + else if (GTK_IS_CELL_RENDERER_PIXBUF(pCellRenderer)) + { + const bool bExpander = g_list_next(pRenderer) != nullptr; + if (bExpander && m_nExpanderImageCol == -1) + m_nExpanderImageCol = nIndex; + else if (m_nImageCol == -1) + m_nImageCol = nIndex; + } + m_aModelColToViewCol.push_back(m_aViewColToModelCol.size()); + ++nIndex; + } + g_list_free(pRenderers); + m_aViewColToModelCol.push_back(nIndex - 1); + } + + m_nIdCol = nIndex++; + + for (auto& a : m_aToggleVisMap) + a.second = nIndex++; + for (auto& a : m_aToggleTriStateMap) + a.second = nIndex++; + for (auto& a : m_aWeightMap) + a.second = nIndex++; + for (auto& a : m_aSensitiveMap) + a.second = nIndex++; + for (auto& a : m_aIndentMap) + a.second = nIndex++; + for (auto& a : m_aAlignMap) + a.second = nIndex++; + + ensure_drag_begin_end(); + + GtkTreeModel *pModel = GTK_TREE_MODEL(m_pTreeStore); + m_nRowDeletedSignalId = g_signal_connect(pModel, "row-deleted", G_CALLBACK(signalRowDeleted), this); + m_nRowInsertedSignalId = g_signal_connect(pModel, "row-inserted", G_CALLBACK(signalRowInserted), this); + } + + virtual void connect_query_tooltip(const Link<const weld::TreeIter&, OUString>& rLink) override + { + weld::TreeView::connect_query_tooltip(rLink); + m_nQueryTooltipSignalId = g_signal_connect(m_pTreeView, "query-tooltip", G_CALLBACK(signalQueryTooltip), this); + } + + virtual void columns_autosize() override + { + gtk_tree_view_columns_autosize(m_pTreeView); + } + + virtual void set_column_fixed_widths(const std::vector<int>& rWidths) override + { + GList* pEntry = g_list_first(m_pColumns); + for (auto nWidth : rWidths) + { + assert(pEntry && "wrong count"); + GtkTreeViewColumn* pColumn = GTK_TREE_VIEW_COLUMN(pEntry->data); + gtk_tree_view_column_set_fixed_width(pColumn, nWidth); + pEntry = g_list_next(pEntry); + } + } + + virtual void set_column_editables(const std::vector<bool>& rEditables) override + { + size_t nTabCount = rEditables.size(); + for (size_t i = 0 ; i < nTabCount; ++i) + set_column_editable(i, rEditables[i]); + } + + virtual void set_centered_column(int nCol) override + { + for (GList* pEntry = g_list_first(m_pColumns); pEntry; pEntry = g_list_next(pEntry)) + { + GtkTreeViewColumn* pColumn = GTK_TREE_VIEW_COLUMN(pEntry->data); + GList *pRenderers = gtk_cell_layout_get_cells(GTK_CELL_LAYOUT(pColumn)); + for (GList* pRenderer = g_list_first(pRenderers); pRenderer; pRenderer = g_list_next(pRenderer)) + { + GtkCellRenderer* pCellRenderer = GTK_CELL_RENDERER(pRenderer->data); + void* pData = g_object_get_data(G_OBJECT(pCellRenderer), "g-lo-CellIndex"); + if (reinterpret_cast<sal_IntPtr>(pData) == nCol) + { + g_object_set(G_OBJECT(pCellRenderer), "xalign", 0.5, nullptr); + break; + } + } + g_list_free(pRenderers); + } + } + + virtual int get_column_width(int nColumn) const override + { + GtkTreeViewColumn* pColumn = GTK_TREE_VIEW_COLUMN(g_list_nth_data(m_pColumns, nColumn)); + assert(pColumn && "wrong count"); + int nWidth = gtk_tree_view_column_get_width(pColumn); + // https://github.com/exaile/exaile/issues/580 + // after setting fixed_width on a column and requesting width before + // gtk has a chance to do its layout of the column means that the width + // request hasn't come into effect + if (!nWidth) + nWidth = gtk_tree_view_column_get_fixed_width(pColumn); + return nWidth; + } + + virtual OUString get_column_title(int nColumn) const override + { + GtkTreeViewColumn* pColumn = GTK_TREE_VIEW_COLUMN(g_list_nth_data(m_pColumns, nColumn)); + assert(pColumn && "wrong count"); + const gchar* pTitle = gtk_tree_view_column_get_title(pColumn); + OUString sRet(pTitle, pTitle ? strlen(pTitle) : 0, RTL_TEXTENCODING_UTF8); + return sRet; + } + + virtual void set_column_title(int nColumn, const OUString& rTitle) override + { + GtkTreeViewColumn* pColumn = GTK_TREE_VIEW_COLUMN(g_list_nth_data(m_pColumns, nColumn)); + assert(pColumn && "wrong count"); + gtk_tree_view_column_set_title(pColumn, OUStringToOString(rTitle, RTL_TEXTENCODING_UTF8).getStr()); + } + + virtual void set_column_custom_renderer(int nColumn, bool bEnable) override + { + assert(n_children() == 0 && "tree must be empty"); + GtkTreeViewColumn* pColumn = GTK_TREE_VIEW_COLUMN(g_list_nth_data(m_pColumns, nColumn)); + assert(pColumn && "wrong count"); + gtk_cell_layout_clear(GTK_CELL_LAYOUT(pColumn)); + if (bEnable) + { + GtkCellRenderer *pRenderer = custom_cell_renderer_surface_new(); + GValue value = G_VALUE_INIT; + g_value_init(&value, G_TYPE_POINTER); + g_value_set_pointer(&value, static_cast<gpointer>(this)); + g_object_set_property(G_OBJECT(pRenderer), "instance", &value); + gtk_tree_view_column_pack_start(pColumn, pRenderer, true); + gtk_tree_view_column_add_attribute(pColumn, pRenderer, "text", m_nTextCol); + gtk_tree_view_column_add_attribute(pColumn, pRenderer, "id", m_nIdCol); + } + else + { + GtkCellRenderer *pRenderer = gtk_cell_renderer_text_new(); + gtk_tree_view_column_pack_start(pColumn, pRenderer, true); + gtk_tree_view_column_add_attribute(pColumn, pRenderer, "text", m_nTextCol); + } + } + + virtual void insert(const weld::TreeIter* pParent, int pos, const OUString* pText, const OUString* pId, const OUString* pIconName, + VirtualDevice* pImageSurface, const OUString* pExpanderName, + bool bChildrenOnDemand, weld::TreeIter* pRet) override + { + disable_notify_events(); + GtkTreeIter iter; + const GtkInstanceTreeIter* pGtkIter = static_cast<const GtkInstanceTreeIter*>(pParent); + insert_row(iter, pGtkIter ? &pGtkIter->iter : nullptr, pos, pId, pText, pIconName, pImageSurface, pExpanderName); + if (bChildrenOnDemand) + { + GtkTreeIter subiter; + OUString sDummy("<dummy>"); + insert_row(subiter, &iter, -1, nullptr, &sDummy, nullptr, nullptr, nullptr); + } + if (pRet) + { + GtkInstanceTreeIter* pGtkRetIter = static_cast<GtkInstanceTreeIter*>(pRet); + pGtkRetIter->iter = iter; + } + enable_notify_events(); + } + + virtual void set_font_color(int pos, const Color& rColor) override + { + GtkTreeIter iter; + gtk_tree_model_iter_nth_child(GTK_TREE_MODEL(m_pTreeStore), &iter, nullptr, pos); + set_font_color(iter, rColor); + } + + virtual void set_font_color(const weld::TreeIter& rIter, const Color& rColor) override + { + const GtkInstanceTreeIter& rGtkIter = static_cast<const GtkInstanceTreeIter&>(rIter); + set_font_color(rGtkIter.iter, rColor); + } + + virtual void remove(int pos) override + { + disable_notify_events(); + GtkTreeIter iter; + gtk_tree_model_iter_nth_child(GTK_TREE_MODEL(m_pTreeStore), &iter, nullptr, pos); + gtk_tree_store_remove(m_pTreeStore, &iter); + enable_notify_events(); + } + + virtual int find_text(const OUString& rText) const override + { + Search aSearch(rText, m_nTextCol); + gtk_tree_model_foreach(GTK_TREE_MODEL(m_pTreeStore), foreach_find, &aSearch); + return aSearch.index; + } + + virtual int find_id(const OUString& rId) const override + { + Search aSearch(rId, m_nIdCol); + gtk_tree_model_foreach(GTK_TREE_MODEL(m_pTreeStore), foreach_find, &aSearch); + return aSearch.index; + } + + virtual void bulk_insert_for_each(int nSourceCount, const std::function<void(weld::TreeIter&, int nSourceIndex)>& func, + const std::vector<int>* pFixedWidths) override + { + freeze(); + clear(); + GtkInstanceTreeIter aGtkIter(nullptr); + + if (pFixedWidths) + set_column_fixed_widths(*pFixedWidths); + + while (nSourceCount) + { + // tdf#125241 inserting backwards is massively faster + gtk_tree_store_prepend(m_pTreeStore, &aGtkIter.iter, nullptr); + func(aGtkIter, --nSourceCount); + } + + thaw(); + } + + virtual void swap(int pos1, int pos2) override + { + disable_notify_events(); + + GtkTreeModel *pModel = GTK_TREE_MODEL(m_pTreeStore); + + GtkTreeIter iter1; + gtk_tree_model_iter_nth_child(pModel, &iter1, nullptr, pos1); + + GtkTreeIter iter2; + gtk_tree_model_iter_nth_child(pModel, &iter2, nullptr, pos2); + + gtk_tree_store_swap(m_pTreeStore, &iter1, &iter2); + + enable_notify_events(); + } + + virtual void clear() override + { + disable_notify_events(); + gtk_tree_store_clear(m_pTreeStore); + enable_notify_events(); + } + + virtual void make_sorted() override + { + // thaw wants to restore sort state of freeze + assert(gtk_tree_view_get_model(m_pTreeView) && "don't select when frozen, select after thaw. Note selection doesn't survive a freeze"); + m_xSorter.reset(new comphelper::string::NaturalStringSorter( + ::comphelper::getProcessComponentContext(), + Application::GetSettings().GetUILanguageTag().getLocale())); + GtkTreeSortable* pSortable = GTK_TREE_SORTABLE(m_pTreeStore); + gtk_tree_sortable_set_sort_func(pSortable, m_nTextCol, sortFunc, this, nullptr); + gtk_tree_sortable_set_sort_column_id(pSortable, m_nTextCol, GTK_SORT_ASCENDING); + } + + virtual void make_unsorted() override + { + m_xSorter.reset(); + int nSortColumn; + GtkSortType eSortType; + GtkTreeSortable* pSortable = GTK_TREE_SORTABLE(m_pTreeStore); + gtk_tree_sortable_get_sort_column_id(pSortable, &nSortColumn, &eSortType); + gtk_tree_sortable_set_sort_column_id(pSortable, GTK_TREE_SORTABLE_UNSORTED_SORT_COLUMN_ID, eSortType); + } + + virtual void set_sort_order(bool bAscending) override + { + GtkSortType eSortType = bAscending ? GTK_SORT_ASCENDING : GTK_SORT_DESCENDING; + + gint sort_column_id(0); + GtkTreeSortable* pSortable = GTK_TREE_SORTABLE(m_pTreeStore); + gtk_tree_sortable_get_sort_column_id(pSortable, &sort_column_id, nullptr); + gtk_tree_sortable_set_sort_column_id(pSortable, sort_column_id, eSortType); + } + + virtual bool get_sort_order() const override + { + GtkSortType eSortType; + + GtkTreeSortable* pSortable = GTK_TREE_SORTABLE(m_pTreeStore); + gtk_tree_sortable_get_sort_column_id(pSortable, nullptr, &eSortType); + return eSortType == GTK_SORT_ASCENDING; + } + + virtual void set_sort_indicator(TriState eState, int col) override + { + if (col == -1) + col = get_view_col(m_nTextCol); + + GtkTreeViewColumn* pColumn = GTK_TREE_VIEW_COLUMN(g_list_nth_data(m_pColumns, col)); + assert(pColumn && "wrong count"); + if (eState == TRISTATE_INDET) + gtk_tree_view_column_set_sort_indicator(pColumn, false); + else + { + gtk_tree_view_column_set_sort_indicator(pColumn, true); + GtkSortType eSortType = eState == TRISTATE_TRUE ? GTK_SORT_ASCENDING : GTK_SORT_DESCENDING; + gtk_tree_view_column_set_sort_order(pColumn, eSortType); + } + } + + virtual TriState get_sort_indicator(int col) const override + { + if (col == -1) + col = get_view_col(m_nTextCol); + + GtkTreeViewColumn* pColumn = GTK_TREE_VIEW_COLUMN(g_list_nth_data(m_pColumns, col)); + if (!gtk_tree_view_column_get_sort_indicator(pColumn)) + return TRISTATE_INDET; + return gtk_tree_view_column_get_sort_order(pColumn) == GTK_SORT_ASCENDING ? TRISTATE_TRUE : TRISTATE_FALSE; + } + + virtual int get_sort_column() const override + { + GtkTreeSortable* pSortable = GTK_TREE_SORTABLE(m_pTreeStore); + gint sort_column_id(0); + if (!gtk_tree_sortable_get_sort_column_id(pSortable, &sort_column_id, nullptr)) + return -1; + return get_view_col(sort_column_id); + } + + virtual void set_sort_column(int nColumn) override + { + if (nColumn == -1) + { + make_unsorted(); + return; + } + GtkSortType eSortType; + GtkTreeSortable* pSortable = GTK_TREE_SORTABLE(m_pTreeStore); + gtk_tree_sortable_get_sort_column_id(pSortable, nullptr, &eSortType); + int nSortCol = get_model_col(nColumn); + gtk_tree_sortable_set_sort_func(pSortable, nSortCol, sortFunc, this, nullptr); + gtk_tree_sortable_set_sort_column_id(pSortable, nSortCol, eSortType); + } + + virtual void set_sort_func(const std::function<int(const weld::TreeIter&, const weld::TreeIter&)>& func) override + { + weld::TreeView::set_sort_func(func); + GtkTreeSortable* pSortable = GTK_TREE_SORTABLE(m_pTreeStore); + gtk_tree_sortable_sort_column_changed(pSortable); + } + + virtual int n_children() const override + { + return gtk_tree_model_iter_n_children(GTK_TREE_MODEL(m_pTreeStore), nullptr); + } + + virtual int iter_n_children(const weld::TreeIter& rIter) const override + { + const GtkInstanceTreeIter& rGtkIter = static_cast<const GtkInstanceTreeIter&>(rIter); + return gtk_tree_model_iter_n_children(GTK_TREE_MODEL(m_pTreeStore), const_cast<GtkTreeIter*>(&rGtkIter.iter)); + } + + virtual void select(int pos) override + { + assert(gtk_tree_view_get_model(m_pTreeView) && "don't select when frozen, select after thaw. Note selection doesn't survive a freeze"); + disable_notify_events(); + if (pos == -1 || (pos == 0 && n_children() == 0)) + { + gtk_tree_selection_unselect_all(gtk_tree_view_get_selection(m_pTreeView)); + } + else + { + GtkTreePath* path = gtk_tree_path_new_from_indices(pos, -1); + gtk_tree_selection_select_path(gtk_tree_view_get_selection(m_pTreeView), path); + gtk_tree_view_scroll_to_cell(m_pTreeView, path, nullptr, false, 0, 0); + gtk_tree_path_free(path); + } + enable_notify_events(); + } + + virtual void set_cursor(int pos) override + { + disable_notify_events(); + GtkTreePath* path = gtk_tree_path_new_from_indices(pos, -1); + gtk_tree_view_scroll_to_cell(m_pTreeView, path, nullptr, false, 0, 0); + gtk_tree_view_set_cursor(m_pTreeView, path, nullptr, false); + gtk_tree_path_free(path); + enable_notify_events(); + } + + virtual void scroll_to_row(int pos) override + { + assert(gtk_tree_view_get_model(m_pTreeView) && "don't select when frozen, select after thaw. Note selection doesn't survive a freeze"); + disable_notify_events(); + GtkTreePath* path = gtk_tree_path_new_from_indices(pos, -1); + gtk_tree_view_expand_to_path(m_pTreeView, path); + gtk_tree_view_scroll_to_cell(m_pTreeView, path, nullptr, false, 0, 0); + gtk_tree_path_free(path); + enable_notify_events(); + } + + virtual bool is_selected(int pos) const override + { + GtkTreeIter iter; + gtk_tree_model_iter_nth_child(GTK_TREE_MODEL(m_pTreeStore), &iter, nullptr, pos); + return gtk_tree_selection_iter_is_selected(gtk_tree_view_get_selection(m_pTreeView), &iter); + } + + virtual void unselect(int pos) override + { + assert(gtk_tree_view_get_model(m_pTreeView) && "don't select when frozen, select after thaw. Note selection doesn't survive a freeze"); + disable_notify_events(); + if (pos == -1 || (pos == 0 && n_children() == 0)) + { + gtk_tree_selection_select_all(gtk_tree_view_get_selection(m_pTreeView)); + } + else + { + GtkTreePath* path = gtk_tree_path_new_from_indices(pos, -1); + gtk_tree_selection_unselect_path(gtk_tree_view_get_selection(m_pTreeView), path); + gtk_tree_path_free(path); + } + enable_notify_events(); + } + + virtual std::vector<int> get_selected_rows() const override + { + std::vector<int> aRows; + + GList* pList = gtk_tree_selection_get_selected_rows(gtk_tree_view_get_selection(m_pTreeView), nullptr); + for (GList* pItem = g_list_first(pList); pItem; pItem = g_list_next(pItem)) + { + GtkTreePath* path = static_cast<GtkTreePath*>(pItem->data); + + gint depth; + gint* indices = gtk_tree_path_get_indices_with_depth(path, &depth); + int nRow = indices[depth-1]; + + aRows.push_back(nRow); + } + g_list_free_full(pList, reinterpret_cast<GDestroyNotify>(gtk_tree_path_free)); + + return aRows; + } + + virtual void all_foreach(const std::function<bool(weld::TreeIter&)>& func) override + { + GtkInstanceTreeIter aGtkIter(nullptr); + if (get_iter_first(aGtkIter)) + { + do + { + if (func(aGtkIter)) + break; + } while (iter_next(aGtkIter)); + } + } + + virtual void selected_foreach(const std::function<bool(weld::TreeIter&)>& func) override + { + GtkInstanceTreeIter aGtkIter(nullptr); + + GtkTreeModel* pModel; + GList* pList = gtk_tree_selection_get_selected_rows(gtk_tree_view_get_selection(m_pTreeView), &pModel); + for (GList* pItem = g_list_first(pList); pItem; pItem = g_list_next(pItem)) + { + GtkTreePath* path = static_cast<GtkTreePath*>(pItem->data); + gtk_tree_model_get_iter(pModel, &aGtkIter.iter, path); + if (func(aGtkIter)) + break; + } + g_list_free_full(pList, reinterpret_cast<GDestroyNotify>(gtk_tree_path_free)); + } + + virtual void visible_foreach(const std::function<bool(weld::TreeIter&)>& func) override + { + GtkTreePath* start_path; + GtkTreePath* end_path; + + if (gtk_tree_view_get_visible_range(m_pTreeView, &start_path, &end_path)) + { + GtkInstanceTreeIter aGtkIter(nullptr); + GtkTreeModel *pModel = GTK_TREE_MODEL(m_pTreeStore); + gtk_tree_model_get_iter(pModel, &aGtkIter.iter, start_path); + + do + { + if (func(aGtkIter)) + break; + GtkTreePath* path = gtk_tree_model_get_path(pModel, &aGtkIter.iter); + bool bContinue = gtk_tree_path_compare(path, end_path) != 0; + gtk_tree_path_free(path); + if (!bContinue) + break; + if (!iter_next(aGtkIter)) + break; + } while(true); + + gtk_tree_path_free(start_path); + gtk_tree_path_free(end_path); + } + } + + virtual void connect_visible_range_changed(const Link<weld::TreeView&, void>& rLink) override + { + weld::TreeView::connect_visible_range_changed(rLink); + if (!m_nVAdjustmentChangedSignalId) + { + GtkAdjustment* pVAdjustment = gtk_scrollable_get_vadjustment(GTK_SCROLLABLE(m_pTreeView)); + m_nVAdjustmentChangedSignalId = g_signal_connect(pVAdjustment, "value-changed", G_CALLBACK(signalVAdjustmentChanged), this); + } + } + + virtual bool is_selected(const weld::TreeIter& rIter) const override + { + const GtkInstanceTreeIter& rGtkIter = static_cast<const GtkInstanceTreeIter&>(rIter); + return gtk_tree_selection_iter_is_selected(gtk_tree_view_get_selection(m_pTreeView), const_cast<GtkTreeIter*>(&rGtkIter.iter)); + } + + virtual OUString get_text(int pos, int col) const override + { + if (col == -1) + return get(pos, m_nTextCol); + return get(pos, get_model_col(col)); + } + + virtual void set_text(int pos, const OUString& rText, int col) override + { + if (col == -1) + col = m_nTextCol; + else + col = get_model_col(col); + set(pos, col, rText); + } + + virtual TriState get_toggle(int pos, int col) const override + { + col = get_model_col(col); + if (get_bool(pos, m_aToggleTriStateMap.find(col)->second)) + return TRISTATE_INDET; + return get_bool(pos, col) ? TRISTATE_TRUE : TRISTATE_FALSE; + } + + virtual TriState get_toggle(const weld::TreeIter& rIter, int col) const override + { + col = get_model_col(col); + const GtkInstanceTreeIter& rGtkIter = static_cast<const GtkInstanceTreeIter&>(rIter); + if (get_bool(rGtkIter.iter, m_aToggleTriStateMap.find(col)->second)) + return TRISTATE_INDET; + return get_bool(rGtkIter.iter, col) ? TRISTATE_TRUE : TRISTATE_FALSE; + } + + virtual void set_toggle(const weld::TreeIter& rIter, TriState eState, int col) override + { + const GtkInstanceTreeIter& rGtkIter = static_cast<const GtkInstanceTreeIter&>(rIter); + col = get_model_col(col); + // checkbuttons are invisible until toggled on or off + set(rGtkIter.iter, m_aToggleVisMap[col], true); + if (eState == TRISTATE_INDET) + set(rGtkIter.iter, m_aToggleTriStateMap[col], true); + else + { + set(rGtkIter.iter, m_aToggleTriStateMap[col], false); + set(rGtkIter.iter, col, eState == TRISTATE_TRUE); + } + } + + virtual void set_toggle(int pos, TriState eState, int col) override + { + GtkTreeModel *pModel = GTK_TREE_MODEL(m_pTreeStore); + GtkTreeIter iter; + if (gtk_tree_model_iter_nth_child(pModel, &iter, nullptr, pos)) + set_toggle(iter, eState, col); + } + + virtual void set_extra_row_indent(const weld::TreeIter& rIter, int nIndentLevel) override + { + const GtkInstanceTreeIter& rGtkIter = static_cast<const GtkInstanceTreeIter&>(rIter); + set(rGtkIter.iter, m_aIndentMap[m_nTextCol], nIndentLevel * get_expander_size()); + } + + virtual void set_text_emphasis(const weld::TreeIter& rIter, bool bOn, int col) override + { + const GtkInstanceTreeIter& rGtkIter = static_cast<const GtkInstanceTreeIter&>(rIter); + col = get_model_col(col); + set(rGtkIter.iter, m_aWeightMap[col], bOn ? PANGO_WEIGHT_BOLD : PANGO_WEIGHT_NORMAL); + } + + virtual void set_text_emphasis(int pos, bool bOn, int col) override + { + col = get_model_col(col); + set(pos, m_aWeightMap[col], bOn ? PANGO_WEIGHT_BOLD : PANGO_WEIGHT_NORMAL); + } + + virtual bool get_text_emphasis(const weld::TreeIter& rIter, int col) const override + { + const GtkInstanceTreeIter& rGtkIter = static_cast<const GtkInstanceTreeIter&>(rIter); + col = get_model_col(col); + return get_int(rGtkIter.iter, m_aWeightMap.find(col)->second) == PANGO_WEIGHT_BOLD; + } + + virtual bool get_text_emphasis(int pos, int col) const override + { + col = get_model_col(col); + return get_int(pos, m_aWeightMap.find(col)->second) == PANGO_WEIGHT_BOLD; + } + + virtual void set_text_align(const weld::TreeIter& rIter, double fAlign, int col) override + { + const GtkInstanceTreeIter& rGtkIter = static_cast<const GtkInstanceTreeIter&>(rIter); + col = get_model_col(col); + set(rGtkIter.iter, m_aAlignMap[col], fAlign); + } + + virtual void set_text_align(int pos, double fAlign, int col) override + { + col = get_model_col(col); + set(pos, m_aAlignMap[col], fAlign); + } + + using GtkInstanceWidget::set_sensitive; + + virtual void set_sensitive(int pos, bool bSensitive, int col) override + { + if (col == -1) + col = m_nTextCol; + else + col = get_model_col(col); + set(pos, m_aSensitiveMap[col], bSensitive); + } + + virtual void set_sensitive(const weld::TreeIter& rIter, bool bSensitive, int col) override + { + if (col == -1) + col = m_nTextCol; + else + col = get_model_col(col); + const GtkInstanceTreeIter& rGtkIter = static_cast<const GtkInstanceTreeIter&>(rIter); + set(rGtkIter.iter, m_aSensitiveMap[col], bSensitive); + } + + void set_image(const GtkTreeIter& iter, int col, GdkPixbuf* pixbuf) + { + if (col == -1) + col = m_nExpanderImageCol; + else + col = get_model_col(col); + gtk_tree_store_set(m_pTreeStore, const_cast<GtkTreeIter*>(&iter), col, pixbuf, -1); + if (pixbuf) + g_object_unref(pixbuf); + } + + void set_image(int pos, GdkPixbuf* pixbuf, int col) + { + GtkTreeModel *pModel = GTK_TREE_MODEL(m_pTreeStore); + GtkTreeIter iter; + if (gtk_tree_model_iter_nth_child(pModel, &iter, nullptr, pos)) + { + set_image(iter, col, pixbuf); + } + } + + virtual void set_image(int pos, const css::uno::Reference<css::graphic::XGraphic>& rImage, int col) override + { + set_image(pos, getPixbuf(rImage), col); + } + + virtual void set_image(int pos, const OUString& rImage, int col) override + { + set_image(pos, getPixbuf(rImage), col); + } + + virtual void set_image(int pos, VirtualDevice& rImage, int col) override + { + set_image(pos, getPixbuf(rImage), col); + } + + virtual void set_image(const weld::TreeIter& rIter, const css::uno::Reference<css::graphic::XGraphic>& rImage, int col) override + { + const GtkInstanceTreeIter& rGtkIter = static_cast<const GtkInstanceTreeIter&>(rIter); + set_image(rGtkIter.iter, col, getPixbuf(rImage)); + } + + virtual void set_image(const weld::TreeIter& rIter, const OUString& rImage, int col) override + { + const GtkInstanceTreeIter& rGtkIter = static_cast<const GtkInstanceTreeIter&>(rIter); + set_image(rGtkIter.iter, col, getPixbuf(rImage)); + } + + virtual void set_image(const weld::TreeIter& rIter, VirtualDevice& rImage, int col) override + { + const GtkInstanceTreeIter& rGtkIter = static_cast<const GtkInstanceTreeIter&>(rIter); + set_image(rGtkIter.iter, col, getPixbuf(rImage)); + } + + virtual OUString get_id(int pos) const override + { + return get(pos, m_nIdCol); + } + + virtual void set_id(int pos, const OUString& rId) override + { + return set(pos, m_nIdCol, rId); + } + + virtual int get_iter_index_in_parent(const weld::TreeIter& rIter) const override + { + const GtkInstanceTreeIter& rGtkIter = static_cast<const GtkInstanceTreeIter&>(rIter); + + GtkTreeModel *pModel = GTK_TREE_MODEL(m_pTreeStore); + GtkTreePath* path = gtk_tree_model_get_path(pModel, const_cast<GtkTreeIter*>(&rGtkIter.iter)); + + gint depth; + gint* indices = gtk_tree_path_get_indices_with_depth(path, &depth); + int nRet = indices[depth-1]; + + gtk_tree_path_free(path); + + return nRet; + } + + virtual int iter_compare(const weld::TreeIter& a, const weld::TreeIter& b) const override + { + const GtkInstanceTreeIter& rGtkIterA = static_cast<const GtkInstanceTreeIter&>(a); + const GtkInstanceTreeIter& rGtkIterB = static_cast<const GtkInstanceTreeIter&>(b); + + GtkTreeModel *pModel = GTK_TREE_MODEL(m_pTreeStore); + GtkTreePath* pathA = gtk_tree_model_get_path(pModel, const_cast<GtkTreeIter*>(&rGtkIterA.iter)); + GtkTreePath* pathB = gtk_tree_model_get_path(pModel, const_cast<GtkTreeIter*>(&rGtkIterB.iter)); + + int nRet = gtk_tree_path_compare(pathA, pathB); + + gtk_tree_path_free(pathB); + gtk_tree_path_free(pathA); + + return nRet; + } + + // by copy and delete of old copy + void move_subtree(GtkTreeIter& rFromIter, GtkTreeIter* pGtkParentIter, int nIndexInNewParent) + { + GtkTreeModel *pModel = GTK_TREE_MODEL(m_pTreeStore); + + int nCols = gtk_tree_model_get_n_columns(pModel); + GValue value; + + GtkTreeIter toiter; + gtk_tree_store_insert(m_pTreeStore, &toiter, pGtkParentIter, nIndexInNewParent); + + for (int i = 0; i < nCols; ++i) + { + memset(&value, 0, sizeof(GValue)); + gtk_tree_model_get_value(pModel, &rFromIter, i, &value); + gtk_tree_store_set_value(m_pTreeStore, &toiter, i, &value); + g_value_unset(&value); + } + + GtkTreeIter tmpfromiter; + if (gtk_tree_model_iter_children(pModel, &tmpfromiter, &rFromIter)) + { + int j = 0; + do + { + move_subtree(tmpfromiter, &toiter, j++); + } while (gtk_tree_model_iter_next(pModel, &tmpfromiter)); + } + + gtk_tree_store_remove(m_pTreeStore, &rFromIter); + } + + virtual void move_subtree(weld::TreeIter& rNode, const weld::TreeIter* pNewParent, int nIndexInNewParent) override + { + GtkInstanceTreeIter& rGtkIter = static_cast<GtkInstanceTreeIter&>(rNode); + const GtkInstanceTreeIter* pGtkParentIter = static_cast<const GtkInstanceTreeIter*>(pNewParent); + move_subtree(rGtkIter.iter, pGtkParentIter ? const_cast<GtkTreeIter*>(&pGtkParentIter->iter) : nullptr, nIndexInNewParent); + } + + virtual int get_selected_index() const override + { + assert(gtk_tree_view_get_model(m_pTreeView) && "don't request selection when frozen"); + int nRet = -1; + GtkTreeSelection *selection = gtk_tree_view_get_selection(m_pTreeView); + if (gtk_tree_selection_get_mode(selection) != GTK_SELECTION_MULTIPLE) + { + GtkTreeIter iter; + GtkTreeModel* pModel; + if (gtk_tree_selection_get_selected(gtk_tree_view_get_selection(m_pTreeView), &pModel, &iter)) + { + GtkTreePath* path = gtk_tree_model_get_path(pModel, &iter); + + gint depth; + gint* indices = gtk_tree_path_get_indices_with_depth(path, &depth); + nRet = indices[depth-1]; + + gtk_tree_path_free(path); + } + } + else + { + auto vec = get_selected_rows(); + return vec.empty() ? -1 : vec[0]; + } + return nRet; + } + + bool get_selected_iterator(GtkTreeIter* pIter) const + { + assert(gtk_tree_view_get_model(m_pTreeView) && "don't request selection when frozen"); + bool bRet = false; + GtkTreeSelection *selection = gtk_tree_view_get_selection(m_pTreeView); + if (gtk_tree_selection_get_mode(selection) != GTK_SELECTION_MULTIPLE) + bRet = gtk_tree_selection_get_selected(gtk_tree_view_get_selection(m_pTreeView), nullptr, pIter); + else + { + GtkTreeModel* pModel; + GList* pList = gtk_tree_selection_get_selected_rows(gtk_tree_view_get_selection(m_pTreeView), &pModel); + for (GList* pItem = g_list_first(pList); pItem; pItem = g_list_next(pItem)) + { + if (pIter) + { + GtkTreePath* path = static_cast<GtkTreePath*>(pItem->data); + gtk_tree_model_get_iter(pModel, pIter, path); + } + bRet = true; + break; + } + g_list_free_full(pList, reinterpret_cast<GDestroyNotify>(gtk_tree_path_free)); + } + return bRet; + } + + virtual OUString get_selected_text() const override + { + assert(gtk_tree_view_get_model(m_pTreeView) && "don't request selection when frozen"); + GtkTreeIter iter; + if (get_selected_iterator(&iter)) + return get(iter, m_nTextCol); + return OUString(); + } + + virtual OUString get_selected_id() const override + { + assert(gtk_tree_view_get_model(m_pTreeView) && "don't request selection when frozen"); + GtkTreeIter iter; + if (get_selected_iterator(&iter)) + return get(iter, m_nIdCol); + return OUString(); + } + + virtual std::unique_ptr<weld::TreeIter> make_iterator(const weld::TreeIter* pOrig) const override + { + return std::unique_ptr<weld::TreeIter>(new GtkInstanceTreeIter(static_cast<const GtkInstanceTreeIter*>(pOrig))); + } + + virtual void copy_iterator(const weld::TreeIter& rSource, weld::TreeIter& rDest) const override + { + const GtkInstanceTreeIter& rGtkSource(static_cast<const GtkInstanceTreeIter&>(rSource)); + GtkInstanceTreeIter& rGtkDest(static_cast<GtkInstanceTreeIter&>(rDest)); + rGtkDest.iter = rGtkSource.iter; + } + + virtual bool get_selected(weld::TreeIter* pIter) const override + { + GtkInstanceTreeIter* pGtkIter = static_cast<GtkInstanceTreeIter*>(pIter); + return get_selected_iterator(pGtkIter ? &pGtkIter->iter : nullptr); + } + + virtual bool get_cursor(weld::TreeIter* pIter) const override + { + GtkInstanceTreeIter* pGtkIter = static_cast<GtkInstanceTreeIter*>(pIter); + GtkTreePath* path; + gtk_tree_view_get_cursor(m_pTreeView, &path, nullptr); + if (pGtkIter && path) + { + GtkTreeModel *pModel = GTK_TREE_MODEL(m_pTreeStore); + gtk_tree_model_get_iter(pModel, &pGtkIter->iter, path); + } + if (!path) + return false; + gtk_tree_path_free(path); + return true; + } + + virtual int get_cursor_index() const override + { + int nRet = -1; + + GtkTreePath* path; + gtk_tree_view_get_cursor(m_pTreeView, &path, nullptr); + if (path) + { + gint depth; + gint* indices = gtk_tree_path_get_indices_with_depth(path, &depth); + nRet = indices[depth-1]; + gtk_tree_path_free(path); + } + + return nRet; + } + + virtual void set_cursor(const weld::TreeIter& rIter) override + { + disable_notify_events(); + const GtkInstanceTreeIter& rGtkIter = static_cast<const GtkInstanceTreeIter&>(rIter); + GtkTreeModel *pModel = GTK_TREE_MODEL(m_pTreeStore); + GtkTreeIter Iter; + if (gtk_tree_model_iter_parent(pModel, &Iter, const_cast<GtkTreeIter*>(&rGtkIter.iter))) + { + GtkTreePath* path = gtk_tree_model_get_path(pModel, &Iter); + if (!gtk_tree_view_row_expanded(m_pTreeView, path)) + gtk_tree_view_expand_to_path(m_pTreeView, path); + gtk_tree_path_free(path); + } + GtkTreePath* path = gtk_tree_model_get_path(pModel, const_cast<GtkTreeIter*>(&rGtkIter.iter)); + gtk_tree_view_scroll_to_cell(m_pTreeView, path, nullptr, false, 0, 0); + gtk_tree_view_set_cursor(m_pTreeView, path, nullptr, false); + gtk_tree_path_free(path); + enable_notify_events(); + } + + virtual bool get_iter_first(weld::TreeIter& rIter) const override + { + GtkInstanceTreeIter& rGtkIter = static_cast<GtkInstanceTreeIter&>(rIter); + GtkTreeModel *pModel = GTK_TREE_MODEL(m_pTreeStore); + return gtk_tree_model_get_iter_first(pModel, &rGtkIter.iter); + } + + virtual bool iter_next_sibling(weld::TreeIter& rIter) const override + { + GtkInstanceTreeIter& rGtkIter = static_cast<GtkInstanceTreeIter&>(rIter); + GtkTreeModel *pModel = GTK_TREE_MODEL(m_pTreeStore); + return gtk_tree_model_iter_next(pModel, &rGtkIter.iter); + } + + virtual bool iter_previous_sibling(weld::TreeIter& rIter) const override + { + GtkInstanceTreeIter& rGtkIter = static_cast<GtkInstanceTreeIter&>(rIter); + GtkTreeModel *pModel = GTK_TREE_MODEL(m_pTreeStore); + return gtk_tree_model_iter_previous(pModel, &rGtkIter.iter); + } + + virtual bool iter_next(weld::TreeIter& rIter) const override + { + return iter_next(rIter, false); + } + + virtual bool iter_previous(weld::TreeIter& rIter) const override + { + bool ret = false; + GtkInstanceTreeIter& rGtkIter = static_cast<GtkInstanceTreeIter&>(rIter); + GtkTreeModel *pModel = GTK_TREE_MODEL(m_pTreeStore); + GtkTreeIter iter = rGtkIter.iter; + GtkTreeIter tmp = iter; + if (gtk_tree_model_iter_previous(pModel, &tmp)) + { + // Move down level(s) until we find the level where the last node exists. + int nChildren = gtk_tree_model_iter_n_children(pModel, &tmp); + if (!nChildren) + rGtkIter.iter = tmp; + else + last_child(pModel, &rGtkIter.iter, &tmp, nChildren); + ret = true; + } + else + { + // Move up level + if (gtk_tree_model_iter_parent(pModel, &tmp, &iter)) + { + rGtkIter.iter = tmp; + ret = true; + } + } + + if (ret) + { + //on-demand dummy entry doesn't count + if (get_text(rGtkIter, -1) == "<dummy>") + return iter_previous(rGtkIter); + return true; + } + + return false; + } + + virtual bool iter_next_visible(weld::TreeIter& rIter) const override + { + return iter_next(rIter, true); + } + + virtual bool iter_children(weld::TreeIter& rIter) const override + { + GtkInstanceTreeIter& rGtkIter = static_cast<GtkInstanceTreeIter&>(rIter); + GtkTreeModel *pModel = GTK_TREE_MODEL(m_pTreeStore); + GtkTreeIter tmp; + bool ret = gtk_tree_model_iter_children(pModel, &tmp, &rGtkIter.iter); + rGtkIter.iter = tmp; + if (ret) + { + //on-demand dummy entry doesn't count + return get_text(rGtkIter, -1) != "<dummy>"; + } + return ret; + } + + virtual bool iter_parent(weld::TreeIter& rIter) const override + { + GtkInstanceTreeIter& rGtkIter = static_cast<GtkInstanceTreeIter&>(rIter); + GtkTreeModel *pModel = GTK_TREE_MODEL(m_pTreeStore); + GtkTreeIter tmp; + auto ret = gtk_tree_model_iter_parent(pModel, &tmp, &rGtkIter.iter); + rGtkIter.iter = tmp; + return ret; + } + + virtual void remove(const weld::TreeIter& rIter) override + { + disable_notify_events(); + const GtkInstanceTreeIter& rGtkIter = static_cast<const GtkInstanceTreeIter&>(rIter); + gtk_tree_store_remove(m_pTreeStore, const_cast<GtkTreeIter*>(&rGtkIter.iter)); + enable_notify_events(); + } + + virtual void remove_selection() override + { + disable_notify_events(); + + std::vector<GtkTreeIter> aIters; + GtkTreeModel* pModel; + GList* pList = gtk_tree_selection_get_selected_rows(gtk_tree_view_get_selection(m_pTreeView), &pModel); + for (GList* pItem = g_list_first(pList); pItem; pItem = g_list_next(pItem)) + { + GtkTreePath* path = static_cast<GtkTreePath*>(pItem->data); + aIters.emplace_back(); + gtk_tree_model_get_iter(pModel, &aIters.back(), path); + } + g_list_free_full(pList, reinterpret_cast<GDestroyNotify>(gtk_tree_path_free)); + + for (auto& iter : aIters) + gtk_tree_store_remove(m_pTreeStore, &iter); + + enable_notify_events(); + } + + virtual void select(const weld::TreeIter& rIter) override + { + assert(gtk_tree_view_get_model(m_pTreeView) && "don't select when frozen, select after thaw. Note selection doesn't survive a freeze"); + disable_notify_events(); + const GtkInstanceTreeIter& rGtkIter = static_cast<const GtkInstanceTreeIter&>(rIter); + gtk_tree_selection_select_iter(gtk_tree_view_get_selection(m_pTreeView), const_cast<GtkTreeIter*>(&rGtkIter.iter)); + enable_notify_events(); + } + + virtual void scroll_to_row(const weld::TreeIter& rIter) override + { + assert(gtk_tree_view_get_model(m_pTreeView) && "don't select when frozen, select after thaw. Note selection doesn't survive a freeze"); + disable_notify_events(); + const GtkInstanceTreeIter& rGtkIter = static_cast<const GtkInstanceTreeIter&>(rIter); + GtkTreeModel *pModel = GTK_TREE_MODEL(m_pTreeStore); + GtkTreePath* path = gtk_tree_model_get_path(pModel, const_cast<GtkTreeIter*>(&rGtkIter.iter)); + gtk_tree_view_expand_to_path(m_pTreeView, path); + gtk_tree_view_scroll_to_cell(m_pTreeView, path, nullptr, false, 0, 0); + gtk_tree_path_free(path); + enable_notify_events(); + } + + virtual void unselect(const weld::TreeIter& rIter) override + { + assert(gtk_tree_view_get_model(m_pTreeView) && "don't select when frozen, select after thaw. Note selection doesn't survive a freeze"); + disable_notify_events(); + const GtkInstanceTreeIter& rGtkIter = static_cast<const GtkInstanceTreeIter&>(rIter); + gtk_tree_selection_unselect_iter(gtk_tree_view_get_selection(m_pTreeView), const_cast<GtkTreeIter*>(&rGtkIter.iter)); + enable_notify_events(); + } + + virtual int get_iter_depth(const weld::TreeIter& rIter) const override + { + const GtkInstanceTreeIter& rGtkIter = static_cast<const GtkInstanceTreeIter&>(rIter); + GtkTreeModel *pModel = GTK_TREE_MODEL(m_pTreeStore); + GtkTreePath* path = gtk_tree_model_get_path(pModel, const_cast<GtkTreeIter*>(&rGtkIter.iter)); + int ret = gtk_tree_path_get_depth(path) - 1; + gtk_tree_path_free(path); + return ret; + } + + virtual bool iter_has_child(const weld::TreeIter& rIter) const override + { + weld::TreeIter& rNonConstIter = const_cast<weld::TreeIter&>(rIter); + GtkInstanceTreeIter& rGtkIter = static_cast<GtkInstanceTreeIter&>(rNonConstIter); + GtkTreeIter restore(rGtkIter.iter); + bool ret = iter_children(rNonConstIter); + rGtkIter.iter = restore; + return ret; + } + + virtual bool get_row_expanded(const weld::TreeIter& rIter) const override + { + const GtkInstanceTreeIter& rGtkIter = static_cast<const GtkInstanceTreeIter&>(rIter); + GtkTreeModel *pModel = GTK_TREE_MODEL(m_pTreeStore); + GtkTreePath* path = gtk_tree_model_get_path(pModel, const_cast<GtkTreeIter*>(&rGtkIter.iter)); + bool ret = gtk_tree_view_row_expanded(m_pTreeView, path); + gtk_tree_path_free(path); + return ret; + } + + virtual bool get_children_on_demand(const weld::TreeIter& rIter) const override + { + const GtkInstanceTreeIter& rGtkIter = static_cast<const GtkInstanceTreeIter&>(rIter); + GtkInstanceTreeIter aIter(&rGtkIter); + return child_is_placeholder(aIter); + } + + virtual void set_children_on_demand(const weld::TreeIter& rIter, bool bChildrenOnDemand) override + { + disable_notify_events(); + + const GtkInstanceTreeIter& rGtkIter = static_cast<const GtkInstanceTreeIter&>(rIter); + GtkInstanceTreeIter aPlaceHolderIter(&rGtkIter); + + bool bPlaceHolder = child_is_placeholder(aPlaceHolderIter); + + if (bChildrenOnDemand && !bPlaceHolder) + { + GtkTreeIter subiter; + OUString sDummy("<dummy>"); + insert_row(subiter, &rGtkIter.iter, -1, nullptr, &sDummy, nullptr, nullptr, nullptr); + } + else if (!bChildrenOnDemand && bPlaceHolder) + remove(aPlaceHolderIter); + + enable_notify_events(); + } + + virtual void expand_row(const weld::TreeIter& rIter) override + { + assert(gtk_tree_view_get_model(m_pTreeView) && "don't expand when frozen"); + + const GtkInstanceTreeIter& rGtkIter = static_cast<const GtkInstanceTreeIter&>(rIter); + GtkTreeModel *pModel = GTK_TREE_MODEL(m_pTreeStore); + GtkTreePath* path = gtk_tree_model_get_path(pModel, const_cast<GtkTreeIter*>(&rGtkIter.iter)); + if (!gtk_tree_view_row_expanded(m_pTreeView, path)) + gtk_tree_view_expand_to_path(m_pTreeView, path); + gtk_tree_path_free(path); + } + + virtual void collapse_row(const weld::TreeIter& rIter) override + { + const GtkInstanceTreeIter& rGtkIter = static_cast<const GtkInstanceTreeIter&>(rIter); + GtkTreeModel *pModel = GTK_TREE_MODEL(m_pTreeStore); + GtkTreePath* path = gtk_tree_model_get_path(pModel, const_cast<GtkTreeIter*>(&rGtkIter.iter)); + if (gtk_tree_view_row_expanded(m_pTreeView, path)) + gtk_tree_view_collapse_row(m_pTreeView, path); + gtk_tree_path_free(path); + } + + virtual OUString get_text(const weld::TreeIter& rIter, int col) const override + { + const GtkInstanceTreeIter& rGtkIter = static_cast<const GtkInstanceTreeIter&>(rIter); + if (col == -1) + col = m_nTextCol; + else + col = get_model_col(col); + return get(rGtkIter.iter, col); + } + + virtual void set_text(const weld::TreeIter& rIter, const OUString& rText, int col) override + { + const GtkInstanceTreeIter& rGtkIter = static_cast<const GtkInstanceTreeIter&>(rIter); + if (col == -1) + col = m_nTextCol; + else + col = get_model_col(col); + set(rGtkIter.iter, col, rText); + } + + virtual OUString get_id(const weld::TreeIter& rIter) const override + { + const GtkInstanceTreeIter& rGtkIter = static_cast<const GtkInstanceTreeIter&>(rIter); + return get(rGtkIter.iter, m_nIdCol); + } + + virtual void set_id(const weld::TreeIter& rIter, const OUString& rId) override + { + const GtkInstanceTreeIter& rGtkIter = static_cast<const GtkInstanceTreeIter&>(rIter); + set(rGtkIter.iter, m_nIdCol, rId); + } + + virtual void freeze() override + { + disable_notify_events(); + g_object_ref(m_pTreeStore); + GtkInstanceContainer::freeze(); + gtk_tree_view_set_model(m_pTreeView, nullptr); + if (m_xSorter) + { + int nSortColumn; + GtkSortType eSortType; + GtkTreeSortable* pSortable = GTK_TREE_SORTABLE(m_pTreeStore); + gtk_tree_sortable_get_sort_column_id(pSortable, &nSortColumn, &eSortType); + gtk_tree_sortable_set_sort_column_id(pSortable, GTK_TREE_SORTABLE_UNSORTED_SORT_COLUMN_ID, eSortType); + + m_aSavedSortColumns.push_back(nSortColumn); + m_aSavedSortTypes.push_back(eSortType); + } + enable_notify_events(); + } + + virtual void thaw() override + { + disable_notify_events(); + if (m_xSorter) + { + GtkTreeSortable* pSortable = GTK_TREE_SORTABLE(m_pTreeStore); + gtk_tree_sortable_set_sort_column_id(pSortable, m_aSavedSortColumns.back(), m_aSavedSortTypes.back()); + m_aSavedSortTypes.pop_back(); + m_aSavedSortColumns.pop_back(); + } + gtk_tree_view_set_model(m_pTreeView, GTK_TREE_MODEL(m_pTreeStore)); + GtkInstanceContainer::thaw(); + g_object_unref(m_pTreeStore); + enable_notify_events(); + } + + virtual int get_height_rows(int nRows) const override + { + return ::get_height_rows(m_pTreeView, m_pColumns, nRows); + } + + virtual Size get_size_request() const override + { + GtkWidget* pParent = gtk_widget_get_parent(m_pWidget); + if (GTK_IS_SCROLLED_WINDOW(pParent)) + { + return Size(gtk_scrolled_window_get_min_content_width(GTK_SCROLLED_WINDOW(pParent)), + gtk_scrolled_window_get_min_content_height(GTK_SCROLLED_WINDOW(pParent))); + } + int nWidth, nHeight; + gtk_widget_get_size_request(m_pWidget, &nWidth, &nHeight); + return Size(nWidth, nHeight); + } + + virtual Size get_preferred_size() const override + { + Size aRet(-1, -1); + GtkWidget* pParent = gtk_widget_get_parent(m_pWidget); + if (GTK_IS_SCROLLED_WINDOW(pParent)) + { + aRet = Size(gtk_scrolled_window_get_min_content_width(GTK_SCROLLED_WINDOW(pParent)), + gtk_scrolled_window_get_min_content_height(GTK_SCROLLED_WINDOW(pParent))); + } + GtkRequisition size; + gtk_widget_get_preferred_size(m_pWidget, nullptr, &size); + if (aRet.Width() == -1) + aRet.setWidth(size.width); + if (aRet.Height() == -1) + aRet.setHeight(size.height); + return aRet; + } + + virtual void show() override + { + GtkWidget* pParent = gtk_widget_get_parent(m_pWidget); + if (GTK_IS_SCROLLED_WINDOW(pParent)) + gtk_widget_show(pParent); + gtk_widget_show(m_pWidget); + } + + virtual void hide() override + { + GtkWidget* pParent = gtk_widget_get_parent(m_pWidget); + if (GTK_IS_SCROLLED_WINDOW(pParent)) + gtk_widget_hide(pParent); + gtk_widget_hide(m_pWidget); + } + + virtual void enable_drag_source(rtl::Reference<TransferDataContainer>& rHelper, sal_uInt8 eDNDConstants) override + { + do_enable_drag_source(rHelper, eDNDConstants); + } + + virtual void drag_source_set(const std::vector<GtkTargetEntry>& rGtkTargets, GdkDragAction eDragAction) override + { + gtk_tree_view_enable_model_drag_source(m_pTreeView, GDK_BUTTON1_MASK, rGtkTargets.data(), rGtkTargets.size(), eDragAction); + } + + virtual void set_selection_mode(SelectionMode eMode) override + { + disable_notify_events(); + gtk_tree_selection_set_mode(gtk_tree_view_get_selection(m_pTreeView), VclToGtk(eMode)); + enable_notify_events(); + } + + virtual int count_selected_rows() const override + { + return gtk_tree_selection_count_selected_rows(gtk_tree_view_get_selection(m_pTreeView)); + } + + int starts_with(const OUString& rStr, int col, int nStartRow, bool bCaseSensitive) + { + return ::starts_with(GTK_TREE_MODEL(m_pTreeStore), rStr, get_model_col(col), nStartRow, bCaseSensitive); + } + + virtual void disable_notify_events() override + { + g_signal_handler_block(gtk_tree_view_get_selection(m_pTreeView), m_nChangedSignalId); + g_signal_handler_block(m_pTreeView, m_nRowActivatedSignalId); + + GtkTreeModel *pModel = GTK_TREE_MODEL(m_pTreeStore); + g_signal_handler_block(pModel, m_nRowDeletedSignalId); + g_signal_handler_block(pModel, m_nRowInsertedSignalId); + + GtkInstanceContainer::disable_notify_events(); + } + + virtual void enable_notify_events() override + { + GtkInstanceContainer::enable_notify_events(); + + GtkTreeModel *pModel = GTK_TREE_MODEL(m_pTreeStore); + g_signal_handler_unblock(pModel, m_nRowDeletedSignalId); + g_signal_handler_unblock(pModel, m_nRowInsertedSignalId); + + g_signal_handler_unblock(m_pTreeView, m_nRowActivatedSignalId); + g_signal_handler_unblock(gtk_tree_view_get_selection(m_pTreeView), m_nChangedSignalId); + } + + virtual void connect_popup_menu(const Link<const CommandEvent&, bool>& rLink) override + { + ensureButtonPressSignal(); + weld::TreeView::connect_popup_menu(rLink); + } + + virtual bool get_dest_row_at_pos(const Point &rPos, weld::TreeIter* pResult, bool bHighLightTarget) override + { + const bool bAsTree = gtk_tree_view_get_enable_tree_lines(m_pTreeView); + + // to keep it simple we'll default to always drop before the current row + // except for the special edge cases + GtkTreeViewDropPosition pos = bAsTree ? GTK_TREE_VIEW_DROP_INTO_OR_BEFORE : GTK_TREE_VIEW_DROP_BEFORE; + + // unhighlight current highlighted row + gtk_tree_view_set_drag_dest_row(m_pTreeView, nullptr, pos); + + if (m_bWorkAroundBadDragRegion) + gtk_drag_unhighlight(GTK_WIDGET(m_pTreeView)); + + GtkTreePath *path = nullptr; + GtkTreeViewDropPosition gtkpos = bAsTree ? GTK_TREE_VIEW_DROP_INTO_OR_BEFORE : GTK_TREE_VIEW_DROP_BEFORE; + bool ret = gtk_tree_view_get_dest_row_at_pos(m_pTreeView, rPos.X(), rPos.Y(), + &path, >kpos); + + // find the last entry in the model for comparison + GtkTreeModel *pModel = GTK_TREE_MODEL(m_pTreeStore); + GtkTreePath *lastpath = get_path_of_last_entry(pModel); + + if (!ret) + { + // empty space, draw an indicator at the last entry + assert(!path); + path = gtk_tree_path_copy(lastpath); + pos = GTK_TREE_VIEW_DROP_AFTER; + } + else if (gtk_tree_path_compare(path, lastpath) == 0) + { + // if we're on the last entry, see if gtk thinks + // the drop should be before or after it, and if + // its after, treat it like a drop into empty + // space, i.e. append it + if (gtkpos == GTK_TREE_VIEW_DROP_AFTER || + gtkpos == GTK_TREE_VIEW_DROP_INTO_OR_AFTER) + { + ret = false; + pos = bAsTree ? gtkpos : GTK_TREE_VIEW_DROP_AFTER; + } + } + + if (ret && pResult) + { + GtkInstanceTreeIter& rGtkIter = static_cast<GtkInstanceTreeIter&>(*pResult); + gtk_tree_model_get_iter(pModel, &rGtkIter.iter, path); + } + + if (m_bInDrag && bHighLightTarget) // bHighLightTarget alone might be sufficient + { + // highlight the row + gtk_tree_view_set_drag_dest_row(m_pTreeView, path, pos); + } + + assert(path); + gtk_tree_path_free(path); + gtk_tree_path_free(lastpath); + + // auto scroll if we're close to the edges + GtkAdjustment* pVAdjustment = gtk_scrollable_get_vadjustment(GTK_SCROLLABLE(m_pTreeView)); + double fStep = gtk_adjustment_get_step_increment(pVAdjustment); + if (rPos.Y() < fStep) + { + double fValue = gtk_adjustment_get_value(pVAdjustment) - fStep; + if (fValue < 0) + fValue = 0.0; + gtk_adjustment_set_value(pVAdjustment, fValue); + } + else + { + GdkRectangle aRect; + gtk_tree_view_get_visible_rect(m_pTreeView, &aRect); + if (rPos.Y() > aRect.height - fStep) + { + double fValue = gtk_adjustment_get_value(pVAdjustment) + fStep; + double fMax = gtk_adjustment_get_upper(pVAdjustment); + if (fValue > fMax) + fValue = fMax; + gtk_adjustment_set_value(pVAdjustment, fValue); + } + } + + return ret; + } + + virtual void unset_drag_dest_row() override + { + gtk_tree_view_set_drag_dest_row(m_pTreeView, nullptr, GTK_TREE_VIEW_DROP_BEFORE); + } + + virtual tools::Rectangle get_row_area(const weld::TreeIter& rIter) const override + { + const GtkInstanceTreeIter& rGtkIter = static_cast<const GtkInstanceTreeIter&>(rIter); + GtkTreeModel* pModel = GTK_TREE_MODEL(m_pTreeStore); + GtkTreePath* pPath = gtk_tree_model_get_path(pModel, const_cast<GtkTreeIter*>(&rGtkIter.iter)); + tools::Rectangle aRet = ::get_row_area(m_pTreeView, m_pColumns, pPath); + gtk_tree_path_free(pPath); + return aRet; + } + + virtual void start_editing(const weld::TreeIter& rIter) override + { + int col = get_view_col(m_nTextCol); + GtkTreeViewColumn* pColumn = GTK_TREE_VIEW_COLUMN(g_list_nth_data(m_pColumns, col)); + assert(pColumn && "wrong column"); + + const GtkInstanceTreeIter& rGtkIter = static_cast<const GtkInstanceTreeIter&>(rIter); + GtkTreeModel *pModel = GTK_TREE_MODEL(m_pTreeStore); + GtkTreePath* path = gtk_tree_model_get_path(pModel, const_cast<GtkTreeIter*>(&rGtkIter.iter)); + + // allow editing of cells which are not usually editable, so we can have double click + // do its usual row-activate but if we explicitly want to edit (remote files dialog) + // we can still do that + GList *pRenderers = gtk_cell_layout_get_cells(GTK_CELL_LAYOUT(pColumn)); + for (GList* pRenderer = g_list_first(pRenderers); pRenderer; pRenderer = g_list_next(pRenderer)) + { + GtkCellRenderer* pCellRenderer = GTK_CELL_RENDERER(pRenderer->data); + if (GTK_IS_CELL_RENDERER_TEXT(pCellRenderer)) + { + gboolean is_editable(false); + g_object_get(pCellRenderer, "editable", &is_editable, nullptr); + if (!is_editable) + { + g_object_set(pCellRenderer, "editable", true, "editable-set", true, nullptr); + g_object_set_data(G_OBJECT(pCellRenderer), "g-lo-RestoreNonEditable", reinterpret_cast<gpointer>(true)); + break; + } + } + } + g_list_free(pRenderers); + + gtk_tree_view_scroll_to_cell(m_pTreeView, path, pColumn, false, 0, 0); + gtk_tree_view_set_cursor(m_pTreeView, path, pColumn, true); + + gtk_tree_path_free(path); + } + + virtual void end_editing() override + { + GtkTreeViewColumn *focus_column = nullptr; + gtk_tree_view_get_cursor(m_pTreeView, nullptr, &focus_column); + if (focus_column) + gtk_cell_area_stop_editing(gtk_cell_layout_get_area(GTK_CELL_LAYOUT(focus_column)), true); + } + + virtual TreeView* get_drag_source() const override + { + return g_DragSource; + } + + virtual bool do_signal_drag_begin(bool& rUnsetDragIcon) override + { + if (m_aDragBeginHdl.Call(rUnsetDragIcon)) + return true; + g_DragSource = this; + return false; + } + + virtual void do_signal_drag_end() override + { + g_DragSource = nullptr; + } + + // Under gtk 3.24.8 dragging into the TreeView is not highlighting + // entire TreeView widget, just the rectangle which has no entries + // in it, so as a workaround highlight the parent container + // on drag start, and undo it on drag end, and trigger removal + // of the treeview's highlight effort + virtual void drag_started() override + { + m_bInDrag = true; + GtkWidget* pWidget = GTK_WIDGET(m_pTreeView); + GtkWidget* pParent = gtk_widget_get_parent(pWidget); + if (GTK_IS_SCROLLED_WINDOW(pParent)) + { + gtk_drag_unhighlight(pWidget); + gtk_drag_highlight(pParent); + m_bWorkAroundBadDragRegion = true; + } + } + + virtual void drag_ended() override + { + m_bInDrag = false; + if (m_bWorkAroundBadDragRegion) + { + GtkWidget* pWidget = GTK_WIDGET(m_pTreeView); + GtkWidget* pParent = gtk_widget_get_parent(pWidget); + gtk_drag_unhighlight(pParent); + m_bWorkAroundBadDragRegion = false; + } + // unhighlight the row + gtk_tree_view_set_drag_dest_row(m_pTreeView, nullptr, GTK_TREE_VIEW_DROP_BEFORE); + } + + virtual int vadjustment_get_value() const override + { + if (m_nPendingVAdjustment != -1) + return m_nPendingVAdjustment; + return gtk_adjustment_get_value(m_pVAdjustment); + } + + virtual void vadjustment_set_value(int value) override + { + disable_notify_events(); + + /* This rube goldberg device is to remove flicker from setting the + scroll position of a GtkTreeView directly after clearing it and + filling it. As a specific example the writer navigator with ~100 + tables, scroll to the end, right click on an entry near the end + and rename it, the tree is cleared and refilled and an attempt + made to set the scroll position of the freshly refilled tree to + the same point as before the clear. + */ + + // This forces the tree to recalculate now its preferred size + // after being cleared + GtkRequisition size; + gtk_widget_get_preferred_size(GTK_WIDGET(m_pTreeView), nullptr, &size); + + m_nPendingVAdjustment = value; + + // The value set here just has to be different to the final value + // set later so that isn't a no-op + gtk_adjustment_set_value(m_pVAdjustment, value - 0.0001); + + // This will set the desired m_nPendingVAdjustment value right + // before the tree gets drawn + gtk_widget_add_tick_callback(GTK_WIDGET(m_pTreeView), setAdjustmentCallback, this, nullptr); + + enable_notify_events(); + } + + void call_signal_custom_render(VirtualDevice& rOutput, const tools::Rectangle& rRect, bool bSelected, const OUString& rId) + { + signal_custom_render(rOutput, rRect, bSelected, rId); + } + + Size call_signal_custom_get_size(VirtualDevice& rOutput, const OUString& rId) + { + return signal_custom_get_size(rOutput, rId); + } + + virtual ~GtkInstanceTreeView() override + { + if (m_pChangeEvent) + Application::RemoveUserEvent(m_pChangeEvent); + if (m_nQueryTooltipSignalId) + g_signal_handler_disconnect(m_pTreeView, m_nQueryTooltipSignalId); + g_signal_handler_disconnect(m_pTreeView, m_nKeyPressSignalId); + g_signal_handler_disconnect(m_pTreeView, m_nPopupMenuSignalId); + GtkTreeModel *pModel = GTK_TREE_MODEL(m_pTreeStore); + g_signal_handler_disconnect(pModel, m_nRowDeletedSignalId); + g_signal_handler_disconnect(pModel, m_nRowInsertedSignalId); + + if (m_nVAdjustmentChangedSignalId) + { + GtkAdjustment* pVAdjustment = gtk_scrollable_get_vadjustment(GTK_SCROLLABLE(m_pTreeView)); + g_signal_handler_disconnect(pVAdjustment, m_nVAdjustmentChangedSignalId); + } + + g_signal_handler_disconnect(m_pTreeView, m_nTestCollapseRowSignalId); + g_signal_handler_disconnect(m_pTreeView, m_nTestExpandRowSignalId); + g_signal_handler_disconnect(m_pTreeView, m_nRowActivatedSignalId); + g_signal_handler_disconnect(gtk_tree_view_get_selection(m_pTreeView), m_nChangedSignalId); + + GValue value = G_VALUE_INIT; + g_value_init(&value, G_TYPE_POINTER); + g_value_set_pointer(&value, static_cast<gpointer>(nullptr)); + + for (GList* pEntry = g_list_last(m_pColumns); pEntry; pEntry = g_list_previous(pEntry)) + { + GtkTreeViewColumn* pColumn = GTK_TREE_VIEW_COLUMN(pEntry->data); + g_signal_handler_disconnect(pColumn, m_aColumnSignalIds.back()); + m_aColumnSignalIds.pop_back(); + + // unset "instance" to avoid dangling "instance" points in any CustomCellRenderers + GList *pRenderers = gtk_cell_layout_get_cells(GTK_CELL_LAYOUT(pColumn)); + for (GList* pRenderer = g_list_first(pRenderers); pRenderer; pRenderer = g_list_next(pRenderer)) + { + GtkCellRenderer* pCellRenderer = GTK_CELL_RENDERER(pRenderer->data); + if (!CUSTOM_IS_CELL_RENDERER_SURFACE(pCellRenderer)) + continue; + g_object_set_property(G_OBJECT(pCellRenderer), "instance", &value); + } + g_list_free(pRenderers); + } + g_list_free(m_pColumns); + } +}; + +void ensure_device(CustomCellRendererSurface *cellsurface, weld::Widget* pWidget) +{ + if (!cellsurface->device) + { + cellsurface->device = VclPtr<VirtualDevice>::Create(); + cellsurface->device->SetBackground(COL_TRANSPARENT); + // expand the point size of the desired font to the equivalent pixel size + if (vcl::Window* pDefaultDevice = dynamic_cast<vcl::Window*>(Application::GetDefaultDevice())) + pDefaultDevice->SetPointFont(*cellsurface->device, pWidget->get_font()); + } +} + +} + +IMPL_LINK_NOARG(GtkInstanceTreeView, async_signal_changed, void*, void) +{ + m_pChangeEvent = nullptr; + signal_changed(); +} + +IMPL_LINK_NOARG(GtkInstanceTreeView, async_stop_cell_editing, void*, void) +{ + end_editing(); +} + +namespace { + +class GtkInstanceIconView : public GtkInstanceContainer, public virtual weld::IconView +{ +private: + GtkIconView* m_pIconView; + GtkTreeStore* m_pTreeStore; + gint m_nTextCol; + gint m_nImageCol; + gint m_nIdCol; + gulong m_nSelectionChangedSignalId; + gulong m_nItemActivatedSignalId; + ImplSVEvent* m_pSelectionChangeEvent; + + DECL_LINK(async_signal_selection_changed, void*, void); + + void launch_signal_selection_changed() + { + //tdf#117991 selection change is sent before the focus change, and focus change + //is what will cause a spinbutton that currently has the focus to set its contents + //as the spin button value. So any LibreOffice callbacks on + //signal-change would happen before the spinbutton value-change occurs. + //To avoid this, send the signal-change to LibreOffice to occur after focus-change + //has been processed + if (m_pSelectionChangeEvent) + Application::RemoveUserEvent(m_pSelectionChangeEvent); + m_pSelectionChangeEvent = Application::PostUserEvent(LINK(this, GtkInstanceIconView, async_signal_selection_changed)); + } + + static void signalSelectionChanged(GtkIconView*, gpointer widget) + { + GtkInstanceIconView* pThis = static_cast<GtkInstanceIconView*>(widget); + pThis->launch_signal_selection_changed(); + } + + void handle_item_activated() + { + if (signal_item_activated()) + return; + } + + static void signalItemActivated(GtkIconView*, GtkTreePath*, gpointer widget) + { + GtkInstanceIconView* pThis = static_cast<GtkInstanceIconView*>(widget); + SolarMutexGuard aGuard; + pThis->handle_item_activated(); + } + + void insert_item(GtkTreeIter& iter, int pos, const OUString* pId, const OUString* pText, const OUString* pIconName) + { + gtk_tree_store_insert_with_values(m_pTreeStore, &iter, nullptr, pos, + m_nTextCol, !pText ? nullptr : OUStringToOString(*pText, RTL_TEXTENCODING_UTF8).getStr(), + m_nIdCol, !pId ? nullptr : OUStringToOString(*pId, RTL_TEXTENCODING_UTF8).getStr(), + -1); + if (pIconName) + { + GdkPixbuf* pixbuf = getPixbuf(*pIconName); + gtk_tree_store_set(m_pTreeStore, &iter, m_nImageCol, pixbuf, -1); + if (pixbuf) + g_object_unref(pixbuf); + } + } + + OUString get(const GtkTreeIter& iter, int col) const + { + GtkTreeModel *pModel = GTK_TREE_MODEL(m_pTreeStore); + gchar* pStr; + gtk_tree_model_get(pModel, const_cast<GtkTreeIter*>(&iter), col, &pStr, -1); + OUString sRet(pStr, pStr ? strlen(pStr) : 0, RTL_TEXTENCODING_UTF8); + g_free(pStr); + return sRet; + } + + bool get_selected_iterator(GtkTreeIter* pIter) const + { + assert(gtk_icon_view_get_model(m_pIconView) && "don't request selection when frozen"); + bool bRet = false; + { + GtkTreeModel* pModel = GTK_TREE_MODEL(m_pTreeStore); + GList* pList = gtk_icon_view_get_selected_items(m_pIconView); + for (GList* pItem = g_list_first(pList); pItem; pItem = g_list_next(pItem)) + { + if (pIter) + { + GtkTreePath* path = static_cast<GtkTreePath*>(pItem->data); + gtk_tree_model_get_iter(pModel, pIter, path); + } + bRet = true; + break; + } + g_list_free_full(pList, reinterpret_cast<GDestroyNotify>(gtk_tree_path_free)); + } + return bRet; + } + +public: + GtkInstanceIconView(GtkIconView* pIconView, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : GtkInstanceContainer(GTK_CONTAINER(pIconView), pBuilder, bTakeOwnership) + , m_pIconView(pIconView) + , m_pTreeStore(GTK_TREE_STORE(gtk_icon_view_get_model(m_pIconView))) + , m_nTextCol(gtk_icon_view_get_text_column(m_pIconView)) + , m_nImageCol(gtk_icon_view_get_pixbuf_column(m_pIconView)) + , m_nSelectionChangedSignalId(g_signal_connect(pIconView, "selection-changed", + G_CALLBACK(signalSelectionChanged), this)) + , m_nItemActivatedSignalId(g_signal_connect(pIconView, "item-activated", G_CALLBACK(signalItemActivated), this)) + , m_pSelectionChangeEvent(nullptr) + { + m_nIdCol = m_nTextCol + 1; + } + + virtual void insert(int pos, const OUString* pText, const OUString* pId, const OUString* pIconName, weld::TreeIter* pRet) override + { + disable_notify_events(); + GtkTreeIter iter; + insert_item(iter, pos, pId, pText, pIconName); + if (pRet) + { + GtkInstanceTreeIter* pGtkRetIter = static_cast<GtkInstanceTreeIter*>(pRet); + pGtkRetIter->iter = iter; + } + enable_notify_events(); + } + + virtual OUString get_selected_id() const override + { + assert(gtk_icon_view_get_model(m_pIconView) && "don't request selection when frozen"); + GtkTreeIter iter; + if (get_selected_iterator(&iter)) + return get(iter, m_nIdCol); + return OUString(); + } + + virtual void clear() override + { + disable_notify_events(); + gtk_tree_store_clear(m_pTreeStore); + enable_notify_events(); + } + + virtual void freeze() override + { + disable_notify_events(); + g_object_ref(m_pTreeStore); + GtkInstanceContainer::freeze(); + gtk_icon_view_set_model(m_pIconView, nullptr); + enable_notify_events(); + } + + virtual void thaw() override + { + disable_notify_events(); + gtk_icon_view_set_model(m_pIconView, GTK_TREE_MODEL(m_pTreeStore)); + GtkInstanceContainer::thaw(); + g_object_unref(m_pTreeStore); + enable_notify_events(); + } + + virtual Size get_size_request() const override + { + GtkWidget* pParent = gtk_widget_get_parent(m_pWidget); + if (GTK_IS_SCROLLED_WINDOW(pParent)) + { + return Size(gtk_scrolled_window_get_min_content_width(GTK_SCROLLED_WINDOW(pParent)), + gtk_scrolled_window_get_min_content_height(GTK_SCROLLED_WINDOW(pParent))); + } + int nWidth, nHeight; + gtk_widget_get_size_request(m_pWidget, &nWidth, &nHeight); + return Size(nWidth, nHeight); + } + + virtual Size get_preferred_size() const override + { + Size aRet(-1, -1); + GtkWidget* pParent = gtk_widget_get_parent(m_pWidget); + if (GTK_IS_SCROLLED_WINDOW(pParent)) + { + aRet = Size(gtk_scrolled_window_get_min_content_width(GTK_SCROLLED_WINDOW(pParent)), + gtk_scrolled_window_get_min_content_height(GTK_SCROLLED_WINDOW(pParent))); + } + GtkRequisition size; + gtk_widget_get_preferred_size(m_pWidget, nullptr, &size); + if (aRet.Width() == -1) + aRet.setWidth(size.width); + if (aRet.Height() == -1) + aRet.setHeight(size.height); + return aRet; + } + + virtual void show() override + { + GtkWidget* pParent = gtk_widget_get_parent(m_pWidget); + if (GTK_IS_SCROLLED_WINDOW(pParent)) + gtk_widget_show(pParent); + gtk_widget_show(m_pWidget); + } + + virtual void hide() override + { + GtkWidget* pParent = gtk_widget_get_parent(m_pWidget); + if (GTK_IS_SCROLLED_WINDOW(pParent)) + gtk_widget_hide(pParent); + gtk_widget_hide(m_pWidget); + } + + virtual OUString get_selected_text() const override + { + assert(gtk_icon_view_get_model(m_pIconView) && "don't request selection when frozen"); + GtkTreeIter iter; + if (get_selected_iterator(&iter)) + return get(iter, m_nTextCol); + return OUString(); + } + + virtual int count_selected_items() const override + { + GList* pList = gtk_icon_view_get_selected_items(m_pIconView); + int nRet = g_list_length(pList); + g_list_free_full(pList, reinterpret_cast<GDestroyNotify>(gtk_tree_path_free)); + return nRet; + } + + virtual void select(int pos) override + { + assert(gtk_icon_view_get_model(m_pIconView) && "don't select when frozen, select after thaw. Note selection doesn't survive a freeze"); + disable_notify_events(); + if (pos == -1 || (pos == 0 && n_children() == 0)) + { + gtk_icon_view_unselect_all(m_pIconView); + } + else + { + GtkTreePath* path = gtk_tree_path_new_from_indices(pos, -1); + gtk_icon_view_select_path(m_pIconView, path); + gtk_icon_view_scroll_to_path(m_pIconView, path, false, 0, 0); + gtk_tree_path_free(path); + } + enable_notify_events(); + } + + virtual void unselect(int pos) override + { + assert(gtk_icon_view_get_model(m_pIconView) && "don't select when frozen, select after thaw. Note selection doesn't survive a freeze"); + disable_notify_events(); + if (pos == -1 || (pos == 0 && n_children() == 0)) + { + gtk_icon_view_select_all(m_pIconView); + } + else + { + GtkTreePath* path = gtk_tree_path_new_from_indices(pos, -1); + gtk_icon_view_select_path(m_pIconView, path); + gtk_tree_path_free(path); + } + enable_notify_events(); + } + + virtual bool get_selected(weld::TreeIter* pIter) const override + { + GtkInstanceTreeIter* pGtkIter = static_cast<GtkInstanceTreeIter*>(pIter); + return get_selected_iterator(pGtkIter ? &pGtkIter->iter : nullptr); + } + + virtual bool get_cursor(weld::TreeIter* pIter) const override + { + GtkInstanceTreeIter* pGtkIter = static_cast<GtkInstanceTreeIter*>(pIter); + GtkTreePath* path; + gtk_icon_view_get_cursor(m_pIconView, &path, nullptr); + if (pGtkIter && path) + { + GtkTreeModel *pModel = GTK_TREE_MODEL(m_pTreeStore); + gtk_tree_model_get_iter(pModel, &pGtkIter->iter, path); + } + return path != nullptr; + } + + virtual void set_cursor(const weld::TreeIter& rIter) override + { + disable_notify_events(); + const GtkInstanceTreeIter& rGtkIter = static_cast<const GtkInstanceTreeIter&>(rIter); + GtkTreeModel *pModel = GTK_TREE_MODEL(m_pTreeStore); + GtkTreePath* path = gtk_tree_model_get_path(pModel, const_cast<GtkTreeIter*>(&rGtkIter.iter)); + gtk_icon_view_set_cursor(m_pIconView, path, nullptr, false); + gtk_tree_path_free(path); + enable_notify_events(); + } + + virtual bool get_iter_first(weld::TreeIter& rIter) const override + { + GtkInstanceTreeIter& rGtkIter = static_cast<GtkInstanceTreeIter&>(rIter); + GtkTreeModel *pModel = GTK_TREE_MODEL(m_pTreeStore); + return gtk_tree_model_get_iter_first(pModel, &rGtkIter.iter); + } + + virtual void scroll_to_item(const weld::TreeIter& rIter) override + { + assert(gtk_icon_view_get_model(m_pIconView) && "don't select when frozen, select after thaw. Note selection doesn't survive a freeze"); + disable_notify_events(); + const GtkInstanceTreeIter& rGtkIter = static_cast<const GtkInstanceTreeIter&>(rIter); + GtkTreeModel *pModel = GTK_TREE_MODEL(m_pTreeStore); + GtkTreePath* path = gtk_tree_model_get_path(pModel, const_cast<GtkTreeIter*>(&rGtkIter.iter)); + gtk_icon_view_scroll_to_path(m_pIconView, path, false, 0, 0); + gtk_tree_path_free(path); + enable_notify_events(); + } + + virtual std::unique_ptr<weld::TreeIter> make_iterator(const weld::TreeIter* pOrig) const override + { + return std::unique_ptr<weld::TreeIter>(new GtkInstanceTreeIter(static_cast<const GtkInstanceTreeIter*>(pOrig))); + } + + virtual void selected_foreach(const std::function<bool(weld::TreeIter&)>& func) override + { + GtkInstanceTreeIter aGtkIter(nullptr); + + GtkTreeModel *pModel = GTK_TREE_MODEL(m_pTreeStore); + GList* pList = gtk_icon_view_get_selected_items(m_pIconView); + for (GList* pItem = g_list_first(pList); pItem; pItem = g_list_next(pItem)) + { + GtkTreePath* path = static_cast<GtkTreePath*>(pItem->data); + gtk_tree_model_get_iter(pModel, &aGtkIter.iter, path); + if (func(aGtkIter)) + break; + } + g_list_free_full(pList, reinterpret_cast<GDestroyNotify>(gtk_tree_path_free)); + } + + virtual int n_children() const override + { + return gtk_tree_model_iter_n_children(GTK_TREE_MODEL(m_pTreeStore), nullptr); + } + + virtual OUString get_id(const weld::TreeIter& rIter) const override + { + const GtkInstanceTreeIter& rGtkIter = static_cast<const GtkInstanceTreeIter&>(rIter); + return get(rGtkIter.iter, m_nIdCol); + } + + virtual void disable_notify_events() override + { + g_signal_handler_block(m_pIconView, m_nSelectionChangedSignalId); + g_signal_handler_block(m_pIconView, m_nItemActivatedSignalId); + + GtkInstanceContainer::disable_notify_events(); + } + + virtual void enable_notify_events() override + { + GtkInstanceContainer::enable_notify_events(); + + g_signal_handler_unblock(m_pIconView, m_nItemActivatedSignalId); + g_signal_handler_unblock(m_pIconView, m_nSelectionChangedSignalId); + } + + virtual ~GtkInstanceIconView() override + { + if (m_pSelectionChangeEvent) + Application::RemoveUserEvent(m_pSelectionChangeEvent); + + g_signal_handler_disconnect(m_pIconView, m_nItemActivatedSignalId); + g_signal_handler_disconnect(m_pIconView, m_nSelectionChangedSignalId); + } +}; + +} + +IMPL_LINK_NOARG(GtkInstanceIconView, async_signal_selection_changed, void*, void) +{ + m_pSelectionChangeEvent = nullptr; + signal_selection_changed(); +} + +namespace { + +class GtkInstanceSpinButton : public GtkInstanceEntry, public virtual weld::SpinButton +{ +private: + GtkSpinButton* m_pButton; + gulong m_nValueChangedSignalId; + gulong m_nOutputSignalId; + gulong m_nInputSignalId; + bool m_bFormatting; + bool m_bBlockOutput; + bool m_bBlank; + + static void signalValueChanged(GtkSpinButton*, gpointer widget) + { + GtkInstanceSpinButton* pThis = static_cast<GtkInstanceSpinButton*>(widget); + SolarMutexGuard aGuard; + pThis->m_bBlank = false; + pThis->signal_value_changed(); + } + + bool guarded_signal_output() + { + if (m_bBlockOutput) + return true; + m_bFormatting = true; + bool bRet = signal_output(); + m_bFormatting = false; + return bRet; + } + + static gboolean signalOutput(GtkSpinButton*, gpointer widget) + { + GtkInstanceSpinButton* pThis = static_cast<GtkInstanceSpinButton*>(widget); + SolarMutexGuard aGuard; + return pThis->guarded_signal_output(); + } + + static gint signalInput(GtkSpinButton*, gdouble* new_value, gpointer widget) + { + GtkInstanceSpinButton* pThis = static_cast<GtkInstanceSpinButton*>(widget); + SolarMutexGuard aGuard; + int result; + TriState eHandled = pThis->signal_input(&result); + if (eHandled == TRISTATE_INDET) + return 0; + if (eHandled == TRISTATE_TRUE) + { + *new_value = pThis->toGtk(result); + return 1; + } + return GTK_INPUT_ERROR; + } + + virtual void signal_activate() override + { + gtk_spin_button_update(m_pButton); + GtkInstanceEntry::signal_activate(); + } + + double toGtk(int nValue) const + { + return static_cast<double>(nValue) / Power10(get_digits()); + } + + int fromGtk(double fValue) const + { + return FRound(fValue * Power10(get_digits())); + } + +public: + GtkInstanceSpinButton(GtkSpinButton* pButton, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : GtkInstanceEntry(GTK_ENTRY(pButton), pBuilder, bTakeOwnership) + , m_pButton(pButton) + , m_nValueChangedSignalId(g_signal_connect(pButton, "value-changed", G_CALLBACK(signalValueChanged), this)) + , m_nOutputSignalId(g_signal_connect(pButton, "output", G_CALLBACK(signalOutput), this)) + , m_nInputSignalId(g_signal_connect(pButton, "input", G_CALLBACK(signalInput), this)) + , m_bFormatting(false) + , m_bBlockOutput(false) + , m_bBlank(false) + { + } + + virtual int get_value() const override + { + return fromGtk(gtk_spin_button_get_value(m_pButton)); + } + + virtual void set_value(int value) override + { + disable_notify_events(); + m_bBlank = false; + gtk_spin_button_set_value(m_pButton, toGtk(value)); + enable_notify_events(); + } + + virtual void set_text(const OUString& rText) override + { + disable_notify_events(); + // tdf#122786 if we're just formatting a value, then we're done, + // however if set_text has been called directly we want to update our + // value from this new text, but don't want to reformat with that value + if (!m_bFormatting) + { + gtk_entry_set_text(GTK_ENTRY(m_pButton), OUStringToOString(rText, RTL_TEXTENCODING_UTF8).getStr()); + + m_bBlockOutput = true; + gtk_spin_button_update(m_pButton); + m_bBlank = rText.isEmpty(); + m_bBlockOutput = false; + } + else + { + bool bKeepBlank = m_bBlank && get_value() == 0; + if (!bKeepBlank) + { + gtk_entry_set_text(GTK_ENTRY(m_pButton), OUStringToOString(rText, RTL_TEXTENCODING_UTF8).getStr()); + m_bBlank = false; + } + } + enable_notify_events(); + } + + virtual void set_range(int min, int max) override + { + disable_notify_events(); + gtk_spin_button_set_range(m_pButton, toGtk(min), toGtk(max)); + enable_notify_events(); + } + + virtual void get_range(int& min, int& max) const override + { + double gtkmin, gtkmax; + gtk_spin_button_get_range(m_pButton, >kmin, >kmax); + min = fromGtk(gtkmin); + max = fromGtk(gtkmax); + } + + virtual void set_increments(int step, int page) override + { + disable_notify_events(); + gtk_spin_button_set_increments(m_pButton, toGtk(step), toGtk(page)); + enable_notify_events(); + } + + virtual void get_increments(int& step, int& page) const override + { + double gtkstep, gtkpage; + gtk_spin_button_get_increments(m_pButton, >kstep, >kpage); + step = fromGtk(gtkstep); + page = fromGtk(gtkpage); + } + + virtual void set_digits(unsigned int digits) override + { + disable_notify_events(); + gtk_spin_button_set_digits(m_pButton, digits); + enable_notify_events(); + } + + virtual unsigned int get_digits() const override + { + return gtk_spin_button_get_digits(m_pButton); + } + + virtual void disable_notify_events() override + { + g_signal_handler_block(m_pButton, m_nValueChangedSignalId); + GtkInstanceEntry::disable_notify_events(); + } + + virtual void enable_notify_events() override + { + GtkInstanceEntry::enable_notify_events(); + g_signal_handler_unblock(m_pButton, m_nValueChangedSignalId); + } + + virtual ~GtkInstanceSpinButton() override + { + g_signal_handler_disconnect(m_pButton, m_nInputSignalId); + g_signal_handler_disconnect(m_pButton, m_nOutputSignalId); + g_signal_handler_disconnect(m_pButton, m_nValueChangedSignalId); + } +}; + +class GtkInstanceFormattedSpinButton : public GtkInstanceEntry, public virtual weld::FormattedSpinButton +{ +private: + GtkSpinButton* m_pButton; + SvNumberFormatter* m_pFormatter; + Color* m_pLastOutputColor; + sal_uInt32 m_nFormatKey; + bool m_bTreatAsNumber; + gulong m_nValueChangedSignalId; + gulong m_nOutputSignalId; + gulong m_nInputSignalId; + + bool signal_output() + { + if (!m_pFormatter) + return false; + double dVal = get_value(); + OUString sNewText; + if (m_pFormatter->IsTextFormat(m_nFormatKey)) + { + // first convert the number as string in standard format + OUString sTemp; + m_pFormatter->GetOutputString(dVal, 0, sTemp, &m_pLastOutputColor); + // then encode the string in the corresponding text format + m_pFormatter->GetOutputString(sTemp, m_nFormatKey, sNewText, &m_pLastOutputColor); + } + else + { + m_pFormatter->GetInputLineString(dVal, m_nFormatKey, sNewText); + } + set_text(sNewText); + return true; + } + + static gboolean signalOutput(GtkSpinButton*, gpointer widget) + { + GtkInstanceFormattedSpinButton* pThis = static_cast<GtkInstanceFormattedSpinButton*>(widget); + SolarMutexGuard aGuard; + return pThis->signal_output(); + } + + gint signal_input(double* value) + { + if (!m_pFormatter) + return 0; + + sal_uInt32 nFormatKey = m_nFormatKey; // IsNumberFormat changes the FormatKey! + + if (m_pFormatter->IsTextFormat(nFormatKey) && m_bTreatAsNumber) + // for detection of values like "1,1" in fields that are formatted as text + nFormatKey = 0; + + OUString sText(get_text()); + + // special treatment for percentage formatting + if (m_pFormatter->GetType(m_nFormatKey) == SvNumFormatType::PERCENT) + { + // the language of our format + LanguageType eLanguage = m_pFormatter->GetEntry(m_nFormatKey)->GetLanguage(); + // the default number format for this language + sal_uLong nStandardNumericFormat = m_pFormatter->GetStandardFormat(SvNumFormatType::NUMBER, eLanguage); + + sal_uInt32 nTempFormat = nStandardNumericFormat; + double dTemp; + if (m_pFormatter->IsNumberFormat(sText, nTempFormat, dTemp) && + SvNumFormatType::NUMBER == m_pFormatter->GetType(nTempFormat)) + // the string is equivalent to a number formatted one (has no % sign) -> append it + sText += "%"; + // (with this, an input of '3' becomes '3%', which then by the formatter is translated + // into 0.03. Without this, the formatter would give us the double 3 for an input '3', + // which equals 300 percent. + } + if (!m_pFormatter->IsNumberFormat(sText, nFormatKey, *value)) + return GTK_INPUT_ERROR; + + return 1; + } + + static gint signalInput(GtkSpinButton*, gdouble* new_value, gpointer widget) + { + GtkInstanceFormattedSpinButton* pThis = static_cast<GtkInstanceFormattedSpinButton*>(widget); + SolarMutexGuard aGuard; + return pThis->signal_input(new_value); + } + + static void signalValueChanged(GtkSpinButton*, gpointer widget) + { + GtkInstanceFormattedSpinButton* pThis = static_cast<GtkInstanceFormattedSpinButton*>(widget); + SolarMutexGuard aGuard; + pThis->signal_value_changed(); + } + +public: + GtkInstanceFormattedSpinButton(GtkSpinButton* pButton, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : GtkInstanceEntry(GTK_ENTRY(pButton), pBuilder, bTakeOwnership) + , m_pButton(pButton) + , m_pFormatter(nullptr) + , m_pLastOutputColor(nullptr) + , m_nFormatKey(0) + , m_bTreatAsNumber(true) + , m_nValueChangedSignalId(g_signal_connect(pButton, "value-changed", G_CALLBACK(signalValueChanged), this)) + , m_nOutputSignalId(g_signal_connect(pButton, "output", G_CALLBACK(signalOutput), this)) + , m_nInputSignalId(g_signal_connect(pButton, "input", G_CALLBACK(signalInput), this)) + { + } + + virtual double get_value() const override + { + return gtk_spin_button_get_value(m_pButton); + } + + virtual void set_value(double value) override + { + disable_notify_events(); + gtk_spin_button_set_value(m_pButton, value); + enable_notify_events(); + } + + virtual void set_range(double min, double max) override + { + disable_notify_events(); + gtk_spin_button_set_range(m_pButton, min, max); + enable_notify_events(); + } + + virtual void get_range(double& min, double& max) const override + { + gtk_spin_button_get_range(m_pButton, &min, &max); + } + + virtual void set_formatter(SvNumberFormatter* pFormatter) override + { + m_pFormatter = pFormatter; + + // calc the default format key from the Office's UI locale + if (m_pFormatter) + { + // get the Office's locale and translate + LanguageType eSysLanguage = Application::GetSettings().GetUILanguageTag().getLanguageType( false); + // get the standard numeric format for this language + m_nFormatKey = m_pFormatter->GetStandardFormat( SvNumFormatType::NUMBER, eSysLanguage ); + } + else + m_nFormatKey = 0; + signal_output(); + } + + virtual SvNumberFormatter* get_formatter() override + { + return m_pFormatter; + } + + virtual sal_Int32 get_format_key() const override + { + return m_nFormatKey; + } + + virtual void set_format_key(sal_Int32 nFormatKey) override + { + m_nFormatKey = nFormatKey; + } + + virtual void treat_as_number(bool bSet) override + { + m_bTreatAsNumber = bSet; + } + + virtual void set_digits(unsigned int digits) override + { + disable_notify_events(); + gtk_spin_button_set_digits(m_pButton, digits); + enable_notify_events(); + } + + virtual ~GtkInstanceFormattedSpinButton() override + { + g_signal_handler_disconnect(m_pButton, m_nInputSignalId); + g_signal_handler_disconnect(m_pButton, m_nOutputSignalId); + g_signal_handler_disconnect(m_pButton, m_nValueChangedSignalId); + } +}; + +class GtkInstanceLabel : public GtkInstanceWidget, public virtual weld::Label +{ +private: + GtkLabel* m_pLabel; + + void set_text_color(const Color& rColor) + { + guint16 nRed = rColor.GetRed() << 8; + guint16 nGreen = rColor.GetRed() << 8; + guint16 nBlue = rColor.GetBlue() << 8; + + PangoAttrList* pAttrs = pango_attr_list_new(); + pango_attr_list_insert(pAttrs, pango_attr_background_new(nRed, nGreen, nBlue)); + gtk_label_set_attributes(m_pLabel, pAttrs); + pango_attr_list_unref(pAttrs); + } + +public: + GtkInstanceLabel(GtkLabel* pLabel, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : GtkInstanceWidget(GTK_WIDGET(pLabel), pBuilder, bTakeOwnership) + , m_pLabel(pLabel) + { + } + + virtual void set_label(const OUString& rText) override + { + ::set_label(m_pLabel, rText); + } + + virtual OUString get_label() const override + { + return ::get_label(m_pLabel); + } + + virtual void set_mnemonic_widget(Widget* pTarget) override + { + assert(!gtk_label_get_selectable(m_pLabel) && "don't use set_mnemonic_widget on selectable labels, for consistency with gen backend"); + GtkInstanceWidget* pTargetWidget = dynamic_cast<GtkInstanceWidget*>(pTarget); + gtk_label_set_mnemonic_widget(m_pLabel, pTargetWidget ? pTargetWidget->getWidget() : nullptr); + } + + virtual void set_message_type(weld::EntryMessageType eType) override + { + if (eType == weld::EntryMessageType::Error) + set_text_color(Application::GetSettings().GetStyleSettings().GetHighlightColor()); + else if (eType == weld::EntryMessageType::Warning) + set_text_color(COL_YELLOW); + else + gtk_label_set_attributes(m_pLabel, nullptr); + } + + virtual void set_font(const vcl::Font& rFont) override + { + PangoAttrList* pAttrList = create_attr_list(rFont); + gtk_label_set_attributes(m_pLabel, pAttrList); + pango_attr_list_unref(pAttrList); + } +}; + +} + +std::unique_ptr<weld::Label> GtkInstanceFrame::weld_label_widget() const +{ + GtkWidget* pLabel = gtk_frame_get_label_widget(m_pFrame); + if (!pLabel || !GTK_IS_LABEL(pLabel)) + return nullptr; + return std::make_unique<GtkInstanceLabel>(GTK_LABEL(pLabel), m_pBuilder, false); +} + +namespace { + +class GtkInstanceTextView : public GtkInstanceContainer, public virtual weld::TextView +{ +private: + GtkTextView* m_pTextView; + GtkTextBuffer* m_pTextBuffer; + GtkAdjustment* m_pVAdjustment; + gulong m_nChangedSignalId; + gulong m_nCursorPosSignalId; + gulong m_nVAdjustChangedSignalId; + + static void signalChanged(GtkTextView*, gpointer widget) + { + GtkInstanceTextView* pThis = static_cast<GtkInstanceTextView*>(widget); + SolarMutexGuard aGuard; + pThis->signal_changed(); + } + + static void signalCursorPosition(GtkTextView*, GParamSpec*, gpointer widget) + { + GtkInstanceTextView* pThis = static_cast<GtkInstanceTextView*>(widget); + pThis->signal_cursor_position(); + } + + static void signalVAdjustValueChanged(GtkAdjustment*, gpointer widget) + { + GtkInstanceTextView* pThis = static_cast<GtkInstanceTextView*>(widget); + SolarMutexGuard aGuard; + pThis->signal_vadjustment_changed(); + } + +public: + GtkInstanceTextView(GtkTextView* pTextView, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : GtkInstanceContainer(GTK_CONTAINER(pTextView), pBuilder, bTakeOwnership) + , m_pTextView(pTextView) + , m_pTextBuffer(gtk_text_view_get_buffer(pTextView)) + , m_pVAdjustment(gtk_scrollable_get_vadjustment(GTK_SCROLLABLE(pTextView))) + , m_nChangedSignalId(g_signal_connect(m_pTextBuffer, "changed", G_CALLBACK(signalChanged), this)) + , m_nCursorPosSignalId(g_signal_connect(m_pTextBuffer, "notify::cursor-position", G_CALLBACK(signalCursorPosition), this)) + , m_nVAdjustChangedSignalId(g_signal_connect(m_pVAdjustment, "value-changed", G_CALLBACK(signalVAdjustValueChanged), this)) + { + } + + virtual void set_size_request(int nWidth, int nHeight) override + { + GtkWidget* pParent = gtk_widget_get_parent(m_pWidget); + if (GTK_IS_SCROLLED_WINDOW(pParent)) + { + gtk_scrolled_window_set_min_content_width(GTK_SCROLLED_WINDOW(pParent), nWidth); + gtk_scrolled_window_set_min_content_height(GTK_SCROLLED_WINDOW(pParent), nHeight); + return; + } + gtk_widget_set_size_request(m_pWidget, nWidth, nHeight); + } + + virtual void set_text(const OUString& rText) override + { + disable_notify_events(); + GtkTextBuffer* pBuffer = gtk_text_view_get_buffer(m_pTextView); + OString sText(OUStringToOString(rText, RTL_TEXTENCODING_UTF8)); + gtk_text_buffer_set_text(pBuffer, sText.getStr(), sText.getLength()); + enable_notify_events(); + } + + virtual OUString get_text() const override + { + GtkTextBuffer* pBuffer = gtk_text_view_get_buffer(m_pTextView); + GtkTextIter start, end; + gtk_text_buffer_get_bounds(pBuffer, &start, &end); + char* pStr = gtk_text_buffer_get_text(pBuffer, &start, &end, true); + OUString sRet(pStr, pStr ? strlen(pStr) : 0, RTL_TEXTENCODING_UTF8); + g_free(pStr); + return sRet; + } + + virtual void replace_selection(const OUString& rText) override + { + disable_notify_events(); + GtkTextBuffer* pBuffer = gtk_text_view_get_buffer(m_pTextView); + gtk_text_buffer_delete_selection(pBuffer, false, gtk_text_view_get_editable(m_pTextView)); + OString sText(OUStringToOString(rText, RTL_TEXTENCODING_UTF8)); + gtk_text_buffer_insert_at_cursor(pBuffer, sText.getStr(), sText.getLength()); + enable_notify_events(); + } + + virtual bool get_selection_bounds(int& rStartPos, int& rEndPos) override + { + GtkTextBuffer* pBuffer = gtk_text_view_get_buffer(m_pTextView); + GtkTextIter start, end; + gtk_text_buffer_get_selection_bounds(pBuffer, &start, &end); + rStartPos = gtk_text_iter_get_offset(&start); + rEndPos = gtk_text_iter_get_offset(&end); + return rStartPos != rEndPos; + } + + virtual void select_region(int nStartPos, int nEndPos) override + { + disable_notify_events(); + GtkTextBuffer* pBuffer = gtk_text_view_get_buffer(m_pTextView); + GtkTextIter start, end; + gtk_text_buffer_get_iter_at_offset(pBuffer, &start, nStartPos); + gtk_text_buffer_get_iter_at_offset(pBuffer, &end, nEndPos); + gtk_text_buffer_select_range(pBuffer, &start, &end); + GtkTextMark* mark = gtk_text_buffer_create_mark(pBuffer, "scroll", &end, true); + gtk_text_view_scroll_mark_onscreen(m_pTextView, mark); + enable_notify_events(); + } + + virtual void set_editable(bool bEditable) override + { + gtk_text_view_set_editable(m_pTextView, bEditable); + } + + virtual void set_monospace(bool bMonospace) override + { + gtk_text_view_set_monospace(m_pTextView, bMonospace); + } + + virtual void disable_notify_events() override + { + g_signal_handler_block(m_pVAdjustment, m_nVAdjustChangedSignalId); + g_signal_handler_block(m_pTextBuffer, m_nCursorPosSignalId); + g_signal_handler_block(m_pTextBuffer, m_nChangedSignalId); + GtkInstanceContainer::disable_notify_events(); + } + + virtual void enable_notify_events() override + { + GtkInstanceContainer::enable_notify_events(); + g_signal_handler_unblock(m_pTextBuffer, m_nChangedSignalId); + g_signal_handler_unblock(m_pTextBuffer, m_nCursorPosSignalId); + g_signal_handler_unblock(m_pVAdjustment, m_nVAdjustChangedSignalId); + } + + virtual int vadjustment_get_value() const override + { + return gtk_adjustment_get_value(m_pVAdjustment); + } + + virtual void vadjustment_set_value(int value) override + { + disable_notify_events(); + gtk_adjustment_set_value(m_pVAdjustment, value); + enable_notify_events(); + } + + virtual int vadjustment_get_upper() const override + { + return gtk_adjustment_get_upper(m_pVAdjustment); + } + + virtual int vadjustment_get_lower() const override + { + return gtk_adjustment_get_lower(m_pVAdjustment); + } + + virtual int vadjustment_get_page_size() const override + { + return gtk_adjustment_get_page_size(m_pVAdjustment); + } + + virtual void show() override + { + GtkWidget* pParent = gtk_widget_get_parent(m_pWidget); + if (GTK_IS_SCROLLED_WINDOW(pParent)) + gtk_widget_show(pParent); + gtk_widget_show(m_pWidget); + } + + virtual void hide() override + { + GtkWidget* pParent = gtk_widget_get_parent(m_pWidget); + if (GTK_IS_SCROLLED_WINDOW(pParent)) + gtk_widget_hide(pParent); + gtk_widget_hide(m_pWidget); + } + + virtual ~GtkInstanceTextView() override + { + g_signal_handler_disconnect(m_pVAdjustment, m_nVAdjustChangedSignalId); + g_signal_handler_disconnect(m_pTextBuffer, m_nChangedSignalId); + g_signal_handler_disconnect(m_pTextBuffer, m_nCursorPosSignalId); + } +}; + +// IMHandler +class IMHandler; + +AtkObject* (*default_drawing_area_get_accessible)(GtkWidget *widget); + +class GtkInstanceDrawingArea : public GtkInstanceWidget, public virtual weld::DrawingArea +{ +private: + GtkDrawingArea* m_pDrawingArea; + a11yref m_xAccessible; + AtkObject *m_pAccessible; + ScopedVclPtrInstance<VirtualDevice> m_xDevice; + std::unique_ptr<IMHandler> m_xIMHandler; + cairo_surface_t* m_pSurface; + gulong m_nDrawSignalId; + gulong m_nStyleUpdatedSignalId; + gulong m_nQueryTooltip; + gulong m_nPopupMenu; + gulong m_nScrollEvent; + + static gboolean signalDraw(GtkWidget*, cairo_t* cr, gpointer widget) + { + GtkInstanceDrawingArea* pThis = static_cast<GtkInstanceDrawingArea*>(widget); + SolarMutexGuard aGuard; + pThis->signal_draw(cr); + return false; + } + void signal_draw(cairo_t* cr) + { + GdkRectangle rect; + if (!m_pSurface || !gdk_cairo_get_clip_rectangle(cr, &rect)) + return; + tools::Rectangle aRect(Point(rect.x, rect.y), Size(rect.width, rect.height)); + aRect = m_xDevice->PixelToLogic(aRect); + m_xDevice->Erase(aRect); + m_aDrawHdl.Call(std::pair<vcl::RenderContext&, const tools::Rectangle&>(*m_xDevice, aRect)); + cairo_surface_mark_dirty(m_pSurface); + + cairo_set_source_surface(cr, m_pSurface, 0, 0); + cairo_paint(cr); + + tools::Rectangle aFocusRect(m_aGetFocusRectHdl.Call(*this)); + if (!aFocusRect.IsEmpty()) + { + gtk_render_focus(gtk_widget_get_style_context(GTK_WIDGET(m_pDrawingArea)), cr, + aFocusRect.Left(), aFocusRect.Top(), aFocusRect.GetWidth(), aFocusRect.GetHeight()); + } + } + virtual void signal_size_allocate(guint nWidth, guint nHeight) override + { + m_xDevice->SetOutputSizePixel(Size(nWidth, nHeight)); + m_pSurface = get_underlying_cairo_surface(*m_xDevice); + GtkInstanceWidget::signal_size_allocate(nWidth, nHeight); + } + static void signalStyleUpdated(GtkWidget*, gpointer widget) + { + GtkInstanceDrawingArea* pThis = static_cast<GtkInstanceDrawingArea*>(widget); + SolarMutexGuard aGuard; + return pThis->signal_style_updated(); + } + void signal_style_updated() + { + m_aStyleUpdatedHdl.Call(*this); + } + static gboolean signalQueryTooltip(GtkWidget* pGtkWidget, gint x, gint y, + gboolean /*keyboard_mode*/, GtkTooltip *tooltip, + gpointer widget) + { + GtkInstanceDrawingArea* pThis = static_cast<GtkInstanceDrawingArea*>(widget); + tools::Rectangle aHelpArea(x, y); + OUString aTooltip = pThis->signal_query_tooltip(aHelpArea); + if (aTooltip.isEmpty()) + return false; + gtk_tooltip_set_text(tooltip, OUStringToOString(aTooltip, RTL_TEXTENCODING_UTF8).getStr()); + GdkRectangle aGdkHelpArea; + aGdkHelpArea.x = aHelpArea.Left(); + aGdkHelpArea.y = aHelpArea.Top(); + aGdkHelpArea.width = aHelpArea.GetWidth(); + aGdkHelpArea.height = aHelpArea.GetHeight(); + if (pThis->SwapForRTL()) + aGdkHelpArea.x = gtk_widget_get_allocated_width(pGtkWidget) - aGdkHelpArea.width - 1 - aGdkHelpArea.x; + gtk_tooltip_set_tip_area(tooltip, &aGdkHelpArea); + return true; + } + virtual bool signal_popup_menu(const CommandEvent& rCEvt) override + { + return signal_command(rCEvt); + } + bool signal_scroll(GdkEventScroll* pEvent) + { + SalWheelMouseEvent aEvt(GtkSalFrame::GetWheelEvent(*pEvent)); + + if (SwapForRTL()) + aEvt.mnX = gtk_widget_get_allocated_width(m_pWidget) - 1 - aEvt.mnX; + + CommandWheelMode nMode; + sal_uInt16 nCode = aEvt.mnCode; + bool bHorz = aEvt.mbHorz; + if (nCode & KEY_MOD1) + nMode = CommandWheelMode::ZOOM; + else if (nCode & KEY_MOD2) + nMode = CommandWheelMode::DATAZOOM; + else + { + nMode = CommandWheelMode::SCROLL; + // #i85450# interpret shift-wheel as horizontal wheel action + if( (nCode & (KEY_SHIFT | KEY_MOD1 | KEY_MOD2 | KEY_MOD3)) == KEY_SHIFT ) + bHorz = true; + } + + CommandWheelData aWheelData(aEvt.mnDelta, aEvt.mnNotchDelta, aEvt.mnScrollLines, + nMode, nCode, bHorz, aEvt.mbDeltaIsPixel); + CommandEvent aCEvt(Point(aEvt.mnX, aEvt.mnY), CommandEventId::Wheel, true, &aWheelData); + return m_aCommandHdl.Call(aCEvt); + } + static gboolean signalScroll(GtkWidget*, GdkEventScroll* pEvent, gpointer widget) + { + GtkInstanceDrawingArea* pThis = static_cast<GtkInstanceDrawingArea*>(widget); + return pThis->signal_scroll(pEvent); + } +public: + GtkInstanceDrawingArea(GtkDrawingArea* pDrawingArea, GtkInstanceBuilder* pBuilder, const a11yref& rA11y, bool bTakeOwnership) + : GtkInstanceWidget(GTK_WIDGET(pDrawingArea), pBuilder, bTakeOwnership) + , m_pDrawingArea(pDrawingArea) + , m_xAccessible(rA11y) + , m_pAccessible(nullptr) + , m_xDevice(DeviceFormat::DEFAULT) + , m_pSurface(nullptr) + , m_nDrawSignalId(g_signal_connect(m_pDrawingArea, "draw", G_CALLBACK(signalDraw), this)) + , m_nStyleUpdatedSignalId(g_signal_connect(m_pDrawingArea,"style-updated", G_CALLBACK(signalStyleUpdated), this)) + , m_nQueryTooltip(g_signal_connect(m_pDrawingArea, "query-tooltip", G_CALLBACK(signalQueryTooltip), this)) + , m_nPopupMenu(g_signal_connect(m_pDrawingArea, "popup-menu", G_CALLBACK(signalPopupMenu), this)) + , m_nScrollEvent(g_signal_connect(m_pDrawingArea, "scroll-event", G_CALLBACK(signalScroll), this)) + { + gtk_widget_set_has_tooltip(m_pWidget, true); + g_object_set_data(G_OBJECT(m_pDrawingArea), "g-lo-GtkInstanceDrawingArea", this); + m_xDevice->EnableRTL(get_direction()); + } + + AtkObject* GetAtkObject(AtkObject* pDefaultAccessible) + { + if (!m_pAccessible && m_xAccessible.is()) + { + GtkWidget* pParent = gtk_widget_get_parent(m_pWidget); + m_pAccessible = atk_object_wrapper_new(m_xAccessible, gtk_widget_get_accessible(pParent), pDefaultAccessible); + if (m_pAccessible) + g_object_ref(m_pAccessible); + } + return m_pAccessible; + } + + virtual void set_direction(bool bRTL) override + { + GtkInstanceWidget::set_direction(bRTL); + m_xDevice->EnableRTL(bRTL); + } + + virtual void set_cursor(PointerStyle ePointerStyle) override + { + GdkCursor *pCursor = GtkSalFrame::getDisplay()->getCursor(ePointerStyle); + if (!gtk_widget_get_realized(GTK_WIDGET(m_pDrawingArea))) + gtk_widget_realize(GTK_WIDGET(m_pDrawingArea)); + gdk_window_set_cursor(gtk_widget_get_window(GTK_WIDGET(m_pDrawingArea)), pCursor); + } + + virtual void set_input_context(const InputContext& rInputContext) override; + + virtual void im_context_set_cursor_location(const tools::Rectangle& rCursorRect, int nExtTextInputWidth) override; + + int im_context_get_surrounding(OUString& rSurroundingText) + { + return signal_im_context_get_surrounding(rSurroundingText); + } + + bool im_context_delete_surrounding(const Selection& rRange) + { + return signal_im_context_delete_surrounding(rRange); + } + + virtual void queue_draw() override + { + gtk_widget_queue_draw(GTK_WIDGET(m_pDrawingArea)); + } + + virtual void queue_draw_area(int x, int y, int width, int height) override + { + tools::Rectangle aRect(Point(x, y), Size(width, height)); + aRect = m_xDevice->LogicToPixel(aRect); + gtk_widget_queue_draw_area(GTK_WIDGET(m_pDrawingArea), aRect.Left(), aRect.Top(), aRect.GetWidth(), aRect.GetHeight()); + } + + virtual void queue_resize() override + { + gtk_widget_queue_resize(GTK_WIDGET(m_pDrawingArea)); + } + + virtual a11yref get_accessible_parent() override + { + //get_accessible_parent should only be needed for the vcl implementation, + //in the gtk impl the native AtkObject parent set via + //atk_object_wrapper_new(m_xAccessible, gtk_widget_get_accessible(pParent)); + //should negate the need. + assert(false && "get_accessible_parent should only be called on a vcl impl"); + return uno::Reference<css::accessibility::XAccessible>(); + } + + virtual a11yrelationset get_accessible_relation_set() override + { + //get_accessible_relation_set should only be needed for the vcl implementation, + //in the gtk impl the native equivalent should negate the need. + assert(false && "get_accessible_parent should only be called on a vcl impl"); + return uno::Reference<css::accessibility::XAccessibleRelationSet>(); + } + + virtual Point get_accessible_location() override + { + AtkObject* pAtkObject = default_drawing_area_get_accessible(m_pWidget); + gint x(0), y(0); + if (pAtkObject && ATK_IS_COMPONENT(pAtkObject)) + atk_component_get_extents(ATK_COMPONENT(pAtkObject), &x, &y, nullptr, nullptr, ATK_XY_WINDOW); + return Point(x, y); + } + + virtual void set_accessible_name(const OUString& rName) override + { + AtkObject* pAtkObject = default_drawing_area_get_accessible(m_pWidget); + if (!pAtkObject) + return; + atk_object_set_name(pAtkObject, OUStringToOString(rName, RTL_TEXTENCODING_UTF8).getStr()); + } + + virtual OUString get_accessible_name() const override + { + AtkObject* pAtkObject = default_drawing_area_get_accessible(m_pWidget); + const char* pStr = pAtkObject ? atk_object_get_name(pAtkObject) : nullptr; + return OUString(pStr, pStr ? strlen(pStr) : 0, RTL_TEXTENCODING_UTF8); + } + + virtual OUString get_accessible_description() const override + { + AtkObject* pAtkObject = default_drawing_area_get_accessible(m_pWidget); + const char* pStr = pAtkObject ? atk_object_get_description(pAtkObject) : nullptr; + return OUString(pStr, pStr ? strlen(pStr) : 0, RTL_TEXTENCODING_UTF8); + } + + virtual void enable_drag_source(rtl::Reference<TransferDataContainer>& rHelper, sal_uInt8 eDNDConstants) override + { + do_enable_drag_source(rHelper, eDNDConstants); + } + + virtual bool do_signal_drag_begin(bool& rUnsetDragIcon) override + { + rUnsetDragIcon = false; + if (m_aDragBeginHdl.Call(*this)) + return true; + return false; + } + + virtual ~GtkInstanceDrawingArea() override + { + g_object_steal_data(G_OBJECT(m_pDrawingArea), "g-lo-GtkInstanceDrawingArea"); + if (m_pAccessible) + g_object_unref(m_pAccessible); + css::uno::Reference<css::lang::XComponent> xComp(m_xAccessible, css::uno::UNO_QUERY); + if (xComp.is()) + xComp->dispose(); + g_signal_handler_disconnect(m_pDrawingArea, m_nScrollEvent); + g_signal_handler_disconnect(m_pDrawingArea, m_nPopupMenu); + g_signal_handler_disconnect(m_pDrawingArea, m_nQueryTooltip); + g_signal_handler_disconnect(m_pDrawingArea, m_nStyleUpdatedSignalId); + g_signal_handler_disconnect(m_pDrawingArea, m_nDrawSignalId); + } + + virtual OutputDevice& get_ref_device() override + { + return *m_xDevice; + } + + bool signal_command(const CommandEvent& rCEvt) + { + return m_aCommandHdl.Call(rCEvt); + } +}; + +class IMHandler +{ +private: + GtkInstanceDrawingArea* m_pArea; + GtkIMContext* m_pIMContext; + OUString m_sPreeditText; + gulong m_nFocusInSignalId; + gulong m_nFocusOutSignalId; + bool m_bExtTextInput; + +public: + IMHandler(GtkInstanceDrawingArea* pArea) + : m_pArea(pArea) + , m_pIMContext(gtk_im_multicontext_new()) + , m_nFocusInSignalId(g_signal_connect(m_pArea->getWidget(), "focus-in-event", G_CALLBACK(signalFocusIn), this)) + , m_nFocusOutSignalId(g_signal_connect(m_pArea->getWidget(), "focus-out-event", G_CALLBACK(signalFocusOut), this)) + , m_bExtTextInput(false) + { + g_signal_connect(m_pIMContext, "preedit-start", G_CALLBACK(signalIMPreeditStart), this); + g_signal_connect(m_pIMContext, "preedit-end", G_CALLBACK(signalIMPreeditEnd), this); + g_signal_connect(m_pIMContext, "commit", G_CALLBACK(signalIMCommit), this); + g_signal_connect(m_pIMContext, "preedit-changed", G_CALLBACK(signalIMPreeditChanged), this); + g_signal_connect(m_pIMContext, "retrieve-surrounding", G_CALLBACK(signalIMRetrieveSurrounding), this); + g_signal_connect(m_pIMContext, "delete-surrounding", G_CALLBACK(signalIMDeleteSurrounding), this); + + GtkWidget* pWidget = m_pArea->getWidget(); + if (!gtk_widget_get_realized(pWidget)) + gtk_widget_realize(pWidget); + GdkWindow* pWin = gtk_widget_get_window(pWidget); + gtk_im_context_set_client_window(m_pIMContext, pWin); + gtk_im_context_focus_in(m_pIMContext); + } + + void signalFocus(bool bIn) + { + if (bIn) + gtk_im_context_focus_in(m_pIMContext); + else + gtk_im_context_focus_out(m_pIMContext); + } + + static gboolean signalFocusIn(GtkWidget*, GdkEvent*, gpointer im_handler) + { + IMHandler* pThis = static_cast<IMHandler*>(im_handler); + pThis->signalFocus(true); + return false; + } + + static gboolean signalFocusOut(GtkWidget*, GdkEvent*, gpointer im_handler) + { + IMHandler* pThis = static_cast<IMHandler*>(im_handler); + pThis->signalFocus(false); + return false; + } + + ~IMHandler() + { + EndExtTextInput(); + + g_signal_handler_disconnect(m_pArea->getWidget(), m_nFocusOutSignalId); + g_signal_handler_disconnect(m_pArea->getWidget(), m_nFocusInSignalId); + + // first give IC a chance to deinitialize + gtk_im_context_set_client_window(m_pIMContext, nullptr); + // destroy old IC + g_object_unref(m_pIMContext); + } + + void updateIMSpotLocation() + { + CommandEvent aCEvt(Point(), CommandEventId::CursorPos); + // we expect set_cursor_location to get triggered by this + m_pArea->signal_command(aCEvt); + } + + void set_cursor_location(const tools::Rectangle& rRect) + { + GdkRectangle aArea{static_cast<int>(rRect.Left()), static_cast<int>(rRect.Top()), + static_cast<int>(rRect.GetWidth()), static_cast<int>(rRect.GetHeight())}; + gtk_im_context_set_cursor_location(m_pIMContext, &aArea); + } + + static void signalIMCommit(GtkIMContext* /*pContext*/, gchar* pText, gpointer im_handler) + { + IMHandler* pThis = static_cast<IMHandler*>(im_handler); + + // at least editeng expects to have seen a start before accepting a commit + pThis->StartExtTextInput(); + + OUString sText(pText, strlen(pText), RTL_TEXTENCODING_UTF8); + CommandExtTextInputData aData(sText, nullptr, sText.getLength(), 0, false); + CommandEvent aCEvt(Point(), CommandEventId::ExtTextInput, false, &aData); + pThis->m_pArea->signal_command(aCEvt); + + pThis->updateIMSpotLocation(); + + pThis->EndExtTextInput(); + + pThis->m_sPreeditText.clear(); + } + + static void signalIMPreeditChanged(GtkIMContext* pIMContext, gpointer im_handler) + { + IMHandler* pThis = static_cast<IMHandler*>(im_handler); + + sal_Int32 nCursorPos(0); + sal_uInt8 nCursorFlags(0); + std::vector<ExtTextInputAttr> aInputFlags; + OUString sText = GtkSalFrame::GetPreeditDetails(pIMContext, aInputFlags, nCursorPos, nCursorFlags); + + // change from nothing to nothing -> do not start preedit e.g. this + // will activate input into a calc cell without user input + if (sText.isEmpty() && pThis->m_sPreeditText.isEmpty()) + return; + + pThis->m_sPreeditText = sText; + + CommandExtTextInputData aData(sText, aInputFlags.data(), nCursorPos, nCursorFlags, false); + CommandEvent aCEvt(Point(), CommandEventId::ExtTextInput, false, &aData); + pThis->m_pArea->signal_command(aCEvt); + + pThis->updateIMSpotLocation(); + } + + static gboolean signalIMRetrieveSurrounding(GtkIMContext* pContext, gpointer im_handler) + { + IMHandler* pThis = static_cast<IMHandler*>(im_handler); + + OUString sSurroundingText; + int nCursorIndex = pThis->m_pArea->im_context_get_surrounding(sSurroundingText); + + if (nCursorIndex != -1) + { + OString sUTF = OUStringToOString(sSurroundingText, RTL_TEXTENCODING_UTF8); + OUString sCursorText(sSurroundingText.copy(0, nCursorIndex)); + gtk_im_context_set_surrounding(pContext, sUTF.getStr(), sUTF.getLength(), + OUStringToOString(sCursorText, RTL_TEXTENCODING_UTF8).getLength()); + } + + return true; + } + + static gboolean signalIMDeleteSurrounding(GtkIMContext*, gint nOffset, gint nChars, + gpointer im_handler) + { + bool bRet = false; + + IMHandler* pThis = static_cast<IMHandler*>(im_handler); + + OUString sSurroundingText; + sal_Int32 nCursorIndex = pThis->m_pArea->im_context_get_surrounding(sSurroundingText); + + if (nCursorIndex != -1) + { + // Note that offset and n_chars are in characters not in bytes + // which differs from the usage other places in GtkIMContext + + if (nOffset > 0) + { + while (nCursorIndex < sSurroundingText.getLength()) + sSurroundingText.iterateCodePoints(&nCursorIndex, 1); + } + else if (nOffset < 0) + { + while (nCursorIndex > 0) + sSurroundingText.iterateCodePoints(&nCursorIndex, -1); + } + + sal_Int32 nCursorEndIndex(nCursorIndex); + sal_Int32 nCount(0); + while (nCount < nChars && nCursorEndIndex < sSurroundingText.getLength()) + ++nCount; + + bRet = pThis->m_pArea->im_context_delete_surrounding(Selection(nCursorIndex, nCursorEndIndex)); + } + + return bRet; + } + + void StartExtTextInput() + { + if (m_bExtTextInput) + return; + CommandEvent aCEvt(Point(), CommandEventId::StartExtTextInput); + m_pArea->signal_command(aCEvt); + m_bExtTextInput = true; + } + + static void signalIMPreeditStart(GtkIMContext*, gpointer im_handler) + { + IMHandler* pThis = static_cast<IMHandler*>(im_handler); + pThis->StartExtTextInput(); + pThis->updateIMSpotLocation(); + } + + void EndExtTextInput() + { + if (!m_bExtTextInput) + return; + CommandEvent aCEvt(Point(), CommandEventId::EndExtTextInput); + m_pArea->signal_command(aCEvt); + m_bExtTextInput = false; + } + + static void signalIMPreeditEnd(GtkIMContext*, gpointer im_handler) + { + IMHandler* pThis = static_cast<IMHandler*>(im_handler); + pThis->updateIMSpotLocation(); + pThis->EndExtTextInput(); + } +}; + +void GtkInstanceDrawingArea::set_input_context(const InputContext& rInputContext) +{ + bool bUseIm(rInputContext.GetOptions() & InputContextFlags::Text); + if (!bUseIm) + { + m_xIMHandler.reset(); + return; + } + // create a new im context + if (!m_xIMHandler) + m_xIMHandler.reset(new IMHandler(this)); +} + +void GtkInstanceDrawingArea::im_context_set_cursor_location(const tools::Rectangle& rCursorRect, int /*nExtTextInputWidth*/) +{ + if (!m_xIMHandler) + return; + m_xIMHandler->set_cursor_location(rCursorRect); +} + +} + +namespace { + +GtkBuilder* makeComboBoxBuilder() +{ + OUString aUri(VclBuilderContainer::getUIRootDir() + "vcl/ui/combobox.ui"); + OUString aPath; + osl::FileBase::getSystemPathFromFileURL(aUri, aPath); + return gtk_builder_new_from_file(OUStringToOString(aPath, RTL_TEXTENCODING_UTF8).getStr()); +} + +struct GtkTreeRowReferenceDeleter +{ + void operator()(GtkTreeRowReference* p) const + { + gtk_tree_row_reference_free(p); + } +}; + +// pop down the toplevel combobox menu when something is activated from a custom +// submenu, i.e. wysiwyg style menu +class CustomRenderMenuButtonHelper : public MenuHelper +{ +private: + GtkToggleButton* m_pComboBox; +public: + CustomRenderMenuButtonHelper(GtkMenu* pMenu, GtkToggleButton* pComboBox) + : MenuHelper(pMenu, false) + , m_pComboBox(pComboBox) + { + } + virtual void signal_activate(GtkMenuItem*) override + { + gtk_toggle_button_set_active(m_pComboBox, false); + } +}; + +class GtkInstanceComboBox : public GtkInstanceContainer, public vcl::ISearchableStringList, public virtual weld::ComboBox +{ +private: + GtkBuilder* m_pComboBuilder; + GtkComboBox* m_pComboBox; + GtkOverlay* m_pOverlay; + GtkTreeView* m_pTreeView; + GtkMenuButton* m_pOverlayButton; // button that the StyleDropdown uses on an active row + GtkWindow* m_pMenuWindow; + GtkTreeModel* m_pTreeModel; + GtkCellRenderer* m_pButtonTextRenderer; + GtkCellRenderer* m_pMenuTextRenderer; + GtkWidget* m_pToggleButton; + GtkWidget* m_pEntry; + GtkCellView* m_pCellView; + std::unique_ptr<CustomRenderMenuButtonHelper> m_xCustomMenuButtonHelper; + std::unique_ptr<vcl::Font> m_xFont; + std::unique_ptr<comphelper::string::NaturalStringSorter> m_xSorter; + vcl::QuickSelectionEngine m_aQuickSelectionEngine; + std::vector<std::unique_ptr<GtkTreeRowReference, GtkTreeRowReferenceDeleter>> m_aSeparatorRows; + OUString m_sMenuButtonRow; + bool m_bHoverSelection; + bool m_bMouseInOverlayButton; + bool m_bPopupActive; + bool m_bAutoComplete; + bool m_bAutoCompleteCaseSensitive; + bool m_bChangedByMenu; + bool m_bCustomRenderer; + bool m_bActivateCalled; + gint m_nTextCol; + gint m_nIdCol; + gulong m_nToggleFocusInSignalId; + gulong m_nToggleFocusOutSignalId; + gulong m_nRowActivatedSignalId; + gulong m_nChangedSignalId; + gulong m_nPopupShownSignalId; + gulong m_nKeyPressEventSignalId; + gulong m_nEntryInsertTextSignalId; + gulong m_nEntryActivateSignalId; + gulong m_nEntryFocusInSignalId; + gulong m_nEntryFocusOutSignalId; + gulong m_nEntryKeyPressEventSignalId; + guint m_nAutoCompleteIdleId; + gint m_nNonCustomLineHeight; + gint m_nPrePopupCursorPos; + int m_nMRUCount; + int m_nMaxMRUCount; + + static gboolean idleAutoComplete(gpointer widget) + { + GtkInstanceComboBox* pThis = static_cast<GtkInstanceComboBox*>(widget); + pThis->auto_complete(); + return false; + } + + void auto_complete() + { + m_nAutoCompleteIdleId = 0; + OUString aStartText = get_active_text(); + int nStartPos, nEndPos; + get_entry_selection_bounds(nStartPos, nEndPos); + int nMaxSelection = std::max(nStartPos, nEndPos); + if (nMaxSelection != aStartText.getLength()) + return; + + disable_notify_events(); + int nActive = get_active(); + int nStart = nActive; + + if (nStart == -1) + nStart = 0; + + int nPos = -1; + + int nZeroRow = 0; + if (m_nMRUCount) + nZeroRow += (m_nMRUCount + 1); + + if (!m_bAutoCompleteCaseSensitive) + { + // Try match case insensitive from current position + nPos = starts_with(m_pTreeModel, aStartText, 0, nStart, false); + if (nPos == -1 && nStart != 0) + { + // Try match case insensitive, but from start + nPos = starts_with(m_pTreeModel, aStartText, 0, nZeroRow, false); + } + } + + if (nPos == -1) + { + // Try match case sensitive from current position + nPos = starts_with(m_pTreeModel, aStartText, 0, nStart, true); + if (nPos == -1 && nStart != 0) + { + // Try match case sensitive, but from start + nPos = starts_with(m_pTreeModel, aStartText, 0, nZeroRow, true); + } + } + + if (nPos != -1) + { + OUString aText = get_text_including_mru(nPos); + if (aText != aStartText) + set_active_text(aText); + select_entry_region(aText.getLength(), aStartText.getLength()); + } + enable_notify_events(); + } + + static void signalEntryInsertText(GtkEntry* pEntry, const gchar* pNewText, gint nNewTextLength, + gint* position, gpointer widget) + { + GtkInstanceComboBox* pThis = static_cast<GtkInstanceComboBox*>(widget); + SolarMutexGuard aGuard; + pThis->signal_entry_insert_text(pEntry, pNewText, nNewTextLength, position); + } + + void signal_entry_insert_text(GtkEntry* pEntry, const gchar* pNewText, gint nNewTextLength, gint* position) + { + // first filter inserted text + if (m_aEntryInsertTextHdl.IsSet()) + { + OUString sText(pNewText, nNewTextLength, RTL_TEXTENCODING_UTF8); + const bool bContinue = m_aEntryInsertTextHdl.Call(sText); + if (bContinue && !sText.isEmpty()) + { + OString sFinalText(OUStringToOString(sText, RTL_TEXTENCODING_UTF8)); + g_signal_handlers_block_by_func(pEntry, reinterpret_cast<gpointer>(signalEntryInsertText), this); + gtk_editable_insert_text(GTK_EDITABLE(pEntry), sFinalText.getStr(), sFinalText.getLength(), position); + g_signal_handlers_unblock_by_func(pEntry, reinterpret_cast<gpointer>(signalEntryInsertText), this); + } + g_signal_stop_emission_by_name(pEntry, "insert-text"); + } + if (m_bAutoComplete) + { + // now check for autocompletes + if (m_nAutoCompleteIdleId) + g_source_remove(m_nAutoCompleteIdleId); + m_nAutoCompleteIdleId = g_idle_add(idleAutoComplete, this); + } + } + + static void signalChanged(GtkEntry*, gpointer widget) + { + GtkInstanceComboBox* pThis = static_cast<GtkInstanceComboBox*>(widget); + SolarMutexGuard aGuard; + pThis->fire_signal_changed(); + } + + void fire_signal_changed() + { + signal_changed(); + m_bChangedByMenu = false; + } + + static void signalPopupToggled(GtkToggleButton* /*pToggleButton*/, gpointer widget) + { + GtkInstanceComboBox* pThis = static_cast<GtkInstanceComboBox*>(widget); + pThis->signal_popup_toggled(); + } + + int get_popup_height(gint& rPopupWidth) + { + const StyleSettings& rSettings = Application::GetSettings().GetStyleSettings(); + + int nMaxRows = rSettings.GetListBoxMaximumLineCount(); + bool bAddScrollWidth = false; + int nRows = get_count_including_mru(); + if (nMaxRows < nRows) + { + nRows = nMaxRows; + bAddScrollWidth = true; + } + + GList* pColumns = gtk_tree_view_get_columns(m_pTreeView); + gint nRowHeight = get_height_row(m_pTreeView, pColumns); + g_list_free(pColumns); + + gint nSeparatorHeight = get_height_row_separator(m_pTreeView); + gint nHeight = get_height_rows(nRowHeight, nSeparatorHeight, nRows); + + // if we're using a custom renderer, limit the height to the height nMaxRows would be + // for a normal renderer, and then round down to how many custom rows fit in that + // space + if (m_nNonCustomLineHeight != -1 && nRowHeight) + { + gint nNormalHeight = get_height_rows(m_nNonCustomLineHeight, nSeparatorHeight, nMaxRows); + if (nHeight > nNormalHeight) + { + gint nRowsOnly = nNormalHeight - get_height_rows(0, nSeparatorHeight, nMaxRows); + gint nCustomRows = (nRowsOnly + (nRowHeight - 1)) / nRowHeight; + nHeight = get_height_rows(nRowHeight, nSeparatorHeight, nCustomRows); + } + } + + if (bAddScrollWidth) + rPopupWidth += rSettings.GetScrollBarSize(); + + return nHeight; + } + + void toggle_menu() + { + if (!gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(m_pToggleButton))) + { + if (m_bHoverSelection) + { + // turn hover selection back off until mouse is moved again + // *after* menu is shown again + gtk_tree_view_set_hover_selection(m_pTreeView, false); + m_bHoverSelection = false; + } + + do_ungrab(GTK_WIDGET(m_pMenuWindow)); + + gtk_widget_hide(GTK_WIDGET(m_pMenuWindow)); + + // so gdk_window_move_to_rect will work again the next time + gtk_widget_unrealize(GTK_WIDGET(m_pMenuWindow)); + + gtk_widget_set_size_request(GTK_WIDGET(m_pMenuWindow), -1, -1); + + if (!m_bActivateCalled) + tree_view_set_cursor(m_nPrePopupCursorPos); + + // undo show_menu tooltip blocking + GtkWidget* pParent = gtk_widget_get_toplevel(m_pToggleButton); + GtkSalFrame* pFrame = pParent ? GtkSalFrame::getFromWindow(pParent) : nullptr; + if (pFrame) + pFrame->UnblockTooltip(); + } + else + { + GtkWidget* pComboBox = GTK_WIDGET(getContainer()); + + gint nComboWidth = gtk_widget_get_allocated_width(pComboBox); + GtkRequisition size; + gtk_widget_get_preferred_size(GTK_WIDGET(m_pMenuWindow), nullptr, &size); + + gint nPopupWidth = size.width; + gint nPopupHeight = get_popup_height(nPopupWidth); + nPopupWidth = std::max(nPopupWidth, nComboWidth); + + gtk_widget_set_size_request(GTK_WIDGET(m_pMenuWindow), nPopupWidth, nPopupHeight); + + m_nPrePopupCursorPos = get_active(); + + m_bActivateCalled = false; + + // if we are in mru mode always start with the cursor at the top of the menu + if (m_nMaxMRUCount) + tree_view_set_cursor(0); + + show_menu(pComboBox, m_pMenuWindow); + } + } + + virtual void signal_popup_toggled() override + { + m_aQuickSelectionEngine.Reset(); + + toggle_menu(); + + bool bIsShown = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(m_pToggleButton)); + if (m_bPopupActive != bIsShown) + { + m_bPopupActive = bIsShown; + ComboBox::signal_popup_toggled(); + if (!m_bPopupActive) + { + //restore focus to the entry view when the popup is gone, which + //is what the vcl case does, to ease the transition a little + grab_focus(); + } + } + } + + static gboolean signalEntryFocusIn(GtkWidget*, GdkEvent*, gpointer widget) + { + GtkInstanceComboBox* pThis = static_cast<GtkInstanceComboBox*>(widget); + pThis->signal_entry_focus_in(); + return false; + } + + void signal_entry_focus_in() + { + signal_focus_in(); + } + + static gboolean signalEntryFocusOut(GtkWidget*, GdkEvent*, gpointer widget) + { + GtkInstanceComboBox* pThis = static_cast<GtkInstanceComboBox*>(widget); + pThis->signal_entry_focus_out(); + return false; + } + + void signal_entry_focus_out() + { + // if we have an untidy selection on losing focus remove the selection + int nStartPos, nEndPos; + if (get_entry_selection_bounds(nStartPos, nEndPos)) + { + int nMin = std::min(nStartPos, nEndPos); + int nMax = std::max(nStartPos, nEndPos); + if (nMin != 0 || nMax != get_active_text().getLength()) + select_entry_region(0, 0); + } + signal_focus_out(); + } + + static void signalEntryActivate(GtkEntry*, gpointer widget) + { + GtkInstanceComboBox* pThis = static_cast<GtkInstanceComboBox*>(widget); + pThis->signal_entry_activate(); + } + + void signal_entry_activate() + { + if (m_aEntryActivateHdl.IsSet()) + { + SolarMutexGuard aGuard; + if (m_aEntryActivateHdl.Call(*this)) + g_signal_stop_emission_by_name(m_pEntry, "activate"); + } + update_mru(); + } + + OUString get(int pos, int col) const + { + OUString sRet; + GtkTreeIter iter; + if (gtk_tree_model_iter_nth_child(m_pTreeModel, &iter, nullptr, pos)) + { + gchar* pStr; + gtk_tree_model_get(m_pTreeModel, &iter, col, &pStr, -1); + sRet = OUString(pStr, pStr ? strlen(pStr) : 0, RTL_TEXTENCODING_UTF8); + g_free(pStr); + } + return sRet; + } + + void set(int pos, int col, const OUString& rText) + { + GtkTreeIter iter; + if (gtk_tree_model_iter_nth_child(m_pTreeModel, &iter, nullptr, pos)) + { + OString aStr(OUStringToOString(rText, RTL_TEXTENCODING_UTF8)); + gtk_list_store_set(GTK_LIST_STORE(m_pTreeModel), &iter, col, aStr.getStr(), -1); + } + } + + int find(const OUString& rStr, int col, bool bSearchMRUArea) const + { + GtkTreeIter iter; + if (!gtk_tree_model_get_iter_first(m_pTreeModel, &iter)) + return -1; + + int nRet = 0; + + if (!bSearchMRUArea && m_nMRUCount) + { + if (!gtk_tree_model_iter_nth_child(m_pTreeModel, &iter, nullptr, m_nMRUCount + 1)) + return -1; + nRet += (m_nMRUCount + 1); + } + + OString aStr(OUStringToOString(rStr, RTL_TEXTENCODING_UTF8).getStr()); + do + { + gchar* pStr; + gtk_tree_model_get(m_pTreeModel, &iter, col, &pStr, -1); + const bool bEqual = g_strcmp0(pStr, aStr.getStr()) == 0; + g_free(pStr); + if (bEqual) + return nRet; + ++nRet; + } while (gtk_tree_model_iter_next(m_pTreeModel, &iter)); + + return -1; + } + + bool separator_function(GtkTreePath* path) + { + bool bFound = false; + for (auto& a : m_aSeparatorRows) + { + GtkTreePath* seppath = gtk_tree_row_reference_get_path(a.get()); + if (seppath) + { + bFound = gtk_tree_path_compare(path, seppath) == 0; + gtk_tree_path_free(seppath); + } + if (bFound) + break; + } + return bFound; + } + + bool separator_function(int pos) + { + GtkTreePath* path = gtk_tree_path_new_from_indices(pos, -1); + bool bRet = separator_function(path); + gtk_tree_path_free(path); + return bRet; + } + + static gboolean separatorFunction(GtkTreeModel* pTreeModel, GtkTreeIter* pIter, gpointer widget) + { + GtkInstanceComboBox* pThis = static_cast<GtkInstanceComboBox*>(widget); + GtkTreePath* path = gtk_tree_model_get_path(pTreeModel, pIter); + bool bRet = pThis->separator_function(path); + gtk_tree_path_free(path); + return bRet; + } + + // https://gitlab.gnome.org/GNOME/gtk/issues/310 + // + // in the absence of a built-in solution + // a) support typeahead for the case where there is no entry widget, typing ahead + // into the button itself will select via the vcl selection engine, a matching + // entry + static gboolean signalKeyPress(GtkWidget*, GdkEventKey* pEvent, gpointer widget) + { + GtkInstanceComboBox* pThis = static_cast<GtkInstanceComboBox*>(widget); + return pThis->signal_key_press(pEvent); + } + + // tdf#131076 we want return in a ComboBox to act like return in a + // GtkEntry and activate the default dialog/assistant button + bool combobox_activate() + { + GtkWidget *pComboBox = GTK_WIDGET(m_pToggleButton); + GtkWidget *pToplevel = gtk_widget_get_toplevel(pComboBox); + GtkWindow *pWindow = GTK_WINDOW(pToplevel); + if (!pWindow) + return false; + if (!GTK_IS_DIALOG(pWindow) && !GTK_IS_ASSISTANT(pWindow)) + return false; + bool bDone = false; + GtkWidget *pDefaultWidget = gtk_window_get_default_widget(pWindow); + if (pDefaultWidget && pDefaultWidget != m_pToggleButton && gtk_widget_get_sensitive(pDefaultWidget)) + bDone = gtk_widget_activate(pDefaultWidget); + return bDone; + } + + static gboolean signalEntryKeyPress(GtkWidget*, GdkEventKey* pEvent, gpointer widget) + { + GtkInstanceComboBox* pThis = static_cast<GtkInstanceComboBox*>(widget); + return pThis->signal_entry_key_press(pEvent); + } + + bool signal_entry_key_press(const GdkEventKey* pEvent) + { + KeyEvent aKEvt(GtkToVcl(*pEvent)); + + vcl::KeyCode aKeyCode = aKEvt.GetKeyCode(); + + bool bDone = false; + + auto nCode = aKeyCode.GetCode(); + switch (nCode) + { + case KEY_DOWN: + { + sal_uInt16 nKeyMod = aKeyCode.GetModifier(); + if (!nKeyMod) + { + int nCount = get_count_including_mru(); + int nActive = get_active_including_mru() + 1; + while (nActive < nCount && separator_function(nActive)) + ++nActive; + if (nActive < nCount) + set_active_including_mru(nActive, true); + bDone = true; + } + else if (nKeyMod == KEY_MOD2 && !m_bPopupActive) + { + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(m_pToggleButton), true); + bDone = true; + } + break; + } + case KEY_UP: + { + sal_uInt16 nKeyMod = aKeyCode.GetModifier(); + if (!nKeyMod) + { + int nStartBound = m_bPopupActive ? 0 : (m_nMRUCount + 1); + int nActive = get_active_including_mru() - 1; + while (nActive >= nStartBound && separator_function(nActive)) + --nActive; + if (nActive >= nStartBound) + set_active_including_mru(nActive, true); + bDone = true; + } + break; + } + case KEY_PAGEUP: + { + sal_uInt16 nKeyMod = aKeyCode.GetModifier(); + if (!nKeyMod) + { + int nCount = get_count_including_mru(); + int nStartBound = m_bPopupActive ? 0 : (m_nMRUCount + 1); + int nActive = nStartBound; + while (nActive < nCount && separator_function(nActive)) + ++nActive; + if (nActive < nCount) + set_active_including_mru(nActive, true); + bDone = true; + } + break; + } + case KEY_PAGEDOWN: + { + sal_uInt16 nKeyMod = aKeyCode.GetModifier(); + if (!nKeyMod) + { + int nActive = get_count_including_mru() - 1; + int nStartBound = m_bPopupActive ? 0 : (m_nMRUCount + 1); + while (nActive >= nStartBound && separator_function(nActive)) + --nActive; + if (nActive >= nStartBound) + set_active_including_mru(nActive, true); + bDone = true; + } + break; + } + default: + break; + } + + return bDone; + } + + bool signal_key_press(const GdkEventKey* pEvent) + { + if (m_bHoverSelection) + { + // once a key is pressed, turn off hover selection until mouse is + // moved again otherwise when the treeview scrolls it jumps to the + // position under the mouse. + gtk_tree_view_set_hover_selection(m_pTreeView, false); + m_bHoverSelection = false; + } + + KeyEvent aKEvt(GtkToVcl(*pEvent)); + + vcl::KeyCode aKeyCode = aKEvt.GetKeyCode(); + + bool bDone = false; + + auto nCode = aKeyCode.GetCode(); + switch (nCode) + { + case KEY_DOWN: + case KEY_UP: + case KEY_PAGEUP: + case KEY_PAGEDOWN: + case KEY_HOME: + case KEY_END: + case KEY_LEFT: + case KEY_RIGHT: + case KEY_RETURN: + { + m_aQuickSelectionEngine.Reset(); + sal_uInt16 nKeyMod = aKeyCode.GetModifier(); + // tdf#131076 don't let bare return toggle menu popup active, but do allow deactive + if (nCode == KEY_RETURN && !nKeyMod && !m_bPopupActive) + bDone = combobox_activate(); + else if (nCode == KEY_UP && nKeyMod == KEY_MOD2 && m_bPopupActive) + { + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(m_pToggleButton), false); + bDone = true; + } + else if (nCode == KEY_DOWN && nKeyMod == KEY_MOD2 && !m_bPopupActive) + { + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(m_pToggleButton), true); + bDone = true; + } + break; + } + case KEY_ESCAPE: + { + m_aQuickSelectionEngine.Reset(); + if (m_bPopupActive) + { + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(m_pToggleButton), false); + bDone = true; + } + break; + } + default: + // tdf#131076 let base space toggle menu popup when it's not already visible + if (nCode == KEY_SPACE && !aKeyCode.GetModifier() && !m_bPopupActive) + bDone = false; + else + bDone = m_aQuickSelectionEngine.HandleKeyEvent(aKEvt); + break; + } + + if (!bDone && !m_pEntry) + bDone = signal_entry_key_press(pEvent); + + return bDone; + } + + vcl::StringEntryIdentifier typeahead_getEntry(int nPos, OUString& out_entryText) const + { + int nEntryCount(get_count_including_mru()); + if (nPos >= nEntryCount) + nPos = 0; + out_entryText = get_text_including_mru(nPos); + + // vcl::StringEntryIdentifier does not allow for 0 values, but our position is 0-based + // => normalize + return reinterpret_cast<vcl::StringEntryIdentifier>(nPos + 1); + } + + static int typeahead_getEntryPos(vcl::StringEntryIdentifier entry) + { + // our pos is 0-based, but StringEntryIdentifier does not allow for a NULL + return reinterpret_cast<sal_Int64>(entry) - 1; + } + + void tree_view_set_cursor(int pos) + { + if (pos == -1) + { + gtk_tree_selection_unselect_all(gtk_tree_view_get_selection(m_pTreeView)); + if (m_pCellView) + gtk_cell_view_set_displayed_row(m_pCellView, nullptr); + } + else + { + GtkTreePath* path = gtk_tree_path_new_from_indices(pos, -1); + if (gtk_tree_view_get_model(m_pTreeView)) + gtk_tree_view_scroll_to_cell(m_pTreeView, path, nullptr, false, 0, 0); + gtk_tree_view_set_cursor(m_pTreeView, path, nullptr, false); + if (m_pCellView) + gtk_cell_view_set_displayed_row(m_pCellView, path); + gtk_tree_path_free(path); + } + } + + int tree_view_get_cursor() const + { + int nRet = -1; + + GtkTreePath* path; + gtk_tree_view_get_cursor(m_pTreeView, &path, nullptr); + if (path) + { + gint depth; + gint* indices = gtk_tree_path_get_indices_with_depth(path, &depth); + nRet = indices[depth-1]; + gtk_tree_path_free(path); + } + + return nRet; + } + + int get_selected_entry() const + { + if (m_bPopupActive) + return tree_view_get_cursor(); + else + return get_active_including_mru(); + } + + void set_typeahead_selected_entry(int nSelect) + { + if (m_bPopupActive) + tree_view_set_cursor(nSelect); + else + set_active_including_mru(nSelect, true); + } + + virtual vcl::StringEntryIdentifier CurrentEntry(OUString& out_entryText) const override + { + int nCurrentPos = get_selected_entry(); + return typeahead_getEntry((nCurrentPos == -1) ? 0 : nCurrentPos, out_entryText); + } + + virtual vcl::StringEntryIdentifier NextEntry(vcl::StringEntryIdentifier currentEntry, OUString& out_entryText) const override + { + int nNextPos = typeahead_getEntryPos(currentEntry) + 1; + return typeahead_getEntry(nNextPos, out_entryText); + } + + virtual void SelectEntry(vcl::StringEntryIdentifier entry) override + { + int nSelect = typeahead_getEntryPos(entry); + if (nSelect == get_selected_entry()) + { + // ignore that. This method is a callback from the QuickSelectionEngine, which means the user attempted + // to select the given entry by typing its starting letters. No need to act. + return; + } + + // normalize + int nCount = get_count_including_mru(); + if (nSelect >= nCount) + nSelect = nCount ? nCount-1 : -1; + + set_typeahead_selected_entry(nSelect); + } + + static void signalGrabBroken(GtkWidget*, GdkEventGrabBroken *pEvent, gpointer widget) + { + GtkInstanceComboBox* pThis = static_cast<GtkInstanceComboBox*>(widget); + pThis->grab_broken(pEvent); + } + + void grab_broken(const GdkEventGrabBroken *event) + { + if (event->grab_window == nullptr) + { + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(m_pToggleButton), false); + } + else + { + //try and regrab, so when we lose the grab to the menu of the color palette + //combobox we regain it so the color palette doesn't itself disappear on next + //click on the color palette combobox + do_grab(GTK_WIDGET(m_pMenuWindow)); + } + } + + static gboolean signalButtonPress(GtkWidget* pWidget, GdkEventButton* pEvent, gpointer widget) + { + GtkInstanceComboBox* pThis = static_cast<GtkInstanceComboBox*>(widget); + return pThis->button_press(pWidget, pEvent); + } + + bool button_press(GtkWidget* pWidget, GdkEventButton* pEvent) + { + //we want to pop down if the button was pressed outside our popup + gdouble x = pEvent->x_root; + gdouble y = pEvent->y_root; + gint xoffset, yoffset; + gdk_window_get_root_origin(gtk_widget_get_window(pWidget), &xoffset, &yoffset); + + GtkAllocation alloc; + gtk_widget_get_allocation(pWidget, &alloc); + xoffset += alloc.x; + yoffset += alloc.y; + + gtk_widget_get_allocation(GTK_WIDGET(m_pMenuWindow), &alloc); + gint x1 = alloc.x + xoffset; + gint y1 = alloc.y + yoffset; + gint x2 = x1 + alloc.width; + gint y2 = y1 + alloc.height; + + if (x > x1 && x < x2 && y > y1 && y < y2) + return false; + + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(m_pToggleButton), false); + + return false; + } + + static gboolean signalMotion(GtkWidget*, GdkEventMotion*, gpointer widget) + { + GtkInstanceComboBox* pThis = static_cast<GtkInstanceComboBox*>(widget); + pThis->signal_motion(); + return false; + } + + void signal_motion() + { + // if hover-selection was disabled after pressing a key, then turn it back on again + if (!m_bHoverSelection && !m_bMouseInOverlayButton) + { + gtk_tree_view_set_hover_selection(m_pTreeView, true); + m_bHoverSelection = true; + } + } + + static void signalRowActivated(GtkTreeView*, GtkTreePath*, GtkTreeViewColumn*, gpointer widget) + { + GtkInstanceComboBox* pThis = static_cast<GtkInstanceComboBox*>(widget); + pThis->handle_row_activated(); + } + + void handle_row_activated() + { + m_bActivateCalled = true; + m_bChangedByMenu = true; + disable_notify_events(); + int nActive = get_active(); + if (m_pEntry) + gtk_entry_set_text(GTK_ENTRY(m_pEntry), OUStringToOString(get_text(nActive), RTL_TEXTENCODING_UTF8).getStr()); + else + tree_view_set_cursor(nActive); + enable_notify_events(); + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(m_pToggleButton), false); + fire_signal_changed(); + update_mru(); + } + + void do_clear() + { + disable_notify_events(); + gtk_tree_view_set_row_separator_func(m_pTreeView, nullptr, nullptr, nullptr); + m_aSeparatorRows.clear(); + gtk_list_store_clear(GTK_LIST_STORE(m_pTreeModel)); + m_nMRUCount = 0; + enable_notify_events(); + } + + virtual int get_max_mru_count() const override + { + return m_nMaxMRUCount; + } + + virtual void set_max_mru_count(int nMaxMRUCount) override + { + m_nMaxMRUCount = nMaxMRUCount; + update_mru(); + } + + void update_mru() + { + int nMRUCount = m_nMRUCount; + + if (m_nMaxMRUCount) + { + OUString sActiveText = get_active_text(); + OUString sActiveId = get_active_id(); + insert_including_mru(0, sActiveText, &sActiveId, nullptr, nullptr); + ++m_nMRUCount; + + for (int i = 1; i < m_nMRUCount - 1; ++i) + { + if (get_text_including_mru(i) == sActiveText) + { + remove_including_mru(i); + --m_nMRUCount; + break; + } + } + } + + while (m_nMRUCount > m_nMaxMRUCount) + { + remove_including_mru(m_nMRUCount - 1); + --m_nMRUCount; + } + + if (m_nMRUCount && !nMRUCount) + insert_separator_including_mru(m_nMRUCount, "separator"); + else if (!m_nMRUCount && nMRUCount) + remove_including_mru(m_nMRUCount); // remove separator + } + + int get_count_including_mru() const + { + return gtk_tree_model_iter_n_children(m_pTreeModel, nullptr); + } + + int get_active_including_mru() const + { + return tree_view_get_cursor(); + } + + void set_active_including_mru(int pos, bool bInteractive) + { + disable_notify_events(); + + tree_view_set_cursor(pos); + + if (m_pEntry) + { + if (pos != -1) + gtk_entry_set_text(GTK_ENTRY(m_pEntry), OUStringToOString(get_text_including_mru(pos), RTL_TEXTENCODING_UTF8).getStr()); + else + gtk_entry_set_text(GTK_ENTRY(m_pEntry), ""); + } + + m_bChangedByMenu = false; + enable_notify_events(); + + if (bInteractive && !m_bPopupActive) + signal_changed(); + } + + int find_text_including_mru(const OUString& rStr, bool bSearchMRU) const + { + return find(rStr, m_nTextCol, bSearchMRU); + } + + int find_id_including_mru(const OUString& rId, bool bSearchMRU) const + { + return find(rId, m_nIdCol, bSearchMRU); + } + + OUString get_text_including_mru(int pos) const + { + return get(pos, m_nTextCol); + } + + OUString get_id_including_mru(int pos) const + { + return get(pos, m_nIdCol); + } + + void set_id_including_mru(int pos, const OUString& rId) + { + set(pos, m_nIdCol, rId); + } + + void remove_including_mru(int pos) + { + disable_notify_events(); + GtkTreeIter iter; + gtk_tree_model_iter_nth_child(m_pTreeModel, &iter, nullptr, pos); + if (!m_aSeparatorRows.empty()) + { + bool bFound = false; + + GtkTreePath* pPath = gtk_tree_path_new_from_indices(pos, -1); + + for (auto aIter = m_aSeparatorRows.begin(); aIter != m_aSeparatorRows.end(); ++aIter) + { + GtkTreePath* seppath = gtk_tree_row_reference_get_path(aIter->get()); + if (seppath) + { + if (gtk_tree_path_compare(pPath, seppath) == 0) + bFound = true; + gtk_tree_path_free(seppath); + } + if (bFound) + { + m_aSeparatorRows.erase(aIter); + break; + } + } + + gtk_tree_path_free(pPath); + } + gtk_list_store_remove(GTK_LIST_STORE(m_pTreeModel), &iter); + enable_notify_events(); + } + + void insert_separator_including_mru(int pos, const OUString& rId) + { + disable_notify_events(); + GtkTreeIter iter; + if (!gtk_tree_view_get_row_separator_func(m_pTreeView)) + gtk_tree_view_set_row_separator_func(m_pTreeView, separatorFunction, this, nullptr); + insert_row(GTK_LIST_STORE(m_pTreeModel), iter, pos, &rId, "", nullptr, nullptr); + GtkTreePath* pPath = gtk_tree_path_new_from_indices(pos, -1); + m_aSeparatorRows.emplace_back(gtk_tree_row_reference_new(m_pTreeModel, pPath)); + gtk_tree_path_free(pPath); + enable_notify_events(); + } + + void insert_including_mru(int pos, const OUString& rText, const OUString* pId, const OUString* pIconName, VirtualDevice* pImageSurface) + { + disable_notify_events(); + GtkTreeIter iter; + insert_row(GTK_LIST_STORE(m_pTreeModel), iter, pos, pId, rText, pIconName, pImageSurface); + enable_notify_events(); + } + + static gboolean signalGetChildPosition(GtkOverlay*, GtkWidget*, GdkRectangle* pAllocation, gpointer widget) + { + GtkInstanceComboBox* pThis = static_cast<GtkInstanceComboBox*>(widget); + return pThis->signal_get_child_position(pAllocation); + } + + bool signal_get_child_position(GdkRectangle* pAllocation) + { + if (!gtk_widget_get_visible(GTK_WIDGET(m_pOverlayButton))) + return false; + if (!gtk_widget_get_realized(GTK_WIDGET(m_pTreeView))) + return false; + int nRow = find_id_including_mru(m_sMenuButtonRow, true); + if (nRow == -1) + return false; + + gtk_widget_get_preferred_width(GTK_WIDGET(m_pOverlayButton), &pAllocation->width, nullptr); + + GtkTreePath* pPath = gtk_tree_path_new_from_indices(nRow, -1); + GList* pColumns = gtk_tree_view_get_columns(m_pTreeView); + tools::Rectangle aRect = get_row_area(m_pTreeView, pColumns, pPath); + gtk_tree_path_free(pPath); + g_list_free(pColumns); + + pAllocation->x = aRect.Right() - pAllocation->width; + pAllocation->y = aRect.Top(); + pAllocation->height = aRect.GetHeight(); + + return true; + } + + static gboolean signalOverlayButtonCrossing(GtkWidget*, GdkEventCrossing* pEvent, gpointer widget) + { + GtkInstanceComboBox* pThis = static_cast<GtkInstanceComboBox*>(widget); + pThis->signal_overlay_button_crossing(pEvent->type == GDK_ENTER_NOTIFY); + return false; + } + + void signal_overlay_button_crossing(bool bEnter) + { + m_bMouseInOverlayButton = bEnter; + if (bEnter) + { + if (m_bHoverSelection) + { + // once toggled button is pressed, turn off hover selection until + // mouse leaves the overlay button + gtk_tree_view_set_hover_selection(m_pTreeView, false); + m_bHoverSelection = false; + } + int nRow = find_id_including_mru(m_sMenuButtonRow, true); + assert(nRow != -1); + tree_view_set_cursor(nRow); // select the buttons row + } + } + + void signal_combo_mnemonic_activate() + { + if (m_pEntry) + gtk_widget_grab_focus(m_pEntry); + else + gtk_widget_grab_focus(m_pToggleButton); + } + + static gboolean signalComboMnemonicActivate(GtkWidget*, gboolean, gpointer widget) + { + GtkInstanceComboBox* pThis = static_cast<GtkInstanceComboBox*>(widget); + pThis->signal_combo_mnemonic_activate(); + return true; + } + +public: + GtkInstanceComboBox(GtkBuilder* pComboBuilder, GtkComboBox* pComboBox, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : GtkInstanceContainer(GTK_CONTAINER(gtk_builder_get_object(pComboBuilder, "box")), pBuilder, bTakeOwnership) + , m_pComboBuilder(pComboBuilder) + , m_pComboBox(pComboBox) + , m_pOverlay(GTK_OVERLAY(gtk_builder_get_object(pComboBuilder, "overlay"))) + , m_pTreeView(GTK_TREE_VIEW(gtk_builder_get_object(pComboBuilder, "treeview"))) + , m_pOverlayButton(GTK_MENU_BUTTON(gtk_builder_get_object(pComboBuilder, "overlaybutton"))) + , m_pMenuWindow(GTK_WINDOW(gtk_builder_get_object(pComboBuilder, "popup"))) + , m_pTreeModel(gtk_combo_box_get_model(pComboBox)) + , m_pButtonTextRenderer(nullptr) + , m_pToggleButton(GTK_WIDGET(gtk_builder_get_object(pComboBuilder, "button"))) + , m_pEntry(GTK_WIDGET(gtk_builder_get_object(pComboBuilder, "entry"))) + , m_pCellView(nullptr) + , m_aQuickSelectionEngine(*this) + , m_bHoverSelection(false) + , m_bMouseInOverlayButton(false) + , m_bPopupActive(false) + , m_bAutoComplete(false) + , m_bAutoCompleteCaseSensitive(false) + , m_bChangedByMenu(false) + , m_bCustomRenderer(false) + , m_bActivateCalled(false) + , m_nTextCol(gtk_combo_box_get_entry_text_column(pComboBox)) + , m_nIdCol(gtk_combo_box_get_id_column(pComboBox)) + , m_nToggleFocusInSignalId(0) + , m_nToggleFocusOutSignalId(0) + , m_nRowActivatedSignalId(g_signal_connect(m_pTreeView, "row-activated", G_CALLBACK(signalRowActivated), this)) + , m_nChangedSignalId(g_signal_connect(m_pEntry, "changed", G_CALLBACK(signalChanged), this)) + , m_nPopupShownSignalId(g_signal_connect(m_pToggleButton, "toggled", G_CALLBACK(signalPopupToggled), this)) + , m_nAutoCompleteIdleId(0) + , m_nNonCustomLineHeight(-1) + , m_nPrePopupCursorPos(-1) + , m_nMRUCount(0) + , m_nMaxMRUCount(0) + { + int nActive = gtk_combo_box_get_active(m_pComboBox); + insertAsParent(GTK_WIDGET(m_pComboBox), GTK_WIDGET(getContainer())); + gtk_widget_set_visible(GTK_WIDGET(m_pComboBox), false); + gtk_widget_set_no_show_all(GTK_WIDGET(m_pComboBox), true); + + gtk_tree_view_set_model(m_pTreeView, m_pTreeModel); + /* tdf#136455 gtk_combo_box_set_model with a null Model should be good + enough. But in practice, while the ComboBox model is unset, GTK + doesn't unset the ComboBox menus model, so that remains listening to + additions to the ListStore and slowing things down massively. + Using a new model does reset the menu to listen to that unused one instead */ + gtk_combo_box_set_model(m_pComboBox, GTK_TREE_MODEL(gtk_list_store_new (2, G_TYPE_STRING, G_TYPE_STRING))); + + GtkTreeViewColumn* pCol = gtk_tree_view_column_new(); + gtk_tree_view_append_column(m_pTreeView, pCol); + + bool bPixbufUsedSurface = gtk_tree_model_get_n_columns(m_pTreeModel) == 4; + + GList* cells = gtk_cell_layout_get_cells(GTK_CELL_LAYOUT(m_pComboBox)); + // move the cell renderers from the combobox to the replacement treeview + m_pMenuTextRenderer = static_cast<GtkCellRenderer*>(cells->data); + for (GList* pRenderer = g_list_first(cells); pRenderer; pRenderer = g_list_next(pRenderer)) + { + GtkCellRenderer* pCellRenderer = GTK_CELL_RENDERER(pRenderer->data); + bool bTextRenderer = pCellRenderer == m_pMenuTextRenderer; + gtk_tree_view_column_pack_end(pCol, pCellRenderer, bTextRenderer); + if (!bTextRenderer) + { + if (bPixbufUsedSurface) + gtk_tree_view_column_set_attributes(pCol, pCellRenderer, "surface", 3, nullptr); + else + gtk_tree_view_column_set_attributes(pCol, pCellRenderer, "pixbuf", 2, nullptr); + } + } + + gtk_tree_view_column_set_attributes(pCol, m_pMenuTextRenderer, "text", m_nTextCol, nullptr); + + if (gtk_combo_box_get_has_entry(m_pComboBox)) + { + m_bAutoComplete = true; + m_nEntryInsertTextSignalId = g_signal_connect(m_pEntry, "insert-text", G_CALLBACK(signalEntryInsertText), this); + m_nEntryActivateSignalId = g_signal_connect(m_pEntry, "activate", G_CALLBACK(signalEntryActivate), this); + m_nEntryFocusInSignalId = g_signal_connect(m_pEntry, "focus-in-event", G_CALLBACK(signalEntryFocusIn), this); + m_nEntryFocusOutSignalId = g_signal_connect(m_pEntry, "focus-out-event", G_CALLBACK(signalEntryFocusOut), this); + m_nEntryKeyPressEventSignalId = g_signal_connect(m_pEntry, "key-press-event", G_CALLBACK(signalEntryKeyPress), this); + m_nKeyPressEventSignalId = 0; + } + else + { + gtk_widget_set_visible(m_pEntry, false); + m_pEntry = nullptr; + + GtkWidget* pArrow = GTK_WIDGET(gtk_builder_get_object(pComboBuilder, "arrow")); + gtk_container_child_set(getContainer(), m_pToggleButton, "expand", true, nullptr); + + auto m_pCellArea = gtk_cell_area_box_new(); + m_pCellView = GTK_CELL_VIEW(gtk_cell_view_new_with_context(m_pCellArea, nullptr)); + gtk_widget_set_hexpand(GTK_WIDGET(m_pCellView), true); + GtkBox* pBox = GTK_BOX(gtk_widget_get_parent(pArrow)); + + gint nImageSpacing(2); + GtkStyleContext *pContext = gtk_widget_get_style_context(GTK_WIDGET(m_pToggleButton)); + gtk_style_context_get_style(pContext, "image-spacing", &nImageSpacing, nullptr); + gtk_box_set_spacing(pBox, nImageSpacing); + + gtk_box_pack_start(pBox, GTK_WIDGET(m_pCellView), false, true, 0); + + gtk_cell_view_set_fit_model(m_pCellView, true); + gtk_cell_view_set_model(m_pCellView, m_pTreeModel); + + m_pButtonTextRenderer = gtk_cell_renderer_text_new(); + gtk_cell_layout_pack_end(GTK_CELL_LAYOUT(m_pCellView), m_pButtonTextRenderer, true); + gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(m_pCellView), m_pButtonTextRenderer, "text", m_nTextCol, nullptr); + if (g_list_length(cells) > 1) + { + GtkCellRenderer* pCellRenderer = gtk_cell_renderer_pixbuf_new(); + gtk_cell_layout_pack_end(GTK_CELL_LAYOUT(m_pCellView), pCellRenderer, false); + if (bPixbufUsedSurface) + gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(m_pCellView), pCellRenderer, "surface", 3, nullptr); + else + gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(m_pCellView), pCellRenderer, "pixbuf", 2, nullptr); + } + + gtk_widget_show_all(GTK_WIDGET(m_pCellView)); + + m_nEntryInsertTextSignalId = 0; + m_nEntryActivateSignalId = 0; + m_nEntryFocusInSignalId = 0; + m_nEntryFocusOutSignalId = 0; + m_nEntryKeyPressEventSignalId = 0; + m_nKeyPressEventSignalId = g_signal_connect(m_pToggleButton, "key-press-event", G_CALLBACK(signalKeyPress), this); + } + + g_list_free(cells); + + if (nActive != -1) + tree_view_set_cursor(nActive); + + g_signal_connect(getContainer(), "mnemonic-activate", G_CALLBACK(signalComboMnemonicActivate), this); + + g_signal_connect(m_pMenuWindow, "grab-broken-event", G_CALLBACK(signalGrabBroken), this); + g_signal_connect(m_pMenuWindow, "button-press-event", G_CALLBACK(signalButtonPress), this); + g_signal_connect(m_pMenuWindow, "motion-notify-event", G_CALLBACK(signalMotion), this); + // support typeahead for the menu itself, typing into the menu will + // select via the vcl selection engine, a matching entry. + g_signal_connect(m_pMenuWindow, "key-press-event", G_CALLBACK(signalKeyPress), this); + + g_signal_connect(m_pOverlay, "get-child-position", G_CALLBACK(signalGetChildPosition), this); + gtk_overlay_add_overlay(m_pOverlay, GTK_WIDGET(m_pOverlayButton)); + g_signal_connect(m_pOverlayButton, "leave-notify-event", G_CALLBACK(signalOverlayButtonCrossing), this); + g_signal_connect(m_pOverlayButton, "enter-notify-event", G_CALLBACK(signalOverlayButtonCrossing), this); + } + + virtual int get_active() const override + { + int nActive = get_active_including_mru(); + if (nActive == -1) + return -1; + + if (m_nMRUCount) + { + if (nActive < m_nMRUCount) + nActive = find_text(get_text_including_mru(nActive)); + else + nActive -= (m_nMRUCount + 1); + } + + return nActive; + } + + virtual OUString get_active_id() const override + { + int nActive = get_active(); + return nActive != -1 ? get_id(nActive) : OUString(); + } + + virtual void set_active_id(const OUString& rStr) override + { + set_active(find_id(rStr)); + m_bChangedByMenu = false; + } + + virtual void set_size_request(int nWidth, int nHeight) override + { + if (m_pButtonTextRenderer) + { + // tweak the cell render to get a narrower size to stick + if (nWidth != -1) + { + // this bit isn't great, I really want to be able to ellipse the text in the comboboxtext itself and let + // the popup menu render them in full, in the interim ellipse both of them + g_object_set(G_OBJECT(m_pButtonTextRenderer), "ellipsize", PANGO_ELLIPSIZE_MIDDLE, nullptr); + + // to find out how much of the width of the combobox belongs to the cell, set + // the cell and widget to the min cell width and see what the difference is + int min; + gtk_cell_renderer_get_preferred_width(m_pButtonTextRenderer, m_pWidget, &min, nullptr); + gtk_cell_renderer_set_fixed_size(m_pButtonTextRenderer, min, -1); + gtk_widget_set_size_request(m_pWidget, min, -1); + int nNonCellWidth = get_preferred_size().Width() - min; + + int nCellWidth = nWidth - nNonCellWidth; + if (nCellWidth >= 0) + { + // now set the cell to the max width which it can be within the + // requested widget width + gtk_cell_renderer_set_fixed_size(m_pButtonTextRenderer, nWidth - nNonCellWidth, -1); + } + } + else + { + g_object_set(G_OBJECT(m_pButtonTextRenderer), "ellipsize", PANGO_ELLIPSIZE_NONE, nullptr); + gtk_cell_renderer_set_fixed_size(m_pButtonTextRenderer, -1, -1); + } + } + + gtk_widget_set_size_request(m_pWidget, nWidth, nHeight); + } + + virtual void set_active(int pos) override + { + if (m_nMRUCount && pos != -1) + pos += (m_nMRUCount + 1); + set_active_including_mru(pos, false); + } + + virtual OUString get_active_text() const override + { + if (m_pEntry) + { + const gchar* pText = gtk_entry_get_text(GTK_ENTRY(m_pEntry)); + return OUString(pText, pText ? strlen(pText) : 0, RTL_TEXTENCODING_UTF8); + } + + int nActive = get_active(); + if (nActive == -1) + return OUString(); + + return get_text(nActive); + } + + virtual OUString get_text(int pos) const override + { + if (m_nMRUCount) + pos += (m_nMRUCount + 1); + return get_text_including_mru(pos); + } + + virtual OUString get_id(int pos) const override + { + if (m_nMRUCount) + pos += (m_nMRUCount + 1); + return get_id_including_mru(pos); + } + + virtual void set_id(int pos, const OUString& rId) override + { + if (m_nMRUCount) + pos += (m_nMRUCount + 1); + set_id_including_mru(pos, rId); + } + + virtual void insert_vector(const std::vector<weld::ComboBoxEntry>& rItems, bool bKeepExisting) override + { + freeze(); + if (!bKeepExisting) + clear(); + GtkTreeIter iter; + for (const auto& rItem : rItems) + { + insert_row(GTK_LIST_STORE(m_pTreeModel), iter, -1, rItem.sId.isEmpty() ? nullptr : &rItem.sId, + rItem.sString, rItem.sImage.isEmpty() ? nullptr : &rItem.sImage, nullptr); + } + thaw(); + } + + virtual void remove(int pos) override + { + if (m_nMRUCount) + pos += (m_nMRUCount + 1); + remove_including_mru(pos); + } + + virtual void insert(int pos, const OUString& rText, const OUString* pId, const OUString* pIconName, VirtualDevice* pImageSurface) override + { + if (m_nMRUCount && pos != -1) + pos += (m_nMRUCount + 1); + insert_including_mru(pos, rText, pId, pIconName, pImageSurface); + } + + virtual void insert_separator(int pos, const OUString& rId) override + { + pos = pos == -1 ? get_count() : pos; + if (m_nMRUCount) + pos += (m_nMRUCount + 1); + insert_separator_including_mru(pos, rId); + } + + virtual int get_count() const override + { + int nCount = get_count_including_mru(); + if (m_nMRUCount) + nCount -= (m_nMRUCount + 1); + return nCount; + } + + virtual int find_text(const OUString& rStr) const override + { + int nPos = find_text_including_mru(rStr, false); + if (nPos != -1 && m_nMRUCount) + nPos -= (m_nMRUCount + 1); + return nPos; + } + + virtual int find_id(const OUString& rId) const override + { + int nPos = find_id_including_mru(rId, false); + if (nPos != -1 && m_nMRUCount) + nPos -= (m_nMRUCount + 1); + return nPos; + } + + virtual void clear() override + { + do_clear(); + } + + virtual void make_sorted() override + { + m_xSorter.reset(new comphelper::string::NaturalStringSorter( + ::comphelper::getProcessComponentContext(), + Application::GetSettings().GetUILanguageTag().getLocale())); + GtkTreeSortable* pSortable = GTK_TREE_SORTABLE(m_pTreeModel); + gtk_tree_sortable_set_sort_column_id(pSortable, m_nTextCol, GTK_SORT_ASCENDING); + gtk_tree_sortable_set_sort_func(pSortable, m_nTextCol, default_sort_func, m_xSorter.get(), nullptr); + } + + virtual bool has_entry() const override + { + return gtk_combo_box_get_has_entry(m_pComboBox); + } + + virtual void set_entry_message_type(weld::EntryMessageType eType) override + { + assert(m_pEntry); + ::set_entry_message_type(GTK_ENTRY(m_pEntry), eType); + } + + virtual void set_entry_text(const OUString& rText) override + { + assert(m_pEntry); + disable_notify_events(); + gtk_entry_set_text(GTK_ENTRY(m_pEntry), OUStringToOString(rText, RTL_TEXTENCODING_UTF8).getStr()); + enable_notify_events(); + } + + virtual void set_entry_width_chars(int nChars) override + { + assert(m_pEntry); + disable_notify_events(); + gtk_entry_set_width_chars(GTK_ENTRY(m_pEntry), nChars); + gtk_entry_set_max_width_chars(GTK_ENTRY(m_pEntry), nChars); + enable_notify_events(); + } + + virtual void set_entry_max_length(int nChars) override + { + assert(m_pEntry); + disable_notify_events(); + gtk_entry_set_max_length(GTK_ENTRY(m_pEntry), nChars); + enable_notify_events(); + } + + virtual void select_entry_region(int nStartPos, int nEndPos) override + { + assert(m_pEntry); + disable_notify_events(); + gtk_editable_select_region(GTK_EDITABLE(m_pEntry), nStartPos, nEndPos); + enable_notify_events(); + } + + virtual bool get_entry_selection_bounds(int& rStartPos, int &rEndPos) override + { + assert(m_pEntry); + return gtk_editable_get_selection_bounds(GTK_EDITABLE(m_pEntry), &rStartPos, &rEndPos); + } + + virtual void set_entry_completion(bool bEnable, bool bCaseSensitive) override + { + m_bAutoComplete = bEnable; + m_bAutoCompleteCaseSensitive = bCaseSensitive; + } + + virtual void set_entry_placeholder_text(const OUString& rText) override + { + assert(m_pEntry); + gtk_entry_set_placeholder_text(GTK_ENTRY(m_pEntry), rText.toUtf8().getStr()); + } + + virtual void set_entry_editable(bool bEditable) override + { + assert(m_pEntry); + gtk_editable_set_editable(GTK_EDITABLE(m_pEntry), bEditable); + } + + virtual void cut_entry_clipboard() override + { + assert(m_pEntry); + gtk_editable_cut_clipboard(GTK_EDITABLE(m_pEntry)); + } + + virtual void copy_entry_clipboard() override + { + assert(m_pEntry); + gtk_editable_copy_clipboard(GTK_EDITABLE(m_pEntry)); + } + + virtual void paste_entry_clipboard() override + { + assert(m_pEntry); + gtk_editable_paste_clipboard(GTK_EDITABLE(m_pEntry)); + } + + virtual void set_entry_font(const vcl::Font& rFont) override + { + m_xFont.reset(new vcl::Font(rFont)); + PangoAttrList* pAttrList = create_attr_list(rFont); + assert(m_pEntry); + gtk_entry_set_attributes(GTK_ENTRY(m_pEntry), pAttrList); + pango_attr_list_unref(pAttrList); + } + + virtual vcl::Font get_entry_font() override + { + if (m_xFont) + return *m_xFont; + assert(m_pEntry); + PangoContext* pContext = gtk_widget_get_pango_context(m_pEntry); + return pango_to_vcl(pango_context_get_font_description(pContext), + Application::GetSettings().GetUILanguageTag().getLocale()); + } + + virtual void disable_notify_events() override + { + if (m_pEntry) + { + g_signal_handler_block(m_pEntry, m_nEntryInsertTextSignalId); + g_signal_handler_block(m_pEntry, m_nEntryActivateSignalId); + g_signal_handler_block(m_pEntry, m_nEntryFocusInSignalId); + g_signal_handler_block(m_pEntry, m_nEntryFocusOutSignalId); + g_signal_handler_block(m_pEntry, m_nEntryKeyPressEventSignalId); + g_signal_handler_block(m_pEntry, m_nChangedSignalId); + } + else + g_signal_handler_block(m_pToggleButton, m_nKeyPressEventSignalId); + if (m_nToggleFocusInSignalId) + g_signal_handler_block(m_pToggleButton, m_nToggleFocusInSignalId); + if (m_nToggleFocusOutSignalId) + g_signal_handler_block(m_pToggleButton, m_nToggleFocusOutSignalId); + g_signal_handler_block(m_pTreeView, m_nRowActivatedSignalId); + g_signal_handler_block(m_pToggleButton, m_nPopupShownSignalId); + GtkInstanceContainer::disable_notify_events(); + } + + virtual void enable_notify_events() override + { + GtkInstanceContainer::enable_notify_events(); + g_signal_handler_unblock(m_pToggleButton, m_nPopupShownSignalId); + g_signal_handler_unblock(m_pTreeView, m_nRowActivatedSignalId); + if (m_nToggleFocusInSignalId) + g_signal_handler_unblock(m_pToggleButton, m_nToggleFocusInSignalId); + if (m_nToggleFocusOutSignalId) + g_signal_handler_unblock(m_pToggleButton, m_nToggleFocusOutSignalId); + if (m_pEntry) + { + g_signal_handler_unblock(m_pEntry, m_nChangedSignalId); + g_signal_handler_unblock(m_pEntry, m_nEntryActivateSignalId); + g_signal_handler_unblock(m_pEntry, m_nEntryFocusInSignalId); + g_signal_handler_unblock(m_pEntry, m_nEntryFocusOutSignalId); + g_signal_handler_unblock(m_pEntry, m_nEntryKeyPressEventSignalId); + g_signal_handler_unblock(m_pEntry, m_nEntryInsertTextSignalId); + } + else + g_signal_handler_unblock(m_pToggleButton, m_nKeyPressEventSignalId); + } + + virtual void freeze() override + { + disable_notify_events(); + g_object_ref(m_pTreeModel); + GtkInstanceContainer::freeze(); + gtk_tree_view_set_model(m_pTreeView, nullptr); + if (m_xSorter) + { + GtkTreeSortable* pSortable = GTK_TREE_SORTABLE(m_pTreeModel); + gtk_tree_sortable_set_sort_column_id(pSortable, GTK_TREE_SORTABLE_UNSORTED_SORT_COLUMN_ID, GTK_SORT_ASCENDING); + } + enable_notify_events(); + } + + virtual void thaw() override + { + disable_notify_events(); + if (m_xSorter) + { + GtkTreeSortable* pSortable = GTK_TREE_SORTABLE(m_pTreeModel); + gtk_tree_sortable_set_sort_column_id(pSortable, m_nTextCol, GTK_SORT_ASCENDING); + } + gtk_tree_view_set_model(m_pTreeView, m_pTreeModel); + + GtkInstanceContainer::thaw(); + g_object_unref(m_pTreeModel); + enable_notify_events(); + } + + virtual bool get_popup_shown() const override + { + return m_bPopupActive; + } + + virtual void connect_focus_in(const Link<Widget&, void>& rLink) override + { + if (!m_nToggleFocusInSignalId) + m_nToggleFocusInSignalId = g_signal_connect(m_pToggleButton, "focus-in-event", G_CALLBACK(signalFocusIn), this); + weld::Widget::connect_focus_in(rLink); + } + + virtual void connect_focus_out(const Link<Widget&, void>& rLink) override + { + if (!m_nToggleFocusOutSignalId) + m_nToggleFocusOutSignalId = g_signal_connect(m_pToggleButton, "focus-out-event", G_CALLBACK(signalFocusOut), this); + weld::Widget::connect_focus_out(rLink); + } + + virtual void grab_focus() override + { + disable_notify_events(); + if (m_pEntry) + gtk_widget_grab_focus(m_pEntry); + else + gtk_widget_grab_focus(m_pToggleButton); + enable_notify_events(); + } + + virtual bool has_focus() const override + { + if (m_pEntry && gtk_widget_has_focus(m_pEntry)) + return true; + + if (gtk_widget_has_focus(m_pToggleButton)) + return true; + + if (gtk_widget_get_visible(GTK_WIDGET(m_pMenuWindow))) + { + if (gtk_widget_has_focus(GTK_WIDGET(m_pOverlayButton)) || gtk_widget_has_focus(GTK_WIDGET(m_pTreeView))) + return true; + } + + return GtkInstanceWidget::has_focus(); + } + + virtual bool changed_by_direct_pick() const override + { + return m_bChangedByMenu; + } + + virtual void set_custom_renderer(bool bOn) override + { + if (bOn == m_bCustomRenderer) + return; + GList* pColumns = gtk_tree_view_get_columns(m_pTreeView); + // keep the original height around for optimal popup height calculation + m_nNonCustomLineHeight = bOn ? ::get_height_row(m_pTreeView, pColumns) : -1; + GtkTreeViewColumn* pColumn = GTK_TREE_VIEW_COLUMN(pColumns->data); + gtk_cell_layout_clear(GTK_CELL_LAYOUT(pColumn)); + if (bOn) + { + GtkCellRenderer *pRenderer = custom_cell_renderer_surface_new(); + GValue value = G_VALUE_INIT; + g_value_init(&value, G_TYPE_POINTER); + g_value_set_pointer(&value, static_cast<gpointer>(this)); + g_object_set_property(G_OBJECT(pRenderer), "instance", &value); + gtk_tree_view_column_pack_start(pColumn, pRenderer, true); + gtk_tree_view_column_add_attribute(pColumn, pRenderer, "text", m_nTextCol); + gtk_tree_view_column_add_attribute(pColumn, pRenderer, "id", m_nIdCol); + } + else + { + GtkCellRenderer *pRenderer = gtk_cell_renderer_text_new(); + gtk_tree_view_column_pack_start(pColumn, pRenderer, true); + gtk_tree_view_column_add_attribute(pColumn, pRenderer, "text", m_nTextCol); + } + g_list_free(pColumns); + m_bCustomRenderer = bOn; + } + + void call_signal_custom_render(VirtualDevice& rOutput, const tools::Rectangle& rRect, bool bSelected, const OUString& rId) + { + signal_custom_render(rOutput, rRect, bSelected, rId); + } + + Size call_signal_custom_get_size(VirtualDevice& rOutput) + { + return signal_custom_get_size(rOutput); + } + + VclPtr<VirtualDevice> create_render_virtual_device() const override + { + return create_virtual_device(); + } + + virtual void set_item_menu(const OString& rIdent, weld::Menu* pMenu) override + { + m_xCustomMenuButtonHelper.reset(); + GtkInstanceMenu* pPopoverWidget = dynamic_cast<GtkInstanceMenu*>(pMenu); + GtkWidget* pMenuWidget = GTK_WIDGET(pPopoverWidget ? pPopoverWidget->getMenu() : nullptr); + gtk_menu_button_set_popup(m_pOverlayButton, pMenuWidget); + gtk_widget_set_visible(GTK_WIDGET(m_pOverlayButton), pMenuWidget != nullptr); + gtk_widget_queue_resize_no_redraw(GTK_WIDGET(m_pOverlayButton)); // force location recalc + if (pMenuWidget) + m_xCustomMenuButtonHelper.reset(new CustomRenderMenuButtonHelper(GTK_MENU(pMenuWidget), GTK_TOGGLE_BUTTON(m_pToggleButton))); + m_sMenuButtonRow = OUString::fromUtf8(rIdent); + } + + OUString get_mru_entries() const override + { + const sal_Unicode cSep = ';'; + + OUStringBuffer aEntries; + for (sal_Int32 n = 0; n < m_nMRUCount; n++) + { + aEntries.append(get_text_including_mru(n)); + if (n < m_nMRUCount - 1) + aEntries.append(cSep); + } + return aEntries.makeStringAndClear(); + } + + virtual void set_mru_entries(const OUString& rEntries) override + { + const sal_Unicode cSep = ';'; + + // Remove old MRU entries + for (sal_Int32 n = m_nMRUCount; n;) + remove_including_mru(--n); + + sal_Int32 nMRUCount = 0; + sal_Int32 nIndex = 0; + do + { + OUString aEntry = rEntries.getToken(0, cSep, nIndex); + // Accept only existing entries + int nPos = find_text(aEntry); + if (nPos != -1) + { + OUString sId = get_id(nPos); + insert_including_mru(0, aEntry, &sId, nullptr, nullptr); + ++nMRUCount; + } + } + while (nIndex >= 0); + + if (nMRUCount && !m_nMRUCount) + insert_separator_including_mru(nMRUCount, "separator"); + else if (!nMRUCount && m_nMRUCount) + remove_including_mru(m_nMRUCount); // remove separator + + m_nMRUCount = nMRUCount; + } + + int get_menu_button_width() const override + { + bool bVisible = gtk_widget_get_visible(GTK_WIDGET(m_pOverlayButton)); + if (!bVisible) + gtk_widget_set_visible(GTK_WIDGET(m_pOverlayButton), true); + gint nWidth; + gtk_widget_get_preferred_width(GTK_WIDGET(m_pOverlayButton), &nWidth, nullptr); + if (!bVisible) + gtk_widget_set_visible(GTK_WIDGET(m_pOverlayButton), false); + return nWidth; + } + + virtual ~GtkInstanceComboBox() override + { + m_xCustomMenuButtonHelper.reset(); + do_clear(); + if (m_nAutoCompleteIdleId) + g_source_remove(m_nAutoCompleteIdleId); + if (m_pEntry) + { + g_signal_handler_disconnect(m_pEntry, m_nChangedSignalId); + g_signal_handler_disconnect(m_pEntry, m_nEntryInsertTextSignalId); + g_signal_handler_disconnect(m_pEntry, m_nEntryActivateSignalId); + g_signal_handler_disconnect(m_pEntry, m_nEntryFocusInSignalId); + g_signal_handler_disconnect(m_pEntry, m_nEntryFocusOutSignalId); + g_signal_handler_disconnect(m_pEntry, m_nEntryKeyPressEventSignalId); + } + else + g_signal_handler_disconnect(m_pToggleButton, m_nKeyPressEventSignalId); + if (m_nToggleFocusInSignalId) + g_signal_handler_disconnect(m_pToggleButton, m_nToggleFocusInSignalId); + if (m_nToggleFocusOutSignalId) + g_signal_handler_disconnect(m_pToggleButton, m_nToggleFocusOutSignalId); + g_signal_handler_disconnect(m_pTreeView, m_nRowActivatedSignalId); + g_signal_handler_disconnect(m_pToggleButton, m_nPopupShownSignalId); + + gtk_combo_box_set_model(m_pComboBox, m_pTreeModel); + gtk_tree_view_set_model(m_pTreeView, nullptr); + + // restore original hierarchy in dtor so a new GtkInstanceComboBox will + // result in the same layout each time + { + g_object_ref(m_pComboBox); + + GtkContainer* pContainer = getContainer(); + + gtk_container_remove(pContainer, GTK_WIDGET(m_pComboBox)); + + replaceWidget(GTK_WIDGET(pContainer), GTK_WIDGET(m_pComboBox)); + + g_object_unref(m_pComboBox); + } + + g_object_unref(m_pComboBuilder); + } +}; + +} + +bool custom_cell_renderer_surface_get_preferred_size(GtkCellRenderer *cell, + GtkOrientation orientation, + gint *minimum_size, + gint *natural_size) +{ + GValue value = G_VALUE_INIT; + g_value_init(&value, G_TYPE_STRING); + g_object_get_property(G_OBJECT(cell), "id", &value); + + const char* pStr = g_value_get_string(&value); + + OUString sId(pStr, pStr ? strlen(pStr) : 0, RTL_TEXTENCODING_UTF8); + + value = G_VALUE_INIT; + g_value_init(&value, G_TYPE_POINTER); + g_object_get_property(G_OBJECT(cell), "instance", &value); + + CustomCellRendererSurface *cellsurface = CUSTOM_CELL_RENDERER_SURFACE(cell); + + GtkInstanceWidget* pWidget = static_cast<GtkInstanceWidget*>(g_value_get_pointer(&value)); + + Size aSize; + + if (pWidget) + { + ensure_device(cellsurface, pWidget); + if (GtkInstanceTreeView* pTreeView = dynamic_cast<GtkInstanceTreeView*>(pWidget)) + aSize = pTreeView->call_signal_custom_get_size(*cellsurface->device, sId); + else if (GtkInstanceComboBox* pComboBox = dynamic_cast<GtkInstanceComboBox*>(pWidget)) + aSize = pComboBox->call_signal_custom_get_size(*cellsurface->device); + } + + if (orientation == GTK_ORIENTATION_HORIZONTAL) + { + if (minimum_size) + *minimum_size = aSize.Width(); + + if (natural_size) + *natural_size = aSize.Width(); + } + else + { + if (minimum_size) + *minimum_size = aSize.Height(); + + if (natural_size) + *natural_size = aSize.Height(); + } + + return true; +} + +void custom_cell_renderer_surface_render(GtkCellRenderer* cell, + cairo_t* cr, + GtkWidget* /*widget*/, + const GdkRectangle* /*background_area*/, + const GdkRectangle* cell_area, + GtkCellRendererState flags) +{ + GValue value = G_VALUE_INIT; + g_value_init(&value, G_TYPE_STRING); + g_object_get_property(G_OBJECT(cell), "id", &value); + + const char* pStr = g_value_get_string(&value); + OUString sId(pStr, pStr ? strlen(pStr) : 0, RTL_TEXTENCODING_UTF8); + + value = G_VALUE_INIT; + g_value_init(&value, G_TYPE_POINTER); + g_object_get_property(G_OBJECT(cell), "instance", &value); + + CustomCellRendererSurface *cellsurface = CUSTOM_CELL_RENDERER_SURFACE(cell); + + GtkInstanceWidget* pWidget = static_cast<GtkInstanceWidget*>(g_value_get_pointer(&value)); + + if (!pWidget) + return; + + ensure_device(cellsurface, pWidget); + + Size aSize(cell_area->width, cell_area->height); + // false to not bother setting the bg on resize as we'll do that + // ourself via cairo + cellsurface->device->SetOutputSizePixel(aSize, false); + + cairo_surface_t* pSurface = get_underlying_cairo_surface(*cellsurface->device); + + // fill surface as transparent so it can be blended with the potentially + // selected background + cairo_t* tempcr = cairo_create(pSurface); + cairo_set_source_rgba(tempcr, 0, 0, 0, 0); + cairo_set_operator(tempcr, CAIRO_OPERATOR_SOURCE); + cairo_paint(tempcr); + cairo_destroy(tempcr); + cairo_surface_flush(pSurface); + + if (GtkInstanceTreeView* pTreeView = dynamic_cast<GtkInstanceTreeView*>(pWidget)) + pTreeView->call_signal_custom_render(*cellsurface->device, tools::Rectangle(Point(0, 0), aSize), flags & GTK_CELL_RENDERER_SELECTED, sId); + else if (GtkInstanceComboBox* pComboBox = dynamic_cast<GtkInstanceComboBox*>(pWidget)) + pComboBox->call_signal_custom_render(*cellsurface->device, tools::Rectangle(Point(0, 0), aSize), flags & GTK_CELL_RENDERER_SELECTED, sId); + cairo_surface_mark_dirty(pSurface); + + cairo_set_source_surface(cr, pSurface, cell_area->x, cell_area->y); + cairo_paint(cr); +} + +namespace { + +class GtkInstanceEntryTreeView : public GtkInstanceContainer, public virtual weld::EntryTreeView +{ +private: + GtkInstanceEntry* m_pEntry; + GtkInstanceTreeView* m_pTreeView; + gulong m_nKeyPressSignalId; + gulong m_nEntryInsertTextSignalId; + guint m_nAutoCompleteIdleId; + bool m_bAutoCompleteCaseSensitive; + bool m_bTreeChange; + + bool signal_key_press(GdkEventKey* pEvent) + { + if (GtkSalFrame::GetMouseModCode(pEvent->state)) // only with no modifiers held + return false; + + if (pEvent->keyval == GDK_KEY_KP_Up || pEvent->keyval == GDK_KEY_Up || pEvent->keyval == GDK_KEY_KP_Page_Up || pEvent->keyval == GDK_KEY_Page_Up || + pEvent->keyval == GDK_KEY_KP_Down || pEvent->keyval == GDK_KEY_Down || pEvent->keyval == GDK_KEY_KP_Page_Down || pEvent->keyval == GDK_KEY_Page_Down) + { + gboolean ret; + disable_notify_events(); + GtkWidget* pWidget = m_pTreeView->getWidget(); + if (m_pTreeView->get_selected_index() == -1) + { + m_pTreeView->set_cursor(0); + m_pTreeView->select(0); + m_xEntry->set_text(m_xTreeView->get_selected_text()); + } + else + { + gtk_widget_grab_focus(pWidget); + g_signal_emit_by_name(pWidget, "key-press-event", pEvent, &ret); + m_xEntry->set_text(m_xTreeView->get_selected_text()); + gtk_widget_grab_focus(m_pEntry->getWidget()); + } + m_xEntry->select_region(0, -1); + enable_notify_events(); + m_bTreeChange = true; + m_pEntry->fire_signal_changed(); + m_bTreeChange = false; + return true; + } + return false; + } + + static gboolean signalKeyPress(GtkWidget*, GdkEventKey* pEvent, gpointer widget) + { + GtkInstanceEntryTreeView* pThis = static_cast<GtkInstanceEntryTreeView*>(widget); + return pThis->signal_key_press(pEvent); + } + + static gboolean idleAutoComplete(gpointer widget) + { + GtkInstanceEntryTreeView* pThis = static_cast<GtkInstanceEntryTreeView*>(widget); + pThis->auto_complete(); + return false; + } + + void auto_complete() + { + m_nAutoCompleteIdleId = 0; + OUString aStartText = get_active_text(); + int nStartPos, nEndPos; + get_entry_selection_bounds(nStartPos, nEndPos); + int nMaxSelection = std::max(nStartPos, nEndPos); + if (nMaxSelection != aStartText.getLength()) + return; + + disable_notify_events(); + int nActive = get_active(); + int nStart = nActive; + + if (nStart == -1) + nStart = 0; + + // Try match case sensitive from current position + int nPos = m_pTreeView->starts_with(aStartText, 0, nStart, true); + if (nPos == -1 && nStart != 0) + { + // Try match case insensitive, but from start + nPos = m_pTreeView->starts_with(aStartText, 0, 0, true); + } + + if (!m_bAutoCompleteCaseSensitive) + { + // Try match case insensitive from current position + nPos = m_pTreeView->starts_with(aStartText, 0, nStart, false); + if (nPos == -1 && nStart != 0) + { + // Try match case insensitive, but from start + nPos = m_pTreeView->starts_with(aStartText, 0, 0, false); + } + } + + if (nPos == -1) + { + // Try match case sensitive from current position + nPos = m_pTreeView->starts_with(aStartText, 0, nStart, true); + if (nPos == -1 && nStart != 0) + { + // Try match case sensitive, but from start + nPos = m_pTreeView->starts_with(aStartText, 0, 0, true); + } + } + + if (nPos != -1) + { + OUString aText = get_text(nPos); + if (aText != aStartText) + set_active_text(aText); + select_entry_region(aText.getLength(), aStartText.getLength()); + } + enable_notify_events(); + } + + void signal_entry_insert_text(GtkEntry*, const gchar*, gint, gint*) + { + // now check for autocompletes + if (m_nAutoCompleteIdleId) + g_source_remove(m_nAutoCompleteIdleId); + m_nAutoCompleteIdleId = g_idle_add(idleAutoComplete, this); + } + + static void signalEntryInsertText(GtkEntry* pEntry, const gchar* pNewText, gint nNewTextLength, + gint* position, gpointer widget) + { + GtkInstanceEntryTreeView* pThis = static_cast<GtkInstanceEntryTreeView*>(widget); + pThis->signal_entry_insert_text(pEntry, pNewText, nNewTextLength, position); + } + + +public: + GtkInstanceEntryTreeView(GtkContainer* pContainer, GtkInstanceBuilder* pBuilder, bool bTakeOwnership, + std::unique_ptr<weld::Entry> xEntry, std::unique_ptr<weld::TreeView> xTreeView) + : EntryTreeView(std::move(xEntry), std::move(xTreeView)) + , GtkInstanceContainer(pContainer, pBuilder, bTakeOwnership) + , m_pEntry(dynamic_cast<GtkInstanceEntry*>(m_xEntry.get())) + , m_pTreeView(dynamic_cast<GtkInstanceTreeView*>(m_xTreeView.get())) + , m_nAutoCompleteIdleId(0) + , m_bAutoCompleteCaseSensitive(false) + , m_bTreeChange(false) + { + assert(m_pEntry); + GtkWidget* pWidget = m_pEntry->getWidget(); + m_nKeyPressSignalId = g_signal_connect(pWidget, "key-press-event", G_CALLBACK(signalKeyPress), this); + m_nEntryInsertTextSignalId = g_signal_connect(pWidget, "insert-text", G_CALLBACK(signalEntryInsertText), this); + } + + virtual void insert_separator(int /*pos*/, const OUString& /*rId*/) override + { + assert(false); + } + + virtual void make_sorted() override + { + GtkWidget* pTreeView = m_pTreeView->getWidget(); + GtkTreeModel* pModel = gtk_tree_view_get_model(GTK_TREE_VIEW(pTreeView)); + GtkTreeSortable* pSortable = GTK_TREE_SORTABLE(pModel); + gtk_tree_sortable_set_sort_column_id(pSortable, 1, GTK_SORT_ASCENDING); + } + + virtual void set_entry_completion(bool bEnable, bool bCaseSensitive) override + { + assert(!bEnable && "not implemented yet"); (void)bEnable; + m_bAutoCompleteCaseSensitive = bCaseSensitive; + } + + virtual void set_entry_placeholder_text(const OUString& rText) override + { + m_xEntry->set_placeholder_text(rText); + } + + virtual void set_entry_editable(bool bEditable) override + { + m_xEntry->set_editable(bEditable); + } + + virtual void cut_entry_clipboard() override + { + m_xEntry->cut_clipboard(); + } + + virtual void copy_entry_clipboard() override + { + m_xEntry->copy_clipboard(); + } + + virtual void paste_entry_clipboard() override + { + m_xEntry->paste_clipboard(); + } + + virtual void set_entry_font(const vcl::Font& rFont) override + { + m_xEntry->set_font(rFont); + } + + virtual vcl::Font get_entry_font() override + { + return m_xEntry->get_font(); + } + + virtual void grab_focus() override { m_xEntry->grab_focus(); } + + virtual void connect_focus_in(const Link<Widget&, void>& rLink) override + { + m_xEntry->connect_focus_in(rLink); + } + + virtual void connect_focus_out(const Link<Widget&, void>& rLink) override + { + m_xEntry->connect_focus_out(rLink); + } + + virtual void disable_notify_events() override + { + GtkWidget* pWidget = m_pEntry->getWidget(); + g_signal_handler_block(pWidget, m_nEntryInsertTextSignalId); + g_signal_handler_block(pWidget, m_nKeyPressSignalId); + m_pTreeView->disable_notify_events(); + GtkInstanceContainer::disable_notify_events(); + } + + virtual void enable_notify_events() override + { + GtkWidget* pWidget = m_pEntry->getWidget(); + g_signal_handler_unblock(pWidget, m_nKeyPressSignalId); + g_signal_handler_unblock(pWidget, m_nEntryInsertTextSignalId); + m_pTreeView->enable_notify_events(); + GtkInstanceContainer::disable_notify_events(); + } + + virtual bool changed_by_direct_pick() const override + { + return m_bTreeChange; + } + + virtual void set_custom_renderer(bool /*bOn*/) override + { + assert(false && "not implemented"); + } + + virtual int get_max_mru_count() const override + { + assert(false && "not implemented"); + return 0; + } + + virtual void set_max_mru_count(int) override + { + assert(false && "not implemented"); + } + + virtual OUString get_mru_entries() const override + { + assert(false && "not implemented"); + return OUString(); + } + + virtual void set_mru_entries(const OUString&) override + { + assert(false && "not implemented"); + } + + virtual void set_item_menu(const OString&, weld::Menu*) override + { + assert(false && "not implemented"); + } + + VclPtr<VirtualDevice> create_render_virtual_device() const override + { + return create_virtual_device(); + } + + int get_menu_button_width() const override + { + assert(false && "not implemented"); + return 0; + } + + virtual ~GtkInstanceEntryTreeView() override + { + if (m_nAutoCompleteIdleId) + g_source_remove(m_nAutoCompleteIdleId); + GtkWidget* pWidget = m_pEntry->getWidget(); + g_signal_handler_disconnect(pWidget, m_nKeyPressSignalId); + g_signal_handler_disconnect(pWidget, m_nEntryInsertTextSignalId); + } +}; + +class GtkInstanceExpander : public GtkInstanceContainer, public virtual weld::Expander +{ +private: + GtkExpander* m_pExpander; + gulong m_nSignalId; + + static void signalExpanded(GtkExpander* pExpander, GParamSpec*, gpointer widget) + { + GtkInstanceExpander* pThis = static_cast<GtkInstanceExpander*>(widget); + SolarMutexGuard aGuard; + + GtkWidget *pToplevel = gtk_widget_get_toplevel(GTK_WIDGET(pExpander)); + + // https://gitlab.gnome.org/GNOME/gtk/issues/70 + // I imagine at some point a release with a fix will be available in which + // case this can be avoided depending on version number + if (pToplevel && GTK_IS_WINDOW(pToplevel) && gtk_widget_get_realized(pToplevel)) + { + int nToplevelWidth, nToplevelHeight; + int nChildHeight; + + GtkWidget* child = gtk_bin_get_child(GTK_BIN(pExpander)); + gtk_widget_get_preferred_height(child, &nChildHeight, nullptr); + gtk_window_get_size(GTK_WINDOW(pToplevel), &nToplevelWidth, &nToplevelHeight); + + if (pThis->get_expanded()) + nToplevelHeight += nChildHeight; + else + nToplevelHeight -= nChildHeight; + + gtk_window_resize(GTK_WINDOW(pToplevel), nToplevelWidth, nToplevelHeight); + } + + pThis->signal_expanded(); + } + +public: + GtkInstanceExpander(GtkExpander* pExpander, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : GtkInstanceContainer(GTK_CONTAINER(pExpander), pBuilder, bTakeOwnership) + , m_pExpander(pExpander) + , m_nSignalId(g_signal_connect(m_pExpander, "notify::expanded", G_CALLBACK(signalExpanded), this)) + { + } + + virtual bool get_expanded() const override + { + return gtk_expander_get_expanded(m_pExpander); + } + + virtual void set_expanded(bool bExpand) override + { + gtk_expander_set_expanded(m_pExpander, bExpand); + } + + virtual ~GtkInstanceExpander() override + { + g_signal_handler_disconnect(m_pExpander, m_nSignalId); + } +}; + + gboolean signalTooltipQuery(GtkWidget* pWidget, gint /*x*/, gint /*y*/, + gboolean /*keyboard_mode*/, GtkTooltip *tooltip) + { + const ImplSVHelpData& aHelpData = ImplGetSVHelpData(); + if (aHelpData.mbBalloonHelp) + { + /*Current mechanism which needs help installed*/ + OString sHelpId = ::get_help_id(pWidget); + Help* pHelp = !sHelpId.isEmpty() ? Application::GetHelp() : nullptr; + if (pHelp) + { + OUString sHelpText = pHelp->GetHelpText(OStringToOUString(sHelpId, RTL_TEXTENCODING_UTF8), static_cast<weld::Widget*>(nullptr)); + if (!sHelpText.isEmpty()) + { + gtk_tooltip_set_text(tooltip, OUStringToOString(sHelpText, RTL_TEXTENCODING_UTF8).getStr()); + return true; + } + } + + /*This is how I would prefer things to be, only a few like this though*/ + AtkObject* pAtkObject = gtk_widget_get_accessible(pWidget); + const char* pDesc = pAtkObject ? atk_object_get_description(pAtkObject) : nullptr; + if (pDesc && pDesc[0]) + { + gtk_tooltip_set_text(tooltip, pDesc); + return true; + } + } + + const char* pDesc = gtk_widget_get_tooltip_text(pWidget); + if (pDesc && pDesc[0]) + { + gtk_tooltip_set_text(tooltip, pDesc); + return true; + } + + return false; + } +} + +namespace +{ + +AtkObject* drawing_area_get_accessibity(GtkWidget *pWidget) +{ + AtkObject* pDefaultAccessible = default_drawing_area_get_accessible(pWidget); + void* pData = g_object_get_data(G_OBJECT(pWidget), "g-lo-GtkInstanceDrawingArea"); + GtkInstanceDrawingArea* pDrawingArea = static_cast<GtkInstanceDrawingArea*>(pData); + AtkObject *pAtkObj = pDrawingArea ? pDrawingArea->GetAtkObject(pDefaultAccessible) : nullptr; + if (pAtkObj) + return pAtkObj; + return pDefaultAccessible; +} + +void ensure_intercept_drawing_area_accessibility() +{ + static bool bDone; + if (!bDone) + { + gpointer pClass = g_type_class_ref(GTK_TYPE_DRAWING_AREA); + GtkWidgetClass* pWidgetClass = GTK_WIDGET_CLASS(pClass); + default_drawing_area_get_accessible = pWidgetClass->get_accessible; + pWidgetClass->get_accessible = drawing_area_get_accessibity; + g_type_class_unref(pClass); + bDone = true; + } +} + +void ensure_disable_ctrl_page_up_down(GType eType) +{ + gpointer pClass = g_type_class_ref(eType); + GtkWidgetClass* pWidgetClass = GTK_WIDGET_CLASS(pClass); + GtkBindingSet* pBindingSet = gtk_binding_set_by_class(pWidgetClass); + gtk_binding_entry_remove(pBindingSet, GDK_KEY_Page_Up, GDK_CONTROL_MASK); + gtk_binding_entry_remove(pBindingSet, GDK_KEY_Page_Up, static_cast<GdkModifierType>(GDK_SHIFT_MASK|GDK_CONTROL_MASK)); + gtk_binding_entry_remove(pBindingSet, GDK_KEY_Page_Down, GDK_CONTROL_MASK); + gtk_binding_entry_remove(pBindingSet, GDK_KEY_Page_Down, static_cast<GdkModifierType>(GDK_SHIFT_MASK|GDK_CONTROL_MASK)); + g_type_class_unref(pClass); +} + +// tdf#130400 disable ctrl+page_up and ctrl+page_down bindings so the +// keystrokes are consumed by the surrounding notebook bindings instead +void ensure_disable_ctrl_page_up_down_bindings() +{ + static bool bDone; + if (!bDone) + { + ensure_disable_ctrl_page_up_down(GTK_TYPE_TREE_VIEW); + ensure_disable_ctrl_page_up_down(GTK_TYPE_SPIN_BUTTON); + bDone = true; + } +} + +class GtkInstanceBuilder : public weld::Builder +{ +private: + ResHookProc m_pStringReplace; + OString m_aUtf8HelpRoot; + OUString m_aIconTheme; + OUString m_aUILang; + GtkBuilder* m_pBuilder; + GSList* m_pObjectList; + GtkWidget* m_pParentWidget; + gulong m_nNotifySignalId; + std::vector<GtkButton*> m_aMnemonicButtons; + std::vector<GtkLabel*> m_aMnemonicLabels; + + VclPtr<SystemChildWindow> m_xInterimGlue; + + void postprocess_widget(GtkWidget* pWidget) + { + const bool bHideHelp = comphelper::LibreOfficeKit::isActive() && + officecfg::Office::Common::Help::HelpRootURL::get().isEmpty(); + + //fixup icons + //wanted: better way to do this, e.g. make gtk use gio for + //loading from a filename and provide gio protocol handler + //for our image in a zip urls + // + //unpack the images and keep them as dirs and just + //add the paths to the gtk icon theme dir + if (GTK_IS_IMAGE(pWidget)) + { + GtkImage* pImage = GTK_IMAGE(pWidget); + const gchar* icon_name; + gtk_image_get_icon_name(pImage, &icon_name, nullptr); + if (icon_name) + { + OUString aIconName(icon_name, strlen(icon_name), RTL_TEXTENCODING_UTF8); + GdkPixbuf* pixbuf = load_icon_by_name_theme_lang(aIconName, m_aIconTheme, m_aUILang); + if (pixbuf) + { + gtk_image_set_from_pixbuf(pImage, pixbuf); + g_object_unref(pixbuf); + } + } + } + else if (GTK_IS_TOOL_BUTTON(pWidget)) + { + GtkToolButton* pToolButton = GTK_TOOL_BUTTON(pWidget); + const gchar* icon_name = gtk_tool_button_get_icon_name(pToolButton); + if (icon_name) + { + OUString aIconName(icon_name, strlen(icon_name), RTL_TEXTENCODING_UTF8); + GdkPixbuf* pixbuf = load_icon_by_name_theme_lang(aIconName, m_aIconTheme, m_aUILang); + if (pixbuf) + { + GtkWidget* pImage = gtk_image_new_from_pixbuf(pixbuf); + g_object_unref(pixbuf); + gtk_tool_button_set_icon_widget(pToolButton, pImage); + gtk_widget_show(pImage); + } + } + + // if no tooltip reuse the label as default tooltip + if (!gtk_widget_get_tooltip_text(pWidget)) + { + if (const gchar* label = gtk_tool_button_get_label(pToolButton)) + gtk_widget_set_tooltip_text(pWidget, label); + } + } + + //set helpids + const gchar* pStr = gtk_buildable_get_name(GTK_BUILDABLE(pWidget)); + size_t nLen = pStr ? strlen(pStr) : 0; + if (nLen) + { + OString sBuildableName(pStr, nLen); + OString sHelpId = m_aUtf8HelpRoot + sBuildableName; + set_help_id(pWidget, sHelpId); + //hook up for extended help + const ImplSVHelpData& aHelpData = ImplGetSVHelpData(); + if (aHelpData.mbBalloonHelp && !GTK_IS_DIALOG(pWidget) && !GTK_IS_ASSISTANT(pWidget)) + { + gtk_widget_set_has_tooltip(pWidget, true); + g_signal_connect(pWidget, "query-tooltip", G_CALLBACK(signalTooltipQuery), nullptr); + } + + if (bHideHelp && sBuildableName == "help") + gtk_widget_hide(pWidget); + } + + // expand placeholder and collect potentially missing mnemonics + if (GTK_IS_BUTTON(pWidget)) + { + GtkButton* pButton = GTK_BUTTON(pWidget); + if (m_pStringReplace != nullptr) + { + OUString aLabel(get_label(pButton)); + if (!aLabel.isEmpty()) + set_label(pButton, (*m_pStringReplace)(aLabel)); + } + if (gtk_button_get_use_underline(pButton) && !gtk_button_get_use_stock(pButton)) + m_aMnemonicButtons.push_back(pButton); + } + else if (GTK_IS_LABEL(pWidget)) + { + GtkLabel* pLabel = GTK_LABEL(pWidget); + if (m_pStringReplace != nullptr) + { + OUString aLabel(get_label(pLabel)); + if (!aLabel.isEmpty()) + set_label(pLabel, (*m_pStringReplace)(aLabel)); + } + if (gtk_label_get_use_underline(pLabel)) + m_aMnemonicLabels.push_back(pLabel); + } + else if (GTK_IS_TEXT_VIEW(pWidget)) + { + GtkTextView* pTextView = GTK_TEXT_VIEW(pWidget); + if (m_pStringReplace != nullptr) + { + GtkTextBuffer* pBuffer = gtk_text_view_get_buffer(pTextView); + GtkTextIter start, end; + gtk_text_buffer_get_bounds(pBuffer, &start, &end); + char* pTextStr = gtk_text_buffer_get_text(pBuffer, &start, &end, true); + int nTextLen = pTextStr ? strlen(pTextStr) : 0; + if (nTextLen) + { + OUString sOldText(pTextStr, nTextLen, RTL_TEXTENCODING_UTF8); + OString sText(OUStringToOString((*m_pStringReplace)(sOldText), RTL_TEXTENCODING_UTF8)); + gtk_text_buffer_set_text(pBuffer, sText.getStr(), sText.getLength()); + } + g_free(pTextStr); + } + } + else if (GTK_IS_WINDOW(pWidget)) + { + if (m_pStringReplace != nullptr) { + GtkWindow* pWindow = GTK_WINDOW(pWidget); + set_title(pWindow, (*m_pStringReplace)(get_title(pWindow))); + if (GTK_IS_MESSAGE_DIALOG(pWindow)) + { + GtkMessageDialog* pMessageDialog = GTK_MESSAGE_DIALOG(pWindow); + set_primary_text(pMessageDialog, (*m_pStringReplace)(get_primary_text(pMessageDialog))); + set_secondary_text(pMessageDialog, (*m_pStringReplace)(get_secondary_text(pMessageDialog))); + } + } + } + } + + //GtkBuilder sets translation domain during parse, and unsets it again afterwards. + //In order for GtkBuilder to find the translations bindtextdomain has to be called + //for the domain. So here on the first setting of "domain" we call Translate::Create + //to make sure that happens. Without this, if some other part of LibreOffice has + //used the translation machinery for this domain it will still work, but if it + //hasn't, e.g. tdf#119929, then the translation fails + void translation_domain_set() + { + Translate::Create(gtk_builder_get_translation_domain(m_pBuilder), LanguageTag(m_aUILang)); + g_signal_handler_disconnect(m_pBuilder, m_nNotifySignalId); + } + + static void signalNotify(GObject*, GParamSpec *pSpec, gpointer pData) + { + g_return_if_fail(pSpec != nullptr); + if (strcmp(pSpec->name, "translation-domain") == 0) + { + GtkInstanceBuilder* pBuilder = static_cast<GtkInstanceBuilder*>(pData); + pBuilder->translation_domain_set(); + } + } + + static void postprocess(gpointer data, gpointer user_data) + { + GObject* pObject = static_cast<GObject*>(data); + if (!GTK_IS_WIDGET(pObject)) + return; + GtkInstanceBuilder* pThis = static_cast<GtkInstanceBuilder*>(user_data); + pThis->postprocess_widget(GTK_WIDGET(pObject)); + } +public: + GtkInstanceBuilder(GtkWidget* pParent, const OUString& rUIRoot, const OUString& rUIFile, SystemChildWindow* pInterimGlue) + : weld::Builder() + , m_pStringReplace(Translate::GetReadStringHook()) + , m_pParentWidget(pParent) + , m_nNotifySignalId(0) + , m_xInterimGlue(pInterimGlue) + { + OUString sHelpRoot(rUIFile); + ensure_intercept_drawing_area_accessibility(); + ensure_disable_ctrl_page_up_down_bindings(); + + sal_Int32 nIdx = sHelpRoot.lastIndexOf('.'); + if (nIdx != -1) + sHelpRoot = sHelpRoot.copy(0, nIdx); + sHelpRoot += OUString('/'); + m_aUtf8HelpRoot = OUStringToOString(sHelpRoot, RTL_TEXTENCODING_UTF8); + m_aIconTheme = Application::GetSettings().GetStyleSettings().DetermineIconTheme(); + m_aUILang = Application::GetSettings().GetUILanguageTag().getBcp47(); + + OUString aUri(rUIRoot + rUIFile); + OUString aPath; + osl::FileBase::getSystemPathFromFileURL(aUri, aPath); + m_pBuilder = gtk_builder_new(); + m_nNotifySignalId = g_signal_connect_data(G_OBJECT(m_pBuilder), "notify", G_CALLBACK(signalNotify), this, nullptr, G_CONNECT_AFTER); + auto rc = gtk_builder_add_from_file(m_pBuilder, OUStringToOString(aPath, RTL_TEXTENCODING_UTF8).getStr(), nullptr); + assert(rc && "could not load UI file"); + (void) rc; + + m_pObjectList = gtk_builder_get_objects(m_pBuilder); + g_slist_foreach(m_pObjectList, postprocess, this); + + GenerateMissingMnemonics(); + + if (m_xInterimGlue) + { + assert(m_pParentWidget); + g_object_set_data(G_OBJECT(m_pParentWidget), "InterimWindowGlue", m_xInterimGlue.get()); + } + } + + void GenerateMissingMnemonics() + { + MnemonicGenerator aMnemonicGenerator('_'); + for (const auto a : m_aMnemonicButtons) + aMnemonicGenerator.RegisterMnemonic(get_label(a)); + for (const auto a : m_aMnemonicLabels) + aMnemonicGenerator.RegisterMnemonic(get_label(a)); + + for (const auto a : m_aMnemonicButtons) + { + OUString aLabel(get_label(a)); + OUString aNewLabel = aMnemonicGenerator.CreateMnemonic(aLabel); + if (aLabel == aNewLabel) + continue; + set_label(a, aNewLabel); + } + for (const auto a : m_aMnemonicLabels) + { + OUString aLabel(get_label(a)); + OUString aNewLabel = aMnemonicGenerator.CreateMnemonic(aLabel); + if (aLabel == aNewLabel) + continue; + set_label(a, aNewLabel); + } + + m_aMnemonicLabels.clear(); + m_aMnemonicButtons.clear(); + } + + OString get_current_page_help_id() + { + OString sPageHelpId; + // check to see if there is a notebook called tabcontrol and get the + // helpid for the current page of that + std::unique_ptr<weld::Notebook> xNotebook(weld_notebook("tabcontrol", false)); + if (xNotebook) + { + if (GtkInstanceContainer* pPage = dynamic_cast<GtkInstanceContainer*>(xNotebook->get_page(xNotebook->get_current_page_ident()))) + { + GtkWidget* pContainer = pPage->getWidget(); + GList* pChildren = gtk_container_get_children(GTK_CONTAINER(pContainer)); + GList* pChild = g_list_first(pChildren); + if (pChild) + { + GtkWidget* pPageWidget = static_cast<GtkWidget*>(pChild->data); + sPageHelpId = ::get_help_id(pPageWidget); + } + g_list_free(pChildren); + } + } + return sPageHelpId; + } + + virtual ~GtkInstanceBuilder() override + { + g_slist_free(m_pObjectList); + g_object_unref(m_pBuilder); + m_xInterimGlue.disposeAndClear(); + } + + //ideally we would have/use weld::Container add and explicitly + //call add when we want to do this, but in the vcl impl the + //parent has to be set when the child is created, so for the + //gtk impl emulate this by doing this implicitly at weld time + void auto_add_parentless_widgets_to_container(GtkWidget* pWidget) + { + if (gtk_widget_get_toplevel(pWidget) == pWidget && !GTK_IS_POPOVER(pWidget)) + gtk_container_add(GTK_CONTAINER(m_pParentWidget), pWidget); + } + + virtual std::unique_ptr<weld::MessageDialog> weld_message_dialog(const OString &id, bool bTakeOwnership) override + { + GtkMessageDialog* pMessageDialog = GTK_MESSAGE_DIALOG(gtk_builder_get_object(m_pBuilder, id.getStr())); + if (!pMessageDialog) + return nullptr; + gtk_window_set_transient_for(GTK_WINDOW(pMessageDialog), GTK_WINDOW(gtk_widget_get_toplevel(m_pParentWidget))); + return std::make_unique<GtkInstanceMessageDialog>(pMessageDialog, this, bTakeOwnership); + } + + virtual std::unique_ptr<weld::Assistant> weld_assistant(const OString &id, bool bTakeOwnership) override + { + GtkAssistant* pAssistant = GTK_ASSISTANT(gtk_builder_get_object(m_pBuilder, id.getStr())); + if (!pAssistant) + return nullptr; + if (m_pParentWidget) + gtk_window_set_transient_for(GTK_WINDOW(pAssistant), GTK_WINDOW(gtk_widget_get_toplevel(m_pParentWidget))); + return std::make_unique<GtkInstanceAssistant>(pAssistant, this, bTakeOwnership); + } + + virtual std::unique_ptr<weld::Dialog> weld_dialog(const OString &id, bool bTakeOwnership) override + { + GtkWindow* pDialog = GTK_WINDOW(gtk_builder_get_object(m_pBuilder, id.getStr())); + if (!pDialog) + return nullptr; + if (m_pParentWidget) + gtk_window_set_transient_for(pDialog, GTK_WINDOW(gtk_widget_get_toplevel(m_pParentWidget))); + return std::make_unique<GtkInstanceDialog>(pDialog, this, bTakeOwnership); + } + + virtual std::unique_ptr<weld::Window> create_screenshot_window() override + { + GtkWidget* pTopLevel = nullptr; + + for (GSList* l = m_pObjectList; l; l = g_slist_next(l)) + { + GObject* pObj = static_cast<GObject*>(l->data); + + if (!GTK_IS_WIDGET(pObj) || gtk_widget_get_parent(GTK_WIDGET(pObj))) + continue; + + if (!pTopLevel) + pTopLevel = GTK_WIDGET(pObj); + else if (GTK_IS_WINDOW(pObj)) + pTopLevel = GTK_WIDGET(pObj); + } + + if (!pTopLevel) + return nullptr; + + GtkWindow* pDialog; + if (GTK_IS_WINDOW(pTopLevel)) + pDialog = GTK_WINDOW(pTopLevel); + else + { + pDialog = GTK_WINDOW(gtk_dialog_new()); + ::set_help_id(GTK_WIDGET(pDialog), ::get_help_id(pTopLevel)); + + GtkWidget* pContentArea = gtk_dialog_get_content_area(GTK_DIALOG(pDialog)); + gtk_container_add(GTK_CONTAINER(pContentArea), pTopLevel); + gtk_widget_show_all(pTopLevel); + } + + if (m_pParentWidget) + gtk_window_set_transient_for(pDialog, GTK_WINDOW(gtk_widget_get_toplevel(m_pParentWidget))); + return std::make_unique<GtkInstanceDialog>(pDialog, this, true); + } + + virtual std::unique_ptr<weld::Window> weld_window(const OString &id, bool bTakeOwnership) override + { + GtkWindow* pWindow = GTK_WINDOW(gtk_builder_get_object(m_pBuilder, id.getStr())); + return pWindow ? std::make_unique<GtkInstanceWindow>(pWindow, this, bTakeOwnership) : nullptr; + } + + virtual std::unique_ptr<weld::Widget> weld_widget(const OString &id, bool bTakeOwnership) override + { + GtkWidget* pWidget = GTK_WIDGET(gtk_builder_get_object(m_pBuilder, id.getStr())); + if (!pWidget) + return nullptr; + auto_add_parentless_widgets_to_container(pWidget); + return std::make_unique<GtkInstanceWidget>(pWidget, this, bTakeOwnership); + } + + virtual std::unique_ptr<weld::Container> weld_container(const OString &id, bool bTakeOwnership) override + { + GtkContainer* pContainer = GTK_CONTAINER(gtk_builder_get_object(m_pBuilder, id.getStr())); + if (!pContainer) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pContainer)); + return std::make_unique<GtkInstanceContainer>(pContainer, this, bTakeOwnership); + } + + virtual std::unique_ptr<weld::Box> weld_box(const OString &id, bool bTakeOwnership) override + { + GtkBox* pBox = GTK_BOX(gtk_builder_get_object(m_pBuilder, id.getStr())); + if (!pBox) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pBox)); + return std::make_unique<GtkInstanceBox>(pBox, this, bTakeOwnership); + } + + virtual std::unique_ptr<weld::Paned> weld_paned(const OString &id, bool bTakeOwnership) override + { + GtkPaned* pPaned = GTK_PANED(gtk_builder_get_object(m_pBuilder, id.getStr())); + if (!pPaned) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pPaned)); + return std::make_unique<GtkInstancePaned>(pPaned, this, bTakeOwnership); + } + + virtual std::unique_ptr<weld::Frame> weld_frame(const OString &id, bool bTakeOwnership) override + { + GtkFrame* pFrame = GTK_FRAME(gtk_builder_get_object(m_pBuilder, id.getStr())); + if (!pFrame) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pFrame)); + return std::make_unique<GtkInstanceFrame>(pFrame, this, bTakeOwnership); + } + + virtual std::unique_ptr<weld::ScrolledWindow> weld_scrolled_window(const OString &id, bool bTakeOwnership) override + { + GtkScrolledWindow* pScrolledWindow = GTK_SCROLLED_WINDOW(gtk_builder_get_object(m_pBuilder, id.getStr())); + if (!pScrolledWindow) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pScrolledWindow)); + return std::make_unique<GtkInstanceScrolledWindow>(pScrolledWindow, this, bTakeOwnership); + } + + virtual std::unique_ptr<weld::Notebook> weld_notebook(const OString &id, bool bTakeOwnership) override + { + GtkNotebook* pNotebook = GTK_NOTEBOOK(gtk_builder_get_object(m_pBuilder, id.getStr())); + if (!pNotebook) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pNotebook)); + return std::make_unique<GtkInstanceNotebook>(pNotebook, this, bTakeOwnership); + } + + virtual std::unique_ptr<weld::Button> weld_button(const OString &id, bool bTakeOwnership) override + { + GtkButton* pButton = GTK_BUTTON(gtk_builder_get_object(m_pBuilder, id.getStr())); + if (!pButton) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pButton)); + return std::make_unique<GtkInstanceButton>(pButton, this, bTakeOwnership); + } + + virtual std::unique_ptr<weld::MenuButton> weld_menu_button(const OString &id, bool bTakeOwnership) override + { + GtkMenuButton* pButton = GTK_MENU_BUTTON(gtk_builder_get_object(m_pBuilder, id.getStr())); + if (!pButton) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pButton)); + return std::make_unique<GtkInstanceMenuButton>(pButton, nullptr, this, bTakeOwnership); + } + + virtual std::unique_ptr<weld::LinkButton> weld_link_button(const OString &id, bool bTakeOwnership) override + { + GtkLinkButton* pButton = GTK_LINK_BUTTON(gtk_builder_get_object(m_pBuilder, id.getStr())); + if (!pButton) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pButton)); + return std::make_unique<GtkInstanceLinkButton>(pButton, this, bTakeOwnership); + } + + virtual std::unique_ptr<weld::ToggleButton> weld_toggle_button(const OString &id, bool bTakeOwnership) override + { + GtkToggleButton* pToggleButton = GTK_TOGGLE_BUTTON(gtk_builder_get_object(m_pBuilder, id.getStr())); + if (!pToggleButton) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pToggleButton)); + return std::make_unique<GtkInstanceToggleButton>(pToggleButton, this, bTakeOwnership); + } + + virtual std::unique_ptr<weld::RadioButton> weld_radio_button(const OString &id, bool bTakeOwnership) override + { + GtkRadioButton* pRadioButton = GTK_RADIO_BUTTON(gtk_builder_get_object(m_pBuilder, id.getStr())); + if (!pRadioButton) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pRadioButton)); + return std::make_unique<GtkInstanceRadioButton>(pRadioButton, this, bTakeOwnership); + } + + virtual std::unique_ptr<weld::CheckButton> weld_check_button(const OString &id, bool bTakeOwnership) override + { + GtkCheckButton* pCheckButton = GTK_CHECK_BUTTON(gtk_builder_get_object(m_pBuilder, id.getStr())); + if (!pCheckButton) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pCheckButton)); + return std::make_unique<GtkInstanceCheckButton>(pCheckButton, this, bTakeOwnership); + } + + virtual std::unique_ptr<weld::Scale> weld_scale(const OString &id, bool bTakeOwnership) override + { + GtkScale* pScale = GTK_SCALE(gtk_builder_get_object(m_pBuilder, id.getStr())); + if (!pScale) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pScale)); + return std::make_unique<GtkInstanceScale>(pScale, this, bTakeOwnership); + } + + virtual std::unique_ptr<weld::ProgressBar> weld_progress_bar(const OString &id, bool bTakeOwnership) override + { + GtkProgressBar* pProgressBar = GTK_PROGRESS_BAR(gtk_builder_get_object(m_pBuilder, id.getStr())); + if (!pProgressBar) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pProgressBar)); + return std::make_unique<GtkInstanceProgressBar>(pProgressBar, this, bTakeOwnership); + } + + virtual std::unique_ptr<weld::Spinner> weld_spinner(const OString &id, bool bTakeOwnership) override + { + GtkSpinner* pSpinner = GTK_SPINNER(gtk_builder_get_object(m_pBuilder, id.getStr())); + if (!pSpinner) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pSpinner)); + return std::make_unique<GtkInstanceSpinner>(pSpinner, this, bTakeOwnership); + } + + virtual std::unique_ptr<weld::Image> weld_image(const OString &id, bool bTakeOwnership) override + { + GtkImage* pImage = GTK_IMAGE(gtk_builder_get_object(m_pBuilder, id.getStr())); + if (!pImage) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pImage)); + return std::make_unique<GtkInstanceImage>(pImage, this, bTakeOwnership); + } + + virtual std::unique_ptr<weld::Calendar> weld_calendar(const OString &id, bool bTakeOwnership) override + { + GtkCalendar* pCalendar = GTK_CALENDAR(gtk_builder_get_object(m_pBuilder, id.getStr())); + if (!pCalendar) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pCalendar)); + return std::make_unique<GtkInstanceCalendar>(pCalendar, this, bTakeOwnership); + } + + virtual std::unique_ptr<weld::Entry> weld_entry(const OString &id, bool bTakeOwnership) override + { + GtkEntry* pEntry = GTK_ENTRY(gtk_builder_get_object(m_pBuilder, id.getStr())); + if (!pEntry) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pEntry)); + return std::make_unique<GtkInstanceEntry>(pEntry, this, bTakeOwnership); + } + + virtual std::unique_ptr<weld::SpinButton> weld_spin_button(const OString &id, bool bTakeOwnership) override + { + GtkSpinButton* pSpinButton = GTK_SPIN_BUTTON(gtk_builder_get_object(m_pBuilder, id.getStr())); + if (!pSpinButton) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pSpinButton)); + return std::make_unique<GtkInstanceSpinButton>(pSpinButton, this, bTakeOwnership); + } + + virtual std::unique_ptr<weld::MetricSpinButton> weld_metric_spin_button(const OString& id, FieldUnit eUnit, + bool bTakeOwnership) override + { + return std::make_unique<weld::MetricSpinButton>(weld_spin_button(id, bTakeOwnership), eUnit); + } + + virtual std::unique_ptr<weld::FormattedSpinButton> weld_formatted_spin_button(const OString &id, bool bTakeOwnership) override + { + GtkSpinButton* pSpinButton = GTK_SPIN_BUTTON(gtk_builder_get_object(m_pBuilder, id.getStr())); + if (!pSpinButton) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pSpinButton)); + return std::make_unique<GtkInstanceFormattedSpinButton>(pSpinButton, this, bTakeOwnership); + } + + virtual std::unique_ptr<weld::TimeSpinButton> weld_time_spin_button(const OString& id, TimeFieldFormat eFormat, + bool bTakeOwnership) override + { + return std::make_unique<weld::TimeSpinButton>(weld_spin_button(id, bTakeOwnership), eFormat); + } + + virtual std::unique_ptr<weld::ComboBox> weld_combo_box(const OString &id, bool bTakeOwnership) override + { + GtkComboBox* pComboBox = GTK_COMBO_BOX(gtk_builder_get_object(m_pBuilder, id.getStr())); + if (!pComboBox) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pComboBox)); + + /* we replace GtkComboBox because of difficulties with too tall menus + + 1) https://gitlab.gnome.org/GNOME/gtk/issues/1910 + has_entry long menus take forever to appear (tdf#125388) + + on measuring each row, the GtkComboBox GtkTreeMenu will call + its area_apply_attributes_cb function on the row, but that calls + gtk_tree_menu_get_path_item which then loops through each child of the + menu looking for the widget of the row, so performance drops to useless. + + All area_apply_attributes_cb does it set menu item sensitivity, so block it from running + with fragile hackery which assumes that the unwanted callback is the only one with a + + 2) https://gitlab.gnome.org/GNOME/gtk/issues/94 + when a super tall combobox menu is activated, and the selected + entry is sufficiently far down the list, then the menu doesn't + appear under wayland + + 3) https://gitlab.gnome.org/GNOME/gtk/issues/310 + no typeahead support + + 4) we want to be able to control the width of the button, but have a drop down menu which + is not limited to the width of the button + + 5) https://bugs.documentfoundation.org/show_bug.cgi?id=131120 + super tall menu doesn't appear under X sometimes + */ + GtkBuilder* pComboBuilder = makeComboBoxBuilder(); + return std::make_unique<GtkInstanceComboBox>(pComboBuilder, pComboBox, this, bTakeOwnership); + } + + virtual std::unique_ptr<weld::TreeView> weld_tree_view(const OString &id, bool bTakeOwnership) override + { + GtkTreeView* pTreeView = GTK_TREE_VIEW(gtk_builder_get_object(m_pBuilder, id.getStr())); + if (!pTreeView) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pTreeView)); + return std::make_unique<GtkInstanceTreeView>(pTreeView, this, bTakeOwnership); + } + + virtual std::unique_ptr<weld::IconView> weld_icon_view(const OString &id, bool bTakeOwnership) override + { + GtkIconView* pIconView = GTK_ICON_VIEW(gtk_builder_get_object(m_pBuilder, id.getStr())); + if (!pIconView) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pIconView)); + return std::make_unique<GtkInstanceIconView>(pIconView, this, bTakeOwnership); + } + + virtual std::unique_ptr<weld::EntryTreeView> weld_entry_tree_view(const OString& containerid, const OString& entryid, const OString& treeviewid, bool bTakeOwnership) override + { + GtkContainer* pContainer = GTK_CONTAINER(gtk_builder_get_object(m_pBuilder, containerid.getStr())); + if (!pContainer) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pContainer)); + return std::make_unique<GtkInstanceEntryTreeView>(pContainer, this, bTakeOwnership, + weld_entry(entryid, bTakeOwnership), + weld_tree_view(treeviewid, bTakeOwnership)); + } + + virtual std::unique_ptr<weld::Label> weld_label(const OString &id, bool bTakeOwnership) override + { + GtkLabel* pLabel = GTK_LABEL(gtk_builder_get_object(m_pBuilder, id.getStr())); + if (!pLabel) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pLabel)); + return std::make_unique<GtkInstanceLabel>(pLabel, this, bTakeOwnership); + } + + virtual std::unique_ptr<weld::TextView> weld_text_view(const OString &id, bool bTakeOwnership) override + { + GtkTextView* pTextView = GTK_TEXT_VIEW(gtk_builder_get_object(m_pBuilder, id.getStr())); + if (!pTextView) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pTextView)); + return std::make_unique<GtkInstanceTextView>(pTextView, this, bTakeOwnership); + } + + virtual std::unique_ptr<weld::Expander> weld_expander(const OString &id, bool bTakeOwnership) override + { + GtkExpander* pExpander = GTK_EXPANDER(gtk_builder_get_object(m_pBuilder, id.getStr())); + if (!pExpander) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pExpander)); + return std::make_unique<GtkInstanceExpander>(pExpander, this, bTakeOwnership); + } + + virtual std::unique_ptr<weld::DrawingArea> weld_drawing_area(const OString &id, const a11yref& rA11y, + FactoryFunction /*pUITestFactoryFunction*/, void* /*pUserData*/, bool bTakeOwnership) override + { + GtkDrawingArea* pDrawingArea = GTK_DRAWING_AREA(gtk_builder_get_object(m_pBuilder, id.getStr())); + if (!pDrawingArea) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pDrawingArea)); + return std::make_unique<GtkInstanceDrawingArea>(pDrawingArea, this, rA11y, bTakeOwnership); + } + + virtual std::unique_ptr<weld::Menu> weld_menu(const OString &id, bool bTakeOwnership) override + { + GtkMenu* pMenu = GTK_MENU(gtk_builder_get_object(m_pBuilder, id.getStr())); + if (!pMenu) + return nullptr; + return std::make_unique<GtkInstanceMenu>(pMenu, bTakeOwnership); + } + + virtual std::unique_ptr<weld::Toolbar> weld_toolbar(const OString &id, bool bTakeOwnership) override + { + GtkToolbar* pToolbar = GTK_TOOLBAR(gtk_builder_get_object(m_pBuilder, id.getStr())); + if (!pToolbar) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pToolbar)); + return std::make_unique<GtkInstanceToolbar>(pToolbar, this, bTakeOwnership); + } + + virtual std::unique_ptr<weld::SizeGroup> create_size_group() override + { + return std::make_unique<GtkInstanceSizeGroup>(); + } +}; + +} + +void GtkInstanceWindow::help() +{ + //show help for widget with keyboard focus + GtkWidget* pWidget = gtk_window_get_focus(m_pWindow); + if (!pWidget) + pWidget = GTK_WIDGET(m_pWindow); + OString sHelpId = ::get_help_id(pWidget); + while (sHelpId.isEmpty()) + { + pWidget = gtk_widget_get_parent(pWidget); + if (!pWidget) + break; + sHelpId = ::get_help_id(pWidget); + } + std::unique_ptr<weld::Widget> xTemp(pWidget != m_pWidget ? new GtkInstanceWidget(pWidget, m_pBuilder, false) : nullptr); + weld::Widget* pSource = xTemp ? xTemp.get() : this; + bool bRunNormalHelpRequest = !m_aHelpRequestHdl.IsSet() || m_aHelpRequestHdl.Call(*pSource); + Help* pHelp = bRunNormalHelpRequest ? Application::GetHelp() : nullptr; + if (pHelp) + { + // tdf#126007, there's a nice fallback route for offline help where + // the current page of a notebook will get checked when the help + // button is pressed and there was no help for the dialog found. + // + // But for online help that route doesn't get taken, so bodge this here + // by using the page help id if available and if the help button itself + // was the original id + if (m_pBuilder && sHelpId.endsWith("/help")) + { + OString sPageId = m_pBuilder->get_current_page_help_id(); + if (!sPageId.isEmpty()) + sHelpId = sPageId; + else + { + // tdf#129068 likewise the help for the wrapping dialog is less + // helpful than the help for the content area could be + GtkContainer* pContainer = nullptr; + if (GTK_IS_DIALOG(m_pWindow)) + pContainer = GTK_CONTAINER(gtk_dialog_get_content_area(GTK_DIALOG(m_pWindow))); + else if (GTK_IS_ASSISTANT(m_pWindow)) + { + GtkAssistant* pAssistant = GTK_ASSISTANT(m_pWindow); + pContainer = GTK_CONTAINER(gtk_assistant_get_nth_page(pAssistant, gtk_assistant_get_current_page(pAssistant))); + } + if (pContainer) + { + GList* pChildren = gtk_container_get_children(pContainer); + GList* pChild = g_list_first(pChildren); + if (pChild) + { + GtkWidget* pContentWidget = static_cast<GtkWidget*>(pChild->data); + sHelpId = ::get_help_id(pContentWidget); + } + g_list_free(pChildren); + } + } + } + pHelp->Start(OStringToOUString(sHelpId, RTL_TEXTENCODING_UTF8), pSource); + } +} + +//iterate upwards through the hierarchy from this widgets through its parents +//calling func with their helpid until func returns true or we run out of parents +void GtkInstanceWidget::help_hierarchy_foreach(const std::function<bool(const OString&)>& func) +{ + GtkWidget* pParent = m_pWidget; + while ((pParent = gtk_widget_get_parent(pParent))) + { + if (func(::get_help_id(pParent))) + return; + } +} + +weld::Builder* GtkInstance::CreateBuilder(weld::Widget* pParent, const OUString& rUIRoot, const OUString& rUIFile) +{ + GtkInstanceWidget* pParentWidget = dynamic_cast<GtkInstanceWidget*>(pParent); + if (pParent && !pParentWidget) //remove when complete + return SalInstance::CreateBuilder(pParent, rUIRoot, rUIFile); + GtkWidget* pBuilderParent = pParentWidget ? pParentWidget->getWidget() : nullptr; + return new GtkInstanceBuilder(pBuilderParent, rUIRoot, rUIFile, nullptr); +} + +// tdf#135965 for the case of native widgets inside a GtkSalFrame and F1 pressed, run help +// on gtk widget help ids until we hit a vcl parent and then use vcl window help ids +gboolean GtkSalFrame::NativeWidgetHelpPressed(GtkAccelGroup*, GObject*, guint, GdkModifierType, gpointer pFrame) +{ + Help* pHelp = Application::GetHelp(); + if (!pHelp) + return true; + + GtkWindow* pWindow = static_cast<GtkWindow*>(pFrame); + + vcl::Window* pChildWindow = nullptr; + + //show help for widget with keyboard focus + GtkWidget* pWidget = gtk_window_get_focus(pWindow); + if (!pWidget) + pWidget = GTK_WIDGET(pWindow); + OString sHelpId = ::get_help_id(pWidget); + while (sHelpId.isEmpty()) + { + pWidget = gtk_widget_get_parent(pWidget); + if (!pWidget) + break; + pChildWindow = static_cast<vcl::Window*>(g_object_get_data(G_OBJECT(pWidget), "InterimWindowGlue")); + if (pChildWindow) + { + sHelpId = pChildWindow->GetHelpId(); + break; + } + sHelpId = ::get_help_id(pWidget); + } + + if (pChildWindow) + { + while (sHelpId.isEmpty()) + { + pChildWindow = pChildWindow->GetParent(); + if (!pChildWindow) + break; + sHelpId = pChildWindow->GetHelpId(); + } + if (!pChildWindow) + return true; + pHelp->Start(OStringToOUString(sHelpId, RTL_TEXTENCODING_UTF8), pChildWindow); + return true; + } + + if (!pWidget) + return true; + std::unique_ptr<weld::Widget> xTemp(new GtkInstanceWidget(pWidget, nullptr, false)); + pHelp->Start(OStringToOUString(sHelpId, RTL_TEXTENCODING_UTF8), xTemp.get()); + return true; +} + +weld::Builder* GtkInstance::CreateInterimBuilder(vcl::Window* pParent, const OUString& rUIRoot, const OUString& rUIFile) +{ + // Create a foreign window which we know is a GtkGrid and make the native widgets a child of that, so we can + // support GtkWidgets within a vcl::Window + SystemWindowData winData = {}; + winData.bClipUsingNativeWidget = true; + auto xEmbedWindow = VclPtr<SystemChildWindow>::Create(pParent, 0, &winData, false); + xEmbedWindow->Show(true, ShowFlags::NoActivate); + xEmbedWindow->set_expand(true); + + const SystemEnvData* pEnvData = xEmbedWindow->GetSystemData(); + if (!pEnvData) + return nullptr; + + GtkWidget *pWindow = static_cast<GtkWidget*>(pEnvData->pWidget); + gtk_widget_show_all(pWindow); + + // build the widget tree as a child of the GtkEventBox GtkGrid parent + return new GtkInstanceBuilder(pWindow, rUIRoot, rUIFile, xEmbedWindow.get()); +} + +weld::MessageDialog* GtkInstance::CreateMessageDialog(weld::Widget* pParent, VclMessageType eMessageType, VclButtonsType eButtonsType, const OUString &rPrimaryMessage) +{ + GtkInstanceWidget* pParentInstance = dynamic_cast<GtkInstanceWidget*>(pParent); + GtkWindow* pParentWindow = pParentInstance ? pParentInstance->getWindow() : nullptr; + GtkMessageDialog* pMessageDialog = GTK_MESSAGE_DIALOG(gtk_message_dialog_new(pParentWindow, GTK_DIALOG_MODAL, + VclToGtk(eMessageType), VclToGtk(eButtonsType), "%s", + OUStringToOString(rPrimaryMessage, RTL_TEXTENCODING_UTF8).getStr())); + return new GtkInstanceMessageDialog(pMessageDialog, nullptr, true); +} + +weld::Window* GtkInstance::GetFrameWeld(const css::uno::Reference<css::awt::XWindow>& rWindow) +{ + if (SalGtkXWindow* pGtkXWindow = dynamic_cast<SalGtkXWindow*>(rWindow.get())) + return pGtkXWindow->getFrameWeld(); + return SalInstance::GetFrameWeld(rWindow); +} + +weld::Window* GtkSalFrame::GetFrameWeld() const +{ + if (!m_xFrameWeld) + m_xFrameWeld.reset(new GtkInstanceWindow(GTK_WINDOW(gtk_widget_get_toplevel(getWindow())), nullptr, false)); + return m_xFrameWeld.get(); +} + +void* GtkInstance::CreateGStreamerSink(const SystemChildWindow *pWindow) +{ +#if ENABLE_GSTREAMER_1_0 + auto aSymbol = gstElementFactoryNameSymbol(); + if (!aSymbol) + return nullptr; + + const SystemEnvData* pEnvData = pWindow->GetSystemData(); + if (!pEnvData) + return nullptr; + + GstElement* pVideosink = aSymbol("gtksink", "gtksink"); + if (!pVideosink) + return nullptr; + + GtkWidget *pGstWidget; + g_object_get(pVideosink, "widget", &pGstWidget, nullptr); + gtk_widget_set_vexpand(pGstWidget, true); + gtk_widget_set_hexpand(pGstWidget, true); + + GtkWidget *pParent = static_cast<GtkWidget*>(pEnvData->pWidget); + gtk_container_add(GTK_CONTAINER(pParent), pGstWidget); + g_object_unref(pGstWidget); + gtk_widget_show_all(pParent); + + return pVideosink; +#else + (void)pWindow; + return nullptr; +#endif +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/gtk3gtkobject.cxx b/vcl/unx/gtk3/gtk3gtkobject.cxx new file mode 100644 index 000000000..cb60406ec --- /dev/null +++ b/vcl/unx/gtk3/gtk3gtkobject.cxx @@ -0,0 +1,463 @@ +/* -*- 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#ifdef AIX +#define _LINUX_SOURCE_COMPAT +#include <sys/timer.h> +#undef _LINUX_SOURCE_COMPAT +#endif + +#include <unx/gtk/gtkbackend.hxx> +#include <unx/gtk/gtkobject.hxx> +#include <unx/gtk/gtkframe.hxx> +#include <unx/gtk/gtkdata.hxx> + +GtkSalObjectBase::GtkSalObjectBase(GtkSalFrame* pParent) + : m_pSocket(nullptr) + , m_pParent(pParent) + , m_pRegion(nullptr) +{ + if (!m_pParent) + return; +} + +GtkSalObject::GtkSalObject(GtkSalFrame* pParent, bool bShow) + : GtkSalObjectBase(pParent) +{ + if (!m_pParent) + return; + + // our plug window + m_pSocket = gtk_grid_new(); + Show( bShow ); + // insert into container + gtk_fixed_put( pParent->getFixedContainer(), + m_pSocket, + 0, 0 ); + + Init(); + + g_signal_connect( G_OBJECT(m_pSocket), "destroy", G_CALLBACK(signalDestroy), this ); + + // #i59255# necessary due to sync effects with java child windows + pParent->Flush(); +} + +void GtkSalObjectBase::Init() +{ + // realize so we can get a window id + gtk_widget_realize( m_pSocket ); + + // system data + m_aSystemData.aWindow = m_pParent->GetNativeWindowHandle(m_pSocket); + m_aSystemData.aShellWindow = reinterpret_cast<sal_IntPtr>(this); + m_aSystemData.pSalFrame = nullptr; + m_aSystemData.pWidget = m_pSocket; + m_aSystemData.nScreen = m_pParent->getXScreenNumber().getXScreen(); + m_aSystemData.toolkit = SystemEnvData::Toolkit::Gtk3; + GdkScreen* pScreen = gtk_widget_get_screen(m_pParent->getWindow()); + GdkVisual* pVisual = gdk_screen_get_system_visual(pScreen); + +#if defined(GDK_WINDOWING_X11) + GdkDisplay *pDisplay = GtkSalFrame::getGdkDisplay(); + if (DLSYM_GDK_IS_X11_DISPLAY(pDisplay)) + { + m_aSystemData.pDisplay = gdk_x11_display_get_xdisplay(pDisplay); + m_aSystemData.pVisual = gdk_x11_visual_get_xvisual(pVisual); + m_aSystemData.platform = SystemEnvData::Platform::Xcb; + } +#endif +#if defined(GDK_WINDOWING_WAYLAND) + if (DLSYM_GDK_IS_WAYLAND_DISPLAY(pDisplay)) + { + m_aSystemData.pDisplay = gdk_wayland_display_get_wl_display(pDisplay); + m_aSystemData.platform = SystemEnvData::Platform::Wayland; + } +#endif + + g_signal_connect( G_OBJECT(m_pSocket), "button-press-event", G_CALLBACK(signalButton), this ); + g_signal_connect( G_OBJECT(m_pSocket), "button-release-event", G_CALLBACK(signalButton), this ); + g_signal_connect( G_OBJECT(m_pSocket), "focus-in-event", G_CALLBACK(signalFocus), this ); + g_signal_connect( G_OBJECT(m_pSocket), "focus-out-event", G_CALLBACK(signalFocus), this ); +} + +GtkSalObjectBase::~GtkSalObjectBase() +{ + if( m_pRegion ) + { + cairo_region_destroy( m_pRegion ); + } +} + +GtkSalObject::~GtkSalObject() +{ + if( m_pSocket ) + { + // remove socket from parent frame's fixed container + gtk_container_remove( GTK_CONTAINER(gtk_widget_get_parent(m_pSocket)), + m_pSocket ); + // get rid of the socket + // actually the gtk_container_remove should let the ref count + // of the socket sink to 0 and destroy it (see signalDestroy) + // this is just a sanity check + if( m_pSocket ) + gtk_widget_destroy( m_pSocket ); + } +} + +void GtkSalObject::ResetClipRegion() +{ + if( m_pSocket ) + gdk_window_shape_combine_region( gtk_widget_get_window(m_pSocket), nullptr, 0, 0 ); +} + +void GtkSalObjectBase::BeginSetClipRegion( sal_uInt32 ) +{ + if (m_pRegion) + cairo_region_destroy(m_pRegion); + m_pRegion = cairo_region_create(); +} + +void GtkSalObjectBase::UnionClipRegion( long nX, long nY, long nWidth, long nHeight ) +{ + GdkRectangle aRect; + aRect.x = nX; + aRect.y = nY; + aRect.width = nWidth; + aRect.height = nHeight; + + cairo_region_union_rectangle( m_pRegion, &aRect ); +} + +void GtkSalObject::EndSetClipRegion() +{ + if( m_pSocket ) + gdk_window_shape_combine_region( gtk_widget_get_window(m_pSocket), m_pRegion, 0, 0 ); +} + +void GtkSalObject::SetPosSize(long nX, long nY, long nWidth, long nHeight) +{ + if (m_pSocket) + { + GtkFixed* pContainer = GTK_FIXED(gtk_widget_get_parent(m_pSocket)); + gtk_fixed_move( pContainer, m_pSocket, nX, nY ); + gtk_widget_set_size_request( m_pSocket, nWidth, nHeight ); + m_pParent->nopaint_container_resize_children(GTK_CONTAINER(pContainer)); + } +} + +void GtkSalObject::Reparent(SalFrame* pFrame) +{ + GtkSalFrame* pNewParent = static_cast<GtkSalFrame*>(pFrame); + if (m_pSocket) + { + GtkFixed* pContainer = GTK_FIXED(gtk_widget_get_parent(m_pSocket)); + + gint nX(0), nY(0); + gtk_container_child_get(GTK_CONTAINER(pContainer), m_pSocket, + "x", &nX, + "y", &nY, + nullptr); + + g_object_ref(m_pSocket); + gtk_container_remove(GTK_CONTAINER(pContainer), m_pSocket); + + gtk_fixed_put(pNewParent->getFixedContainer(), + m_pSocket, + nX, nY); + + g_object_unref(m_pSocket); + } + m_pParent = pNewParent; +} + +void GtkSalObject::Show( bool bVisible ) +{ + if( m_pSocket ) + { + if( bVisible ) + gtk_widget_show(m_pSocket); + else + gtk_widget_hide(m_pSocket); + } +} + +Size GtkSalObjectBase::GetOptimalSize() const +{ + if (m_pSocket) + { + bool bVisible = gtk_widget_get_visible(m_pSocket); + if (!bVisible) + gtk_widget_set_visible(m_pSocket, true); + GtkRequisition size; + gtk_widget_get_preferred_size(m_pSocket, nullptr, &size); + if (!bVisible) + gtk_widget_set_visible(m_pSocket, false); + return Size(size.width, size.height); + } + return Size(); +} + +const SystemEnvData* GtkSalObjectBase::GetSystemData() const +{ + return &m_aSystemData; +} + +gboolean GtkSalObjectBase::signalButton( GtkWidget*, GdkEventButton* pEvent, gpointer object ) +{ + GtkSalObjectBase* pThis = static_cast<GtkSalObject*>(object); + + if( pEvent->type == GDK_BUTTON_PRESS ) + { + pThis->CallCallback( SalObjEvent::ToTop ); + } + + return FALSE; +} + +gboolean GtkSalObjectBase::signalFocus( GtkWidget*, GdkEventFocus* pEvent, gpointer object ) +{ + GtkSalObjectBase* pThis = static_cast<GtkSalObject*>(object); + + pThis->CallCallback( pEvent->in ? SalObjEvent::GetFocus : SalObjEvent::LoseFocus ); + + return FALSE; +} + +void GtkSalObject::signalDestroy( GtkWidget* pObj, gpointer object ) +{ + GtkSalObject* pThis = static_cast<GtkSalObject*>(object); + if( pObj == pThis->m_pSocket ) + { + pThis->m_pSocket = nullptr; + } +} + +void GtkSalObjectBase::SetForwardKey( bool bEnable ) +{ + if( bEnable ) + gtk_widget_add_events( GTK_WIDGET( m_pSocket ), GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK ); + else + gtk_widget_set_events( GTK_WIDGET( m_pSocket ), ~(GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK) & gtk_widget_get_events( GTK_WIDGET( m_pSocket ) ) ); +} + +GtkSalObjectWidgetClip::GtkSalObjectWidgetClip(GtkSalFrame* pParent, bool bShow) + : GtkSalObjectBase(pParent) + , m_pScrolledWindow(nullptr) +{ + if( !pParent ) + return; + + m_pScrolledWindow = gtk_scrolled_window_new(nullptr, nullptr); + gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(m_pScrolledWindow), + GTK_POLICY_EXTERNAL, GTK_POLICY_EXTERNAL); + g_signal_connect(m_pScrolledWindow, "scroll-event", G_CALLBACK(signalScroll), this); + + // insert into container + gtk_fixed_put( pParent->getFixedContainer(), + m_pScrolledWindow, + 0, 0 ); + + // deliberately without adjustments to avoid gtk's auto adjustment on changing focus + GtkWidget* pViewPort = gtk_viewport_new(nullptr, nullptr); + + // force in a fake background of a suitable color + GtkStyleContext *pWidgetContext = gtk_widget_get_style_context(pViewPort); + GtkCssProvider* pBgCssProvider = gtk_css_provider_new(); + OUString sColor = Application::GetSettings().GetStyleSettings().GetDialogColor().AsRGBHexString(); + OUString aBuffer = "* { background-color: #" + sColor + "; }"; + OString aResult = OUStringToOString(aBuffer, RTL_TEXTENCODING_UTF8); + gtk_css_provider_load_from_data(pBgCssProvider, aResult.getStr(), aResult.getLength(), nullptr); + gtk_style_context_add_provider(pWidgetContext, GTK_STYLE_PROVIDER(pBgCssProvider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + + gtk_container_add(GTK_CONTAINER(m_pScrolledWindow), pViewPort); + gtk_widget_show(pViewPort); + + // our plug window + m_pSocket = gtk_grid_new(); + gtk_container_add(GTK_CONTAINER(pViewPort), m_pSocket); + gtk_widget_show(m_pSocket); + + Show(bShow); + + Init(); + + g_signal_connect( G_OBJECT(m_pSocket), "destroy", G_CALLBACK(signalDestroy), this ); +} + +GtkSalObjectWidgetClip::~GtkSalObjectWidgetClip() +{ + if( m_pSocket ) + { + // remove socket from parent frame's fixed container + gtk_container_remove( GTK_CONTAINER(gtk_widget_get_parent(m_pScrolledWindow)), + m_pScrolledWindow ); + // get rid of the socket + // actually the gtk_container_remove should let the ref count + // of the socket sink to 0 and destroy it (see signalDestroy) + // this is just a sanity check + if( m_pScrolledWindow ) + gtk_widget_destroy( m_pScrolledWindow ); + } +} + +void GtkSalObjectWidgetClip::ResetClipRegion() +{ + m_aClipRect = tools::Rectangle(); + ApplyClipRegion(); +} + +void GtkSalObjectWidgetClip::EndSetClipRegion() +{ + int nRects = cairo_region_num_rectangles(m_pRegion); + assert(nRects == 0 || nRects == 1); + if (nRects == 0) + m_aClipRect = tools::Rectangle(); + else + { + cairo_rectangle_int_t rectangle; + cairo_region_get_rectangle(m_pRegion, 0, &rectangle); + m_aClipRect = tools::Rectangle(Point(rectangle.x, rectangle.y), Size(rectangle.width, rectangle.height)); + } + ApplyClipRegion(); +} + +void GtkSalObjectWidgetClip::ApplyClipRegion() +{ + if( m_pSocket ) + { + GtkFixed* pContainer = GTK_FIXED(gtk_widget_get_parent(m_pScrolledWindow)); + + GtkAllocation allocation; + allocation.x = m_aRect.Left() + m_aClipRect.Left(); + allocation.y = m_aRect.Top() + m_aClipRect.Top(); + if (m_aClipRect.IsEmpty()) + { + allocation.width = m_aRect.GetWidth(); + allocation.height = m_aRect.GetHeight(); + } + else + { + allocation.width = m_aClipRect.GetWidth(); + allocation.height = m_aClipRect.GetHeight(); + } + + if (AllSettings::GetLayoutRTL()) + { + GtkAllocation aParentAllocation; + gtk_widget_get_allocation(GTK_WIDGET(pContainer), &aParentAllocation); + gtk_fixed_move(pContainer, m_pScrolledWindow, aParentAllocation.width - allocation.width - 1 - allocation.x, allocation.y); + } + else + gtk_fixed_move(pContainer, m_pScrolledWindow, allocation.x, allocation.y); + gtk_widget_set_size_request(m_pScrolledWindow, allocation.width, allocation.height); + gtk_widget_size_allocate(m_pScrolledWindow, &allocation); + + gtk_adjustment_set_value(gtk_scrolled_window_get_hadjustment(GTK_SCROLLED_WINDOW(m_pScrolledWindow)), m_aClipRect.Left()); + gtk_adjustment_set_value(gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(m_pScrolledWindow)), m_aClipRect.Top()); + } +} + +void GtkSalObjectWidgetClip::SetPosSize(long nX, long nY, long nWidth, long nHeight) +{ + m_aRect = tools::Rectangle(Point(nX, nY), Size(nWidth, nHeight)); + if (m_pSocket) + { + GtkFixed* pContainer = GTK_FIXED(gtk_widget_get_parent(m_pScrolledWindow)); + gtk_widget_set_size_request(m_pSocket, nWidth, nHeight); + ApplyClipRegion(); + m_pParent->nopaint_container_resize_children(GTK_CONTAINER(pContainer)); + } +} + +void GtkSalObjectWidgetClip::Reparent(SalFrame* pFrame) +{ + GtkSalFrame* pNewParent = static_cast<GtkSalFrame*>(pFrame); + if (m_pSocket) + { + GtkFixed* pContainer = GTK_FIXED(gtk_widget_get_parent(m_pScrolledWindow)); + + gint nX(0), nY(0); + gtk_container_child_get(GTK_CONTAINER(pContainer), m_pScrolledWindow, + "x", &nX, + "y", &nY, + nullptr); + + g_object_ref(m_pScrolledWindow); + gtk_container_remove(GTK_CONTAINER(pContainer), m_pScrolledWindow); + + gtk_fixed_put(pNewParent->getFixedContainer(), + m_pScrolledWindow, + nX, nY); + + g_object_unref(m_pScrolledWindow); + } + m_pParent = pNewParent; +} + +void GtkSalObjectWidgetClip::Show( bool bVisible ) +{ + if( m_pSocket ) + { + if( bVisible ) + gtk_widget_show(m_pScrolledWindow); + else + gtk_widget_hide(m_pScrolledWindow); + } +} + +void GtkSalObjectWidgetClip::signalDestroy( GtkWidget* pObj, gpointer object ) +{ + GtkSalObjectWidgetClip* pThis = static_cast<GtkSalObjectWidgetClip*>(object); + if( pObj == pThis->m_pSocket ) + { + pThis->m_pSocket = nullptr; + pThis->m_pScrolledWindow = nullptr; + } +} + +gboolean GtkSalObjectWidgetClip::signalScroll(GtkWidget* pScrolledWindow, GdkEvent* pEvent, gpointer object) +{ + GtkSalObjectWidgetClip* pThis = static_cast<GtkSalObjectWidgetClip*>(object); + return pThis->signal_scroll(pScrolledWindow, pEvent); +} + +// forward the wheel scroll events onto the main window instead +bool GtkSalObjectWidgetClip::signal_scroll(GtkWidget*, GdkEvent* pEvent) +{ + GtkWidget* pEventWidget = gtk_get_event_widget(pEvent); + + GtkWidget* pMouseEventWidget = m_pParent->getMouseEventWidget(); + + gint dest_x, dest_y; + gtk_widget_translate_coordinates(pEventWidget, + pMouseEventWidget, + pEvent->scroll.x, + pEvent->scroll.y, + &dest_x, + &dest_y); + pEvent->scroll.x = dest_x; + pEvent->scroll.y = dest_y; + + GtkSalFrame::signalScroll(pMouseEventWidget, pEvent, m_pParent); + return true; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/gtk3gtkprintwrapper.cxx b/vcl/unx/gtk3/gtk3gtkprintwrapper.cxx new file mode 100644 index 000000000..0066fa004 --- /dev/null +++ b/vcl/unx/gtk3/gtk3gtkprintwrapper.cxx @@ -0,0 +1,153 @@ +/* -*- 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 <unx/gtk/gtkprintwrapper.hxx> + +namespace vcl::unx +{ + +GtkPrintWrapper::GtkPrintWrapper() +{ +} + +GtkPrintWrapper::~GtkPrintWrapper() +{ +} + +bool GtkPrintWrapper::supportsPrinting() const +{ + (void) this; // loplugin:staticmethods + return true; +} + +bool GtkPrintWrapper::supportsPrintSelection() const +{ + (void) this; // loplugin:staticmethods + return true; +} + +GtkPageSetup* GtkPrintWrapper::page_setup_new() const +{ + (void) this; // loplugin:staticmethods + return gtk_page_setup_new(); +} + +GtkPrintJob* GtkPrintWrapper::print_job_new(const gchar* title, GtkPrinter* printer, GtkPrintSettings* settings, GtkPageSetup* page_setup) const +{ + (void) this; // loplugin:staticmethods + return gtk_print_job_new(title, printer, settings, page_setup); +} + +void GtkPrintWrapper::print_job_send(GtkPrintJob* job, GtkPrintJobCompleteFunc callback, gpointer user_data, GDestroyNotify dnotify) const +{ + (void) this; // loplugin:staticmethods + gtk_print_job_send(job, callback, user_data, dnotify); +} + +gboolean GtkPrintWrapper::print_job_set_source_file(GtkPrintJob* job, const gchar* filename, GError** error) const +{ + (void) this; // loplugin:staticmethods + return gtk_print_job_set_source_file(job, filename, error); +} + +const gchar* GtkPrintWrapper::print_settings_get(GtkPrintSettings* settings, const gchar* key) const +{ + (void) this; // loplugin:staticmethods + return gtk_print_settings_get(settings, key); +} + +gboolean GtkPrintWrapper::print_settings_get_collate(GtkPrintSettings* settings) const +{ + (void) this; // loplugin:staticmethods + return gtk_print_settings_get_collate(settings); +} + +void GtkPrintWrapper::print_settings_set_collate(GtkPrintSettings* settings, gboolean collate) const +{ + (void) this; // loplugin:staticmethods + gtk_print_settings_set_collate(settings, collate); +} + +gint GtkPrintWrapper::print_settings_get_n_copies(GtkPrintSettings* settings) const +{ + (void) this; // loplugin:staticmethods + return gtk_print_settings_get_n_copies(settings); +} + +void GtkPrintWrapper::print_settings_set_n_copies(GtkPrintSettings* settings, gint num_copies) const +{ + (void) this; // loplugin:staticmethods + gtk_print_settings_set_n_copies(settings, num_copies); +} + +GtkPageRange* GtkPrintWrapper::print_settings_get_page_ranges(GtkPrintSettings* settings, gint* num_ranges) const +{ + (void) this; // loplugin:staticmethods + return gtk_print_settings_get_page_ranges(settings, num_ranges); +} + +void GtkPrintWrapper::print_settings_set_print_pages(GtkPrintSettings* settings, GtkPrintPages pages) const +{ + (void) this; // loplugin:staticmethods + gtk_print_settings_set_print_pages(settings, pages); +} + +GtkWidget* GtkPrintWrapper::print_unix_dialog_new() const +{ + (void) this; // loplugin:staticmethods + return gtk_print_unix_dialog_new(nullptr, nullptr); +} + +void GtkPrintWrapper::print_unix_dialog_add_custom_tab(GtkPrintUnixDialog* dialog, GtkWidget* child, GtkWidget* tab_label) const +{ + (void) this; // loplugin:staticmethods + gtk_print_unix_dialog_add_custom_tab(dialog, child, tab_label); +} + +GtkPrinter* GtkPrintWrapper::print_unix_dialog_get_selected_printer(GtkPrintUnixDialog* dialog) const +{ + (void) this; // loplugin:staticmethods + GtkPrinter* pRet = gtk_print_unix_dialog_get_selected_printer(dialog); + g_object_ref(G_OBJECT(pRet)); + return pRet; +} + +void GtkPrintWrapper::print_unix_dialog_set_manual_capabilities(GtkPrintUnixDialog* dialog, GtkPrintCapabilities capabilities) const +{ + (void) this; // loplugin:staticmethods + gtk_print_unix_dialog_set_manual_capabilities(dialog, capabilities); +} + +GtkPrintSettings* GtkPrintWrapper::print_unix_dialog_get_settings(GtkPrintUnixDialog* dialog) const +{ + (void) this; // loplugin:staticmethods + return gtk_print_unix_dialog_get_settings(dialog); +} + +void GtkPrintWrapper::print_unix_dialog_set_settings(GtkPrintUnixDialog* dialog, GtkPrintSettings* settings) const +{ + (void) this; // loplugin:staticmethods + gtk_print_unix_dialog_set_settings(dialog, settings); +} + +void GtkPrintWrapper::print_unix_dialog_set_support_selection(GtkPrintUnixDialog* dialog, gboolean support_selection) const +{ + (void) this; // loplugin:staticmethods + gtk_print_unix_dialog_set_support_selection(dialog, support_selection); +} + +void GtkPrintWrapper::print_unix_dialog_set_has_selection(GtkPrintUnixDialog* dialog, gboolean has_selection) const +{ + (void) this; // loplugin:staticmethods + gtk_print_unix_dialog_set_has_selection(dialog, has_selection); +} + +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/gtk3gtksalmenu.cxx b/vcl/unx/gtk3/gtk3gtksalmenu.cxx new file mode 100644 index 000000000..262187f16 --- /dev/null +++ b/vcl/unx/gtk3/gtk3gtksalmenu.cxx @@ -0,0 +1,1409 @@ +/* -*- 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 <unx/gtk/gtksalmenu.hxx> + +#include <unx/gtk/gtkdata.hxx> +#include <unx/gtk/glomenu.h> +#include <unx/gtk/gloactiongroup.h> +#include <vcl/floatwin.hxx> +#include <vcl/menu.hxx> +#include <vcl/pngwrite.hxx> + +#include <sal/log.hxx> +#include <tools/stream.hxx> +#include <window.h> +#include <strings.hrc> + +static bool bUnityMode = false; + +/* + * This function generates a unique command name for each menu item + */ +static gchar* GetCommandForItem(GtkSalMenu* pParentMenu, sal_uInt16 nItemId) +{ + OString aCommand = "window-" + + OString::number(reinterpret_cast<unsigned long>(pParentMenu)) + + "-" + OString::number(nItemId); + return g_strdup(aCommand.getStr()); +} + +static gchar* GetCommandForItem(GtkSalMenuItem* pSalMenuItem) +{ + return GetCommandForItem(pSalMenuItem->mpParentMenu, + pSalMenuItem->mnId); +} + +bool GtkSalMenu::PrepUpdate() +{ + return mpMenuModel && mpActionGroup; +} + +/* + * Menu updating methods + */ + +static void RemoveSpareItemsFromNativeMenu( GLOMenu* pMenu, GList** pOldCommandList, unsigned nSection, unsigned nValidItems ) +{ + sal_Int32 nSectionItems = g_lo_menu_get_n_items_from_section( pMenu, nSection ); + + while ( nSectionItems > static_cast<sal_Int32>(nValidItems) ) + { + gchar* aCommand = g_lo_menu_get_command_from_item_in_section( pMenu, nSection, --nSectionItems ); + + if ( aCommand != nullptr && pOldCommandList != nullptr ) + *pOldCommandList = g_list_append( *pOldCommandList, g_strdup( aCommand ) ); + + g_free( aCommand ); + + g_lo_menu_remove_from_section( pMenu, nSection, nSectionItems ); + } +} + +typedef std::pair<GtkSalMenu*, sal_uInt16> MenuAndId; + +namespace +{ + MenuAndId decode_command(const gchar *action_name) + { + OString sCommand(action_name); + + sal_Int32 nIndex = 0; + OString sWindow = sCommand.getToken(0, '-', nIndex); + OString sGtkSalMenu = sCommand.getToken(0, '-', nIndex); + OString sItemId = sCommand.getToken(0, '-', nIndex); + + GtkSalMenu* pSalSubMenu = reinterpret_cast<GtkSalMenu*>(sGtkSalMenu.toInt64()); + + assert(sWindow == "window" && pSalSubMenu); + (void) sWindow; + + return MenuAndId(pSalSubMenu, sItemId.toInt32()); + } +} + +static void RemoveDisabledItemsFromNativeMenu(GLOMenu* pMenu, GList** pOldCommandList, + sal_Int32 nSection, GActionGroup* pActionGroup) +{ + while (nSection >= 0) + { + sal_Int32 nSectionItems = g_lo_menu_get_n_items_from_section( pMenu, nSection ); + while (nSectionItems--) + { + gchar* pCommand = g_lo_menu_get_command_from_item_in_section(pMenu, nSection, nSectionItems); + // remove disabled entries + bool bRemove = !g_action_group_get_action_enabled(pActionGroup, pCommand); + if (!bRemove) + { + //also remove any empty submenus + GLOMenu* pSubMenuModel = g_lo_menu_get_submenu_from_item_in_section(pMenu, nSection, nSectionItems); + if (pSubMenuModel) + { + gint nSubMenuSections = g_menu_model_get_n_items(G_MENU_MODEL(pSubMenuModel)); + if (nSubMenuSections == 0) + bRemove = true; + else if (nSubMenuSections == 1) + { + gint nItems = g_lo_menu_get_n_items_from_section(pSubMenuModel, 0); + if (nItems == 0) + bRemove = true; + else if (nItems == 1) + { + //If the only entry is the "No Selection Possible" entry, then we are allowed + //to removed it + gchar* pSubCommand = g_lo_menu_get_command_from_item_in_section(pSubMenuModel, 0, 0); + MenuAndId aMenuAndId(decode_command(pSubCommand)); + bRemove = aMenuAndId.second == 0xFFFF; + g_free(pSubCommand); + } + } + } + } + + if (bRemove) + { + //but tdf#86850 Always display clipboard functions + bRemove = g_strcmp0(pCommand, ".uno:Cut") && + g_strcmp0(pCommand, ".uno:Copy") && + g_strcmp0(pCommand, ".uno:Paste"); + } + + if (bRemove) + { + if (pCommand != nullptr && pOldCommandList != nullptr) + *pOldCommandList = g_list_append(*pOldCommandList, g_strdup(pCommand)); + g_lo_menu_remove_from_section(pMenu, nSection, nSectionItems); + } + + g_free(pCommand); + } + --nSection; + } +} + +static void RemoveSpareSectionsFromNativeMenu( GLOMenu* pMenu, GList** pOldCommandList, sal_Int32 nLastSection ) +{ + if ( pMenu == nullptr || pOldCommandList == nullptr ) + return; + + sal_Int32 n = g_menu_model_get_n_items( G_MENU_MODEL( pMenu ) ) - 1; + + for ( ; n > nLastSection; n--) + { + RemoveSpareItemsFromNativeMenu( pMenu, pOldCommandList, n, 0 ); + g_lo_menu_remove( pMenu, n ); + } +} + +static gint CompareStr( gpointer str1, gpointer str2 ) +{ + return g_strcmp0( static_cast<const gchar*>(str1), static_cast<const gchar*>(str2) ); +} + +static void RemoveUnusedCommands( GLOActionGroup* pActionGroup, GList* pOldCommandList, GList* pNewCommandList ) +{ + if ( pActionGroup == nullptr || pOldCommandList == nullptr ) + { + g_list_free_full( pOldCommandList, g_free ); + g_list_free_full( pNewCommandList, g_free ); + return; + } + + while ( pNewCommandList != nullptr ) + { + GList* pNewCommand = g_list_first( pNewCommandList ); + pNewCommandList = g_list_remove_link( pNewCommandList, pNewCommand ); + + gpointer aCommand = g_list_nth_data( pNewCommand, 0 ); + + GList* pOldCommand = g_list_find_custom( pOldCommandList, aCommand, reinterpret_cast<GCompareFunc>(CompareStr) ); + + if ( pOldCommand != nullptr ) + { + pOldCommandList = g_list_remove_link( pOldCommandList, pOldCommand ); + g_list_free_full( pOldCommand, g_free ); + } + + g_list_free_full( pNewCommand, g_free ); + } + + while ( pOldCommandList != nullptr ) + { + GList* pCommand = g_list_first( pOldCommandList ); + pOldCommandList = g_list_remove_link( pOldCommandList, pCommand ); + + gchar* aCommand = static_cast<gchar*>(g_list_nth_data( pCommand, 0 )); + + g_lo_action_group_remove( pActionGroup, aCommand ); + + g_list_free_full( pCommand, g_free ); + } +} + +void GtkSalMenu::ImplUpdate(bool bRecurse, bool bRemoveDisabledEntries) +{ + SolarMutexGuard aGuard; + + SAL_INFO("vcl.unity", "ImplUpdate pre PrepUpdate"); + if( !PrepUpdate() ) + return; + + if (mbNeedsUpdate) + { + mbNeedsUpdate = false; + if (mbMenuBar && maUpdateMenuBarIdle.IsActive()) + { + maUpdateMenuBarIdle.Stop(); + // tdf#124391 Prevent doubled menus in global menu + if (!bUnityMode) + { + maUpdateMenuBarIdle.Invoke(); + return; + } + } + } + + Menu* pVCLMenu = mpVCLMenu; + GLOMenu* pLOMenu = G_LO_MENU( mpMenuModel ); + GLOActionGroup* pActionGroup = G_LO_ACTION_GROUP( mpActionGroup ); + SAL_INFO("vcl.unity", "Syncing vcl menu " << pVCLMenu << " to menu model " << pLOMenu << " and action group " << pActionGroup); + GList *pOldCommandList = nullptr; + GList *pNewCommandList = nullptr; + + sal_uInt16 nLOMenuSize = g_menu_model_get_n_items( G_MENU_MODEL( pLOMenu ) ); + + if ( nLOMenuSize == 0 ) + g_lo_menu_new_section( pLOMenu, 0, nullptr ); + + sal_Int32 nSection = 0; + sal_Int32 nItemPos = 0; + sal_Int32 validItems = 0; + sal_Int32 nItem; + + for ( nItem = 0; nItem < static_cast<sal_Int32>(GetItemCount()); nItem++ ) { + if ( !IsItemVisible( nItem ) ) + continue; + + GtkSalMenuItem *pSalMenuItem = GetItemAtPos( nItem ); + sal_uInt16 nId = pSalMenuItem->mnId; + + // PopupMenu::ImplExecute might add <No Selection Possible> entry to top-level + // popup menu, but we have our own implementation below, so skip that one. + if ( nId == 0xFFFF ) + continue; + + if ( pSalMenuItem->mnType == MenuItemType::SEPARATOR ) + { + // Delete extra items from current section. + RemoveSpareItemsFromNativeMenu( pLOMenu, &pOldCommandList, nSection, validItems ); + + nSection++; + nItemPos = 0; + validItems = 0; + + if ( nLOMenuSize <= nSection ) + { + g_lo_menu_new_section( pLOMenu, nSection, nullptr ); + nLOMenuSize++; + } + + continue; + } + + if ( nItemPos >= g_lo_menu_get_n_items_from_section( pLOMenu, nSection ) ) + g_lo_menu_insert_in_section( pLOMenu, nSection, nItemPos, "EMPTY STRING" ); + + // Get internal menu item values. + OUString aText = pVCLMenu->GetItemText( nId ); + Image aImage = pVCLMenu->GetItemImage( nId ); + bool bEnabled = pVCLMenu->IsItemEnabled( nId ); + vcl::KeyCode nAccelKey = pVCLMenu->GetAccelKey( nId ); + bool bChecked = pVCLMenu->IsItemChecked( nId ); + MenuItemBits itemBits = pVCLMenu->GetItemBits( nId ); + + // Store current item command in command list. + gchar *aCurrentCommand = g_lo_menu_get_command_from_item_in_section( pLOMenu, nSection, nItemPos ); + + if ( aCurrentCommand != nullptr ) + pOldCommandList = g_list_append( pOldCommandList, aCurrentCommand ); + + // Get the new command for the item. + gchar* aNativeCommand = GetCommandForItem(pSalMenuItem); + + // Force updating of native menu labels. + NativeSetItemText( nSection, nItemPos, aText ); + NativeSetItemIcon( nSection, nItemPos, aImage ); + NativeSetAccelerator( nSection, nItemPos, nAccelKey, nAccelKey.GetName( GetFrame()->GetWindow() ) ); + + if ( g_strcmp0( aNativeCommand, "" ) != 0 && pSalMenuItem->mpSubMenu == nullptr ) + { + NativeSetItemCommand( nSection, nItemPos, nId, aNativeCommand, itemBits, bChecked, false ); + NativeCheckItem( nSection, nItemPos, itemBits, bChecked ); + NativeSetEnableItem( aNativeCommand, bEnabled ); + + pNewCommandList = g_list_append( pNewCommandList, g_strdup( aNativeCommand ) ); + } + + GtkSalMenu* pSubmenu = pSalMenuItem->mpSubMenu; + + if ( pSubmenu && pSubmenu->GetMenu() ) + { + bool bNonMenuChangedToMenu = NativeSetItemCommand( nSection, nItemPos, nId, aNativeCommand, itemBits, false, true ); + pNewCommandList = g_list_append( pNewCommandList, g_strdup( aNativeCommand ) ); + + GLOMenu* pSubMenuModel = g_lo_menu_get_submenu_from_item_in_section( pLOMenu, nSection, nItemPos ); + + if ( pSubMenuModel == nullptr ) + { + g_lo_menu_new_submenu_in_item_in_section( pLOMenu, nSection, nItemPos ); + pSubMenuModel = g_lo_menu_get_submenu_from_item_in_section( pLOMenu, nSection, nItemPos ); + } + + g_object_unref( pSubMenuModel ); + + if (bRecurse || bNonMenuChangedToMenu) + { + SAL_INFO("vcl.unity", "preparing submenu " << pSubMenuModel << " to menu model " << G_MENU_MODEL(pSubMenuModel) << " and action group " << G_ACTION_GROUP(pActionGroup)); + pSubmenu->SetMenuModel( G_MENU_MODEL( pSubMenuModel ) ); + pSubmenu->SetActionGroup( G_ACTION_GROUP( pActionGroup ) ); + pSubmenu->ImplUpdate(true, bRemoveDisabledEntries); + } + } + + g_free( aNativeCommand ); + + ++nItemPos; + ++validItems; + } + + if (bRemoveDisabledEntries) + { + // Delete disabled items in last section. + RemoveDisabledItemsFromNativeMenu(pLOMenu, &pOldCommandList, nSection, G_ACTION_GROUP(pActionGroup)); + } + + // Delete extra items in last section. + RemoveSpareItemsFromNativeMenu( pLOMenu, &pOldCommandList, nSection, validItems ); + + // Delete extra sections. + RemoveSpareSectionsFromNativeMenu( pLOMenu, &pOldCommandList, nSection ); + + // Delete unused commands. + RemoveUnusedCommands( pActionGroup, pOldCommandList, pNewCommandList ); + + // Resolves: tdf#103166 if the menu is empty, add a disabled + // <No Selection Possible> placeholder. + sal_Int32 nSectionsCount = g_menu_model_get_n_items(G_MENU_MODEL(pLOMenu)); + gint nItemsCount = 0; + for (nSection = 0; nSection < nSectionsCount; ++nSection) + { + nItemsCount += g_lo_menu_get_n_items_from_section(pLOMenu, nSection); + if (nItemsCount) + break; + } + if (!nItemsCount) + { + gchar* aNativeCommand = GetCommandForItem(this, 0xFFFF); + OUString aPlaceholderText(VclResId(SV_RESID_STRING_NOSELECTIONPOSSIBLE)); + g_lo_menu_insert_in_section(pLOMenu, nSection-1, 0, + OUStringToOString(aPlaceholderText, RTL_TEXTENCODING_UTF8).getStr()); + NativeSetItemCommand(nSection-1, 0, 0xFFFF, aNativeCommand, MenuItemBits::NONE, false, false); + NativeSetEnableItem(aNativeCommand, false); + g_free(aNativeCommand); + } +} + +void GtkSalMenu::Update() +{ + //find out if top level is a menubar or not, if not, then it's a popup menu + //hierarchy and in those we hide (most) disabled entries + const GtkSalMenu* pMenu = this; + while (pMenu->mpParentSalMenu) + pMenu = pMenu->mpParentSalMenu; + + bool bAlwaysShowDisabledEntries; + if (pMenu->mbMenuBar) + bAlwaysShowDisabledEntries = true; + else + bAlwaysShowDisabledEntries = bool(mpVCLMenu->GetMenuFlags() & MenuFlags::AlwaysShowDisabledEntries); + + ImplUpdate(false, !bAlwaysShowDisabledEntries); +} + +static void MenuPositionFunc(GtkMenu* menu, gint* x, gint* y, gboolean* push_in, gpointer user_data) +{ + Point *pPos = static_cast<Point*>(user_data); + *x = pPos->X(); + if (gtk_widget_get_default_direction() == GTK_TEXT_DIR_RTL) + { + GtkRequisition natural_size; + gtk_widget_get_preferred_size(GTK_WIDGET(menu), nullptr, &natural_size); + *x -= natural_size.width; + } + *y = pPos->Y(); + *push_in = false; +} + +bool GtkSalMenu::ShowNativePopupMenu(FloatingWindow* pWin, const tools::Rectangle& rRect, + FloatWinPopupFlags nFlags) +{ + VclPtr<vcl::Window> xParent = pWin->ImplGetWindowImpl()->mpRealParent; + mpFrame = static_cast<GtkSalFrame*>(xParent->ImplGetFrame()); + + GLOActionGroup* pActionGroup = g_lo_action_group_new(); + mpActionGroup = G_ACTION_GROUP(pActionGroup); + mpMenuModel = G_MENU_MODEL(g_lo_menu_new()); + // Generate the main menu structure, populates mpMenuModel + UpdateFull(); + + GtkWidget *pWidget = gtk_menu_new_from_model(mpMenuModel); + gtk_menu_attach_to_widget(GTK_MENU(pWidget), mpFrame->getMouseEventWidget(), nullptr); + gtk_widget_insert_action_group(mpFrame->getMouseEventWidget(), "win", mpActionGroup); + + //run in a sub main loop because we need to keep vcl PopupMenu alive to use + //it during DispatchCommand, returning now to the outer loop causes the + //launching PopupMenu to be destroyed, instead run the subloop here + //until the gtk menu is destroyed + GMainLoop* pLoop = g_main_loop_new(nullptr, true); + g_signal_connect_swapped(G_OBJECT(pWidget), "deactivate", G_CALLBACK(g_main_loop_quit), pLoop); + +#if GTK_CHECK_VERSION(3,22,0) + if (gtk_check_version(3, 22, 0) == nullptr) + { + GdkGravity rect_anchor = GDK_GRAVITY_SOUTH_WEST, menu_anchor = GDK_GRAVITY_NORTH_WEST; + + if (nFlags & FloatWinPopupFlags::Left) + { + rect_anchor = GDK_GRAVITY_NORTH_WEST; + menu_anchor = GDK_GRAVITY_NORTH_EAST; + } + else if (nFlags & FloatWinPopupFlags::Up) + { + rect_anchor = GDK_GRAVITY_NORTH_WEST; + menu_anchor = GDK_GRAVITY_SOUTH_WEST; + } + else if (nFlags & FloatWinPopupFlags::Right) + { + rect_anchor = GDK_GRAVITY_NORTH_EAST; + } + + tools::Rectangle aFloatRect = FloatingWindow::ImplConvertToAbsPos(xParent, rRect); + aFloatRect.Move(-mpFrame->maGeometry.nX, -mpFrame->maGeometry.nY); + GdkRectangle rect {static_cast<int>(aFloatRect.Left()), static_cast<int>(aFloatRect.Top()), + static_cast<int>(aFloatRect.GetWidth()), static_cast<int>(aFloatRect.GetHeight())}; + + GdkWindow* gdkWindow = gtk_widget_get_window(mpFrame->getMouseEventWidget()); + gtk_menu_popup_at_rect(GTK_MENU(pWidget), gdkWindow, &rect, rect_anchor, menu_anchor, nullptr); + } + else +#endif + { + guint nButton; + guint32 nTime; + + //typically there is an event, and we can then distinguish if this was + //launched from the keyboard (gets auto-mnemoniced) or the mouse (which + //doesn't) + GdkEvent *pEvent = gtk_get_current_event(); + if (pEvent) + { + gdk_event_get_button(pEvent, &nButton); + nTime = gdk_event_get_time(pEvent); + } + else + { + nButton = 0; + nTime = GtkSalFrame::GetLastInputEventTime(); + } + + // do the same strange semantics as vcl popup windows to arrive at a frame geometry + // in mirrored UI case; best done by actually executing the same code + sal_uInt16 nArrangeIndex; + Point aPos = FloatingWindow::ImplCalcPos(pWin, rRect, nFlags, nArrangeIndex); + aPos = FloatingWindow::ImplConvertToAbsPos(xParent, aPos); + + gtk_menu_popup(GTK_MENU(pWidget), nullptr, nullptr, MenuPositionFunc, + &aPos, nButton, nTime); + } + + if (g_main_loop_is_running(pLoop)) + { + gdk_threads_leave(); + g_main_loop_run(pLoop); + gdk_threads_enter(); + } + g_main_loop_unref(pLoop); + + mpVCLMenu->Deactivate(); + + gtk_widget_insert_action_group(mpFrame->getMouseEventWidget(), "win", nullptr); + + gtk_widget_destroy(pWidget); + + g_object_unref(mpActionGroup); + ClearActionGroupAndMenuModel(); + + mpFrame = nullptr; + + return true; +} + +/* + * GtkSalMenu + */ + +GtkSalMenu::GtkSalMenu( bool bMenuBar ) : + mbInActivateCallback( false ), + mbMenuBar( bMenuBar ), + mbNeedsUpdate( false ), + mbReturnFocusToDocument( false ), + mbAddedGrab( false ), + mpMenuBarContainerWidget( nullptr ), + mpMenuAllowShrinkWidget( nullptr ), + mpMenuBarWidget( nullptr ), + mpMenuBarContainerProvider( nullptr ), + mpMenuBarProvider( nullptr ), + mpCloseButton( nullptr ), + mpVCLMenu( nullptr ), + mpParentSalMenu( nullptr ), + mpFrame( nullptr ), + mpMenuModel( nullptr ), + mpActionGroup( nullptr ) +{ + //typically this only gets called after the menu has been customized on the + //next idle slot, in the normal case of a new menubar SetFrame is called + //directly long before this idle would get called. + maUpdateMenuBarIdle.SetPriority(TaskPriority::HIGHEST); + maUpdateMenuBarIdle.SetInvokeHandler(LINK(this, GtkSalMenu, MenuBarHierarchyChangeHandler)); + maUpdateMenuBarIdle.SetDebugName("Native Gtk Menu Update Idle"); +} + +IMPL_LINK_NOARG(GtkSalMenu, MenuBarHierarchyChangeHandler, Timer *, void) +{ + SAL_WARN_IF(!mpFrame, "vcl.gtk", "MenuBar layout changed, but no frame for some reason!"); + if (!mpFrame) + return; + SetFrame(mpFrame); +} + +void GtkSalMenu::SetNeedsUpdate() +{ + GtkSalMenu* pMenu = this; + // start that the menu and its parents are in need of an update + // on the next activation + while (pMenu && !pMenu->mbNeedsUpdate) + { + pMenu->mbNeedsUpdate = true; + pMenu = pMenu->mpParentSalMenu; + } + // only if a menubar is directly updated do we force in a full + // structure update + if (mbMenuBar && !maUpdateMenuBarIdle.IsActive()) + maUpdateMenuBarIdle.Start(); +} + +void GtkSalMenu::SetMenuModel(GMenuModel* pMenuModel) +{ + if (mpMenuModel) + g_object_unref(mpMenuModel); + mpMenuModel = pMenuModel; + if (mpMenuModel) + g_object_ref(mpMenuModel); +} + +GtkSalMenu::~GtkSalMenu() +{ + SolarMutexGuard aGuard; + + DestroyMenuBarWidget(); + + if (mpMenuModel) + g_object_unref(mpMenuModel); + + maItems.clear(); + + if (mpFrame) + mpFrame->SetMenu(nullptr); +} + +bool GtkSalMenu::VisibleMenuBar() +{ + return mbMenuBar && (bUnityMode || mpMenuBarContainerWidget); +} + +void GtkSalMenu::InsertItem( SalMenuItem* pSalMenuItem, unsigned nPos ) +{ + SolarMutexGuard aGuard; + GtkSalMenuItem *pItem = static_cast<GtkSalMenuItem*>( pSalMenuItem ); + + if ( nPos == MENU_APPEND ) + maItems.push_back( pItem ); + else + maItems.insert( maItems.begin() + nPos, pItem ); + + pItem->mpParentMenu = this; + + SetNeedsUpdate(); +} + +void GtkSalMenu::RemoveItem( unsigned nPos ) +{ + SolarMutexGuard aGuard; + maItems.erase( maItems.begin() + nPos ); + SetNeedsUpdate(); +} + +void GtkSalMenu::SetSubMenu( SalMenuItem* pSalMenuItem, SalMenu* pSubMenu, unsigned ) +{ + SolarMutexGuard aGuard; + GtkSalMenuItem *pItem = static_cast< GtkSalMenuItem* >( pSalMenuItem ); + GtkSalMenu *pGtkSubMenu = static_cast< GtkSalMenu* >( pSubMenu ); + + if ( pGtkSubMenu == nullptr ) + return; + + pGtkSubMenu->mpParentSalMenu = this; + pItem->mpSubMenu = pGtkSubMenu; + + SetNeedsUpdate(); +} + +static void CloseMenuBar(GtkWidget *, gpointer pMenu) +{ + Application::PostUserEvent(static_cast<MenuBar*>(pMenu)->GetCloseButtonClickHdl()); +} + +void GtkSalMenu::ShowCloseButton(bool bShow) +{ + assert(mbMenuBar); + if (!mpMenuBarContainerWidget) + return; + + if (!bShow) + { + if (mpCloseButton) + { + gtk_widget_destroy(mpCloseButton); + mpCloseButton = nullptr; + } + return; + } + + MenuBar *pVclMenuBar = static_cast<MenuBar*>(mpVCLMenu.get()); + mpCloseButton = gtk_button_new(); + g_signal_connect(mpCloseButton, "clicked", G_CALLBACK(CloseMenuBar), pVclMenuBar); + + gtk_button_set_relief(GTK_BUTTON(mpCloseButton), GTK_RELIEF_NONE); + gtk_button_set_focus_on_click(GTK_BUTTON(mpCloseButton), false); + gtk_widget_set_can_focus(mpCloseButton, false); + + GtkStyleContext *pButtonContext = gtk_widget_get_style_context(GTK_WIDGET(mpCloseButton)); + + GtkCssProvider *pProvider = gtk_css_provider_new(); + static const gchar data[] = "* { " + "padding: 0;" + "margin-left: 8px;" + "margin-right: 8px;" + "min-width: 18px;" + "min-height: 18px;" + "}"; + const gchar olddata[] = "* { " + "padding: 0;" + "margin-left: 8px;" + "margin-right: 8px;" + "}"; + gtk_css_provider_load_from_data(pProvider, gtk_check_version(3, 20, 0) == nullptr ? data : olddata, -1, nullptr); + gtk_style_context_add_provider(pButtonContext, + GTK_STYLE_PROVIDER(pProvider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + + gtk_style_context_add_class(pButtonContext, "flat"); + gtk_style_context_add_class(pButtonContext, "small-button"); + + GIcon* icon = g_themed_icon_new_with_default_fallbacks("window-close-symbolic"); + GtkWidget* image = gtk_image_new_from_gicon(icon, GTK_ICON_SIZE_MENU); + gtk_widget_show(image); + g_object_unref(icon); + + OUString sToolTip(VclResId(SV_HELPTEXT_CLOSEDOCUMENT)); + gtk_widget_set_tooltip_text(mpCloseButton, + OUStringToOString(sToolTip, RTL_TEXTENCODING_UTF8).getStr()); + + gtk_widget_set_valign(mpCloseButton, GTK_ALIGN_CENTER); + + gtk_container_add(GTK_CONTAINER(mpCloseButton), image); + gtk_grid_attach(GTK_GRID(mpMenuBarContainerWidget), GTK_WIDGET(mpCloseButton), 1, 0, 1, 1); + gtk_widget_show_all(mpCloseButton); +} + +//Typically when the menubar is deactivated we want the focus to return +//to where it came from. If the menubar was activated because of F6 +//moving focus into the associated VCL menubar then on pressing ESC +//or any other normal reason for deactivation we want focus to return +//to the document, definitely not still stuck in the associated +//VCL menubar. But if F6 is pressed while the menubar is activated +//we want to pass that F6 back to the VCL menubar which will move +//focus to the next pane by itself. +void GtkSalMenu::ReturnFocus() +{ + if (mbAddedGrab) + { + gtk_grab_remove(mpMenuBarWidget); + mbAddedGrab = false; + } + if (!mbReturnFocusToDocument) + gtk_widget_grab_focus(GTK_WIDGET(mpFrame->getEventBox())); + else + mpFrame->GetWindow()->GrabFocusToDocument(); + mbReturnFocusToDocument = false; +} + +gboolean GtkSalMenu::SignalKey(GdkEventKey const * pEvent) +{ + if (pEvent->keyval == GDK_KEY_F6) + { + mbReturnFocusToDocument = false; + gtk_menu_shell_cancel(GTK_MENU_SHELL(mpMenuBarWidget)); + //because we return false here, the keypress will continue + //to propagate and in the case that vcl focus is in + //the vcl menubar then that will also process F6 and move + //to the next pane + } + return false; +} + +//The GtkSalMenu is owner by a Vcl Menu/MenuBar. In the menubar +//case the vcl menubar is present and "visible", but with a 0 height +//so it not apparent. Normally it acts as though it is not there when +//a Native menubar is active. If we return true here, then for keyboard +//activation and traversal with F6 through panes then the vcl menubar +//acts as though it *is* present and we translate its take focus and F6 +//traversal key events into the gtk menubar equivalents. +bool GtkSalMenu::CanGetFocus() const +{ + return mpMenuBarWidget != nullptr; +} + +bool GtkSalMenu::TakeFocus() +{ + if (!mpMenuBarWidget) + return false; + + //Send a keyboard event to the gtk menubar to let it know it has been + //activated via the keyboard. Doesn't do anything except cause the gtk + //menubar "keyboard_mode" member to get set to true, so typically mnemonics + //are shown which will serve as indication that the menubar has focus + //(given that we want to show it with no menus popped down) + GdkEvent *event = GtkSalFrame::makeFakeKeyPress(mpMenuBarWidget); + gtk_widget_event(mpMenuBarWidget, event); + gdk_event_free(event); + + //this pairing results in a menubar with keyboard focus with no menus + //auto-popped down + gtk_grab_add(mpMenuBarWidget); + mbAddedGrab = true; + gtk_menu_shell_select_first(GTK_MENU_SHELL(mpMenuBarWidget), false); + gtk_menu_shell_deselect(GTK_MENU_SHELL(mpMenuBarWidget)); + mbReturnFocusToDocument = true; + return true; +} + +static void MenuBarReturnFocus(GtkMenuShell*, gpointer menu) +{ + GtkSalFrame::UpdateLastInputEventTime(gtk_get_current_event_time()); + GtkSalMenu* pMenu = static_cast<GtkSalMenu*>(menu); + pMenu->ReturnFocus(); +} + +static gboolean MenuBarSignalKey(GtkWidget*, GdkEventKey* pEvent, gpointer menu) +{ + GtkSalMenu* pMenu = static_cast<GtkSalMenu*>(menu); + return pMenu->SignalKey(pEvent); +} + +void GtkSalMenu::CreateMenuBarWidget() +{ + if (mpMenuBarContainerWidget) + return; + + GtkGrid* pGrid = mpFrame->getTopLevelGridWidget(); + mpMenuBarContainerWidget = gtk_grid_new(); + + gtk_widget_set_hexpand(GTK_WIDGET(mpMenuBarContainerWidget), true); + gtk_grid_insert_row(pGrid, 0); + gtk_grid_attach(pGrid, mpMenuBarContainerWidget, 0, 0, 1, 1); + + mpMenuAllowShrinkWidget = gtk_scrolled_window_new(nullptr, nullptr); + // tdf#129634 don't allow this scrolled window as a candidate to tab into + gtk_widget_set_can_focus(GTK_WIDGET(mpMenuAllowShrinkWidget), false); + gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(mpMenuAllowShrinkWidget), GTK_SHADOW_NONE); + // tdf#116290 external policy on scrolledwindow will not show a scrollbar, + // but still allow scrolled window to not be sized to the child content. + // So the menubar can be shrunk past its nominal smallest width. + // Unlike a hack using GtkFixed/GtkLayout the correct placement of the menubar occurs under RTL + gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(mpMenuAllowShrinkWidget), GTK_POLICY_EXTERNAL, GTK_POLICY_NEVER); + gtk_grid_attach(GTK_GRID(mpMenuBarContainerWidget), mpMenuAllowShrinkWidget, 0, 0, 1, 1); + + mpMenuBarWidget = gtk_menu_bar_new_from_model(mpMenuModel); + + gtk_widget_insert_action_group(mpMenuBarWidget, "win", mpActionGroup); + gtk_widget_set_hexpand(GTK_WIDGET(mpMenuBarWidget), true); + gtk_widget_set_hexpand(mpMenuAllowShrinkWidget, true); + gtk_container_add(GTK_CONTAINER(mpMenuAllowShrinkWidget), mpMenuBarWidget); + + g_signal_connect(G_OBJECT(mpMenuBarWidget), "deactivate", G_CALLBACK(MenuBarReturnFocus), this); + g_signal_connect(G_OBJECT(mpMenuBarWidget), "key-press-event", G_CALLBACK(MenuBarSignalKey), this); + + gtk_widget_show_all(mpMenuBarContainerWidget); + + ShowCloseButton( static_cast<MenuBar*>(mpVCLMenu.get())->HasCloseButton() ); + + ApplyPersona(); +} + +void GtkSalMenu::ApplyPersona() +{ + if (!mpMenuBarContainerWidget) + return; + assert(mbMenuBar); + // I'm dubious about the persona theming feature, but as it exists, lets try and support + // it, apply the image to the mpMenuBarContainerWidget + const BitmapEx& rPersonaBitmap = Application::GetSettings().GetStyleSettings().GetPersonaHeader(); + + GtkStyleContext *pMenuBarContainerContext = gtk_widget_get_style_context(GTK_WIDGET(mpMenuBarContainerWidget)); + if (mpMenuBarContainerProvider) + { + gtk_style_context_remove_provider(pMenuBarContainerContext, GTK_STYLE_PROVIDER(mpMenuBarContainerProvider)); + mpMenuBarContainerProvider = nullptr; + } + GtkStyleContext *pMenuBarContext = gtk_widget_get_style_context(GTK_WIDGET(mpMenuBarWidget)); + if (mpMenuBarProvider) + { + gtk_style_context_remove_provider(pMenuBarContext, GTK_STYLE_PROVIDER(mpMenuBarProvider)); + mpMenuBarProvider = nullptr; + } + + if (!rPersonaBitmap.IsEmpty()) + { + if (maPersonaBitmap != rPersonaBitmap) + { + vcl::PNGWriter aPNGWriter(rPersonaBitmap); + mxPersonaImage.reset(new utl::TempFile); + mxPersonaImage->EnableKillingFile(true); + SvStream* pStream = mxPersonaImage->GetStream(StreamMode::WRITE); + aPNGWriter.Write(*pStream); + mxPersonaImage->CloseStream(); + } + + mpMenuBarContainerProvider = gtk_css_provider_new(); + OUString aBuffer = "* { background-image: url(\"" + mxPersonaImage->GetURL() + "\"); background-position: top right; }"; + OString aResult = OUStringToOString(aBuffer, RTL_TEXTENCODING_UTF8); + gtk_css_provider_load_from_data(mpMenuBarContainerProvider, aResult.getStr(), aResult.getLength(), nullptr); + gtk_style_context_add_provider(pMenuBarContainerContext, GTK_STYLE_PROVIDER(mpMenuBarContainerProvider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + + + // force the menubar to be transparent when persona is active otherwise for + // me the menubar becomes gray when its in the backdrop + mpMenuBarProvider = gtk_css_provider_new(); + static const gchar data[] = "* { " + "background-image: none;" + "background-color: transparent;" + "}"; + gtk_css_provider_load_from_data(mpMenuBarProvider, data, -1, nullptr); + gtk_style_context_add_provider(pMenuBarContext, + GTK_STYLE_PROVIDER(mpMenuBarProvider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + } + maPersonaBitmap = rPersonaBitmap; +} + +void GtkSalMenu::DestroyMenuBarWidget() +{ + if (mpMenuBarContainerWidget) + { + gtk_widget_destroy(mpMenuBarContainerWidget); + mpMenuBarContainerWidget = nullptr; + mpCloseButton = nullptr; + } +} + +void GtkSalMenu::SetFrame(const SalFrame* pFrame) +{ + SolarMutexGuard aGuard; + assert(mbMenuBar); + SAL_INFO("vcl.unity", "GtkSalMenu set to frame"); + mpFrame = const_cast<GtkSalFrame*>(static_cast<const GtkSalFrame*>(pFrame)); + + // if we had a menu on the GtkSalMenu we have to free it as we generate a + // full menu anyway and we might need to reuse an existing model and + // actiongroup + mpFrame->SetMenu( this ); + mpFrame->EnsureAppMenuWatch(); + + // Clean menu model and action group if needed. + GtkWidget* pWidget = mpFrame->getWindow(); + GdkWindow* gdkWindow = gtk_widget_get_window( pWidget ); + + GLOMenu* pMenuModel = G_LO_MENU( g_object_get_data( G_OBJECT( gdkWindow ), "g-lo-menubar" ) ); + GLOActionGroup* pActionGroup = G_LO_ACTION_GROUP( g_object_get_data( G_OBJECT( gdkWindow ), "g-lo-action-group" ) ); + SAL_INFO("vcl.unity", "Found menu model: " << pMenuModel << " and action group: " << pActionGroup); + + if ( pMenuModel ) + { + if ( g_menu_model_get_n_items( G_MENU_MODEL( pMenuModel ) ) > 0 ) + g_lo_menu_remove( pMenuModel, 0 ); + + mpMenuModel = G_MENU_MODEL( g_lo_menu_new() ); + } + + if ( pActionGroup ) + { + g_lo_action_group_clear( pActionGroup ); + mpActionGroup = G_ACTION_GROUP( pActionGroup ); + } + + // Generate the main menu structure. + if ( PrepUpdate() ) + UpdateFull(); + + g_lo_menu_insert_section( pMenuModel, 0, nullptr, mpMenuModel ); + + if (!bUnityMode && static_cast<MenuBar*>(mpVCLMenu.get())->IsDisplayable()) + { + DestroyMenuBarWidget(); + CreateMenuBarWidget(); + } +} + +const GtkSalFrame* GtkSalMenu::GetFrame() const +{ + SolarMutexGuard aGuard; + const GtkSalMenu* pMenu = this; + while( pMenu && ! pMenu->mpFrame ) + pMenu = pMenu->mpParentSalMenu; + return pMenu ? pMenu->mpFrame : nullptr; +} + +void GtkSalMenu::NativeCheckItem( unsigned nSection, unsigned nItemPos, MenuItemBits bits, gboolean bCheck ) +{ + SolarMutexGuard aGuard; + + if ( mpActionGroup == nullptr ) + return; + + gchar* aCommand = g_lo_menu_get_command_from_item_in_section( G_LO_MENU( mpMenuModel ), nSection, nItemPos ); + + if ( aCommand != nullptr || g_strcmp0( aCommand, "" ) != 0 ) + { + GVariant *pCheckValue = nullptr; + GVariant *pCurrentState = g_action_group_get_action_state( mpActionGroup, aCommand ); + + if ( bits & MenuItemBits::RADIOCHECK ) + pCheckValue = bCheck ? g_variant_new_string( aCommand ) : g_variant_new_string( "" ); + else + { + // By default, all checked items are checkmark buttons. + if (bCheck || pCurrentState != nullptr) + pCheckValue = g_variant_new_boolean( bCheck ); + } + + if ( pCheckValue != nullptr ) + { + if ( pCurrentState == nullptr || g_variant_equal( pCurrentState, pCheckValue ) == FALSE ) + { + g_action_group_change_action_state( mpActionGroup, aCommand, pCheckValue ); + } + else + { + g_variant_unref (pCheckValue); + } + } + + if ( pCurrentState != nullptr ) + g_variant_unref( pCurrentState ); + } + + if ( aCommand ) + g_free( aCommand ); +} + +void GtkSalMenu::NativeSetEnableItem( gchar const * aCommand, gboolean bEnable ) +{ + SolarMutexGuard aGuard; + GLOActionGroup* pActionGroup = G_LO_ACTION_GROUP( mpActionGroup ); + + if ( g_action_group_get_action_enabled( G_ACTION_GROUP( pActionGroup ), aCommand ) != bEnable ) + g_lo_action_group_set_action_enabled( pActionGroup, aCommand, bEnable ); +} + +void GtkSalMenu::NativeSetItemText( unsigned nSection, unsigned nItemPos, const OUString& rText ) +{ + SolarMutexGuard aGuard; + // Escape all underscores so that they don't get interpreted as hotkeys + OUString aText = rText.replaceAll( "_", "__" ); + // Replace the LibreOffice hotkey identifier with an underscore + aText = aText.replace( '~', '_' ); + OString aConvertedText = OUStringToOString( aText, RTL_TEXTENCODING_UTF8 ); + + // Update item text only when necessary. + gchar* aLabel = g_lo_menu_get_label_from_item_in_section( G_LO_MENU( mpMenuModel ), nSection, nItemPos ); + + if ( aLabel == nullptr || g_strcmp0( aLabel, aConvertedText.getStr() ) != 0 ) + g_lo_menu_set_label_to_item_in_section( G_LO_MENU( mpMenuModel ), nSection, nItemPos, aConvertedText.getStr() ); + + if ( aLabel ) + g_free( aLabel ); +} + +namespace +{ + void DestroyMemoryStream(gpointer data) + { + SvMemoryStream* pMemStm = static_cast<SvMemoryStream*>(data); + delete pMemStm; + } +} + +void GtkSalMenu::NativeSetItemIcon( unsigned nSection, unsigned nItemPos, const Image& rImage ) +{ +#if GLIB_CHECK_VERSION(2,38,0) + if (!rImage && mbHasNullItemIcon) + return; + + SolarMutexGuard aGuard; + + if (!!rImage) + { + SvMemoryStream* pMemStm = new SvMemoryStream; + vcl::PNGWriter aWriter(rImage.GetBitmapEx()); + aWriter.Write(*pMemStm); + + GBytes *pBytes = g_bytes_new_with_free_func(pMemStm->GetData(), + pMemStm->TellEnd(), + DestroyMemoryStream, + pMemStm); + + GIcon *pIcon = g_bytes_icon_new(pBytes); + + g_lo_menu_set_icon_to_item_in_section( G_LO_MENU( mpMenuModel ), nSection, nItemPos, pIcon ); + g_object_unref(pIcon); + g_bytes_unref(pBytes); + mbHasNullItemIcon = false; + } + else + { + g_lo_menu_set_icon_to_item_in_section( G_LO_MENU( mpMenuModel ), nSection, nItemPos, nullptr ); + mbHasNullItemIcon = true; + } +#else + (void)nSection; + (void)nItemPos; + (void)rImage; +#endif +} + +void GtkSalMenu::NativeSetAccelerator( unsigned nSection, unsigned nItemPos, const vcl::KeyCode& rKeyCode, const OUString& rKeyName ) +{ + SolarMutexGuard aGuard; + + if ( rKeyName.isEmpty() ) + return; + + guint nKeyCode; + GdkModifierType nModifiers; + GtkSalFrame::KeyCodeToGdkKey(rKeyCode, &nKeyCode, &nModifiers); + + gchar* aAccelerator = gtk_accelerator_name( nKeyCode, nModifiers ); + + gchar* aCurrentAccel = g_lo_menu_get_accelerator_from_item_in_section( G_LO_MENU( mpMenuModel ), nSection, nItemPos ); + + if ( aCurrentAccel == nullptr && g_strcmp0( aCurrentAccel, aAccelerator ) != 0 ) + g_lo_menu_set_accelerator_to_item_in_section ( G_LO_MENU( mpMenuModel ), nSection, nItemPos, aAccelerator ); + + g_free( aAccelerator ); + g_free( aCurrentAccel ); +} + +bool GtkSalMenu::NativeSetItemCommand( unsigned nSection, + unsigned nItemPos, + sal_uInt16 nId, + const gchar* aCommand, + MenuItemBits nBits, + bool bChecked, + bool bIsSubmenu ) +{ + bool bSubMenuAddedOrRemoved = false; + + SolarMutexGuard aGuard; + GLOActionGroup* pActionGroup = G_LO_ACTION_GROUP( mpActionGroup ); + + GVariant *pTarget = nullptr; + + if (g_action_group_has_action(mpActionGroup, aCommand)) + g_lo_action_group_remove(pActionGroup, aCommand); + + if ( ( nBits & MenuItemBits::CHECKABLE ) || bIsSubmenu ) + { + // Item is a checkmark button. + GVariantType* pStateType = g_variant_type_new( reinterpret_cast<gchar const *>(G_VARIANT_TYPE_BOOLEAN) ); + GVariant* pState = g_variant_new_boolean( bChecked ); + + g_lo_action_group_insert_stateful( pActionGroup, aCommand, nId, bIsSubmenu, nullptr, pStateType, nullptr, pState ); + } + else if ( nBits & MenuItemBits::RADIOCHECK ) + { + // Item is a radio button. + GVariantType* pParameterType = g_variant_type_new( reinterpret_cast<gchar const *>(G_VARIANT_TYPE_STRING) ); + GVariantType* pStateType = g_variant_type_new( reinterpret_cast<gchar const *>(G_VARIANT_TYPE_STRING) ); + GVariant* pState = g_variant_new_string( "" ); + pTarget = g_variant_new_string( aCommand ); + + g_lo_action_group_insert_stateful( pActionGroup, aCommand, nId, FALSE, pParameterType, pStateType, nullptr, pState ); + } + else + { + // Item is not special, so insert a stateless action. + g_lo_action_group_insert( pActionGroup, aCommand, nId, FALSE ); + } + + GLOMenu* pMenu = G_LO_MENU( mpMenuModel ); + + // Menu item is not updated unless it's necessary. + gchar* aCurrentCommand = g_lo_menu_get_command_from_item_in_section( pMenu, nSection, nItemPos ); + + if ( aCurrentCommand == nullptr || g_strcmp0( aCurrentCommand, aCommand ) != 0 ) + { + bool bOldHasSubmenu = g_lo_menu_get_submenu_from_item_in_section(pMenu, nSection, nItemPos) != nullptr; + bSubMenuAddedOrRemoved = bOldHasSubmenu != bIsSubmenu; + if (bSubMenuAddedOrRemoved) + { + //tdf#98636 it's not good enough to unset the "submenu-action" attribute to change something + //from a submenu to a non-submenu item, so remove the old one entirely and re-add it to + //support achieving that + gchar* pLabel = g_lo_menu_get_label_from_item_in_section(pMenu, nSection, nItemPos); + g_lo_menu_remove_from_section(pMenu, nSection, nItemPos); + g_lo_menu_insert_in_section(pMenu, nSection, nItemPos, pLabel); + g_free(pLabel); + } + + g_lo_menu_set_command_to_item_in_section( pMenu, nSection, nItemPos, aCommand ); + + gchar* aItemCommand = g_strconcat("win.", aCommand, nullptr ); + + if ( bIsSubmenu ) + g_lo_menu_set_submenu_action_to_item_in_section( pMenu, nSection, nItemPos, aItemCommand ); + else + { + g_lo_menu_set_action_and_target_value_to_item_in_section( pMenu, nSection, nItemPos, aItemCommand, pTarget ); + pTarget = nullptr; + } + + g_free( aItemCommand ); + } + + if ( aCurrentCommand ) + g_free( aCurrentCommand ); + + if (pTarget) + g_variant_unref(pTarget); + + return bSubMenuAddedOrRemoved; +} + +GtkSalMenu* GtkSalMenu::GetTopLevel() +{ + GtkSalMenu *pMenu = this; + while (pMenu->mpParentSalMenu) + pMenu = pMenu->mpParentSalMenu; + return pMenu; +} + +void GtkSalMenu::DispatchCommand(const gchar *pCommand) +{ + SolarMutexGuard aGuard; + MenuAndId aMenuAndId = decode_command(pCommand); + GtkSalMenu* pSalSubMenu = aMenuAndId.first; + GtkSalMenu* pTopLevel = pSalSubMenu->GetTopLevel(); + if (pTopLevel->mpMenuBarWidget) + { + // tdf#125803 spacebar will toggle radios and checkbuttons without automatically + // closing the menu. To handle this properly I imagine we need to set groups for the + // radiobuttons so the others visually untoggle when the active one is toggled and + // we would further need to teach vcl that the state can change more than once. + // + // or we could unconditionally deactivate the menus if regardless of what particular + // type of menu item got activated + gtk_menu_shell_deactivate(GTK_MENU_SHELL(pTopLevel->mpMenuBarWidget)); + } + pTopLevel->GetMenu()->HandleMenuCommandEvent(pSalSubMenu->GetMenu(), aMenuAndId.second); +} + +void GtkSalMenu::ActivateAllSubmenus(Menu* pMenuBar) +{ + for (GtkSalMenuItem* pSalItem : maItems) + { + if ( pSalItem->mpSubMenu != nullptr ) + { + // We can re-enter this method via the new event loop that gets created + // in GtkClipboardTransferable::getTransferDataFlavorsAsVector, so use the InActivateCallback + // flag to detect that and skip some startup work. + if (!pSalItem->mpSubMenu->mbInActivateCallback) + { + pSalItem->mpSubMenu->mbInActivateCallback = true; + pMenuBar->HandleMenuActivateEvent(pSalItem->mpSubMenu->GetMenu()); + pSalItem->mpSubMenu->mbInActivateCallback = false; + pSalItem->mpSubMenu->ActivateAllSubmenus(pMenuBar); + pSalItem->mpSubMenu->Update(); + pMenuBar->HandleMenuDeActivateEvent(pSalItem->mpSubMenu->GetMenu()); + } + } + } +} + +void GtkSalMenu::ClearActionGroupAndMenuModel() +{ + SetMenuModel(nullptr); + mpActionGroup = nullptr; + for (GtkSalMenuItem* pSalItem : maItems) + { + if ( pSalItem->mpSubMenu != nullptr ) + { + pSalItem->mpSubMenu->ClearActionGroupAndMenuModel(); + } + } +} + +void GtkSalMenu::Activate(const gchar* pCommand) +{ + MenuAndId aMenuAndId = decode_command(pCommand); + GtkSalMenu* pSalMenu = aMenuAndId.first; + GtkSalMenu* pTopLevel = pSalMenu->GetTopLevel(); + Menu* pVclMenu = pSalMenu->GetMenu(); + Menu* pVclSubMenu = pVclMenu->GetPopupMenu(aMenuAndId.second); + GtkSalMenu* pSubMenu = pSalMenu->GetItemAtPos(pVclMenu->GetItemPos(aMenuAndId.second))->mpSubMenu; + + pSubMenu->mbInActivateCallback = true; + pTopLevel->GetMenu()->HandleMenuActivateEvent(pVclSubMenu); + pSubMenu->mbInActivateCallback = false; + pVclSubMenu->UpdateNativeMenu(); +} + +void GtkSalMenu::Deactivate(const gchar* pCommand) +{ + MenuAndId aMenuAndId = decode_command(pCommand); + GtkSalMenu* pSalMenu = aMenuAndId.first; + GtkSalMenu* pTopLevel = pSalMenu->GetTopLevel(); + Menu* pVclMenu = pSalMenu->GetMenu(); + Menu* pVclSubMenu = pVclMenu->GetPopupMenu(aMenuAndId.second); + pTopLevel->GetMenu()->HandleMenuDeActivateEvent(pVclSubMenu); +} + +void GtkSalMenu::EnableUnity(bool bEnable) +{ + bUnityMode = bEnable; + + MenuBar* pMenuBar(static_cast<MenuBar*>(mpVCLMenu.get())); + bool bDisplayable(pMenuBar->IsDisplayable()); + + if (bEnable) + { + DestroyMenuBarWidget(); + UpdateFull(); + if (!bDisplayable) + ShowMenuBar(false); + } + else + { + Update(); + ShowMenuBar(bDisplayable); + } + + pMenuBar->LayoutChanged(); +} + +void GtkSalMenu::ShowMenuBar( bool bVisible ) +{ + // Unity tdf#106271: Can't hide global menu, so empty it instead when user wants to hide menubar, + if (bUnityMode) + { + if (bVisible) + Update(); + else if (mpMenuModel && g_menu_model_get_n_items(G_MENU_MODEL(mpMenuModel)) > 0) + g_lo_menu_remove(G_LO_MENU(mpMenuModel), 0); + } + else if (bVisible) + CreateMenuBarWidget(); + else + DestroyMenuBarWidget(); +} + +bool GtkSalMenu::IsItemVisible( unsigned nPos ) +{ + SolarMutexGuard aGuard; + bool bVisible = false; + + if ( nPos < maItems.size() ) + bVisible = maItems[ nPos ]->mbVisible; + + return bVisible; +} + +void GtkSalMenu::CheckItem( unsigned, bool ) +{ +} + +void GtkSalMenu::EnableItem( unsigned nPos, bool bEnable ) +{ + SolarMutexGuard aGuard; + if ( bUnityMode && !mbInActivateCallback && !mbNeedsUpdate && GetTopLevel()->mbMenuBar && ( nPos < maItems.size() ) ) + { + gchar* pCommand = GetCommandForItem( GetItemAtPos( nPos ) ); + NativeSetEnableItem( pCommand, bEnable ); + g_free( pCommand ); + } +} + +void GtkSalMenu::ShowItem( unsigned nPos, bool bShow ) +{ + SolarMutexGuard aGuard; + if ( nPos < maItems.size() ) + { + maItems[ nPos ]->mbVisible = bShow; + if ( bUnityMode && !mbInActivateCallback && !mbNeedsUpdate && GetTopLevel()->mbMenuBar ) + Update(); + } +} + +void GtkSalMenu::SetItemText( unsigned nPos, SalMenuItem* pSalMenuItem, const OUString& rText ) +{ + SolarMutexGuard aGuard; + if ( bUnityMode && !mbInActivateCallback && !mbNeedsUpdate && GetTopLevel()->mbMenuBar && ( nPos < maItems.size() ) ) + { + gchar* pCommand = GetCommandForItem( static_cast< GtkSalMenuItem* >( pSalMenuItem ) ); + + gint nSectionsCount = g_menu_model_get_n_items( mpMenuModel ); + for ( gint nSection = 0; nSection < nSectionsCount; ++nSection ) + { + gint nItemsCount = g_lo_menu_get_n_items_from_section( G_LO_MENU( mpMenuModel ), nSection ); + for ( gint nItem = 0; nItem < nItemsCount; ++nItem ) + { + gchar* pCommandFromModel = g_lo_menu_get_command_from_item_in_section( G_LO_MENU( mpMenuModel ), nSection, nItem ); + + if ( !g_strcmp0( pCommandFromModel, pCommand ) ) + { + NativeSetItemText( nSection, nItem, rText ); + g_free( pCommandFromModel ); + g_free( pCommand ); + return; + } + + g_free( pCommandFromModel ); + } + } + + g_free( pCommand ); + } +} + +void GtkSalMenu::SetItemImage( unsigned, SalMenuItem*, const Image& ) +{ +} + +void GtkSalMenu::SetAccelerator( unsigned, SalMenuItem*, const vcl::KeyCode&, const OUString& ) +{ +} + +void GtkSalMenu::GetSystemMenuData( SystemMenuData* ) +{ +} + +int GtkSalMenu::GetMenuBarHeight() const +{ + return mpMenuBarWidget ? gtk_widget_get_allocated_height(mpMenuBarWidget) : 0; +} + +/* + * GtkSalMenuItem + */ + +GtkSalMenuItem::GtkSalMenuItem( const SalItemParams* pItemData ) : + mpParentMenu( nullptr ), + mpSubMenu( nullptr ), + mnType( pItemData->eType ), + mnId( pItemData->nId ), + mbVisible( true ) +{ +} + +GtkSalMenuItem::~GtkSalMenuItem() +{ +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/gtk3gtksys.cxx b/vcl/unx/gtk3/gtk3gtksys.cxx new file mode 100644 index 000000000..379c87343 --- /dev/null +++ b/vcl/unx/gtk3/gtk3gtksys.cxx @@ -0,0 +1,275 @@ +/* -*- 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <gtk/gtk.h> +#include <unx/gtk/gtkinst.hxx> +#include <unx/gtk/gtksys.hxx> +#include <unx/gtk/gtkbackend.hxx> +#include <osl/module.h> + +GtkSalSystem *GtkSalSystem::GetSingleton() +{ + static GtkSalSystem *pSingleton = new GtkSalSystem(); + return pSingleton; +} + +SalSystem *GtkInstance::CreateSalSystem() +{ + return GtkSalSystem::GetSingleton(); +} + +GtkSalSystem::GtkSalSystem() : SalGenericSystem() +{ + mpDisplay = gdk_display_get_default(); + countScreenMonitors(); + // rhbz#1285356, native look will be gtk2, which crashes + // when gtk3 is already loaded. Until there is a solution + // java-side force look and feel to something that doesn't + // crash when we are using gtk3 + setenv("STOC_FORCE_SYSTEM_LAF", "true", 1); +} + +GtkSalSystem::~GtkSalSystem() +{ +} + +namespace +{ + +struct GdkRectangleCoincidentLess +{ + // fdo#78799 - detect and elide overlaying monitors of different sizes + bool operator()(GdkRectangle const& rLeft, GdkRectangle const& rRight) + { + return + rLeft.x < rRight.x + || rLeft.y < rRight.y + ; + } +}; +struct GdkRectangleCoincident +{ + // fdo#78799 - detect and elide overlaying monitors of different sizes + bool operator()(GdkRectangle const& rLeft, GdkRectangle const& rRight) + { + return + rLeft.x == rRight.x + && rLeft.y == rRight.y + ; + } +}; + +} + +/** + * GtkSalSystem::countScreenMonitors() + * + * This method builds the vector which allows us to map from VCL's + * idea of linear integer ScreenNumber to gtk+'s rather more + * complicated screen + monitor concept. + */ +void +GtkSalSystem::countScreenMonitors() +{ + maScreenMonitors.clear(); + for (gint i = 0; i < gdk_display_get_n_screens(mpDisplay); i++) + { + GdkScreen* const pScreen(gdk_display_get_screen(mpDisplay, i)); + gint nMonitors(pScreen ? gdk_screen_get_n_monitors(pScreen) : 0); + if (nMonitors > 1) + { + std::vector<GdkRectangle> aGeometries; + aGeometries.reserve(nMonitors); + for (gint j(0); j != nMonitors; ++j) + { + GdkRectangle aGeometry; + gdk_screen_get_monitor_geometry(pScreen, j, &aGeometry); + aGeometries.push_back(aGeometry); + } + std::sort(aGeometries.begin(), aGeometries.end(), + GdkRectangleCoincidentLess()); + const std::vector<GdkRectangle>::iterator aUniqueEnd( + std::unique(aGeometries.begin(), aGeometries.end(), + GdkRectangleCoincident())); + nMonitors = std::distance(aGeometries.begin(), aUniqueEnd); + } + maScreenMonitors.emplace_back(pScreen, nMonitors); + } +} + +SalX11Screen +GtkSalSystem::getXScreenFromDisplayScreen(unsigned int nScreen) +{ + gint nMonitor; + + GdkScreen *pScreen = getScreenMonitorFromIdx (nScreen, nMonitor); + if (!pScreen) + return SalX11Screen (0); + if (!DLSYM_GDK_IS_X11_DISPLAY(mpDisplay)) + return SalX11Screen (0); + return SalX11Screen (gdk_x11_screen_get_screen_number (pScreen)); +} + +GdkScreen * +GtkSalSystem::getScreenMonitorFromIdx (int nIdx, gint &nMonitor) +{ + GdkScreen *pScreen = nullptr; + for (auto const& screenMonitor : maScreenMonitors) + { + pScreen = screenMonitor.first; + if (!pScreen) + break; + if (nIdx >= screenMonitor.second) + nIdx -= screenMonitor.second; + else + break; + } + nMonitor = nIdx; + + // handle invalid monitor indexes as non-existent screens + if (nMonitor < 0 || (pScreen && nMonitor >= gdk_screen_get_n_monitors (pScreen))) + pScreen = nullptr; + + return pScreen; +} + +int +GtkSalSystem::getScreenIdxFromPtr (GdkScreen *pScreen) +{ + int nIdx = 0; + for (auto const& screenMonitor : maScreenMonitors) + { + if (screenMonitor.first == pScreen) + return nIdx; + nIdx += screenMonitor.second; + } + g_warning ("failed to find screen %p", pScreen); + return 0; +} + +int GtkSalSystem::getScreenMonitorIdx (GdkScreen *pScreen, + int nX, int nY) +{ + // TODO: this will fail horribly for exotic combinations like two + // monitors in mirror mode and one extra. Hopefully such + // abominations are not used (or, even better, not possible) in + // practice .-) + return getScreenIdxFromPtr (pScreen) + + gdk_screen_get_monitor_at_point (pScreen, nX, nY); +} + +unsigned int GtkSalSystem::GetDisplayScreenCount() +{ + gint nMonitor; + (void)getScreenMonitorFromIdx (G_MAXINT, nMonitor); + return G_MAXINT - nMonitor; +} + +bool GtkSalSystem::IsUnifiedDisplay() +{ + return gdk_display_get_n_screens (mpDisplay) == 1; +} + +namespace { +int _fallback_get_primary_monitor (GdkScreen *pScreen) +{ + // Use monitor name as primacy heuristic + int max = gdk_screen_get_n_monitors (pScreen); + for (int i = 0; i < max; ++i) + { + char *name = gdk_screen_get_monitor_plug_name (pScreen, i); + bool bLaptop = (name && !g_ascii_strncasecmp (name, "LVDS", 4)); + g_free (name); + if (bLaptop) + return i; + } + return 0; +} + +int _get_primary_monitor (GdkScreen *pScreen) +{ + static int (*get_fn) (GdkScreen *) = nullptr; + get_fn = gdk_screen_get_primary_monitor; + // Perhaps we have a newer gtk+ with this symbol: + if (!get_fn) + { + get_fn = reinterpret_cast<int(*)(GdkScreen*)>(osl_getAsciiFunctionSymbol(nullptr, + "gdk_screen_get_primary_monitor")); + } + if (!get_fn) + get_fn = _fallback_get_primary_monitor; + if (get_fn) + return get_fn (pScreen); + else + return 0; +} +} // end anonymous namespace + +unsigned int GtkSalSystem::GetDisplayBuiltInScreen() +{ + GdkScreen *pDefault = gdk_display_get_default_screen (mpDisplay); + int idx = getScreenIdxFromPtr (pDefault); + return idx + _get_primary_monitor (pDefault); +} + +tools::Rectangle GtkSalSystem::GetDisplayScreenPosSizePixel (unsigned int nScreen) +{ + gint nMonitor; + GdkScreen *pScreen; + GdkRectangle aRect; + pScreen = getScreenMonitorFromIdx (nScreen, nMonitor); + if (!pScreen) + return tools::Rectangle(); + gdk_screen_get_monitor_geometry (pScreen, nMonitor, &aRect); + return tools::Rectangle (Point(aRect.x, aRect.y), Size(aRect.width, aRect.height)); +} + +// convert ~ to indicate mnemonic to '_' +static OString MapToGtkAccelerator(const OUString &rStr) +{ + return OUStringToOString(rStr.replaceFirst("~", "_"), RTL_TEXTENCODING_UTF8); +} + +int GtkSalSystem::ShowNativeDialog (const OUString& rTitle, const OUString& rMessage, + const std::vector< OUString >& rButtonNames) +{ + OString aTitle (OUStringToOString (rTitle, RTL_TEXTENCODING_UTF8)); + OString aMessage (OUStringToOString (rMessage, RTL_TEXTENCODING_UTF8)); + + GtkDialog *pDialog = GTK_DIALOG ( + g_object_new (GTK_TYPE_MESSAGE_DIALOG, + "title", aTitle.getStr(), + "message-type", int(GTK_MESSAGE_WARNING), + "text", aMessage.getStr(), + nullptr)); + int nButton = 0; + for (auto const& buttonName : rButtonNames) + gtk_dialog_add_button (pDialog, MapToGtkAccelerator(buttonName).getStr(), nButton++); + gtk_dialog_set_default_response (pDialog, 0/*nDefaultButton*/); + + nButton = gtk_dialog_run (pDialog); + if (nButton < 0) + nButton = -1; + + gtk_widget_destroy (GTK_WIDGET (pDialog)); + + return nButton; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/gtk3hudawareness.cxx b/vcl/unx/gtk3/gtk3hudawareness.cxx new file mode 100644 index 000000000..dddad28da --- /dev/null +++ b/vcl/unx/gtk3/gtk3hudawareness.cxx @@ -0,0 +1,110 @@ +/* -*- 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 <string.h> + +#include <unx/gtk/hudawareness.h> + +namespace { + +struct HudAwarenessHandle +{ + GDBusConnection *connection; + HudAwarenessCallback callback; + gpointer user_data; + GDestroyNotify notify; +}; + +} + +static void +hud_awareness_method_call (GDBusConnection * /* connection */, + const gchar * /* sender */, + const gchar * /* object_path */, + const gchar * /* interface_name */, + const gchar *method_name, + GVariant *parameters, + GDBusMethodInvocation *invocation, + gpointer user_data) +{ + HudAwarenessHandle *handle = static_cast<HudAwarenessHandle*>(user_data); + + if (g_str_equal (method_name, "HudActiveChanged")) + { + gboolean active; + + g_variant_get (parameters, "(b)", &active); + + (* handle->callback) (active, handle->user_data); + } + + g_dbus_method_invocation_return_value (invocation, nullptr); +} + +guint +hud_awareness_register (GDBusConnection *connection, + const gchar *object_path, + HudAwarenessCallback callback, + gpointer user_data, + GDestroyNotify notify, + GError **error) +{ + static GDBusInterfaceInfo *iface; + static GDBusNodeInfo *info; + GDBusInterfaceVTable vtable; + HudAwarenessHandle *handle; + guint object_id; + + memset (static_cast<void *>(&vtable), 0, sizeof (vtable)); + vtable.method_call = hud_awareness_method_call; + + if G_UNLIKELY (iface == nullptr) + { + GError *local_error = nullptr; + + info = g_dbus_node_info_new_for_xml ("<node>" + "<interface name='com.canonical.hud.Awareness'>" + "<method name='CheckAwareness'/>" + "<method name='HudActiveChanged'>" + "<arg type='b'/>" + "</method>" + "</interface>" + "</node>", + &local_error); + g_assert_no_error (local_error); + iface = g_dbus_node_info_lookup_interface (info, "com.canonical.hud.Awareness"); + g_assert (iface != nullptr); + } + + handle = static_cast<HudAwarenessHandle*>(g_malloc (sizeof (HudAwarenessHandle))); + + object_id = g_dbus_connection_register_object (connection, object_path, iface, &vtable, handle, &g_free, error); + + if (object_id == 0) + { + g_free (handle); + return 0; + } + + handle->connection = static_cast<GDBusConnection*>(g_object_ref (connection)); + handle->callback = callback; + handle->user_data = user_data; + handle->notify = notify; + + return object_id; +} + +void +hud_awareness_unregister (GDBusConnection *connection, + guint subscription_id) +{ + g_dbus_connection_unregister_object (connection, subscription_id); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/gtk3salnativewidgets-gtk.cxx b/vcl/unx/gtk3/gtk3salnativewidgets-gtk.cxx new file mode 100644 index 000000000..50e894f40 --- /dev/null +++ b/vcl/unx/gtk3/gtk3salnativewidgets-gtk.cxx @@ -0,0 +1,3731 @@ +/* -*- 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 <sal/config.h> +#include <sal/log.hxx> +#include <osl/module.h> + +#include <config_cairo_canvas.h> + +#include <unx/gtk/gtkframe.hxx> +#include <unx/gtk/gtkdata.hxx> +#include <unx/gtk/gtkinst.hxx> +#include <unx/gtk/gtkgdi.hxx> +#include <unx/gtk/gtkbackend.hxx> +#include <vcl/decoview.hxx> +#include <vcl/settings.hxx> +#include <unx/fontmanager.hxx> + +#include "cairo_gtk3_cairo.hxx" +#include <optional> + +GtkStyleContext* GtkSalGraphics::mpWindowStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpButtonStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpLinkButtonStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpEntryStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpTextViewStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpVScrollbarStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpVScrollbarContentsStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpVScrollbarTroughStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpVScrollbarSliderStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpVScrollbarButtonStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpHScrollbarStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpHScrollbarContentsStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpHScrollbarTroughStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpHScrollbarSliderStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpHScrollbarButtonStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpToolbarStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpToolButtonStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpToolbarSeperatorStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpCheckButtonStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpCheckButtonCheckStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpRadioButtonStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpRadioButtonRadioStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpSpinStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpSpinEntryStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpSpinUpStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpSpinDownStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpComboboxStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpComboboxBoxStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpComboboxEntryStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpComboboxButtonStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpComboboxButtonBoxStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpComboboxButtonArrowStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpListboxStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpListboxBoxStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpListboxButtonStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpListboxButtonBoxStyle= nullptr; +GtkStyleContext* GtkSalGraphics::mpListboxButtonArrowStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpFrameInStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpFrameOutStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpFixedHoriLineStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpFixedVertLineStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpTreeHeaderButtonStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpProgressBarStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpProgressBarTroughStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpProgressBarProgressStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpNotebookStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpNotebookStackStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpNotebookHeaderStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpNotebookHeaderTabsStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpNotebookHeaderTabsTabStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpNotebookHeaderTabsTabLabelStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpNotebookHeaderTabsTabActiveLabelStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpNotebookHeaderTabsTabHoverLabelStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpMenuBarStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpMenuBarItemStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpMenuWindowStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpMenuStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpMenuItemStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpMenuItemArrowStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpMenuItemLabelStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpCheckMenuItemStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpCheckMenuItemCheckStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpRadioMenuItemStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpRadioMenuItemRadioStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpSeparatorMenuItemStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpSeparatorMenuItemSeparatorStyle = nullptr; + +bool GtkSalGraphics::style_loaded = false; +/************************************************************************ + * State conversion + ************************************************************************/ +static GtkStateFlags NWConvertVCLStateToGTKState(ControlState nVCLState) +{ + GtkStateFlags nGTKState = GTK_STATE_FLAG_NORMAL; + + if (!( nVCLState & ControlState::ENABLED )) + { + nGTKState = GTK_STATE_FLAG_INSENSITIVE; + } + + if ( nVCLState & ControlState::PRESSED ) + { + nGTKState = static_cast<GtkStateFlags>(nGTKState | GTK_STATE_FLAG_ACTIVE); + } + + if ( nVCLState & ControlState::ROLLOVER ) + { + nGTKState = static_cast<GtkStateFlags>(nGTKState | GTK_STATE_FLAG_PRELIGHT); + } + + if ( nVCLState & ControlState::SELECTED ) + nGTKState = static_cast<GtkStateFlags>(nGTKState | GTK_STATE_FLAG_SELECTED); + + if ( nVCLState & ControlState::FOCUSED ) + nGTKState = static_cast<GtkStateFlags>(nGTKState | GTK_STATE_FLAG_FOCUSED); + + if (AllSettings::GetLayoutRTL()) + { + nGTKState = static_cast<GtkStateFlags>(nGTKState | GTK_STATE_FLAG_DIR_RTL); + } + else + { + nGTKState = static_cast<GtkStateFlags>(nGTKState | GTK_STATE_FLAG_DIR_LTR); + } + + return nGTKState; +} + +namespace { + +enum class RenderType { + BackgroundAndFrame = 1, + Check, + Background, + MenuSeparator, + ToolbarSeparator, + Separator, + Arrow, + Radio, + Scrollbar, + Spinbutton, + Combobox, + Expander, + Icon, + Progress, + TabItem, + Focus +}; + +} + +static void NWCalcArrowRect( const tools::Rectangle& rButton, tools::Rectangle& rArrow ) +{ + // Size the arrow appropriately + Size aSize( rButton.GetWidth()/2, rButton.GetHeight()/2 ); + rArrow.SetSize( aSize ); + + rArrow.SetPos( Point( + rButton.Left() + ( rButton.GetWidth() - rArrow.GetWidth() ) / 2, + rButton.Top() + ( rButton.GetHeight() - rArrow.GetHeight() ) / 2 + ) ); +} + +tools::Rectangle GtkSalGraphics::NWGetSpinButtonRect( ControlPart nPart, tools::Rectangle aAreaRect) +{ + gint w, h; + gtk_icon_size_lookup (GTK_ICON_SIZE_MENU, &w, &h); + gint icon_size = std::max(w, h); + + GtkBorder padding, border; + gtk_style_context_get_padding(mpSpinUpStyle, gtk_style_context_get_state(mpSpinUpStyle), &padding); + gtk_style_context_get_border(mpSpinUpStyle, gtk_style_context_get_state(mpSpinUpStyle), &border); + + gint buttonWidth = icon_size + padding.left + padding.right + + border.left + border.right; + + gint buttonHeight = icon_size + padding.top + padding.bottom + + border.top + border.bottom; + + tools::Rectangle buttonRect; + buttonRect.SetSize(Size(buttonWidth, buttonHeight)); + buttonRect.setY(aAreaRect.Top()); + buttonRect.SetBottom( buttonRect.Top() + aAreaRect.GetHeight() ); + tools::Rectangle partRect(buttonRect); + if ( nPart == ControlPart::ButtonUp ) + { + if (AllSettings::GetLayoutRTL()) + partRect.setX(aAreaRect.Left()); + else + partRect.setX(aAreaRect.Left() + (aAreaRect.GetWidth() - buttonRect.GetWidth())); + } + else if( nPart == ControlPart::ButtonDown ) + { + if (AllSettings::GetLayoutRTL()) + partRect.setX(aAreaRect.Left() + buttonRect.GetWidth()); + else + partRect.setX(aAreaRect.Left() + (aAreaRect.GetWidth() - 2 * buttonRect.GetWidth())); + } + else + { + if (AllSettings::GetLayoutRTL()) + { + partRect.SetRight( aAreaRect.Left() + aAreaRect.GetWidth() ); + partRect.SetLeft( aAreaRect.Left() + (2 * buttonRect.GetWidth()) - 1 ); + } + else + { + partRect.SetRight( (aAreaRect.Left() + (aAreaRect.GetWidth() - 2 * buttonRect.GetWidth())) - 1 ); + partRect.SetLeft( aAreaRect.Left() ); + } + partRect.SetTop( aAreaRect.Top() ); + partRect.SetBottom( aAreaRect.Bottom() ); + } + + return partRect; +} + +namespace +{ + void QuerySize(GtkStyleContext *pContext, Size &rSize) + { + GtkBorder margin, border, padding; + GtkStateFlags stateflags = gtk_style_context_get_state (pContext); + + gtk_style_context_get_margin(pContext, stateflags, &margin); + gtk_style_context_get_border(pContext, stateflags, &border); + gtk_style_context_get_padding(pContext, stateflags, &padding); + + int nMinWidth, nMinHeight; + gtk_style_context_get(pContext, stateflags, + "min-width", &nMinWidth, "min-height", &nMinHeight, nullptr); + + nMinWidth += margin.left + margin.right + border.left + border.right + padding.left + padding.right; + nMinHeight += margin.top + margin.bottom + border.top + border.bottom + padding.top + padding.bottom; + + rSize = Size(std::max<long>(rSize.Width(), nMinWidth), std::max<long>(rSize.Height(), nMinHeight)); + } +} + +tools::Rectangle GtkSalGraphics::NWGetScrollButtonRect( ControlPart nPart, tools::Rectangle aAreaRect ) +{ + tools::Rectangle buttonRect; + + gboolean has_forward; + gboolean has_forward2; + gboolean has_backward; + gboolean has_backward2; + + GtkStyleContext* pScrollbarStyle = nullptr; + if ((nPart == ControlPart::ButtonLeft) || (nPart == ControlPart::ButtonRight)) + pScrollbarStyle = mpHScrollbarStyle; + else // (nPart == ControlPart::ButtonUp) || (nPart == ControlPart::ButtonDown) + pScrollbarStyle = mpVScrollbarStyle; + + gtk_style_context_get_style( pScrollbarStyle, + "has-forward-stepper", &has_forward, + "has-secondary-forward-stepper", &has_forward2, + "has-backward-stepper", &has_backward, + "has-secondary-backward-stepper", &has_backward2, nullptr ); + gint buttonWidth; + gint buttonHeight; + + gint nFirst = 0; + gint nSecond = 0; + + if ( has_forward ) nSecond += 1; + if ( has_forward2 ) nFirst += 1; + if ( has_backward ) nFirst += 1; + if ( has_backward2 ) nSecond += 1; + + if (gtk_check_version(3, 20, 0) == nullptr) + { + Size aSize; + if (nPart == ControlPart::ButtonLeft || nPart == ControlPart::ButtonRight) + { + QuerySize(mpHScrollbarStyle, aSize); + QuerySize(mpHScrollbarContentsStyle, aSize); + QuerySize(mpHScrollbarButtonStyle, aSize); + } + else + { + QuerySize(mpVScrollbarStyle, aSize); + QuerySize(mpVScrollbarContentsStyle, aSize); + QuerySize(mpVScrollbarButtonStyle, aSize); + } + + if (nPart == ControlPart::ButtonUp) + { + aSize.setHeight( aSize.Height() * nFirst ); + buttonRect.setX(aAreaRect.Left()); + buttonRect.setY(aAreaRect.Top()); + } + else if (nPart == ControlPart::ButtonLeft) + { + aSize.setWidth( aSize.Width() * nFirst ); + buttonRect.setX(aAreaRect.Left()); + buttonRect.setY(aAreaRect.Top()); + } + else if (nPart == ControlPart::ButtonDown) + { + aSize.setHeight( aSize.Height() * nSecond ); + buttonRect.setX(aAreaRect.Left()); + buttonRect.setY(aAreaRect.Top() + aAreaRect.GetHeight() - aSize.Height()); + } + else if (nPart == ControlPart::ButtonRight) + { + aSize.setWidth( aSize.Width() * nSecond ); + buttonRect.setX(aAreaRect.Left() + aAreaRect.GetWidth() - aSize.Width()); + buttonRect.setY(aAreaRect.Top()); + } + + buttonRect.SetSize(aSize); + + return buttonRect; + } + + gint slider_width; + gint stepper_size; + gint stepper_spacing; + gint trough_border; + + // Grab some button style attributes + gtk_style_context_get_style( pScrollbarStyle, + "slider-width", &slider_width, + "stepper-size", &stepper_size, + "trough-border", &trough_border, + "stepper-spacing", &stepper_spacing, nullptr ); + + if ( ( nPart == ControlPart::ButtonUp ) || ( nPart == ControlPart::ButtonDown ) ) + { + buttonWidth = slider_width + 2 * trough_border; + buttonHeight = stepper_size + trough_border + stepper_spacing; + } + else + { + buttonWidth = stepper_size + trough_border + stepper_spacing; + buttonHeight = slider_width + 2 * trough_border; + } + + if ( nPart == ControlPart::ButtonUp ) + { + buttonHeight *= nFirst; + buttonHeight -= 1; + buttonRect.setX( aAreaRect.Left() ); + buttonRect.setY( aAreaRect.Top() ); + } + else if ( nPart == ControlPart::ButtonLeft ) + { + buttonWidth *= nFirst; + buttonWidth -= 1; + buttonRect.setX( aAreaRect.Left() ); + buttonRect.setY( aAreaRect.Top() ); + } + else if ( nPart == ControlPart::ButtonDown ) + { + buttonHeight *= nSecond; + buttonRect.setX( aAreaRect.Left() ); + buttonRect.setY( aAreaRect.Top() + aAreaRect.GetHeight() - buttonHeight ); + } + else if ( nPart == ControlPart::ButtonRight ) + { + buttonWidth *= nSecond; + buttonRect.setX( aAreaRect.Left() + aAreaRect.GetWidth() - buttonWidth ); + buttonRect.setY( aAreaRect.Top() ); + } + + buttonRect.SetSize( Size( buttonWidth, buttonHeight ) ); + + return buttonRect; +} + +static GtkWidget* gCacheWindow; +static GtkWidget* gDumbContainer; +static GtkWidget* gSpinBox; +static GtkWidget* gEntryBox; +static GtkWidget* gComboBox; +static GtkWidget* gListBox; +static GtkWidget* gMenuBarWidget; +static GtkWidget* gMenuItemMenuBarWidget; +static GtkWidget* gCheckMenuItemWidget; +static GtkWidget* gTreeViewWidget; + +namespace +{ + void style_context_set_state(GtkStyleContext* context, GtkStateFlags flags) + { + do + { + gtk_style_context_set_state(context, flags); + } + while ((context = gtk_style_context_get_parent(context))); + } + + class StyleContextSave + { + private: + std::vector<std::pair<GtkStyleContext*, GtkStateFlags>> m_aStates; + public: + void save(GtkStyleContext* context) + { + do + { + m_aStates.emplace_back(context, gtk_style_context_get_state(context)); + } + while ((context = gtk_style_context_get_parent(context))); + } + void restore() + { + for (auto a = m_aStates.rbegin(); a != m_aStates.rend(); ++a) + { + gtk_style_context_set_state(a->first, a->second); + } + m_aStates.clear(); + } + }; + + tools::Rectangle render_common(GtkStyleContext *pContext, cairo_t *cr, const tools::Rectangle &rIn, GtkStateFlags flags) + { + if (!pContext) + return rIn; + + gtk_style_context_set_state(pContext, flags); + + tools::Rectangle aRect(rIn); + GtkBorder margin; + gtk_style_context_get_margin(pContext, gtk_style_context_get_state(pContext), &margin); + + aRect.AdjustLeft(margin.left ); + aRect.AdjustTop(margin.top ); + aRect.AdjustRight( -(margin.right) ); + aRect.AdjustBottom( -(margin.bottom) ); + + gtk_render_background(pContext, cr, aRect.Left(), aRect.Top(), + aRect.GetWidth(), aRect.GetHeight()); + gtk_render_frame(pContext, cr, aRect.Left(), aRect.Top(), + aRect.GetWidth(), aRect.GetHeight()); + + GtkBorder border, padding; + gtk_style_context_get_border(pContext, gtk_style_context_get_state(pContext), &border); + gtk_style_context_get_padding(pContext, gtk_style_context_get_state(pContext), &padding); + + aRect.AdjustLeft(border.left + padding.left ); + aRect.AdjustTop(border.top + padding.top ); + aRect.AdjustRight( -(border.right + padding.right) ); + aRect.AdjustBottom( -(border.bottom + padding.bottom) ); + + return aRect; + } +} + +void GtkSalGraphics::PaintScrollbar(GtkStyleContext *context, + cairo_t *cr, + const tools::Rectangle& rControlRectangle, + ControlPart nPart, + const ImplControlValue& rValue ) +{ + if (gtk_check_version(3, 20, 0) == nullptr) + { + assert(rValue.getType() == ControlType::Scrollbar); + const ScrollbarValue& rScrollbarVal = static_cast<const ScrollbarValue&>(rValue); + tools::Rectangle scrollbarRect; + GtkStateFlags stateFlags; + GtkOrientation scrollbarOrientation; + tools::Rectangle thumbRect = rScrollbarVal.maThumbRect; + tools::Rectangle button11BoundRect = rScrollbarVal.maButton1Rect; // backward + tools::Rectangle button22BoundRect = rScrollbarVal.maButton2Rect; // forward + tools::Rectangle button12BoundRect = rScrollbarVal.maButton1Rect; // secondary forward + tools::Rectangle button21BoundRect = rScrollbarVal.maButton2Rect; // secondary backward + gdouble arrow1Angle; // backward + gdouble arrow2Angle; // forward + tools::Rectangle arrowRect; + gint slider_width = 0; + gint stepper_size = 0; + + // make controlvalue rectangles relative to area + thumbRect.Move( -rControlRectangle.Left(), -rControlRectangle.Top() ); + button11BoundRect.Move( -rControlRectangle.Left(), -rControlRectangle.Top() ); + button22BoundRect.Move( -rControlRectangle.Left(), -rControlRectangle.Top() ); + button12BoundRect.Move( -rControlRectangle.Left(), -rControlRectangle.Top() ); + button21BoundRect.Move( -rControlRectangle.Left(), -rControlRectangle.Top() ); + + // Find the overall bounding rect of the control + scrollbarRect = rControlRectangle; + if (scrollbarRect.IsEmpty()) + return; + + gint slider_side; + Size aSize; + if (nPart == ControlPart::DrawBackgroundHorz) + { + QuerySize(mpHScrollbarStyle, aSize); + QuerySize(mpHScrollbarContentsStyle, aSize); + QuerySize(mpHScrollbarTroughStyle, aSize); + QuerySize(mpHScrollbarSliderStyle, aSize); + slider_side = aSize.Height(); + gtk_style_context_get(mpHScrollbarButtonStyle, + gtk_style_context_get_state(mpHScrollbarButtonStyle), + "min-height", &slider_width, + "min-width", &stepper_size, nullptr); + } + else + { + QuerySize(mpVScrollbarStyle, aSize); + QuerySize(mpVScrollbarContentsStyle, aSize); + QuerySize(mpVScrollbarTroughStyle, aSize); + QuerySize(mpVScrollbarSliderStyle, aSize); + slider_side = aSize.Width(); + gtk_style_context_get(mpVScrollbarButtonStyle, + gtk_style_context_get_state(mpVScrollbarButtonStyle), + "min-width", &slider_width, + "min-height", &stepper_size, nullptr); + } + + gboolean has_forward; + gboolean has_forward2; + gboolean has_backward; + gboolean has_backward2; + + gtk_style_context_get_style( context, + "has-forward-stepper", &has_forward, + "has-secondary-forward-stepper", &has_forward2, + "has-backward-stepper", &has_backward, + "has-secondary-backward-stepper", &has_backward2, nullptr ); + + if ( nPart == ControlPart::DrawBackgroundHorz ) + { + // Center vertically in the track + scrollbarRect.Move( 0, (scrollbarRect.GetHeight() - slider_side) / 2 ); + scrollbarRect.SetSize( Size( scrollbarRect.GetWidth(), slider_side ) ); + thumbRect.Move( 0, (scrollbarRect.GetHeight() - slider_side) / 2 ); + thumbRect.SetSize( Size( thumbRect.GetWidth(), slider_side ) ); + + scrollbarOrientation = GTK_ORIENTATION_HORIZONTAL; + arrow1Angle = G_PI * 3 / 2; + arrow2Angle = G_PI / 2; + + if ( has_backward ) + { + button12BoundRect.Move( stepper_size, + (scrollbarRect.GetHeight() - slider_width) / 2 ); + } + + button11BoundRect.Move( 0, (scrollbarRect.GetHeight() - slider_width) / 2 ); + button11BoundRect.SetSize( Size( stepper_size, slider_width ) ); + button12BoundRect.SetSize( Size( stepper_size, slider_width ) ); + + if ( has_backward2 ) + { + button22BoundRect.Move( stepper_size, (scrollbarRect.GetHeight() - slider_width) / 2 ); + button21BoundRect.Move( 0, (scrollbarRect.GetHeight() - slider_width) / 2 ); + } + else + { + button22BoundRect.Move( 0, (scrollbarRect.GetHeight() - slider_width) / 2 ); + } + + button21BoundRect.SetSize( Size( stepper_size, slider_width ) ); + button22BoundRect.SetSize( Size( stepper_size, slider_width ) ); + } + else + { + // Center horizontally in the track + scrollbarRect.Move( (scrollbarRect.GetWidth() - slider_side) / 2, 0 ); + scrollbarRect.SetSize( Size( slider_side, scrollbarRect.GetHeight() ) ); + thumbRect.Move( (scrollbarRect.GetWidth() - slider_side) / 2, 0 ); + thumbRect.SetSize( Size( slider_side, thumbRect.GetHeight() ) ); + + scrollbarOrientation = GTK_ORIENTATION_VERTICAL; + arrow1Angle = 0; + arrow2Angle = G_PI; + + if ( has_backward ) + { + button12BoundRect.Move( (scrollbarRect.GetWidth() - slider_width) / 2, + stepper_size ); + } + button11BoundRect.Move( (scrollbarRect.GetWidth() - slider_width) / 2, 0 ); + button11BoundRect.SetSize( Size( slider_width, stepper_size ) ); + button12BoundRect.SetSize( Size( slider_width, stepper_size ) ); + + if ( has_backward2 ) + { + button22BoundRect.Move( (scrollbarRect.GetWidth() - slider_width) / 2, stepper_size ); + button21BoundRect.Move( (scrollbarRect.GetWidth() - slider_width) / 2, 0 ); + } + else + { + button22BoundRect.Move( (scrollbarRect.GetWidth() - slider_width) / 2, 0 ); + } + + button21BoundRect.SetSize( Size( slider_width, stepper_size ) ); + button22BoundRect.SetSize( Size( slider_width, stepper_size ) ); + } + + bool has_slider = !thumbRect.IsEmpty(); + + // ----------------- CONTENTS + GtkStyleContext* pScrollbarContentsStyle = scrollbarOrientation == GTK_ORIENTATION_VERTICAL ? + mpVScrollbarContentsStyle : mpHScrollbarContentsStyle; + + gtk_render_background(gtk_widget_get_style_context(gCacheWindow), cr, 0, 0, + scrollbarRect.GetWidth(), scrollbarRect.GetHeight() ); + + gtk_render_background(context, cr, 0, 0, + scrollbarRect.GetWidth(), scrollbarRect.GetHeight() ); + gtk_render_frame(context, cr, 0, 0, + scrollbarRect.GetWidth(), scrollbarRect.GetHeight() ); + + gtk_render_background(pScrollbarContentsStyle, cr, 0, 0, + scrollbarRect.GetWidth(), scrollbarRect.GetHeight() ); + gtk_render_frame(pScrollbarContentsStyle, cr, 0, 0, + scrollbarRect.GetWidth(), scrollbarRect.GetHeight() ); + + bool backwardButtonInsensitive = + rScrollbarVal.mnCur == rScrollbarVal.mnMin; + bool forwardButtonInsensitive = rScrollbarVal.mnMax == 0 || + rScrollbarVal.mnCur + rScrollbarVal.mnVisibleSize >= rScrollbarVal.mnMax; + + // ----------------- BUTTON 1 + if ( has_backward ) + { + stateFlags = NWConvertVCLStateToGTKState(rScrollbarVal.mnButton1State); + if ( backwardButtonInsensitive ) + stateFlags = GTK_STATE_FLAG_INSENSITIVE; + + GtkStyleContext* pScrollbarButtonStyle = scrollbarOrientation == GTK_ORIENTATION_VERTICAL ? + mpVScrollbarButtonStyle : mpHScrollbarButtonStyle; + + gtk_style_context_set_state(pScrollbarButtonStyle, stateFlags); + + gtk_render_background(pScrollbarButtonStyle, cr, + button11BoundRect.Left(), button11BoundRect.Top(), + button11BoundRect.GetWidth(), button11BoundRect.GetHeight() ); + gtk_render_frame(pScrollbarButtonStyle, cr, + button11BoundRect.Left(), button11BoundRect.Top(), + button11BoundRect.GetWidth(), button11BoundRect.GetHeight() ); + + // ----------------- ARROW 1 + NWCalcArrowRect( button11BoundRect, arrowRect ); + gtk_render_arrow(pScrollbarButtonStyle, cr, + arrow1Angle, + arrowRect.Left(), arrowRect.Top(), + MIN(arrowRect.GetWidth(), arrowRect.GetHeight()) ); + } + if ( has_forward2 ) + { + stateFlags = NWConvertVCLStateToGTKState(rScrollbarVal.mnButton2State); + if ( forwardButtonInsensitive ) + stateFlags = GTK_STATE_FLAG_INSENSITIVE; + + GtkStyleContext* pScrollbarButtonStyle = scrollbarOrientation == GTK_ORIENTATION_VERTICAL ? + mpVScrollbarButtonStyle : mpHScrollbarButtonStyle; + + gtk_style_context_set_state(pScrollbarButtonStyle, stateFlags); + + gtk_render_background(pScrollbarButtonStyle, cr, + button12BoundRect.Left(), button12BoundRect.Top(), + button12BoundRect.GetWidth(), button12BoundRect.GetHeight() ); + gtk_render_frame(pScrollbarButtonStyle, cr, + button12BoundRect.Left(), button12BoundRect.Top(), + button12BoundRect.GetWidth(), button12BoundRect.GetHeight() ); + + // ----------------- ARROW 1 + NWCalcArrowRect( button12BoundRect, arrowRect ); + gtk_render_arrow(pScrollbarButtonStyle, cr, + arrow2Angle, + arrowRect.Left(), arrowRect.Top(), + MIN(arrowRect.GetWidth(), arrowRect.GetHeight()) ); + } + // ----------------- BUTTON 2 + + if ( has_forward ) + { + stateFlags = NWConvertVCLStateToGTKState(rScrollbarVal.mnButton2State); + if ( forwardButtonInsensitive ) + stateFlags = GTK_STATE_FLAG_INSENSITIVE; + + GtkStyleContext* pScrollbarButtonStyle = scrollbarOrientation == GTK_ORIENTATION_VERTICAL ? + mpVScrollbarButtonStyle : mpHScrollbarButtonStyle; + + gtk_style_context_set_state(pScrollbarButtonStyle, stateFlags); + + gtk_render_background(pScrollbarButtonStyle, cr, + button22BoundRect.Left(), button22BoundRect.Top(), + button22BoundRect.GetWidth(), button22BoundRect.GetHeight() ); + gtk_render_frame(pScrollbarButtonStyle, cr, + button22BoundRect.Left(), button22BoundRect.Top(), + button22BoundRect.GetWidth(), button22BoundRect.GetHeight() ); + + // ----------------- ARROW 2 + NWCalcArrowRect( button22BoundRect, arrowRect ); + gtk_render_arrow(pScrollbarButtonStyle, cr, + arrow2Angle, + arrowRect.Left(), arrowRect.Top(), + MIN(arrowRect.GetWidth(), arrowRect.GetHeight()) ); + } + + if ( has_backward2 ) + { + stateFlags = NWConvertVCLStateToGTKState(rScrollbarVal.mnButton1State); + if ( backwardButtonInsensitive ) + stateFlags = GTK_STATE_FLAG_INSENSITIVE; + + GtkStyleContext* pScrollbarButtonStyle = scrollbarOrientation == GTK_ORIENTATION_VERTICAL ? + mpVScrollbarButtonStyle : mpHScrollbarButtonStyle; + + gtk_style_context_set_state(pScrollbarButtonStyle, stateFlags); + + gtk_render_background(pScrollbarButtonStyle, cr, + button21BoundRect.Left(), button21BoundRect.Top(), + button21BoundRect.GetWidth(), button21BoundRect.GetHeight() ); + gtk_render_frame(pScrollbarButtonStyle, cr, + button21BoundRect.Left(), button21BoundRect.Top(), + button21BoundRect.GetWidth(), button21BoundRect.GetHeight() ); + + // ----------------- ARROW 2 + NWCalcArrowRect( button21BoundRect, arrowRect ); + gtk_render_arrow(pScrollbarButtonStyle, cr, + arrow1Angle, + arrowRect.Left(), arrowRect.Top(), + MIN(arrowRect.GetWidth(), arrowRect.GetHeight()) ); + } + + // ----------------- TROUGH + // trackrect matches that of ScrollBar::ImplCalc + tools::Rectangle aTrackRect(Point(0, 0), scrollbarRect.GetSize()); + if (nPart == ControlPart::DrawBackgroundHorz) + { + tools::Rectangle aBtn1Rect = NWGetScrollButtonRect(ControlPart::ButtonLeft, aTrackRect); + tools::Rectangle aBtn2Rect = NWGetScrollButtonRect(ControlPart::ButtonRight, aTrackRect); + if (!aBtn1Rect.IsWidthEmpty()) + aTrackRect.SetLeft( aBtn1Rect.Right() ); + if (!aBtn2Rect.IsWidthEmpty()) + aTrackRect.SetRight( aBtn2Rect.Left() ); + } + else + { + tools::Rectangle aBtn1Rect = NWGetScrollButtonRect(ControlPart::ButtonUp, aTrackRect); + tools::Rectangle aBtn2Rect = NWGetScrollButtonRect(ControlPart::ButtonDown, aTrackRect); + if (!aBtn1Rect.IsHeightEmpty()) + aTrackRect.SetTop( aBtn1Rect.Bottom() + 1 ); + if (!aBtn2Rect.IsHeightEmpty()) + aTrackRect.SetBottom( aBtn2Rect.Top() ); + } + + GtkStyleContext* pScrollbarTroughStyle = scrollbarOrientation == GTK_ORIENTATION_VERTICAL ? + mpVScrollbarTroughStyle : mpHScrollbarTroughStyle; + gtk_render_background(pScrollbarTroughStyle, cr, aTrackRect.Left(), aTrackRect.Top(), + aTrackRect.GetWidth(), aTrackRect.GetHeight() ); + gtk_render_frame(pScrollbarTroughStyle, cr, aTrackRect.Left(), aTrackRect.Top(), + aTrackRect.GetWidth(), aTrackRect.GetHeight() ); + + // ----------------- THUMB + if ( has_slider ) + { + stateFlags = NWConvertVCLStateToGTKState(rScrollbarVal.mnThumbState); + if ( rScrollbarVal.mnThumbState & ControlState::PRESSED ) + stateFlags = static_cast<GtkStateFlags>(stateFlags | GTK_STATE_FLAG_PRELIGHT); + + GtkStyleContext* pScrollbarSliderStyle = scrollbarOrientation == GTK_ORIENTATION_VERTICAL ? + mpVScrollbarSliderStyle : mpHScrollbarSliderStyle; + + gtk_style_context_set_state(pScrollbarSliderStyle, stateFlags); + + GtkBorder margin; + gtk_style_context_get_margin(pScrollbarSliderStyle, stateFlags, &margin); + + gtk_render_background(pScrollbarSliderStyle, cr, + thumbRect.Left() + margin.left, thumbRect.Top() + margin.top, + thumbRect.GetWidth() - margin.left - margin.right, + thumbRect.GetHeight() - margin.top - margin.bottom); + + gtk_render_frame(pScrollbarSliderStyle, cr, + thumbRect.Left() + margin.left, thumbRect.Top() + margin.top, + thumbRect.GetWidth() - margin.left - margin.right, + thumbRect.GetHeight() - margin.top - margin.bottom); + } + + return; + } + + OSL_ASSERT( rValue.getType() == ControlType::Scrollbar ); + const ScrollbarValue& rScrollbarVal = static_cast<const ScrollbarValue&>(rValue); + tools::Rectangle scrollbarRect; + GtkStateFlags stateFlags; + GtkOrientation scrollbarOrientation; + tools::Rectangle thumbRect = rScrollbarVal.maThumbRect; + tools::Rectangle button11BoundRect = rScrollbarVal.maButton1Rect; // backward + tools::Rectangle button22BoundRect = rScrollbarVal.maButton2Rect; // forward + tools::Rectangle button12BoundRect = rScrollbarVal.maButton1Rect; // secondary forward + tools::Rectangle button21BoundRect = rScrollbarVal.maButton2Rect; // secondary backward + gdouble arrow1Angle; // backward + gdouble arrow2Angle; // forward + tools::Rectangle arrowRect; + gint slider_width = 0; + gint stepper_size = 0; + gint trough_border = 0; + + // make controlvalue rectangles relative to area + thumbRect.Move( -rControlRectangle.Left(), -rControlRectangle.Top() ); + button11BoundRect.Move( -rControlRectangle.Left(), -rControlRectangle.Top() ); + button22BoundRect.Move( -rControlRectangle.Left(), -rControlRectangle.Top() ); + button12BoundRect.Move( -rControlRectangle.Left(), -rControlRectangle.Top() ); + button21BoundRect.Move( -rControlRectangle.Left(), -rControlRectangle.Top() ); + + // Find the overall bounding rect of the control + scrollbarRect = rControlRectangle; + scrollbarRect.SetSize( Size( scrollbarRect.GetWidth() + 1, + scrollbarRect.GetHeight() + 1 ) ); + + if ( (scrollbarRect.GetWidth() <= 1) || (scrollbarRect.GetHeight() <= 1) ) + return; + + // Grab some button style attributes + gtk_style_context_get_style( context, + "slider_width", &slider_width, + "stepper_size", &stepper_size, + "trough_border", &trough_border, nullptr ); + gboolean has_forward; + gboolean has_forward2; + gboolean has_backward; + gboolean has_backward2; + + gtk_style_context_get_style( context, + "has-forward-stepper", &has_forward, + "has-secondary-forward-stepper", &has_forward2, + "has-backward-stepper", &has_backward, + "has-secondary-backward-stepper", &has_backward2, nullptr ); + gint magic = trough_border ? 1 : 0; + gint slider_side = slider_width + (trough_border * 2); + + if ( nPart == ControlPart::DrawBackgroundHorz ) + { + scrollbarRect.Move( 0, (scrollbarRect.GetHeight() - slider_side) / 2 ); + scrollbarRect.SetSize( Size( scrollbarRect.GetWidth(), slider_side ) ); + + scrollbarOrientation = GTK_ORIENTATION_HORIZONTAL; + arrow1Angle = G_PI * 3 / 2; + arrow2Angle = G_PI / 2; + + if ( has_backward ) + { + button12BoundRect.Move( stepper_size - trough_border, + (scrollbarRect.GetHeight() - slider_width) / 2 ); + } + + button11BoundRect.Move( trough_border, (scrollbarRect.GetHeight() - slider_width) / 2 ); + button11BoundRect.SetSize( Size( stepper_size, slider_width ) ); + button12BoundRect.SetSize( Size( stepper_size, slider_width ) ); + + if ( has_backward2 ) + { + button22BoundRect.Move( stepper_size+(trough_border+1)/2, (scrollbarRect.GetHeight() - slider_width) / 2 ); + button21BoundRect.Move( (trough_border+1)/2, (scrollbarRect.GetHeight() - slider_width) / 2 ); + } + else + { + button22BoundRect.Move( (trough_border+1)/2, (scrollbarRect.GetHeight() - slider_width) / 2 ); + } + + button21BoundRect.SetSize( Size( stepper_size, slider_width ) ); + button22BoundRect.SetSize( Size( stepper_size, slider_width ) ); + + thumbRect.SetBottom( thumbRect.Top() + slider_width - 1 ); + // Make sure the thumb is at least the default width (so we don't get tiny thumbs), + // but if the VCL gives us a size smaller than the theme's default thumb size, + // honor the VCL size + thumbRect.AdjustRight(magic ); + // Center vertically in the track + thumbRect.Move( 0, (scrollbarRect.GetHeight() - slider_width) / 2 ); + } + else + { + scrollbarRect.Move( (scrollbarRect.GetWidth() - slider_side) / 2, 0 ); + scrollbarRect.SetSize( Size( slider_side, scrollbarRect.GetHeight() ) ); + + scrollbarOrientation = GTK_ORIENTATION_VERTICAL; + arrow1Angle = 0; + arrow2Angle = G_PI; + + if ( has_backward ) + { + button12BoundRect.Move( (scrollbarRect.GetWidth() - slider_width) / 2, + stepper_size + trough_border ); + } + button11BoundRect.Move( (scrollbarRect.GetWidth() - slider_width) / 2, trough_border ); + button11BoundRect.SetSize( Size( slider_width, stepper_size ) ); + button12BoundRect.SetSize( Size( slider_width, stepper_size ) ); + + if ( has_backward2 ) + { + button22BoundRect.Move( (scrollbarRect.GetWidth() - slider_width) / 2, stepper_size+(trough_border+1)/2 ); + button21BoundRect.Move( (scrollbarRect.GetWidth() - slider_width) / 2, (trough_border+1)/2 ); + } + else + { + button22BoundRect.Move( (scrollbarRect.GetWidth() - slider_width) / 2, (trough_border+1)/2 ); + } + + button21BoundRect.SetSize( Size( slider_width, stepper_size ) ); + button22BoundRect.SetSize( Size( slider_width, stepper_size ) ); + + thumbRect.SetRight( thumbRect.Left() + slider_width - 1 ); + + thumbRect.AdjustBottom(magic ); + // Center horizontally in the track + thumbRect.Move( (scrollbarRect.GetWidth() - slider_width) / 2, 0 ); + } + + bool has_slider = !thumbRect.IsEmpty(); + + // ----------------- CONTENTS + GtkStyleContext* pScrollbarContentsStyle = scrollbarOrientation == GTK_ORIENTATION_VERTICAL ? + mpVScrollbarContentsStyle : mpHScrollbarContentsStyle; + + gtk_render_background(gtk_widget_get_style_context(gCacheWindow), cr, 0, 0, + scrollbarRect.GetWidth(), scrollbarRect.GetHeight() ); + + gtk_render_background(context, cr, 0, 0, + scrollbarRect.GetWidth(), scrollbarRect.GetHeight() ); + gtk_render_frame(context, cr, 0, 0, + scrollbarRect.GetWidth(), scrollbarRect.GetHeight() ); + + gtk_render_background(pScrollbarContentsStyle, cr, 0, 0, + scrollbarRect.GetWidth(), scrollbarRect.GetHeight() ); + gtk_render_frame(pScrollbarContentsStyle, cr, 0, 0, + scrollbarRect.GetWidth(), scrollbarRect.GetHeight() ); + + // ----------------- TROUGH + GtkStyleContext* pScrollbarTroughStyle = scrollbarOrientation == GTK_ORIENTATION_VERTICAL ? + mpVScrollbarTroughStyle : mpHScrollbarTroughStyle; + gtk_render_background(pScrollbarTroughStyle, cr, 0, 0, + scrollbarRect.GetWidth(), scrollbarRect.GetHeight() ); + gtk_render_frame(pScrollbarTroughStyle, cr, 0, 0, + scrollbarRect.GetWidth(), scrollbarRect.GetHeight() ); + + // ----------------- THUMB + if ( has_slider ) + { + stateFlags = NWConvertVCLStateToGTKState(rScrollbarVal.mnThumbState); + if ( rScrollbarVal.mnThumbState & ControlState::PRESSED ) + stateFlags = static_cast<GtkStateFlags>(stateFlags | GTK_STATE_FLAG_PRELIGHT); + + GtkStyleContext* pScrollbarSliderStyle = scrollbarOrientation == GTK_ORIENTATION_VERTICAL ? + mpVScrollbarSliderStyle : mpHScrollbarSliderStyle; + + gtk_style_context_set_state(pScrollbarSliderStyle, stateFlags); + + GtkBorder margin; + gtk_style_context_get_margin(pScrollbarSliderStyle, stateFlags, &margin); + + gtk_render_background(pScrollbarSliderStyle, cr, + thumbRect.Left() + margin.left, thumbRect.Top() + margin.top, + thumbRect.GetWidth() - margin.left - margin.right, + thumbRect.GetHeight() - margin.top - margin.bottom); + + gtk_render_frame(pScrollbarSliderStyle, cr, + thumbRect.Left() + margin.left, thumbRect.Top() + margin.top, + thumbRect.GetWidth() - margin.left - margin.right, + thumbRect.GetHeight() - margin.top - margin.bottom); + } + + bool backwardButtonInsensitive = + rScrollbarVal.mnCur == rScrollbarVal.mnMin; + bool forwardButtonInsensitive = rScrollbarVal.mnMax == 0 || + rScrollbarVal.mnCur + rScrollbarVal.mnVisibleSize >= rScrollbarVal.mnMax; + + // ----------------- BUTTON 1 + if ( has_backward ) + { + stateFlags = NWConvertVCLStateToGTKState(rScrollbarVal.mnButton1State); + if ( backwardButtonInsensitive ) + stateFlags = GTK_STATE_FLAG_INSENSITIVE; + + GtkStyleContext* pScrollbarButtonStyle = scrollbarOrientation == GTK_ORIENTATION_VERTICAL ? + mpVScrollbarButtonStyle : mpHScrollbarButtonStyle; + + gtk_style_context_set_state(pScrollbarButtonStyle, stateFlags); + + gtk_render_background(pScrollbarButtonStyle, cr, + button11BoundRect.Left(), button11BoundRect.Top(), + button11BoundRect.GetWidth(), button11BoundRect.GetHeight() ); + gtk_render_frame(pScrollbarButtonStyle, cr, + button11BoundRect.Left(), button11BoundRect.Top(), + button11BoundRect.GetWidth(), button11BoundRect.GetHeight() ); + + // ----------------- ARROW 1 + NWCalcArrowRect( button11BoundRect, arrowRect ); + gtk_render_arrow(pScrollbarButtonStyle, cr, + arrow1Angle, + arrowRect.Left(), arrowRect.Top(), + MIN(arrowRect.GetWidth(), arrowRect.GetHeight()) ); + } + if ( has_forward2 ) + { + stateFlags = NWConvertVCLStateToGTKState(rScrollbarVal.mnButton2State); + if ( forwardButtonInsensitive ) + stateFlags = GTK_STATE_FLAG_INSENSITIVE; + + GtkStyleContext* pScrollbarButtonStyle = scrollbarOrientation == GTK_ORIENTATION_VERTICAL ? + mpVScrollbarButtonStyle : mpHScrollbarButtonStyle; + + gtk_style_context_set_state(pScrollbarButtonStyle, stateFlags); + + gtk_render_background(pScrollbarButtonStyle, cr, + button12BoundRect.Left(), button12BoundRect.Top(), + button12BoundRect.GetWidth(), button12BoundRect.GetHeight() ); + gtk_render_frame(pScrollbarButtonStyle, cr, + button12BoundRect.Left(), button12BoundRect.Top(), + button12BoundRect.GetWidth(), button12BoundRect.GetHeight() ); + + // ----------------- ARROW 1 + NWCalcArrowRect( button12BoundRect, arrowRect ); + gtk_render_arrow(pScrollbarButtonStyle, cr, + arrow2Angle, + arrowRect.Left(), arrowRect.Top(), + MIN(arrowRect.GetWidth(), arrowRect.GetHeight()) ); + } + // ----------------- BUTTON 2 + if ( has_backward2 ) + { + stateFlags = NWConvertVCLStateToGTKState(rScrollbarVal.mnButton1State); + if ( backwardButtonInsensitive ) + stateFlags = GTK_STATE_FLAG_INSENSITIVE; + + GtkStyleContext* pScrollbarButtonStyle = scrollbarOrientation == GTK_ORIENTATION_VERTICAL ? + mpVScrollbarButtonStyle : mpHScrollbarButtonStyle; + + gtk_style_context_set_state(pScrollbarButtonStyle, stateFlags); + + gtk_render_background(pScrollbarButtonStyle, cr, + button21BoundRect.Left(), button21BoundRect.Top(), + button21BoundRect.GetWidth(), button21BoundRect.GetHeight() ); + gtk_render_frame(pScrollbarButtonStyle, cr, + button21BoundRect.Left(), button21BoundRect.Top(), + button21BoundRect.GetWidth(), button21BoundRect.GetHeight() ); + + // ----------------- ARROW 2 + NWCalcArrowRect( button21BoundRect, arrowRect ); + gtk_render_arrow(pScrollbarButtonStyle, cr, + arrow1Angle, + arrowRect.Left(), arrowRect.Top(), + MIN(arrowRect.GetWidth(), arrowRect.GetHeight()) ); + } + if ( has_forward ) + { + stateFlags = NWConvertVCLStateToGTKState(rScrollbarVal.mnButton2State); + if ( forwardButtonInsensitive ) + stateFlags = GTK_STATE_FLAG_INSENSITIVE; + + GtkStyleContext* pScrollbarButtonStyle = scrollbarOrientation == GTK_ORIENTATION_VERTICAL ? + mpVScrollbarButtonStyle : mpHScrollbarButtonStyle; + + gtk_style_context_set_state(pScrollbarButtonStyle, stateFlags); + + gtk_render_background(pScrollbarButtonStyle, cr, + button22BoundRect.Left(), button22BoundRect.Top(), + button22BoundRect.GetWidth(), button22BoundRect.GetHeight() ); + gtk_render_frame(pScrollbarButtonStyle, cr, + button22BoundRect.Left(), button22BoundRect.Top(), + button22BoundRect.GetWidth(), button22BoundRect.GetHeight() ); + + // ----------------- ARROW 2 + NWCalcArrowRect( button22BoundRect, arrowRect ); + gtk_render_arrow(pScrollbarButtonStyle, cr, + arrow2Angle, + arrowRect.Left(), arrowRect.Top(), + MIN(arrowRect.GetWidth(), arrowRect.GetHeight()) ); + } +} + +void GtkSalGraphics::PaintOneSpinButton( GtkStyleContext *context, + cairo_t *cr, + ControlPart nPart, + tools::Rectangle aAreaRect, + ControlState nState ) +{ + GtkBorder padding, border; + + GtkStateFlags stateFlags = NWConvertVCLStateToGTKState(nState); + tools::Rectangle buttonRect = NWGetSpinButtonRect( nPart, aAreaRect ); + + gtk_style_context_set_state(context, stateFlags); + stateFlags = gtk_style_context_get_state(context); + + gtk_style_context_get_padding(context, stateFlags, &padding); + gtk_style_context_get_border(context, stateFlags, &border); + + gtk_render_background(context, cr, + buttonRect.Left(), buttonRect.Top(), + buttonRect.GetWidth(), buttonRect.GetHeight() ); + + gint iconWidth = buttonRect.GetWidth() - padding.left - padding.right - border.left - border.right; + gint iconHeight = buttonRect.GetHeight() - padding.top - padding.bottom - border.top - border.bottom; + + const char* icon = (nPart == ControlPart::ButtonUp) ? "list-add-symbolic" : "list-remove-symbolic"; + GtkIconTheme *pIconTheme = gtk_icon_theme_get_for_screen(gtk_widget_get_screen(mpWindow)); + + gint scale = gtk_style_context_get_scale (context); + GtkIconInfo *info = gtk_icon_theme_lookup_icon_for_scale(pIconTheme, icon, std::min(iconWidth, iconHeight), scale, + static_cast<GtkIconLookupFlags>(0)); + + GdkPixbuf *pixbuf = gtk_icon_info_load_symbolic_for_context(info, context, nullptr, nullptr); + g_object_unref(info); + + iconWidth = gdk_pixbuf_get_width(pixbuf)/scale; + iconHeight = gdk_pixbuf_get_height(pixbuf)/scale; + tools::Rectangle arrowRect; + arrowRect.SetSize(Size(iconWidth, iconHeight)); + arrowRect.setX( buttonRect.Left() + (buttonRect.GetWidth() - arrowRect.GetWidth()) / 2 ); + arrowRect.setY( buttonRect.Top() + (buttonRect.GetHeight() - arrowRect.GetHeight()) / 2 ); + + gtk_style_context_save (context); + gtk_style_context_set_scale (context, 1); + gtk_render_icon(context, cr, pixbuf, arrowRect.Left(), arrowRect.Top()); + gtk_style_context_restore (context); + g_object_unref(pixbuf); + + gtk_render_frame(context, cr, + buttonRect.Left(), buttonRect.Top(), + buttonRect.GetWidth(), buttonRect.GetHeight() ); +} + +void GtkSalGraphics::PaintSpinButton(GtkStateFlags flags, + cairo_t *cr, + const tools::Rectangle& rControlRectangle, + ControlPart nPart, + const ImplControlValue& rValue ) +{ + const SpinbuttonValue *pSpinVal = (rValue.getType() == ControlType::SpinButtons) ? static_cast<const SpinbuttonValue *>(&rValue) : nullptr; + ControlPart upBtnPart = ControlPart::ButtonUp; + ControlState upBtnState = ControlState::NONE; + ControlPart downBtnPart = ControlPart::ButtonDown; + ControlState downBtnState = ControlState::NONE; + + if ( pSpinVal ) + { + upBtnPart = pSpinVal->mnUpperPart; + upBtnState = pSpinVal->mnUpperState; + + downBtnPart = pSpinVal->mnLowerPart; + downBtnState = pSpinVal->mnLowerState; + } + + if (nPart == ControlPart::Entire) + { + gtk_style_context_set_state(mpWindowStyle, flags); + + gtk_render_background(mpWindowStyle, cr, + 0, 0, + rControlRectangle.GetWidth(), rControlRectangle.GetHeight()); + + gtk_style_context_set_state(mpSpinStyle, flags); + + gtk_render_background(mpSpinStyle, cr, + 0, 0, + rControlRectangle.GetWidth(), rControlRectangle.GetHeight()); + } + + cairo_translate(cr, -rControlRectangle.Left(), -rControlRectangle.Top()); + PaintOneSpinButton(mpSpinUpStyle, cr, upBtnPart, rControlRectangle, upBtnState ); + PaintOneSpinButton(mpSpinDownStyle, cr, downBtnPart, rControlRectangle, downBtnState ); + cairo_translate(cr, rControlRectangle.Left(), rControlRectangle.Top()); + + if (nPart == ControlPart::Entire) + { + gtk_render_frame(mpSpinStyle, cr, + 0, 0, + rControlRectangle.GetWidth(), rControlRectangle.GetHeight() ); + } +} + +#define FALLBACK_ARROW_SIZE gint(11 * 0.85) + +tools::Rectangle GtkSalGraphics::NWGetComboBoxButtonRect(ControlType nType, + ControlPart nPart, + tools::Rectangle aAreaRect ) +{ + tools::Rectangle aButtonRect; + + GtkBorder padding; + if (nType == ControlType::Listbox) + gtk_style_context_get_padding(mpListboxButtonStyle, gtk_style_context_get_state(mpListboxButtonStyle), &padding); + else + gtk_style_context_get_padding(mpButtonStyle, gtk_style_context_get_state(mpButtonStyle), &padding); + + gint nArrowWidth = FALLBACK_ARROW_SIZE; + if (gtk_check_version(3, 20, 0) == nullptr) + { + gtk_style_context_get(mpComboboxButtonArrowStyle, + gtk_style_context_get_state(mpComboboxButtonArrowStyle), + "min-width", &nArrowWidth, nullptr); + } + else + { + nArrowWidth = nArrowWidth * gtk_style_context_get_scale (mpComboboxButtonArrowStyle); + } + + gint nButtonWidth = nArrowWidth + padding.left + padding.right; + if( nPart == ControlPart::ButtonDown ) + { + Point aPos(aAreaRect.Left() + aAreaRect.GetWidth() - nButtonWidth, aAreaRect.Top()); + if (AllSettings::GetLayoutRTL()) + aPos.setX( aAreaRect.Left() ); + aButtonRect.SetSize( Size( nButtonWidth, aAreaRect.GetHeight() ) ); + aButtonRect.SetPos(aPos); + } + else if( nPart == ControlPart::SubEdit ) + { + gint adjust_left = padding.left; + gint adjust_top = padding.top; + gint adjust_right = padding.right; + gint adjust_bottom = padding.bottom; + + aButtonRect.SetSize( Size( aAreaRect.GetWidth() - nButtonWidth - (adjust_left + adjust_right), + aAreaRect.GetHeight() - (adjust_top + adjust_bottom)) ); + Point aEditPos = aAreaRect.TopLeft(); + if (AllSettings::GetLayoutRTL()) + aEditPos.AdjustX(nButtonWidth ); + else + aEditPos.AdjustX(adjust_left ); + aEditPos.AdjustY(adjust_top ); + aButtonRect.SetPos( aEditPos ); + } + + return aButtonRect; +} + +void GtkSalGraphics::PaintCombobox( GtkStateFlags flags, cairo_t *cr, + const tools::Rectangle& rControlRectangle, + ControlType nType, + ControlPart nPart ) +{ + tools::Rectangle areaRect; + tools::Rectangle buttonRect; + tools::Rectangle arrowRect; + + // Find the overall bounding rect of the buttons's drawing area, + // plus its actual draw rect excluding adornment + areaRect = rControlRectangle; + + buttonRect = NWGetComboBoxButtonRect(ControlType::Combobox, ControlPart::ButtonDown, areaRect); + + tools::Rectangle aEditBoxRect( areaRect ); + aEditBoxRect.SetSize( Size( areaRect.GetWidth() - buttonRect.GetWidth(), aEditBoxRect.GetHeight() ) ); + if (AllSettings::GetLayoutRTL()) + aEditBoxRect.SetPos( Point( areaRect.Left() + buttonRect.GetWidth(), areaRect.Top() ) ); + + gint arrow_width = FALLBACK_ARROW_SIZE, arrow_height = FALLBACK_ARROW_SIZE; + if (gtk_check_version(3, 20, 0) == nullptr) + { + if (nType == ControlType::Combobox) + { + gtk_style_context_get(mpComboboxButtonArrowStyle, + gtk_style_context_get_state(mpComboboxButtonArrowStyle), + "min-width", &arrow_width, "min-height", &arrow_height, nullptr); + } + else if (nType == ControlType::Listbox) + { + gtk_style_context_get(mpListboxButtonArrowStyle, + gtk_style_context_get_state(mpListboxButtonArrowStyle), + "min-width", &arrow_width, "min-height", &arrow_height, nullptr); + } + } + else + { + if (nType == ControlType::Combobox) + { + arrow_width = arrow_width * gtk_style_context_get_scale (mpComboboxButtonArrowStyle); + arrow_height = arrow_height * gtk_style_context_get_scale (mpComboboxButtonArrowStyle); + } + else if (nType == ControlType::Listbox) + { + arrow_width = arrow_width * gtk_style_context_get_scale (mpListboxButtonArrowStyle); + arrow_height = arrow_height * gtk_style_context_get_scale (mpListboxButtonArrowStyle); + } + } + + arrowRect.SetSize(Size(arrow_width, arrow_height)); + arrowRect.SetPos( Point( buttonRect.Left() + static_cast<gint>((buttonRect.GetWidth() - arrowRect.GetWidth()) / 2), + buttonRect.Top() + static_cast<gint>((buttonRect.GetHeight() - arrowRect.GetHeight()) / 2) ) ); + + + tools::Rectangle aRect(Point(0, 0), Size(areaRect.GetWidth(), areaRect.GetHeight())); + + if (nType == ControlType::Combobox) + { + if( nPart == ControlPart::Entire ) + { + render_common(mpComboboxStyle, cr, aRect, flags); + render_common(mpComboboxBoxStyle, cr, aRect, flags); + tools::Rectangle aEntryRect(Point(aEditBoxRect.Left() - areaRect.Left(), + aEditBoxRect.Top() - areaRect.Top()), + Size(aEditBoxRect.GetWidth(), aEditBoxRect.GetHeight())); + + GtkJunctionSides eJuncSides = gtk_style_context_get_junction_sides(mpComboboxEntryStyle); + if (AllSettings::GetLayoutRTL()) + gtk_style_context_set_junction_sides(mpComboboxEntryStyle, GTK_JUNCTION_LEFT); + else + gtk_style_context_set_junction_sides(mpComboboxEntryStyle, GTK_JUNCTION_RIGHT); + render_common(mpComboboxEntryStyle, cr, aEntryRect, flags); + gtk_style_context_set_junction_sides(mpComboboxEntryStyle, eJuncSides); + } + + tools::Rectangle aButtonRect(Point(buttonRect.Left() - areaRect.Left(), buttonRect.Top() - areaRect.Top()), + Size(buttonRect.GetWidth(), buttonRect.GetHeight())); + GtkJunctionSides eJuncSides = gtk_style_context_get_junction_sides(mpComboboxButtonStyle); + if (AllSettings::GetLayoutRTL()) + gtk_style_context_set_junction_sides(mpComboboxButtonStyle, GTK_JUNCTION_RIGHT); + else + gtk_style_context_set_junction_sides(mpComboboxButtonStyle, GTK_JUNCTION_LEFT); + render_common(mpComboboxButtonStyle, cr, aButtonRect, flags); + gtk_style_context_set_junction_sides(mpComboboxButtonStyle, eJuncSides); + + gtk_render_arrow(mpComboboxButtonArrowStyle, cr, + G_PI, + (arrowRect.Left() - areaRect.Left()), (arrowRect.Top() - areaRect.Top()), + arrowRect.GetWidth() ); + } + else if (nType == ControlType::Listbox) + { + if( nPart == ControlPart::ListboxWindow ) + { + /* render the popup window with the menu style */ + gtk_render_frame(mpMenuStyle, cr, + 0, 0, + areaRect.GetWidth(), areaRect.GetHeight()); + } + else + { + render_common(mpListboxStyle, cr, aRect, flags); + render_common(mpListboxBoxStyle, cr, aRect, flags); + + render_common(mpListboxButtonStyle, cr, aRect, flags); + + gtk_render_arrow(mpListboxButtonArrowStyle, cr, + G_PI, + (arrowRect.Left() - areaRect.Left()), (arrowRect.Top() - areaRect.Top()), + arrowRect.GetWidth() ); + } + } +} + +static void appendComboEntry(GtkWidgetPath* pSiblingsPath, gtk_widget_path_iter_set_object_nameFunc set_object_name) +{ + gtk_widget_path_append_type(pSiblingsPath, GTK_TYPE_ENTRY); + set_object_name(pSiblingsPath, -1, "entry"); + gtk_widget_path_iter_add_class(pSiblingsPath, -1, "combo"); +} + +static void appendComboButton(GtkWidgetPath* pSiblingsPath, gtk_widget_path_iter_set_object_nameFunc set_object_name) +{ + gtk_widget_path_append_type(pSiblingsPath, GTK_TYPE_BUTTON); + set_object_name(pSiblingsPath, -1, "button"); + gtk_widget_path_iter_add_class(pSiblingsPath, -1, "combo"); +} + +static GtkWidgetPath* buildLTRComboSiblingsPath(gtk_widget_path_iter_set_object_nameFunc set_object_name) +{ + GtkWidgetPath* pSiblingsPath = gtk_widget_path_new(); + + appendComboEntry(pSiblingsPath, set_object_name); + appendComboButton(pSiblingsPath, set_object_name); + + return pSiblingsPath; +} + +static GtkWidgetPath* buildRTLComboSiblingsPath(gtk_widget_path_iter_set_object_nameFunc set_object_name) +{ + GtkWidgetPath* pSiblingsPath = gtk_widget_path_new(); + + appendComboButton(pSiblingsPath, set_object_name); + appendComboEntry(pSiblingsPath, set_object_name); + + return pSiblingsPath; +} + +GtkStyleContext* GtkSalGraphics::makeContext(GtkWidgetPath *pPath, GtkStyleContext *pParent) +{ + GtkStyleContext* context = gtk_style_context_new(); + gtk_style_context_set_screen(context, gtk_widget_get_screen(mpWindow)); + gtk_style_context_set_path(context, pPath); + if (pParent == nullptr) + { + GtkWidget* pTopLevel = gtk_widget_get_toplevel(mpWindow); + GtkStyleContext* pStyle = gtk_widget_get_style_context(pTopLevel); + gtk_style_context_set_parent(context, pStyle); + gtk_style_context_set_scale (context, gtk_style_context_get_scale (pStyle)); + } + else + { + gtk_style_context_set_parent(context, pParent); + gtk_style_context_set_scale (context, gtk_style_context_get_scale (pParent)); + } + gtk_widget_path_unref(pPath); + return context; +} + +GtkStyleContext* GtkSalGraphics::createNewContext(GtkControlPart ePart, gtk_widget_path_iter_set_object_nameFunc set_object_name) +{ + switch (ePart) + { + case GtkControlPart::ToplevelWindow: + { + GtkWidgetPath *path = gtk_widget_path_new(); + gtk_widget_path_append_type(path, G_TYPE_NONE); + set_object_name(path, -1, "window"); + gtk_widget_path_iter_add_class(path, -1, "background"); + return makeContext(path, nullptr); + } + case GtkControlPart::Button: + { + GtkWidgetPath *path = gtk_widget_path_new(); + gtk_widget_path_append_type(path, GTK_TYPE_BUTTON); + set_object_name(path, -1, "button"); + return makeContext(path, nullptr); + } + case GtkControlPart::LinkButton: + { + GtkWidgetPath *path = gtk_widget_path_new(); + gtk_widget_path_append_type(path, GTK_TYPE_BUTTON); + set_object_name(path, -1, "button"); + gtk_widget_path_iter_add_class(path, -1, "link"); + return makeContext(path, nullptr); + } + case GtkControlPart::CheckButton: + { + GtkWidgetPath *path = gtk_widget_path_new(); + gtk_widget_path_append_type(path, GTK_TYPE_CHECK_BUTTON); + set_object_name(path, -1, "checkbutton"); + return makeContext(path, nullptr); + } + case GtkControlPart::CheckButtonCheck: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpCheckButtonStyle)); + gtk_widget_path_append_type(path, GTK_TYPE_CHECK_BUTTON); + set_object_name(path, -1, "check"); + return makeContext(path, mpCheckButtonStyle); + } + case GtkControlPart::RadioButton: + { + GtkWidgetPath *path = gtk_widget_path_new(); + gtk_widget_path_append_type(path, GTK_TYPE_RADIO_BUTTON); + set_object_name(path, -1, "radiobutton"); + return makeContext(path, nullptr); + } + case GtkControlPart::RadioButtonRadio: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpRadioButtonStyle)); + gtk_widget_path_append_type(path, GTK_TYPE_RADIO_BUTTON); + set_object_name(path, -1, "radio"); + return makeContext(path, mpRadioButtonStyle); + } + case GtkControlPart::ComboboxBoxButtonBoxArrow: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpComboboxButtonBoxStyle)); + gtk_widget_path_append_type(path, GTK_TYPE_RADIO_BUTTON); + gtk_widget_path_append_type(path, GTK_TYPE_BUTTON); + set_object_name(path, -1, "arrow"); + return makeContext(path, mpComboboxButtonBoxStyle); + } + case GtkControlPart::ListboxBoxButtonBoxArrow: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpListboxButtonBoxStyle)); + gtk_widget_path_append_type(path, GTK_TYPE_RADIO_BUTTON); + gtk_widget_path_append_type(path, GTK_TYPE_BUTTON); + set_object_name(path, -1, "arrow"); + return makeContext(path, mpListboxButtonBoxStyle); + } + case GtkControlPart::Entry: + { + GtkWidgetPath *path = gtk_widget_path_new(); + gtk_widget_path_append_type(path, GTK_TYPE_ENTRY); + set_object_name(path, -1, "entry"); + return makeContext(path, nullptr); + } + case GtkControlPart::Combobox: + case GtkControlPart::Listbox: + { + GtkWidgetPath *path = gtk_widget_path_new(); + gtk_widget_path_append_type(path, G_TYPE_NONE); + set_object_name(path, -1, "combobox"); + return makeContext(path, nullptr); + } + case GtkControlPart::ComboboxBox: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpComboboxStyle)); + gtk_widget_path_append_type(path, G_TYPE_NONE); + set_object_name(path, -1, "box"); + gtk_widget_path_iter_add_class(path, -1, "horizontal"); + gtk_widget_path_iter_add_class(path, -1, "linked"); + return makeContext(path, mpComboboxStyle); + } + case GtkControlPart::ListboxBox: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpListboxStyle)); + gtk_widget_path_append_type(path, G_TYPE_NONE); + set_object_name(path, -1, "box"); + gtk_widget_path_iter_add_class(path, -1, "horizontal"); + gtk_widget_path_iter_add_class(path, -1, "linked"); + return makeContext(path, mpListboxStyle); + } + case GtkControlPart::ComboboxBoxEntry: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpComboboxBoxStyle)); + GtkWidgetPath* pSiblingsPath; + if (AllSettings::GetLayoutRTL()) + { + pSiblingsPath = buildRTLComboSiblingsPath(set_object_name); + gtk_widget_path_append_with_siblings(path, pSiblingsPath, 1); + } + else + { + pSiblingsPath = buildLTRComboSiblingsPath(set_object_name); + gtk_widget_path_append_with_siblings(path, pSiblingsPath, 0); + } + gtk_widget_path_unref(pSiblingsPath); + return makeContext(path, mpComboboxBoxStyle); + } + case GtkControlPart::ComboboxBoxButton: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpComboboxBoxStyle)); + GtkWidgetPath* pSiblingsPath; + if (AllSettings::GetLayoutRTL()) + { + pSiblingsPath = buildRTLComboSiblingsPath(set_object_name); + gtk_widget_path_append_with_siblings(path, pSiblingsPath, 0); + } + else + { + pSiblingsPath = buildLTRComboSiblingsPath(set_object_name); + gtk_widget_path_append_with_siblings(path, pSiblingsPath, 1); + } + gtk_widget_path_unref(pSiblingsPath); + return makeContext(path, mpComboboxBoxStyle); + } + case GtkControlPart::ListboxBoxButton: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpListboxBoxStyle)); + GtkWidgetPath* pSiblingsPath = gtk_widget_path_new(); + + gtk_widget_path_append_type(pSiblingsPath, GTK_TYPE_BUTTON); + set_object_name(pSiblingsPath, -1, "button"); + gtk_widget_path_iter_add_class(pSiblingsPath, -1, "combo"); + + gtk_widget_path_append_with_siblings(path, pSiblingsPath, 0); + gtk_widget_path_unref(pSiblingsPath); + return makeContext(path, mpListboxBoxStyle); + } + case GtkControlPart::ComboboxBoxButtonBox: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpComboboxButtonStyle)); + gtk_widget_path_append_type(path, G_TYPE_NONE); + set_object_name(path, -1, "box"); + gtk_widget_path_iter_add_class(path, -1, "horizontal"); + return makeContext(path, mpComboboxButtonStyle); + } + case GtkControlPart::ListboxBoxButtonBox: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpListboxButtonStyle)); + gtk_widget_path_append_type(path, G_TYPE_NONE); + set_object_name(path, -1, "box"); + gtk_widget_path_iter_add_class(path, -1, "horizontal"); + return makeContext(path, mpListboxButtonStyle); + } + case GtkControlPart::SpinButton: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpWindowStyle)); + gtk_widget_path_append_type(path, GTK_TYPE_SPIN_BUTTON); + set_object_name(path, -1, "spinbutton"); + gtk_widget_path_iter_add_class(path, -1, GTK_STYLE_CLASS_HORIZONTAL); + return makeContext(path, mpWindowStyle); + } + case GtkControlPart::SpinButtonEntry: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpSpinStyle)); + gtk_widget_path_append_type(path, G_TYPE_NONE); + set_object_name(path, -1, "entry"); + return makeContext(path, mpSpinStyle); + } + case GtkControlPart::SpinButtonUpButton: + case GtkControlPart::SpinButtonDownButton: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpSpinStyle)); + gtk_widget_path_append_type(path, GTK_TYPE_SPIN_BUTTON); + set_object_name(path, -1, "button"); + gtk_widget_path_iter_add_class(path, -1, ePart == GtkControlPart::SpinButtonUpButton ? "up" : "down"); + return makeContext(path, mpSpinStyle); + } + case GtkControlPart::ScrollbarVertical: + case GtkControlPart::ScrollbarHorizontal: + { + GtkWidgetPath *path = gtk_widget_path_new(); + gtk_widget_path_append_type(path, GTK_TYPE_SCROLLBAR); + set_object_name(path, -1, "scrollbar"); + gtk_widget_path_iter_add_class(path, -1, ePart == GtkControlPart::ScrollbarVertical ? "vertical" : "horizontal"); + return makeContext(path, nullptr); + } + case GtkControlPart::ScrollbarVerticalContents: + case GtkControlPart::ScrollbarHorizontalContents: + { + GtkStyleContext *pParent = + (ePart == GtkControlPart::ScrollbarVerticalContents) ? mpVScrollbarStyle : mpHScrollbarStyle; + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(pParent)); + gtk_widget_path_append_type(path, GTK_TYPE_SCROLLBAR); + set_object_name(path, -1, "contents"); + return makeContext(path, pParent); + } + case GtkControlPart::ScrollbarVerticalTrough: + case GtkControlPart::ScrollbarHorizontalTrough: + { + GtkStyleContext *pParent = + (ePart == GtkControlPart::ScrollbarVerticalTrough) ? mpVScrollbarContentsStyle : mpHScrollbarContentsStyle; + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(pParent)); + gtk_widget_path_append_type(path, GTK_TYPE_SCROLLBAR); + set_object_name(path, -1, "trough"); + return makeContext(path, pParent); + } + case GtkControlPart::ScrollbarVerticalSlider: + case GtkControlPart::ScrollbarHorizontalSlider: + { + GtkStyleContext *pParent = + (ePart == GtkControlPart::ScrollbarVerticalSlider) ? mpVScrollbarTroughStyle : mpHScrollbarTroughStyle; + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(pParent)); + gtk_widget_path_append_type(path, GTK_TYPE_SCROLLBAR); + set_object_name(path, -1, "slider"); + return makeContext(path, pParent); + } + case GtkControlPart::ScrollbarVerticalButton: + case GtkControlPart::ScrollbarHorizontalButton: + { + GtkStyleContext *pParent = + (ePart == GtkControlPart::ScrollbarVerticalButton) ? mpVScrollbarStyle : mpHScrollbarStyle; + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(pParent)); + gtk_widget_path_append_type(path, GTK_TYPE_SCROLLBAR); + set_object_name(path, -1, "button"); + return makeContext(path, pParent); + } + case GtkControlPart::ProgressBar: + { + GtkWidgetPath *path = gtk_widget_path_new(); + gtk_widget_path_append_type(path, GTK_TYPE_PROGRESS_BAR); + set_object_name(path, -1, "progressbar"); + gtk_widget_path_iter_add_class(path, -1, GTK_STYLE_CLASS_HORIZONTAL); + return makeContext(path, nullptr); + } + case GtkControlPart::ProgressBarTrough: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpProgressBarStyle)); + gtk_widget_path_append_type(path, GTK_TYPE_PROGRESS_BAR); + set_object_name(path, -1, "trough"); + return makeContext(path, mpProgressBarStyle); + } + case GtkControlPart::ProgressBarProgress: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpProgressBarTroughStyle)); + gtk_widget_path_append_type(path, GTK_TYPE_PROGRESS_BAR); + set_object_name(path, -1, "progress"); + return makeContext(path, mpProgressBarTroughStyle); + } + case GtkControlPart::Notebook: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpWindowStyle)); + gtk_widget_path_append_type(path, GTK_TYPE_NOTEBOOK); + set_object_name(path, -1, "notebook"); + return makeContext(path, mpWindowStyle); + } + case GtkControlPart::NotebookStack: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpNotebookStyle)); + gtk_widget_path_append_type(path, GTK_TYPE_NOTEBOOK); + set_object_name(path, -1, "stack"); + return makeContext(path, mpNotebookStyle); + } + case GtkControlPart::NotebookHeader: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpNotebookStyle)); + gtk_widget_path_append_type(path, GTK_TYPE_NOTEBOOK); + set_object_name(path, -1, "header"); + gtk_widget_path_iter_add_class(path, -1, "frame"); + gtk_widget_path_iter_add_class(path, -1, "top"); + return makeContext(path, mpNotebookStyle); + } + case GtkControlPart::NotebookHeaderTabs: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpNotebookHeaderStyle)); + gtk_widget_path_append_type(path, GTK_TYPE_NOTEBOOK); + set_object_name(path, -1, "tabs"); + gtk_widget_path_iter_add_class(path, -1, "top"); + return makeContext(path, mpNotebookHeaderStyle); + } + case GtkControlPart::NotebookHeaderTabsTab: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpNotebookHeaderTabsStyle)); + gtk_widget_path_append_type(path, GTK_TYPE_NOTEBOOK); + set_object_name(path, -1, "tab"); + gtk_widget_path_iter_add_class(path, -1, "top"); + return makeContext(path, mpNotebookHeaderTabsStyle); + } + case GtkControlPart::NotebookHeaderTabsTabLabel: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpNotebookHeaderTabsTabStyle)); + gtk_widget_path_append_type(path, G_TYPE_NONE); + set_object_name(path, -1, "label"); + return makeContext(path, mpNotebookHeaderTabsTabStyle); + } + case GtkControlPart::NotebookHeaderTabsTabActiveLabel: + case GtkControlPart::NotebookHeaderTabsTabHoverLabel: + return mpNotebookHeaderTabsTabLabelStyle; + case GtkControlPart::FrameBorder: + { + GtkWidgetPath *path = gtk_widget_path_new(); + gtk_widget_path_append_type(path, GTK_TYPE_FRAME); + set_object_name(path, -1, "frame"); + gtk_widget_path_iter_add_class(path, -1, "frame"); + return makeContext(path, nullptr); + } + case GtkControlPart::MenuBar: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpWindowStyle)); + gtk_widget_path_append_type(path, GTK_TYPE_MENU_BAR); + set_object_name(path, -1, "menubar"); + return makeContext(path, mpWindowStyle); + } + case GtkControlPart::MenuBarItem: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpMenuBarStyle)); + gtk_widget_path_append_type(path, GTK_TYPE_MENU_ITEM); + set_object_name(path, -1, "menuitem"); + return makeContext(path, mpMenuBarStyle); + } + case GtkControlPart::MenuWindow: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpMenuBarItemStyle)); + gtk_widget_path_append_type(path, G_TYPE_NONE); + set_object_name(path, -1, "window"); + gtk_widget_path_iter_add_class(path, -1, "background"); + gtk_widget_path_iter_add_class(path, -1, "popup"); + return makeContext(path, mpMenuBarItemStyle); + } + case GtkControlPart::Menu: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpMenuWindowStyle)); + gtk_widget_path_append_type(path, GTK_TYPE_MENU); + set_object_name(path, -1, "menu"); + return makeContext(path, mpMenuWindowStyle); + } + case GtkControlPart::MenuItem: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpMenuStyle)); + gtk_widget_path_append_type(path, GTK_TYPE_MENU_ITEM); + set_object_name(path, -1, "menuitem"); + return makeContext(path, mpMenuStyle); + } + case GtkControlPart::MenuItemLabel: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpMenuItemStyle)); + gtk_widget_path_append_type(path, G_TYPE_NONE); + set_object_name(path, -1, "label"); + return makeContext(path, mpMenuItemStyle); + } + case GtkControlPart::MenuItemArrow: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpMenuItemStyle)); + gtk_widget_path_append_type(path, GTK_TYPE_MENU_ITEM); + set_object_name(path, -1, "arrow"); + return makeContext(path, mpMenuItemStyle); + } + case GtkControlPart::CheckMenuItem: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpMenuStyle)); + gtk_widget_path_append_type(path, GTK_TYPE_CHECK_MENU_ITEM); + set_object_name(path, -1, "menuitem"); + return makeContext(path, mpMenuStyle); + } + case GtkControlPart::CheckMenuItemCheck: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpCheckMenuItemStyle)); + gtk_widget_path_append_type(path, GTK_TYPE_CHECK_MENU_ITEM); + set_object_name(path, -1, "check"); + return makeContext(path, mpCheckMenuItemStyle); + } + case GtkControlPart::RadioMenuItem: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpMenuStyle)); + gtk_widget_path_append_type(path, GTK_TYPE_RADIO_MENU_ITEM); + set_object_name(path, -1, "menuitem"); + return makeContext(path, mpMenuStyle); + } + case GtkControlPart::RadioMenuItemRadio: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpRadioMenuItemStyle)); + gtk_widget_path_append_type(path, GTK_TYPE_RADIO_MENU_ITEM); + set_object_name(path, -1, "radio"); + return makeContext(path, mpRadioMenuItemStyle); + } + case GtkControlPart::SeparatorMenuItem: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpMenuStyle)); + gtk_widget_path_append_type(path, GTK_TYPE_SEPARATOR_MENU_ITEM); + set_object_name(path, -1, "menuitem"); + return makeContext(path, mpMenuStyle); + } + case GtkControlPart::SeparatorMenuItemSeparator: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpSeparatorMenuItemStyle)); + gtk_widget_path_append_type(path, GTK_TYPE_SEPARATOR_MENU_ITEM); + set_object_name(path, -1, "separator"); + return makeContext(path, mpSeparatorMenuItemStyle); + } + } + + return nullptr; +} + +#ifndef GTK_STYLE_CLASS_POPUP +#define GTK_STYLE_CLASS_POPUP "popup" +#endif +#ifndef GTK_STYLE_CLASS_LABEL +#define GTK_STYLE_CLASS_LABEL "label" +#endif + +GtkStyleContext* GtkSalGraphics::createOldContext(GtkControlPart ePart) +{ + switch (ePart) + { + case GtkControlPart::ToplevelWindow: + { + GtkWidgetPath *path = gtk_widget_path_new(); + gtk_widget_path_append_type(path, GTK_TYPE_WINDOW); + gtk_widget_path_iter_add_class(path, -1, "background"); + return makeContext(path, nullptr); + } + case GtkControlPart::Button: + { + GtkWidgetPath *path = gtk_widget_path_new(); + gtk_widget_path_append_type(path, GTK_TYPE_BUTTON); + gtk_widget_path_iter_add_class(path, -1, GTK_STYLE_CLASS_BUTTON); + gtk_widget_path_iter_add_class(path, -1, "button"); + gtk_widget_path_iter_add_class(path, -1, "text-button"); + return makeContext(path, nullptr); + } + case GtkControlPart::LinkButton: + { + GtkWidgetPath *path = gtk_widget_path_new(); + gtk_widget_path_append_type(path, GTK_TYPE_LINK_BUTTON); + gtk_widget_path_iter_add_class(path, -1, "text-button"); + return makeContext(path, nullptr); + } + case GtkControlPart::CheckButton: + { + GtkWidgetPath *path = gtk_widget_path_new(); + gtk_widget_path_append_type(path, GTK_TYPE_CHECK_BUTTON); + gtk_widget_path_iter_add_class(path, -1, GTK_STYLE_CLASS_CHECK); + gtk_widget_path_iter_add_class(path, -1, "text-button"); + return makeContext(path, nullptr); + } + case GtkControlPart::CheckButtonCheck: + return mpCheckButtonStyle; + case GtkControlPart::RadioButton: + { + GtkWidgetPath *path = gtk_widget_path_new(); + gtk_widget_path_append_type(path, GTK_TYPE_RADIO_BUTTON); + gtk_widget_path_iter_add_class(path, -1, GTK_STYLE_CLASS_RADIO); + gtk_widget_path_iter_add_class(path, -1, "text-button"); + return makeContext(path, nullptr); + } + case GtkControlPart::RadioButtonRadio: + return mpRadioButtonStyle; + case GtkControlPart::ComboboxBoxButtonBoxArrow: + case GtkControlPart::ListboxBoxButtonBoxArrow: + { + return (ePart == GtkControlPart::ComboboxBoxButtonBoxArrow) + ? mpComboboxButtonStyle : mpListboxButtonStyle; + } + case GtkControlPart::Entry: + case GtkControlPart::ComboboxBoxEntry: + case GtkControlPart::SpinButtonEntry: + { + GtkWidgetPath *path = gtk_widget_path_new(); + gtk_widget_path_append_type(path, GTK_TYPE_ENTRY); + gtk_widget_path_iter_add_class(path, -1, GTK_STYLE_CLASS_ENTRY); + return makeContext(path, nullptr); + } + case GtkControlPart::Combobox: + { + GtkWidgetPath *path = gtk_widget_path_new(); + gtk_widget_path_append_type(path, GTK_TYPE_COMBO_BOX_TEXT); + return makeContext(path, nullptr); + } + case GtkControlPart::Listbox: + { + GtkWidgetPath *path = gtk_widget_path_new(); + gtk_widget_path_append_type(path, GTK_TYPE_COMBO_BOX); + return makeContext(path, nullptr); + } + case GtkControlPart::ComboboxBoxButton: + case GtkControlPart::ListboxBoxButton: + { + GtkStyleContext *pParent = + (ePart == GtkControlPart::ComboboxBoxButton ) ? mpComboboxStyle : mpListboxStyle; + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(pParent)); + gtk_widget_path_append_type(path, GTK_TYPE_TOGGLE_BUTTON); + gtk_widget_path_iter_add_class(path, -1, GTK_STYLE_CLASS_BUTTON); + gtk_widget_path_iter_add_class(path, -1, "the-button-in-the-combobox"); + return makeContext(path, pParent); + } + case GtkControlPart::SpinButton: + { + GtkWidgetPath *path = gtk_widget_path_new(); + gtk_widget_path_append_type(path, GTK_TYPE_SPIN_BUTTON); + gtk_widget_path_iter_add_class(path, -1, GTK_STYLE_CLASS_ENTRY); + gtk_widget_path_iter_add_class(path, -1, GTK_STYLE_CLASS_HORIZONTAL); + gtk_widget_path_iter_add_class(path, -1, GTK_STYLE_CLASS_SPINBUTTON); + return makeContext(path, nullptr); + } + case GtkControlPart::SpinButtonUpButton: + case GtkControlPart::SpinButtonDownButton: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpSpinStyle)); + gtk_widget_path_append_type(path, GTK_TYPE_SPIN_BUTTON); + gtk_widget_path_iter_add_class(path, -1, GTK_STYLE_CLASS_SPINBUTTON); + gtk_widget_path_iter_add_class(path, -1, GTK_STYLE_CLASS_BUTTON); + return makeContext(path, mpSpinStyle); + } + case GtkControlPart::ScrollbarVertical: + case GtkControlPart::ScrollbarHorizontal: + { + GtkWidgetPath *path = gtk_widget_path_new(); + gtk_widget_path_append_type(path, GTK_TYPE_SCROLLBAR); + gtk_widget_path_iter_add_class(path, -1, GTK_STYLE_CLASS_SCROLLBAR); + gtk_widget_path_iter_add_class(path, -1, ePart == GtkControlPart::ScrollbarVertical ? "vertical" : "horizontal"); + return makeContext(path, nullptr); + } + case GtkControlPart::ScrollbarVerticalContents: + case GtkControlPart::ScrollbarHorizontalContents: + { + GtkStyleContext *pParent = + (ePart == GtkControlPart::ScrollbarVerticalContents) ? mpVScrollbarStyle : mpHScrollbarStyle; + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(pParent)); + gtk_widget_path_append_type(path, GTK_TYPE_SCROLLBAR); + gtk_widget_path_iter_add_class(path, -1, GTK_STYLE_CLASS_SCROLLBAR); + gtk_widget_path_iter_add_class(path, -1, "contents"); + return makeContext(path, pParent); + } + case GtkControlPart::ScrollbarHorizontalTrough: + case GtkControlPart::ScrollbarVerticalTrough: + { + GtkStyleContext *pParent = + (ePart == GtkControlPart::ScrollbarVerticalTrough) ? mpVScrollbarContentsStyle : mpHScrollbarContentsStyle; + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(pParent)); + gtk_widget_path_append_type(path, GTK_TYPE_SCROLLBAR); + gtk_widget_path_iter_add_class(path, -1, GTK_STYLE_CLASS_SCROLLBAR); + gtk_widget_path_iter_add_class(path, -1, GTK_STYLE_CLASS_TROUGH); + return makeContext(path, pParent); + } + case GtkControlPart::ScrollbarHorizontalSlider: + case GtkControlPart::ScrollbarVerticalSlider: + { + GtkStyleContext *pParent = + (ePart == GtkControlPart::ScrollbarVerticalSlider) ? mpVScrollbarContentsStyle : mpHScrollbarContentsStyle; + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(pParent)); + gtk_widget_path_append_type(path, GTK_TYPE_SCROLLBAR); + gtk_widget_path_iter_add_class(path, -1, GTK_STYLE_CLASS_SCROLLBAR); + gtk_widget_path_iter_add_class(path, -1, GTK_STYLE_CLASS_SLIDER); + return makeContext(path, pParent); + } + case GtkControlPart::ScrollbarHorizontalButton: + case GtkControlPart::ScrollbarVerticalButton: + { + GtkStyleContext *pParent = + (ePart == GtkControlPart::ScrollbarVerticalButton) ? mpVScrollbarStyle : mpHScrollbarStyle; + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(pParent)); + gtk_widget_path_append_type(path, GTK_TYPE_SCROLLBAR); + gtk_widget_path_iter_add_class(path, -1, GTK_STYLE_CLASS_SCROLLBAR); + gtk_widget_path_iter_add_class(path, -1, GTK_STYLE_CLASS_BUTTON); + return makeContext(path, pParent); + } + case GtkControlPart::ProgressBar: + { + GtkWidgetPath *path = gtk_widget_path_new(); + gtk_widget_path_append_type(path, GTK_TYPE_PROGRESS_BAR); + gtk_widget_path_iter_add_class(path, -1, GTK_STYLE_CLASS_PROGRESSBAR); + gtk_widget_path_iter_add_class(path, -1, GTK_STYLE_CLASS_HORIZONTAL); + return makeContext(path, nullptr); + } + case GtkControlPart::ProgressBarTrough: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpProgressBarStyle)); + gtk_widget_path_append_type(path, GTK_TYPE_PROGRESS_BAR); + gtk_widget_path_iter_add_class(path, -1, GTK_STYLE_CLASS_TROUGH); + return makeContext(path, mpProgressBarStyle); + } + case GtkControlPart::ProgressBarProgress: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpProgressBarTroughStyle)); + gtk_widget_path_append_type(path, GTK_TYPE_PROGRESS_BAR); + gtk_widget_path_iter_add_class(path, -1, GTK_STYLE_CLASS_PROGRESSBAR); + return makeContext(path, mpProgressBarTroughStyle); + } + case GtkControlPart::Notebook: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpWindowStyle)); + gtk_widget_path_append_type(path, GTK_TYPE_NOTEBOOK); + gtk_widget_path_iter_add_class(path, -1, GTK_STYLE_CLASS_NOTEBOOK); + gtk_widget_path_iter_add_class(path, -1, "frame"); + return makeContext(path, mpWindowStyle); + } + case GtkControlPart::NotebookStack: + return mpNotebookStyle; + case GtkControlPart::NotebookHeader: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpNotebookStyle)); + gtk_widget_path_iter_add_class(path, -1, GTK_STYLE_CLASS_HEADER); + gtk_widget_path_iter_add_class(path, -1, "top"); + return makeContext(path, gtk_style_context_get_parent(mpNotebookStyle)); + } + case GtkControlPart::NotebookHeaderTabs: + return mpNotebookHeaderStyle; + case GtkControlPart::NotebookHeaderTabsTab: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpNotebookHeaderTabsStyle)); + gtk_widget_path_append_type(path, GTK_TYPE_NOTEBOOK); + gtk_widget_path_iter_add_class(path, -1, GTK_STYLE_CLASS_HEADER); + gtk_widget_path_iter_add_class(path, -1, "top"); + gtk_widget_path_iter_add_region(path, -1, GTK_STYLE_REGION_TAB, static_cast<GtkRegionFlags>(GTK_REGION_EVEN | GTK_REGION_FIRST)); + return makeContext(path, mpNotebookHeaderTabsStyle); + } + case GtkControlPart::NotebookHeaderTabsTabLabel: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpNotebookHeaderTabsTabStyle)); + gtk_widget_path_append_type(path, GTK_TYPE_LABEL); + gtk_widget_path_iter_add_class(path, -1, GTK_STYLE_CLASS_LABEL); + return makeContext(path, mpNotebookHeaderTabsTabStyle); + } + case GtkControlPart::NotebookHeaderTabsTabActiveLabel: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpNotebookHeaderTabsTabStyle)); + gtk_widget_path_append_type(path, GTK_TYPE_LABEL); + gtk_widget_path_iter_add_class(path, -1, GTK_STYLE_CLASS_LABEL); + gtk_widget_path_iter_add_class(path, -1, "active-page"); + return makeContext(path, mpNotebookHeaderTabsTabStyle); + } + case GtkControlPart::NotebookHeaderTabsTabHoverLabel: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpNotebookHeaderTabsTabStyle)); + gtk_widget_path_append_type(path, GTK_TYPE_LABEL); + gtk_widget_path_iter_add_class(path, -1, GTK_STYLE_CLASS_LABEL); + gtk_widget_path_iter_add_class(path, -1, "prelight-page"); + return makeContext(path, mpNotebookHeaderTabsTabStyle); + } + case GtkControlPart::FrameBorder: + { + GtkWidgetPath *path = gtk_widget_path_new(); + gtk_widget_path_append_type(path, GTK_TYPE_FRAME); + gtk_widget_path_iter_add_class(path, -1, "frame"); + return makeContext(path, nullptr); + } + case GtkControlPart::MenuBar: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpWindowStyle)); + gtk_widget_path_append_type(path, GTK_TYPE_MENU_BAR); + gtk_widget_path_iter_add_class(path, -1, GTK_STYLE_CLASS_MENUBAR); + return makeContext(path, mpWindowStyle); + } + case GtkControlPart::MenuBarItem: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpMenuBarStyle)); + gtk_widget_path_append_type(path, GTK_TYPE_MENU_ITEM); + gtk_widget_path_iter_add_class(path, -1, GTK_STYLE_CLASS_MENUITEM); + return makeContext(path, mpMenuBarStyle); + } + case GtkControlPart::MenuWindow: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpMenuBarItemStyle)); + gtk_widget_path_append_type(path, GTK_TYPE_WINDOW); + gtk_widget_path_iter_add_class(path, -1, GTK_STYLE_CLASS_POPUP); + gtk_widget_path_iter_add_class(path, -1, "background"); + return makeContext(path, mpMenuBarItemStyle); + } + case GtkControlPart::Menu: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpMenuWindowStyle)); + gtk_widget_path_append_type(path, GTK_TYPE_MENU); + gtk_widget_path_iter_add_class(path, -1, GTK_STYLE_CLASS_MENU); + return makeContext(path, mpMenuWindowStyle); + } + case GtkControlPart::MenuItem: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpMenuStyle)); + gtk_widget_path_append_type(path, GTK_TYPE_MENU_ITEM); + gtk_widget_path_iter_add_class(path, -1, GTK_STYLE_CLASS_MENUITEM); + return makeContext(path, mpMenuStyle); + } + case GtkControlPart::MenuItemLabel: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpMenuItemStyle)); + gtk_widget_path_append_type(path, GTK_TYPE_LABEL); + return makeContext(path, mpMenuItemStyle); + } + case GtkControlPart::MenuItemArrow: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpMenuItemStyle)); + gtk_widget_path_append_type(path, GTK_TYPE_MENU_ITEM); + gtk_widget_path_iter_add_class(path, -1, GTK_STYLE_CLASS_ARROW); + return makeContext(path, mpMenuItemStyle); + } + case GtkControlPart::CheckMenuItem: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpMenuStyle)); + gtk_widget_path_append_type(path, GTK_TYPE_CHECK_MENU_ITEM); + gtk_widget_path_iter_add_class(path, -1, GTK_STYLE_CLASS_CHECK); + return makeContext(path, mpMenuStyle); + } + case GtkControlPart::CheckMenuItemCheck: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpCheckMenuItemStyle)); + gtk_widget_path_append_type(path, GTK_TYPE_CHECK_MENU_ITEM); + gtk_widget_path_iter_add_class(path, -1, GTK_STYLE_CLASS_CHECK); + return makeContext(path, mpCheckMenuItemStyle); + } + case GtkControlPart::RadioMenuItem: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpMenuStyle)); + gtk_widget_path_append_type(path, GTK_TYPE_RADIO_MENU_ITEM); + gtk_widget_path_iter_add_class(path, -1, GTK_STYLE_CLASS_MENUITEM); + return makeContext(path, mpMenuStyle); + } + case GtkControlPart::RadioMenuItemRadio: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpRadioMenuItemStyle)); + gtk_widget_path_append_type(path, GTK_TYPE_RADIO_MENU_ITEM); + gtk_widget_path_iter_add_class(path, -1, GTK_STYLE_CLASS_RADIO); + return makeContext(path, mpRadioMenuItemStyle); + } + case GtkControlPart::SeparatorMenuItem: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpMenuStyle)); + gtk_widget_path_append_type(path, GTK_TYPE_SEPARATOR_MENU_ITEM); + gtk_widget_path_iter_add_class(path, -1, GTK_STYLE_CLASS_MENUITEM); + return makeContext(path, mpMenuStyle); + } + case GtkControlPart::SeparatorMenuItemSeparator: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpSeparatorMenuItemStyle)); + gtk_widget_path_append_type(path, GTK_TYPE_SEPARATOR_MENU_ITEM); + gtk_widget_path_iter_add_class(path, -1, GTK_STYLE_CLASS_SEPARATOR); + return makeContext(path, mpSeparatorMenuItemStyle); + } + case GtkControlPart::ComboboxBox: + case GtkControlPart::ListboxBox: + case GtkControlPart::ComboboxBoxButtonBox: + case GtkControlPart::ListboxBoxButtonBox: + return nullptr; + default: + break; + } + + return mpButtonStyle; +} + +GtkStyleContext* GtkSalGraphics::createStyleContext(gtk_widget_path_iter_set_object_nameFunc set_object_name, + GtkControlPart ePart) +{ + if (set_object_name) + return createNewContext(ePart, set_object_name); + return createOldContext(ePart); +} + +namespace +{ + GtkStateFlags ACTIVE_TAB() + { +#if GTK_CHECK_VERSION(3,20,0) + if (gtk_check_version(3, 20, 0) == nullptr) + return GTK_STATE_FLAG_CHECKED; +#endif + return GTK_STATE_FLAG_ACTIVE; + } +} + +void GtkSalGraphics::PaintCheckOrRadio(cairo_t *cr, GtkStyleContext *context, + const tools::Rectangle& rControlRectangle, bool bIsCheck, bool bInMenu) +{ + gint indicator_size; + gtk_style_context_get_style(context, "indicator-size", &indicator_size, nullptr); + + gint x = (rControlRectangle.GetWidth() - indicator_size) / 2; + gint y = (rControlRectangle.GetHeight() - indicator_size) / 2; + + if (!bInMenu) + gtk_render_background(context, cr, x, y, indicator_size, indicator_size); + + if (bIsCheck) + gtk_render_check(context, cr, x, y, indicator_size, indicator_size); + else + gtk_render_option(context, cr, x, y, indicator_size, indicator_size); + + gtk_render_frame(context, cr, x, y, indicator_size, indicator_size); +} + +void GtkSalGraphics::PaintCheck(cairo_t *cr, GtkStyleContext *context, + const tools::Rectangle& rControlRectangle, bool bInMenu) +{ + PaintCheckOrRadio(cr, context, rControlRectangle, true, bInMenu); +} + +void GtkSalGraphics::PaintRadio(cairo_t *cr, GtkStyleContext *context, + const tools::Rectangle& rControlRectangle, bool bInMenu) +{ + PaintCheckOrRadio(cr, context, rControlRectangle, false, bInMenu); +} + +static gfloat getArrowSize(GtkStyleContext* context) +{ + if (gtk_check_version(3, 20, 0) == nullptr) + { + gint min_width, min_weight; + gtk_style_context_get_style(context, "min-width", &min_width, nullptr); + gtk_style_context_get_style(context, "min-height", &min_weight, nullptr); + gfloat arrow_size = 11 * MAX (min_width, min_weight); + return arrow_size; + } + else + { + gfloat arrow_scaling = 1.0; + gtk_style_context_get_style(context, "arrow-scaling", &arrow_scaling, nullptr); + gfloat arrow_size = 11 * arrow_scaling; + return arrow_size; + } +} + +namespace +{ + void draw_vertical_separator(GtkStyleContext *context, cairo_t *cr, const tools::Rectangle& rControlRegion) + { + long nX = 0; + long nY = 0; + + const bool bNewStyle = gtk_check_version(3, 20, 0) == nullptr; + + gint nSeparatorWidth = 1; + + if (bNewStyle) + { + gtk_style_context_get(context, + gtk_style_context_get_state(context), + "min-width", &nSeparatorWidth, nullptr); + } + + gint nHalfSeparatorWidth = nSeparatorWidth / 2; + gint nHalfRegionWidth = rControlRegion.GetWidth() / 2; + + nX = nX + nHalfRegionWidth - nHalfSeparatorWidth; + nY = rControlRegion.GetHeight() > 5 ? 1 : 0; + int nHeight = rControlRegion.GetHeight() - (2 * nY); + + if (bNewStyle) + { + gtk_render_background(context, cr, nX, nY, nSeparatorWidth, nHeight); + gtk_render_frame(context, cr, nX, nY, nSeparatorWidth, nHeight); + } + else + { + gtk_render_line(context, cr, nX, nY, nX, nY + nHeight); + } + } + + void draw_horizontal_separator(GtkStyleContext *context, cairo_t *cr, const tools::Rectangle& rControlRegion) + { + long nX = 0; + long nY = 0; + + const bool bNewStyle = gtk_check_version(3, 20, 0) == nullptr; + + gint nSeparatorHeight = 1; + + if (bNewStyle) + { + gtk_style_context_get(context, + gtk_style_context_get_state(context), + "min-height", &nSeparatorHeight, nullptr); + } + + gint nHalfSeparatorHeight = nSeparatorHeight / 2; + gint nHalfRegionHeight = rControlRegion.GetHeight() / 2; + + nY = nY + nHalfRegionHeight - nHalfSeparatorHeight; + nX = rControlRegion.GetWidth() > 5 ? 1 : 0; + int nWidth = rControlRegion.GetWidth() - (2 * nX); + + if (bNewStyle) + { + gtk_render_background(context, cr, nX, nY, nWidth, nSeparatorHeight); + gtk_render_frame(context, cr, nX, nY, nWidth, nSeparatorHeight); + } + else + { + gtk_render_line(context, cr, nX, nY, nX + nWidth, nY); + } + } +} + +void GtkSalGraphics::handleDamage(const tools::Rectangle& rDamagedRegion) +{ + assert(m_pWidgetDraw); + assert(!rDamagedRegion.IsEmpty()); + mpFrame->damaged(rDamagedRegion.Left(), rDamagedRegion.Top(), rDamagedRegion.GetWidth(), rDamagedRegion.GetHeight()); +} + +bool GtkSalGraphics::drawNativeControl( ControlType nType, ControlPart nPart, const tools::Rectangle& rControlRegion, + ControlState nState, const ImplControlValue& rValue, + const OUString&, const Color& rBackgroundColor) +{ + RenderType renderType = nPart == ControlPart::Focus ? RenderType::Focus : RenderType::BackgroundAndFrame; + GtkStyleContext *context = nullptr; + const gchar *styleClass = nullptr; + GdkPixbuf *pixbuf = nullptr; + bool bInMenu = false; + + GtkStateFlags flags = NWConvertVCLStateToGTKState(nState); + + switch(nType) + { + case ControlType::Spinbox: + case ControlType::SpinButtons: + context = mpSpinStyle; + renderType = RenderType::Spinbutton; + break; + case ControlType::Editbox: + context = mpEntryStyle; + break; + case ControlType::MultilineEditbox: + context = mpTextViewStyle; + break; + case ControlType::Combobox: + context = mpComboboxStyle; + renderType = RenderType::Combobox; + break; + case ControlType::Listbox: + if (nPart == ControlPart::Focus) + { + renderType = RenderType::Focus; + context = mpListboxButtonStyle; + } + else + { + renderType = RenderType::Combobox; + context = mpListboxStyle; + } + break; + case ControlType::MenuPopup: + bInMenu = true; + + // map selected menu entries in vcl parlance to gtk prelight + if (nPart >= ControlPart::MenuItem && nPart <= ControlPart::SubmenuArrow && (nState & ControlState::SELECTED)) + flags = static_cast<GtkStateFlags>(flags | GTK_STATE_FLAG_PRELIGHT); + flags = static_cast<GtkStateFlags>(flags & ~GTK_STATE_FLAG_ACTIVE); + switch(nPart) + { + case ControlPart::MenuItem: + context = mpMenuItemStyle; + renderType = RenderType::BackgroundAndFrame; + break; + case ControlPart::MenuItemCheckMark: + if (gtk_check_version(3, 20, 0) == nullptr) + context = mpCheckMenuItemCheckStyle; + else + { + context = gtk_widget_get_style_context(gCheckMenuItemWidget); + styleClass = GTK_STYLE_CLASS_CHECK; + } + renderType = RenderType::Check; + nType = ControlType::Checkbox; + if (nState & ControlState::PRESSED) + { + flags = static_cast<GtkStateFlags>(flags | GTK_STATE_FLAG_CHECKED); + } + break; + case ControlPart::MenuItemRadioMark: + if (gtk_check_version(3, 20, 0) == nullptr) + context = mpRadioMenuItemRadioStyle; + else + { + context = gtk_widget_get_style_context(gCheckMenuItemWidget); + styleClass = GTK_STYLE_CLASS_RADIO; + } + renderType = RenderType::Radio; + nType = ControlType::Radiobutton; + if (nState & ControlState::PRESSED) + { + flags = static_cast<GtkStateFlags>(flags | GTK_STATE_FLAG_CHECKED); + } + break; + case ControlPart::Separator: + context = mpSeparatorMenuItemSeparatorStyle; + flags = GtkStateFlags(GTK_STATE_FLAG_BACKDROP | GTK_STATE_FLAG_INSENSITIVE); //GTK_STATE_FLAG_BACKDROP hack ? + renderType = RenderType::MenuSeparator; + break; + case ControlPart::SubmenuArrow: + if (gtk_check_version(3, 20, 0) == nullptr) + context = mpMenuItemArrowStyle; + else + { + context = gtk_widget_get_style_context(gCheckMenuItemWidget); + styleClass = GTK_STYLE_CLASS_ARROW; + } + renderType = RenderType::Arrow; + break; + case ControlPart::Entire: + context = mpMenuStyle; + renderType = RenderType::Background; + break; + default: break; + } + break; + case ControlType::Toolbar: + switch(nPart) + { + case ControlPart::DrawBackgroundHorz: + case ControlPart::DrawBackgroundVert: + context = mpToolbarStyle; + break; + case ControlPart::Button: + /* For all checkbuttons in the toolbars */ + flags = static_cast<GtkStateFlags>(flags | + ( (rValue.getTristateVal() == ButtonValue::On) ? GTK_STATE_FLAG_CHECKED : GTK_STATE_FLAG_NORMAL)); + context = mpToolButtonStyle; + break; + case ControlPart::SeparatorVert: + context = mpToolbarSeperatorStyle; + renderType = RenderType::ToolbarSeparator; + break; + default: + return false; + } + break; + case ControlType::Radiobutton: + flags = static_cast<GtkStateFlags>(flags | + ( (rValue.getTristateVal() == ButtonValue::On) ? GTK_STATE_FLAG_CHECKED : GTK_STATE_FLAG_NORMAL)); + context = mpRadioButtonRadioStyle; + renderType = nPart == ControlPart::Focus ? RenderType::Focus : RenderType::Radio; + break; + case ControlType::Checkbox: + flags = static_cast<GtkStateFlags>(flags | + ( (rValue.getTristateVal() == ButtonValue::On) ? GTK_STATE_FLAG_CHECKED : + (rValue.getTristateVal() == ButtonValue::Mixed) ? GTK_STATE_FLAG_INCONSISTENT : + GTK_STATE_FLAG_NORMAL)); + context = mpCheckButtonCheckStyle; + renderType = nPart == ControlPart::Focus ? RenderType::Focus : RenderType::Check; + break; + case ControlType::Pushbutton: + context = mpButtonStyle; + break; + case ControlType::Scrollbar: + switch(nPart) + { + case ControlPart::DrawBackgroundVert: + case ControlPart::DrawBackgroundHorz: + context = (nPart == ControlPart::DrawBackgroundVert) + ? mpVScrollbarStyle : mpHScrollbarStyle; + renderType = RenderType::Scrollbar; + break; + default: break; + } + break; + case ControlType::ListNet: + return true; + break; + case ControlType::TabPane: + context = mpNotebookStyle; + break; + case ControlType::TabBody: + context = mpNotebookStackStyle; + break; + case ControlType::TabHeader: + context = mpNotebookHeaderStyle; + break; + case ControlType::TabItem: + context = mpNotebookHeaderTabsTabStyle; + if (nState & ControlState::SELECTED) + flags = static_cast<GtkStateFlags>(flags | ACTIVE_TAB()); + renderType = RenderType::TabItem; + break; + case ControlType::WindowBackground: + context = gtk_widget_get_style_context(gtk_widget_get_toplevel(mpWindow)); + break; + case ControlType::Frame: + { + DrawFrameStyle nStyle = static_cast<DrawFrameStyle>(rValue.getNumericVal() & 0x0f); + if (nStyle == DrawFrameStyle::In) + context = mpFrameOutStyle; + else + context = mpFrameInStyle; + break; + } + case ControlType::Menubar: + if (nPart == ControlPart::MenuItem) + { + context = mpMenuBarItemStyle; + + flags = (!(nState & ControlState::ENABLED)) ? GTK_STATE_FLAG_INSENSITIVE : GTK_STATE_FLAG_NORMAL; + if (nState & ControlState::SELECTED) + flags = static_cast<GtkStateFlags>(flags | GTK_STATE_FLAG_PRELIGHT); + } + else + { + context = mpMenuBarStyle; + } + break; + case ControlType::Fixedline: + context = nPart == ControlPart::SeparatorHorz ? mpFixedHoriLineStyle : mpFixedVertLineStyle; + renderType = RenderType::Separator; + break; + case ControlType::ListNode: + { + context = mpTreeHeaderButtonStyle; + ButtonValue aButtonValue = rValue.getTristateVal(); + if (aButtonValue == ButtonValue::On) + flags = static_cast<GtkStateFlags>(flags | GTK_STATE_FLAG_CHECKED); + renderType = RenderType::Expander; + break; + } + case ControlType::ListHeader: + context = mpTreeHeaderButtonStyle; + if (nPart == ControlPart::Arrow) + { + const char* icon = (rValue.getNumericVal() & 1) ? "pan-down-symbolic" : "pan-up-symbolic"; + GtkIconTheme *pIconTheme = gtk_icon_theme_get_for_screen(gtk_widget_get_screen(mpWindow)); + pixbuf = gtk_icon_theme_load_icon_for_scale(pIconTheme, icon, + std::max(rControlRegion.GetWidth(), rControlRegion.GetHeight()), + gtk_style_context_get_scale (context), + static_cast<GtkIconLookupFlags>(0), nullptr); + flags = GTK_STATE_FLAG_SELECTED; + renderType = RenderType::Icon; + } + break; + case ControlType::Progress: + context = mpProgressBarProgressStyle; + renderType = RenderType::Progress; + break; + default: + return false; + } + + cairo_t *cr = getCairoContext(false); + clipRegion(cr); + cairo_translate(cr, rControlRegion.Left(), rControlRegion.Top()); + + long nX = 0; + long nY = 0; + long nWidth = rControlRegion.GetWidth(); + long nHeight = rControlRegion.GetHeight(); + + StyleContextSave aContextState; + aContextState.save(context); + style_context_set_state(context, flags); + + if (styleClass) + { + gtk_style_context_add_class(context, styleClass); + } + + // apply background in style, if explicitly set + // note: for more complex controls that use multiple styles for their elements, + // background may have to be applied for more of those as well (s. case RenderType::Combobox below) + GtkCssProvider* pBgCssProvider = nullptr; + if (rBackgroundColor != COL_AUTO) + { + const OUString sColorCss = "* { background-color: #" + rBackgroundColor.AsRGBHexString() + "; }"; + const OString aResult = OUStringToOString(sColorCss, RTL_TEXTENCODING_UTF8); + pBgCssProvider = gtk_css_provider_new(); + gtk_css_provider_load_from_data(pBgCssProvider, aResult.getStr(), aResult.getLength(), nullptr); + gtk_style_context_add_provider(context, GTK_STYLE_PROVIDER(pBgCssProvider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + } + + switch(renderType) + { + case RenderType::Background: + case RenderType::BackgroundAndFrame: + gtk_render_background(context, cr, nX, nY, nWidth, nHeight); + if (renderType == RenderType::BackgroundAndFrame) + { + gtk_render_frame(context, cr, nX, nY, nWidth, nHeight); + } + break; + case RenderType::Check: + { + PaintCheck(cr, context, rControlRegion, bInMenu); + break; + } + case RenderType::Radio: + { + PaintRadio(cr, context, rControlRegion, bInMenu); + break; + } + case RenderType::MenuSeparator: + gtk_render_line(context, cr, + 0, rControlRegion.GetHeight() / 2, + rControlRegion.GetWidth() - 1, rControlRegion.GetHeight() / 2); + break; + case RenderType::ToolbarSeparator: + { + draw_vertical_separator(context, cr, rControlRegion); + break; + } + case RenderType::Separator: + if (nPart == ControlPart::SeparatorHorz) + draw_horizontal_separator(context, cr, rControlRegion); + else + draw_vertical_separator(context, cr, rControlRegion); + break; + case RenderType::Arrow: + gtk_render_arrow(context, cr, + G_PI / 2, 0, 0, + MIN(rControlRegion.GetWidth(), 1 + rControlRegion.GetHeight())); + break; + case RenderType::Expander: + gtk_render_expander(context, cr, -2, -2, nWidth+4, nHeight+4); + break; + case RenderType::Scrollbar: + PaintScrollbar(context, cr, rControlRegion, nPart, rValue); + break; + case RenderType::Spinbutton: + PaintSpinButton(flags, cr, rControlRegion, nPart, rValue); + break; + case RenderType::Combobox: + if (pBgCssProvider) + { + gtk_style_context_add_provider(mpComboboxEntryStyle, GTK_STYLE_PROVIDER(pBgCssProvider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + } + PaintCombobox(flags, cr, rControlRegion, nType, nPart); + if (pBgCssProvider) + { + gtk_style_context_remove_provider(mpComboboxEntryStyle, GTK_STYLE_PROVIDER(pBgCssProvider)); + } + break; + case RenderType::Icon: + gtk_style_context_save (context); + gtk_style_context_set_scale (context, 1); + gtk_render_icon(context, cr, pixbuf, nX, nY); + gtk_style_context_restore (context); + g_object_unref(pixbuf); + break; + case RenderType::Focus: + { + if (nType == ControlType::Checkbox || + nType == ControlType::Radiobutton) + { + nX -= 2; nY -=2; + nHeight += 4; nWidth += 4; + } + else + { + GtkBorder border; + + gtk_style_context_get_border(context, flags, &border); + + nX += border.left; + nY += border.top; + nWidth -= border.left + border.right; + nHeight -= border.top + border.bottom; + } + + gtk_render_focus(context, cr, nX, nY, nWidth, nHeight); + + break; + } + case RenderType::Progress: + { + gtk_render_background(mpProgressBarTroughStyle, cr, nX, nY, nWidth, nHeight); + + long nProgressWidth = rValue.getNumericVal(); + if (nProgressWidth) + { + GtkBorder padding; + gtk_style_context_get_padding(context, gtk_style_context_get_state(context), &padding); + + nX += padding.left; + nY += padding.top; + nHeight -= (padding.top + padding.bottom); + nProgressWidth -= (padding.left + padding.right); + gtk_render_background(context, cr, nX, nY, nProgressWidth, nHeight); + gtk_render_frame(context, cr, nX, nY, nProgressWidth, nHeight); + } + + gtk_render_frame(mpProgressBarTroughStyle, cr, nX, nY, nWidth, nHeight); + + break; + } + case RenderType::TabItem: + { + if (gtk_check_version(3, 20, 0) != nullptr) + { + gint initial_gap(0); + gtk_style_context_get_style(mpNotebookStyle, + "initial-gap", &initial_gap, + nullptr); + + nX += initial_gap/2; + nWidth -= initial_gap; + } + tools::Rectangle aRect(Point(nX, nY), Size(nWidth, nHeight)); + render_common(mpNotebookHeaderTabsTabStyle, cr, aRect, flags); + break; + } + default: + break; + } + + if (styleClass) + { + gtk_style_context_remove_class(context, styleClass); + } + if (pBgCssProvider) + { + gtk_style_context_remove_provider(context, GTK_STYLE_PROVIDER(pBgCssProvider)); + } + aContextState.restore(); + + cairo_destroy(cr); // unref + + if (!rControlRegion.IsEmpty()) + mpFrame->damaged(rControlRegion.Left(), rControlRegion.Top(), rControlRegion.GetWidth(), rControlRegion.GetHeight()); + + return true; +} + +static tools::Rectangle GetWidgetSize(const tools::Rectangle& rControlRegion, GtkWidget* widget) +{ + GtkRequisition aReq; + gtk_widget_get_preferred_size(widget, nullptr, &aReq); + long nHeight = std::max<long>(rControlRegion.GetHeight(), aReq.height); + return tools::Rectangle(rControlRegion.TopLeft(), Size(rControlRegion.GetWidth(), nHeight)); +} + +static tools::Rectangle AdjustRectForTextBordersPadding(GtkStyleContext* pStyle, long nContentWidth, long nContentHeight, const tools::Rectangle& rControlRegion) +{ + GtkBorder border; + gtk_style_context_get_border(pStyle, gtk_style_context_get_state(pStyle), &border); + + GtkBorder padding; + gtk_style_context_get_padding(pStyle, gtk_style_context_get_state(pStyle), &padding); + + gint nWidgetHeight = nContentHeight + padding.top + padding.bottom + border.top + border.bottom; + nWidgetHeight = std::max(std::max<gint>(nWidgetHeight, rControlRegion.GetHeight()), 34); + + gint nWidgetWidth = nContentWidth + padding.left + padding.right + border.left + border.right; + nWidgetWidth = std::max<gint>(nWidgetWidth, rControlRegion.GetWidth()); + + tools::Rectangle aEditRect(rControlRegion.TopLeft(), Size(nWidgetWidth, nWidgetHeight)); + + return aEditRect; +} + +bool GtkSalGraphics::getNativeControlRegion( ControlType nType, ControlPart nPart, const tools::Rectangle& rControlRegion, ControlState, + const ImplControlValue& rValue, const OUString&, + tools::Rectangle &rNativeBoundingRegion, tools::Rectangle &rNativeContentRegion ) +{ + /* TODO: all this functions needs improvements */ + tools::Rectangle aEditRect = rControlRegion; + gint indicator_size, indicator_spacing, point; + + if(((nType == ControlType::Checkbox) || (nType == ControlType::Radiobutton)) && + nPart == ControlPart::Entire) + { + rNativeBoundingRegion = rControlRegion; + + GtkStyleContext *pButtonStyle = (nType == ControlType::Checkbox) ? mpCheckButtonCheckStyle : mpRadioButtonRadioStyle; + + + gtk_style_context_get_style( pButtonStyle, + "indicator-size", &indicator_size, + "indicator-spacing", &indicator_spacing, + nullptr ); + + GtkBorder border; + gtk_style_context_get_border(pButtonStyle, gtk_style_context_get_state(pButtonStyle), &border); + + GtkBorder padding; + gtk_style_context_get_padding(pButtonStyle, gtk_style_context_get_state(pButtonStyle), &padding); + + + indicator_size += 2*indicator_spacing + border.left + padding.left + border.right + padding.right; + tools::Rectangle aIndicatorRect( Point( 0, + (rControlRegion.GetHeight()-indicator_size)/2), + Size( indicator_size, indicator_size ) ); + rNativeContentRegion = aIndicatorRect; + + return true; + } + else if( nType == ControlType::MenuPopup) + { + if ((nPart == ControlPart::MenuItemCheckMark) || + (nPart == ControlPart::MenuItemRadioMark) ) + { + indicator_size = 0; + + GtkStyleContext *pMenuItemStyle = (nPart == ControlPart::MenuItemCheckMark ) ? mpCheckMenuItemCheckStyle + : mpRadioMenuItemRadioStyle; + + gtk_style_context_get_style( pMenuItemStyle, + "indicator-size", &indicator_size, + nullptr ); + + point = MAX(0, rControlRegion.GetHeight() - indicator_size); + aEditRect = tools::Rectangle( Point( 0, point / 2), + Size( indicator_size, indicator_size ) ); + } + else if (nPart == ControlPart::Separator) + { + gint separator_height, separator_width, wide_separators; + + gtk_style_context_get_style (mpSeparatorMenuItemSeparatorStyle, + "wide-separators", &wide_separators, + "separator-width", &separator_width, + "separator-height", &separator_height, + nullptr); + + aEditRect = tools::Rectangle( aEditRect.TopLeft(), + Size( aEditRect.GetWidth(), wide_separators ? separator_height : 1 ) ); + } + else if (nPart == ControlPart::SubmenuArrow) + { + gfloat arrow_size = getArrowSize(mpMenuItemArrowStyle); + aEditRect = tools::Rectangle( aEditRect.TopLeft(), + Size( arrow_size, arrow_size ) ); + } + } + else if ( (nType==ControlType::Scrollbar) && + ((nPart==ControlPart::ButtonLeft) || (nPart==ControlPart::ButtonRight) || + (nPart==ControlPart::ButtonUp) || (nPart==ControlPart::ButtonDown) ) ) + { + rNativeBoundingRegion = NWGetScrollButtonRect( nPart, rControlRegion ); + rNativeContentRegion = rNativeBoundingRegion; + + if (!rNativeContentRegion.GetWidth()) + rNativeContentRegion.SetRight( rNativeContentRegion.Left() + 1 ); + if (!rNativeContentRegion.GetHeight()) + rNativeContentRegion.SetBottom( rNativeContentRegion.Top() + 1 ); + + return true; + } + else if ( (nType==ControlType::Spinbox) && + ((nPart==ControlPart::ButtonUp) || (nPart==ControlPart::ButtonDown) || + (nPart==ControlPart::SubEdit)) ) + { + tools::Rectangle aControlRegion(GetWidgetSize(rControlRegion, gSpinBox)); + aEditRect = NWGetSpinButtonRect(nPart, aControlRegion); + } + else if ( (nType==ControlType::Combobox) && + ((nPart==ControlPart::ButtonDown) || (nPart==ControlPart::SubEdit)) ) + { + aEditRect = NWGetComboBoxButtonRect(nType, nPart, rControlRegion); + } + else if ( (nType==ControlType::Listbox) && + ((nPart==ControlPart::ButtonDown) || (nPart==ControlPart::SubEdit)) ) + { + aEditRect = NWGetComboBoxButtonRect(nType, nPart, rControlRegion); + } + else if (nType == ControlType::Editbox && nPart == ControlPart::Entire) + { + aEditRect = GetWidgetSize(rControlRegion, gEntryBox); + } + else if (nType == ControlType::Listbox && nPart == ControlPart::Entire) + { + aEditRect = GetWidgetSize(rControlRegion, gListBox); + } + else if (nType == ControlType::Combobox && nPart == ControlPart::Entire) + { + aEditRect = GetWidgetSize(rControlRegion, gComboBox); + } + else if (nType == ControlType::Spinbox && nPart == ControlPart::Entire) + { + aEditRect = GetWidgetSize(rControlRegion, gSpinBox); + } + else if (nType == ControlType::TabItem && nPart == ControlPart::Entire) + { + const TabitemValue& rTabitemValue = static_cast<const TabitemValue&>(rValue); + const tools::Rectangle& rTabitemRect = rTabitemValue.getContentRect(); + + aEditRect = AdjustRectForTextBordersPadding(mpNotebookHeaderTabsTabStyle, rTabitemRect.GetWidth(), + rTabitemRect.GetHeight(), rControlRegion); + } + else if (nType == ControlType::Frame && nPart == ControlPart::Border) + { + aEditRect = rControlRegion; + DrawFrameFlags nStyle = static_cast<DrawFrameFlags>(rValue.getNumericVal() & 0xfff0); + if (nStyle & DrawFrameFlags::NoDraw) + { + GtkBorder padding; + gtk_style_context_get_padding(mpFrameInStyle, gtk_style_context_get_state(mpFrameInStyle), &padding); + + GtkBorder border; + gtk_style_context_get_border(mpFrameInStyle, gtk_style_context_get_state(mpFrameInStyle), &border); + + int x1 = aEditRect.Left(); + int y1 = aEditRect.Top(); + int x2 = aEditRect.Right(); + int y2 = aEditRect.Bottom(); + + rNativeBoundingRegion = aEditRect; + rNativeContentRegion = tools::Rectangle(x1 + (padding.left + border.left), + y1 + (padding.top + border.top), + x2 - (padding.right + border.right), + y2 - (padding.bottom + border.bottom)); + + return true; + } + else + rNativeContentRegion = rControlRegion; + } + else + { + return false; + } + + rNativeBoundingRegion = aEditRect; + rNativeContentRegion = rNativeBoundingRegion; + + return true; +} +/************************************************************************ + * helper for GtkSalFrame + ************************************************************************/ +static ::Color getColor( const GdkRGBA& rCol ) +{ + return ::Color( static_cast<int>(rCol.red * 0xFFFF) >> 8, static_cast<int>(rCol.green * 0xFFFF) >> 8, static_cast<int>(rCol.blue * 0xFFFF) >> 8 ); +} + +static vcl::Font getFont(GtkStyleContext* pStyle, const css::lang::Locale& rLocale) +{ + const PangoFontDescription* font = gtk_style_context_get_font(pStyle, gtk_style_context_get_state(pStyle)); + return pango_to_vcl(font, rLocale); +} + +vcl::Font pango_to_vcl(const PangoFontDescription* font, const css::lang::Locale& rLocale) +{ + OString aFamily = pango_font_description_get_family( font ); + int nPangoHeight = pango_font_description_get_size( font ); + PangoStyle eStyle = pango_font_description_get_style( font ); + PangoWeight eWeight = pango_font_description_get_weight( font ); + PangoStretch eStretch = pango_font_description_get_stretch( font ); + + psp::FastPrintFontInfo aInfo; + // set family name + aInfo.m_aFamilyName = OStringToOUString( aFamily, RTL_TEXTENCODING_UTF8 ); + // set italic + switch( eStyle ) + { + case PANGO_STYLE_NORMAL: aInfo.m_eItalic = ITALIC_NONE;break; + case PANGO_STYLE_ITALIC: aInfo.m_eItalic = ITALIC_NORMAL;break; + case PANGO_STYLE_OBLIQUE: aInfo.m_eItalic = ITALIC_OBLIQUE;break; + } + // set weight + if( eWeight <= PANGO_WEIGHT_ULTRALIGHT ) + aInfo.m_eWeight = WEIGHT_ULTRALIGHT; + else if( eWeight <= PANGO_WEIGHT_LIGHT ) + aInfo.m_eWeight = WEIGHT_LIGHT; + else if( eWeight <= PANGO_WEIGHT_NORMAL ) + aInfo.m_eWeight = WEIGHT_NORMAL; + else if( eWeight <= PANGO_WEIGHT_BOLD ) + aInfo.m_eWeight = WEIGHT_BOLD; + else + aInfo.m_eWeight = WEIGHT_ULTRABOLD; + // set width + switch( eStretch ) + { + case PANGO_STRETCH_ULTRA_CONDENSED: aInfo.m_eWidth = WIDTH_ULTRA_CONDENSED;break; + case PANGO_STRETCH_EXTRA_CONDENSED: aInfo.m_eWidth = WIDTH_EXTRA_CONDENSED;break; + case PANGO_STRETCH_CONDENSED: aInfo.m_eWidth = WIDTH_CONDENSED;break; + case PANGO_STRETCH_SEMI_CONDENSED: aInfo.m_eWidth = WIDTH_SEMI_CONDENSED;break; + case PANGO_STRETCH_NORMAL: aInfo.m_eWidth = WIDTH_NORMAL;break; + case PANGO_STRETCH_SEMI_EXPANDED: aInfo.m_eWidth = WIDTH_SEMI_EXPANDED;break; + case PANGO_STRETCH_EXPANDED: aInfo.m_eWidth = WIDTH_EXPANDED;break; + case PANGO_STRETCH_EXTRA_EXPANDED: aInfo.m_eWidth = WIDTH_EXTRA_EXPANDED;break; + case PANGO_STRETCH_ULTRA_EXPANDED: aInfo.m_eWidth = WIDTH_ULTRA_EXPANDED;break; + } + +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.gtk3", "font name BEFORE system match: \"" + << aFamily << "\"."); +#endif + + // match font to e.g. resolve "Sans" + psp::PrintFontManager::get().matchFont(aInfo, rLocale); + +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.gtk3", "font match " + << (aInfo.m_nID != 0 ? "succeeded" : "failed") + << ", name AFTER: \"" + << aInfo.m_aFamilyName + << "\"."); +#endif + + int nPointHeight = nPangoHeight/PANGO_SCALE; + + vcl::Font aFont( aInfo.m_aFamilyName, Size( 0, nPointHeight ) ); + if( aInfo.m_eWeight != WEIGHT_DONTKNOW ) + aFont.SetWeight( aInfo.m_eWeight ); + if( aInfo.m_eWidth != WIDTH_DONTKNOW ) + aFont.SetWidthType( aInfo.m_eWidth ); + if( aInfo.m_eItalic != ITALIC_DONTKNOW ) + aFont.SetItalic( aInfo.m_eItalic ); + if( aInfo.m_ePitch != PITCH_DONTKNOW ) + aFont.SetPitch( aInfo.m_ePitch ); + return aFont; +} + +bool GtkSalGraphics::updateSettings(AllSettings& rSettings) +{ + GtkWidget* pTopLevel = gtk_widget_get_toplevel(mpWindow); + GtkStyleContext* pStyle = gtk_widget_get_style_context(pTopLevel); + StyleContextSave aContextState; + aContextState.save(pStyle); + GtkSettings* pSettings = gtk_widget_get_settings(pTopLevel); + StyleSettings aStyleSet = rSettings.GetStyleSettings(); + GdkRGBA color; + + // text colors + GdkRGBA text_color; + style_context_set_state(pStyle, GTK_STATE_FLAG_NORMAL); + gtk_style_context_get_color(pStyle, gtk_style_context_get_state(pStyle), &text_color); + ::Color aTextColor = getColor( text_color ); + aStyleSet.SetDialogTextColor( aTextColor ); + aStyleSet.SetButtonTextColor( aTextColor ); + aStyleSet.SetDefaultActionButtonTextColor(aTextColor); + aStyleSet.SetActionButtonTextColor(aTextColor); + aStyleSet.SetRadioCheckTextColor( aTextColor ); + aStyleSet.SetGroupTextColor( aTextColor ); + aStyleSet.SetLabelTextColor( aTextColor ); + aStyleSet.SetWindowTextColor( aTextColor ); + aStyleSet.SetFieldTextColor( aTextColor ); + + // background colors + GdkRGBA background_color; + gtk_style_context_get_background_color(pStyle, gtk_style_context_get_state(pStyle), &background_color); + + ::Color aBackColor = getColor( background_color ); + aStyleSet.BatchSetBackgrounds( aBackColor ); + + // UI font + vcl::Font aFont(getFont(pStyle, rSettings.GetUILanguageTag().getLocale())); + + aStyleSet.BatchSetFonts( aFont, aFont); + + aFont.SetWeight( WEIGHT_BOLD ); + aStyleSet.SetTitleFont( aFont ); + aStyleSet.SetFloatTitleFont( aFont ); + + // mouse over text colors + style_context_set_state(pStyle, GTK_STATE_FLAG_PRELIGHT); + gtk_style_context_get_color(pStyle, gtk_style_context_get_state(pStyle), &text_color); + aTextColor = getColor(text_color); + aStyleSet.SetDefaultButtonRolloverTextColor(aTextColor); + aStyleSet.SetButtonRolloverTextColor(aTextColor); + aStyleSet.SetDefaultActionButtonRolloverTextColor(aTextColor); + aStyleSet.SetActionButtonRolloverTextColor(aTextColor); + aStyleSet.SetFlatButtonRolloverTextColor(aTextColor); + aStyleSet.SetFieldRolloverTextColor(aTextColor); + + aContextState.restore(); + + // button mouse over colors + { + GdkRGBA normal_button_rollover_text_color, pressed_button_rollover_text_color; + aContextState.save(mpButtonStyle); + style_context_set_state(mpButtonStyle, GTK_STATE_FLAG_PRELIGHT); + gtk_style_context_get_color(mpButtonStyle, gtk_style_context_get_state(mpButtonStyle), &normal_button_rollover_text_color); + aTextColor = getColor(normal_button_rollover_text_color); + aStyleSet.SetButtonRolloverTextColor( aTextColor ); + style_context_set_state(mpButtonStyle, static_cast<GtkStateFlags>(GTK_STATE_FLAG_PRELIGHT | GTK_STATE_FLAG_ACTIVE)); + gtk_style_context_get_color(mpButtonStyle, gtk_style_context_get_state(mpButtonStyle), &pressed_button_rollover_text_color); + aTextColor = getColor(pressed_button_rollover_text_color); + style_context_set_state(mpButtonStyle, GTK_STATE_FLAG_NORMAL); + aStyleSet.SetButtonPressedRolloverTextColor( aTextColor ); + aContextState.restore(); + } + + // tooltip colors + { + GtkWidgetPath *pCPath = gtk_widget_path_new(); + guint pos = gtk_widget_path_append_type(pCPath, GTK_TYPE_WINDOW); + gtk_widget_path_iter_add_class(pCPath, pos, GTK_STYLE_CLASS_TOOLTIP); + pos = gtk_widget_path_append_type (pCPath, GTK_TYPE_LABEL); + gtk_widget_path_iter_add_class(pCPath, pos, GTK_STYLE_CLASS_LABEL); + GtkStyleContext *pCStyle = makeContext (pCPath, nullptr); + aContextState.save(pCStyle); + + GdkRGBA tooltip_bg_color, tooltip_fg_color; + style_context_set_state(pCStyle, GTK_STATE_FLAG_NORMAL); + gtk_style_context_get_color(pCStyle, gtk_style_context_get_state(pCStyle), &tooltip_fg_color); + gtk_style_context_get_background_color(pCStyle, gtk_style_context_get_state(pCStyle), &tooltip_bg_color); + + aContextState.restore(); + g_object_unref( pCStyle ); + + aStyleSet.SetHelpColor( getColor( tooltip_bg_color )); + aStyleSet.SetHelpTextColor( getColor( tooltip_fg_color )); + } + + { + // construct style context for text view + GtkWidgetPath *pCPath = gtk_widget_path_new(); + gtk_widget_path_append_type( pCPath, GTK_TYPE_TEXT_VIEW ); + gtk_widget_path_iter_add_class( pCPath, -1, GTK_STYLE_CLASS_VIEW ); + GtkStyleContext *pCStyle = makeContext( pCPath, nullptr ); + aContextState.save(pCStyle); + + // highlighting colors + style_context_set_state(pCStyle, GTK_STATE_FLAG_SELECTED); + gtk_style_context_get_background_color(pCStyle, gtk_style_context_get_state(pCStyle), &text_color); + ::Color aHighlightColor = getColor( text_color ); + gtk_style_context_get_color(pCStyle, gtk_style_context_get_state(pCStyle), &text_color); + ::Color aHighlightTextColor = getColor( text_color ); + aStyleSet.SetHighlightColor( aHighlightColor ); + aStyleSet.SetHighlightTextColor( aHighlightTextColor ); + + // field background color + GdkRGBA field_background_color; + style_context_set_state(pCStyle, GTK_STATE_FLAG_NORMAL); + gtk_style_context_get_background_color(pCStyle, gtk_style_context_get_state(pCStyle), &field_background_color); + + ::Color aBackFieldColor = getColor( field_background_color ); + aStyleSet.SetFieldColor( aBackFieldColor ); + // This baby is the default page/paper color + aStyleSet.SetWindowColor( aBackFieldColor ); + + // Cursor width + gfloat caretAspectRatio = 0.04f; + gtk_style_context_get_style( pCStyle, "cursor-aspect-ratio", &caretAspectRatio, nullptr ); + // Assume 20px tall for the ratio computation, which should give reasonable results + aStyleSet.SetCursorSize( 20 * caretAspectRatio + 1 ); + + // Dark shadow color + style_context_set_state(pCStyle, GTK_STATE_FLAG_INSENSITIVE); + gtk_style_context_get_color(pCStyle, gtk_style_context_get_state(pCStyle), &color); + ::Color aDarkShadowColor = getColor( color ); + aStyleSet.SetDarkShadowColor( aDarkShadowColor ); + + ::Color aShadowColor(aBackColor); + if (aDarkShadowColor.GetLuminance() > aBackColor.GetLuminance()) + aShadowColor.IncreaseLuminance(64); + else + aShadowColor.DecreaseLuminance(64); + aStyleSet.SetShadowColor(aShadowColor); + + aContextState.restore(); + g_object_unref( pCStyle ); + + // Tab colors + aStyleSet.SetActiveTabColor( aBackFieldColor ); // same as the window color. + aStyleSet.SetInactiveTabColor( aBackColor ); + } + + // menu disabled entries handling + aStyleSet.SetSkipDisabledInMenus( true ); + aStyleSet.SetPreferredContextMenuShortcuts( false ); + + aContextState.save(mpMenuItemLabelStyle); + + // menu colors + style_context_set_state(mpMenuStyle, GTK_STATE_FLAG_NORMAL); + gtk_style_context_get_background_color( mpMenuStyle, gtk_style_context_get_state(mpMenuStyle), &background_color ); + aBackColor = getColor( background_color ); + aStyleSet.SetMenuColor( aBackColor ); + + // menu bar + style_context_set_state(mpMenuBarStyle, GTK_STATE_FLAG_NORMAL); + gtk_style_context_get_background_color( mpMenuBarStyle, gtk_style_context_get_state(mpMenuBarStyle), &background_color ); + aBackColor = getColor( background_color ); + aStyleSet.SetMenuBarColor( aBackColor ); + aStyleSet.SetMenuBarRolloverColor( aBackColor ); + + style_context_set_state(mpMenuBarItemStyle, GTK_STATE_FLAG_NORMAL); + gtk_style_context_get_color( mpMenuBarItemStyle, gtk_style_context_get_state(mpMenuBarItemStyle), &text_color ); + aTextColor = aStyleSet.GetPersonaMenuBarTextColor().value_or( getColor( text_color ) ); + aStyleSet.SetMenuBarTextColor( aTextColor ); + aStyleSet.SetMenuBarRolloverTextColor( aTextColor ); + + style_context_set_state(mpMenuBarItemStyle, GTK_STATE_FLAG_PRELIGHT); + gtk_style_context_get_color( mpMenuBarItemStyle, gtk_style_context_get_state(mpMenuBarItemStyle), &text_color ); + aTextColor = aStyleSet.GetPersonaMenuBarTextColor().value_or( getColor( text_color ) ); + aStyleSet.SetMenuBarHighlightTextColor( aTextColor ); + + // menu items + style_context_set_state(mpMenuItemLabelStyle, GTK_STATE_FLAG_NORMAL); + gtk_style_context_get_color(mpMenuItemLabelStyle, gtk_style_context_get_state(mpMenuItemLabelStyle), &color); + aTextColor = getColor(color); + aStyleSet.SetMenuTextColor(aTextColor); + + style_context_set_state(mpMenuItemLabelStyle, GTK_STATE_FLAG_PRELIGHT); + gtk_style_context_get_background_color( mpMenuItemLabelStyle, gtk_style_context_get_state(mpMenuItemLabelStyle), &background_color ); + ::Color aHighlightColor = getColor( background_color ); + aStyleSet.SetMenuHighlightColor( aHighlightColor ); + + gtk_style_context_get_color( mpMenuItemLabelStyle, gtk_style_context_get_state(mpMenuItemLabelStyle), &color ); + ::Color aHighlightTextColor = getColor( color ); + aStyleSet.SetMenuHighlightTextColor( aHighlightTextColor ); + + aContextState.restore(); + + // hyperlink colors + aContextState.save(mpLinkButtonStyle); + style_context_set_state(mpLinkButtonStyle, GTK_STATE_FLAG_LINK); + gtk_style_context_get_color(mpLinkButtonStyle, gtk_style_context_get_state(mpLinkButtonStyle), &text_color); + aStyleSet.SetLinkColor(getColor(text_color)); + style_context_set_state(mpLinkButtonStyle, GTK_STATE_FLAG_VISITED); + gtk_style_context_get_color(mpLinkButtonStyle, gtk_style_context_get_state(mpLinkButtonStyle), &text_color); + aStyleSet.SetVisitedLinkColor(getColor(text_color)); + aContextState.restore(); + + { + GtkStyleContext *pCStyle = mpNotebookHeaderTabsTabLabelStyle; + aContextState.save(pCStyle); + style_context_set_state(pCStyle, GTK_STATE_FLAG_NORMAL); + gtk_style_context_get_color(pCStyle, gtk_style_context_get_state(pCStyle), &text_color); + aTextColor = getColor( text_color ); + aStyleSet.SetTabTextColor(aTextColor); + aStyleSet.SetTabFont(getFont(mpNotebookHeaderTabsTabLabelStyle, rSettings.GetUILanguageTag().getLocale())); + aContextState.restore(); + } + + { + GtkStyleContext *pCStyle = mpToolButtonStyle; + aContextState.save(pCStyle); + style_context_set_state(pCStyle, GTK_STATE_FLAG_NORMAL); + gtk_style_context_get_color(pCStyle, gtk_style_context_get_state(pCStyle), &text_color); + aTextColor = getColor( text_color ); + aStyleSet.SetToolTextColor(aTextColor); + aStyleSet.SetToolFont(getFont(mpToolButtonStyle, rSettings.GetUILanguageTag().getLocale())); + aContextState.restore(); + } + + // mouse over text colors + { + GtkStyleContext *pCStyle = mpNotebookHeaderTabsTabHoverLabelStyle; + aContextState.save(pCStyle); + style_context_set_state(pCStyle, GTK_STATE_FLAG_PRELIGHT); + gtk_style_context_get_color(pCStyle, gtk_style_context_get_state(pCStyle), &text_color); + aTextColor = getColor( text_color ); + aStyleSet.SetTabRolloverTextColor(aTextColor); + aContextState.restore(); + } + + { + GtkStyleContext *pCStyle = mpNotebookHeaderTabsTabActiveLabelStyle; + aContextState.save(pCStyle); + style_context_set_state(pCStyle, ACTIVE_TAB()); + gtk_style_context_get_color(pCStyle, gtk_style_context_get_state(pCStyle), &text_color); + aTextColor = getColor( text_color ); + aStyleSet.SetTabHighlightTextColor(aTextColor); + aContextState.restore(); + } + + // get cursor blink time + gboolean blink = false; + + g_object_get( pSettings, "gtk-cursor-blink", &blink, nullptr ); + if( blink ) + { + gint blink_time = static_cast<gint>(STYLE_CURSOR_NOBLINKTIME); + g_object_get( pSettings, "gtk-cursor-blink-time", &blink_time, nullptr ); + // set the blink_time if there is a setting and it is reasonable + // else leave the default value + if( blink_time > 100 ) + aStyleSet.SetCursorBlinkTime( blink_time/2 ); + } + else + aStyleSet.SetCursorBlinkTime( STYLE_CURSOR_NOBLINKTIME ); + + MouseSettings aMouseSettings = rSettings.GetMouseSettings(); + int iDoubleClickTime, iDoubleClickDistance, iDragThreshold; + static const int MENU_POPUP_DELAY = 225; + g_object_get( pSettings, + "gtk-double-click-time", &iDoubleClickTime, + "gtk-double-click-distance", &iDoubleClickDistance, + "gtk-dnd-drag-threshold", &iDragThreshold, + nullptr ); + aMouseSettings.SetDoubleClickTime( iDoubleClickTime ); + aMouseSettings.SetDoubleClickWidth( iDoubleClickDistance ); + aMouseSettings.SetDoubleClickHeight( iDoubleClickDistance ); + aMouseSettings.SetStartDragWidth( iDragThreshold ); + aMouseSettings.SetStartDragHeight( iDragThreshold ); + aMouseSettings.SetMenuDelay( MENU_POPUP_DELAY ); + rSettings.SetMouseSettings( aMouseSettings ); + + gboolean primarybuttonwarps = false; + g_object_get( pSettings, + "gtk-primary-button-warps-slider", &primarybuttonwarps, + nullptr ); + aStyleSet.SetPreferredUseImagesInMenus(false); + aStyleSet.SetPrimaryButtonWarpsSlider(primarybuttonwarps); + + // set scrollbar settings + gint min_slider_length = 21; + + // Grab some button style attributes + if (gtk_check_version(3, 20, 0) == nullptr) + { + Size aSize; + QuerySize(mpHScrollbarStyle, aSize); + QuerySize(mpHScrollbarContentsStyle, aSize); + QuerySize(mpHScrollbarTroughStyle, aSize); + QuerySize(mpHScrollbarSliderStyle, aSize); + + gboolean has_forward, has_forward2, has_backward, has_backward2; + gtk_style_context_get_style(mpHScrollbarStyle, + "has-forward-stepper", &has_forward, + "has-secondary-forward-stepper", &has_forward2, + "has-backward-stepper", &has_backward, + "has-secondary-backward-stepper", &has_backward2, nullptr); + if (has_forward || has_backward || has_forward2 || has_backward2) + QuerySize(mpHScrollbarButtonStyle, aSize); + + aStyleSet.SetScrollBarSize(aSize.Height()); + + gtk_style_context_get(mpVScrollbarSliderStyle, gtk_style_context_get_state(mpVScrollbarSliderStyle), + "min-height", &min_slider_length, + nullptr); + aStyleSet.SetMinThumbSize(min_slider_length); + } + else + { + gint slider_width = 14; + gint trough_border = 1; + + gtk_style_context_get_style(mpVScrollbarStyle, + "slider-width", &slider_width, + "trough-border", &trough_border, + "min-slider-length", &min_slider_length, + nullptr); + aStyleSet.SetScrollBarSize(slider_width + 2*trough_border); + gint magic = trough_border ? 1 : 0; + aStyleSet.SetMinThumbSize(min_slider_length - magic); + } + + // preferred icon style + gchar* pIconThemeName = nullptr; + gboolean bDarkIconTheme = false; + g_object_get(pSettings, "gtk-icon-theme-name", &pIconThemeName, + "gtk-application-prefer-dark-theme", &bDarkIconTheme, + nullptr ); + OUString sIconThemeName(OUString::createFromAscii(pIconThemeName)); + aStyleSet.SetPreferredIconTheme(sIconThemeName, bDarkIconTheme); + g_free( pIconThemeName ); + + aStyleSet.SetToolbarIconSize( ToolbarIconSize::Large ); + + // finally update the collected settings + rSettings.SetStyleSettings( aStyleSet ); +#if OSL_DEBUG_LEVEL > 1 + gchar* pThemeName = NULL; + g_object_get( pSettings, "gtk-theme-name", &pThemeName, nullptr ); + SAL_INFO("vcl.gtk3", "Theme name is \"" + << pThemeName + << "\"."); + g_free(pThemeName); +#endif + + return true; +} + +bool GtkSalGraphics::isNativeControlSupported( ControlType nType, ControlPart nPart ) +{ + switch(nType) + { + case ControlType::Pushbutton: + case ControlType::Radiobutton: + case ControlType::Checkbox: + case ControlType::Progress: + case ControlType::ListNode: + case ControlType::ListNet: + if (nPart==ControlPart::Entire || nPart == ControlPart::Focus) + return true; + break; + + case ControlType::Scrollbar: + if(nPart==ControlPart::DrawBackgroundHorz || nPart==ControlPart::DrawBackgroundVert || + nPart==ControlPart::Entire || nPart==ControlPart::HasThreeButtons) + return true; + break; + + case ControlType::Editbox: + case ControlType::MultilineEditbox: + if (nPart==ControlPart::Entire || nPart==ControlPart::HasBackgroundTexture) + return true; + break; + + case ControlType::Combobox: + if (nPart==ControlPart::Entire || nPart==ControlPart::HasBackgroundTexture || nPart == ControlPart::AllButtons) + return true; + break; + + case ControlType::Spinbox: + if (nPart==ControlPart::Entire || nPart==ControlPart::HasBackgroundTexture || nPart == ControlPart::AllButtons || nPart == ControlPart::ButtonUp || nPart == ControlPart::ButtonDown) + return true; + break; + + case ControlType::SpinButtons: + if (nPart==ControlPart::Entire || nPart==ControlPart::AllButtons) + return true; + break; + + case ControlType::Frame: + case ControlType::WindowBackground: + return true; + + case ControlType::TabItem: + case ControlType::TabHeader: + case ControlType::TabPane: + case ControlType::TabBody: + if(nPart==ControlPart::Entire || nPart==ControlPart::TabsDrawRtl) + return true; + break; + + case ControlType::Listbox: + if (nPart==ControlPart::Entire || nPart==ControlPart::ListboxWindow || nPart==ControlPart::HasBackgroundTexture || nPart == ControlPart::Focus) + return true; + break; + + case ControlType::Toolbar: + if( nPart==ControlPart::Entire +// || nPart==ControlPart::DrawBackgroundHorz +// || nPart==ControlPart::DrawBackgroundVert +// || nPart==ControlPart::ThumbHorz +// || nPart==ControlPart::ThumbVert + || nPart==ControlPart::Button +// || nPart==ControlPart::SeparatorHorz + || nPart==ControlPart::SeparatorVert + ) + return true; + break; + + case ControlType::Menubar: + if (nPart==ControlPart::Entire || nPart==ControlPart::MenuItem) + return true; + break; + + case ControlType::MenuPopup: + if (nPart==ControlPart::Entire + || nPart==ControlPart::MenuItem + || nPart==ControlPart::MenuItemCheckMark + || nPart==ControlPart::MenuItemRadioMark + || nPart==ControlPart::Separator + || nPart==ControlPart::SubmenuArrow + ) + return true; + break; + +// case ControlType::Slider: +// if(nPart == ControlPart::TrackHorzArea || nPart == ControlPart::TrackVertArea) +// return true; +// break; + + case ControlType::Fixedline: + if (nPart == ControlPart::SeparatorVert || nPart == ControlPart::SeparatorHorz) + return true; + break; + + case ControlType::ListHeader: + if (nPart == ControlPart::Button || nPart == ControlPart::Arrow) + return true; + break; + default: break; + } + + SAL_INFO("vcl.gtk", "Unhandled is native supported for Type:" << static_cast<int>(nType) << ", Part" << static_cast<int>(nPart)); + + return false; +} + +#if ENABLE_CAIRO_CANVAS + +bool GtkSalGraphics::SupportsCairo() const +{ + return true; +} + +cairo::SurfaceSharedPtr GtkSalGraphics::CreateSurface(const cairo::CairoSurfaceSharedPtr& rSurface) const +{ + return std::make_shared<cairo::Gtk3Surface>(rSurface); +} + +cairo::SurfaceSharedPtr GtkSalGraphics::CreateSurface(const OutputDevice& /*rRefDevice*/, int x, int y, int width, int height) const +{ + return std::make_shared<cairo::Gtk3Surface>(this, x, y, width, height); +} + +#endif + +void GtkSalGraphics::WidgetQueueDraw() const +{ + //request gtk to sync the entire contents + GtkWidget *pWidget = GTK_WIDGET(mpFrame->getFixedContainer()); + gtk_widget_queue_draw(pWidget); +} + +namespace { + +void getStyleContext(GtkStyleContext** style, GtkWidget* widget) +{ + gtk_container_add(GTK_CONTAINER(gDumbContainer), widget); + *style = gtk_widget_get_style_context(widget); + g_object_ref(*style); +} + +} + +void GtkSalData::initNWF() +{ + ImplSVData* pSVData = ImplGetSVData(); + pSVData->maNWFData.mbFlatMenu = true; + pSVData->maNWFData.mbDockingAreaAvoidTBFrames = true; + pSVData->maNWFData.mbCanDrawWidgetAnySize = true; + pSVData->maNWFData.mbDDListBoxNoTextArea = true; + pSVData->maNWFData.mbNoFocusRects = true; + pSVData->maNWFData.mbNoFocusRectsForFlatButtons = true; + pSVData->maNWFData.mbAutoAccel = true; + +#if defined(GDK_WINDOWING_WAYLAND) + //gnome#768128 for the car crash that is wayland + //and floating dockable toolbars + GdkDisplay *pDisplay = gdk_display_get_default(); + if (DLSYM_GDK_IS_WAYLAND_DISPLAY(pDisplay)) + pSVData->maNWFData.mbCanDetermineWindowPosition = false; +#endif +} + +void GtkSalData::deInitNWF() +{ + if (gCacheWindow) + gtk_widget_destroy(gCacheWindow); +} + +GtkSalGraphics::GtkSalGraphics( GtkSalFrame *pFrame, GtkWidget *pWindow ) + : SvpSalGraphics(), + mpFrame( pFrame ), + mpWindow( pWindow ) +{ + if (style_loaded) + return; + + style_loaded = true; + + /* Load the GtkStyleContexts, it might be a bit slow, but usually, + * gtk apps create a lot of widgets at startup, so, it shouldn't be + * too slow */ + gtk_widget_path_iter_set_object_nameFunc set_object_name = + reinterpret_cast<gtk_widget_path_iter_set_object_nameFunc>(osl_getAsciiFunctionSymbol(nullptr, + "gtk_widget_path_iter_set_object_name")); + + gCacheWindow = gtk_window_new(GTK_WINDOW_TOPLEVEL); + gDumbContainer = gtk_fixed_new(); + gtk_container_add(GTK_CONTAINER(gCacheWindow), gDumbContainer); + gtk_widget_realize(gDumbContainer); + gtk_widget_realize(gCacheWindow); + + gEntryBox = gtk_entry_new(); + gtk_container_add(GTK_CONTAINER(gDumbContainer), gEntryBox); + + mpWindowStyle = createStyleContext(set_object_name, GtkControlPart::ToplevelWindow); + mpEntryStyle = createStyleContext(set_object_name, GtkControlPart::Entry); + + getStyleContext(&mpTextViewStyle, gtk_text_view_new()); + + mpButtonStyle = createStyleContext(set_object_name, GtkControlPart::Button); + mpLinkButtonStyle = createStyleContext(set_object_name, GtkControlPart::LinkButton); + + GtkWidget* pToolbar = gtk_toolbar_new(); + mpToolbarStyle = gtk_widget_get_style_context(pToolbar); + gtk_style_context_add_class(mpToolbarStyle, GTK_STYLE_CLASS_TOOLBAR); + + GtkToolItem *item = gtk_separator_tool_item_new(); + gtk_toolbar_insert(GTK_TOOLBAR(pToolbar), item, -1); + mpToolbarSeperatorStyle = gtk_widget_get_style_context(GTK_WIDGET(item)); + + GtkWidget *pButton = gtk_button_new(); + item = gtk_tool_button_new(pButton, nullptr); + gtk_toolbar_insert(GTK_TOOLBAR(pToolbar), item, -1); + mpToolButtonStyle = gtk_widget_get_style_context(GTK_WIDGET(pButton)); + + mpVScrollbarStyle = createStyleContext(set_object_name, GtkControlPart::ScrollbarVertical); + mpVScrollbarContentsStyle = createStyleContext(set_object_name, GtkControlPart::ScrollbarVerticalContents); + mpVScrollbarTroughStyle = createStyleContext(set_object_name, GtkControlPart::ScrollbarVerticalTrough); + mpVScrollbarSliderStyle = createStyleContext(set_object_name, GtkControlPart::ScrollbarVerticalSlider); + mpVScrollbarButtonStyle = createStyleContext(set_object_name, GtkControlPart::ScrollbarVerticalButton); + mpHScrollbarStyle = createStyleContext(set_object_name, GtkControlPart::ScrollbarHorizontal); + mpHScrollbarContentsStyle = createStyleContext(set_object_name, GtkControlPart::ScrollbarHorizontalContents); + mpHScrollbarTroughStyle = createStyleContext(set_object_name, GtkControlPart::ScrollbarHorizontalTrough); + mpHScrollbarSliderStyle = createStyleContext(set_object_name, GtkControlPart::ScrollbarHorizontalSlider); + mpHScrollbarButtonStyle = createStyleContext(set_object_name, GtkControlPart::ScrollbarHorizontalButton); + + mpCheckButtonStyle = createStyleContext(set_object_name, GtkControlPart::CheckButton); + mpCheckButtonCheckStyle = createStyleContext(set_object_name, GtkControlPart::CheckButtonCheck); + + mpRadioButtonStyle = createStyleContext(set_object_name, GtkControlPart::RadioButton); + mpRadioButtonRadioStyle = createStyleContext(set_object_name, GtkControlPart::RadioButtonRadio); + + /* Spinbutton */ + gSpinBox = gtk_spin_button_new(nullptr, 0, 0); + gtk_container_add(GTK_CONTAINER(gDumbContainer), gSpinBox); + mpSpinStyle = createStyleContext(set_object_name, GtkControlPart::SpinButton); + mpSpinEntryStyle = createStyleContext(set_object_name, GtkControlPart::SpinButtonEntry); + mpSpinUpStyle = createStyleContext(set_object_name, GtkControlPart::SpinButtonUpButton); + mpSpinDownStyle = createStyleContext(set_object_name, GtkControlPart::SpinButtonDownButton); + + /* NoteBook */ + mpNotebookStyle = createStyleContext(set_object_name, GtkControlPart::Notebook); + mpNotebookStackStyle = createStyleContext(set_object_name, GtkControlPart::NotebookStack); + mpNotebookHeaderStyle = createStyleContext(set_object_name, GtkControlPart::NotebookHeader); + mpNotebookHeaderTabsStyle = createStyleContext(set_object_name, GtkControlPart::NotebookHeaderTabs); + mpNotebookHeaderTabsTabStyle = createStyleContext(set_object_name, GtkControlPart::NotebookHeaderTabsTab); + mpNotebookHeaderTabsTabLabelStyle = createStyleContext(set_object_name, GtkControlPart::NotebookHeaderTabsTabLabel); + mpNotebookHeaderTabsTabActiveLabelStyle = createStyleContext(set_object_name, GtkControlPart::NotebookHeaderTabsTabActiveLabel); + mpNotebookHeaderTabsTabHoverLabelStyle = createStyleContext(set_object_name, GtkControlPart::NotebookHeaderTabsTabHoverLabel); + + /* Combobox */ + gComboBox = gtk_combo_box_text_new_with_entry(); + gtk_container_add(GTK_CONTAINER(gDumbContainer), gComboBox); + mpComboboxStyle = createStyleContext(set_object_name, GtkControlPart::Combobox); + mpComboboxBoxStyle = createStyleContext(set_object_name, GtkControlPart::ComboboxBox); + mpComboboxEntryStyle = createStyleContext(set_object_name, GtkControlPart::ComboboxBoxEntry); + mpComboboxButtonStyle = createStyleContext(set_object_name, GtkControlPart::ComboboxBoxButton); + mpComboboxButtonBoxStyle = createStyleContext(set_object_name, GtkControlPart::ComboboxBoxButtonBox); + mpComboboxButtonArrowStyle = createStyleContext(set_object_name, GtkControlPart::ComboboxBoxButtonBoxArrow); + + /* Listbox */ + gListBox = gtk_combo_box_text_new(); + gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(gListBox), "sample"); + gtk_container_add(GTK_CONTAINER(gDumbContainer), gListBox); + mpListboxStyle = createStyleContext(set_object_name, GtkControlPart::Listbox); + mpListboxBoxStyle = createStyleContext(set_object_name, GtkControlPart::ListboxBox); + mpListboxButtonStyle = createStyleContext(set_object_name, GtkControlPart::ListboxBoxButton); + mpListboxButtonBoxStyle = createStyleContext(set_object_name, GtkControlPart::ListboxBoxButtonBox); + mpListboxButtonArrowStyle = createStyleContext(set_object_name, GtkControlPart::ListboxBoxButtonBoxArrow); + + /* Menu bar */ + gMenuBarWidget = gtk_menu_bar_new(); + gMenuItemMenuBarWidget = gtk_menu_item_new_with_label( "b" ); + gtk_menu_shell_append(GTK_MENU_SHELL(gMenuBarWidget), gMenuItemMenuBarWidget); + gtk_container_add(GTK_CONTAINER(gDumbContainer), gMenuBarWidget); + + mpMenuBarStyle = createStyleContext(set_object_name, GtkControlPart::MenuBar); + mpMenuBarItemStyle = createStyleContext(set_object_name, GtkControlPart::MenuBarItem); + + /* Menu */ + mpMenuWindowStyle = createStyleContext(set_object_name, GtkControlPart::MenuWindow); + mpMenuStyle = createStyleContext(set_object_name, GtkControlPart::Menu); + GtkWidget *menu = gtk_menu_new(); + gtk_menu_item_set_submenu(GTK_MENU_ITEM(gMenuItemMenuBarWidget), menu); + + /* Menu Items */ + gCheckMenuItemWidget = gtk_check_menu_item_new_with_label("M"); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), gCheckMenuItemWidget); + + mpMenuItemStyle = createStyleContext(set_object_name, GtkControlPart::MenuItem); + mpMenuItemLabelStyle = createStyleContext(set_object_name, GtkControlPart::MenuItemLabel); + mpMenuItemArrowStyle = createStyleContext(set_object_name, GtkControlPart::MenuItemArrow); + mpCheckMenuItemStyle = createStyleContext(set_object_name, GtkControlPart::CheckMenuItem); + mpCheckMenuItemCheckStyle = createStyleContext(set_object_name, GtkControlPart::CheckMenuItemCheck); + mpRadioMenuItemStyle = createStyleContext(set_object_name, GtkControlPart::RadioMenuItem); + mpRadioMenuItemRadioStyle = createStyleContext(set_object_name, GtkControlPart::RadioMenuItemRadio); + mpSeparatorMenuItemStyle = createStyleContext(set_object_name, GtkControlPart::SeparatorMenuItem); + mpSeparatorMenuItemSeparatorStyle = createStyleContext(set_object_name, GtkControlPart::SeparatorMenuItemSeparator); + + /* Frames */ + mpFrameOutStyle = mpFrameInStyle = createStyleContext(set_object_name, GtkControlPart::FrameBorder); + getStyleContext(&mpFixedHoriLineStyle, gtk_separator_new(GTK_ORIENTATION_HORIZONTAL)); + getStyleContext(&mpFixedVertLineStyle, gtk_separator_new(GTK_ORIENTATION_VERTICAL)); + + + /* Tree List */ + gTreeViewWidget = gtk_tree_view_new(); + gtk_container_add(GTK_CONTAINER(gDumbContainer), gTreeViewWidget); + + GtkTreeViewColumn* firstTreeViewColumn = gtk_tree_view_column_new(); + gtk_tree_view_column_set_title(firstTreeViewColumn, "M"); + gtk_tree_view_append_column(GTK_TREE_VIEW(gTreeViewWidget), firstTreeViewColumn); + + GtkTreeViewColumn* middleTreeViewColumn = gtk_tree_view_column_new(); + gtk_tree_view_column_set_title(middleTreeViewColumn, "M"); + gtk_tree_view_append_column(GTK_TREE_VIEW(gTreeViewWidget), middleTreeViewColumn); + gtk_tree_view_set_expander_column(GTK_TREE_VIEW(gTreeViewWidget), middleTreeViewColumn); + + GtkTreeViewColumn* lastTreeViewColumn = gtk_tree_view_column_new(); + gtk_tree_view_column_set_title(lastTreeViewColumn, "M"); + gtk_tree_view_append_column(GTK_TREE_VIEW(gTreeViewWidget), lastTreeViewColumn); + + /* Use the middle column's header for our button */ + GtkWidget* pTreeHeaderCellWidget = gtk_tree_view_column_get_button(middleTreeViewColumn); + mpTreeHeaderButtonStyle = gtk_widget_get_style_context(pTreeHeaderCellWidget); + + /* Progress Bar */ + mpProgressBarStyle = createStyleContext(set_object_name, GtkControlPart::ProgressBar); + mpProgressBarTroughStyle = createStyleContext(set_object_name, GtkControlPart::ProgressBarTrough); + mpProgressBarProgressStyle = createStyleContext(set_object_name, GtkControlPart::ProgressBarProgress); + + gtk_widget_show_all(gDumbContainer); +} + +void GtkSalGraphics::GetResolution(sal_Int32& rDPIX, sal_Int32& rDPIY) +{ + char* pForceDpi; + if ((pForceDpi = getenv("SAL_FORCEDPI"))) + { + OString sForceDPI(pForceDpi); + rDPIX = rDPIY = sForceDPI.toInt32(); + return; + } + + GdkScreen* pScreen = gtk_widget_get_screen(mpWindow); + double fResolution = -1.0; + g_object_get(pScreen, "resolution", &fResolution, nullptr); + + if (fResolution > 0.0) + rDPIX = rDPIY = sal_Int32(fResolution); + else + rDPIX = rDPIY = 96; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/gtk3salprn-gtk.cxx b/vcl/unx/gtk3/gtk3salprn-gtk.cxx new file mode 100644 index 000000000..62e88c919 --- /dev/null +++ b/vcl/unx/gtk3/gtk3salprn-gtk.cxx @@ -0,0 +1,954 @@ +/* -*- 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 <unx/gtk/gtkprintwrapper.hxx> + +#include <unx/gtk/gtkdata.hxx> +#include <unx/gtk/gtkframe.hxx> +#include <unx/gtk/gtkinst.hxx> +#include <unx/gtk/gtkprn.hxx> + +#include <configsettings.hxx> +#include <vcl/help.hxx> +#include <vcl/print.hxx> +#include <vcl/svapp.hxx> +#include <vcl/window.hxx> + +#include <gtk/gtk.h> + +#include <com/sun/star/beans/PropertyValue.hpp> +#include <com/sun/star/view/PrintableState.hpp> + +#include <officecfg/Office/Common.hxx> + +#include <rtl/ustrbuf.hxx> +#include <rtl/ustring.hxx> +#include <sal/log.hxx> + +#include <cstring> +#include <map> + +namespace beans = com::sun::star::beans; +namespace uno = com::sun::star::uno; +namespace view = com::sun::star::view; + +using vcl::unx::GtkPrintWrapper; + +namespace { + +class GtkPrintDialog +{ +public: + explicit GtkPrintDialog(vcl::PrinterController& io_rController); + bool run(); + GtkPrinter* getPrinter() const + { + return m_xWrapper->print_unix_dialog_get_selected_printer(GTK_PRINT_UNIX_DIALOG(m_pDialog)); + } + GtkPrintSettings* getSettings() const + { + return m_xWrapper->print_unix_dialog_get_settings(GTK_PRINT_UNIX_DIALOG(m_pDialog)); + } + void updateControllerPrintRange(); + + ~GtkPrintDialog(); + + static void UIOption_CheckHdl(GtkWidget* i_pWidget, GtkPrintDialog* io_pThis) + { + io_pThis->impl_UIOption_CheckHdl(i_pWidget); + } + static void UIOption_RadioHdl(GtkWidget* i_pWidget, GtkPrintDialog* io_pThis) + { + io_pThis->impl_UIOption_RadioHdl(i_pWidget); + } + static void UIOption_SelectHdl(GtkWidget* i_pWidget, GtkPrintDialog* io_pThis) + { + io_pThis->impl_UIOption_SelectHdl(i_pWidget); + } + +private: + beans::PropertyValue* impl_queryPropertyValue(GtkWidget* i_pWidget) const; + void impl_checkOptionalControlDependencies(); + + void impl_UIOption_CheckHdl(GtkWidget* i_pWidget); + void impl_UIOption_RadioHdl(GtkWidget* i_pWidget); + void impl_UIOption_SelectHdl(GtkWidget* i_pWidget); + + void impl_initDialog(); + void impl_initCustomTab(); + void impl_initPrintContent(uno::Sequence<sal_Bool> const& i_rDisabled); + + void impl_readFromSettings(); + void impl_storeToSettings() const; + +private: + GtkWidget* m_pDialog; + vcl::PrinterController& m_rController; + std::map<GtkWidget*, OUString> m_aControlToPropertyMap; + std::map<GtkWidget*, sal_Int32> m_aControlToNumValMap; + std::shared_ptr<GtkPrintWrapper> m_xWrapper; +}; + +} + +struct GtkSalPrinter_Impl +{ + OString m_sSpoolFile; + OUString m_sJobName; + GtkPrinter* m_pPrinter; + GtkPrintSettings* m_pSettings; + + GtkSalPrinter_Impl(); + ~GtkSalPrinter_Impl(); +}; + +GtkSalPrinter_Impl::GtkSalPrinter_Impl() + : m_pPrinter(nullptr) + , m_pSettings(nullptr) +{ +} + +GtkSalPrinter_Impl::~GtkSalPrinter_Impl() +{ + if (m_pPrinter) + { + g_object_unref(G_OBJECT(m_pPrinter)); + m_pPrinter = nullptr; + } + if (m_pSettings) + { + g_object_unref(G_OBJECT(m_pSettings)); + m_pSettings = nullptr; + } +} + +namespace +{ + +GtkInstance const& +lcl_getGtkSalInstance() +{ + // we _know_ this is GtkInstance + return *static_cast<GtkInstance*>(GetGtkSalData()->m_pInstance); +} + +bool +lcl_useSystemPrintDialog() +{ + return officecfg::Office::Common::Misc::UseSystemPrintDialog::get() + && officecfg::Office::Common::Misc::ExperimentalMode::get() + && lcl_getGtkSalInstance().getPrintWrapper()->supportsPrinting(); +} + +} + +GtkSalPrinter::GtkSalPrinter(SalInfoPrinter* const i_pInfoPrinter) + : PspSalPrinter(i_pInfoPrinter) +{ +} + +GtkSalPrinter::~GtkSalPrinter() = default; + +bool +GtkSalPrinter::impl_doJob( + const OUString* const i_pFileName, + const OUString& i_rJobName, + const OUString& i_rAppName, + ImplJobSetup* const io_pSetupData, + const bool i_bCollate, + vcl::PrinterController& io_rController) +{ + io_rController.setJobState(view::PrintableState_JOB_STARTED); + io_rController.jobStarted(); + const bool bJobStarted( + PspSalPrinter::StartJob(i_pFileName, i_rJobName, i_rAppName, + 1/*i_nCopies*/, i_bCollate, true, io_pSetupData)) + ; + + if (bJobStarted) + { + io_rController.createProgressDialog(); + const int nPages(io_rController.getFilteredPageCount()); + for (int nPage(0); nPage != nPages; ++nPage) + { + if (nPage == nPages - 1) + io_rController.setLastPage(true); + io_rController.printFilteredPage(nPage); + } + io_rController.setJobState(view::PrintableState_JOB_COMPLETED); + } + + return bJobStarted; +} + +bool +GtkSalPrinter::StartJob( + const OUString* const i_pFileName, + const OUString& i_rJobName, + const OUString& i_rAppName, + ImplJobSetup* io_pSetupData, + vcl::PrinterController& io_rController) +{ + if (!lcl_useSystemPrintDialog()) + return PspSalPrinter::StartJob(i_pFileName, i_rJobName, i_rAppName, io_pSetupData, io_rController); + + assert(!m_xImpl); + + m_xImpl.reset(new GtkSalPrinter_Impl()); + m_xImpl->m_sJobName = i_rJobName; + + OString sFileName; + if (i_pFileName) + sFileName = OUStringToOString(*i_pFileName, osl_getThreadTextEncoding()); + + GtkPrintDialog aDialog(io_rController); + if (!aDialog.run()) + { + io_rController.abortJob(); + return false; + } + aDialog.updateControllerPrintRange(); + m_xImpl->m_pPrinter = aDialog.getPrinter(); + m_xImpl->m_pSettings = aDialog.getSettings(); + + //To-Do proper name, watch for encodings + sFileName = OString("/tmp/hacking.ps"); + m_xImpl->m_sSpoolFile = sFileName; + + OUString aFileName = OStringToOUString(sFileName, osl_getThreadTextEncoding()); + + //To-Do, swap ps/pdf for gtk_printer_accepts_ps()/gtk_printer_accepts_pdf() ? + + return impl_doJob(&aFileName, i_rJobName, i_rAppName, io_pSetupData, /*bCollate*/false, io_rController); +} + +bool +GtkSalPrinter::EndJob() +{ + bool bRet = PspSalPrinter::EndJob(); + + if (!lcl_useSystemPrintDialog()) + return bRet; + + assert(m_xImpl); + + if (!bRet || m_xImpl->m_sSpoolFile.isEmpty()) + return bRet; + + std::shared_ptr<GtkPrintWrapper> const xWrapper(lcl_getGtkSalInstance().getPrintWrapper()); + + GtkPageSetup* pPageSetup = xWrapper->page_setup_new(); + + GtkPrintJob* const pJob = xWrapper->print_job_new( + OUStringToOString(m_xImpl->m_sJobName, RTL_TEXTENCODING_UTF8).getStr(), + m_xImpl->m_pPrinter, m_xImpl->m_pSettings, pPageSetup); + + GError* error = nullptr; + bRet = xWrapper->print_job_set_source_file(pJob, m_xImpl->m_sSpoolFile.getStr(), &error); + if (bRet) + xWrapper->print_job_send(pJob, nullptr, nullptr, nullptr); + else + { + //To-Do, do something with this + SAL_WARN("vcl.gtk3", "error was " << error->message); + g_error_free(error); + } + + g_object_unref(pPageSetup); + m_xImpl.reset(); + + //To-Do, remove temp spool file + + return bRet; +} + +namespace +{ + +void +lcl_setHelpText( + GtkWidget* const io_pWidget, + const uno::Sequence<OUString>& i_rHelpTexts, + const sal_Int32 i_nIndex) +{ + if (i_nIndex >= 0 && i_nIndex < i_rHelpTexts.getLength()) + gtk_widget_set_tooltip_text(io_pWidget, + OUStringToOString(i_rHelpTexts.getConstArray()[i_nIndex], RTL_TEXTENCODING_UTF8).getStr()); +} + +GtkWidget* +lcl_makeFrame( + GtkWidget* const i_pChild, + const OUString &i_rText, + const uno::Sequence<OUString> &i_rHelpTexts, + sal_Int32* const io_pCurHelpText) +{ + GtkWidget* const pLabel = gtk_label_new(nullptr); + lcl_setHelpText(pLabel, i_rHelpTexts, !io_pCurHelpText ? 0 : (*io_pCurHelpText)++); + gtk_misc_set_alignment(GTK_MISC(pLabel), 0.0, 0.5); + + { + gchar* const pText = g_markup_printf_escaped("<b>%s</b>", + OUStringToOString(i_rText, RTL_TEXTENCODING_UTF8).getStr()); + gtk_label_set_markup_with_mnemonic(GTK_LABEL(pLabel), pText); + g_free(pText); + } + + GtkWidget* const pFrame = gtk_vbox_new(FALSE, 6); + gtk_box_pack_start(GTK_BOX(pFrame), pLabel, FALSE, FALSE, 0); + + GtkWidget* const pAlignment = gtk_alignment_new(0.0, 0.0, 1.0, 1.0); + gtk_alignment_set_padding(GTK_ALIGNMENT(pAlignment), 0, 0, 12, 0); + gtk_box_pack_start(GTK_BOX(pFrame), pAlignment, FALSE, FALSE, 0); + + gtk_container_add(GTK_CONTAINER(pAlignment), i_pChild); + return pFrame; +} + +void +lcl_extractHelpTextsOrIds( + const beans::PropertyValue& rEntry, + uno::Sequence<OUString>& rHelpStrings) +{ + if (!(rEntry.Value >>= rHelpStrings)) + { + OUString aHelpString; + if (rEntry.Value >>= aHelpString) + { + rHelpStrings.realloc(1); + *rHelpStrings.getArray() = aHelpString; + } + } +} + +GtkWidget* +lcl_combo_box_text_new() +{ + return gtk_combo_box_text_new(); +} + +void +lcl_combo_box_text_append(GtkWidget* const pWidget, gchar const* const pText) +{ + gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(pWidget), pText); +} + +} + +GtkPrintDialog::GtkPrintDialog(vcl::PrinterController& io_rController) + : m_rController(io_rController) + , m_xWrapper(lcl_getGtkSalInstance().getPrintWrapper()) +{ + assert(m_xWrapper->supportsPrinting()); + impl_initDialog(); + impl_initCustomTab(); + impl_readFromSettings(); +} + +void +GtkPrintDialog::impl_initDialog() +{ + //To-Do, like fpicker, set UI language + m_pDialog = m_xWrapper->print_unix_dialog_new(); + + vcl::Window* const pTopWindow(Application::GetActiveTopWindow()); + if (pTopWindow) + { + GtkSalFrame* const pFrame(dynamic_cast<GtkSalFrame*>(pTopWindow->ImplGetFrame())); + if (pFrame) + { + GtkWindow* const pParent(GTK_WINDOW(pFrame->getWindow())); + if (pParent) + gtk_window_set_transient_for(GTK_WINDOW(m_pDialog), pParent); + } + } + + m_xWrapper->print_unix_dialog_set_manual_capabilities(GTK_PRINT_UNIX_DIALOG(m_pDialog), + GtkPrintCapabilities(GTK_PRINT_CAPABILITY_COPIES + | GTK_PRINT_CAPABILITY_COLLATE + | GTK_PRINT_CAPABILITY_REVERSE + | GTK_PRINT_CAPABILITY_GENERATE_PS + | GTK_PRINT_CAPABILITY_NUMBER_UP + | GTK_PRINT_CAPABILITY_NUMBER_UP_LAYOUT + )); +} + +void +GtkPrintDialog::impl_initCustomTab() +{ + typedef std::vector<std::pair<GtkWidget*, OUString> > CustomTabs_t; + + const uno::Sequence<beans::PropertyValue>& rOptions(m_rController.getUIOptions()); + std::map<OUString, GtkWidget*> aPropertyToDependencyRowMap; + CustomTabs_t aCustomTabs; + GtkWidget* pCurParent = nullptr; + GtkWidget* pCurTabPage = nullptr; + GtkWidget* pCurSubGroup = nullptr; + bool bIgnoreSubgroup = false; + for (const auto& rOption : rOptions) + { + uno::Sequence<beans::PropertyValue> aOptProp; + rOption.Value >>= aOptProp; + + OUString aCtrlType; + OUString aText; + OUString aPropertyName; + uno::Sequence<OUString> aChoices; + uno::Sequence<sal_Bool> aChoicesDisabled; + uno::Sequence<OUString> aHelpTexts; + sal_Int64 nMinValue = 0, nMaxValue = 0; + sal_Int32 nCurHelpText = 0; + OUString aDependsOnName; + sal_Int32 nDependsOnValue = 0; + bool bUseDependencyRow = false; + bool bIgnore = false; + GtkWidget* pGroup = nullptr; + bool bGtkInternal = false; + + //Fix fdo#69381 + //Next options if this one is empty + if (!aOptProp.hasElements()) + continue; + + for (const beans::PropertyValue& rEntry : std::as_const(aOptProp)) + { + if ( rEntry.Name == "Text" ) + { + OUString aValue; + rEntry.Value >>= aValue; + aText = aValue.replace('~', '_'); + } + else if ( rEntry.Name == "ControlType" ) + rEntry.Value >>= aCtrlType; + else if ( rEntry.Name == "Choices" ) + rEntry.Value >>= aChoices; + else if ( rEntry.Name == "ChoicesDisabled" ) + rEntry.Value >>= aChoicesDisabled; + else if ( rEntry.Name == "Property" ) + { + beans::PropertyValue aVal; + rEntry.Value >>= aVal; + aPropertyName = aVal.Name; + } + else if ( rEntry.Name == "DependsOnName" ) + rEntry.Value >>= aDependsOnName; + else if ( rEntry.Name == "DependsOnEntry" ) + rEntry.Value >>= nDependsOnValue; + else if ( rEntry.Name == "AttachToDependency" ) + rEntry.Value >>= bUseDependencyRow; + else if ( rEntry.Name == "MinValue" ) + rEntry.Value >>= nMinValue; + else if ( rEntry.Name == "MaxValue" ) + rEntry.Value >>= nMaxValue; + else if ( rEntry.Name == "HelpId" ) + { + uno::Sequence<OUString> aHelpIds; + lcl_extractHelpTextsOrIds(rEntry, aHelpIds); + Help* const pHelp = Application::GetHelp(); + if (pHelp) + { + const int nLen = aHelpIds.getLength(); + aHelpTexts.realloc(nLen); + std::transform(aHelpIds.begin(), aHelpIds.end(), aHelpTexts.begin(), + [&pHelp](const OUString& rHelpId) { return pHelp->GetHelpText(rHelpId, static_cast<weld::Widget*>(nullptr)); }); + } + else // fallback + aHelpTexts = aHelpIds; + } + else if ( rEntry.Name == "HelpText" ) + lcl_extractHelpTextsOrIds(rEntry, aHelpTexts); + else if ( rEntry.Name == "InternalUIOnly" ) + rEntry.Value >>= bIgnore; + else if ( rEntry.Name == "Enabled" ) + { + // Ignore this. We use UIControlOptions::isUIOptionEnabled + // to check whether a control should be enabled. + } + else if ( rEntry.Name == "GroupingHint" ) + { + // Ignore this. We cannot add/modify controls to/on existing + // tabs of the Gtk print dialog. + } + else + { + SAL_INFO("vcl.gtk", "unhandled UI option entry: " << rEntry.Name); + } + } + + if ( aPropertyName == "PrintContent" ) + bGtkInternal = true; + + if (aCtrlType == "Group" || !pCurParent) + { + pCurTabPage = gtk_vbox_new(FALSE, 12); + gtk_container_set_border_width(GTK_CONTAINER(pCurTabPage), 6); + lcl_setHelpText(pCurTabPage, aHelpTexts, 0); + + pCurParent = pCurTabPage; + aCustomTabs.emplace_back(pCurTabPage, aText); + } + else if (aCtrlType == "Subgroup") + { + bIgnoreSubgroup = bIgnore; + if (bIgnore) + continue; + pCurParent = gtk_vbox_new(FALSE, 12); + gtk_container_set_border_width(GTK_CONTAINER(pCurParent), 0); + + pCurSubGroup = lcl_makeFrame(pCurParent, aText, aHelpTexts, nullptr); + gtk_box_pack_start(GTK_BOX(pCurTabPage), pCurSubGroup, FALSE, FALSE, 0); + } + // special case: we need to map these to controls of the gtk print dialog + else if (bGtkInternal) + { + if ( aPropertyName == "PrintContent" ) + { + // What to print? And, more importantly, is there a selection? + impl_initPrintContent(aChoicesDisabled); + } + } + else if (bIgnoreSubgroup || bIgnore) + continue; + else + { + // change handlers for all the controls set up in this block + // should be set _after_ the control has been made (in)active, + // because: + // 1. value of the property is _known_--we are using it to + // _set_ the control, right?--no need to change it back .-) + // 2. it may cause warning because the widget may not + // have been placed in m_aControlToPropertyMap yet + + GtkWidget* pWidget = nullptr; + beans::PropertyValue* pVal = nullptr; + if (aCtrlType == "Bool" && pCurParent) + { + pWidget = gtk_check_button_new_with_mnemonic( + OUStringToOString(aText, RTL_TEXTENCODING_UTF8).getStr()); + lcl_setHelpText(pWidget, aHelpTexts, 0); + m_aControlToPropertyMap[pWidget] = aPropertyName; + + bool bVal = false; + pVal = m_rController.getValue(aPropertyName); + if (pVal) + pVal->Value >>= bVal; + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(pWidget), bVal); + gtk_widget_set_sensitive(pWidget, + m_rController.isUIOptionEnabled(aPropertyName) && pVal != nullptr); + g_signal_connect(pWidget, "toggled", G_CALLBACK(GtkPrintDialog::UIOption_CheckHdl), this); + } + else if (aCtrlType == "Radio" && pCurParent) + { + GtkWidget* const pVbox = gtk_vbox_new(FALSE, 12); + gtk_container_set_border_width(GTK_CONTAINER(pVbox), 0); + + if (!aText.isEmpty()) + pGroup = lcl_makeFrame(pVbox, aText, aHelpTexts, &nCurHelpText); + + sal_Int32 nSelectVal = 0; + pVal = m_rController.getValue(aPropertyName); + if (pVal && pVal->Value.hasValue()) + pVal->Value >>= nSelectVal; + + for (sal_Int32 m = 0; m != aChoices.getLength(); m++) + { + pWidget = gtk_radio_button_new_with_mnemonic_from_widget( + GTK_RADIO_BUTTON(m == 0 ? nullptr : pWidget), + OUStringToOString(aChoices[m].replace('~', '_'), RTL_TEXTENCODING_UTF8).getStr()); + lcl_setHelpText(pWidget, aHelpTexts, nCurHelpText++); + m_aControlToPropertyMap[pWidget] = aPropertyName; + m_aControlToNumValMap[pWidget] = m; + GtkWidget* const pRow = gtk_hbox_new(FALSE, 12); + gtk_box_pack_start(GTK_BOX(pVbox), pRow, FALSE, FALSE, 0); + gtk_box_pack_start(GTK_BOX(pRow), pWidget, FALSE, FALSE, 0); + aPropertyToDependencyRowMap[aPropertyName + OUString::number(m)] = pRow; + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(pWidget), m == nSelectVal); + gtk_widget_set_sensitive(pWidget, + m_rController.isUIOptionEnabled(aPropertyName) && pVal != nullptr); + g_signal_connect(pWidget, "toggled", + G_CALLBACK(GtkPrintDialog::UIOption_RadioHdl), this); + } + + if (pGroup) + pWidget = pGroup; + else + pWidget = pVbox; + } + else if ((aCtrlType == "List" || + aCtrlType == "Range" || + aCtrlType == "Edit" + ) && pCurParent) + { + GtkWidget* const pHbox = gtk_hbox_new(FALSE, 12); + gtk_container_set_border_width(GTK_CONTAINER(pHbox), 0); + + if ( aCtrlType == "List" ) + { + pWidget = lcl_combo_box_text_new(); + + for (const auto& rChoice : std::as_const(aChoices)) + { + lcl_combo_box_text_append(pWidget, + OUStringToOString(rChoice, RTL_TEXTENCODING_UTF8).getStr()); + } + + sal_Int32 nSelectVal = 0; + pVal = m_rController.getValue(aPropertyName); + if (pVal && pVal->Value.hasValue()) + pVal->Value >>= nSelectVal; + gtk_combo_box_set_active(GTK_COMBO_BOX(pWidget), nSelectVal); + g_signal_connect(pWidget, "changed", G_CALLBACK(GtkPrintDialog::UIOption_SelectHdl), this); + } + else if (aCtrlType == "Edit" && pCurParent) + { + pWidget = gtk_entry_new(); + + OUString aCurVal; + pVal = m_rController.getValue(aPropertyName); + if (pVal && pVal->Value.hasValue()) + pVal->Value >>= aCurVal; + gtk_entry_set_text(GTK_ENTRY(pWidget), + OUStringToOString(aCurVal, RTL_TEXTENCODING_UTF8).getStr()); + } + else if (aCtrlType == "Range" && pCurParent) + { + pWidget = gtk_spin_button_new_with_range(nMinValue, nMaxValue, 1.0); + + sal_Int64 nCurVal = 0; + pVal = m_rController.getValue(aPropertyName); + if (pVal && pVal->Value.hasValue()) + pVal->Value >>= nCurVal; + gtk_spin_button_set_value(GTK_SPIN_BUTTON(pWidget), nCurVal); + } + + lcl_setHelpText(pWidget, aHelpTexts, 0); + m_aControlToPropertyMap[pWidget] = aPropertyName; + + gtk_widget_set_sensitive(pWidget, + m_rController.isUIOptionEnabled(aPropertyName) && pVal != nullptr); + + if (!aText.isEmpty()) + { + GtkWidget* const pLabel = gtk_label_new_with_mnemonic( + OUStringToOString(aText, RTL_TEXTENCODING_UTF8).getStr()); + gtk_label_set_mnemonic_widget(GTK_LABEL(pLabel), pWidget); + gtk_box_pack_start(GTK_BOX(pHbox), pLabel, FALSE, FALSE, 0); + } + + gtk_box_pack_start(GTK_BOX(pHbox), pWidget, FALSE, FALSE, 0); + + pWidget = pHbox; + + } + else + SAL_INFO("vcl.gtk", "unhandled option type: " << aCtrlType); + + GtkWidget* pRow = nullptr; + if (pWidget) + { + if (bUseDependencyRow && !aDependsOnName.isEmpty()) + { + pRow = aPropertyToDependencyRowMap[aDependsOnName + OUString::number(nDependsOnValue)]; + if (!pRow) + { + gtk_widget_destroy(pWidget); + pWidget = nullptr; + } + } + } + if (pWidget) + { + if (!pRow) + { + pRow = gtk_hbox_new(FALSE, 12); + gtk_box_pack_start(GTK_BOX(pCurParent), pRow, FALSE, FALSE, 0); + } + if (!pGroup) + aPropertyToDependencyRowMap[aPropertyName + OUString::number(0)] = pRow; + gtk_box_pack_start(GTK_BOX(pRow), pWidget, FALSE, FALSE, 0); + } + } + } + + CustomTabs_t::const_reverse_iterator aEnd = aCustomTabs.rend(); + for (CustomTabs_t::const_reverse_iterator aI = aCustomTabs.rbegin(); aI != aEnd; ++aI) + { + gtk_widget_show_all(aI->first); + m_xWrapper->print_unix_dialog_add_custom_tab(GTK_PRINT_UNIX_DIALOG(m_pDialog), aI->first, + gtk_label_new(OUStringToOString(aI->second, RTL_TEXTENCODING_UTF8).getStr())); + } +} + +void +GtkPrintDialog::impl_initPrintContent(uno::Sequence<sal_Bool> const& i_rDisabled) +{ + SAL_WARN_IF(i_rDisabled.getLength() != 3, "vcl.gtk", "there is more choices than we expected"); + if (i_rDisabled.getLength() != 3) + return; + + GtkPrintUnixDialog* const pDialog(GTK_PRINT_UNIX_DIALOG(m_pDialog)); + + // XXX: This is a hack that depends on the number and the ordering of + // the controls in the rDisabled sequence (cf. the initialization of + // the "PrintContent" UI option in SwPrintUIOptions::SwPrintUIOptions, + // sw/source/core/view/printdata.cxx) + if (m_xWrapper->supportsPrintSelection() && !i_rDisabled[2]) + { + m_xWrapper->print_unix_dialog_set_support_selection(pDialog, true); + m_xWrapper->print_unix_dialog_set_has_selection(pDialog, true); + } + + beans::PropertyValue* const pPrintContent( + m_rController.getValue(OUString("PrintContent"))); + + if (pPrintContent) + { + sal_Int32 nSelectionType(0); + pPrintContent->Value >>= nSelectionType; + GtkPrintSettings* const pSettings(getSettings()); + GtkPrintPages ePrintPages(GTK_PRINT_PAGES_ALL); + switch (nSelectionType) + { + case 0: + ePrintPages = GTK_PRINT_PAGES_ALL; + break; + case 1: + ePrintPages = GTK_PRINT_PAGES_RANGES; + break; + case 2: + if (m_xWrapper->supportsPrintSelection()) + ePrintPages = GTK_PRINT_PAGES_SELECTION; + else + SAL_INFO("vcl.gtk", "the application wants to print a selection, but the present gtk version does not support it"); + break; + default: + SAL_WARN("vcl.gtk", "unexpected selection type: " << nSelectionType); + } + m_xWrapper->print_settings_set_print_pages(pSettings, ePrintPages); + m_xWrapper->print_unix_dialog_set_settings(pDialog, pSettings); + g_object_unref(G_OBJECT(pSettings)); + } +} + +void +GtkPrintDialog::impl_checkOptionalControlDependencies() +{ + for (auto& rEntry : m_aControlToPropertyMap) + { + gtk_widget_set_sensitive(rEntry.first, m_rController.isUIOptionEnabled(rEntry.second)); + } +} + +beans::PropertyValue* +GtkPrintDialog::impl_queryPropertyValue(GtkWidget* const i_pWidget) const +{ + beans::PropertyValue* pVal(nullptr); + std::map<GtkWidget*, OUString>::const_iterator aIt(m_aControlToPropertyMap.find(i_pWidget)); + if (aIt != m_aControlToPropertyMap.end()) + { + pVal = m_rController.getValue(aIt->second); + SAL_WARN_IF(!pVal, "vcl.gtk", "property value not found"); + } + else + { + SAL_WARN("vcl.gtk", "changed control not in property map"); + } + return pVal; +} + +void +GtkPrintDialog::impl_UIOption_CheckHdl(GtkWidget* const i_pWidget) +{ + beans::PropertyValue* const pVal = impl_queryPropertyValue(i_pWidget); + if (pVal) + { + const bool bVal = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(i_pWidget)); + pVal->Value <<= bVal; + + impl_checkOptionalControlDependencies(); + } +} + +void +GtkPrintDialog::impl_UIOption_RadioHdl(GtkWidget* const i_pWidget) +{ + if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(i_pWidget))) + { + beans::PropertyValue* const pVal = impl_queryPropertyValue(i_pWidget); + std::map<GtkWidget*, sal_Int32>::const_iterator it = m_aControlToNumValMap.find(i_pWidget); + if (pVal && it != m_aControlToNumValMap.end()) + { + + const sal_Int32 nVal = it->second; + pVal->Value <<= nVal; + + impl_checkOptionalControlDependencies(); + } + } +} + +void +GtkPrintDialog::impl_UIOption_SelectHdl(GtkWidget* const i_pWidget) +{ + beans::PropertyValue* const pVal = impl_queryPropertyValue(i_pWidget); + if (pVal) + { + const sal_Int32 nVal(gtk_combo_box_get_active(GTK_COMBO_BOX(i_pWidget))); + pVal->Value <<= nVal; + + impl_checkOptionalControlDependencies(); + } +} + +bool +GtkPrintDialog::run() +{ + bool bDoJob = false; + bool bContinue = true; + while (bContinue) + { + bContinue = false; + const gint nStatus = gtk_dialog_run(GTK_DIALOG(m_pDialog)); + switch (nStatus) + { + case GTK_RESPONSE_HELP: + SAL_WARN("vcl.gtk3", "To-Do: Help ?"); + bContinue = true; + break; + case GTK_RESPONSE_OK: + bDoJob = true; + break; + default: + break; + } + } + gtk_widget_hide(m_pDialog); + impl_storeToSettings(); + return bDoJob; +} + +void +GtkPrintDialog::updateControllerPrintRange() +{ + GtkPrintSettings* const pSettings(getSettings()); + // TODO: use get_print_pages + if (const gchar* const pStr = m_xWrapper->print_settings_get(pSettings, GTK_PRINT_SETTINGS_PRINT_PAGES)) + { + beans::PropertyValue* pVal = m_rController.getValue(OUString("PrintRange")); + if (!pVal) + pVal = m_rController.getValue(OUString("PrintContent")); + SAL_WARN_IF(!pVal, "vcl.gtk", "Nothing to map standard print options to!"); + if (pVal) + { + sal_Int32 nVal = 0; + if (!strcmp(pStr, "all")) + nVal = 0; + else if (!strcmp(pStr, "ranges")) + nVal = 1; + else if (!strcmp(pStr, "selection")) + nVal = 2; + pVal->Value <<= nVal; + + if (nVal == 1) + { + pVal = m_rController.getValue(OUString("PageRange")); + SAL_WARN_IF(!pVal, "vcl.gtk", "PageRange doesn't exist!"); + if (pVal) + { + OUStringBuffer sBuf; + gint num_ranges; + const GtkPageRange* const pRanges = m_xWrapper->print_settings_get_page_ranges(pSettings, &num_ranges); + for (gint i = 0; i != num_ranges && pRanges; ++i) + { + sBuf.append(sal_Int32(pRanges[i].start+1)); + if (pRanges[i].start != pRanges[i].end) + { + sBuf.append('-'); + sBuf.append(sal_Int32(pRanges[i].end+1)); + } + + if (i != num_ranges-1) + sBuf.append(','); + } + pVal->Value <<= sBuf.makeStringAndClear(); + } + } + } + } + g_object_unref(G_OBJECT(pSettings)); +} + +GtkPrintDialog::~GtkPrintDialog() +{ + gtk_widget_destroy(m_pDialog); +} + +void +GtkPrintDialog::impl_readFromSettings() +{ + vcl::SettingsConfigItem* const pItem(vcl::SettingsConfigItem::get()); + GtkPrintSettings* const pSettings(getSettings()); + + const OUString aPrintDialogStr("PrintDialog"); + const OUString aCopyCount(pItem->getValue(aPrintDialogStr, + "CopyCount")); + const OUString aCollate(pItem->getValue(aPrintDialogStr, + "Collate")); + + const gint nOldCopyCount(m_xWrapper->print_settings_get_n_copies(pSettings)); + const sal_Int32 nCopyCount(aCopyCount.toInt32()); + if (nCopyCount > 0 && nOldCopyCount != nCopyCount) + { + m_xWrapper->print_settings_set_n_copies(pSettings, sal::static_int_cast<gint>(nCopyCount)); + } + + const bool bOldCollate(m_xWrapper->print_settings_get_collate(pSettings)); + const bool bCollate(aCollate.equalsIgnoreAsciiCase("true")); + if (bOldCollate != bCollate) + { + m_xWrapper->print_settings_set_collate(pSettings, bCollate); + } + + m_xWrapper->print_unix_dialog_set_settings(GTK_PRINT_UNIX_DIALOG(m_pDialog), pSettings); + g_object_unref(G_OBJECT(pSettings)); +} + +void +GtkPrintDialog::impl_storeToSettings() +const +{ + vcl::SettingsConfigItem* const pItem(vcl::SettingsConfigItem::get()); + GtkPrintSettings* const pSettings(getSettings()); + + const OUString aPrintDialogStr("PrintDialog"); + pItem->setValue(aPrintDialogStr, + "CopyCount", + OUString::number(m_xWrapper->print_settings_get_n_copies(pSettings))); + pItem->setValue(aPrintDialogStr, + "Collate", + m_xWrapper->print_settings_get_collate(pSettings) + ? OUString("true") + : OUString("false")) + ; + // pItem->setValue(aPrintDialog, OUString("ToFile"), ); + g_object_unref(G_OBJECT(pSettings)); + pItem->Commit(); +} + +sal_uInt32 +GtkSalInfoPrinter::GetCapabilities( + const ImplJobSetup* const i_pSetupData, + const PrinterCapType i_nType) +{ + if (i_nType == PrinterCapType::ExternalDialog && lcl_useSystemPrintDialog()) + return 1; + return PspSalInfoPrinter::GetCapabilities(i_pSetupData, i_nType); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/gtkprintwrapper.hxx b/vcl/unx/gtk3/gtkprintwrapper.hxx new file mode 100644 index 000000000..9a0c8e3ee --- /dev/null +++ b/vcl/unx/gtk3/gtkprintwrapper.hxx @@ -0,0 +1,17 @@ +/* -*- 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/. + */ + +#ifndef INCLUDED_VCL_UNX_GTK3_INC_GTKPRINTWRAPPER_HXX +#define INCLUDED_VCL_UNX_GTK3_INC_GTKPRINTWRAPPER_HXX + +#include "gtk/gtkprintwrapper.hxx" + +#endif // INCLUDED_VCL_UNX_GTK3_INC_GTKPRINTWRAPPER_HXX + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/FPServiceInfo.hxx b/vcl/unx/gtk3_kde5/FPServiceInfo.hxx new file mode 100644 index 000000000..1fbb8fd27 --- /dev/null +++ b/vcl/unx/gtk3_kde5/FPServiceInfo.hxx @@ -0,0 +1,28 @@ +/* -*- 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#pragma once + +// the service names +#define FILE_PICKER_SERVICE_NAME "com.sun.star.ui.dialogs.Gtk3KDE5FilePicker" + +// the implementation names +#define FILE_PICKER_IMPL_NAME "com.sun.star.ui.dialogs.Gtk3KDE5FilePicker" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkaction.cxx b/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkaction.cxx new file mode 100644 index 000000000..b05929b90 --- /dev/null +++ b/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkaction.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/a11y/gtk3atkaction.cxx" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkbridge.cxx b/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkbridge.cxx new file mode 100644 index 000000000..d31e5e429 --- /dev/null +++ b/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkbridge.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/a11y/gtk3atkbridge.cxx" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkcomponent.cxx b/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkcomponent.cxx new file mode 100644 index 000000000..63f44a3a1 --- /dev/null +++ b/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkcomponent.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/a11y/gtk3atkcomponent.cxx" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkeditabletext.cxx b/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkeditabletext.cxx new file mode 100644 index 000000000..064e72d98 --- /dev/null +++ b/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkeditabletext.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/a11y/gtk3atkeditabletext.cxx" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkfactory.cxx b/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkfactory.cxx new file mode 100644 index 000000000..86d9ac43a --- /dev/null +++ b/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkfactory.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/a11y/gtk3atkfactory.cxx" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkhypertext.cxx b/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkhypertext.cxx new file mode 100644 index 000000000..d2ce059aa --- /dev/null +++ b/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkhypertext.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/a11y/gtk3atkhypertext.cxx" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkimage.cxx b/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkimage.cxx new file mode 100644 index 000000000..3a1234e07 --- /dev/null +++ b/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkimage.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/a11y/gtk3atkimage.cxx" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atklistener.cxx b/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atklistener.cxx new file mode 100644 index 000000000..51d53bf91 --- /dev/null +++ b/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atklistener.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/a11y/gtk3atklistener.cxx" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkregistry.cxx b/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkregistry.cxx new file mode 100644 index 000000000..f5e791720 --- /dev/null +++ b/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkregistry.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/a11y/gtk3atkregistry.cxx" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkselection.cxx b/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkselection.cxx new file mode 100644 index 000000000..ab39c0e13 --- /dev/null +++ b/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkselection.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/a11y/gtk3atkselection.cxx" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atktable.cxx b/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atktable.cxx new file mode 100644 index 000000000..194791b68 --- /dev/null +++ b/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atktable.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/a11y/gtk3atktable.cxx" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atktext.cxx b/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atktext.cxx new file mode 100644 index 000000000..8d668e6e6 --- /dev/null +++ b/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atktext.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/a11y/gtk3atktext.cxx" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atktextattributes.cxx b/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atktextattributes.cxx new file mode 100644 index 000000000..c767a95d0 --- /dev/null +++ b/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atktextattributes.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/a11y/gtk3atktextattributes.cxx" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkutil.cxx b/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkutil.cxx new file mode 100644 index 000000000..39eb5aeeb --- /dev/null +++ b/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkutil.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/a11y/gtk3atkutil.cxx" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkvalue.cxx b/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkvalue.cxx new file mode 100644 index 000000000..5a526e1cf --- /dev/null +++ b/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkvalue.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/a11y/gtk3atkvalue.cxx" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkwrapper.cxx b/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkwrapper.cxx new file mode 100644 index 000000000..b0029f273 --- /dev/null +++ b/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkwrapper.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/a11y/gtk3atkwrapper.cxx" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/filepicker_ipc_commands.hxx b/vcl/unx/gtk3_kde5/filepicker_ipc_commands.hxx new file mode 100644 index 000000000..1d0925257 --- /dev/null +++ b/vcl/unx/gtk3_kde5/filepicker_ipc_commands.hxx @@ -0,0 +1,173 @@ +/* -*- 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#pragma once + +#include <cstdint> +#include <iostream> +#include <vector> + +#include <sal/types.h> +#include <com/sun/star/uno/Sequence.hxx> + +// #define DEBUG_FILEPICKER_IPC + +namespace rtl +{ +class OUString; +} +class QString; + +enum class Commands : uint16_t +{ + SetTitle, + SetWinId, + Execute, + SetMultiSelectionMode, + SetDefaultName, + SetDisplayDirectory, + GetDisplayDirectory, + GetSelectedFiles, + AppendFilter, + SetCurrentFilter, + GetCurrentFilter, + SetValue, + GetValue, + EnableControl, + SetLabel, + GetLabel, + AddCheckBox, + Initialize, + Quit, + EnablePickFolderMode, +}; + +inline std::vector<char> readIpcStringArg(std::istream& stream) +{ + uint32_t length = 0; + stream >> length; + stream.ignore(); // skip space separator + std::vector<char> buffer(length, '\0'); + stream.read(buffer.data(), length); + return buffer; +} + +void readIpcArg(std::istream& stream, OUString& string); +void readIpcArg(std::istream& stream, QString& string); +void readIpcArg(std::istream& stream, css::uno::Sequence<OUString>& seq); + +inline void readIpcArg(std::istream& stream, Commands& value) +{ + uint16_t v = 0; + stream >> v; + stream.ignore(); // skip space + value = static_cast<Commands>(v); +} + +void readIpcArg(std::istream&, sal_Bool) = delete; + +inline void readIpcArg(std::istream& stream, bool& value) +{ + stream >> value; + stream.ignore(); // skip space +} + +inline void readIpcArg(std::istream& stream, sal_Int16& value) +{ + stream >> value; + stream.ignore(); // skip space +} + +inline void readIpcArg(std::istream& stream, sal_uIntPtr& value) +{ + stream >> value; + stream.ignore(); // skip space +} + +#if SAL_TYPES_SIZEOFPOINTER == 4 +inline void readIpcArg(std::istream& stream, uint64_t& value) +{ + stream >> value; + stream.ignore(); // skip space +} +#endif + +inline void readIpcArgs(std::istream& /*stream*/) +{ + // end of arguments, nothing to do +} + +template <typename T, typename... Args> +inline void readIpcArgs(std::istream& stream, T& arg, Args&... args) +{ + readIpcArg(stream, arg); + readIpcArgs(stream, args...); +} + +void sendIpcArg(std::ostream& stream, const OUString& string); +void sendIpcArg(std::ostream& stream, const QString& string); + +inline void sendIpcStringArg(std::ostream& stream, uint32_t length, const char* string) +{ + stream << length << ' '; + stream.write(string, length); + stream << ' '; +} + +inline void sendIpcArg(std::ostream& stream, Commands value) +{ + stream << static_cast<uint16_t>(value) << ' '; +} + +void sendIpcArg(std::ostream&, sal_Bool) = delete; + +inline void sendIpcArg(std::ostream& stream, bool value) { stream << value << ' '; } + +inline void sendIpcArg(std::ostream& stream, sal_Int16 value) { stream << value << ' '; } + +inline void sendIpcArg(std::ostream& stream, sal_uIntPtr value) { stream << value << ' '; } + +#if SAL_TYPES_SIZEOFPOINTER == 4 +inline void sendIpcArg(std::ostream& stream, uint64_t value) { stream << value << ' '; } +#endif + +inline void sendIpcArgsImpl(std::ostream& stream) +{ + // end of arguments, flush stream + stream << std::endl; +} + +template <typename T, typename... Args> +inline void sendIpcArgsImpl(std::ostream& stream, const T& arg, const Args&... args) +{ + sendIpcArg(stream, arg); + sendIpcArgsImpl(stream, args...); +} + +template <typename T, typename... Args> +inline void sendIpcArgs(std::ostream& stream, const T& arg, const Args&... args) +{ + sendIpcArgsImpl(stream, arg, args...); +#ifdef DEBUG_FILEPICKER_IPC + std::cerr << "IPC MSG: "; + sendIpcArgsImpl(std::cerr, arg, args...); +#endif +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/gtk3_kde5_a11y.cxx b/vcl/unx/gtk3_kde5/gtk3_kde5_a11y.cxx new file mode 100644 index 000000000..02fd47a60 --- /dev/null +++ b/vcl/unx/gtk3_kde5/gtk3_kde5_a11y.cxx @@ -0,0 +1,38 @@ +/* -*- 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include "../gtk3/a11y/gtk3atkaction.cxx" +#include "../gtk3/a11y/gtk3atkbridge.cxx" +#include "../gtk3/a11y/gtk3atkcomponent.cxx" +#include "../gtk3/a11y/gtk3atkeditabletext.cxx" +#include "../gtk3/a11y/gtk3atkfactory.cxx" +#include "../gtk3/a11y/gtk3atkhypertext.cxx" +#include "../gtk3/a11y/gtk3atkimage.cxx" +#include "../gtk3/a11y/gtk3atklistener.cxx" +#include "../gtk3/a11y/gtk3atkregistry.cxx" +#include "../gtk3/a11y/gtk3atkselection.cxx" +#include "../gtk3/a11y/gtk3atktable.cxx" +#include "../gtk3/a11y/gtk3atktextattributes.cxx" +#include "../gtk3/a11y/gtk3atktext.cxx" +#include "../gtk3/a11y/gtk3atkutil.cxx" +#include "../gtk3/a11y/gtk3atkvalue.cxx" +#include "../gtk3/a11y/gtk3atkwindow.cxx" +#include "../gtk3/a11y/gtk3atkwrapper.cxx" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/gtk3_kde5_cairo.cxx b/vcl/unx/gtk3_kde5/gtk3_kde5_cairo.cxx new file mode 100644 index 000000000..fc271f160 --- /dev/null +++ b/vcl/unx/gtk3_kde5/gtk3_kde5_cairo.cxx @@ -0,0 +1,22 @@ +/* -*- 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include "../gtk3/cairo_gtk3_cairo.cxx" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/gtk3_kde5_filepicker.cxx b/vcl/unx/gtk3_kde5/gtk3_kde5_filepicker.cxx new file mode 100644 index 000000000..eec532d40 --- /dev/null +++ b/vcl/unx/gtk3_kde5/gtk3_kde5_filepicker.cxx @@ -0,0 +1,456 @@ +/* -*- 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <QUrl> +#include <KFileWidget> + +#include "gtk3_kde5_filepicker.hxx" + +#include <com/sun/star/lang/IllegalArgumentException.hpp> +#include <cppuhelper/supportsservice.hxx> +#include <com/sun/star/ui/dialogs/TemplateDescription.hpp> +#include <com/sun/star/ui/dialogs/CommonFilePickerElementIds.hpp> +#include <com/sun/star/ui/dialogs/ExtendedFilePickerElementIds.hpp> + +#include <sal/log.hxx> +#include <vcl/svapp.hxx> + +#include "FPServiceInfo.hxx" + +#undef Region + +#include <strings.hrc> + +using namespace ::com::sun::star; +using namespace ::com::sun::star::ui::dialogs; +using namespace ::com::sun::star::ui::dialogs::TemplateDescription; +using namespace ::com::sun::star::ui::dialogs::ExtendedFilePickerElementIds; +using namespace ::com::sun::star::ui::dialogs::CommonFilePickerElementIds; +using namespace ::com::sun::star::lang; +using namespace ::com::sun::star::beans; +using namespace ::com::sun::star::uno; + +// helper functions + +namespace +{ +uno::Sequence<OUString> FilePicker_getSupportedServiceNames() +{ + return { "com.sun.star.ui.dialogs.FilePicker", "com.sun.star.ui.dialogs.SystemFilePicker", + "com.sun.star.ui.dialogs.Gtk3KDE5FilePicker" }; +} +} + +// Gtk3KDE5FilePicker + +Gtk3KDE5FilePicker::Gtk3KDE5FilePicker(const uno::Reference<uno::XComponentContext>&) + : Gtk3KDE5FilePicker_Base(_helperMutex) +{ + setMultiSelectionMode(false); + + // tdf#124598 dummy KWidget use to make gtk3_kde5 VCL plugin link against KIO libraries + QString sDummyStr; + QUrl aUrl = KFileWidget::getStartUrl(QUrl(), sDummyStr); + aUrl.setPath("/dev/null"); +} + +Gtk3KDE5FilePicker::~Gtk3KDE5FilePicker() = default; + +void SAL_CALL +Gtk3KDE5FilePicker::addFilePickerListener(const uno::Reference<XFilePickerListener>& xListener) +{ + SolarMutexGuard aGuard; + m_xListener = xListener; +} + +void SAL_CALL +Gtk3KDE5FilePicker::removeFilePickerListener(const uno::Reference<XFilePickerListener>&) +{ + SolarMutexGuard aGuard; + m_xListener.clear(); +} + +void SAL_CALL Gtk3KDE5FilePicker::setTitle(const OUString& title) +{ + m_ipc.sendCommand(Commands::SetTitle, title); +} + +sal_Int16 SAL_CALL Gtk3KDE5FilePicker::execute() +{ + SolarMutexGuard g; + return m_ipc.execute(); +} + +void SAL_CALL Gtk3KDE5FilePicker::setMultiSelectionMode(sal_Bool multiSelect) +{ + m_ipc.sendCommand(Commands::SetMultiSelectionMode, bool(multiSelect)); +} + +void SAL_CALL Gtk3KDE5FilePicker::setDefaultName(const OUString& name) +{ + m_ipc.sendCommand(Commands::SetDefaultName, name); +} + +void SAL_CALL Gtk3KDE5FilePicker::setDisplayDirectory(const OUString& dir) +{ + m_ipc.sendCommand(Commands::SetDisplayDirectory, dir); +} + +OUString SAL_CALL Gtk3KDE5FilePicker::getDisplayDirectory() +{ + auto id = m_ipc.sendCommand(Commands::GetDisplayDirectory); + OUString dir; + m_ipc.readResponse(id, dir); + return dir; +} + +uno::Sequence<OUString> SAL_CALL Gtk3KDE5FilePicker::getFiles() +{ + uno::Sequence<OUString> seq = getSelectedFiles(); + if (seq.getLength() > 1) + seq.realloc(1); + return seq; +} + +uno::Sequence<OUString> SAL_CALL Gtk3KDE5FilePicker::getSelectedFiles() +{ + auto id = m_ipc.sendCommand(Commands::GetSelectedFiles); + uno::Sequence<OUString> seq; + m_ipc.readResponse(id, seq); + return seq; +} + +void SAL_CALL Gtk3KDE5FilePicker::appendFilter(const OUString& title, const OUString& filter) +{ + m_ipc.sendCommand(Commands::AppendFilter, title, filter); +} + +void SAL_CALL Gtk3KDE5FilePicker::setCurrentFilter(const OUString& title) +{ + m_ipc.sendCommand(Commands::SetCurrentFilter, title); +} + +OUString SAL_CALL Gtk3KDE5FilePicker::getCurrentFilter() +{ + auto id = m_ipc.sendCommand(Commands::GetCurrentFilter); + OUString filter; + m_ipc.readResponse(id, filter); + return filter; +} + +void SAL_CALL Gtk3KDE5FilePicker::appendFilterGroup(const OUString& /*rGroupTitle*/, + const uno::Sequence<beans::StringPair>& filters) +{ + const sal_uInt16 length = filters.getLength(); + for (sal_uInt16 i = 0; i < length; ++i) + { + beans::StringPair aPair = filters[i]; + appendFilter(aPair.First, aPair.Second); + } +} + +void SAL_CALL Gtk3KDE5FilePicker::setValue(sal_Int16 controlId, sal_Int16 nControlAction, + const uno::Any& value) +{ + if (value.has<bool>()) + { + m_ipc.sendCommand(Commands::SetValue, controlId, nControlAction, value.get<bool>()); + } + else + { + SAL_INFO("vcl.gtkkde5", "set value of unhandled type " << controlId); + } +} + +uno::Any SAL_CALL Gtk3KDE5FilePicker::getValue(sal_Int16 controlId, sal_Int16 nControlAction) +{ + if (CHECKBOX_AUTOEXTENSION == controlId) + // We ignore this one and rely on QFileDialog to provide the function. + // Always return false, to pretend we do not support this, otherwise + // LO core would try to be smart and cut the extension in some places, + // interfering with QFileDialog's handling of it. QFileDialog also + // saves the value of the setting, so LO core is not needed for that either. + return uno::Any(false); + + auto id = m_ipc.sendCommand(Commands::GetValue, controlId, nControlAction); + + bool value = false; + m_ipc.readResponse(id, value); + + return uno::Any(value); +} + +void SAL_CALL Gtk3KDE5FilePicker::enableControl(sal_Int16 controlId, sal_Bool enable) +{ + m_ipc.sendCommand(Commands::EnableControl, controlId, bool(enable)); +} + +void SAL_CALL Gtk3KDE5FilePicker::setLabel(sal_Int16 controlId, const OUString& label) +{ + m_ipc.sendCommand(Commands::SetLabel, controlId, label); +} + +OUString SAL_CALL Gtk3KDE5FilePicker::getLabel(sal_Int16 controlId) +{ + auto id = m_ipc.sendCommand(Commands::GetLabel, controlId); + OUString label; + m_ipc.readResponse(id, label); + return label; +} + +void Gtk3KDE5FilePicker::addCustomControl(sal_Int16 controlId) +{ + const char* resId = nullptr; + + switch (controlId) + { + case CHECKBOX_AUTOEXTENSION: + resId = STR_FPICKER_AUTO_EXTENSION; + break; + case CHECKBOX_PASSWORD: + resId = STR_FPICKER_PASSWORD; + break; + case CHECKBOX_FILTEROPTIONS: + resId = STR_FPICKER_FILTER_OPTIONS; + break; + case CHECKBOX_READONLY: + resId = STR_FPICKER_READONLY; + break; + case CHECKBOX_LINK: + resId = STR_FPICKER_INSERT_AS_LINK; + break; + case CHECKBOX_PREVIEW: + resId = STR_FPICKER_SHOW_PREVIEW; + break; + case CHECKBOX_SELECTION: + resId = STR_FPICKER_SELECTION; + break; + case CHECKBOX_GPGENCRYPTION: + resId = STR_FPICKER_GPGENCRYPT; + break; + case PUSHBUTTON_PLAY: + resId = STR_FPICKER_PLAY; + break; + case LISTBOX_VERSION: + resId = STR_FPICKER_VERSION; + break; + case LISTBOX_TEMPLATE: + resId = STR_FPICKER_TEMPLATES; + break; + case LISTBOX_IMAGE_TEMPLATE: + resId = STR_FPICKER_IMAGE_TEMPLATE; + break; + case LISTBOX_IMAGE_ANCHOR: + resId = STR_FPICKER_IMAGE_ANCHOR; + break; + case LISTBOX_VERSION_LABEL: + case LISTBOX_TEMPLATE_LABEL: + case LISTBOX_IMAGE_TEMPLATE_LABEL: + case LISTBOX_IMAGE_ANCHOR_LABEL: + case LISTBOX_FILTER_SELECTOR: + break; + } + + switch (controlId) + { + case CHECKBOX_AUTOEXTENSION: + case CHECKBOX_PASSWORD: + case CHECKBOX_FILTEROPTIONS: + case CHECKBOX_READONLY: + case CHECKBOX_LINK: + case CHECKBOX_PREVIEW: + case CHECKBOX_SELECTION: + case CHECKBOX_GPGENCRYPTION: + { + // the checkbox is created even for CHECKBOX_AUTOEXTENSION to simplify + // code, but the checkbox is hidden and ignored + bool hidden = controlId == CHECKBOX_AUTOEXTENSION; + + m_ipc.sendCommand(Commands::AddCheckBox, controlId, hidden, getResString(resId)); + + break; + } + case PUSHBUTTON_PLAY: + case LISTBOX_VERSION: + case LISTBOX_TEMPLATE: + case LISTBOX_IMAGE_TEMPLATE: + case LISTBOX_IMAGE_ANCHOR: + case LISTBOX_VERSION_LABEL: + case LISTBOX_TEMPLATE_LABEL: + case LISTBOX_IMAGE_TEMPLATE_LABEL: + case LISTBOX_IMAGE_ANCHOR_LABEL: + case LISTBOX_FILTER_SELECTOR: + break; + } +} + +void SAL_CALL Gtk3KDE5FilePicker::initialize(const uno::Sequence<uno::Any>& args) +{ + // parameter checking + uno::Any arg; + if (args.getLength() == 0) + { + throw lang::IllegalArgumentException("no arguments", static_cast<XFilePicker2*>(this), 1); + } + + arg = args[0]; + + if ((arg.getValueType() != cppu::UnoType<sal_Int16>::get()) + && (arg.getValueType() != cppu::UnoType<sal_Int8>::get())) + { + throw lang::IllegalArgumentException("invalid argument type", + static_cast<XFilePicker2*>(this), 1); + } + + sal_Int16 templateId = -1; + arg >>= templateId; + + bool saveDialog = false; + switch (templateId) + { + case FILEOPEN_SIMPLE: + break; + + case FILESAVE_SIMPLE: + saveDialog = true; + break; + + case FILESAVE_AUTOEXTENSION: + saveDialog = true; + addCustomControl(CHECKBOX_AUTOEXTENSION); + break; + + case FILESAVE_AUTOEXTENSION_PASSWORD: + { + saveDialog = true; + addCustomControl(CHECKBOX_PASSWORD); + addCustomControl(CHECKBOX_GPGENCRYPTION); + break; + } + case FILESAVE_AUTOEXTENSION_PASSWORD_FILTEROPTIONS: + { + saveDialog = true; + addCustomControl(CHECKBOX_AUTOEXTENSION); + addCustomControl(CHECKBOX_PASSWORD); + addCustomControl(CHECKBOX_GPGENCRYPTION); + addCustomControl(CHECKBOX_FILTEROPTIONS); + break; + } + case FILESAVE_AUTOEXTENSION_SELECTION: + saveDialog = true; + addCustomControl(CHECKBOX_AUTOEXTENSION); + addCustomControl(CHECKBOX_SELECTION); + break; + + case FILESAVE_AUTOEXTENSION_TEMPLATE: + saveDialog = true; + addCustomControl(CHECKBOX_AUTOEXTENSION); + addCustomControl(LISTBOX_TEMPLATE); + break; + + case FILEOPEN_LINK_PREVIEW_IMAGE_TEMPLATE: + addCustomControl(CHECKBOX_LINK); + addCustomControl(CHECKBOX_PREVIEW); + addCustomControl(LISTBOX_IMAGE_TEMPLATE); + break; + + case FILEOPEN_LINK_PREVIEW_IMAGE_ANCHOR: + addCustomControl(CHECKBOX_LINK); + addCustomControl(CHECKBOX_PREVIEW); + addCustomControl(LISTBOX_IMAGE_ANCHOR); + break; + + case FILEOPEN_PLAY: + addCustomControl(PUSHBUTTON_PLAY); + break; + + case FILEOPEN_LINK_PLAY: + addCustomControl(CHECKBOX_LINK); + addCustomControl(PUSHBUTTON_PLAY); + break; + + case FILEOPEN_READONLY_VERSION: + addCustomControl(CHECKBOX_READONLY); + addCustomControl(LISTBOX_VERSION); + break; + + case FILEOPEN_LINK_PREVIEW: + addCustomControl(CHECKBOX_LINK); + addCustomControl(CHECKBOX_PREVIEW); + break; + + case FILEOPEN_PREVIEW: + addCustomControl(CHECKBOX_PREVIEW); + break; + + default: + SAL_INFO("vcl.gtkkde5", "unknown templates " << templateId); + return; + } + + setTitle(getResString(saveDialog ? STR_FPICKER_SAVE : STR_FPICKER_OPEN)); + + m_ipc.sendCommand(Commands::Initialize, saveDialog); +} + +void SAL_CALL Gtk3KDE5FilePicker::cancel() +{ + // TODO +} + +void Gtk3KDE5FilePicker::disposing(const lang::EventObject& rEvent) +{ + uno::Reference<XFilePickerListener> xFilePickerListener(rEvent.Source, uno::UNO_QUERY); + + if (xFilePickerListener.is()) + { + removeFilePickerListener(xFilePickerListener); + } +} + +OUString SAL_CALL Gtk3KDE5FilePicker::getImplementationName() { return FILE_PICKER_IMPL_NAME; } + +sal_Bool SAL_CALL Gtk3KDE5FilePicker::supportsService(const OUString& ServiceName) +{ + return cppu::supportsService(this, ServiceName); +} + +uno::Sequence<OUString> SAL_CALL Gtk3KDE5FilePicker::getSupportedServiceNames() +{ + return FilePicker_getSupportedServiceNames(); +} + +void Gtk3KDE5FilePicker::filterChanged() +{ + FilePickerEvent aEvent; + aEvent.ElementId = LISTBOX_FILTER; + SAL_INFO("vcl.gtkkde5", "filter changed"); + if (m_xListener.is()) + m_xListener->controlStateChanged(aEvent); +} + +void Gtk3KDE5FilePicker::selectionChanged() +{ + FilePickerEvent aEvent; + SAL_INFO("vcl.gtkkde5", "file selection changed"); + if (m_xListener.is()) + m_xListener->fileSelectionChanged(aEvent); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/gtk3_kde5_filepicker.hxx b/vcl/unx/gtk3_kde5/gtk3_kde5_filepicker.hxx new file mode 100644 index 000000000..7ce391004 --- /dev/null +++ b/vcl/unx/gtk3_kde5/gtk3_kde5_filepicker.hxx @@ -0,0 +1,131 @@ +/* -*- 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#pragma once + +#include <cppuhelper/compbase.hxx> + +#include <com/sun/star/lang/XServiceInfo.hpp> +#include <com/sun/star/lang/XInitialization.hpp> +#include <com/sun/star/ui/dialogs/XFilePicker3.hpp> +#include <com/sun/star/ui/dialogs/XFilePickerControlAccess.hpp> +#include <com/sun/star/uno/XComponentContext.hpp> + +#include <osl/mutex.hxx> + +#include "gtk3_kde5_filepicker_ipc.hxx" + +typedef ::cppu::WeakComponentImplHelper<css::ui::dialogs::XFilePicker3, + css::ui::dialogs::XFilePickerControlAccess + // TODO css::ui::dialogs::XFilePreview + , + css::lang::XInitialization, css::lang::XServiceInfo> + Gtk3KDE5FilePicker_Base; + +class Gtk3KDE5FilePicker : public Gtk3KDE5FilePicker_Base +{ +protected: + css::uno::Reference<css::ui::dialogs::XFilePickerListener> m_xListener; + + osl::Mutex _helperMutex; + Gtk3KDE5FilePickerIpc m_ipc; + +public: + explicit Gtk3KDE5FilePicker(const css::uno::Reference<css::uno::XComponentContext>&); + virtual ~Gtk3KDE5FilePicker() override; + + // XFilePickerNotifier + virtual void SAL_CALL addFilePickerListener( + const css::uno::Reference<css::ui::dialogs::XFilePickerListener>& xListener) override; + virtual void SAL_CALL removeFilePickerListener( + const css::uno::Reference<css::ui::dialogs::XFilePickerListener>& xListener) override; + + // XExecutableDialog functions + virtual void SAL_CALL setTitle(const OUString& rTitle) override; + virtual sal_Int16 SAL_CALL execute() override; + + // XFilePicker functions + virtual void SAL_CALL setMultiSelectionMode(sal_Bool bMode) override; + virtual void SAL_CALL setDefaultName(const OUString& rName) override; + virtual void SAL_CALL setDisplayDirectory(const OUString& rDirectory) override; + virtual OUString SAL_CALL getDisplayDirectory() override; + virtual css::uno::Sequence<OUString> SAL_CALL getFiles() override; + + // XFilterManager functions + virtual void SAL_CALL appendFilter(const OUString& rTitle, const OUString& rFilter) override; + virtual void SAL_CALL setCurrentFilter(const OUString& rTitle) override; + virtual OUString SAL_CALL getCurrentFilter() override; + + // XFilterGroupManager functions + virtual void SAL_CALL + appendFilterGroup(const OUString& rGroupTitle, + const css::uno::Sequence<css::beans::StringPair>& rFilters) override; + + // XFilePickerControlAccess functions + virtual void SAL_CALL setValue(sal_Int16 nControlId, sal_Int16 nControlAction, + const css::uno::Any& rValue) override; + virtual css::uno::Any SAL_CALL getValue(sal_Int16 nControlId, + sal_Int16 nControlAction) override; + virtual void SAL_CALL enableControl(sal_Int16 nControlId, sal_Bool bEnable) override; + virtual void SAL_CALL setLabel(sal_Int16 nControlId, const OUString& rLabel) override; + virtual OUString SAL_CALL getLabel(sal_Int16 nControlId) override; + + /* TODO XFilePreview + + virtual css::uno::Sequence< sal_Int16 > SAL_CALL getSupportedImageFormats( ); + virtual sal_Int32 SAL_CALL getTargetColorDepth( ); + virtual sal_Int32 SAL_CALL getAvailableWidth( ); + virtual sal_Int32 SAL_CALL getAvailableHeight( ); + virtual void SAL_CALL setImage( sal_Int16 aImageFormat, const css::uno::Any &rImage ); + virtual sal_Bool SAL_CALL setShowState( sal_Bool bShowState ); + virtual sal_Bool SAL_CALL getShowState( ); + */ + + // XFilePicker2 functions + virtual css::uno::Sequence<OUString> SAL_CALL getSelectedFiles() override; + + // XInitialization + virtual void SAL_CALL initialize(const css::uno::Sequence<css::uno::Any>& rArguments) override; + + // XCancellable + virtual void SAL_CALL cancel() override; + + // XEventListener + virtual void disposing(const css::lang::EventObject& rEvent); + using cppu::WeakComponentImplHelperBase::disposing; + + // XServiceInfo + virtual OUString SAL_CALL getImplementationName() override; + virtual sal_Bool SAL_CALL supportsService(const OUString& rServiceName) override; + virtual css::uno::Sequence<OUString> SAL_CALL getSupportedServiceNames() override; + +private: + Gtk3KDE5FilePicker(const Gtk3KDE5FilePicker&) = delete; + Gtk3KDE5FilePicker& operator=(const Gtk3KDE5FilePicker&) = delete; + + //add a custom control widget to the file dialog + void addCustomControl(sal_Int16 controlId); + + // emit XFilePickerListener controlStateChanged event + void filterChanged(); + // emit XFilePickerListener fileSelectionChanged event + void selectionChanged(); +}; + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/gtk3_kde5_filepicker_ipc.cxx b/vcl/unx/gtk3_kde5/gtk3_kde5_filepicker_ipc.cxx new file mode 100644 index 000000000..80c15938b --- /dev/null +++ b/vcl/unx/gtk3_kde5/gtk3_kde5_filepicker_ipc.cxx @@ -0,0 +1,275 @@ +/* -*- 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include "gtk3_kde5_filepicker_ipc.hxx" + +#undef Region + +#include <system_error> + +#include <com/sun/star/ui/dialogs/ExecutableDialogResults.hpp> + +#include <vcl/svapp.hxx> +#include <vcl/sysdata.hxx> +#include <vcl/syswin.hxx> + +#include <osl/file.h> +#include <osl/process.h> + +#include <gtk/gtk.h> + +#include <boost/filesystem/path.hpp> + +#include <svdata.hxx> + +using namespace ::com::sun::star::ui::dialogs; + +// helper functions + +namespace +{ +OUString applicationDirPath() +{ + OUString applicationFilePath; + osl_getExecutableFile(&applicationFilePath.pData); + OUString applicationSystemPath; + osl_getSystemPathFromFileURL(applicationFilePath.pData, &applicationSystemPath.pData); + const auto utf8Path = applicationSystemPath.toUtf8(); + auto ret = boost::filesystem::path(utf8Path.getStr(), utf8Path.getStr() + utf8Path.getLength()); + ret.remove_filename(); + return OUString::fromUtf8(OString(ret.c_str(), strlen(ret.c_str()))); +} + +OUString findPickerExecutable() +{ + const auto path = applicationDirPath(); + const OUString app("lo_kde5filepicker"); + OUString ret; + osl_searchFileURL(app.pData, path.pData, &ret.pData); + if (ret.isEmpty()) + throw std::system_error(std::make_error_code(std::errc::no_such_file_or_directory), + "could not find lo_kde5filepicker executable"); + return ret; +} +} + +void readIpcArg(std::istream& stream, OUString& str) +{ + const auto buffer = readIpcStringArg(stream); + str = OUString::fromUtf8(OString(buffer.data(), buffer.size())); +} + +void readIpcArg(std::istream& stream, css::uno::Sequence<OUString>& seq) +{ + uint32_t numFiles = 0; + stream >> numFiles; + stream.ignore(); // skip space; + seq.realloc(numFiles); + for (size_t i = 0; i < numFiles; ++i) + { + readIpcArg(stream, seq[i]); + } +} + +void sendIpcArg(std::ostream& stream, const OUString& string) +{ + const auto utf8 = string.toUtf8(); + sendIpcStringArg(stream, utf8.getLength(), utf8.getStr()); +} + +OUString getResString(const char* pResId) +{ + if (pResId == nullptr) + return {}; + + return VclResId(pResId); +} + +// handles the IPC commands for dialog execution and ends the dummy Gtk dialog once the IPC response is there +static void handleIpcForExecute(Gtk3KDE5FilePickerIpc* pFilePickerIpc, GtkWidget* pDummyDialog, + bool* bResult) +{ + auto id = pFilePickerIpc->sendCommand(Commands::Execute); + pFilePickerIpc->readResponse(id, *bResult); + + // end the dummy dialog + gtk_widget_hide(pDummyDialog); +} + +// Gtk3KDE5FilePicker + +Gtk3KDE5FilePickerIpc::Gtk3KDE5FilePickerIpc() +{ + const auto exe = findPickerExecutable(); + oslProcessError result; + oslSecurity pSecurity = osl_getCurrentSecurity(); + result = osl_executeProcess_WithRedirectedIO(exe.pData, nullptr, 0, osl_Process_NORMAL, + pSecurity, nullptr, nullptr, 0, &m_process, + &m_inputWrite, &m_outputRead, nullptr); + osl_freeSecurityHandle(pSecurity); + if (result != osl_Process_E_None) + throw std::system_error(std::make_error_code(std::errc::no_such_process), + "could not start lo_kde5filepicker executable"); +} + +Gtk3KDE5FilePickerIpc::~Gtk3KDE5FilePickerIpc() +{ + if (!m_process) + return; + + sendCommand(Commands::Quit); + osl_joinProcess(m_process); + + if (m_inputWrite) + osl_closeFile(m_inputWrite); + if (m_outputRead) + osl_closeFile(m_outputRead); + osl_freeProcessHandle(m_process); +} + +sal_Int16 Gtk3KDE5FilePickerIpc::execute() +{ + auto restoreMainWindow = blockMainWindow(); + + // dummy gtk dialog that will take care of processing events, + // not meant to be actually seen by user + GtkWidget* pDummyDialog = gtk_dialog_new(); + + bool accepted = false; + + // send IPC command and read response in a separate thread + std::thread aIpcHandler(&handleIpcForExecute, this, pDummyDialog, &accepted); + + // make dummy dialog not to be seen by user + gtk_window_set_decorated(GTK_WINDOW(pDummyDialog), false); + gtk_window_set_default_size(GTK_WINDOW(pDummyDialog), 0, 0); + gtk_window_set_accept_focus(GTK_WINDOW(pDummyDialog), false); + // gtk_widget_set_opacity() only has the desired effect when widget is already shown + gtk_widget_show(pDummyDialog); + gtk_widget_set_opacity(pDummyDialog, 0); + // run dialog, leaving event processing to GTK + // dialog will be closed by the separate 'aIpcHandler' thread once the IPC response is there + gtk_dialog_run(GTK_DIALOG(pDummyDialog)); + + aIpcHandler.join(); + + gtk_widget_destroy(pDummyDialog); + + if (restoreMainWindow) + restoreMainWindow(); + + return accepted ? ExecutableDialogResults::OK : ExecutableDialogResults::CANCEL; +} + +static gboolean ignoreDeleteEvent(GtkWidget* /*widget*/, GdkEvent* /*event*/, + gpointer /*user_data*/) +{ + return true; +} + +std::function<void()> Gtk3KDE5FilePickerIpc::blockMainWindow() +{ + vcl::Window* pParentWin = Application::GetDefDialogParent(); + if (!pParentWin) + return {}; + + const SystemEnvData* pSysData = static_cast<SystemWindow*>(pParentWin)->GetSystemData(); + if (!pSysData) + return {}; + + sendCommand(Commands::SetWinId, pSysData->aWindow); + + auto* pMainWindow = static_cast<GtkWidget*>(pSysData->pWidget); + if (!pMainWindow) + return {}; + + SolarMutexGuard guard; + auto deleteEventSignalId = g_signal_lookup("delete_event", gtk_widget_get_type()); + + // disable the mainwindow + gtk_widget_set_sensitive(pMainWindow, false); + + // block the GtkSalFrame delete_event handler + auto blockedHandler = g_signal_handler_find( + pMainWindow, static_cast<GSignalMatchType>(G_SIGNAL_MATCH_ID | G_SIGNAL_MATCH_DATA), + deleteEventSignalId, 0, nullptr, nullptr, pSysData->pSalFrame); + g_signal_handler_block(pMainWindow, blockedHandler); + + // prevent the window from being closed + auto ignoreDeleteEventHandler + = g_signal_connect(pMainWindow, "delete_event", G_CALLBACK(ignoreDeleteEvent), nullptr); + + return [pMainWindow, ignoreDeleteEventHandler, blockedHandler] { + SolarMutexGuard cleanupGuard; + // re-enable window + gtk_widget_set_sensitive(pMainWindow, true); + + // allow it to be closed again + g_signal_handler_disconnect(pMainWindow, ignoreDeleteEventHandler); + + // unblock the GtkSalFrame handler + g_signal_handler_unblock(pMainWindow, blockedHandler); + }; +} + +void Gtk3KDE5FilePickerIpc::writeResponseLine(const std::string& line) +{ + sal_uInt64 bytesWritten = 0; + osl_writeFile(m_inputWrite, line.c_str(), line.size(), &bytesWritten); +} + +std::string Gtk3KDE5FilePickerIpc::readResponseLine() +{ + if (!m_responseBuffer.empty()) // check whether we have a line in our buffer + { + std::size_t it = m_responseBuffer.find('\n'); + if (it != std::string::npos) + { + auto ret = m_responseBuffer.substr(0, it); + m_responseBuffer.erase(0, it + 1); + return ret; + } + } + + const sal_uInt64 BUF_SIZE = 1024; + char buffer[BUF_SIZE]; + while (true) + { + sal_uInt64 bytesRead = 0; + auto err = osl_readFile(m_outputRead, buffer, BUF_SIZE, &bytesRead); + auto it = std::find(buffer, buffer + bytesRead, '\n'); + if (it != buffer + bytesRead) // check whether the chunk we read contains an EOL + { + // if so, append that part to the buffer and return it + std::string ret = m_responseBuffer.append(buffer, it); + // but keep anything else we may have read in our buffer + ++it; + m_responseBuffer.assign(it, buffer + bytesRead); + return ret; + } + // otherwise append everything we read to the buffer and try again + m_responseBuffer.append(buffer, bytesRead); + + if (err != osl_File_E_None && err != osl_File_E_AGAIN) + break; + } + return {}; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/gtk3_kde5_filepicker_ipc.hxx b/vcl/unx/gtk3_kde5/gtk3_kde5_filepicker_ipc.hxx new file mode 100644 index 000000000..e24fb92de --- /dev/null +++ b/vcl/unx/gtk3_kde5/gtk3_kde5_filepicker_ipc.hxx @@ -0,0 +1,134 @@ +/* -*- 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#pragma once + +#include <osl/process.h> + +#include "filepicker_ipc_commands.hxx" + +#include <functional> +#include <mutex> +#include <thread> +#include <sstream> + +OUString getResString(const char* pResId); + +class Gtk3KDE5FilePickerIpc +{ +protected: + oslProcess m_process; + oslFileHandle m_inputWrite; + oslFileHandle m_outputRead; + // simple multiplexing: every command gets its own ID that can be used to + // read the corresponding response + uint64_t m_msgId = 1; + std::mutex m_mutex; + uint64_t m_incomingResponse = 0; + std::string m_responseBuffer; + std::stringstream m_responseStream; + +public: + explicit Gtk3KDE5FilePickerIpc(); + ~Gtk3KDE5FilePickerIpc(); + + sal_Int16 execute(); + + void writeResponseLine(const std::string& line); + + template <typename... Args> uint64_t sendCommand(Commands command, const Args&... args) + { + auto id = m_msgId; + ++m_msgId; + std::stringstream stream; + sendIpcArgs(stream, id, command, args...); + writeResponseLine(stream.str()); + return id; + } + + std::string readResponseLine(); + + // workaround gcc <= 4.8 bug: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=55914 + template <int...> struct seq + { + }; + template <int N, int... S> struct gens : gens<N - 1, N - 1, S...> + { + }; + template <int... S> struct gens<0, S...> + { + typedef seq<S...> type; + }; + template <typename... Args> struct ArgsReader + { + ArgsReader(Args&... args) + : m_args(args...) + { + } + + void operator()(std::istream& stream) + { + callFunc(stream, typename gens<sizeof...(Args)>::type()); + } + + private: + template <int... S> void callFunc(std::istream& stream, seq<S...>) + { + readIpcArgs(stream, std::get<S>(m_args)...); + } + + std::tuple<Args&...> m_args; + }; + + template <typename... Args> void readResponse(uint64_t id, Args&... args) + { + ArgsReader<Args...> argsReader(args...); + while (true) + { + // only let one thread read at any given time + std::scoped_lock<std::mutex> lock(m_mutex); + + // check if we need to read (and potentially wait) a response ID + if (m_incomingResponse == 0) + { + m_responseStream.clear(); + m_responseStream.str(readResponseLine()); + readIpcArgs(m_responseStream, m_incomingResponse); + } + + if (m_incomingResponse == id) + { + // the response we are waiting for came in + argsReader(m_responseStream); + m_incomingResponse = 0; + break; + } + else + { + // the next response answers some other request, yield + std::this_thread::yield(); + } + } + } + +private: + std::function<void()> blockMainWindow(); +}; + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/gtk3_kde5_folderpicker.cxx b/vcl/unx/gtk3_kde5/gtk3_kde5_folderpicker.cxx new file mode 100644 index 000000000..fa0b562cc --- /dev/null +++ b/vcl/unx/gtk3_kde5/gtk3_kde5_folderpicker.cxx @@ -0,0 +1,85 @@ +/* -*- 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include "gtk3_kde5_folderpicker.hxx" + +#include <vcl/svapp.hxx> + +#include <strings.hrc> + +using namespace ::com::sun::star; +using namespace ::com::sun::star::ui::dialogs; +using namespace ::com::sun::star::lang; +using namespace ::com::sun::star::uno; + +// constructor + +Gtk3KDE5FolderPicker::Gtk3KDE5FolderPicker( + const uno::Reference<uno::XComponentContext>& /*xContext*/) +{ + m_ipc.sendCommand(Commands::EnablePickFolderMode); + setTitle(getResString(STR_FPICKER_FOLDER_DEFAULT_TITLE)); +} + +Gtk3KDE5FolderPicker::~Gtk3KDE5FolderPicker() = default; + +void SAL_CALL Gtk3KDE5FolderPicker::setDisplayDirectory(const OUString& aDirectory) +{ + m_ipc.sendCommand(Commands::SetDisplayDirectory, aDirectory); +} + +OUString SAL_CALL Gtk3KDE5FolderPicker::getDisplayDirectory() +{ + auto id = m_ipc.sendCommand(Commands::GetDisplayDirectory); + OUString ret; + m_ipc.readResponse(id, ret); + return ret; +} + +OUString SAL_CALL Gtk3KDE5FolderPicker::getDirectory() +{ + auto id = m_ipc.sendCommand(Commands::GetSelectedFiles); + uno::Sequence<OUString> seq; + m_ipc.readResponse(id, seq); + return seq.hasElements() ? seq[0] : OUString(); +} + +void SAL_CALL Gtk3KDE5FolderPicker::setDescription(const OUString& /*rDescription*/) {} + +// XExecutableDialog functions + +void SAL_CALL Gtk3KDE5FolderPicker::setTitle(const OUString& aTitle) +{ + m_ipc.sendCommand(Commands::SetTitle, aTitle); +} + +sal_Int16 SAL_CALL Gtk3KDE5FolderPicker::execute() +{ + SolarMutexGuard g; + return m_ipc.execute(); +} + +// XCancellable + +void SAL_CALL Gtk3KDE5FolderPicker::cancel() +{ + // TODO +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/gtk3_kde5_folderpicker.hxx b/vcl/unx/gtk3_kde5/gtk3_kde5_folderpicker.hxx new file mode 100644 index 000000000..9801a072a --- /dev/null +++ b/vcl/unx/gtk3_kde5/gtk3_kde5_folderpicker.hxx @@ -0,0 +1,59 @@ +/* -*- 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#pragma once + +#include <rtl/ustring.hxx> +#include <cppuhelper/implbase.hxx> + +#include <com/sun/star/ui/dialogs/XFolderPicker2.hpp> +#include <com/sun/star/uno/XComponentContext.hpp> + +#include "gtk3_kde5_filepicker_ipc.hxx" + +class Gtk3KDE5FolderPicker : public cppu::WeakImplHelper<css::ui::dialogs::XFolderPicker2> +{ +protected: + Gtk3KDE5FilePickerIpc m_ipc; + +public: + // constructor + explicit Gtk3KDE5FolderPicker( + const css::uno::Reference<css::uno::XComponentContext>& xServiceMgr); + virtual ~Gtk3KDE5FolderPicker() override; + + // XExecutableDialog functions + virtual void SAL_CALL setTitle(const OUString& aTitle) override; + virtual sal_Int16 SAL_CALL execute() override; + + // XFolderPicker functions + virtual void SAL_CALL setDisplayDirectory(const OUString& rDirectory) override; + virtual OUString SAL_CALL getDisplayDirectory() override; + virtual OUString SAL_CALL getDirectory() override; + virtual void SAL_CALL setDescription(const OUString& rDescription) override; + + // XCancellable + virtual void SAL_CALL cancel() override; + +private: + Gtk3KDE5FolderPicker(const Gtk3KDE5FolderPicker&) = delete; + Gtk3KDE5FolderPicker& operator=(const Gtk3KDE5FolderPicker&) = delete; +}; + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/gtk3_kde5_gloactiongroup.cxx b/vcl/unx/gtk3_kde5/gtk3_kde5_gloactiongroup.cxx new file mode 100644 index 000000000..fa71136f9 --- /dev/null +++ b/vcl/unx/gtk3_kde5/gtk3_kde5_gloactiongroup.cxx @@ -0,0 +1,22 @@ +/* -*- 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include "../gtk3/gtk3gloactiongroup.cxx" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/gtk3_kde5_glomenu.cxx b/vcl/unx/gtk3_kde5/gtk3_kde5_glomenu.cxx new file mode 100644 index 000000000..de79c14c1 --- /dev/null +++ b/vcl/unx/gtk3_kde5/gtk3_kde5_glomenu.cxx @@ -0,0 +1,22 @@ +/* -*- 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include "../gtk3/gtk3glomenu.cxx" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/gtk3_kde5_gtkdata.cxx b/vcl/unx/gtk3_kde5/gtk3_kde5_gtkdata.cxx new file mode 100644 index 000000000..200a74131 --- /dev/null +++ b/vcl/unx/gtk3_kde5/gtk3_kde5_gtkdata.cxx @@ -0,0 +1,22 @@ +/* -*- 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include "../gtk3/gtk3gtkdata.cxx" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/gtk3_kde5_gtkframe.cxx b/vcl/unx/gtk3_kde5/gtk3_kde5_gtkframe.cxx new file mode 100644 index 000000000..0b2eb38dc --- /dev/null +++ b/vcl/unx/gtk3_kde5/gtk3_kde5_gtkframe.cxx @@ -0,0 +1,22 @@ +/* -*- 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include "../gtk3/gtk3gtkframe.cxx" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/gtk3_kde5_gtkinst.cxx b/vcl/unx/gtk3_kde5/gtk3_kde5_gtkinst.cxx new file mode 100644 index 000000000..e25ec8b0e --- /dev/null +++ b/vcl/unx/gtk3_kde5/gtk3_kde5_gtkinst.cxx @@ -0,0 +1,57 @@ +/* -*- 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +// make gtk3 plug advertise correctly as kde5 hybrid +#define GTK_TOOLKIT_NAME "gtk3_kde5" +#include "../gtk3/gtk3gtkinst.cxx" + +#include "gtk3_kde5_filepicker.hxx" +#include "gtk3_kde5_folderpicker.hxx" + +#include <system_error> + +uno::Reference<ui::dialogs::XFilePicker2> +GtkInstance::createFilePicker(const uno::Reference<uno::XComponentContext>& xMSF) +{ + try + { + return uno::Reference<ui::dialogs::XFilePicker2>(new Gtk3KDE5FilePicker(xMSF)); + } + catch (const std::system_error& error) + { + OSL_FAIL(error.what()); + return { nullptr }; + } +} + +uno::Reference<ui::dialogs::XFolderPicker2> +GtkInstance::createFolderPicker(const uno::Reference<uno::XComponentContext>& xMSF) +{ + try + { + return uno::Reference<ui::dialogs::XFolderPicker2>(new Gtk3KDE5FolderPicker(xMSF)); + } + catch (const std::system_error& error) + { + OSL_FAIL(error.what()); + return { nullptr }; + } +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/gtk3_kde5_gtkobject.cxx b/vcl/unx/gtk3_kde5/gtk3_kde5_gtkobject.cxx new file mode 100644 index 000000000..a2eb6b9e1 --- /dev/null +++ b/vcl/unx/gtk3_kde5/gtk3_kde5_gtkobject.cxx @@ -0,0 +1,22 @@ +/* -*- 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include "../gtk3/gtk3gtkobject.cxx" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/gtk3_kde5_gtksalmenu.cxx b/vcl/unx/gtk3_kde5/gtk3_kde5_gtksalmenu.cxx new file mode 100644 index 000000000..9eb1c7975 --- /dev/null +++ b/vcl/unx/gtk3_kde5/gtk3_kde5_gtksalmenu.cxx @@ -0,0 +1,22 @@ +/* -*- 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include "../gtk3/gtk3gtksalmenu.cxx" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/gtk3_kde5_gtksys.cxx b/vcl/unx/gtk3_kde5/gtk3_kde5_gtksys.cxx new file mode 100644 index 000000000..8f6b38843 --- /dev/null +++ b/vcl/unx/gtk3_kde5/gtk3_kde5_gtksys.cxx @@ -0,0 +1,22 @@ +/* -*- 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include "../gtk3/gtk3gtksys.cxx" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/gtk3_kde5_hudawareness.cxx b/vcl/unx/gtk3_kde5/gtk3_kde5_hudawareness.cxx new file mode 100644 index 000000000..eb1592389 --- /dev/null +++ b/vcl/unx/gtk3_kde5/gtk3_kde5_hudawareness.cxx @@ -0,0 +1,22 @@ +/* -*- 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include "../gtk3/gtk3hudawareness.cxx" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/gtk3_kde5_printwrapper.cxx b/vcl/unx/gtk3_kde5/gtk3_kde5_printwrapper.cxx new file mode 100644 index 000000000..33768f8ca --- /dev/null +++ b/vcl/unx/gtk3_kde5/gtk3_kde5_printwrapper.cxx @@ -0,0 +1,22 @@ +/* -*- 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include "../gtk3/gtk3gtkprintwrapper.cxx" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/gtk3_kde5_salnativewidgets-gtk.cxx b/vcl/unx/gtk3_kde5/gtk3_kde5_salnativewidgets-gtk.cxx new file mode 100644 index 000000000..108e41d47 --- /dev/null +++ b/vcl/unx/gtk3_kde5/gtk3_kde5_salnativewidgets-gtk.cxx @@ -0,0 +1,22 @@ +/* -*- 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include "../gtk3/gtk3salnativewidgets-gtk.cxx" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/gtk3_kde5_salprn-gtk.cxx b/vcl/unx/gtk3_kde5/gtk3_kde5_salprn-gtk.cxx new file mode 100644 index 000000000..a9f8c076f --- /dev/null +++ b/vcl/unx/gtk3_kde5/gtk3_kde5_salprn-gtk.cxx @@ -0,0 +1,22 @@ +/* -*- 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include "../gtk3/gtk3salprn-gtk.cxx" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/kde5_filepicker.cxx b/vcl/unx/gtk3_kde5/kde5_filepicker.cxx new file mode 100644 index 000000000..65953e4f2 --- /dev/null +++ b/vcl/unx/gtk3_kde5/kde5_filepicker.cxx @@ -0,0 +1,292 @@ +/* -*- 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <vcl/svapp.hxx> + +#include "kde5_filepicker.hxx" + +#include <KWindowSystem> +#include <KFileWidget> + +#include <QtCore/QDebug> +#include <QtCore/QUrl> +#include <QtWidgets/QCheckBox> +#include <QtWidgets/QFileDialog> +#include <QtWidgets/QGridLayout> +#include <QtWidgets/QWidget> +#include <QtWidgets/QApplication> + +// KDE5FilePicker + +KDE5FilePicker::KDE5FilePicker(QObject* parent) + : QObject(parent) + , _dialog(new QFileDialog(nullptr, {}, QDir::homePath())) + , _extraControls(new QWidget) + , _layout(new QGridLayout(_extraControls)) + , _winId(0) +{ + _dialog->setSupportedSchemes({ + QStringLiteral("file"), QStringLiteral("ftp"), QStringLiteral("http"), + QStringLiteral("https"), QStringLiteral("webdav"), QStringLiteral("webdavs"), + QStringLiteral("smb"), + QStringLiteral(""), // this makes removable devices shown + }); + + setMultiSelectionMode(false); + + connect(_dialog, &QFileDialog::filterSelected, this, &KDE5FilePicker::filterChanged); + connect(_dialog, &QFileDialog::fileSelected, this, &KDE5FilePicker::selectionChanged); + + setupCustomWidgets(); +} + +void KDE5FilePicker::enableFolderMode() +{ + _dialog->setOption(QFileDialog::ShowDirsOnly, true); + // Workaround for https://bugs.kde.org/show_bug.cgi?id=406464 : + // Don't set file mode to QFileDialog::Directory when native KDE Plasma 5 + // file dialog is used, since clicking on directory "bar" inside directory "foo" + // and then confirming would return "foo" rather than "foo/bar"; + // on the other hand, non-native file dialog needs 'QFileDialog::Directory' + // and doesn't allow folder selection otherwise + if (Application::GetDesktopEnvironment() != "PLASMA5") + { + _dialog->setFileMode(QFileDialog::Directory); + } +} + +KDE5FilePicker::~KDE5FilePicker() +{ + delete _extraControls; + delete _dialog; +} + +void KDE5FilePicker::setTitle(const QString& title) { _dialog->setWindowTitle(title); } + +bool KDE5FilePicker::execute() +{ + if (!_filters.isEmpty()) + _dialog->setNameFilters(_filters); + if (!_currentFilter.isEmpty()) + _dialog->selectNameFilter(_currentFilter); + + _dialog->show(); + //block and wait for user input + return _dialog->exec() == QFileDialog::Accepted; +} + +void KDE5FilePicker::setMultiSelectionMode(bool multiSelect) +{ + if (_dialog->acceptMode() == QFileDialog::AcceptSave) + return; + + _dialog->setFileMode(multiSelect ? QFileDialog::ExistingFiles : QFileDialog::ExistingFile); +} + +void KDE5FilePicker::setDefaultName(const QString& name) { _dialog->selectFile(name); } + +void KDE5FilePicker::setDisplayDirectory(const QString& dir) +{ + _dialog->setDirectoryUrl(QUrl(dir)); +} + +QString KDE5FilePicker::getDisplayDirectory() const { return _dialog->directoryUrl().url(); } + +QList<QUrl> KDE5FilePicker::getSelectedFiles() const { return _dialog->selectedUrls(); } + +void KDE5FilePicker::appendFilter(const QString& title, const QString& filter) +{ + QString t = title; + QString f = filter; + // '/' need to be escaped else they are assumed to be mime types by kfiledialog + //see the docs + t.replace("/", "\\/"); + + // openoffice gives us filters separated by ';' qt dialogs just want space separated + f.replace(";", " "); + + // make sure "*.*" is not used as "all files" + f.replace("*.*", "*"); + + _filters << QStringLiteral("%1 (%2)").arg(t, f); + _titleToFilters[t] = _filters.constLast(); +} + +void KDE5FilePicker::setCurrentFilter(const QString& title) +{ + _currentFilter = _titleToFilters.value(title); +} + +QString KDE5FilePicker::getCurrentFilter() const +{ + QString filter = _titleToFilters.key(_dialog->selectedNameFilter()); + + //default if not found + if (filter.isEmpty()) + filter = "ODF Text Document (.odt)"; + + return filter; +} + +void KDE5FilePicker::setValue(sal_Int16 controlId, sal_Int16 /*nControlAction*/, bool value) +{ + if (_customWidgets.contains(controlId)) + { + QCheckBox* cb = dynamic_cast<QCheckBox*>(_customWidgets.value(controlId)); + if (cb) + cb->setChecked(value); + } + else + qWarning() << "set value on unknown control" << controlId; +} + +bool KDE5FilePicker::getValue(sal_Int16 controlId, sal_Int16 /*nControlAction*/) const +{ + bool ret = false; + if (_customWidgets.contains(controlId)) + { + QCheckBox* cb = dynamic_cast<QCheckBox*>(_customWidgets.value(controlId)); + if (cb) + ret = cb->isChecked(); + } + else + qWarning() << "get value on unknown control" << controlId; + + return ret; +} + +void KDE5FilePicker::enableControl(sal_Int16 controlId, bool enable) +{ + if (_customWidgets.contains(controlId)) + _customWidgets.value(controlId)->setEnabled(enable); + else + qWarning() << "enable on unknown control" << controlId; +} + +void KDE5FilePicker::setLabel(sal_Int16 controlId, const QString& label) +{ + if (_customWidgets.contains(controlId)) + { + QCheckBox* cb = dynamic_cast<QCheckBox*>(_customWidgets.value(controlId)); + if (cb) + cb->setText(label); + } + else + qWarning() << "set label on unknown control" << controlId; +} + +QString KDE5FilePicker::getLabel(sal_Int16 controlId) const +{ + QString label; + if (_customWidgets.contains(controlId)) + { + QCheckBox* cb = dynamic_cast<QCheckBox*>(_customWidgets.value(controlId)); + if (cb) + label = cb->text(); + } + else + qWarning() << "get label on unknown control" << controlId; + + return label; +} + +void KDE5FilePicker::addCheckBox(sal_Int16 controlId, const QString& label, bool hidden) +{ + auto resString = label; + resString.replace('~', '&'); + + auto widget = new QCheckBox(resString, _extraControls); + widget->setHidden(hidden); + if (!hidden) + { + _layout->addWidget(widget); + } + _customWidgets.insert(controlId, widget); +} + +void KDE5FilePicker::initialize(bool saveDialog) +{ + //default is opening + QFileDialog::AcceptMode operationMode + = saveDialog ? QFileDialog::AcceptSave : QFileDialog::AcceptOpen; + + _dialog->setAcceptMode(operationMode); + + if (saveDialog) + { + SAL_WNODEPRECATED_DECLARATIONS_PUSH + _dialog->setConfirmOverwrite(true); + SAL_WNODEPRECATED_DECLARATIONS_POP + _dialog->setFileMode(QFileDialog::AnyFile); + } +} + +void KDE5FilePicker::setWinId(sal_uInt64 winId) { _winId = winId; } + +void KDE5FilePicker::setupCustomWidgets() +{ + // When using the platform-native Plasma/KDE5 file picker, we currently rely on KFileWidget + // being present to add the custom controls visible (s. 'eventFilter' method). + // Since this doesn't work for other desktop environments, use a non-native + // dialog there in order not to lose the custom controls and insert the custom + // widget in the layout returned by QFileDialog::layout() + // (which returns nullptr for native file dialogs) + if (Application::GetDesktopEnvironment() == "PLASMA5") + { + qApp->installEventFilter(this); + } + else + { + _dialog->setOption(QFileDialog::DontUseNativeDialog); + QGridLayout* pLayout = static_cast<QGridLayout*>(_dialog->layout()); + assert(pLayout); + const int row = pLayout->rowCount(); + pLayout->addWidget(_extraControls, row, 1); + } +} + +bool KDE5FilePicker::eventFilter(QObject* o, QEvent* e) +{ + if (e->type() == QEvent::Show && o->isWidgetType()) + { + auto* w = static_cast<QWidget*>(o); + if (!w->parentWidget() && w->isModal()) + { + /* + To replace when baseline will include kwindowsystem >= 5.62 with: + w->setAttribute(Qt::WA_NativeWindow, true); + KWindowSystem::setMainWindow(w->windowHandle(), _winId); + */ + SAL_WNODEPRECATED_DECLARATIONS_PUSH + KWindowSystem::setMainWindow(w, _winId); + SAL_WNODEPRECATED_DECLARATIONS_POP + if (auto* fileWidget = w->findChild<KFileWidget*>({}, Qt::FindDirectChildrenOnly)) + { + fileWidget->setCustomWidget(_extraControls); + // remove event filter again; the only purpose was to set the custom widget here + qApp->removeEventFilter(this); + } + } + } + return QObject::eventFilter(o, e); +} + +#include <kde5_filepicker.moc> + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/kde5_filepicker.hxx b/vcl/unx/gtk3_kde5/kde5_filepicker.hxx new file mode 100644 index 000000000..74f94d222 --- /dev/null +++ b/vcl/unx/gtk3_kde5/kde5_filepicker.hxx @@ -0,0 +1,110 @@ +/* -*- 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#pragma once + +#include <QtCore/QObject> +#include <QtCore/QString> +#include <QtCore/QStringList> +#include <QtCore/QHash> + +#include <sal/types.h> + +class QFileDialog; +class QWidget; +class QGridLayout; + +class KDE5FilePicker : public QObject +{ + Q_OBJECT +protected: + //the dialog to display + QFileDialog* _dialog; + + //running filter string to add to dialog + QStringList _filters; + // map of filter titles to full filter for selection + QHash<QString, QString> _titleToFilters; + // string to set the current filter + QString _currentFilter; + + //mapping of SAL control ID's to created custom controls + QHash<sal_Int16, QWidget*> _customWidgets; + + //widget to contain extra custom controls + QWidget* _extraControls; + + //layout for extra custom controls + QGridLayout* _layout; + + sal_uInt64 _winId; + +public: + explicit KDE5FilePicker(QObject* parent = nullptr); + ~KDE5FilePicker() override; + + void enableFolderMode(); + + // XExecutableDialog functions + void setTitle(const QString& rTitle); + bool execute(); + + // XFilePicker functions + void setMultiSelectionMode(bool bMode); + void setDefaultName(const QString& rName); + void setDisplayDirectory(const QString& rDirectory); + QString getDisplayDirectory() const; + + // XFilterManager functions + void appendFilter(const QString& rTitle, const QString& rFilter); + void setCurrentFilter(const QString& rTitle); + QString getCurrentFilter() const; + + // XFilePickerControlAccess functions + void setValue(sal_Int16 nControlId, sal_Int16 nControlAction, bool rValue); + bool getValue(sal_Int16 nControlId, sal_Int16 nControlAction) const; + void enableControl(sal_Int16 nControlId, bool bEnable); + void setLabel(sal_Int16 nControlId, const QString& rLabel); + QString getLabel(sal_Int16 nControlId) const; + + // XFilePicker2 functions + QList<QUrl> getSelectedFiles() const; + + // XInitialization + void initialize(bool saveDialog); + + //add a custom control widget to the file dialog + void addCheckBox(sal_Int16 nControlId, const QString& label, bool hidden); + + void setWinId(sal_uInt64 winId); + +private: + Q_DISABLE_COPY(KDE5FilePicker) + // adds the custom controls to the dialog + void setupCustomWidgets(); + +protected: + bool eventFilter(QObject* watched, QEvent* event) override; + +Q_SIGNALS: + void filterChanged(); + void selectionChanged(); +}; + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/kde5_filepicker_ipc.cxx b/vcl/unx/gtk3_kde5/kde5_filepicker_ipc.cxx new file mode 100644 index 000000000..7f0bfff98 --- /dev/null +++ b/vcl/unx/gtk3_kde5/kde5_filepicker_ipc.cxx @@ -0,0 +1,374 @@ +/* -*- 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include "kde5_filepicker_ipc.hxx" + +#include <QUrl> +#include <QApplication> +#include <QDebug> + +#include <iostream> + +#include "kde5_filepicker.hxx" + +void readIpcArg(std::istream& stream, QString& string) +{ + const auto buffer = readIpcStringArg(stream); + string = QString::fromUtf8(buffer.data(), buffer.size()); +} + +void sendIpcArg(std::ostream& stream, const QString& string) +{ + const auto utf8 = string.toUtf8(); + sendIpcStringArg(stream, utf8.size(), utf8.data()); +} + +static void sendIpcArg(std::ostream& stream, const QStringList& list) +{ + stream << static_cast<uint32_t>(list.size()) << ' '; + for (const auto& entry : list) + { + sendIpcArg(stream, entry); + } +} + +static void readCommandArgs(Commands command, QList<QVariant>& args) +{ + switch (command) + { + case Commands::SetTitle: + { + QString title; + readIpcArgs(std::cin, title); + args.append(title); + break; + } + case Commands::SetWinId: + { + sal_uIntPtr winId = 0; + readIpcArgs(std::cin, winId); + QVariant aWinIdVariant; + aWinIdVariant.setValue(winId); + args.append(aWinIdVariant); + break; + } + case Commands::SetMultiSelectionMode: + { + bool multiSelection = false; + readIpcArgs(std::cin, multiSelection); + args.append(multiSelection); + break; + } + case Commands::SetDefaultName: + { + QString name; + readIpcArgs(std::cin, name); + args.append(name); + break; + } + case Commands::SetDisplayDirectory: + { + QString dir; + readIpcArgs(std::cin, dir); + args.append(dir); + break; + } + case Commands::AppendFilter: + { + QString title, filter; + readIpcArgs(std::cin, title, filter); + args.append(title); + args.append(filter); + break; + } + case Commands::SetCurrentFilter: + { + QString title; + readIpcArgs(std::cin, title); + args.append(title); + break; + } + case Commands::SetValue: + { + sal_Int16 controlId = 0; + sal_Int16 nControlAction = 0; + bool value = false; + readIpcArgs(std::cin, controlId, nControlAction, value); + args.append(controlId); + args.append(nControlAction); + args.append(value); + break; + } + case Commands::GetValue: + { + sal_Int16 controlId = 0; + sal_Int16 nControlAction = 0; + readIpcArgs(std::cin, controlId, nControlAction); + args.append(controlId); + args.append(nControlAction); + break; + } + case Commands::EnableControl: + { + sal_Int16 controlId = 0; + bool enabled = false; + readIpcArgs(std::cin, controlId, enabled); + args.append(controlId); + args.append(enabled); + break; + } + case Commands::SetLabel: + { + sal_Int16 controlId = 0; + QString label; + readIpcArgs(std::cin, controlId, label); + args.append(controlId); + args.append(label); + break; + } + case Commands::GetLabel: + { + sal_Int16 controlId = 0; + readIpcArgs(std::cin, controlId); + args.append(controlId); + break; + } + case Commands::AddCheckBox: + { + sal_Int16 controlId = 0; + bool hidden = false; + QString label; + readIpcArgs(std::cin, controlId, hidden, label); + args.append(controlId); + args.append(hidden); + args.append(label); + break; + } + case Commands::Initialize: + { + bool saveDialog = false; + readIpcArgs(std::cin, saveDialog); + args.append(saveDialog); + break; + } + default: + { + // no extra parameters/arguments + break; + } + }; +} + +static void readCommands(FilePickerIpc* ipc) +{ + while (!std::cin.eof()) + { + uint64_t messageId = 0; + Commands command; + readIpcArgs(std::cin, messageId, command); + + // retrieve additional command-specific arguments + QList<QVariant> args; + readCommandArgs(command, args); + + emit ipc->commandReceived(messageId, command, args); + + // stop processing once 'Quit' command has been sent + if (command == Commands::Quit) + { + return; + } + } +} + +FilePickerIpc::FilePickerIpc(KDE5FilePicker* filePicker, QObject* parent) + : QObject(parent) + , m_filePicker(filePicker) +{ + // required to be able to pass those via signal/slot + qRegisterMetaType<uint64_t>("uint64_t"); + qRegisterMetaType<Commands>("Commands"); + + connect(this, &FilePickerIpc::commandReceived, this, &FilePickerIpc::handleCommand); + + // read IPC commands and their args in a separate thread, so this does not block everything else; + // 'commandReceived' signal is emitted every time a command and its args have been read; + // thread will run until the filepicker process is terminated + m_ipcReaderThread = std::make_unique<std::thread>(readCommands, this); +} + +FilePickerIpc::~FilePickerIpc() +{ + // join thread that reads commands + m_ipcReaderThread->join(); +}; + +bool FilePickerIpc::handleCommand(uint64_t messageId, Commands command, QList<QVariant> args) +{ + switch (command) + { + case Commands::SetTitle: + { + QString title = args.takeFirst().toString(); + m_filePicker->setTitle(title); + return true; + } + case Commands::SetWinId: + { + sal_uIntPtr winId = args.takeFirst().value<sal_uIntPtr>(); + m_filePicker->setWinId(winId); + return true; + } + case Commands::Execute: + { + sendIpcArgs(std::cout, messageId, m_filePicker->execute()); + return true; + } + case Commands::SetMultiSelectionMode: + { + bool multiSelection = args.takeFirst().toBool(); + m_filePicker->setMultiSelectionMode(multiSelection); + return true; + } + case Commands::SetDefaultName: + { + QString name = args.takeFirst().toString(); + m_filePicker->setDefaultName(name); + return true; + } + case Commands::SetDisplayDirectory: + { + QString dir = args.takeFirst().toString(); + m_filePicker->setDisplayDirectory(dir); + return true; + } + case Commands::GetDisplayDirectory: + { + sendIpcArgs(std::cout, messageId, m_filePicker->getDisplayDirectory()); + return true; + } + case Commands::GetSelectedFiles: + { + QStringList files; + for (auto const& url_ : m_filePicker->getSelectedFiles()) + { + auto url = url_; + if (url.scheme() == QLatin1String("webdav") + || url.scheme() == QLatin1String("webdavs")) + { + // translate webdav and webdavs URLs into a format supported by LO + url.setScheme(QLatin1String("vnd.sun.star.") + url.scheme()); + } + else if (url.scheme() == QLatin1String("smb")) + { + // clear the user name - the GIO backend does not support this apparently + // when no username is available, it will ask for the password + url.setUserName({}); + } + files << url.toString(); + } + sendIpcArgs(std::cout, messageId, files); + return true; + } + case Commands::AppendFilter: + { + QString title = args.takeFirst().toString(); + QString filter = args.takeFirst().toString(); + m_filePicker->appendFilter(title, filter); + return true; + } + case Commands::SetCurrentFilter: + { + QString title = args.takeFirst().toString(); + m_filePicker->setCurrentFilter(title); + return true; + } + case Commands::GetCurrentFilter: + { + sendIpcArgs(std::cout, messageId, m_filePicker->getCurrentFilter()); + return true; + } + case Commands::SetValue: + { + sal_Int16 controlId = args.takeFirst().value<sal_Int16>(); + sal_Int16 nControlAction = args.takeFirst().value<sal_Int16>(); + bool value = args.takeFirst().toBool(); + m_filePicker->setValue(controlId, nControlAction, value); + return true; + } + case Commands::GetValue: + { + sal_Int16 controlId = args.takeFirst().value<sal_Int16>(); + sal_Int16 nControlAction = args.takeFirst().value<sal_Int16>(); + sendIpcArgs(std::cout, messageId, m_filePicker->getValue(controlId, nControlAction)); + return true; + } + case Commands::EnableControl: + { + sal_Int16 controlId = args.takeFirst().value<sal_Int16>(); + bool enabled = args.takeFirst().toBool(); + m_filePicker->enableControl(controlId, enabled); + return true; + } + case Commands::SetLabel: + { + sal_Int16 controlId = args.takeFirst().value<sal_Int16>(); + QString label = args.takeFirst().toString(); + m_filePicker->setLabel(controlId, label); + return true; + } + case Commands::GetLabel: + { + sal_Int16 controlId = args.takeFirst().value<sal_Int16>(); + sendIpcArgs(std::cout, messageId, m_filePicker->getLabel(controlId)); + return true; + } + case Commands::AddCheckBox: + { + sal_Int16 controlId = args.takeFirst().value<sal_Int16>(); + bool hidden = args.takeFirst().toBool(); + QString label = args.takeFirst().toString(); + m_filePicker->addCheckBox(controlId, label, hidden); + return true; + } + case Commands::Initialize: + { + bool saveDialog = args.takeFirst().toBool(); + m_filePicker->initialize(saveDialog); + return true; + } + case Commands::EnablePickFolderMode: + { + m_filePicker->enableFolderMode(); + return true; + } + case Commands::Quit: + { + QCoreApplication::quit(); + return false; + } + } + qWarning() << "unhandled command " << static_cast<uint16_t>(command); + QCoreApplication::exit(1); + return false; +} + +#include <kde5_filepicker_ipc.moc> + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/kde5_filepicker_ipc.hxx b/vcl/unx/gtk3_kde5/kde5_filepicker_ipc.hxx new file mode 100644 index 000000000..fa9be696c --- /dev/null +++ b/vcl/unx/gtk3_kde5/kde5_filepicker_ipc.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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#pragma once + +#include <memory> +#include <thread> + +#include <QObject> + +#include "filepicker_ipc_commands.hxx" + +class KDE5FilePicker; +class WinIdEmbedder; +class QSocketNotifier; + +class FilePickerIpc : public QObject +{ + Q_OBJECT +public: + explicit FilePickerIpc(KDE5FilePicker* filePicker, QObject* parent = nullptr); + ~FilePickerIpc() override; + +private: + KDE5FilePicker* m_filePicker = nullptr; + std::unique_ptr<std::thread> m_ipcReaderThread; + +private Q_SLOTS: + bool handleCommand(uint64_t messageId, Commands command, QList<QVariant> args); + +Q_SIGNALS: + bool commandReceived(uint64_t messageId, Commands command, QList<QVariant> args); +}; + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/kde5_lo_filepicker_main.cxx b/vcl/unx/gtk3_kde5/kde5_lo_filepicker_main.cxx new file mode 100644 index 000000000..4e73e9ee4 --- /dev/null +++ b/vcl/unx/gtk3_kde5/kde5_lo_filepicker_main.cxx @@ -0,0 +1,53 @@ +/* -*- 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include "kde5_filepicker.hxx" +#include "kde5_filepicker_ipc.hxx" + +#include <QApplication> +#include <QCommandLineParser> + +#include <config_version.h> + +int main(int argc, char** argv) +{ + QApplication::setOrganizationName("LibreOffice"); + QApplication::setOrganizationDomain("libreoffice.org"); + QApplication::setApplicationName(QStringLiteral("lo_kde5filepicker")); + QApplication::setQuitOnLastWindowClosed(false); + QApplication::setApplicationVersion(LIBO_VERSION_DOTTED); + + QApplication app(argc, argv); + + QCommandLineParser parser; + parser.setApplicationDescription( + QObject::tr("Helper executable for LibreOffice KDE/Plasma integration.\n" + "Do not run this executable directly. Rather, use it indirectly via " + "the gtk3_kde5 VCL plugin (SAL_USE_VCLPLUGIN=gtk3_kde5).")); + parser.addVersionOption(); + parser.addHelpOption(); + parser.process(app); + + KDE5FilePicker filePicker; + FilePickerIpc ipc(&filePicker); + + return QApplication::exec(); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ |