diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-15 05:54:39 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-15 05:54:39 +0000 |
commit | 267c6f2ac71f92999e969232431ba04678e7437e (patch) | |
tree | 358c9467650e1d0a1d7227a21dac2e3d08b622b2 /vcl/unx/gtk3/gtkinst.cxx | |
parent | Initial commit. (diff) | |
download | libreoffice-267c6f2ac71f92999e969232431ba04678e7437e.tar.xz libreoffice-267c6f2ac71f92999e969232431ba04678e7437e.zip |
Adding upstream version 4:24.2.0.upstream/4%24.2.0
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'vcl/unx/gtk3/gtkinst.cxx')
-rw-r--r-- | vcl/unx/gtk3/gtkinst.cxx | 24855 |
1 files changed, 24855 insertions, 0 deletions
diff --git a/vcl/unx/gtk3/gtkinst.cxx b/vcl/unx/gtk3/gtkinst.cxx new file mode 100644 index 0000000000..c76d6291ce --- /dev/null +++ b/vcl/unx/gtk3/gtkinst.cxx @@ -0,0 +1,24855 @@ +/* -*- 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 <optional> +#include <stack> +#include <string.h> +#include <string_view> + +#include <dndhelper.hxx> +#include <osl/process.h> +#include <osl/file.hxx> +#include <unx/gtk/gtkdata.hxx> +#include <unx/gtk/gtkinst.hxx> +#include <unx/genprn.h> +#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/gtksalmenu.hxx> +#include <headless/svpvd.hxx> +#include <headless/svpbmp.hxx> +#include <utility> +#include <vcl/builder.hxx> +#include <vcl/inputtypes.hxx> +#include <vcl/specialchars.hxx> +#include <vcl/sysdata.hxx> +#include <vcl/transfer.hxx> +#include <vcl/toolkit/floatwin.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> + +#if !GTK_CHECK_VERSION(4, 0, 0) +#include "a11y/atkwrapper.hxx" +#endif +#include <com/sun/star/awt/XVclWindowPeer.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 <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 <comphelper/lok.hxx> +#include <comphelper/processfactory.hxx> +#include <comphelper/propertyvalue.hxx> +#include <comphelper/sequence.hxx> +#include <comphelper/string.hxx> +#include <cppuhelper/compbase.hxx> +#include <cppuhelper/implbase.hxx> +#include <cppuhelper/supportsservice.hxx> +#include <officecfg/Office/Common.hxx> +#include <rtl/bootstrap.hxx> +#include <o3tl/unreachable.hxx> +#include <o3tl/string_view.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 <unotools/tempfile.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/filter/PngImageWriter.hxx> +#include <vcl/stdtext.hxx> +#include <vcl/syswin.hxx> +#include <vcl/virdev.hxx> +#include <vcl/weld.hxx> +#include <vcl/wrkwin.hxx> +#include "customcellrenderer.hxx" +#include <strings.hrc> +#include <window.h> +#include <numeric> +#include <boost/property_tree/ptree.hpp> +#include <opengl/zone.hxx> + +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*>(GetSalInstance()->GetYieldMutex()) +#if !GTK_CHECK_VERSION(4, 0, 0) + static void GdkThreadsEnter() + { + GtkYieldMutex *pYieldMutex = GET_YIELD_MUTEX(); + pYieldMutex->ThreadsEnter(); + } + static void GdkThreadsLeave() + { + GtkYieldMutex *pYieldMutex = GET_YIELD_MUTEX(); + pYieldMutex->ThreadsLeave(); + } +#endif + + VCLPLUG_GTK_PUBLIC SalInstance* create_SalInstance() + { + SAL_INFO( + "vcl.gtk", + "create vcl plugin instance with gtk version " << gtk_get_major_version() + << " " << gtk_get_minor_version() << " " << gtk_get_micro_version()); + + if (gtk_get_major_version() == 3 && gtk_get_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 + +#if !GTK_CHECK_VERSION(4, 0, 0) + // 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"); +#endif + + auto pYieldMutex = std::make_unique<GtkYieldMutex>(); + +#if !GTK_CHECK_VERSION(4, 0, 0) + gdk_threads_init(); +#endif + + GtkInstance* pInstance = new GtkInstance( std::move(pYieldMutex) ); + SAL_INFO("vcl.gtk", "creating GtkInstance " << pInstance); + + // Create SalData, this does not leak + new GtkSalData(); + + return pInstance; + } +} + +#if !GTK_CHECK_VERSION(4, 0, 0) +static VclInputFlags categorizeEvent(const GdkEvent *pEvent) +{ + VclInputFlags nType = VclInputFlags::NONE; + switch (gdk_event_get_event_type(pEvent)) + { + case GDK_MOTION_NOTIFY: + case GDK_BUTTON_PRESS: +#if !GTK_CHECK_VERSION(4, 0, 0) + case GDK_2BUTTON_PRESS: + case GDK_3BUTTON_PRESS: +#endif + 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; +#if !GTK_CHECK_VERSION(4, 0, 0) + case GDK_EXPOSE: + nType = VclInputFlags::PAINT; + break; +#endif + default: + nType = VclInputFlags::OTHER; + break; + } + return nType; +} +#endif + +GtkInstance::GtkInstance( std::unique_ptr<SalYieldMutex> pMutex ) + : SvpSalInstance( std::move(pMutex) ) + , m_pTimer(nullptr) + , bNeedsInit(true) + , m_pLastCairoFontOptions(nullptr) +{ + m_bSupportsOpenGL = true; +} + +//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(); + +#if !GTK_CHECK_VERSION(4, 0, 0) + InitAtkBridge(); +#endif + + 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 ); +#if !GTK_CHECK_VERSION(4, 0, 0) + DeInitAtkBridge(); +#endif + 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 PspSalInfoPrinter; + configurePspInfoPrinter(pPrinter, pQueueInfo, pSetupData); + return pPrinter; +} + +std::unique_ptr<SalPrinter> GtkInstance::CreatePrinter( SalInfoPrinter* pInfoPrinter ) +{ + EnsureInit(); + mbPrinterInit = true; + return std::unique_ptr<SalPrinter>(new PspSalPrinter(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()) + return; + auto n = yieldCounts.top(); + yieldCounts.pop(); + + const bool bUndoingLeaveWithoutEnter = n == 0; + // if the ThreadsLeave bLeaveWithoutEnter of true condition occurred to + // create this entry then return early undoing the initial acquire of the + // function + if G_UNLIKELY(bUndoingLeaveWithoutEnter) + { + release(); + return; + } + + assert(n > 0); + n--; + if (n > 0) + acquire(n); +} + +void GtkYieldMutex::ThreadsLeave() +{ + const bool bLeaveWithoutEnter = m_nCount == 0; + SAL_WARN_IF(bLeaveWithoutEnter, "vcl.gtk", "gdk_threads_leave without matching gdk_threads_enter"); + yieldCounts.push(m_nCount); + if G_UNLIKELY(bLeaveWithoutEnter) // this ideally shouldn't happen, but can due to the gtk3 file dialog + return; + release(true); +} + +std::unique_ptr<SalVirtualDevice> GtkInstance::CreateVirtualDevice( SalGraphics &rG, + tools::Long &nDX, tools::Long &nDY, + DeviceFormat /*eFormat*/, + const SystemGraphicsData* pGd ) +{ + EnsureInit(); + SvpSalGraphics *pSvpSalGraphics = dynamic_cast<SvpSalGraphics*>(&rG); + 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> xNew(new SvpSalVirtualDevice(pSvpSalGraphics->getSurface(), pPreExistingTarget)); + if (!xNew->SetSize(nDX, nDY)) + xNew.reset(); + return xNew; +} + +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()); +} + +namespace +{ + bool DisplayHasAnyInput() + { + GdkDisplay* pDisplay = gdk_display_get_default(); +#if defined(GDK_WINDOWING_WAYLAND) + if (DLSYM_GDK_IS_WAYLAND_DISPLAY(pDisplay)) + { + bool bRet = false; + wl_display* pWLDisplay = gdk_wayland_display_get_wl_display(pDisplay); + static auto wayland_display_get_fd = reinterpret_cast<int (*) (wl_display*)>(dlsym(nullptr, "wl_display_get_fd")); + if (wayland_display_get_fd) + { + GPollFD aPollFD; + aPollFD.fd = wayland_display_get_fd(pWLDisplay); + aPollFD.events = G_IO_IN | G_IO_ERR | G_IO_HUP; + bRet = g_poll(&aPollFD, 1, 0) > 0; + } + return bRet; + } +#endif +#if defined(GDK_WINDOWING_X11) + if (DLSYM_GDK_IS_X11_DISPLAY(pDisplay)) + { + GPollFD aPollFD; + aPollFD.fd = ConnectionNumber(gdk_x11_display_get_xdisplay(pDisplay)); + aPollFD.events = G_IO_IN; + return g_poll(&aPollFD, 1, 0) > 0; + } +#endif + return false; + } +} + +bool GtkInstance::AnyInput( VclInputFlags nType ) +{ + EnsureInit(); + if( (nType & VclInputFlags::TIMER) && IsTimerExpired() ) + return true; + + // strip timer bits now + nType = nType & ~VclInputFlags::TIMER; + + static constexpr VclInputFlags ANY_INPUT_EXCLUDING_TIMER = VCL_INPUT_ANY & ~VclInputFlags::TIMER; + + const bool bCheckForAnyInput = nType == ANY_INPUT_EXCLUDING_TIMER; + + bool bRet = false; + + if (bCheckForAnyInput) + bRet = DisplayHasAnyInput(); + +#if !GTK_CHECK_VERSION(4, 0, 0) + GdkDisplay* pDisplay = gdk_display_get_default(); + if (!gdk_display_has_pending(pDisplay)) + return bRet; + + if (bCheckForAnyInput) + return true; + + std::deque<GdkEvent*> aEvents; + GdkEvent *pEvent = nullptr; + while ((pEvent = gdk_display_get_event(pDisplay))) + { + 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_display_put_event(pDisplay, pEvent); + gdk_event_free(pEvent); + aEvents.pop_front(); + } +#endif + + return bRet; +} + +std::unique_ptr<GenPspGraphics> GtkInstance::CreatePrintGraphics() +{ + EnsureInit(); + return std::make_unique<GenPspGraphics>(); +} + +const cairo_font_options_t* GtkInstance::GetCairoFontOptions() +{ +#if !GTK_CHECK_VERSION(4, 0, 0) + const cairo_font_options_t* pCairoFontOptions = gdk_screen_get_font_options(gdk_screen_get_default()); +#else + auto pDefaultWin = ImplGetDefaultWindow(); + assert(pDefaultWin); + SalFrame* pDefaultFrame = pDefaultWin->ImplGetFrame(); + GtkSalFrame* pGtkFrame = dynamic_cast<GtkSalFrame*>(pDefaultFrame); + assert(pGtkFrame); + const cairo_font_options_t* pCairoFontOptions = pGtkFrame->get_font_options(); +#endif + 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 + }; + + 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; + } + }; +} + +#if GTK_CHECK_VERSION(4, 0, 0) +std::vector<css::datatransfer::DataFlavor> GtkTransferable::getTransferDataFlavorsAsVector(const char * const *targets, gint n_targets) +#else +std::vector<css::datatransfer::DataFlavor> GtkTransferable::getTransferDataFlavorsAsVector(GdkAtom *targets, gint n_targets) +#endif +{ + std::vector<css::datatransfer::DataFlavor> aVector; + + bool bHaveText = false, bHaveUTF16 = false; + + for (gint i = 0; i < n_targets; ++i) + { +#if GTK_CHECK_VERSION(4, 0, 0) + const gchar* pName = targets[i]; +#else + gchar* pName = gdk_atom_name(targets[i]); +#endif + 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) + { +#if !GTK_CHECK_VERSION(4, 0, 0) + g_free(pName); +#endif + 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) + { +#if !GTK_CHECK_VERSION(4, 0, 0) + g_free(pName); +#endif + continue; + } + + aFlavor.MimeType = OUString(pFinalName, + strlen(pFinalName), + RTL_TEXTENCODING_UTF8); + + m_aMimeTypeToGtkType[aFlavor.MimeType] = targets[i]; + + aFlavor.DataType = cppu::UnoType<Sequence< sal_Int8 >>::get(); + + sal_Int32 nIndex(0); + if (o3tl::getToken(aFlavor.MimeType, 0, ';', nIndex) == u"text/plain") + { + bHaveText = true; + std::u16string_view aToken(o3tl::getToken(aFlavor.MimeType, 0, ';', nIndex)); + if (aToken == u"charset=utf-16") + { + bHaveUTF16 = true; + aFlavor.DataType = cppu::UnoType<OUString>::get(); + } + } + aVector.push_back(aFlavor); +#if !GTK_CHECK_VERSION(4, 0, 0) + g_free(pName); +#endif + } + + //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)); +} + +#if GTK_CHECK_VERSION(4, 0, 0) +void read_transfer_result::read_block_async_completed(GObject* source, GAsyncResult* res, gpointer user_data) +{ + GInputStream* stream = G_INPUT_STREAM(source); + read_transfer_result* pRes = static_cast<read_transfer_result*>(user_data); + + gsize bytes_read = g_input_stream_read_finish(stream, res, nullptr); + + bool bFinished = bytes_read == 0; + + if (bFinished) + { + g_object_unref(stream); + pRes->aVector.resize(pRes->nRead); + pRes->bDone = true; + g_main_context_wakeup(nullptr); + return; + } + + pRes->nRead += bytes_read; + + pRes->aVector.resize(pRes->nRead + read_transfer_result::BlockSize); + + g_input_stream_read_async(stream, + pRes->aVector.data() + pRes->nRead, + read_transfer_result::BlockSize, + G_PRIORITY_DEFAULT, + nullptr, + read_block_async_completed, + user_data); +} + +OUString read_transfer_result::get_as_string() const +{ + const char* pStr = reinterpret_cast<const char*>(aVector.data()); + return OUString(pStr, aVector.size(), RTL_TEXTENCODING_UTF8).replaceAll("\r\n", "\n"); +} + +css::uno::Sequence<sal_Int8> read_transfer_result::get_as_sequence() const +{ + return css::uno::Sequence<sal_Int8>(aVector.data(), aVector.size()); +} +#endif + +namespace { + +GdkClipboard* clipboard_get(SelectionType eSelection) +{ +#if GTK_CHECK_VERSION(4, 0, 0) + if (eSelection == SELECTION_CLIPBOARD) + return gdk_display_get_clipboard(gdk_display_get_default()); + return gdk_display_get_primary_clipboard(gdk_display_get_default()); +#else + return gtk_clipboard_get(eSelection == SELECTION_CLIPBOARD ? GDK_SELECTION_CLIPBOARD : GDK_SELECTION_PRIMARY); +#endif +} + +#if GTK_CHECK_VERSION(4, 0, 0) + +void read_clipboard_async_completed(GObject* source, GAsyncResult* res, gpointer user_data) +{ + GdkClipboard* clipboard = GDK_CLIPBOARD(source); + read_transfer_result* pRes = static_cast<read_transfer_result*>(user_data); + + GInputStream* pResult = gdk_clipboard_read_finish(clipboard, res, nullptr, nullptr); + + if (!pResult) + { + pRes->bDone = true; + g_main_context_wakeup(nullptr); + return; + } + + pRes->aVector.resize(read_transfer_result::BlockSize); + + g_input_stream_read_async(pResult, + pRes->aVector.data(), + pRes->aVector.size(), + G_PRIORITY_DEFAULT, + nullptr, + read_transfer_result::read_block_async_completed, + user_data); +} + +#endif + +class GtkClipboardTransferable : public GtkTransferable +{ +private: + SelectionType m_eSelection; + +public: + + explicit GtkClipboardTransferable(SelectionType eSelection) + : m_eSelection(eSelection) + { + } + + /* + * XTransferable + */ + + virtual css::uno::Any SAL_CALL getTransferData(const css::datatransfer::DataFlavor& rFlavor) override + { + css::uno::Any aRet; + + css::datatransfer::DataFlavor aFlavor(rFlavor); + if (aFlavor.MimeType == "text/plain;charset=utf-16") + aFlavor.MimeType = "text/plain;charset=utf-8"; + + GdkClipboard* clipboard = clipboard_get(m_eSelection); + +#if !GTK_CHECK_VERSION(4, 0, 0) + if (aFlavor.MimeType == "text/plain;charset=utf-8") + { + gchar *pText = gtk_clipboard_wait_for_text(clipboard); + OUString aStr(pText, pText ? strlen(pText) : 0, RTL_TEXTENCODING_UTF8); + g_free(pText); + aRet <<= aStr.replaceAll("\r\n", "\n"); + return aRet; + } +#endif + + auto it = m_aMimeTypeToGtkType.find(aFlavor.MimeType); + if (it == m_aMimeTypeToGtkType.end()) + return css::uno::Any(); + +#if !GTK_CHECK_VERSION(4, 0, 0) + 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; +#else + SalInstance* pInstance = GetSalInstance(); + read_transfer_result aRes; + const char *mime_types[] = { it->second.getStr(), nullptr }; + + gdk_clipboard_read_async(clipboard, + mime_types, + G_PRIORITY_DEFAULT, + nullptr, + read_clipboard_async_completed, + &aRes); + + while (!aRes.bDone) + pInstance->DoYield(true, false); + + if (aFlavor.MimeType == "text/plain;charset=utf-8") + aRet <<= aRes.get_as_string(); + else + aRet <<= aRes.get_as_sequence(); +#endif + return aRet; + } + + std::vector<css::datatransfer::DataFlavor> getTransferDataFlavorsAsVector() + override + { + std::vector<css::datatransfer::DataFlavor> aVector; + + GdkClipboard* clipboard = clipboard_get(m_eSelection); + +#if GTK_CHECK_VERSION(4, 0, 0) + GdkContentFormats* pFormats = gdk_clipboard_get_formats(clipboard); + gsize n_targets; + const char * const *targets = gdk_content_formats_get_mime_types(pFormats, &n_targets); + aVector = GtkTransferable::getTransferDataFlavorsAsVector(targets, n_targets); +#else + GdkAtom *targets; + gint n_targets; + if (gtk_clipboard_wait_for_targets(clipboard, &targets, &n_targets)) + { + aVector = GtkTransferable::getTransferDataFlavorsAsVector(targets, n_targets); + g_free(targets); + } +#endif + + return aVector; + } +}; + +class VclGtkClipboard : + public cppu::WeakComponentImplHelper< + datatransfer::clipboard::XSystemClipboard, + datatransfer::clipboard::XFlushableClipboard, + XServiceInfo> +{ + SelectionType m_eSelection; + 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; +#if GTK_CHECK_VERSION(4, 0, 0) + std::vector<OString> m_aGtkTargets; + TransferableContent* m_pClipboardContent; +#else + std::vector<GtkTargetEntry> m_aGtkTargets; +#endif + VclToGtkHelper m_aConversionHelper; + + DECL_LINK(AsyncSetGtkClipboard, void*, void); + +#if GTK_CHECK_VERSION(4, 0, 0) + DECL_LINK(DetachClipboard, void*, void); +#endif + +public: + + explicit VclGtkClipboard(SelectionType eSelection); + 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; + +#if !GTK_CHECK_VERSION(4, 0, 0) + void ClipboardGet(GtkSelectionData *selection_data, guint info); +#endif + void OwnerPossiblyChanged(GdkClipboard *clipboard); + void ClipboardClear(); + 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_eSelection); +#if GTK_CHECK_VERSION(4, 0, 0) + if (m_pClipboardContent) + transerable_content_set_transferable(m_pClipboardContent, m_aContents.get()); +#endif + } + return m_aContents; +} + +#if !GTK_CHECK_VERSION(4, 0, 0) +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; + } + + void ClipboardGetFunc(GdkClipboard* /*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(GdkClipboard* /*clipboard*/, gpointer user_data_or_owner) + { + VclGtkClipboard* pThis = static_cast<VclGtkClipboard*>(user_data_or_owner); + pThis->ClipboardClear(); + } +} +#endif + +namespace +{ +#if GTK_CHECK_VERSION(4, 0, 0) + void handle_owner_change(GdkClipboard *clipboard, gpointer user_data) + { + VclGtkClipboard* pThis = static_cast<VclGtkClipboard*>(user_data); + pThis->OwnerPossiblyChanged(clipboard); + } +#else + void handle_owner_change(GdkClipboard *clipboard, GdkEvent* /*event*/, gpointer user_data) + { + VclGtkClipboard* pThis = static_cast<VclGtkClipboard*>(user_data); + pThis->OwnerPossiblyChanged(clipboard); + } +#endif +} + +void VclGtkClipboard::OwnerPossiblyChanged(GdkClipboard* clipboard) +{ + SyncGtkClipboard(); // tdf#138183 do any pending SetGtkClipboard calls + if (!m_aContents.is()) + return; + +#if GTK_CHECK_VERSION(4, 0, 0) + bool bSelf = gdk_clipboard_is_local(clipboard); +#else + //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); +#endif + + 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; + } +#if !GTK_CHECK_VERSION(4, 0, 0) + for (auto &a : m_aGtkTargets) + g_free(a.target); +#endif + m_aGtkTargets.clear(); +} + +#if GTK_CHECK_VERSION(4, 0, 0) +IMPL_LINK_NOARG(VclGtkClipboard, DetachClipboard, void*, void) +{ + ClipboardClear(); +} + +OString VclToGtkHelper::makeGtkTargetEntry(const css::datatransfer::DataFlavor& rFlavor) +{ + OString aEntry = OUStringToOString(rFlavor.MimeType, RTL_TEXTENCODING_UTF8); + auto it = std::find_if(aInfoToFlavor.begin(), aInfoToFlavor.end(), + DataFlavorEq(rFlavor)); + if (it == aInfoToFlavor.end()) + aInfoToFlavor.push_back(rFlavor); + return aEntry; +} +#else +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; +} +#endif + +#if GTK_CHECK_VERSION(4, 0, 0) + +namespace +{ + void write_mime_type_done(GObject* pStream, GAsyncResult* pResult, gpointer pTaskPtr) + { + GTask* pTask = static_cast<GTask*>(pTaskPtr); + + GError* pError = nullptr; + if (!g_output_stream_write_all_finish(G_OUTPUT_STREAM(pStream), + pResult, nullptr, &pError)) + { + g_task_return_error(pTask, pError); + } + else + { + g_task_return_boolean(pTask, true); + } + + g_object_unref(pTask); + } + + class MimeTypeEq + { + private: + const OUString& m_rMimeType; + public: + explicit MimeTypeEq(const OUString& rMimeType) : m_rMimeType(rMimeType) {} + bool operator() (const css::datatransfer::DataFlavor& rData) const + { + return rData.MimeType == m_rMimeType; + } + }; +} + +void VclToGtkHelper::setSelectionData(const Reference<css::datatransfer::XTransferable> &rTrans, + GdkContentProvider* provider, + const char* mime_type, + GOutputStream* stream, + int io_priority, + GCancellable* cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GTask *task = g_task_new(provider, cancellable, callback, user_data); + g_task_set_priority(task, io_priority); + + OUString sMimeType(mime_type, strlen(mime_type), RTL_TEXTENCODING_UTF8); + + auto it = std::find_if(aInfoToFlavor.begin(), aInfoToFlavor.end(), + MimeTypeEq(sMimeType)); + if (it == aInfoToFlavor.end()) + { + SAL_WARN( "vcl.gtk", "unknown mime-type request from clipboard"); + g_task_return_new_error(task, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, + "unknown mime-type “%s” request from clipboard", mime_type); + g_object_unref(task); + return; + } + + css::datatransfer::DataFlavor aFlavor(*it); + 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)); + + g_output_stream_write_all_async(stream, aUTF8String.getStr(), aUTF8String.getLength(), + io_priority, cancellable, write_mime_type_done, task); + return; + } + + g_output_stream_write_all_async(stream, aData.getArray(), aData.getLength(), + io_priority, cancellable, write_mime_type_done, task); +} +#else +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()); +} +#endif + +VclGtkClipboard::VclGtkClipboard(SelectionType eSelection) + : cppu::WeakComponentImplHelper<datatransfer::clipboard::XSystemClipboard, + datatransfer::clipboard::XFlushableClipboard, XServiceInfo> + (m_aMutex) + , m_eSelection(eSelection) + , m_pSetClipboardEvent(nullptr) +#if GTK_CHECK_VERSION(4, 0, 0) + , m_pClipboardContent(nullptr) +#endif +{ + GdkClipboard* clipboard = clipboard_get(m_eSelection); +#if GTK_CHECK_VERSION(4, 0, 0) + m_nOwnerChangedSignalId = g_signal_connect(clipboard, "changed", + G_CALLBACK(handle_owner_change), this); +#else + m_nOwnerChangedSignalId = g_signal_connect(clipboard, "owner-change", + G_CALLBACK(handle_owner_change), this); +#endif +} + +void VclGtkClipboard::flushClipboard() +{ +#if !GTK_CHECK_VERSION(4, 0, 0) + SolarMutexGuard aGuard; + + if (m_eSelection != SELECTION_CLIPBOARD) + return; + + GdkClipboard* clipboard = clipboard_get(m_eSelection); + gtk_clipboard_store(clipboard); +#endif +} + +VclGtkClipboard::~VclGtkClipboard() +{ + GdkClipboard* clipboard = clipboard_get(m_eSelection); + g_signal_handler_disconnect(clipboard, m_nOwnerChangedSignalId); + if (!m_aGtkTargets.empty()) + { +#if GTK_CHECK_VERSION(4, 0, 0) + gdk_clipboard_set_content(clipboard, nullptr); + m_pClipboardContent = nullptr; +#else + gtk_clipboard_clear(clipboard); +#endif + ClipboardClear(); + } + assert(!m_pSetClipboardEvent); + assert(m_aGtkTargets.empty()); +} + +#if GTK_CHECK_VERSION(4, 0, 0) +std::vector<OString> VclToGtkHelper::FormatsToGtk(const css::uno::Sequence<css::datatransfer::DataFlavor> &rFormats) +#else +std::vector<GtkTargetEntry> VclToGtkHelper::FormatsToGtk(const css::uno::Sequence<css::datatransfer::DataFlavor> &rFormats) +#endif +{ +#if GTK_CHECK_VERSION(4, 0, 0) + std::vector<OString> aGtkTargets; +#else + std::vector<GtkTargetEntry> aGtkTargets; +#endif + + bool bHaveText(false), bHaveUTF8(false); + for (const css::datatransfer::DataFlavor& rFlavor : rFormats) + { + sal_Int32 nIndex(0); + if (o3tl::getToken(rFlavor.MimeType, 0, ';', nIndex) == u"text/plain") + { + bHaveText = true; + std::u16string_view aToken(o3tl::getToken(rFlavor.MimeType, 0, ';', nIndex)); + if (aToken == u"charset=utf-8") + { + bHaveUTF8 = true; + } + } + aGtkTargets.push_back(makeGtkTargetEntry(rFlavor)); + } + + 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::Guard aGuard( m_aMutex ); + m_pSetClipboardEvent = nullptr; + SetGtkClipboard(); +} + +void VclGtkClipboard::SyncGtkClipboard() +{ + osl::Guard aGuard(m_aMutex); + if (m_pSetClipboardEvent) + { + Application::RemoveUserEvent(m_pSetClipboardEvent); + m_pSetClipboardEvent = nullptr; + SetGtkClipboard(); + } +} + +void VclGtkClipboard::SetGtkClipboard() +{ + GdkClipboard* clipboard = clipboard_get(m_eSelection); +#if GTK_CHECK_VERSION(4, 0, 0) + m_pClipboardContent = TRANSFERABLE_CONTENT(transerable_content_new(&m_aConversionHelper, m_aContents.get())); + transerable_content_set_detach_clipboard_link(m_pClipboardContent, LINK(this, VclGtkClipboard, DetachClipboard)); + gdk_clipboard_set_content(clipboard, GDK_CONTENT_PROVIDER(m_pClipboardContent)); +#else + 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()); +#endif +} + +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; +#if GTK_CHECK_VERSION(4, 0, 0) + if (m_pClipboardContent) + transerable_content_set_transferable(m_pClipboardContent, m_aContents.get()); +#endif + m_aOwner = xClipboardOwner; + + std::vector< Reference< datatransfer::clipboard::XClipboardListener > > aListeners( m_aListeners ); + datatransfer::clipboard::ClipboardEvent aEv; + + GdkClipboard* clipboard = clipboard_get(m_eSelection); + if (!m_aGtkTargets.empty()) + { +#if GTK_CHECK_VERSION(4, 0, 0) + gdk_clipboard_set_content(clipboard, nullptr); + m_pClipboardContent = nullptr; +#else + gtk_clipboard_clear(clipboard); +#endif + ClipboardClear(); + } + assert(m_aGtkTargets.empty()); + if (m_aContents.is()) + { +#if GTK_CHECK_VERSION(4, 0, 0) + std::vector<OString> aGtkTargets(m_aConversionHelper.FormatsToGtk(aFormats)); +#else + std::vector<GtkTargetEntry> aGtkTargets(m_aConversionHelper.FormatsToGtk(aFormats)); +#endif + if (!aGtkTargets.empty()) + { +#if !GTK_CHECK_VERSION(4, 0, 0) + 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); +#endif + 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_eSelection == SELECTION_CLIPBOARD) ? OUString("CLIPBOARD") : OUString("PRIMARY"); +} + +sal_Int8 VclGtkClipboard::getRenderingCapabilities() +{ + return 0; +} + +void VclGtkClipboard::addClipboardListener( const Reference< datatransfer::clipboard::XClipboardListener >& listener ) +{ + osl::Guard aGuard( m_aMutex ); + + m_aListeners.push_back( listener ); +} + +void VclGtkClipboard::removeClipboardListener( const Reference< datatransfer::clipboard::XClipboardListener >& listener ) +{ + osl::Guard aGuard( m_aMutex ); + + std::erase(m_aListeners, listener); +} + +Reference< XInterface > GtkInstance::CreateClipboard(const Sequence< Any >& arguments) +{ + if ( IsRunningUnitTest() ) + return SalInstance::CreateClipboard( 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); + } + + SelectionType eSelection = (sel == "CLIPBOARD") ? SELECTION_CLIPBOARD : SELECTION_PRIMARY; + + if (m_aClipboards[eSelection].is()) + return m_aClipboards[eSelection]; + + Reference<XInterface> xClipboard(getXWeak(new VclGtkClipboard(eSelection))); + m_aClipboards[eSelection] = xClipboard; + return xClipboard; +} + +GtkInstDropTarget::GtkInstDropTarget() + : WeakComponentImplHelper(m_aMutex) + , m_pFrame(nullptr) + , m_pFormatConversionRequest(nullptr) + , m_bActive(false) +#if !GTK_CHECK_VERSION(4, 0, 0) + , m_bInDrag(false) +#endif + , m_nDefaultActions(0) +{ +} + +OUString SAL_CALL GtkInstDropTarget::getImplementationName() +{ + return "com.sun.star.datatransfer.dnd.VclGtkDropTarget"; +} + +sal_Bool SAL_CALL GtkInstDropTarget::supportsService(OUString const & ServiceName) +{ + return cppu::supportsService(this, ServiceName); +} + +css::uno::Sequence<OUString> SAL_CALL GtkInstDropTarget::getSupportedServiceNames() +{ + Sequence<OUString> aRet { "com.sun.star.datatransfer.dnd.GtkDropTarget" }; + return aRet; +} + +GtkInstDropTarget::~GtkInstDropTarget() +{ + if (m_pFrame) + m_pFrame->deregisterDropTarget(this); +} + +void GtkInstDropTarget::deinitialize() +{ + m_pFrame = nullptr; + m_bActive = false; +} + +void GtkInstDropTarget::initialize(const Sequence<Any>& rArguments) +{ + if (rArguments.getLength() < 2) + { + throw RuntimeException("DropTarget::initialize: Cannot install window event handler", + getXWeak()); + } + + sal_IntPtr nFrame = 0; + rArguments.getConstArray()[1] >>= nFrame; + + if (!nFrame) + { + throw RuntimeException("DropTarget::initialize: missing SalFrame", + getXWeak()); + } + + m_pFrame = reinterpret_cast<GtkSalFrame*>(nFrame); + m_pFrame->registerDropTarget(this); + m_bActive = true; +} + +void GtkInstDropTarget::addDropTargetListener( const Reference< css::datatransfer::dnd::XDropTargetListener >& xListener) +{ + ::osl::Guard< ::osl::Mutex > aGuard( m_aMutex ); + + m_aListeners.push_back( xListener ); +} + +void GtkInstDropTarget::removeDropTargetListener( const Reference< css::datatransfer::dnd::XDropTargetListener >& xListener) +{ + ::osl::Guard< ::osl::Mutex > aGuard( m_aMutex ); + + std::erase(m_aListeners, xListener); +} + +void GtkInstDropTarget::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 GtkInstDropTarget::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 GtkInstDropTarget::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 GtkInstDropTarget::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 GtkInstDropTarget::isActive() +{ + return m_bActive; +} + +void GtkInstDropTarget::setActive(sal_Bool bActive) +{ + m_bActive = bActive; +} + +sal_Int8 GtkInstDropTarget::getDefaultActions() +{ + return m_nDefaultActions; +} + +void GtkInstDropTarget::setDefaultActions(sal_Int8 nDefaultActions) +{ + m_nDefaultActions = nDefaultActions; +} + +Reference<XInterface> GtkInstance::ImplCreateDropTarget(const SystemEnvData* pSysEnv) +{ + return vcl::X11DnDHelper(new GtkInstDropTarget(), pSysEnv->aShellWindow); +} + +GtkInstDragSource::~GtkInstDragSource() +{ + if (m_pFrame) + m_pFrame->deregisterDragSource(this); + + if (GtkInstDragSource::g_ActiveDragSource == this) + { + SAL_WARN( "vcl.gtk", "dragEnd should have been called on GtkInstDragSource before dtor"); + GtkInstDragSource::g_ActiveDragSource = nullptr; + } +} + +void GtkInstDragSource::deinitialize() +{ + m_pFrame = nullptr; +} + +sal_Bool GtkInstDragSource::isDragImageSupported() +{ + return true; +} + +sal_Int32 GtkInstDragSource::getDefaultCursor( sal_Int8 ) +{ + return 0; +} + +void GtkInstDragSource::initialize(const css::uno::Sequence<css::uno::Any >& rArguments) +{ + if (rArguments.getLength() < 2) + { + throw RuntimeException("DragSource::initialize: Cannot install window event handler", + getXWeak()); + } + + sal_IntPtr nFrame = 0; + rArguments.getConstArray()[1] >>= nFrame; + + if (!nFrame) + { + throw RuntimeException("DragSource::initialize: missing SalFrame", + getXWeak()); + } + + m_pFrame = reinterpret_cast<GtkSalFrame*>(nFrame); + m_pFrame->registerDragSource(this); +} + +OUString SAL_CALL GtkInstDragSource::getImplementationName() +{ + return "com.sun.star.datatransfer.dnd.VclGtkDragSource"; +} + +sal_Bool SAL_CALL GtkInstDragSource::supportsService(OUString const & ServiceName) +{ + return cppu::supportsService(this, ServiceName); +} + +css::uno::Sequence<OUString> SAL_CALL GtkInstDragSource::getSupportedServiceNames() +{ + Sequence<OUString> aRet { "com.sun.star.datatransfer.dnd.GtkDragSource" }; + return aRet; +} + +Reference<XInterface> GtkInstance::ImplCreateDragSource(const SystemEnvData* pSysEnv) +{ + return vcl::X11DnDHelper(new GtkInstDragSource(), pSysEnv->aShellWindow); +} + +namespace { + +class GtkOpenGLContext : public OpenGLContext +{ + GLWindow m_aGLWin; + GtkWidget *m_pGLArea; + GdkGLContext *m_pContext; + gulong m_nDestroySignalId; + gulong m_nRenderSignalId; + guint m_nAreaFrameBuffer; + guint m_nFrameBuffer; + guint m_nRenderBuffer; + guint m_nDepthBuffer; + guint m_nFrameScratchBuffer; + guint m_nRenderScratchBuffer; + guint m_nDepthScratchBuffer; + +public: + GtkOpenGLContext() + : m_pGLArea(nullptr) + , m_pContext(nullptr) + , m_nDestroySignalId(0) + , m_nRenderSignalId(0) + , 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; + pThis->m_nDestroySignalId = 0; + pThis->m_nRenderSignalId = 0; + } + + 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); + } + + // Use a throw away toplevel to determine the OpenGL version because once + // an GdkGLContext is created for a window then it seems that + // glGenVertexArrays will always be called when the window gets rendered. + static int GetOpenGLVersion() + { + int nMajorGLVersion(0); + + GtkWidget* pWindow; +#if !GTK_CHECK_VERSION(4,0,0) + pWindow = gtk_window_new(GTK_WINDOW_TOPLEVEL); +#else + pWindow = gtk_window_new(); +#endif + + gtk_widget_realize(pWindow); + + if (GdkSurface* pSurface = widget_get_surface(pWindow)) + { + if (GdkGLContext* pContext = surface_create_gl_context(pSurface)) + { + if (gdk_gl_context_realize(pContext, nullptr)) + { + OpenGLZone aZone; + gdk_gl_context_make_current(pContext); + gdk_gl_context_get_version(pContext, &nMajorGLVersion, nullptr); + gdk_gl_context_clear_current(); + } + g_object_unref(pContext); + } + } + +#if !GTK_CHECK_VERSION(4,0,0) + gtk_widget_destroy(pWindow); +#else + gtk_window_destroy(GTK_WINDOW(pWindow)); +#endif + return nMajorGLVersion; + } + + virtual bool ImplInit() override + { + static int nOpenGLVersion = GetOpenGLVersion(); + if (nOpenGLVersion < 3) + { + SAL_WARN("vcl.gtk", "gtk GL requires glGenVertexArrays which is OpenGL 3, while system provides: " << nOpenGLVersion); + return false; + } + + const SystemEnvData* pEnvData = m_pChildWindow->GetSystemData(); + GtkWidget *pParent = static_cast<GtkWidget*>(pEnvData->pWidget); + m_pGLArea = gtk_gl_area_new(); + m_nDestroySignalId = g_signal_connect(G_OBJECT(m_pGLArea), "destroy", G_CALLBACK(signalDestroy), this); + m_nRenderSignalId = 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); +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_container_add(GTK_CONTAINER(pParent), m_pGLArea); + gtk_widget_show_all(pParent); +#else + gtk_grid_attach(GTK_GRID(pParent), m_pGLArea, 0, 0, 1, 1); + gtk_widget_show(pParent); + gtk_widget_show(m_pGLArea); +#endif + + 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); + + GdkSurface* pWindow = widget_get_surface(pParent); + m_pContext = surface_create_gl_context(pWindow); + 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_nDestroySignalId) + g_signal_handler_disconnect(m_pGLArea, m_nDestroySignalId); + if (m_nRenderSignalId) + g_signal_handler_disconnect(m_pGLArea, m_nRenderSignalId); + 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; + static bool bResult = G_TYPE_CHECK_INSTANCE_TYPE(pDisplay, get_type()); + return bResult; +} + +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; + static bool bResult = G_TYPE_CHECK_INSTANCE_TYPE(pDisplay, get_type()); + return bResult; +} + +namespace +{ + +class GtkInstanceBuilder; + + void set_help_id(const GtkWidget *pWidget, std::u16string_view rHelpId) + { + gchar *helpid = g_strdup(OUStringToOString(rHelpId, RTL_TEXTENCODING_UTF8).getStr()); + g_object_set_data_full(G_OBJECT(pWidget), "g-lo-helpid", helpid, g_free); + } + + OUString 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 OUString(pStr, pStr ? strlen(pStr) : 0, RTL_TEXTENCODING_UTF8); + } + + KeyEvent CreateKeyEvent(guint keyval, guint16 hardware_keycode, guint state, guint8 group) + { + sal_uInt16 nKeyCode = GtkSalFrame::GetKeyCode(keyval); +#if !GTK_CHECK_VERSION(4, 0, 0) + if (nKeyCode == 0) + { + guint updated_keyval = GtkSalFrame::GetKeyValFor(gdk_keymap_get_default(), hardware_keycode, group); + nKeyCode = GtkSalFrame::GetKeyCode(updated_keyval); + } +#else + (void)hardware_keycode; + (void)group; +#endif + nKeyCode |= GtkSalFrame::GetKeyModCode(state); + return KeyEvent(gdk_keyval_to_unicode(keyval), nKeyCode, 0); + } + +#if !GTK_CHECK_VERSION(4, 0, 0) + KeyEvent GtkToVcl(const GdkEventKey& rEvent) + { + return CreateKeyEvent(rEvent.keyval, rEvent.hardware_keycode, rEvent.state, rEvent.group); + } +#endif +} + +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(); + } + + GtkWidget* getPopupRect(GtkWidget* pWidget, const tools::Rectangle& rInRect, GdkRectangle& rOutRect) + { + if (GtkSalFrame* pFrame = GtkSalFrame::getFromWindow(pWidget)) + { + // this is the relatively unusual case where pParent is the toplevel GtkSalFrame and not a stock GtkWidget + // so use the same style of logic as GtkSalMenu::ShowNativePopupMenu to get the right position + AbsoluteScreenPixelRectangle aFloatRect = FloatingWindow::ImplConvertToAbsPos(pFrame->GetWindow(), rInRect); + aFloatRect.Move(-pFrame->maGeometry.x(), -pFrame->maGeometry.y()); + + rOutRect = GdkRectangle{static_cast<int>(aFloatRect.Left()), static_cast<int>(aFloatRect.Top()), + static_cast<int>(aFloatRect.GetWidth()), static_cast<int>(aFloatRect.GetHeight())}; + + pWidget = pFrame->getMouseEventWidget(); + } + else + { + rOutRect = GdkRectangle{static_cast<int>(rInRect.Left()), static_cast<int>(rInRect.Top()), + static_cast<int>(rInRect.GetWidth()), static_cast<int>(rInRect.GetHeight())}; + if (SwapForRTL(pWidget)) + rOutRect.x = gtk_widget_get_allocated_width(pWidget) - rOutRect.width - 1 - rOutRect.x; + } + return pWidget; + } + + 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)) + { +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_container_child_get(GTK_CONTAINER(pParent), pWidget, + "left-attach", &nLeftAttach, + "top-attach", &nTopAttach, + "width", &nWidth, + "height", &nHeight, + nullptr); +#else + gtk_grid_query_child(GTK_GRID(pParent), pWidget, + &nLeftAttach, &nTopAttach, + &nWidth, &nHeight); +#endif + } + +#if !GTK_CHECK_VERSION(4, 0, 0) + 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); + } +#endif + +#if !GTK_CHECK_VERSION(4, 0, 0) + // for gtk3 remove before replacement inserted, or there are warnings + // from GTK_BIN about having two children + container_remove(pParent, pWidget); +#endif + + gtk_widget_set_visible(pReplacement, gtk_widget_get_visible(pWidget)); +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_set_no_show_all(pReplacement, gtk_widget_get_no_show_all(pWidget)); +#endif + + 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); + + + if (GTK_IS_GRID(pParent)) + { + gtk_grid_attach(GTK_GRID(pParent), pReplacement, nLeftAttach, nTopAttach, nWidth, nHeight); + } + else if (GTK_IS_BOX(pParent)) + { +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_box_pack_start(GTK_BOX(pParent), pReplacement, bExpand, bFill, nPadding); + gtk_container_child_set(GTK_CONTAINER(pParent), pReplacement, + "pack-type", ePackType, + "position", nPosition, + nullptr); +#else + gtk_box_insert_child_after(GTK_BOX(pParent), pReplacement, pWidget); +#endif + } +#if !GTK_CHECK_VERSION(4, 0, 0) + else + gtk_container_add(GTK_CONTAINER(pParent), pReplacement); +#endif + + 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)); + +#if GTK_CHECK_VERSION(4, 0, 0) + // for gtk4 remove after replacement inserted so we could use gtk_box_insert_child_after + container_remove(pParent, pWidget); +#endif + + // coverity[freed_arg : FALSE] - this does not free pWidget, it is reffed by pReplacement + g_object_unref(pWidget); + } + + void insertAsParent(GtkWidget* pWidget, GtkWidget* pReplacement) + { + g_object_ref(pWidget); + + replaceWidget(pWidget, pReplacement); + + // coverity[pass_freed_arg : FALSE] - pWidget is not freed here due to initial g_object_ref + container_add(pReplacement, pWidget); + + // coverity[freed_arg : FALSE] - this does not free pWidget, it is reffed by pReplacement + g_object_unref(pWidget); + } + + GtkWidget* ensureEventWidget(GtkWidget* pWidget) + { +#if GTK_CHECK_VERSION(4, 0, 0) + return pWidget; +#else + + 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; +#endif + } +} + +namespace { + +#if !GTK_CHECK_VERSION(4, 0, 0) +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; +} +#endif + +GtkWindow* get_active_window() +{ + GtkWindow* pFocus = nullptr; + + GList* pList = gtk_window_list_toplevels(); + + for (GList* pEntry = pList; pEntry; pEntry = pEntry->next) + { +#if GTK_CHECK_VERSION(4, 0, 0) + if (gtk_window_is_active(GTK_WINDOW(pEntry->data))) +#else + if (gtk_window_has_toplevel_focus(GTK_WINDOW(pEntry->data))) +#endif + { + pFocus = GTK_WINDOW(pEntry->data); + break; + } + } + + g_list_free(pList); + + return pFocus; +} + +void LocalizeDecimalSeparator(guint& keyval) +{ + const bool bDecimalKey = keyval == GDK_KEY_KP_Decimal || keyval == GDK_KEY_KP_Separator; + // #i1820# (and tdf#154623) use locale specific decimal separator + if (bDecimalKey && Application::GetSettings().GetMiscSettings().GetEnableLocalizedDecimalSep()) + { + GtkWindow* pFocusWin = get_active_window(); + GtkWidget* pFocus = pFocusWin ? gtk_window_get_focus(pFocusWin) : nullptr; + // tdf#138932 except if the target is a GtkEntry used for passwords + // GTK4: TODO is it a GtkEntry or a child GtkText that has the focus in this situation? + if (!pFocus || !GTK_IS_ENTRY(pFocus) || gtk_entry_get_visibility(GTK_ENTRY(pFocus))) + { + OUString aSep(Application::GetSettings().GetLocaleDataWrapper().getNumDecimalSep()); + keyval = aSep[0]; + } + } +} + +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); +#if GTK_CHECK_VERSION(4, 0, 0) + GdkCursor *pCursor = pName ? gdk_cursor_new_from_name(pName, nullptr) : nullptr; +#else + GdkCursor *pCursor = pName ? gdk_cursor_new_from_name(pDisplay, pName) : nullptr; +#endif + widget_set_cursor(pWidget, pCursor); + gdk_display_flush(pDisplay); + if (pCursor) + g_object_unref(pCursor); +} + +vcl::Font get_font(GtkWidget* pWidget) +{ + PangoContext* pContext = gtk_widget_get_pango_context(pWidget); + return pango_to_vcl(pango_context_get_font_description(pContext), + Application::GetSettings().GetUILanguageTag().getLocale()); +} + +} + +OUString get_buildable_id(GtkBuildable* pWidget) +{ +#if GTK_CHECK_VERSION(4, 0, 0) + const gchar* pStr = gtk_buildable_get_buildable_id(pWidget); +#else + const gchar* pStr = gtk_buildable_get_name(pWidget); +#endif + return OUString(pStr, pStr ? strlen(pStr) : 0, RTL_TEXTENCODING_UTF8); +} + +void set_buildable_id(GtkBuildable* pWidget, const OUString& rId) +{ +#if GTK_CHECK_VERSION(4, 0, 0) + GtkBuildableIface *iface = GTK_BUILDABLE_GET_IFACE(pWidget); + (*iface->set_id)(pWidget, OUStringToOString(rId, RTL_TEXTENCODING_UTF8).getStr()); +#else + gtk_buildable_set_name(pWidget, OUStringToOString(rId, RTL_TEXTENCODING_UTF8).getStr()); +#endif +} + +namespace { + +class GtkInstanceWidget : public virtual weld::Widget +{ +protected: + GtkWidget* m_pWidget; + GtkWidget* m_pMouseEventBox; + GtkInstanceBuilder* m_pBuilder; + +#if !GTK_CHECK_VERSION(4, 0, 0) + DECL_LINK(async_drag_cancel, void*, void); +#endif + + bool IsFirstFreeze() const { return m_nFreezeCount == 0; } + bool IsLastThaw() const { return m_nFreezeCount == 1; } + +#if GTK_CHECK_VERSION(4, 0, 0) + static void signalFocusIn(GtkEventControllerFocus*, gpointer widget) + { + GtkInstanceWidget* pThis = static_cast<GtkInstanceWidget*>(widget); + SolarMutexGuard aGuard; + pThis->signal_focus_in(); + } +#else + static gboolean signalFocusIn(GtkWidget*, GdkEvent*, gpointer widget) + { + GtkInstanceWidget* pThis = static_cast<GtkInstanceWidget*>(widget); + SolarMutexGuard aGuard; + pThis->signal_focus_in(); + return false; + } +#endif + + void signal_focus_in() + { + GtkWidget* pTopLevel = widget_get_toplevel(m_pWidget); + // see commentary in GtkSalObjectWidgetClip::Show + if (pTopLevel && g_object_get_data(G_OBJECT(pTopLevel), "g-lo-BlockFocusChange")) + return; + + 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); + } + +#if GTK_CHECK_VERSION(4, 0, 0) + static void signalFocusOut(GtkEventControllerFocus*, gpointer widget) + { + GtkInstanceWidget* pThis = static_cast<GtkInstanceWidget*>(widget); + SolarMutexGuard aGuard; + pThis->signal_focus_in(); + } +#else + static gboolean signalFocusOut(GtkWidget*, GdkEvent*, gpointer widget) + { + GtkInstanceWidget* pThis = static_cast<GtkInstanceWidget*>(widget); + SolarMutexGuard aGuard; + pThis->signal_focus_out(); + return false; + } +#endif + +#if !GTK_CHECK_VERSION(4, 0, 0) + 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); + } +#endif + + void signal_focus_out() + { + GtkWidget* pTopLevel = widget_get_toplevel(m_pWidget); + // see commentary in GtkSalObjectWidgetClip::Show + if (pTopLevel && g_object_get_data(G_OBJECT(pTopLevel), "g-lo-BlockFocusChange")) + return; + + m_aFocusOutHdl.Call(*this); + } + + virtual void ensureMouseEventWidget() + { + if (!m_pMouseEventBox) + m_pMouseEventBox = ::ensureEventWidget(m_pWidget); + } + + void ensureButtonPressSignal() + { + if (!m_nButtonPressSignalId) + { +#if GTK_CHECK_VERSION(4, 0, 0) + GtkEventController* pClickController = get_click_controller(); + m_nButtonPressSignalId = g_signal_connect(pClickController, "pressed", G_CALLBACK(signalButtonPress), this); +#else + ensureMouseEventWidget(); + m_nButtonPressSignalId = g_signal_connect(m_pMouseEventBox, "button-press-event", G_CALLBACK(signalButtonPress), this); +#endif + } + } + + void ensureButtonReleaseSignal() + { + if (!m_nButtonReleaseSignalId) + { +#if GTK_CHECK_VERSION(4, 0, 0) + GtkEventController* pClickController = get_click_controller(); + m_nButtonReleaseSignalId = g_signal_connect(pClickController, "released", G_CALLBACK(signalButtonRelease), this); +#else + ensureMouseEventWidget(); + m_nButtonReleaseSignalId = g_signal_connect(m_pMouseEventBox, "button-release-event", G_CALLBACK(signalButtonRelease), this); +#endif + } + } + +#if !GTK_CHECK_VERSION(4, 0, 0) + 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); + } +#endif + + virtual void connect_style_updated(const Link<Widget&, void>& rLink) override + { + if (m_aStyleUpdatedHdl.IsSet()) + ImplGetDefaultWindow()->RemoveEventListener(LINK(this, GtkInstanceWidget, SettingsChangedHdl)); + weld::Widget::connect_style_updated(rLink); + if (m_aStyleUpdatedHdl.IsSet()) + ImplGetDefaultWindow()->AddEventListener(LINK(this, GtkInstanceWidget, SettingsChangedHdl)); + } + + bool SwapForRTL() const + { + return ::SwapForRTL(m_pWidget); + } + + void do_enable_drag_source(const rtl::Reference<TransferDataContainer>& rHelper, sal_uInt8 eDNDConstants) + { + ensure_drag_source(); + +#if !GTK_CHECK_VERSION(4, 0, 0) + auto aFormats = rHelper->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(rHelper, rHelper); +#else + (void)rHelper; + (void)eDNDConstants; +#endif + } + + 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()) + { +#if GTK_CHECK_VERSION(4, 0, 0) + m_nKeyPressSignalId = g_signal_connect(get_key_controller(), "key-pressed", G_CALLBACK(signalKeyPressed), this); +#else + m_nKeyPressSignalId = g_signal_connect(m_pWidget, "key-press-event", G_CALLBACK(signalKey), this); +#endif + } + } + + void ensure_drag_begin_end() + { + if (!m_nDragBeginSignalId) + { + // using "after" due to https://gitlab.gnome.org/GNOME/pygobject/issues/251 +#if GTK_CHECK_VERSION(4, 0, 0) + m_nDragBeginSignalId = g_signal_connect_after(get_drag_controller(), "drag-begin", G_CALLBACK(signalDragBegin), this); +#else + m_nDragBeginSignalId = g_signal_connect_after(m_pWidget, "drag-begin", G_CALLBACK(signalDragBegin), this); +#endif + } + if (!m_nDragEndSignalId) + { +#if GTK_CHECK_VERSION(4, 0, 0) + m_nDragEndSignalId = g_signal_connect(get_drag_controller(), "drag-end", G_CALLBACK(signalDragEnd), this); +#else + m_nDragEndSignalId = g_signal_connect(m_pWidget, "drag-end", G_CALLBACK(signalDragEnd), this); +#endif + } + } + + void DisconnectMouseEvents() + { + if (m_nButtonPressSignalId) + { +#if GTK_CHECK_VERSION(4, 0, 0) + g_signal_handler_disconnect(get_click_controller(), m_nButtonPressSignalId); +#else + g_signal_handler_disconnect(m_pMouseEventBox, m_nButtonPressSignalId); +#endif + m_nButtonPressSignalId = 0; + } + if (m_nMotionSignalId) + { +#if GTK_CHECK_VERSION(4, 0, 0) + g_signal_handler_disconnect(get_motion_controller(), m_nMotionSignalId); +#else + g_signal_handler_disconnect(m_pMouseEventBox, m_nMotionSignalId); +#endif + m_nMotionSignalId = 0; + } + if (m_nLeaveSignalId) + { +#if GTK_CHECK_VERSION(4, 0, 0) + g_signal_handler_disconnect(get_motion_controller(), m_nLeaveSignalId); +#else + g_signal_handler_disconnect(m_pMouseEventBox, m_nLeaveSignalId); +#endif + m_nLeaveSignalId = 0; + } + if (m_nEnterSignalId) + { +#if GTK_CHECK_VERSION(4, 0, 0) + g_signal_handler_disconnect(get_motion_controller(), m_nEnterSignalId); +#else + g_signal_handler_disconnect(m_pMouseEventBox, m_nEnterSignalId); +#endif + m_nEnterSignalId = 0; + } + if (m_nButtonReleaseSignalId) + { +#if GTK_CHECK_VERSION(4, 0, 0) + g_signal_handler_disconnect(get_click_controller(), m_nButtonReleaseSignalId); +#else + g_signal_handler_disconnect(m_pMouseEventBox, m_nButtonReleaseSignalId); +#endif + m_nButtonReleaseSignalId = 0; + } + +#if !GTK_CHECK_VERSION(4, 0, 0) + if (!m_pMouseEventBox || m_pMouseEventBox == m_pWidget) + return; + + // GtkWindow replacement for GtkPopover case + if (!GTK_IS_EVENT_BOX(m_pMouseEventBox)) + { + m_pMouseEventBox = nullptr; + return; + } + + // 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); + // coverity[freed_arg : FALSE] - this does not free m_pWidget, it is reffed by pParent + g_object_unref(m_pWidget); + + m_pMouseEventBox = m_pWidget; +#endif + } + +private: + bool m_bTakeOwnership; +#if !GTK_CHECK_VERSION(4, 0, 0) + bool m_bDraggedOver; +#endif + int m_nWaitCount; + int m_nFreezeCount; + sal_uInt16 m_nLastMouseButton; +#if !GTK_CHECK_VERSION(4, 0, 0) + sal_uInt16 m_nLastMouseClicks; +#endif + int m_nPressedButton; +#if !GTK_CHECK_VERSION(4, 0, 0) +protected: + int m_nPressStartX; + int m_nPressStartY; +private: +#endif + ImplSVEvent* m_pDragCancelEvent; + GtkCssProvider* m_pBgCssProvider; +#if !GTK_CHECK_VERSION(4, 0, 0) + GdkDragAction m_eDragAction; +#endif + gulong m_nFocusInSignalId; + gulong m_nMnemonicActivateSignalId; + gulong m_nFocusOutSignalId; + gulong m_nKeyPressSignalId; + gulong m_nKeyReleaseSignalId; +protected: + gulong m_nSizeAllocateSignalId; +private: + 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; + +#if GTK_CHECK_VERSION(4, 0, 0) + int m_nGrabCount; + GtkEventController* m_pFocusController; + GtkEventController* m_pClickController; + GtkEventController* m_pMotionController; + GtkEventController* m_pDragController; + GtkEventController* m_pKeyController; +#endif + + rtl::Reference<GtkInstDropTarget> m_xDropTarget; + rtl::Reference<GtkInstDragSource> 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); + } + +#if GTK_CHECK_VERSION(4, 0, 0) + static gboolean signalKeyPressed(GtkEventControllerKey*, guint keyval, guint keycode, GdkModifierType state, gpointer widget) + { + LocalizeDecimalSeparator(keyval); + GtkInstanceWidget* pThis = static_cast<GtkInstanceWidget*>(widget); + return pThis->signal_key_press(keyval, keycode, state); + } + + static gboolean signalKeyReleased(GtkEventControllerKey*, guint keyval, guint keycode, GdkModifierType state, gpointer widget) + { + LocalizeDecimalSeparator(keyval); + GtkInstanceWidget* pThis = static_cast<GtkInstanceWidget*>(widget); + return pThis->signal_key_release(keyval, keycode, state); + } +#else + static gboolean signalKey(GtkWidget*, GdkEventKey* pEvent, gpointer widget) + { + LocalizeDecimalSeparator(pEvent->keyval); + GtkInstanceWidget* pThis = static_cast<GtkInstanceWidget*>(widget); + if (pEvent->type == GDK_KEY_PRESS) + return pThis->signal_key_press(pEvent); + return pThis->signal_key_release(pEvent); + } +#endif + + virtual bool signal_popup_menu(const CommandEvent&) + { + return false; + } + +#if GTK_CHECK_VERSION(4, 0, 0) + static void signalButtonPress(GtkGestureClick* pGesture, int n_press, gdouble x, gdouble y, gpointer widget) + { + GtkInstanceWidget* pThis = static_cast<GtkInstanceWidget*>(widget); + SolarMutexGuard aGuard; + pThis->signal_button(pGesture, SalEvent::MouseButtonDown, n_press, x, y); + } + + static void signalButtonRelease(GtkGestureClick* pGesture, int n_press, gdouble x, gdouble y, gpointer widget) + { + GtkInstanceWidget* pThis = static_cast<GtkInstanceWidget*>(widget); + SolarMutexGuard aGuard; + pThis->signal_button(pGesture, SalEvent::MouseButtonUp, n_press, x, y); + } + + void signal_button(GtkGestureClick* pGesture, SalEvent nEventType, int n_press, gdouble x, gdouble y) + { + m_nPressedButton = -1; + + Point aPos(x, y); + if (SwapForRTL()) + aPos.setX(gtk_widget_get_allocated_width(m_pWidget) - 1 - aPos.X()); + + if (n_press == 1) + { + GdkEventSequence* pSequence = gtk_gesture_single_get_current_sequence(GTK_GESTURE_SINGLE(pGesture)); + GdkEvent* pEvent = gtk_gesture_get_last_event(GTK_GESTURE(pGesture), pSequence); + if (gdk_event_triggers_context_menu(pEvent)) + { + //if handled for context menu, stop processing + CommandEvent aCEvt(aPos, CommandEventId::ContextMenu, true); + if (signal_popup_menu(aCEvt)) + { + gtk_gesture_set_state(GTK_GESTURE(pGesture), GTK_EVENT_SEQUENCE_CLAIMED); + return; + } + } + } + + GdkModifierType eType = gtk_event_controller_get_current_event_state(GTK_EVENT_CONTROLLER(pGesture)); + int nButton = gtk_gesture_single_get_current_button(GTK_GESTURE_SINGLE(pGesture)); + + switch (nButton) + { + case 1: + m_nLastMouseButton = MOUSE_LEFT; + break; + case 2: + m_nLastMouseButton = MOUSE_MIDDLE; + break; + case 3: + m_nLastMouseButton = MOUSE_RIGHT; + break; + default: + return; + } + + sal_uInt32 nModCode = GtkSalFrame::GetMouseModCode(eType); + // 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, n_press, ImplGetMouseButtonMode(m_nLastMouseButton, nModCode), nCode, nCode); + + if (nEventType == SalEvent::MouseButtonDown && m_aMousePressHdl.Call(aMEvt)) + gtk_gesture_set_state(GTK_GESTURE(pGesture), GTK_EVENT_SEQUENCE_CLAIMED); + + if (nEventType == SalEvent::MouseButtonUp && m_aMouseReleaseHdl.Call(aMEvt)) + gtk_gesture_set_state(GTK_GESTURE(pGesture), GTK_EVENT_SEQUENCE_CLAIMED); + } + +#else + + static gboolean signalButtonPress(GtkWidget*, GdkEventButton* pEvent, gpointer widget) + { + GtkInstanceWidget* pThis = static_cast<GtkInstanceWidget*>(widget); + SolarMutexGuard aGuard; + return pThis->signal_button(pEvent); + } + + static gboolean signalButtonRelease(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; + } + + /* 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; + } + + 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; + } + + 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); + } +#endif + + bool simple_signal_motion(double x, double y, guint nState) + { + if (!m_aMouseMotionHdl.IsSet()) + return false; + + Point aPos(x, y); + if (SwapForRTL()) + aPos.setX(gtk_widget_get_allocated_width(m_pWidget) - 1 - aPos.X()); + sal_uInt32 nModCode = GtkSalFrame::GetMouseModCode(nState); + MouseEvent aMEvt(aPos, 0, ImplGetMouseMoveMode(nModCode), nModCode, nModCode); + + return m_aMouseMotionHdl.Call(aMEvt); + } + +#if GTK_CHECK_VERSION(4, 0, 0) + static void signalMotion(GtkEventControllerMotion *pController, double x, double y, gpointer widget) + { + GtkInstanceWidget* pThis = static_cast<GtkInstanceWidget*>(widget); + GdkModifierType eType = gtk_event_controller_get_current_event_state(GTK_EVENT_CONTROLLER(pController)); + + SolarMutexGuard aGuard; + pThis->simple_signal_motion(x, y, eType); + } + +#else + 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) + { + const bool bDragData = m_eDragAction != 0 && m_nPressedButton != -1 && m_xDragSource.is() && gtk_drag_source_get_target_list(m_pWidget); + bool bUnsetDragIcon(false); + if (bDragData && 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, + gtk_drag_source_get_target_list(m_pWidget), + 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); + cairo_surface_destroy(surface); + } + + m_nPressedButton = -1; + return false; + } + + return simple_signal_motion(pEvent->x, pEvent->y, pEvent->state); + } +#endif + + bool signal_crossing(double x, double y, guint nState, MouseEventModifiers eMouseEventModifiers) + { + if (!m_aMouseMotionHdl.IsSet()) + return false; + + Point aPos(x, y); + if (SwapForRTL()) + aPos.setX(gtk_widget_get_allocated_width(m_pWidget) - 1 - aPos.X()); + sal_uInt32 nModCode = GtkSalFrame::GetMouseModCode(nState); + MouseEventModifiers eModifiers = ImplGetMouseMoveMode(nModCode); + eModifiers = eModifiers | eMouseEventModifiers; + MouseEvent aMEvt(aPos, 0, eModifiers, nModCode, nModCode); + + m_aMouseMotionHdl.Call(aMEvt); + return false; + } + +#if GTK_CHECK_VERSION(4, 0, 0) + static void signalEnter(GtkEventControllerMotion *pController, double x, double y, gpointer widget) + { + GtkInstanceWidget* pThis = static_cast<GtkInstanceWidget*>(widget); + GdkModifierType eType = gtk_event_controller_get_current_event_state(GTK_EVENT_CONTROLLER(pController)); + SolarMutexGuard aGuard; + pThis->signal_crossing(x, y, eType, MouseEventModifiers::ENTERWINDOW); + } + + static void signalLeave(GtkEventControllerMotion *pController, gpointer widget) + { + GtkInstanceWidget* pThis = static_cast<GtkInstanceWidget*>(widget); + GdkModifierType eType = gtk_event_controller_get_current_event_state(GTK_EVENT_CONTROLLER(pController)); + SolarMutexGuard aGuard; + pThis->signal_crossing(-1, -1, eType, MouseEventModifiers::LEAVEWINDOW); + } +#else + static gboolean signalCrossing(GtkWidget*, GdkEventCrossing* pEvent, gpointer widget) + { + GtkInstanceWidget* pThis = static_cast<GtkInstanceWidget*>(widget); + MouseEventModifiers eMouseEventModifiers = pEvent->type == GDK_ENTER_NOTIFY ? MouseEventModifiers::ENTERWINDOW : MouseEventModifiers::LEAVEWINDOW; + SolarMutexGuard aGuard; + return pThis->signal_crossing(pEvent->x, pEvent->y, pEvent->state, eMouseEventModifiers); + } +#endif + + virtual void drag_started() + { + } + +#if !GTK_CHECK_VERSION(4, 0, 0) + 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); + } +#endif + + virtual void drag_ended() + { + } + +#if !GTK_CHECK_VERSION(4, 0, 0) + static void signalDragLeave(GtkWidget* pWidget, GdkDragContext*, guint /*time*/, gpointer widget) + { + GtkInstanceWidget* pThis = static_cast<GtkInstanceWidget*>(widget); + pThis->m_xDropTarget->signalDragLeave(pWidget); + if (pThis->m_bDraggedOver) + { + pThis->m_bDraggedOver = false; + pThis->drag_ended(); + } + } +#endif + +#if GTK_CHECK_VERSION(4, 0, 0) + static void signalDragBegin(GtkDragSource* context, GdkDrag*, gpointer widget) +#else + static void signalDragBegin(GtkWidget*, GdkDragContext* context, gpointer widget) +#endif + { + GtkInstanceWidget* pThis = static_cast<GtkInstanceWidget*>(widget); + pThis->signal_drag_begin(context); + } + + void ensure_drag_source() + { + if (!m_xDragSource) + { + m_xDragSource.set(new GtkInstDragSource); + +#if !GTK_CHECK_VERSION(4, 0, 0) + 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); +#endif + + ensure_drag_begin_end(); + } + } + + virtual bool do_signal_drag_begin(bool& rUnsetDragIcon) + { + rUnsetDragIcon = false; + return false; + } + +#if GTK_CHECK_VERSION(4, 0, 0) + virtual void drag_set_icon(GtkDragSource*) +#else + virtual void drag_set_icon(GdkDragContext*) +#endif + { + } + +#if GTK_CHECK_VERSION(4, 0, 0) + void signal_drag_begin(GtkDragSource* context) +#else + void signal_drag_begin(GdkDragContext* context) +#endif + { + bool bUnsetDragIcon(false); + if (do_signal_drag_begin(bUnsetDragIcon)) + { +#if !GTK_CHECK_VERSION(4, 0, 0) + launch_drag_cancel(context); +#endif + return; + } + if (bUnsetDragIcon) + { +#if !GTK_CHECK_VERSION(4, 0, 0) + cairo_surface_t *surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, 0, 0); + gtk_drag_set_icon_surface(context, surface); + cairo_surface_destroy(surface); +#endif + } + else + { + drag_set_icon(context); + } + + if (!m_xDragSource) + return; + m_xDragSource->setActiveDragSource(); + } + + virtual void do_signal_drag_end() + { + } + +#if GTK_CHECK_VERSION(4, 0, 0) + static void signalDragEnd(GtkGestureDrag* /*gesture*/, double /*offset_x*/, double /*offset_y*/, gpointer widget) +#else + static void signalDragEnd(GtkWidget* /*widget*/, GdkDragContext* context, gpointer widget) +#endif + { + GtkInstanceWidget* pThis = static_cast<GtkInstanceWidget*>(widget); + pThis->do_signal_drag_end(); +#if !GTK_CHECK_VERSION(4, 0, 0) + if (pThis->m_xDragSource.is()) + pThis->m_xDragSource->dragEnd(context); +#endif + } + +#if !GTK_CHECK_VERSION(4, 0, 0) + 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); + } +#endif + +#if !GTK_CHECK_VERSION(4, 0, 0) + virtual void drag_source_set(const std::vector<GtkTargetEntry>& rGtkTargets, GdkDragAction eDragAction) + { + if (rGtkTargets.empty() && !eDragAction) + gtk_drag_source_unset(m_pWidget); + else + gtk_drag_source_set(m_pWidget, GDK_BUTTON1_MASK, rGtkTargets.data(), rGtkTargets.size(), eDragAction); + } +#endif + + void do_set_background(const Color& rColor) + { + const bool bRemoveColor = rColor == COL_AUTO; + if (bRemoveColor && !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 (bRemoveColor) + return; + OUString sColor = rColor.AsRGBHexString(); + m_pBgCssProvider = gtk_css_provider_new(); + OUString aBuffer = "* { background-color: #" + sColor + "; }"; + OString aResult = OUStringToOString(aBuffer, RTL_TEXTENCODING_UTF8); + css_provider_load_from_data(m_pBgCssProvider, aResult.getStr(), aResult.getLength()); + gtk_style_context_add_provider(pWidgetContext, GTK_STYLE_PROVIDER(m_pBgCssProvider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + } + + DECL_LINK(SettingsChangedHdl, VclWindowEvent&, void); + +#if !GTK_CHECK_VERSION(4, 0, 0) + static void update_style(GtkWidget* pWidget, gpointer pData) + { + if (GTK_IS_CONTAINER(pWidget)) + gtk_container_foreach(GTK_CONTAINER(pWidget), update_style, pData); + GtkWidgetClass* pWidgetClass = GTK_WIDGET_GET_CLASS(pWidget); + pWidgetClass->style_updated(pWidget); + } +#endif + +public: + GtkInstanceWidget(GtkWidget* pWidget, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : m_pWidget(pWidget) + , m_pMouseEventBox(nullptr) + , m_pBuilder(pBuilder) + , m_bTakeOwnership(bTakeOwnership) +#if !GTK_CHECK_VERSION(4, 0, 0) + , m_bDraggedOver(false) +#endif + , m_nWaitCount(0) + , m_nFreezeCount(0) + , m_nLastMouseButton(0) +#if !GTK_CHECK_VERSION(4, 0, 0) + , m_nLastMouseClicks(0) +#endif + , m_nPressedButton(-1) +#if !GTK_CHECK_VERSION(4, 0, 0) + , m_nPressStartX(-1) + , m_nPressStartY(-1) +#endif + , m_pDragCancelEvent(nullptr) + , m_pBgCssProvider(nullptr) +#if !GTK_CHECK_VERSION(4, 0, 0) + , m_eDragAction(GdkDragAction(0)) +#endif + , 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 GTK_CHECK_VERSION(4, 0, 0) + , m_nGrabCount(0) + , m_pFocusController(nullptr) + , m_pClickController(nullptr) + , m_pMotionController(nullptr) + , m_pDragController(nullptr) + , m_pKeyController(nullptr) +#endif + { + if (!bTakeOwnership) + g_object_ref(m_pWidget); + + localizeDecimalSeparator(); + } + + virtual void connect_key_press(const Link<const KeyEvent&, bool>& rLink) override + { + if (!m_nKeyPressSignalId) + { +#if GTK_CHECK_VERSION(4, 0, 0) + m_nKeyPressSignalId = g_signal_connect(get_key_controller(), "key-pressed", G_CALLBACK(signalKeyPressed), this); +#else + m_nKeyPressSignalId = g_signal_connect(m_pWidget, "key-press-event", G_CALLBACK(signalKey), this); +#endif + } + weld::Widget::connect_key_press(rLink); + } + + virtual void connect_key_release(const Link<const KeyEvent&, bool>& rLink) override + { + if (!m_nKeyReleaseSignalId) + { +#if GTK_CHECK_VERSION(4, 0, 0) + m_nKeyReleaseSignalId = g_signal_connect(get_key_controller(), "key-released", G_CALLBACK(signalKeyReleased), this); +#else + m_nKeyReleaseSignalId = g_signal_connect(m_pWidget, "key-release-event", G_CALLBACK(signalKey), this); +#endif + } + 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 + { +#if GTK_CHECK_VERSION(4, 0, 0) + GtkEventController* pMotionController = get_motion_controller(); + if (!m_nMotionSignalId) + m_nMotionSignalId = g_signal_connect(pMotionController, "motion", G_CALLBACK(signalMotion), this); + if (!m_nLeaveSignalId) + m_nLeaveSignalId = g_signal_connect(pMotionController, "leave", G_CALLBACK(signalEnter), this); + if (!m_nEnterSignalId) + m_nEnterSignalId = g_signal_connect(pMotionController, "enter", G_CALLBACK(signalLeave), this); +#else + ensureMouseEventWidget(); + 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); +#endif + weld::Widget::connect_mouse_move(rLink); + } + + virtual void connect_mouse_release(const Link<const MouseEvent&, bool>& rLink) override + { + ensureButtonReleaseSignal(); + 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 + { + if (has_focus()) + return; + gtk_widget_grab_focus(m_pWidget); + } + + virtual bool has_focus() const override + { + return gtk_widget_has_focus(m_pWidget); + } + + virtual bool is_active() const override + { + GtkWindow* pTopLevel = GTK_WINDOW(widget_get_toplevel(m_pWidget)); + return pTopLevel && gtk_window_is_active(pTopLevel) && has_focus(); + } + + // is the focus in a child of this widget, where a transient popup attached + // to a widget is considered a child of that widget + virtual bool has_child_focus() const override + { + GtkWindow* pFocusWin = get_active_window(); + if (!pFocusWin) + return false; + GtkWidget* pFocus = gtk_window_get_focus(pFocusWin); + if (pFocus && gtk_widget_is_ancestor(pFocus, m_pWidget)) + return true; +#if !GTK_CHECK_VERSION(4, 0, 0) + GtkWidget* pAttachedTo = gtk_window_get_attached_to(pFocusWin); + if (!pAttachedTo) + return false; + if (pAttachedTo == m_pWidget || gtk_widget_is_ancestor(pAttachedTo, m_pWidget)) + return true; +#endif + return false; + } + + 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 + { + return ::get_font(m_pWidget); + } + + virtual void set_grid_left_attach(int nAttach) override + { + GtkWidget* pParent = gtk_widget_get_parent(m_pWidget); +#if GTK_CHECK_VERSION(4, 0, 0) + int row, width, height; + gtk_grid_query_child(GTK_GRID(pParent), m_pWidget, nullptr, &row, &width, &height); + g_object_ref(m_pWidget); + gtk_grid_remove(GTK_GRID(pParent), m_pWidget); + gtk_grid_attach(GTK_GRID(pParent), m_pWidget, nAttach, row, width, height); + g_object_unref(m_pWidget); +#else + gtk_container_child_set(GTK_CONTAINER(pParent), m_pWidget, "left-attach", nAttach, nullptr); +#endif + } + + virtual int get_grid_left_attach() const override + { + gint nAttach(0); + GtkWidget* pParent = gtk_widget_get_parent(m_pWidget); +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_grid_query_child(GTK_GRID(pParent), m_pWidget, &nAttach, nullptr, nullptr, nullptr); +#else + gtk_container_child_get(GTK_CONTAINER(pParent), m_pWidget, "left-attach", &nAttach, nullptr); +#endif + return nAttach; + } + + virtual void set_grid_width(int nCols) override + { + GtkWidget* pParent = gtk_widget_get_parent(m_pWidget); +#if GTK_CHECK_VERSION(4, 0, 0) + int col, row, height; + gtk_grid_query_child(GTK_GRID(pParent), m_pWidget, &col, &row, nullptr, &height); + g_object_ref(m_pWidget); + gtk_grid_remove(GTK_GRID(pParent), m_pWidget); + gtk_grid_attach(GTK_GRID(pParent), m_pWidget, col, row, nCols, height); + g_object_unref(m_pWidget); +#else + gtk_container_child_set(GTK_CONTAINER(pParent), m_pWidget, "width", nCols, nullptr); +#endif + } + + virtual void set_grid_top_attach(int nAttach) override + { + GtkWidget* pParent = gtk_widget_get_parent(m_pWidget); +#if GTK_CHECK_VERSION(4, 0, 0) + int col, width, height; + gtk_grid_query_child(GTK_GRID(pParent), m_pWidget, &col, nullptr, &width, &height); + g_object_ref(m_pWidget); + gtk_grid_remove(GTK_GRID(pParent), m_pWidget); + gtk_grid_attach(GTK_GRID(pParent), m_pWidget, col, nAttach, width, height); + g_object_unref(m_pWidget); +#else + gtk_container_child_set(GTK_CONTAINER(pParent), m_pWidget, "top-attach", nAttach, nullptr); +#endif + } + + virtual int get_grid_top_attach() const override + { + gint nAttach(0); + GtkWidget* pParent = gtk_widget_get_parent(m_pWidget); +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_grid_query_child(GTK_GRID(pParent), m_pWidget, nullptr, &nAttach, nullptr, nullptr); +#else + gtk_container_child_get(GTK_CONTAINER(pParent), m_pWidget, "top-attach", &nAttach, nullptr); +#endif + 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_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_start(int nMargin) override + { + gtk_widget_set_margin_start(m_pWidget, nMargin); + } + + virtual void set_margin_end(int nMargin) override + { + gtk_widget_set_margin_end(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_start() const override + { + return gtk_widget_get_margin_start(m_pWidget); + } + + virtual int get_margin_end() const override + { + return gtk_widget_get_margin_end(m_pWidget); + } + + virtual void set_accessible_name(const OUString& rName) override + { +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_accessible_update_property(GTK_ACCESSIBLE(m_pWidget), GTK_ACCESSIBLE_PROPERTY_LABEL, + OUStringToOString(rName, RTL_TEXTENCODING_UTF8).getStr(), -1); +#else + AtkObject* pAtkObject = gtk_widget_get_accessible(m_pWidget); + if (!pAtkObject) + return; + atk_object_set_name(pAtkObject, OUStringToOString(rName, RTL_TEXTENCODING_UTF8).getStr()); +#endif + } + + virtual void set_accessible_description(const OUString& rDescription) override + { +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_accessible_update_property(GTK_ACCESSIBLE(m_pWidget), GTK_ACCESSIBLE_PROPERTY_DESCRIPTION, + OUStringToOString(rDescription, RTL_TEXTENCODING_UTF8).getStr(), -1); +#else + AtkObject* pAtkObject = gtk_widget_get_accessible(m_pWidget); + if (!pAtkObject) + return; + atk_object_set_description(pAtkObject, OUStringToOString(rDescription, RTL_TEXTENCODING_UTF8).getStr()); +#endif + } + + virtual OUString get_accessible_name() const override + { +#if !GTK_CHECK_VERSION(4, 0, 0) + 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); +#else + char* pStr = gtk_test_accessible_check_property(GTK_ACCESSIBLE(m_pWidget), GTK_ACCESSIBLE_PROPERTY_LABEL, nullptr); + OUString sRet(pStr, pStr ? strlen(pStr) : 0, RTL_TEXTENCODING_UTF8); + g_free(pStr); + return sRet; +#endif + } + + virtual OUString get_accessible_description() const override + { +#if !GTK_CHECK_VERSION(4, 0, 0) + 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); +#else + char* pStr = gtk_test_accessible_check_property(GTK_ACCESSIBLE(m_pWidget), GTK_ACCESSIBLE_PROPERTY_DESCRIPTION, nullptr); + OUString sRet(pStr, pStr ? strlen(pStr) : 0, RTL_TEXTENCODING_UTF8); + g_free(pStr); + return sRet; +#endif + } + + virtual void set_accessible_relation_labeled_by(weld::Widget* pLabel) override + { + GtkWidget* pGtkLabel = pLabel ? dynamic_cast<GtkInstanceWidget&>(*pLabel).getWidget() : nullptr; +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_accessible_update_relation(GTK_ACCESSIBLE(m_pWidget), + GTK_ACCESSIBLE_RELATION_LABELLED_BY, + pGtkLabel, nullptr, + -1); +#else + AtkObject* pAtkObject = gtk_widget_get_accessible(m_pWidget); + if (!pAtkObject) + return; + AtkObject *pAtkLabel = pGtkLabel ? gtk_widget_get_accessible(pGtkLabel) : 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) + { + // clear ATK_RELATION_LABEL_FOR from old label + GPtrArray* pOldLabelTarget = atk_relation_get_target(pRelation); + guint nElements = pOldLabelTarget ? pOldLabelTarget->len : 0; + for (guint i = 0; i < nElements; ++i) + { + gpointer pOldLabelObject = g_ptr_array_index(pOldLabelTarget, i); + AtkRelationSet *pOldLabelRelationSet = atk_object_ref_relation_set(ATK_OBJECT(pOldLabelObject)); + if (AtkRelation *pOldLabelRelation = atk_relation_set_get_relation_by_type(pRelationSet, ATK_RELATION_LABEL_FOR)) + atk_relation_set_remove(pOldLabelRelationSet, pOldLabelRelation); + g_object_unref(pOldLabelRelationSet); + } + atk_relation_set_remove(pRelationSet, pRelation); + } + + if (pAtkLabel) + { + AtkObject *obj_array_labelled_by[1]; + obj_array_labelled_by[0] = pAtkLabel; + pRelation = atk_relation_new(obj_array_labelled_by, 1, ATK_RELATION_LABELLED_BY); + atk_relation_set_add(pRelationSet, pRelation); + + // add ATK_RELATION_LABEL_FOR to new label to match + AtkRelationSet *pNewLabelRelationSet = atk_object_ref_relation_set(pAtkLabel); + AtkRelation *pNewLabelRelation = atk_relation_set_get_relation_by_type(pNewLabelRelationSet, ATK_RELATION_LABEL_FOR); + if (pNewLabelRelation) + atk_relation_set_remove(pNewLabelRelationSet, pRelation); + AtkObject *obj_array_label_for[1]; + obj_array_label_for[0] = pAtkObject; + pNewLabelRelation = atk_relation_new(obj_array_label_for, 1, ATK_RELATION_LABEL_FOR); + atk_relation_set_add(pNewLabelRelationSet, pNewLabelRelation); + g_object_unref(pNewLabelRelationSet); + } + + g_object_unref(pRelationSet); +#endif + } + + virtual bool get_extents_relative_to(const weld::Widget& rRelative, int& x, int &y, int& width, int &height) const 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 + gtk_coord fX(0.0), fY(0.0); + bool ret = gtk_widget_translate_coordinates(m_pWidget, + dynamic_cast<const GtkInstanceWidget&>(rRelative).getWidget(), + 0, 0, &fX, &fY); + x = fX; + y = fY; + 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 void set_cursor_data(void * /*pData*/) override {}; + + virtual std::unique_ptr<weld::Container> weld_parent() const override; + + virtual OUString get_buildable_name() const override + { + return ::get_buildable_id(GTK_BUILDABLE(m_pWidget)); + } + + virtual void set_buildable_name(const OUString& rId) override + { + ::set_buildable_id(GTK_BUILDABLE(m_pWidget), rId); + } + + virtual void set_help_id(const OUString& rHelpId) override + { + ::set_help_id(m_pWidget, rHelpId); + } + + virtual OUString get_help_id() const override + { + OUString sRet = ::get_help_id(m_pWidget); + if (sRet.isEmpty()) + sRet = "null"; + return sRet; + } + + GtkWidget* getWidget() const + { + return m_pWidget; + } + + GtkWindow* getWindow() const + { + return GTK_WINDOW(widget_get_toplevel(m_pWidget)); + } + +#if GTK_CHECK_VERSION(4, 0, 0) + GtkEventController* get_focus_controller() + { + if (!m_pFocusController) + { + gtk_widget_set_focusable(m_pWidget, true); + m_pFocusController = gtk_event_controller_focus_new(); + gtk_widget_add_controller(m_pWidget, m_pFocusController); + } + return m_pFocusController; + } + +#if GTK_CHECK_VERSION(4, 0, 0) + GtkEventController* get_click_controller() + { + if (!m_pClickController) + { + GtkGesture *pClick = gtk_gesture_click_new(); + gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(pClick), 0); + m_pClickController = GTK_EVENT_CONTROLLER(pClick); + gtk_widget_add_controller(m_pWidget, m_pClickController); + } + return m_pClickController; + } + + GtkEventController* get_motion_controller() + { + if (!m_pMotionController) + { + m_pMotionController = gtk_event_controller_motion_new(); + gtk_widget_add_controller(m_pWidget, m_pMotionController); + } + return m_pMotionController; + } + + GtkEventController* get_drag_controller() + { + if (!m_pDragController) + { + GtkDragSource* pDrag = gtk_drag_source_new(); + m_pDragController = GTK_EVENT_CONTROLLER(pDrag); + gtk_widget_add_controller(m_pWidget, m_pDragController); + } + return m_pDragController; + } + + GtkEventController* get_key_controller() + { + if (!m_pKeyController) + { + m_pKeyController = gtk_event_controller_key_new(); + gtk_widget_add_controller(m_pWidget, m_pKeyController); + } + return m_pKeyController; + } + +#endif + + +#endif + + virtual void connect_focus_in(const Link<Widget&, void>& rLink) override + { + if (!m_nFocusInSignalId) + { +#if GTK_CHECK_VERSION(4, 0, 0) + m_nFocusInSignalId = g_signal_connect(get_focus_controller(), "enter", G_CALLBACK(signalFocusIn), this); +#else + m_nFocusInSignalId = g_signal_connect(m_pWidget, "focus-in-event", G_CALLBACK(signalFocusIn), this); +#endif + } + + 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) + { +#if GTK_CHECK_VERSION(4, 0, 0) + m_nFocusOutSignalId = g_signal_connect(get_focus_controller(), "leave", G_CALLBACK(signalFocusOut), this); +#else + m_nFocusOutSignalId = g_signal_connect(m_pWidget, "focus-out-event", G_CALLBACK(signalFocusOut), this); +#endif + } + 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)); + } + +#if GTK_CHECK_VERSION(4, 0, 0) + bool signal_key_press(guint keyval, guint keycode, GdkModifierType state) + { + if (m_aKeyPressHdl.IsSet()) + { + SolarMutexGuard aGuard; + return m_aKeyPressHdl.Call(CreateKeyEvent(keyval, keycode, state, 0)); + } + return false; + } + + bool signal_key_release(guint keyval, guint keycode, GdkModifierType state) + { + if (m_aKeyReleaseHdl.IsSet()) + { + SolarMutexGuard aGuard; + return m_aKeyReleaseHdl.Call(CreateKeyEvent(keyval, keycode, state, 0)); + } + return false; + } +#else + + virtual bool do_signal_key_press(const GdkEventKey* pEvent) + { + if (m_aKeyPressHdl.IsSet()) + { + SolarMutexGuard aGuard; + return m_aKeyPressHdl.Call(GtkToVcl(*pEvent)); + } + return false; + } + + virtual bool do_signal_key_release(const GdkEventKey* pEvent) + { + if (m_aKeyReleaseHdl.IsSet()) + { + SolarMutexGuard aGuard; + return m_aKeyReleaseHdl.Call(GtkToVcl(*pEvent)); + } + return false; + } + + bool signal_key_press(const GdkEventKey* pEvent) + { + return do_signal_key_press(pEvent); + } + + bool signal_key_release(const GdkEventKey* pEvent) + { + return do_signal_key_release(pEvent); + } +#endif + + virtual void grab_add() override + { +#if GTK_CHECK_VERSION(4, 0, 0) + ++m_nGrabCount; +#else + gtk_grab_add(m_pWidget); +#endif + } + + virtual bool has_grab() const override + { +#if GTK_CHECK_VERSION(4, 0, 0) + return m_nGrabCount != 0; +#else + return gtk_widget_has_grab(m_pWidget); +#endif + } + + virtual void grab_remove() override + { +#if GTK_CHECK_VERSION(4, 0, 0) + --m_nGrabCount; +#else + gtk_grab_remove(m_pWidget); +#endif + } + + 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 + { + ++m_nFreezeCount; +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_freeze_child_notify(m_pWidget); +#endif + g_object_freeze_notify(G_OBJECT(m_pWidget)); + } + + virtual void thaw() override + { + --m_nFreezeCount; + g_object_thaw_notify(G_OBJECT(m_pWidget)); +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_thaw_child_notify(m_pWidget); +#endif + } + + virtual void set_busy_cursor(bool bBusy) override + { + if (bBusy) + ++m_nWaitCount; + else + --m_nWaitCount; + if (m_nWaitCount == 1) + set_cursor(m_pWidget, "progress"); + else if (m_nWaitCount == 0) + set_cursor(m_pWidget, nullptr); + assert (m_nWaitCount >= 0); + } + + virtual void queue_resize() override + { + gtk_widget_queue_resize(m_pWidget); + } + + virtual css::uno::Reference<css::datatransfer::dnd::XDropTarget> get_drop_target() override + { + if (!m_xDropTarget) + { + m_xDropTarget.set(new GtkInstDropTarget); +#if !GTK_CHECK_VERSION(4, 0, 0) + 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); +#endif + } + return m_xDropTarget; + } + + virtual css::uno::Reference<css::datatransfer::clipboard::XClipboard> get_clipboard() const override + { + // the gen backend can have per-frame clipboards which is (presumably) useful for LibreOffice Online + // but normal usage is the shared system clipboard + return GetSystemClipboard(); + } + + virtual void connect_get_property_tree(const Link<tools::JsonWriter&, void>& /*rLink*/) override + { + //not implemented for the gtk variant + } + + virtual void get_property_tree(tools::JsonWriter& /*rJsonWriter*/) override + { + //not implemented for the gtk variant + } + + virtual void call_attention_to() override + { + // Change the class name to restart the animation under + // its other name: https://css-tricks.com/restart-css-animation/ +#if GTK_CHECK_VERSION(4, 0, 0) + if (gtk_widget_has_css_class(m_pWidget, "call_attention_1")) + { + gtk_widget_remove_css_class(m_pWidget, "call_attention_1"); + gtk_widget_add_css_class(m_pWidget, "call_attention_2"); + } + else + { + gtk_widget_remove_css_class(m_pWidget, "call_attention_2"); + gtk_widget_add_css_class(m_pWidget, "call_attention_1"); + } +#else + GtkStyleContext *pWidgetContext = gtk_widget_get_style_context(m_pWidget); + if (gtk_style_context_has_class(pWidgetContext, "call_attention_1")) + { + gtk_style_context_remove_class(pWidgetContext, "call_attention_1"); + gtk_style_context_add_class(pWidgetContext, "call_attention_2"); + } + else + { + gtk_style_context_remove_class(pWidgetContext, "call_attention_2"); + gtk_style_context_add_class(pWidgetContext, "call_attention_1"); + } +#endif + } + + virtual void set_stack_background() override + { + do_set_background(Application::GetSettings().GetStyleSettings().GetWindowColor()); + } + + virtual void set_title_background() override + { + do_set_background(Application::GetSettings().GetStyleSettings().GetShadowColor()); + } + + virtual void set_highlight_background() override + { + do_set_background(Application::GetSettings().GetStyleSettings().GetHighlightColor()); + } + + virtual void set_background(const Color& rColor) override + { + do_set_background(rColor); + } + + virtual void set_toolbar_background() override + { + // no-op + } + + virtual ~GtkInstanceWidget() override + { + if (m_aStyleUpdatedHdl.IsSet()) + ImplGetDefaultWindow()->RemoveEventListener(LINK(this, GtkInstanceWidget, SettingsChangedHdl)); + + 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) + { +#if GTK_CHECK_VERSION(4, 0, 0) + g_signal_handler_disconnect(get_drag_controller(), m_nDragEndSignalId); +#else + g_signal_handler_disconnect(m_pWidget, m_nDragEndSignalId); +#endif + } + if (m_nDragBeginSignalId) + { +#if GTK_CHECK_VERSION(4, 0, 0) + g_signal_handler_disconnect(get_drag_controller(), m_nDragBeginSignalId); +#else + g_signal_handler_disconnect(m_pWidget, m_nDragBeginSignalId); +#endif + } + 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) + { +#if GTK_CHECK_VERSION(4, 0, 0) + g_signal_handler_disconnect(get_key_controller(), m_nKeyPressSignalId); +#else + g_signal_handler_disconnect(m_pWidget, m_nKeyPressSignalId); +#endif + } + if (m_nKeyReleaseSignalId) + { +#if GTK_CHECK_VERSION(4, 0, 0) + g_signal_handler_disconnect(get_key_controller(), m_nKeyReleaseSignalId); +#else + g_signal_handler_disconnect(m_pWidget, m_nKeyReleaseSignalId); +#endif + } + + if (m_nFocusInSignalId) + { +#if GTK_CHECK_VERSION(4, 0, 0) + g_signal_handler_disconnect(get_focus_controller(), m_nFocusInSignalId); +#else + g_signal_handler_disconnect(m_pWidget, m_nFocusInSignalId); +#endif + } + if (m_nMnemonicActivateSignalId) + g_signal_handler_disconnect(m_pWidget, m_nMnemonicActivateSignalId); + if (m_nFocusOutSignalId) + { +#if GTK_CHECK_VERSION(4, 0, 0) + g_signal_handler_disconnect(get_focus_controller(), m_nFocusOutSignalId); +#else + g_signal_handler_disconnect(m_pWidget, m_nFocusOutSignalId); +#endif + } + if (m_nSizeAllocateSignalId) + g_signal_handler_disconnect(m_pWidget, m_nSizeAllocateSignalId); + + do_set_background(COL_AUTO); + + DisconnectMouseEvents(); + + if (m_bTakeOwnership) + { +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_destroy(m_pWidget); +#else + gtk_window_destroy(GTK_WINDOW(m_pWidget)); +#endif + } + else + g_object_unref(m_pWidget); + } + + virtual void disable_notify_events() + { + if (m_nFocusInSignalId) + { +#if GTK_CHECK_VERSION(4, 0, 0) + g_signal_handler_block(get_focus_controller(), m_nFocusInSignalId); +#else + g_signal_handler_block(m_pWidget, m_nFocusInSignalId); +#endif + } + if (m_nMnemonicActivateSignalId) + g_signal_handler_block(m_pWidget, m_nMnemonicActivateSignalId); + if (m_nFocusOutSignalId) + { +#if GTK_CHECK_VERSION(4, 0, 0) + g_signal_handler_block(get_focus_controller(), m_nFocusOutSignalId); +#else + g_signal_handler_block(m_pWidget, m_nFocusOutSignalId); +#endif + } + 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) + { +#if GTK_CHECK_VERSION(4, 0, 0) + g_signal_handler_unblock(get_focus_controller(), m_nFocusOutSignalId); +#else + g_signal_handler_unblock(m_pWidget, m_nFocusOutSignalId); +#endif + } + if (m_nMnemonicActivateSignalId) + g_signal_handler_unblock(m_pWidget, m_nMnemonicActivateSignalId); + + if (m_nFocusInSignalId) + { +#if GTK_CHECK_VERSION(4, 0, 0) + g_signal_handler_unblock(get_focus_controller(), m_nFocusInSignalId); +#else + g_signal_handler_unblock(m_pWidget, m_nFocusInSignalId); +#endif + } + } + + virtual void help_hierarchy_foreach(const std::function<bool(const OUString&)>& 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(OutputDevice& rOutput, const Point& rPos, const Size& rPixelSize) 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 (!bAlreadyRealized) + { +#if !GTK_CHECK_VERSION(4, 0, 0) + /* + tdf#141633 The "sample db" example (Mockup.odb) has multiline + entries used in its "Journal Entry" column. Those are painted by + taking snapshots of a never-really-shown textview widget. + Without this style_updated then the textview is always drawn + using its original default font size and changing the page zoom + has no effect on the size of text in the "Journal Entry" column. + */ + update_style(m_pWidget, nullptr); +#endif + gtk_widget_realize(m_pWidget); + } + if (!bAlreadyVisible) + gtk_widget_show(m_pWidget); + if (!bAlreadyMapped) + gtk_widget_map(m_pWidget); + + assert(gtk_widget_is_drawable(m_pWidget)); // all that should result in this holding + + // turn off animations, otherwise we get a frame of an animation sequence + gboolean bAnimations; + GtkSettings* pSettings = gtk_widget_get_settings(m_pWidget); + g_object_get(pSettings, "gtk-enable-animations", &bAnimations, nullptr); + if (bAnimations) + g_object_set(pSettings, "gtk-enable-animations", false, nullptr); + + Size aSize(rPixelSize); + + GtkAllocation aOrigAllocation; + gtk_widget_get_allocation(m_pWidget, &aOrigAllocation); + + GtkAllocation aNewAllocation {aOrigAllocation.x, + aOrigAllocation.y, + static_cast<int>(aSize.Width()), + static_cast<int>(aSize.Height()) }; +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_size_allocate(m_pWidget, &aNewAllocation); +#else + gtk_widget_size_allocate(m_pWidget, &aNewAllocation, 0); +#endif + +#if !GTK_CHECK_VERSION(4, 0, 0) + if (GTK_IS_CONTAINER(m_pWidget)) + gtk_container_resize_children(GTK_CONTAINER(m_pWidget)); +#endif + + VclPtr<VirtualDevice> xOutput(VclPtr<VirtualDevice>::Create(DeviceFormat::WITHOUT_ALPHA)); + xOutput->SetOutputSizePixel(aSize); + + switch (rOutput.GetOutDevType()) + { + case OUTDEV_WINDOW: + case OUTDEV_VIRDEV: + xOutput->DrawOutDev(Point(), aSize, rPos, aSize, rOutput); + break; + case OUTDEV_PRINTER: + case OUTDEV_PDF: + xOutput->SetBackground(rOutput.GetBackground()); + xOutput->Erase(); + break; + } + + cairo_surface_t* pSurface = get_underlying_cairo_surface(*xOutput); + cairo_t* cr = cairo_create(pSurface); + +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_draw(m_pWidget, cr); +#else + GtkSnapshot* pSnapshot = gtk_snapshot_new(); + GtkWidgetClass* pWidgetClass = GTK_WIDGET_GET_CLASS(m_pWidget); + pWidgetClass->snapshot(m_pWidget, pSnapshot); + GskRenderNode* pNode = gtk_snapshot_free_to_node(pSnapshot); + gsk_render_node_draw(pNode, cr); + gsk_render_node_unref(pNode); +#endif + + cairo_destroy(cr); + +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_set_allocation(m_pWidget, &aOrigAllocation); + gtk_widget_size_allocate(m_pWidget, &aOrigAllocation); +#else + gtk_widget_size_allocate(m_pWidget, &aOrigAllocation, 0); +#endif + + switch (rOutput.GetOutDevType()) + { + case OUTDEV_WINDOW: + case OUTDEV_VIRDEV: + rOutput.DrawOutDev(rPos, aSize, Point(), aSize, *xOutput); + break; + case OUTDEV_PRINTER: + case OUTDEV_PDF: + rOutput.DrawBitmapEx(rPos, xOutput->GetBitmapEx(Point(), aSize)); + break; + } + + if (bAnimations) + g_object_set(pSettings, "gtk-enable-animations", true, nullptr); + + if (!bAlreadyMapped) + gtk_widget_unmap(m_pWidget); + if (!bAlreadyVisible) + gtk_widget_hide(m_pWidget); + if (!bAlreadyRealized) + gtk_widget_unrealize(m_pWidget); + } +}; + +} + +IMPL_LINK(GtkInstanceWidget, SettingsChangedHdl, VclWindowEvent&, rEvent, void) +{ + if (rEvent.GetId() != VclEventId::WindowDataChanged) + return; + + DataChangedEvent* pData = static_cast<DataChangedEvent*>(rEvent.GetData()); + if (pData->GetType() == DataChangedEventType::SETTINGS) + m_aStyleUpdatedHdl.Call(*this); +} + +#if !GTK_CHECK_VERSION(4, 0, 0) +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); +} +#endif + +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()); + } + +#if GTK_CHECK_VERSION(4, 0, 0) + GtkWidget* find_label_widget(GtkWidget* pContainer) + { + GtkWidget* pLabel = nullptr; + for (GtkWidget* pChild = gtk_widget_get_first_child(pContainer); + pChild; pChild = gtk_widget_get_next_sibling(pChild)) + { + if (GTK_IS_LABEL(pChild)) + { + pLabel = pChild; + break; + } + else + { + pLabel = find_label_widget(pChild); + if (pLabel) + break; + } + } + return pLabel; + } + + GtkWidget* find_image_widget(GtkWidget* pContainer) + { + GtkWidget* pImage = nullptr; + for (GtkWidget* pChild = gtk_widget_get_first_child(pContainer); + pChild; pChild = gtk_widget_get_next_sibling(pChild)) + { + if (GTK_IS_IMAGE(pChild)) + { + pImage = pChild; + break; + } + else + { + pImage = find_image_widget(pChild); + if (pImage) + break; + } + } + return pImage; + } +#else + GtkWidget* find_label_widget(GtkContainer* pContainer) + { + GList* pChildren = gtk_container_get_children(pContainer); + + GtkWidget* pChild = nullptr; + for (GList* pCandidate = pChildren; pCandidate; pCandidate = pCandidate->next) + { + if (GTK_IS_LABEL(pCandidate->data)) + { + pChild = GTK_WIDGET(pCandidate->data); + break; + } + else if (GTK_IS_CONTAINER(pCandidate->data)) + { + pChild = find_label_widget(GTK_CONTAINER(pCandidate->data)); + if (pChild) + break; + } + } + g_list_free(pChildren); + + return pChild; + } + + GtkWidget* find_image_widget(GtkContainer* pContainer) + { + GList* pChildren = gtk_container_get_children(pContainer); + + GtkWidget* pChild = nullptr; + for (GList* pCandidate = pChildren; pCandidate; pCandidate = pCandidate->next) + { + if (GTK_IS_IMAGE(pCandidate->data)) + { + pChild = GTK_WIDGET(pCandidate->data); + break; + } + else if (GTK_IS_CONTAINER(pCandidate->data)) + { + pChild = find_image_widget(GTK_CONTAINER(pCandidate->data)); + if (pChild) + break; + } + } + g_list_free(pChildren); + + return pChild; + } +#endif + + GtkLabel* get_label_widget(GtkWidget* pButton) + { +#if !GTK_CHECK_VERSION(4, 0, 0) + GtkWidget* pChild = gtk_bin_get_child(GTK_BIN(pButton)); + + if (GTK_IS_CONTAINER(pChild)) + pChild = find_label_widget(GTK_CONTAINER(pChild)); + else if (!GTK_IS_LABEL(pChild)) + pChild = nullptr; + + return GTK_LABEL(pChild); +#else + return GTK_LABEL(find_label_widget(pButton)); +#endif + } + + GtkImage* get_image_widget(GtkWidget *pButton) + { +#if !GTK_CHECK_VERSION(4, 0, 0) + GtkWidget* pChild = gtk_bin_get_child(GTK_BIN(pButton)); + + if (GTK_IS_CONTAINER(pChild)) + pChild = find_image_widget(GTK_CONTAINER(pChild)); + else if (!GTK_IS_IMAGE(pChild)) + pChild = nullptr; + + return GTK_IMAGE(pChild); +#else + return GTK_IMAGE(find_image_widget(pButton)); +#endif + } + + OUString button_get_label(GtkButton* pButton) + { + if (GtkLabel* pLabel = get_label_widget(GTK_WIDGET(pButton))) + return ::get_label(pLabel); + const gchar* pStr = gtk_button_get_label(pButton); + return OUString(pStr, pStr ? strlen(pStr) : 0, RTL_TEXTENCODING_UTF8); + } + + void button_set_label(GtkButton* pButton, const OUString& rText) + { + if (GtkLabel* pLabel = get_label_widget(GTK_WIDGET(pButton))) + { + ::set_label(pLabel, rText); + gtk_widget_set_visible(GTK_WIDGET(pLabel), true); + return; + } + gtk_button_set_label(pButton, MapToGtkAccelerator(rText).getStr()); + } + +#if GTK_CHECK_VERSION(4, 0, 0) + OUString get_label(GtkCheckButton* pButton) + { + const gchar* pStr = gtk_check_button_get_label(pButton); + return OUString(pStr, pStr ? strlen(pStr) : 0, RTL_TEXTENCODING_UTF8); + } + + void set_label(GtkCheckButton* pButton, const OUString& rText) + { + gtk_check_button_set_label(pButton, MapToGtkAccelerator(rText).getStr()); + } +#endif + + 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, std::u16string_view 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, std::u16string_view rText) + { + g_object_set(G_OBJECT(pMessageDialog), "text", + OUStringToOString(rText, RTL_TEXTENCODING_UTF8).getStr(), + nullptr); + } + + void set_secondary_text(GtkMessageDialog* pMessageDialog, std::u16string_view 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; + } + + std::shared_ptr<SvMemoryStream> get_icon_stream_by_name_theme_lang(const OUString& rIconName, const OUString& rIconTheme, const OUString& rUILang) + { + return ImageTree::get().getImageStream(rIconName, rIconTheme, rUILang); + } + + GdkPixbuf* load_icon_by_name_theme_lang(const OUString& rIconName, const OUString& rIconTheme, const OUString& rUILang) + { + auto xMemStm = get_icon_stream_by_name_theme_lang(rIconName, rIconTheme, rUILang); + if (!xMemStm) + return nullptr; + return load_icon_from_stream(*xMemStm); + } + + std::unique_ptr<utl::TempFileNamed> get_icon_stream_as_file_by_name_theme_lang(const OUString& rIconName, const OUString& rIconTheme, const OUString& rUILang) + { + uno::Reference<io::XInputStream> xInputStream = ImageTree::get().getImageXInputStream(rIconName, rIconTheme, rUILang); + if (!xInputStream) + return nullptr; + + std::unique_ptr<utl::TempFileNamed> xRet(new utl::TempFileNamed); + xRet->EnableKillingFile(true); + SvStream* pStream = xRet->GetStream(StreamMode::WRITE); + + for (;;) + { + const sal_Int32 nSize(2048); + uno::Sequence<sal_Int8> aData(nSize); + sal_Int32 nRead = xInputStream->readBytes(aData, nSize); + pStream->WriteBytes(aData.getConstArray(), nRead); + if (nRead < nSize) + break; + } + xRet->CloseStream(); + + return xRet; + } + + std::unique_ptr<utl::TempFileNamed> get_icon_stream_as_file(const OUString& rIconName) + { + OUString sIconTheme = Application::GetSettings().GetStyleSettings().DetermineIconTheme(); + OUString sUILang = Application::GetSettings().GetUILanguageTag().getBcp47(); + return get_icon_stream_as_file_by_name_theme_lang(rIconName, sIconTheme, sUILang); + } +} + +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 +{ + Image mirrorImage(const Image& rImage) + { + BitmapEx aMirrBitmapEx(rImage.GetBitmapEx()); + aMirrBitmapEx.Mirror(BmpMirrorFlags::Horizontal); + return Image(aMirrBitmapEx); + } + + GdkPixbuf* getPixbuf(const css::uno::Reference<css::graphic::XGraphic>& rImage, bool bMirror = false) + { + Image aImage(rImage); + if (bMirror) + aImage = mirrorImage(aImage); + + OUString sStock(aImage.GetStock()); + if (!sStock.isEmpty()) + return load_icon_by_name(sStock); + + SvMemoryStream aMemStm; + + // We "know" that this gets passed to zlib's deflateInit2_(). 1 means best speed. + css::uno::Sequence<css::beans::PropertyValue> aFilterData{ comphelper::makePropertyValue( + "Compression", sal_Int32(1)) }; + auto aBitmapEx = aImage.GetBitmapEx(); + vcl::PngImageWriter aWriter(aMemStm); + aWriter.setParameters(aFilterData); + aWriter.write(aBitmapEx); + + return load_icon_from_stream(aMemStm); + } + + // tdf#151898 as far as I can see only gtk_image_new_from_file (or gtk_image_new_from_resource) can support the use of a + // scalable input format to create a hidpi GtkImage, rather than an upscaled lodpi one so forced to go via a file here + std::unique_ptr<utl::TempFileNamed> getImageFile(const css::uno::Reference<css::graphic::XGraphic>& rImage, bool bMirror = false) + { + Image aImage(rImage); + if (bMirror) + aImage = mirrorImage(aImage); + + OUString sStock(aImage.GetStock()); + if (!sStock.isEmpty()) + return get_icon_stream_as_file(sStock); + + std::unique_ptr<utl::TempFileNamed> xRet(new utl::TempFileNamed); + xRet->EnableKillingFile(true); + SvStream* pStream = xRet->GetStream(StreamMode::WRITE); + + // We "know" that this gets passed to zlib's deflateInit2_(). 1 means best speed. + css::uno::Sequence<css::beans::PropertyValue> aFilterData{ comphelper::makePropertyValue( + "Compression", sal_Int32(1)) }; + auto aBitmapEx = aImage.GetBitmapEx(); + vcl::PngImageWriter aWriter(*pStream); + aWriter.setParameters(aFilterData); + aWriter.write(aBitmapEx); + + xRet->CloseStream(); + return xRet; + } + + GdkPixbuf* getPixbuf(const VirtualDevice& rDevice) + { + Size aSize(rDevice.GetOutputSizePixel()); + cairo_surface_t* orig_surface = get_underlying_cairo_surface(rDevice); + double fXScale, fYScale; + dl_cairo_surface_get_device_scale(orig_surface, &fXScale, &fYScale); + + cairo_surface_t* surface; + if (fXScale != 1.0 || 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; + } + +#if GTK_CHECK_VERSION(4, 0, 0) + cairo_surface_t* render_paintable_to_surface(GdkPaintable *paintable, int nWidth, int nHeight) + { + cairo_surface_t* surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, nWidth, nHeight); + + GtkSnapshot* snapshot = gtk_snapshot_new(); + gdk_paintable_snapshot(paintable, snapshot, nWidth, nHeight); + GskRenderNode* node = gtk_snapshot_free_to_node(snapshot); + + cairo_t* cr = cairo_create(surface); + gsk_render_node_draw(node, cr); + cairo_destroy(cr); + + gsk_render_node_unref(node); + + return surface; + } +#endif + + 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"); + +#if GTK_CHECK_VERSION(4, 0, 0) + GtkIconTheme *icon_theme = gtk_icon_theme_get_for_display(gdk_display_get_default()); + GtkIconPaintable *icon = gtk_icon_theme_lookup_icon(icon_theme, + OUStringToOString(rIconName, RTL_TEXTENCODING_UTF8).getStr(), + nullptr, + 16, + 1, + AllSettings::GetLayoutRTL() ? GTK_TEXT_DIR_RTL : GTK_TEXT_DIR_LTR, + static_cast<GtkIconLookupFlags>(0)); + GdkPaintable* paintable = GDK_PAINTABLE(icon); + int nWidth = gdk_paintable_get_intrinsic_width(paintable); + int nHeight = gdk_paintable_get_intrinsic_height(paintable); + cairo_surface_t* surface = render_paintable_to_surface(paintable, nWidth, nHeight); + pixbuf = gdk_pixbuf_get_from_surface(surface, 0, 0, nWidth, nHeight); + cairo_surface_destroy(surface); +#else + GtkIconTheme *icon_theme = gtk_icon_theme_get_default(); + GError *error = nullptr; + pixbuf = gtk_icon_theme_load_icon(icon_theme, OUStringToOString(rIconName, RTL_TEXTENCODING_UTF8).getStr(), + 16, GTK_ICON_LOOKUP_USE_BUILTIN, &error); +#endif + } + else + { + const AllSettings& rSettings = Application::GetSettings(); + pixbuf = load_icon_by_name_theme_lang(rIconName, + rSettings.GetStyleSettings().DetermineIconTheme(), + rSettings.GetUILanguageTag().getBcp47()); + } + return pixbuf; + } +} + +namespace +{ +#if GTK_CHECK_VERSION(4, 0, 0) + SurfacePaintable* paintable_new_from_virtual_device(const VirtualDevice& rImageSurface) + { + 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); + + SurfacePaintable* pPaintable = SURFACE_PAINTABLE(g_object_new(surface_paintable_get_type(), nullptr)); + surface_paintable_set_source(pPaintable, target, aSize.Width(), aSize.Height()); + return pPaintable; + } + + GtkWidget* image_new_from_virtual_device(const VirtualDevice& rImageSurface) + { + SurfacePaintable* paintable = paintable_new_from_virtual_device(rImageSurface); + return gtk_image_new_from_paintable(GDK_PAINTABLE(paintable)); + } + + GtkWidget* picture_new_from_virtual_device(const VirtualDevice& rImageSurface) + { + SurfacePaintable* paintable = paintable_new_from_virtual_device(rImageSurface); + return gtk_picture_new_for_paintable(GDK_PAINTABLE(paintable)); + } + +#else + GtkWidget* image_new_from_virtual_device(const VirtualDevice& rImageSurface) + { + GtkWidget* pImage = 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); + return pImage; + } +#endif + + GtkWidget* image_new_from_xgraphic(const css::uno::Reference<css::graphic::XGraphic>& rIcon, bool bMirror) + { + GtkWidget* pImage = nullptr; + if (auto xTempFile = getImageFile(rIcon, bMirror)) + pImage = gtk_image_new_from_file(OUStringToOString(xTempFile->GetFileName(), osl_getThreadTextEncoding()).getStr()); + return pImage; + } + + GtkWidget* image_new_from_icon_name(const OUString& rIconName) + { + GtkWidget* pImage = nullptr; + if (auto xTempFile = get_icon_stream_as_file(rIconName)) + pImage = gtk_image_new_from_file(OUStringToOString(xTempFile->GetFileName(), osl_getThreadTextEncoding()).getStr()); + return pImage; + } + + GtkWidget* image_new_from_icon_name_theme_lang(const OUString& rIconName, const OUString& rIconTheme, const OUString& rUILang) + { + GtkWidget* pImage = nullptr; + if (auto xTempFile = get_icon_stream_as_file_by_name_theme_lang(rIconName, rIconTheme, rUILang)) + pImage = gtk_image_new_from_file(OUStringToOString(xTempFile->GetFileName(), osl_getThreadTextEncoding()).getStr()); + return pImage; + } + + void image_set_from_icon_name(GtkImage* pImage, const OUString& rIconName) + { + if (auto xTempFile = get_icon_stream_as_file(rIconName)) + gtk_image_set_from_file(pImage, OUStringToOString(xTempFile->GetFileName(), osl_getThreadTextEncoding()).getStr()); + else + gtk_image_set_from_pixbuf(pImage, nullptr); + } + + void image_set_from_icon_name_theme_lang(GtkImage* pImage, const OUString& rIconName, const OUString& rIconTheme, const OUString& rUILang) + { + if (auto xTempFile = get_icon_stream_as_file_by_name_theme_lang(rIconName, rIconTheme, rUILang)) + gtk_image_set_from_file(pImage, OUStringToOString(xTempFile->GetFileName(), osl_getThreadTextEncoding()).getStr()); + else + gtk_image_set_from_pixbuf(pImage, nullptr); + } + + void image_set_from_virtual_device(GtkImage* pImage, const VirtualDevice* pDevice) + { +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_image_set_from_paintable(pImage, pDevice ? GDK_PAINTABLE(paintable_new_from_virtual_device(*pDevice)) : nullptr); +#else + gtk_image_set_from_surface(pImage, pDevice ? get_underlying_cairo_surface(*pDevice) : nullptr); +#endif + } + + void image_set_from_xgraphic(GtkImage* pImage, const css::uno::Reference<css::graphic::XGraphic>& rImage) + { + if (auto xTempFile = getImageFile(rImage, false)) + gtk_image_set_from_file(pImage, OUStringToOString(xTempFile->GetFileName(), osl_getThreadTextEncoding()).getStr()); + else + gtk_image_set_from_pixbuf(pImage, nullptr); + } + +#if GTK_CHECK_VERSION(4, 0, 0) + void picture_set_from_icon_name(GtkPicture* pPicture, const OUString& rIconName) + { + if (auto xTempFile = get_icon_stream_as_file(rIconName)) + gtk_picture_set_filename(pPicture, OUStringToOString(xTempFile->GetFileName(), osl_getThreadTextEncoding()).getStr()); + else + gtk_picture_set_pixbuf(pPicture, nullptr); + } + + void picture_set_from_icon_name_theme_lang(GtkPicture* pPicture, const OUString& rIconName, const OUString& rIconTheme, const OUString& rUILang) + { + if (auto xTempFile = get_icon_stream_as_file_by_name_theme_lang(rIconName, rIconTheme, rUILang)) + gtk_picture_set_filename(pPicture, OUStringToOString(xTempFile->GetFileName(), osl_getThreadTextEncoding()).getStr()); + else + gtk_picture_set_pixbuf(pPicture, nullptr); + } + + void picture_set_from_virtual_device(GtkPicture* pPicture, const VirtualDevice* pDevice) + { + if (!pDevice) + gtk_picture_set_paintable(pPicture, nullptr); + else + gtk_picture_set_paintable(pPicture, GDK_PAINTABLE(paintable_new_from_virtual_device(*pDevice))); + } + + void picture_set_from_xgraphic(GtkPicture* pPicture, const css::uno::Reference<css::graphic::XGraphic>& rPicture) + { + if (auto xTempFile = getImageFile(rPicture, false)) + gtk_picture_set_filename(pPicture, OUStringToOString(xTempFile->GetFileName(), osl_getThreadTextEncoding()).getStr()); + else + gtk_picture_set_pixbuf(pPicture, nullptr); + } +#endif + + void button_set_from_icon_name(GtkButton* pButton, const OUString& rIconName) + { + if (GtkImage* pImage = get_image_widget(GTK_WIDGET(pButton))) + { + ::image_set_from_icon_name(pImage, rIconName); + gtk_widget_set_visible(GTK_WIDGET(pImage), true); + return; + } + + GtkWidget* pImage = image_new_from_icon_name(rIconName); +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_button_set_child(pButton, pImage); +#else + gtk_button_set_image(pButton, pImage); +#endif + } + + void button_set_image(GtkButton* pButton, const VirtualDevice* pDevice) + { +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_button_set_always_show_image(pButton, true); + gtk_button_set_image_position(pButton, GTK_POS_LEFT); +#endif + GtkWidget* pImage = pDevice ? image_new_from_virtual_device(*pDevice) : nullptr; +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_button_set_child(pButton, pImage); +#else + gtk_button_set_image(pButton, pImage); +#endif + } + + void button_set_image(GtkButton* pButton, const css::uno::Reference<css::graphic::XGraphic>& rImage) + { + if (GtkImage* pImage = get_image_widget(GTK_WIDGET(pButton))) + { + ::image_set_from_xgraphic(pImage, rImage); + gtk_widget_set_visible(GTK_WIDGET(pImage), true); + return; + } + + GtkWidget* pImage = image_new_from_xgraphic(rImage, false); +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_button_set_child(pButton, pImage); +#else + gtk_button_set_image(pButton, pImage); +#endif + } + + +class MenuHelper +{ +protected: +#if !GTK_CHECK_VERSION(4, 0, 0) + GtkMenu* m_pMenu; + + std::map<OUString, GtkMenuItem*> m_aMap; +#else + GtkPopoverMenu* m_pMenu; + + o3tl::sorted_vector<OString> m_aInsertedActions; // must outlive m_aActionEntries + std::map<OUString, OString> m_aIdToAction; + std::set<OUString> m_aHiddenIds; + std::vector<GActionEntry> m_aActionEntries; + GActionGroup* m_pActionGroup; + // move 'invisible' entries to m_pHiddenActionGroup + GActionGroup* m_pHiddenActionGroup; +#endif + bool m_bTakeOwnership; +private: + + virtual void signal_item_activate(const OUString& rIdent) = 0; + +#if !GTK_CHECK_VERSION(4, 0, 0) + 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_item_activate(::get_buildable_id(GTK_BUILDABLE(pItem))); + } +#else + static std::pair<GMenuModel*, int> find_id(GMenuModel* pMenuModel, const OUString& rId) + { + for (int i = 0, nCount = g_menu_model_get_n_items(pMenuModel); i < nCount; ++i) + { + OUString sTarget; + char *id; + if (g_menu_model_get_item_attribute(pMenuModel, i, "target", "s", &id)) + { + sTarget = OStringToOUString(id, RTL_TEXTENCODING_UTF8); + g_free(id); + } + + if (sTarget == rId) + return std::make_pair(pMenuModel, i); + + if (GMenuModel* pSectionModel = g_menu_model_get_item_link(pMenuModel, i, G_MENU_LINK_SECTION)) + { + std::pair<GMenuModel*, int> aRet = find_id(pSectionModel, rId); + if (aRet.first) + return aRet; + } + if (GMenuModel* pSubMenuModel = g_menu_model_get_item_link(pMenuModel, i, G_MENU_LINK_SUBMENU)) + { + std::pair<GMenuModel*, int> aRet = find_id(pSubMenuModel, rId); + if (aRet.first) + return aRet; + } + } + + return std::make_pair(nullptr, -1); + } + + void clear_actions() + { + for (const auto& rAction : m_aActionEntries) + { + g_action_map_remove_action(G_ACTION_MAP(m_pActionGroup), rAction.name); + g_action_map_remove_action(G_ACTION_MAP(m_pHiddenActionGroup), rAction.name); + } + m_aActionEntries.clear(); + m_aInsertedActions.clear(); + m_aIdToAction.clear(); + } + + static void action_activated(GSimpleAction*, GVariant* pParameter, gpointer widget) + { + gsize nLength(0); + const gchar* pStr = g_variant_get_string(pParameter, &nLength); + OUString aStr(pStr, nLength, RTL_TEXTENCODING_UTF8); + MenuHelper* pThis = static_cast<MenuHelper*>(widget); + SolarMutexGuard aGuard; + pThis->signal_item_activate(aStr); + } +#endif + +public: +#if !GTK_CHECK_VERSION(4, 0, 0) + MenuHelper(GtkMenu* pMenu, bool bTakeOwnership) +#else + MenuHelper(GtkPopoverMenu* pMenu, bool bTakeOwnership) +#endif + : m_pMenu(pMenu) + , m_bTakeOwnership(bTakeOwnership) + { +#if !GTK_CHECK_VERSION(4, 0, 0) + if (!m_pMenu) + return; + gtk_container_foreach(GTK_CONTAINER(m_pMenu), collect, this); +#else + m_pActionGroup = G_ACTION_GROUP(g_simple_action_group_new()); + m_pHiddenActionGroup = G_ACTION_GROUP(g_simple_action_group_new()); +#endif + } + +#if !GTK_CHECK_VERSION(4, 0, 0) + void add_to_map(GtkMenuItem* pMenuItem) + { + OUString id = ::get_buildable_id(GTK_BUILDABLE(pMenuItem)); + m_aMap[id] = pMenuItem; + g_signal_connect(pMenuItem, "activate", G_CALLBACK(signalActivate), this); + } + + void remove_from_map(GtkMenuItem* pMenuItem) + { + OUString id = ::get_buildable_id(GTK_BUILDABLE(pMenuItem)); + 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); + } +#endif + +#if GTK_CHECK_VERSION(4, 0, 0) + /* LibreOffice likes to think of separators between menu entries, while gtk likes + to think of sections of menus with separators drawn between sections. We always + arrange to have a section in a menu so toplevel menumodels comprise of + sections and we move entries between sections on pretending to insert separators */ + static std::pair<GMenuModel*, int> get_section_and_pos_for(GMenuModel* pMenuModel, int pos) + { + int nSectionCount = g_menu_model_get_n_items(pMenuModel); + assert(nSectionCount); + + GMenuModel* pSectionModel = nullptr; + int nIndexWithinSection = 0; + + int nExternalPos = 0; + for (int nSection = 0; nSection < nSectionCount; ++nSection) + { + pSectionModel = g_menu_model_get_item_link(pMenuModel, nSection, G_MENU_LINK_SECTION); + assert(pSectionModel); + int nCount = g_menu_model_get_n_items(pSectionModel); + for (nIndexWithinSection = 0; nIndexWithinSection < nCount; ++nIndexWithinSection) + { + if (pos == nExternalPos) + break; + ++nExternalPos; + } + ++nExternalPos; + } + + return std::make_pair(pSectionModel, nIndexWithinSection); + } + + static int count_immediate_children(GMenuModel* pMenuModel) + { + int nSectionCount = g_menu_model_get_n_items(pMenuModel); + assert(nSectionCount); + + int nExternalPos = 0; + for (int nSection = 0; nSection < nSectionCount; ++nSection) + { + GMenuModel* pSectionModel = g_menu_model_get_item_link(pMenuModel, nSection, G_MENU_LINK_SECTION); + assert(pSectionModel); + int nCount = g_menu_model_get_n_items(pSectionModel); + for (int nIndexWithinSection = 0; nIndexWithinSection < nCount; ++nIndexWithinSection) + { + ++nExternalPos; + } + ++nExternalPos; + } + + return nExternalPos - 1; + } +#endif + +#if GTK_CHECK_VERSION(4, 0, 0) + void process_menu_model(GMenuModel* pMenuModel) + { + for (int i = 0, nCount = g_menu_model_get_n_items(pMenuModel); i < nCount; ++i) + { + OString sAction; + OUString sTarget; + char *id; + if (g_menu_model_get_item_attribute(pMenuModel, i, "action", "s", &id)) + { + assert(o3tl::starts_with(id, "menu.")); + + sAction = OString(id + 5); + + auto res = m_aInsertedActions.insert(sAction); + if (res.second) + { + // the const char* arg isn't copied by anything so it must continue to exist for the life time of + // the action group + if (sAction.startsWith("radio.")) + m_aActionEntries.push_back({res.first->getStr(), action_activated, "s", "'none'", nullptr, {}}); + else + m_aActionEntries.push_back({res.first->getStr(), action_activated, "s", nullptr, nullptr, {}}); + } + + g_free(id); + } + + if (g_menu_model_get_item_attribute(pMenuModel, i, "target", "s", &id)) + { + sTarget = OStringToOUString(id, RTL_TEXTENCODING_UTF8); + g_free(id); + } + + m_aIdToAction[sTarget] = sAction; + + if (GMenuModel* pSectionModel = g_menu_model_get_item_link(pMenuModel, i, G_MENU_LINK_SECTION)) + process_menu_model(pSectionModel); + if (GMenuModel* pSubMenuModel = g_menu_model_get_item_link(pMenuModel, i, G_MENU_LINK_SUBMENU)) + process_menu_model(pSubMenuModel); + } + } + + // build an action group for the menu, "action" is the normal menu entry case + // the others are radiogroups + void update_action_group_from_popover_model() + { + clear_actions(); + + if (GMenuModel* pMenuModel = m_pMenu ? gtk_popover_menu_get_menu_model(m_pMenu) : nullptr) + { + process_menu_model(pMenuModel); + } + + // move hidden entries to m_pHiddenActionGroup + g_action_map_add_action_entries(G_ACTION_MAP(m_pActionGroup), m_aActionEntries.data(), m_aActionEntries.size(), this); + for (const auto& id : m_aHiddenIds) + { + GAction* pAction = g_action_map_lookup_action(G_ACTION_MAP(m_pActionGroup), m_aIdToAction[id].getStr()); + g_action_map_add_action(G_ACTION_MAP(m_pHiddenActionGroup), pAction); + g_action_map_remove_action(G_ACTION_MAP(m_pActionGroup), m_aIdToAction[id].getStr()); + } + } +#endif + + void insert_item(int pos, const OUString& rId, const OUString& rStr, + const OUString* pIconName, const VirtualDevice* pImageSurface, + TriState eCheckRadioFalse) + { +#if !GTK_CHECK_VERSION(4, 0, 0) + GtkWidget* pImage = nullptr; + if (pIconName && !pIconName->isEmpty()) + pImage = image_new_from_icon_name(*pIconName); + else if (pImageSurface) + pImage = image_new_from_virtual_device(*pImageSurface); + + GtkWidget *pItem; + if (pImage) + { + GtkBox *pBox = GTK_BOX(gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6)); + GtkWidget *pLabel = gtk_label_new_with_mnemonic(MapToGtkAccelerator(rStr).getStr()); + pItem = eCheckRadioFalse != TRISTATE_INDET ? gtk_check_menu_item_new() : gtk_menu_item_new(); + gtk_box_pack_start(pBox, pImage, true, true, 0); + gtk_box_pack_start(pBox, pLabel, true, true, 0); + gtk_container_add(GTK_CONTAINER(pItem), GTK_WIDGET(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); + + ::set_buildable_id(GTK_BUILDABLE(pItem), rId); + 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); +#else + (void)pIconName; (void)pImageSurface; + + if (GMenuModel* pMenuModel = m_pMenu ? gtk_popover_menu_get_menu_model(m_pMenu) : nullptr) + { + auto aSectionAndPos = get_section_and_pos_for(pMenuModel, pos); + GMenu* pMenu = G_MENU(aSectionAndPos.first); + // action with a target value ... the action name and target value are separated by a double + // colon ... For example: "app.action::target" + OUString sActionAndTarget; + if (eCheckRadioFalse == TRISTATE_INDET) + sActionAndTarget = "menu.normal." + rId + "::" + rId; + else + sActionAndTarget = "menu.radio." + rId + "::" + rId; + g_menu_insert(pMenu, aSectionAndPos.second, MapToGtkAccelerator(rStr).getStr(), sActionAndTarget.toUtf8().getStr()); + + assert(eCheckRadioFalse == TRISTATE_INDET); // come back to this later + + // TODO not redo entire group + update_action_group_from_popover_model(); + } +#endif + } + + void insert_separator(int pos, const OUString& rId) + { +#if !GTK_CHECK_VERSION(4, 0, 0) + GtkWidget* pItem = gtk_separator_menu_item_new(); + ::set_buildable_id(GTK_BUILDABLE(pItem), rId); + 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); +#else + if (GMenuModel* pMenuModel = m_pMenu ? gtk_popover_menu_get_menu_model(m_pMenu) : nullptr) + { + auto aSectionAndPos = get_section_and_pos_for(pMenuModel, pos); + + for (int nSection = 0, nSectionCount = g_menu_model_get_n_items(pMenuModel); nSection < nSectionCount; ++nSection) + { + GMenuModel* pSectionModel = g_menu_model_get_item_link(pMenuModel, nSection, G_MENU_LINK_SECTION); + assert(pSectionModel); + if (aSectionAndPos.first == pSectionModel) + { + GMenu* pNewSection = g_menu_new(); + GMenuItem* pSectionItem = g_menu_item_new_section(nullptr, G_MENU_MODEL(pNewSection)); + OUString sActionAndTarget = "menu.separator." + rId + "::" + rId; + g_menu_item_set_detailed_action(pSectionItem, sActionAndTarget.toUtf8().getStr()); + g_menu_insert_item(G_MENU(pMenuModel), nSection + 1, pSectionItem); + int nOldSectionCount = g_menu_model_get_n_items(pSectionModel); + for (int i = nOldSectionCount - 1; i >= aSectionAndPos.second; --i) + { + GMenuItem* pMenuItem = g_menu_item_new_from_model(pSectionModel, i); + g_menu_prepend_item(pNewSection, pMenuItem); + g_menu_remove(G_MENU(pSectionModel), i); + g_object_unref(pMenuItem); + } + g_object_unref(pSectionItem); + g_object_unref(pNewSection); + } + } + } + +#endif + } + + void remove_item(const OUString& rIdent) + { +#if !GTK_CHECK_VERSION(4, 0, 0) + GtkMenuItem* pMenuItem = m_aMap[rIdent]; + remove_from_map(pMenuItem); + gtk_widget_destroy(GTK_WIDGET(pMenuItem)); +#else + if (GMenuModel* pMenuModel = m_pMenu ? gtk_popover_menu_get_menu_model(m_pMenu) : nullptr) + { + std::pair<GMenuModel*, int> aRes = find_id(pMenuModel, rIdent); + if (!aRes.first) + return; + g_menu_remove(G_MENU(aRes.first), aRes.second); + } +#endif + } + + void set_item_sensitive(const OUString& rIdent, bool bSensitive) + { +#if GTK_CHECK_VERSION(4, 0, 0) + GActionGroup* pActionGroup = m_aHiddenIds.find(rIdent) == m_aHiddenIds.end() ? m_pActionGroup : m_pHiddenActionGroup; + GAction* pAction = g_action_map_lookup_action(G_ACTION_MAP(pActionGroup), m_aIdToAction[rIdent].getStr()); + g_simple_action_set_enabled(G_SIMPLE_ACTION(pAction), bSensitive); +#else + gtk_widget_set_sensitive(GTK_WIDGET(m_aMap[rIdent]), bSensitive); +#endif + } + + bool get_item_sensitive(const OUString& rIdent) const + { +#if GTK_CHECK_VERSION(4, 0, 0) + GActionGroup* pActionGroup = m_aHiddenIds.find(rIdent) == m_aHiddenIds.end() ? m_pActionGroup : m_pHiddenActionGroup; + GAction* pAction = g_action_map_lookup_action(G_ACTION_MAP(pActionGroup), m_aIdToAction.find(rIdent)->second.getStr()); + return g_action_get_enabled(pAction); +#else + return gtk_widget_get_sensitive(GTK_WIDGET(m_aMap.find(rIdent)->second)); +#endif + } + + void set_item_active(const OUString& rIdent, bool bActive) + { +#if !GTK_CHECK_VERSION(4, 0, 0) + disable_item_notify_events(); + gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(m_aMap[rIdent]), bActive); + enable_item_notify_events(); +#else + GActionGroup* pActionGroup = m_aHiddenIds.find(rIdent) == m_aHiddenIds.end() ? m_pActionGroup : m_pHiddenActionGroup; + g_action_group_change_action_state(pActionGroup, m_aIdToAction[rIdent].getStr(), + g_variant_new_string(bActive ? OUStringToOString(rIdent, RTL_TEXTENCODING_UTF8).getStr() : "'none'")); +#endif + } + + bool get_item_active(const OUString& rIdent) const + { +#if !GTK_CHECK_VERSION(4, 0, 0) + return gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(m_aMap.find(rIdent)->second)); +#else + GActionGroup* pActionGroup = m_aHiddenIds.find(rIdent) == m_aHiddenIds.end() ? m_pActionGroup : m_pHiddenActionGroup; + GVariant* pState = g_action_group_get_action_state(pActionGroup, m_aIdToAction.find(rIdent)->second.getStr()); + if (!pState) + return false; + const char *pStateString = g_variant_get_string(pState, nullptr); + bool bInactive = g_strcmp0(pStateString, "'none'") == 0; + g_variant_unref(pState); + return bInactive; +#endif + } + + void set_item_label(const OUString& rIdent, const OUString& rText) + { +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_menu_item_set_label(m_aMap[rIdent], MapToGtkAccelerator(rText).getStr()); +#else + if (GMenuModel* pMenuModel = m_pMenu ? gtk_popover_menu_get_menu_model(m_pMenu) : nullptr) + { + std::pair<GMenuModel*, int> aRes = find_id(pMenuModel, rIdent); + if (!aRes.first) + return; + // clone the original item, remove the original, insert the replacement at + // the original location + GMenuItem* pMenuItem = g_menu_item_new_from_model(aRes.first, aRes.second); + g_menu_remove(G_MENU(aRes.first), aRes.second); + g_menu_item_set_label(pMenuItem, MapToGtkAccelerator(rText).getStr()); + g_menu_insert_item(G_MENU(aRes.first), aRes.second, pMenuItem); + g_object_unref(pMenuItem); + } +#endif + } + + OUString get_item_label(const OUString& rIdent) const + { +#if !GTK_CHECK_VERSION(4, 0, 0) + const gchar* pText = gtk_menu_item_get_label(m_aMap.find(rIdent)->second); + return OUString(pText, pText ? strlen(pText) : 0, RTL_TEXTENCODING_UTF8); +#else + if (GMenuModel* pMenuModel = m_pMenu ? gtk_popover_menu_get_menu_model(m_pMenu) : nullptr) + { + std::pair<GMenuModel*, int> aRes = find_id(pMenuModel, rIdent); + if (!aRes.first) + return OUString(); + + // clone the original item to query its label + GMenuItem* pMenuItem = g_menu_item_new_from_model(aRes.first, aRes.second); + char *pLabel = nullptr; + g_menu_item_get_attribute(pMenuItem, G_MENU_ATTRIBUTE_LABEL, "&s", &pLabel); + OUString aRet(pLabel, pLabel ? strlen(pLabel) : 0, RTL_TEXTENCODING_UTF8); + g_free(pLabel); + g_object_unref(pMenuItem); + return aRet; + } + return OUString(); +#endif + } + + void set_item_visible(const OUString& rIdent, bool bShow) + { +#if !GTK_CHECK_VERSION(4, 0, 0) + GtkWidget* pWidget = GTK_WIDGET(m_aMap[rIdent]); + if (bShow) + gtk_widget_show(pWidget); + else + gtk_widget_hide(pWidget); +#else + bool bOldVisible = m_aHiddenIds.find(rIdent) == m_aHiddenIds.end(); + if (bShow == bOldVisible) + return; + + if (!bShow) + { + GAction* pAction = g_action_map_lookup_action(G_ACTION_MAP(m_pActionGroup), m_aIdToAction[rIdent].getStr()); + g_action_map_add_action(G_ACTION_MAP(m_pHiddenActionGroup), pAction); + g_action_map_remove_action(G_ACTION_MAP(m_pActionGroup), m_aIdToAction[rIdent].getStr()); + m_aHiddenIds.insert(rIdent); + } + else + { + GAction* pAction = g_action_map_lookup_action(G_ACTION_MAP(m_pHiddenActionGroup), m_aIdToAction[rIdent].getStr()); + g_action_map_add_action(G_ACTION_MAP(m_pActionGroup), pAction); + g_action_map_remove_action(G_ACTION_MAP(m_pHiddenActionGroup), m_aIdToAction[rIdent].getStr()); + m_aHiddenIds.erase(rIdent); + } +#endif + } + + OUString get_item_id(int pos) const + { +#if !GTK_CHECK_VERSION(4, 0, 0) + GList* pChildren = gtk_container_get_children(GTK_CONTAINER(m_pMenu)); + gpointer pMenuItem = g_list_nth_data(pChildren, pos); + OUString id = ::get_buildable_id(GTK_BUILDABLE(pMenuItem)); + g_list_free(pChildren); + return id; +#else + OUString sTarget; + if (GMenuModel* pMenuModel = m_pMenu ? gtk_popover_menu_get_menu_model(m_pMenu) : nullptr) + { + auto aSectionAndPos = get_section_and_pos_for(pMenuModel, pos); + char *id; + if (g_menu_model_get_item_attribute(aSectionAndPos.first, aSectionAndPos.second, "target", "s", &id)) + { + sTarget = OStringToOUString(id, RTL_TEXTENCODING_UTF8); + g_free(id); + } + } + return sTarget; +#endif + } + + int get_n_children() const + { +#if !GTK_CHECK_VERSION(4, 0, 0) + GList* pChildren = gtk_container_get_children(GTK_CONTAINER(m_pMenu)); + int nLen = g_list_length(pChildren); + g_list_free(pChildren); + return nLen; +#else + if (GMenuModel* pMenuModel = m_pMenu ? gtk_popover_menu_get_menu_model(m_pMenu) : nullptr) + return count_immediate_children(pMenuModel); + return 0; +#endif + } + + void clear_items() + { +#if !GTK_CHECK_VERSION(4, 0, 0) + 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(); +#else + if (GMenuModel* pMenuModel = m_pMenu ? gtk_popover_menu_get_menu_model(m_pMenu) : nullptr) + { + GMenu* pMenu = G_MENU(pMenuModel); + g_menu_remove_all(pMenu); + g_menu_insert_section(pMenu, 0, nullptr, G_MENU_MODEL(g_menu_new())); + m_aHiddenIds.clear(); + update_action_group_from_popover_model(); + } +#endif + } + +#if !GTK_CHECK_VERSION(4, 0, 0) + GtkMenu* getMenu() const +#else + GtkPopoverMenu* getMenu() const +#endif + { + return m_pMenu; + } + + virtual ~MenuHelper() + { +#if !GTK_CHECK_VERSION(4, 0, 0) + for (auto& a : m_aMap) + g_signal_handlers_disconnect_by_data(a.second, this); + if (m_bTakeOwnership) + gtk_widget_destroy(GTK_WIDGET(m_pMenu)); +#else + g_object_unref(m_pActionGroup); + g_object_unref(m_pHiddenActionGroup); +#endif + } +}; + +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( "ChildFrame maLayoutIdle" ) + { + maLayoutIdle.SetPriority(TaskPriority::RESIZE); + maLayoutIdle.SetInvokeHandler( LINK( this, ChildFrame, ImplHandleLayoutTimerHdl ) ); + } + + 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(); + } + + void Layout() + { + if (vcl::Window *pChild = GetWindow(GetWindowType::FirstChild)) + pChild->SetPosSizePixel(Point(0, 0), GetSizePixel()); + } + + virtual void Resize() override + { + maLayoutIdle.Stop(); + Layout(); + WorkWindow::Resize(); + } +}; + +IMPL_LINK_NOARG(ChildFrame, ImplHandleLayoutTimerHdl, Timer*, void) +{ + Layout(); +} + +class GtkInstanceContainer : public GtkInstanceWidget, public virtual weld::Container +{ +private: +#if !GTK_CHECK_VERSION(4, 0, 0) + GtkContainer* m_pContainer; +#else + GtkWidget* m_pContainer; +#endif + gulong m_nSetFocusChildSignalId; + bool m_bChildHasFocus; + + void signal_set_focus_child(bool bChildHasFocus) + { + if (m_bChildHasFocus != bChildHasFocus) + { + m_bChildHasFocus = bChildHasFocus; + signal_container_focus_changed(); + } + } + +#if !GTK_CHECK_VERSION(4, 0, 0) + static void signalSetFocusChild(GtkContainer*, GtkWidget* pChild, gpointer widget) + { + GtkInstanceContainer* pThis = static_cast<GtkInstanceContainer*>(widget); + pThis->signal_set_focus_child(pChild != nullptr); + } +#endif + +public: +#if !GTK_CHECK_VERSION(4, 0, 0) + GtkInstanceContainer(GtkContainer* pContainer, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : GtkInstanceWidget(GTK_WIDGET(pContainer), pBuilder, bTakeOwnership) +#else + GtkInstanceContainer(GtkWidget* pContainer, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : GtkInstanceWidget(pContainer, pBuilder, bTakeOwnership) +#endif + , m_pContainer(pContainer) + , m_nSetFocusChildSignalId(0) + , m_bChildHasFocus(false) + { + } + + virtual void connect_container_focus_changed(const Link<Container&, void>& rLink) override + { +#if !GTK_CHECK_VERSION(4, 0, 0) + if (!m_nSetFocusChildSignalId) + m_nSetFocusChildSignalId = g_signal_connect(G_OBJECT(m_pContainer), "set-focus-child", G_CALLBACK(signalSetFocusChild), this); +#endif + weld::Container::connect_container_focus_changed(rLink); + } + +#if GTK_CHECK_VERSION(4, 0, 0) + GtkWidget* getContainer() { return m_pContainer; } +#else + GtkContainer* getContainer() { return m_pContainer; } +#endif + + virtual void child_grab_focus() override + { + gtk_widget_grab_focus(m_pWidget); +#if GTK_CHECK_VERSION(4, 0, 0) + bool bHasFocusChild = gtk_widget_get_focus_child(GTK_WIDGET(m_pContainer)); +#else + bool bHasFocusChild = gtk_container_get_focus_child(m_pContainer); +#endif + if (!bHasFocusChild) + { +#if GTK_CHECK_VERSION(4, 0, 0) + if (GtkWidget* pChild = gtk_widget_get_first_child(m_pContainer)) + { + gtk_widget_set_focus_child(m_pContainer, pChild); + bHasFocusChild = true; + } +#else + GList* pChildren = gtk_container_get_children(m_pContainer); + if (GList* pChild = g_list_first(pChildren)) + { + gtk_container_set_focus_child(m_pContainer, static_cast<GtkWidget*>(pChild->data)); + bHasFocusChild = true; + } + g_list_free(pChildren); +#endif + } + + if (bHasFocusChild) + { +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_child_focus(gtk_widget_get_focus_child(m_pWidget), GTK_DIR_TAB_FORWARD); +#else + gtk_widget_child_focus(gtk_container_get_focus_child(GTK_CONTAINER(m_pWidget)), GTK_DIR_TAB_FORWARD); +#endif + } + + } + + 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); + auto pOldContainer = getContainer(); + container_remove(GTK_WIDGET(pOldContainer), pChild); + + GtkInstanceContainer* pNewGtkParent = dynamic_cast<GtkInstanceContainer*>(pNewParent); + assert(!pNewParent || pNewGtkParent); + if (pNewGtkParent) + { + auto pNewContainer = pNewGtkParent->getContainer(); + container_add(GTK_WIDGET(pNewContainer), pChild); + } + g_object_unref(pChild); + } + + 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); + container_remove(pParent, pWindow); + container_add(GTK_WIDGET(m_pContainer), pWindow); +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_container_child_set(m_pContainer, pWindow, "expand", true, "fill", true, nullptr); +#endif + 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; + } + + virtual ~GtkInstanceContainer() override + { + if (m_nSetFocusChildSignalId) + g_signal_handler_disconnect(m_pContainer, m_nSetFocusChildSignalId); + } +}; + +} + +std::unique_ptr<weld::Container> GtkInstanceWidget::weld_parent() const +{ + GtkWidget* pParent = gtk_widget_get_parent(m_pWidget); + if (!pParent) + return nullptr; +#if !GTK_CHECK_VERSION(4, 0, 0) + return std::make_unique<GtkInstanceContainer>(GTK_CONTAINER(pParent), m_pBuilder, false); +#else + return std::make_unique<GtkInstanceContainer>(pParent, m_pBuilder, false); +#endif +} + +namespace { + +bool sortButtons(const GtkWidget* pA, const GtkWidget* pB) +{ + //order within groups according to platform rules + return getButtonPriority(get_buildable_id(GTK_BUILDABLE(pA))) < + getButtonPriority(get_buildable_id(GTK_BUILDABLE(pB))); +} + +void sort_native_button_order(GtkBox* pContainer) +{ + std::vector<GtkWidget*> aChildren; +#if GTK_CHECK_VERSION(4, 0, 0) + for (GtkWidget* pChild = gtk_widget_get_first_child(GTK_WIDGET(pContainer)); + pChild; pChild = gtk_widget_get_next_sibling(pChild)) + { + aChildren.push_back(pChild); + } +#else + 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); +#endif + + //sort child order within parent so that we match the platform button order + std::stable_sort(aChildren.begin(), aChildren.end(), sortButtons); + +#if GTK_CHECK_VERSION(4, 0, 0) + for (size_t pos = 0; pos < aChildren.size(); ++pos) + gtk_box_reorder_child_after(pContainer, aChildren[pos], pos ? aChildren[pos - 1] : nullptr); +#else + for (size_t pos = 0; pos < aChildren.size(); ++pos) + gtk_box_reorder_child(pContainer, aChildren[pos], pos); +#endif +} + +class GtkInstanceBox : public GtkInstanceContainer, public virtual weld::Box +{ +private: + GtkBox* m_pBox; + +public: + GtkInstanceBox(GtkBox* pBox, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) +#if !GTK_CHECK_VERSION(4, 0, 0) + : GtkInstanceContainer(GTK_CONTAINER(pBox), pBuilder, bTakeOwnership) +#else + : GtkInstanceContainer(GTK_WIDGET(pBox), pBuilder, bTakeOwnership) +#endif + , 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(); + +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_box_reorder_child(m_pBox, pChild, nNewPosition); +#else + if (nNewPosition == 0) + gtk_box_reorder_child_after(m_pBox, pChild, nullptr); + else + { + int nNewSiblingPos = nNewPosition - 1; + int nChildPosition = 0; + for (GtkWidget* pNewSibling = gtk_widget_get_first_child(GTK_WIDGET(m_pBox)); + pNewSibling; pNewSibling = gtk_widget_get_next_sibling(pNewSibling)) + { + if (nChildPosition == nNewSiblingPos) + { + gtk_box_reorder_child_after(m_pBox, pChild, pNewSibling); + break; + } + ++nChildPosition; + } + } +#endif + } + + virtual void sort_native_button_order() override + { + ::sort_native_button_order(m_pBox); + } +}; + +} + +namespace +{ + Point get_csd_offset(GtkWidget* pTopLevel) + { + // try and omit drawing CSD under wayland + GtkWidget* pChild = widget_get_first_child(pTopLevel); + + gtk_coord x, y; + gtk_widget_translate_coordinates(pChild, pTopLevel, 0, 0, &x, &y); + +#if !GTK_CHECK_VERSION(4, 0, 0) + int innerborder = gtk_container_get_border_width(GTK_CONTAINER(pChild)); + int outerborder = gtk_container_get_border_width(GTK_CONTAINER(pTopLevel)); + int totalborder = outerborder + innerborder; + x -= totalborder; + y -= totalborder; +#endif + + return Point(x, y); + } + + void do_collect_screenshot_data(GtkWidget* pItem, gpointer data) + { + GtkWidget* pTopLevel = widget_get_toplevel(pItem); + + gtk_coord 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_CHECK_VERSION(4, 0, 0) + for (GtkWidget* pChild = gtk_widget_get_first_child(pItem); + pChild; pChild = gtk_widget_get_next_sibling(pChild)) + { + do_collect_screenshot_data(pChild, data); + } +#else + if (GTK_IS_CONTAINER(pItem)) + gtk_container_forall(GTK_CONTAINER(pItem), do_collect_screenshot_data, data); +#endif + } + + AbsoluteScreenPixelRectangle get_monitor_workarea(GtkWidget* pWindow) + { + GdkRectangle aRect; +#if !GTK_CHECK_VERSION(4, 0, 0) + GdkScreen* pScreen = gtk_widget_get_screen(pWindow); + gint nMonitor = gdk_screen_get_monitor_at_window(pScreen, widget_get_surface(pWindow)); + gdk_screen_get_monitor_workarea(pScreen, nMonitor, &aRect); +#else + GdkDisplay* pDisplay = gtk_widget_get_display(pWindow); + GdkSurface* gdkWindow = widget_get_surface(pWindow); + GdkMonitor* pMonitor = gdk_display_get_monitor_at_surface(pDisplay, gdkWindow); + gdk_monitor_get_geometry(pMonitor, &aRect); +#endif + return AbsoluteScreenPixelRectangle(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; +protected: + std::optional<Point> m_aPosWhileInvis; //tdf#146648 store last known position when visible to return as pos if hidden + +#if !GTK_CHECK_VERSION(4, 0, 0) + 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); + } + + void recursively_unset_default_buttons() + { + implResetDefault(GTK_WIDGET(m_pWindow), nullptr); + } +#endif + +#if !GTK_CHECK_VERSION(4, 0, 0) + static gboolean help_pressed(GtkAccelGroup*, GObject*, guint, GdkModifierType, gpointer widget) + { + GtkInstanceWindow* pThis = static_cast<GtkInstanceWindow*>(widget); + pThis->help(); + return true; + } +#endif + + static void signalToplevelFocusChanged(GtkWindow*, GParamSpec*, gpointer widget) + { + GtkInstanceWindow* pThis = static_cast<GtkInstanceWindow*>(widget); + pThis->signal_container_focus_changed(); + } + + bool isPositioningAllowed() const + { + // no X/Y positioning under Wayland + GdkDisplay *pDisplay = gtk_widget_get_display(m_pWidget); + return !DLSYM_GDK_IS_WAYLAND_DISPLAY(pDisplay); + } + +protected: + void help(); +public: + GtkInstanceWindow(GtkWindow* pWindow, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) +#if !GTK_CHECK_VERSION(4, 0, 0) + : GtkInstanceContainer(GTK_CONTAINER(pWindow), pBuilder, bTakeOwnership) +#else + : GtkInstanceContainer(GTK_WIDGET(pWindow), pBuilder, bTakeOwnership) +#endif + , m_pWindow(pWindow) + , m_nToplevelFocusChangedSignalId(0) + { +#if !GTK_CHECK_VERSION(4, 0, 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); + } +#endif + } + + 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 m_xWindow; + } + + 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 + { +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_window_set_default_size(m_pWindow, 1, 1); +#else + gtk_window_resize(m_pWindow, 1, 1); +#endif + } + + virtual void window_move(int x, int y) override + { +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_window_move(m_pWindow, x, y); +#else + (void)x; + (void)y; +#endif + } + + virtual SystemEnvData get_system_data() const override + { + GtkSalFrame* pFrame = GtkSalFrame::getFromWindow(GTK_WIDGET(m_pWindow)); + assert(pFrame && "nothing should call this impl, yet anyway, if ever, except on result of GetFrameWeld()"); + const SystemEnvData* pEnvData = pFrame->GetSystemData(); + assert(pEnvData); + return *pEnvData; + } + + virtual Size get_size() const override + { + int current_width, current_height; +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_window_get_size(m_pWindow, ¤t_width, ¤t_height); +#else + gtk_window_get_default_size(m_pWindow, ¤t_width, ¤t_height); +#endif + return Size(current_width, current_height); + } + + virtual Point get_position() const override + { + if (m_aPosWhileInvis) + { + assert(!get_visible()); + return *m_aPosWhileInvis; + } + + int current_x(0), current_y(0); +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_window_get_position(m_pWindow, ¤t_x, ¤t_y); +#endif + return Point(current_x, current_y); + } + + virtual void show() override + { + m_aPosWhileInvis.reset(); + GtkInstanceContainer::show(); + } + + virtual void hide() override + { + if (is_visible()) + m_aPosWhileInvis = get_position(); + GtkInstanceContainer::hide(); + } + + virtual AbsoluteScreenPixelRectangle get_monitor_workarea() const override + { + return ::get_monitor_workarea(GTK_WIDGET(m_pWindow)); + } + + virtual void set_centered_on_parent(bool bTrackGeometryRequests) override + { +#if !GTK_CHECK_VERSION(4, 0, 0) + 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); +#else + (void)bTrackGeometryRequests; +#endif + } + + virtual bool get_resizable() const override + { + return gtk_window_get_resizable(m_pWindow); + } + + virtual bool has_toplevel_focus() const override + { +#if GTK_CHECK_VERSION(4, 0, 0) + return gtk_window_is_active(m_pWindow); +#else + return gtk_window_has_toplevel_focus(m_pWindow); +#endif + } + + virtual void present() override + { + gtk_window_present(m_pWindow); + } + + virtual void change_default_widget(weld::Widget* pOld, weld::Widget* pNew) override + { + GtkInstanceWidget* pGtkNew = dynamic_cast<GtkInstanceWidget*>(pNew); + GtkWidget* pWidgetNew = pGtkNew ? pGtkNew->getWidget() : nullptr; +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_window_set_default_widget(m_pWindow, pWidgetNew); + (void)pOld; +#else + GtkInstanceWidget* pGtkOld = dynamic_cast<GtkInstanceWidget*>(pOld); + GtkWidget* pWidgetOld = pGtkOld ? pGtkOld->getWidget() : nullptr; + if (pWidgetOld) + g_object_set(G_OBJECT(pWidgetOld), "has-default", false, nullptr); + else + recursively_unset_default_buttons(); + if (pWidgetNew) + g_object_set(G_OBJECT(pWidgetNew), "has-default", true, nullptr); +#endif + } + + virtual bool is_default_widget(const weld::Widget* pCandidate) const override + { + const GtkInstanceWidget* pGtkCandidate = dynamic_cast<const GtkInstanceWidget*>(pCandidate); + GtkWidget* pWidget = pGtkCandidate ? pGtkCandidate->getWidget() : nullptr; +#if GTK_CHECK_VERSION(4, 0, 0) + return pWidget && gtk_window_get_default_widget(m_pWindow) == pWidget; +#else + gboolean has_default(false); + if (pWidget) + g_object_get(G_OBJECT(pWidget), "has-default", &has_default, nullptr); + return has_default; +#endif + } + + virtual void set_window_state(const OUString& rStr) override + { + const vcl::WindowData aData(rStr); + const auto nMask = aData.mask(); + const auto nState = aData.state() & vcl::WindowState::SystemMask; + + if ((nMask & vcl::WindowDataMask::Size) == vcl::WindowDataMask::Size) + { + gtk_window_set_default_size(m_pWindow, aData.width(), aData.height()); + } + if (nMask & vcl::WindowDataMask::State) + { + if (nState & vcl::WindowState::Maximized) + gtk_window_maximize(m_pWindow); + else + gtk_window_unmaximize(m_pWindow); + } + +#if !GTK_CHECK_VERSION(4, 0, 0) + if (isPositioningAllowed() && ((nMask & vcl::WindowDataMask::Pos) == vcl::WindowDataMask::Pos)) + { + gtk_window_move(m_pWindow, aData.x(), aData.y()); + } +#endif + } + + virtual OUString get_window_state(vcl::WindowDataMask nMask) const override + { + bool bPositioningAllowed = isPositioningAllowed(); + + vcl::WindowData aData; + vcl::WindowDataMask nAvailable = vcl::WindowDataMask::State | vcl::WindowDataMask::Size; + if (bPositioningAllowed) + nAvailable |= vcl::WindowDataMask::Pos; + aData.setMask(nMask & nAvailable); + + if (nMask & vcl::WindowDataMask::State) + { + vcl::WindowState nState = vcl::WindowState::Normal; + if (gtk_window_is_maximized(m_pWindow)) + nState |= vcl::WindowState::Maximized; + aData.setState(nState); + } + + if (bPositioningAllowed && (nMask & vcl::WindowDataMask::Pos)) + aData.setPos(get_position()); + + if (nMask & vcl::WindowDataMask::Size) + aData.setSize(get_size()); + + return aData.toStr(); + } + + virtual void connect_container_focus_changed(const Link<Container&, void>& rLink) override + { + if (!m_nToplevelFocusChangedSignalId) + m_nToplevelFocusChangedSignalId = g_signal_connect(m_pWindow, "notify::has-toplevel-focus", G_CALLBACK(signalToplevelFocusChanged), this); + weld::Container::connect_container_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 VclPtr<VirtualDevice> screenshot() 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 !GTK_CHECK_VERSION(4, 0, 0) + 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)); + } +#endif + + if (!bAlreadyRealized) + { + GtkAllocation allocation; + gtk_widget_realize(GTK_WIDGET(m_pWindow)); + gtk_widget_get_allocation(GTK_WIDGET(m_pWindow), &allocation); +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_size_allocate(GTK_WIDGET(m_pWindow), &allocation); +#else + gtk_widget_size_allocate(GTK_WIDGET(m_pWindow), &allocation, 0); +#endif + } + + VclPtr<VirtualDevice> xOutput(VclPtr<VirtualDevice>::Create(DeviceFormat::WITHOUT_ALPHA)); + xOutput->SetOutputSizePixel(get_size()); + cairo_surface_t* pSurface = get_underlying_cairo_surface(*xOutput); + cairo_t* cr = cairo_create(pSurface); + + Point aOffset = get_csd_offset(GTK_WIDGET(m_pWindow)); + + cairo_translate(cr, -aOffset.X(), -aOffset.Y()); + +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_draw(GTK_WIDGET(m_pWindow), cr); +#else + GtkSnapshot* pSnapshot = gtk_snapshot_new(); + GtkWidgetClass* pWidgetClass = GTK_WIDGET_GET_CLASS(GTK_WIDGET(m_pWindow)); + pWidgetClass->snapshot(GTK_WIDGET(m_pWindow), pSnapshot); + GskRenderNode* pNode = gtk_snapshot_free_to_node(pSnapshot); + gsk_render_node_draw(pNode, cr); + gsk_render_node_unref(pNode); +#endif + + cairo_destroy(cr); + + if (!bAlreadyVisible) + gtk_widget_hide(GTK_WIDGET(m_pWindow)); + if (!bAlreadyRealized) + gtk_widget_unrealize(GTK_WIDGET(m_pWindow)); + + return xOutput; + } + + virtual weld::ScreenShotCollection collect_screenshot_data() override + { + weld::ScreenShotCollection aRet; + +#if GTK_CHECK_VERSION(4, 0, 0) + for (GtkWidget* pChild = gtk_widget_get_first_child(GTK_WIDGET(m_pWindow)); + pChild; pChild = gtk_widget_get_next_sibling(pChild)) + { + do_collect_screenshot_data(pChild, &aRet); + } +#else + gtk_container_foreach(GTK_CONTAINER(m_pWindow), do_collect_screenshot_data, &aRet); +#endif + + return aRet; + } + + virtual const vcl::ILibreOfficeKitNotifier* GetLOKNotifier() override + { + // dummy implementation + return nullptr; + } + + 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); + +#if !GTK_CHECK_VERSION(4, 0, 0) + 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 */ + } +#endif + + 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; +#if !GTK_CHECK_VERSION(4, 0, 0) + gulong nSignalDeleteId = g_signal_connect(m_pDialog, "delete-event", G_CALLBACK(signal_delete), this); +#endif + 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; + + main_loop_run(m_pLoop); + + 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); +#if !GTK_CHECK_VERSION(4, 0, 0) + g_signal_handler_disconnect(m_pDialog, nSignalDeleteId); +#endif + 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 +{ +#if GTK_CHECK_VERSION(4, 0, 0) + void collectVisibleChildren(GtkWidget* pTop, winset& rVisibleWidgets) + { + for (GtkWidget* pChild = gtk_widget_get_first_child(pTop); + pChild; pChild = gtk_widget_get_next_sibling(pChild)) + { + if (!gtk_widget_get_visible(pChild)) + continue; + rVisibleWidgets.insert(pChild); + collectVisibleChildren(pChild, rVisibleWidgets); + } + } +#endif + + void hideUnless(GtkWidget* pTop, const winset& rVisibleWidgets, + std::vector<GtkWidget*> &rWasVisibleWidgets) + { +#if GTK_CHECK_VERSION(4, 0, 0) + for (GtkWidget* pChild = gtk_widget_get_first_child(pTop); + pChild; pChild = gtk_widget_get_next_sibling(pChild)) + { + 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 + { + hideUnless(pChild, rVisibleWidgets, rWasVisibleWidgets); + } + } +#else + GList* pChildren = gtk_container_get_children(GTK_CONTAINER(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(pChild, rVisibleWidgets, rWasVisibleWidgets); + } + } + g_list_free(pChildren); +#endif + } + +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 +#if !GTK_CHECK_VERSION(4, 0, 0) + int m_nOldBorderWidth; // border width for expanded dialog +#endif + + 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); + } + +#if !GTK_CHECK_VERSION(4, 0, 0) + 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 */ + } +#endif + + 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); + +#if !GTK_CHECK_VERSION(4, 0, 0) + static void signalActivate(GtkMenuItem*, gpointer data) + { + bool* pActivate = static_cast<bool*>(data); + *pActivate = true; + } +#endif + +#if !GTK_CHECK_VERSION(4, 0, 0) + bool signal_screenshot_popup_menu(const 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)) + main_loop_run(pLoop); + + 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; + } +#endif + + static gboolean signalScreenshotPopupMenu(GtkWidget*, gpointer widget) + { +#if !GTK_CHECK_VERSION(4, 0, 0) + GtkInstanceDialog* pThis = static_cast<GtkInstanceDialog*>(widget); + return pThis->signal_screenshot_popup_menu(nullptr); +#else + (void)widget; + return false; +#endif + } + +#if !GTK_CHECK_VERSION(4, 0, 0) + 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; + } +#endif + +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) +#if !GTK_CHECK_VERSION(4, 0, 0) + , m_nOldBorderWidth(0) +#endif + { + 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); +#if !GTK_CHECK_VERSION(4, 0, 0) + g_signal_connect(m_pDialog, "button-press-event", G_CALLBACK(signalScreenshotButton), this); +#endif + } + } + + 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; +#if !GTK_CHECK_VERSION(4, 0, 0) + m_nSignalDeleteId = g_signal_connect(m_pDialog, "delete-event", G_CALLBACK(signalAsyncDelete), this); +#endif + + 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; +#if !GTK_CHECK_VERSION(4, 0, 0) + m_nSignalDeleteId = g_signal_connect(m_pDialog, "delete-event", G_CALLBACK(signalAsyncDelete), this); +#endif + + 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_CHECK_VERSION(4, 0, 0) + if (GTK_IS_DIALOG(m_pDialog)) + sort_native_button_order(GTK_BOX(gtk_dialog_get_action_area(GTK_DIALOG(m_pDialog)))); +#endif + 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 OUString& 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 + { +#if !GTK_CHECK_VERSION(4, 0, 0) + return new GtkInstanceContainer(GTK_CONTAINER(gtk_dialog_get_content_area(GTK_DIALOG(m_pDialog))), m_pBuilder, false); +#else + return new GtkInstanceContainer(gtk_dialog_get_content_area(GTK_DIALOG(m_pDialog)), m_pBuilder, false); +#endif + } + + 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); + } +#if GTK_CHECK_VERSION(4, 0, 0) + collectVisibleChildren(pRefEdit, aVisibleWidgets); +#endif + if (pRefBtn) + { +#if GTK_CHECK_VERSION(4, 0, 0) + collectVisibleChildren(pRefBtn, aVisibleWidgets); +#endif + //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(pContentArea, aVisibleWidgets, m_aHiddenWidgets); + gtk_widget_set_size_request(pRefEdit, m_nOldEditWidth, -1); +#if !GTK_CHECK_VERSION(4, 0, 0) + 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); + gtk_widget_show_all(pRefEdit); + if (pRefBtn) + gtk_widget_show_all(pRefBtn); +#else + if (GtkWidget* pActionArea = gtk_dialog_get_header_bar(GTK_DIALOG(m_pDialog))) + gtk_widget_hide(pActionArea); +#endif + + // 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 + bool bWorkaroundSizeSpringingBack = DLSYM_GDK_IS_WAYLAND_DISPLAY(gtk_widget_get_display(m_pWidget)); + if (bWorkaroundSizeSpringingBack) + gtk_widget_unmap(GTK_WIDGET(m_pDialog)); + + resize_to_request(); + + if (bWorkaroundSizeSpringingBack) + gtk_widget_map(GTK_WIDGET(m_pDialog)); + + 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; +#if !GTK_CHECK_VERSION(4, 0, 0) + 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); +#else + if (GtkWidget* pActionArea = gtk_dialog_get_header_bar(GTK_DIALOG(m_pDialog))) + gtk_widget_show(pActionArea); +#endif + 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 + { +#if !GTK_CHECK_VERSION(4, 0, 0) + return new GtkInstanceContainer(GTK_CONTAINER(gtk_message_dialog_get_message_area(m_pMessageDialog)), m_pBuilder, false); +#else + return new GtkInstanceContainer(gtk_message_dialog_get_message_area(m_pMessageDialog), m_pBuilder, false); +#endif + } +}; + +void set_label_wrap(GtkLabel* pLabel, bool bWrap) +{ +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_label_set_wrap(pLabel, bWrap); +#else + gtk_label_set_line_wrap(pLabel, bWrap); +#endif +} + +class GtkInstanceAssistant : public GtkInstanceDialog, public virtual weld::Assistant +{ +private: + GtkAssistant* m_pAssistant; + GtkWidget* m_pSidebar; + GtkWidget* m_pSidebarEventBox; +#if !GTK_CHECK_VERSION(4, 0, 0) + GtkButtonBox* m_pButtonBox; +#else + GtkBox* m_pButtonBox; + GtkEventController* m_pSidebarClickController; +#endif + 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<OUString, bool> m_aNotClickable; + + int find_page(std::u16string_view ident) 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); + OUString sBuildableName = ::get_buildable_id(GTK_BUILDABLE(pPage)); + if (sBuildableName == ident) + return i; + } + return -1; + } + + static void wrap_sidebar_label(GtkWidget *pWidget, gpointer /*user_data*/) + { + if (GTK_IS_LABEL(pWidget)) + { + ::set_label_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) + { + OUString sBuildableName = ::get_buildable_id(GTK_BUILDABLE(pWidget)); + if (sBuildableName == "sidebar") + { + GtkWidget **ppSidebar = static_cast<GtkWidget**>(user_data); + *ppSidebar = pWidget; + } +#if !GTK_CHECK_VERSION(4, 0, 0) + if (GTK_IS_CONTAINER(pWidget)) + gtk_container_forall(GTK_CONTAINER(pWidget), find_sidebar, user_data); +#endif + } + + static void signalHelpClicked(GtkButton*, gpointer widget) + { + GtkInstanceAssistant* pThis = static_cast<GtkInstanceAssistant*>(widget); + pThis->signal_help_clicked(); + } + + void signal_help_clicked() + { + help(); + } + +#if GTK_CHECK_VERSION(4, 0, 0) + static void signalButton(GtkGestureClick* /*pGesture*/, int /*n_press*/, gdouble x, gdouble y, gpointer widget) + { + GtkInstanceAssistant* pThis = static_cast<GtkInstanceAssistant*>(widget); + SolarMutexGuard aGuard; + pThis->signal_button(x, y); + } +#else + static gboolean signalButton(GtkWidget*, GdkEventButton* pEvent, gpointer widget) + { + GtkInstanceAssistant* pThis = static_cast<GtkInstanceAssistant*>(widget); + SolarMutexGuard aGuard; + return pThis->signal_button(pEvent->x, pEvent->y); + } +#endif + + bool signal_button(gtk_coord event_x, gtk_coord event_y) + { + int nNewCurrentPage = -1; + + GtkAllocation allocation; + + int nPageIndex = 0; + +#if GTK_CHECK_VERSION(4, 0, 0) + for (GtkWidget* pWidget = gtk_widget_get_first_child(m_pSidebar); + pWidget; pWidget = gtk_widget_get_next_sibling(pWidget)) + { +#else + 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); +#endif + if (!gtk_widget_get_visible(pWidget)) + continue; + + gtk_widget_get_allocation(pWidget, &allocation); + + gtk_coord dest_x1, dest_y1; + gtk_widget_translate_coordinates(pWidget, + m_pSidebarEventBox, + 0, + 0, + &dest_x1, + &dest_y1); + + gtk_coord dest_x2, dest_y2; + gtk_widget_translate_coordinates(pWidget, + m_pSidebarEventBox, + allocation.width, + allocation.height, + &dest_x2, + &dest_y2); + + + if (event_x >= dest_x1 && event_x <= dest_x2 && event_y >= dest_y1 && event_y <= dest_y2) + { + nNewCurrentPage = nPageIndex; + break; + } + + ++nPageIndex; + } +#if !GTK_CHECK_VERSION(4, 0, 0) + g_list_free(pChildren); +#endif + + if (nNewCurrentPage != -1 && nNewCurrentPage != get_current_page()) + { + OUString 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) +#if GTK_CHECK_VERSION(4, 0, 0) + , m_pSidebarClickController(nullptr) +#endif + , m_nButtonPressSignalId(0) + { +#if !GTK_CHECK_VERSION(4, 0, 0) + 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); +#else + m_pButtonBox = GTK_BOX(gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6)); +#endif + + m_pBack = GTK_BUTTON(gtk_button_new_with_mnemonic(MapToGtkAccelerator(GetStandardText(StandardButtonType::Back)).getStr())); +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_set_can_default(GTK_WIDGET(m_pBack), true); +#endif + ::set_buildable_id(GTK_BUILDABLE(m_pBack), "previous"); +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_box_append(GTK_BOX(m_pButtonBox), GTK_WIDGET(m_pBack)); +#else + gtk_box_pack_end(GTK_BOX(m_pButtonBox), GTK_WIDGET(m_pBack), false, false, 0); +#endif + + m_pNext = GTK_BUTTON(gtk_button_new_with_mnemonic(MapToGtkAccelerator(GetStandardText(StandardButtonType::Next)).getStr())); +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_set_can_default(GTK_WIDGET(m_pNext), true); +#endif + ::set_buildable_id(GTK_BUILDABLE(m_pNext), "next"); +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_box_append(GTK_BOX(m_pButtonBox), GTK_WIDGET(m_pNext)); +#else + gtk_box_pack_end(GTK_BOX(m_pButtonBox), GTK_WIDGET(m_pNext), false, false, 0); +#endif + + m_pCancel = GTK_BUTTON(gtk_button_new_with_mnemonic(MapToGtkAccelerator(GetStandardText(StandardButtonType::Cancel)).getStr())); +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_set_can_default(GTK_WIDGET(m_pCancel), true); +#endif +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_box_append(GTK_BOX(m_pButtonBox), GTK_WIDGET(m_pCancel)); +#else + gtk_box_pack_end(GTK_BOX(m_pButtonBox), GTK_WIDGET(m_pCancel), false, false, 0); +#endif + + m_pFinish = GTK_BUTTON(gtk_button_new_with_mnemonic(MapToGtkAccelerator(GetStandardText(StandardButtonType::Finish)).getStr())); +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_set_can_default(GTK_WIDGET(m_pFinish), true); +#endif + ::set_buildable_id(GTK_BUILDABLE(m_pFinish), "finish"); +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_box_append(GTK_BOX(m_pButtonBox), GTK_WIDGET(m_pFinish)); +#else + gtk_box_pack_end(GTK_BOX(m_pButtonBox), GTK_WIDGET(m_pFinish), false, false, 0); +#endif + +#if GTK_CHECK_VERSION(4, 0, 0) + m_pHelp = GTK_BUTTON(gtk_button_new_from_icon_name("help-browser-symbolic")); +#else + m_pHelp = GTK_BUTTON(gtk_button_new_with_mnemonic(MapToGtkAccelerator(GetStandardText(StandardButtonType::Help)).getStr())); +#endif +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_set_can_default(GTK_WIDGET(m_pHelp), true); +#endif + g_signal_connect(m_pHelp, "clicked", G_CALLBACK(signalHelpClicked), this); +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_box_prepend(GTK_BOX(m_pButtonBox), GTK_WIDGET(m_pHelp)); + gtk_widget_set_hexpand(GTK_WIDGET(m_pHelp), true); + gtk_widget_set_halign(GTK_WIDGET(m_pHelp), GTK_ALIGN_START); +#else + gtk_box_pack_end(GTK_BOX(m_pButtonBox), GTK_WIDGET(m_pHelp), false, false, 0); +#endif + + gtk_assistant_add_action_widget(pAssistant, GTK_WIDGET(m_pButtonBox)); +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_button_box_set_child_secondary(m_pButtonBox, GTK_WIDGET(m_pHelp), true); +#endif + gtk_widget_set_hexpand(GTK_WIDGET(m_pButtonBox), true); + + GtkWidget* pParent = gtk_widget_get_parent(GTK_WIDGET(m_pButtonBox)); +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_container_child_set(GTK_CONTAINER(pParent), GTK_WIDGET(m_pButtonBox), "expand", true, "fill", true, nullptr); +#endif + 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 +#if GTK_CHECK_VERSION(4, 0, 0) + for (GtkWidget* pChild = gtk_widget_get_first_child(pParent); + pChild; pChild = gtk_widget_get_next_sibling(pChild)) + { + gtk_widget_hide(pChild); + } +#else + 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); +#endif + +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_show_all(GTK_WIDGET(m_pButtonBox)); +#else + gtk_widget_show(GTK_WIDGET(m_pButtonBox)); +#endif + + find_sidebar(GTK_WIDGET(m_pAssistant), &m_pSidebar); + + m_pSidebarEventBox = ::ensureEventWidget(m_pSidebar); + if (m_pSidebarEventBox) + { +#if GTK_CHECK_VERSION(4, 0, 0) + GtkGesture *pClick = gtk_gesture_click_new(); + gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(pClick), 0); + m_pSidebarClickController = GTK_EVENT_CONTROLLER(pClick); + gtk_widget_add_controller(m_pSidebarEventBox, m_pSidebarClickController); + m_nButtonPressSignalId = g_signal_connect(m_pSidebarClickController, "pressed", G_CALLBACK(signalButton), this); +#else + m_nButtonPressSignalId = g_signal_connect(m_pSidebarEventBox, "button-press-event", G_CALLBACK(signalButton), this); +#endif + } + } + + 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 OUString get_page_ident(int nPage) const override + { + const GtkWidget* pWidget = gtk_assistant_get_nth_page(m_pAssistant, nPage); + return ::get_buildable_id(GTK_BUILDABLE(pWidget)); + } + + virtual OUString 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 OUString& rIdent) override + { + int nPage = find_page(rIdent); + if (nPage == -1) + return; + set_current_page(nPage); + } + + virtual void set_page_title(const OUString& 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()); +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_container_forall(GTK_CONTAINER(m_pSidebar), wrap_sidebar_label, nullptr); +#endif + } + + virtual OUString get_page_title(const OUString& 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 OUString& rIdent, bool bSensitive) override + { + m_aNotClickable[rIdent] = !bSensitive; + } + + virtual void set_page_index(const OUString& 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); + std::optional<OString> sTitle; + if (auto const title = gtk_assistant_get_page_title(m_pAssistant, pPage)) { + sTitle = title; + } + 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 ? sTitle->getStr() : nullptr); +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_container_forall(GTK_CONTAINER(m_pSidebar), wrap_sidebar_label, nullptr); +#endif + g_object_unref(pPage); + } + + virtual weld::Container* append_page(const OUString& rIdent) override + { + disable_notify_events(); + + GtkWidget *pChild = gtk_grid_new(); + ::set_buildable_id(GTK_BUILDABLE(pChild), rIdent); + 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(); + +#if !GTK_CHECK_VERSION(4, 0, 0) + m_aPages.emplace_back(new GtkInstanceContainer(GTK_CONTAINER(pChild), m_pBuilder, false)); +#else + m_aPages.emplace_back(new GtkInstanceContainer(pChild, m_pBuilder, false)); +#endif + + return m_aPages.back().get(); + } + + virtual void set_page_side_help_id(const OUString& 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 void set_page_side_image(const OUString& /*rImage*/) override + { + // Since GTK+ 3.2, sidebar images are not shown anymore + } + + virtual ~GtkInstanceAssistant() override + { + if (m_nButtonPressSignalId) + { +#if GTK_CHECK_VERSION(4, 0, 0) + g_signal_handler_disconnect(m_pSidebarClickController, m_nButtonPressSignalId); +#else + g_signal_handler_disconnect(m_pSidebarEventBox, m_nButtonPressSignalId); +#endif + } + } +}; + +class GtkInstanceFrame : public GtkInstanceContainer, public virtual weld::Frame +{ +private: + GtkFrame* m_pFrame; +public: + GtkInstanceFrame(GtkFrame* pFrame, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) +#if !GTK_CHECK_VERSION(4, 0, 0) + : GtkInstanceContainer(GTK_CONTAINER(pFrame), pBuilder, bTakeOwnership) +#else + : GtkInstanceContainer(GTK_WIDGET(pFrame), pBuilder, bTakeOwnership) +#endif + , 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) +#if !GTK_CHECK_VERSION(4, 0, 0) + : GtkInstanceContainer(GTK_CONTAINER(pPaned), pBuilder, bTakeOwnership) +#else + : GtkInstanceContainer(GTK_WIDGET(pPaned), pBuilder, bTakeOwnership) +#endif + , 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 immobilized_viewport_get_type(); +static gpointer immobilized_viewport_parent_class; + +#ifndef NDEBUG +# define IMMOBILIZED_TYPE_VIEWPORT (immobilized_viewport_get_type()) +# define IMMOBILIZED_IS_VIEWPORT(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), IMMOBILIZED_TYPE_VIEWPORT)) +#endif + +namespace { + +struct ImmobilizedViewportPrivate +{ + GtkAdjustment *hadjustment; + GtkAdjustment *vadjustment; +}; + +} + +#define IMMOBILIZED_VIEWPORT_PRIVATE_DATA "ImmobilizedViewportPrivateData" + +enum +{ + PROP_0, + PROP_HADJUSTMENT, + PROP_VADJUSTMENT, + PROP_HSCROLL_POLICY, + PROP_VSCROLL_POLICY, + PROP_SHADOW_TYPE +}; + +static void viewport_set_adjustment(GtkViewport *viewport, + GtkOrientation orientation, + GtkAdjustment *adjustment) +{ + ImmobilizedViewportPrivate* priv = + static_cast<ImmobilizedViewportPrivate*>(g_object_get_data(G_OBJECT(viewport), + IMMOBILIZED_VIEWPORT_PRIVATE_DATA)); + + if (!adjustment) + adjustment = gtk_adjustment_new(0.0, 0.0, 0.0, 0.0, 0.0, 0.0); + + if (orientation == GTK_ORIENTATION_HORIZONTAL) + { + if (priv->hadjustment) + g_object_unref(priv->hadjustment); + priv->hadjustment = adjustment; + } + else + { + if (priv->vadjustment) + g_object_unref(priv->vadjustment); + priv->vadjustment = adjustment; + } + + g_object_ref_sink(adjustment); +} + +static void +immobilized_viewport_set_property(GObject* object, + guint prop_id, + const GValue* value, + GParamSpec* /*pspec*/) +{ + GtkViewport *viewport = GTK_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 +immobilized_viewport_get_property(GObject* object, + guint prop_id, + GValue* value, + GParamSpec* /*pspec*/) +{ + ImmobilizedViewportPrivate* priv = + static_cast<ImmobilizedViewportPrivate*>(g_object_get_data(object, + IMMOBILIZED_VIEWPORT_PRIVATE_DATA)); + + switch (prop_id) + { + case PROP_HADJUSTMENT: + g_value_set_object(value, priv->hadjustment); + break; + case PROP_VADJUSTMENT: + g_value_set_object(value, priv->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 ImmobilizedViewportPrivate* +immobilized_viewport_new_private_data() +{ + ImmobilizedViewportPrivate* priv = g_slice_new0(ImmobilizedViewportPrivate); + priv->hadjustment = nullptr; + priv->vadjustment = nullptr; + return priv; +} + +static void +immobilized_viewport_instance_init(GTypeInstance *instance, gpointer /*klass*/) +{ + GObject* object = G_OBJECT(instance); + g_object_set_data(object, IMMOBILIZED_VIEWPORT_PRIVATE_DATA, + immobilized_viewport_new_private_data()); +} + +static void +immobilized_viewport_finalize(GObject* object) +{ + void* priv = g_object_get_data(object, IMMOBILIZED_VIEWPORT_PRIVATE_DATA); + if (priv) + { + g_slice_free(ImmobilizedViewportPrivate, priv); + g_object_set_data(object, IMMOBILIZED_VIEWPORT_PRIVATE_DATA, nullptr); + } + G_OBJECT_CLASS(immobilized_viewport_parent_class)->finalize(object); +} + +static void immobilized_viewport_class_init(GtkWidgetClass* klass) +{ + immobilized_viewport_parent_class = g_type_class_peek_parent(klass); + + GObjectClass* o_class = G_OBJECT_CLASS(klass); + + /* GObject signals */ + o_class->finalize = immobilized_viewport_finalize; + o_class->set_property = immobilized_viewport_set_property; + o_class->get_property = immobilized_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 immobilized_viewport_get_type() +{ + static GType type = 0; + + if (!type) + { + GTypeQuery query; + g_type_query(gtk_viewport_get_type(), &query); + + static const GTypeInfo tinfo = + { + static_cast<guint16>(query.class_size), + nullptr, /* base init */ + nullptr, /* base finalize */ + reinterpret_cast<GClassInitFunc>(immobilized_viewport_class_init), /* class init */ + nullptr, /* class finalize */ + nullptr, /* class data */ + static_cast<guint16>(query.instance_size), /* instance size */ + 0, /* nb preallocs */ + immobilized_viewport_instance_init, /* instance init */ + nullptr /* value table */ + }; + + type = g_type_register_static(GTK_TYPE_VIEWPORT, "ImmobilizedViewport", + &tinfo, GTypeFlags(0)); + } + + return type; +} + +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; + GtkCssProvider* m_pScrollBarCssProvider; + 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, bool bUserManagedScrolling) +#if !GTK_CHECK_VERSION(4, 0, 0) + : GtkInstanceContainer(GTK_CONTAINER(pScrolledWindow), pBuilder, bTakeOwnership) +#else + : GtkInstanceContainer(GTK_WIDGET(pScrolledWindow), pBuilder, bTakeOwnership) +#endif + , m_pScrolledWindow(pScrolledWindow) + , m_pOrigViewport(nullptr) + , m_pScrollBarCssProvider(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)) + { + if (bUserManagedScrolling) + set_user_managed_scrolling(); + } + + void set_user_managed_scrolling() + { + 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); +#if GTK_CHECK_VERSION(4, 0, 0) + GtkWidget *pViewport = gtk_scrolled_window_get_child(m_pScrolledWindow); +#else + GtkWidget *pViewport = gtk_bin_get_child(GTK_BIN(m_pScrolledWindow)); +#endif + assert(GTK_IS_VIEWPORT(pViewport)); +#if GTK_CHECK_VERSION(4, 0, 0) + GtkWidget *pChild= gtk_viewport_get_child(GTK_VIEWPORT(pViewport)); +#else + GtkWidget *pChild = gtk_bin_get_child(GTK_BIN(pViewport)); +#endif + g_object_ref(pChild); +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_viewport_set_child(GTK_VIEWPORT(pViewport), nullptr); +#else + gtk_container_remove(GTK_CONTAINER(pViewport), pChild); +#endif + g_object_ref(pViewport); +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_scrolled_window_set_child(m_pScrolledWindow, nullptr); +#else + gtk_container_remove(GTK_CONTAINER(m_pScrolledWindow), pViewport); +#endif + GtkWidget* pNewViewport = GTK_WIDGET(g_object_new(immobilized_viewport_get_type(), nullptr)); + gtk_widget_show(pNewViewport); +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_scrolled_window_set_child(m_pScrolledWindow, pNewViewport); + gtk_viewport_set_child(GTK_VIEWPORT(pNewViewport), pChild); +#else + gtk_container_add(GTK_CONTAINER(m_pScrolledWindow), pNewViewport); + gtk_container_add(GTK_CONTAINER(pNewViewport), pChild); +#endif + 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, VclToGtk(eHPolicy), eGtkVPolicy); + } + + virtual VclPolicyType get_hpolicy() const override + { + GtkPolicyType eGtkHPolicy; + gtk_scrolled_window_get_policy(m_pScrolledWindow, &eGtkHPolicy, nullptr); + return GtkToVcl(eGtkHPolicy); + } + + 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_scroll_thickness() const override + { + if (gtk_scrolled_window_get_overlay_scrolling(m_pScrolledWindow)) + return 0; + GtkRequisition size; + gtk_widget_get_preferred_size(gtk_scrolled_window_get_vscrollbar(m_pScrolledWindow), nullptr, &size); + return size.width; + } + + virtual void set_scroll_thickness(int nThickness) override + { + GtkWidget *pHorzBar = gtk_scrolled_window_get_hscrollbar(m_pScrolledWindow); + GtkWidget *pVertBar = gtk_scrolled_window_get_vscrollbar(m_pScrolledWindow); + gtk_widget_set_size_request(pHorzBar, -1, nThickness); + gtk_widget_set_size_request(pVertBar, nThickness, -1); + } + + 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 void customize_scrollbars(const Color& rBackgroundColor, + const Color& rShadowColor, + const Color& rFaceColor) override + { + GtkWidget *pHorzBar = gtk_scrolled_window_get_hscrollbar(m_pScrolledWindow); + GtkWidget *pVertBar = gtk_scrolled_window_get_vscrollbar(m_pScrolledWindow); + GtkStyleContext *pHorzContext = gtk_widget_get_style_context(pHorzBar); + GtkStyleContext *pVertContext = gtk_widget_get_style_context(pVertBar); + if (m_pScrollBarCssProvider) + { + gtk_style_context_remove_provider(pHorzContext, GTK_STYLE_PROVIDER(m_pScrollBarCssProvider)); + gtk_style_context_remove_provider(pVertContext, GTK_STYLE_PROVIDER(m_pScrollBarCssProvider)); + } + + m_pScrollBarCssProvider = gtk_css_provider_new(); + // intentionally 'trough' a long, narrow open container. + OUString aBuffer = "scrollbar contents trough { background-color: #" + rBackgroundColor.AsRGBHexString() + "; } " + "scrollbar contents trough slider { background-color: #" + rShadowColor.AsRGBHexString() + "; } " + "scrollbar contents button { background-color: #" + rFaceColor.AsRGBHexString() + "; } " + "scrollbar contents button { color: #000000; } " + "scrollbar contents button:disabled { color: #7f7f7f; }"; + OString aResult = OUStringToOString(aBuffer, RTL_TEXTENCODING_UTF8); + css_provider_load_from_data(m_pScrollBarCssProvider, aResult.getStr(), aResult.getLength()); + + gtk_style_context_add_provider(pHorzContext, GTK_STYLE_PROVIDER(m_pScrollBarCssProvider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + gtk_style_context_add_provider(pVertContext, GTK_STYLE_PROVIDER(m_pScrollBarCssProvider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + } + + 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); + + if (m_pScrollBarCssProvider) + { + GtkStyleContext *pHorzContext = gtk_widget_get_style_context(gtk_scrolled_window_get_hscrollbar(m_pScrolledWindow)); + GtkStyleContext *pVertContext = gtk_widget_get_style_context(gtk_scrolled_window_get_vscrollbar(m_pScrolledWindow)); + gtk_style_context_remove_provider(pHorzContext, GTK_STYLE_PROVIDER(m_pScrollBarCssProvider)); + gtk_style_context_remove_provider(pVertContext, GTK_STYLE_PROVIDER(m_pScrollBarCssProvider)); + m_pScrollBarCssProvider = nullptr; + } + + //put it back the way it was + if (!m_pOrigViewport) + return; + + 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); + +#if GTK_CHECK_VERSION(4, 0, 0) + GtkWidget *pViewport = gtk_scrolled_window_get_child(m_pScrolledWindow); +#else + GtkWidget *pViewport = gtk_bin_get_child(GTK_BIN(m_pScrolledWindow)); +#endif + assert(IMMOBILIZED_IS_VIEWPORT(pViewport)); +#if GTK_CHECK_VERSION(4, 0, 0) + GtkWidget *pChild= gtk_viewport_get_child(GTK_VIEWPORT(pViewport)); +#else + GtkWidget *pChild = gtk_bin_get_child(GTK_BIN(pViewport)); +#endif + g_object_ref(pChild); +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_viewport_set_child(GTK_VIEWPORT(pViewport), nullptr); +#else + gtk_container_remove(GTK_CONTAINER(pViewport), pChild); +#endif + g_object_ref(pViewport); +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_scrolled_window_set_child(m_pScrolledWindow, nullptr); +#else + gtk_container_remove(GTK_CONTAINER(m_pScrolledWindow), pViewport); +#endif + +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_scrolled_window_set_child(m_pScrolledWindow, m_pOrigViewport); +#else + gtk_container_add(GTK_CONTAINER(m_pScrolledWindow), m_pOrigViewport); +#endif + // coverity[freed_arg : FALSE] - this does not free m_pOrigViewport, it is reffed by m_pScrolledWindow + g_object_unref(m_pOrigViewport); +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_viewport_set_child(GTK_VIEWPORT(m_pOrigViewport), pChild); +#else + gtk_container_add(GTK_CONTAINER(m_pOrigViewport), pChild); +#endif + g_object_unref(pChild); +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_destroy(pViewport); +#endif + g_object_unref(pViewport); + m_pOrigViewport = nullptr; + GtkInstanceContainer::enable_notify_events(); + } +}; + +class GtkInstanceScrollbar final : public GtkInstanceWidget, public virtual weld::Scrollbar +{ +private: + GtkScrollbar* m_pScrollbar; + GtkAdjustment* m_pAdjustment; + GtkCssProvider* m_pThicknessCssProvider; + gulong m_nAdjustChangedSignalId; + + static void signalAdjustValueChanged(GtkAdjustment*, gpointer widget) + { + GtkInstanceScrollbar* pThis = static_cast<GtkInstanceScrollbar*>(widget); + SolarMutexGuard aGuard; + pThis->signal_adjustment_changed(); + } + +#if GTK_CHECK_VERSION(4, 0, 0) + // if the widget is inside a GtkSalFrame then ensure the event is processed by the GtkSalFrame and not the + // GtkScrollbar + static gboolean signalScroll(GtkEventControllerScroll* pController, double delta_x, double delta_y, gpointer widget) + { + GtkInstanceScrollbar* pThis = static_cast<GtkInstanceScrollbar*>(widget); + + GtkWidget* pParent = widget_get_toplevel(GTK_WIDGET(pThis->m_pScrollbar)); + GtkSalFrame* pFrame = pParent ? GtkSalFrame::getFromWindow(pParent) : nullptr; + + return pFrame && pFrame->event_controller_scroll_forward(pController, delta_x, delta_y); + } +#else + static gboolean signalScroll(GtkWidget* pWidget, GdkEventScroll* /*pEvent*/, gpointer widget) + { + GtkInstanceScrollbar* pThis = static_cast<GtkInstanceScrollbar*>(widget); + + GtkWidget* pParent = widget_get_toplevel(GTK_WIDGET(pThis->m_pScrollbar)); + GtkSalFrame* pFrame = pParent ? GtkSalFrame::getFromWindow(pParent) : nullptr; + + if (pFrame) + g_signal_stop_emission_by_name(pWidget, "scroll-event"); + + return false; + } +#endif + +public: + GtkInstanceScrollbar(GtkScrollbar* pScrollbar, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : GtkInstanceWidget(GTK_WIDGET(pScrollbar), pBuilder, bTakeOwnership) + , m_pScrollbar(pScrollbar) +#if GTK_CHECK_VERSION(4, 0, 0) + , m_pAdjustment(gtk_scrollbar_get_adjustment(m_pScrollbar)) +#else + , m_pAdjustment(gtk_range_get_adjustment(GTK_RANGE(m_pScrollbar))) +#endif + , m_pThicknessCssProvider(nullptr) + , m_nAdjustChangedSignalId(g_signal_connect(m_pAdjustment, "value-changed", G_CALLBACK(signalAdjustValueChanged), this)) + { +#if GTK_CHECK_VERSION(4, 0, 0) + GtkEventController* pScrollController = gtk_event_controller_scroll_new(GTK_EVENT_CONTROLLER_SCROLL_BOTH_AXES); + gtk_event_controller_set_propagation_phase(pScrollController, GTK_PHASE_CAPTURE); + g_signal_connect(pScrollController, "scroll", G_CALLBACK(signalScroll), this); + gtk_widget_add_controller(GTK_WIDGET(pScrollbar), pScrollController); +#else + g_signal_connect(pScrollbar, "scroll-event", G_CALLBACK(signalScroll), this); +#endif + } + + virtual void adjustment_configure(int value, int lower, int upper, + int step_increment, int page_increment, + int page_size) override + { + disable_notify_events(); + gtk_adjustment_configure(m_pAdjustment, value, lower, upper, step_increment, page_increment, page_size); + enable_notify_events(); + } + + virtual int adjustment_get_value() const override + { + return gtk_adjustment_get_value(m_pAdjustment); + } + + virtual void adjustment_set_value(int value) override + { + disable_notify_events(); + gtk_adjustment_set_value(m_pAdjustment, value); + enable_notify_events(); + } + + virtual int adjustment_get_upper() const override + { + return gtk_adjustment_get_upper(m_pAdjustment); + } + + virtual void adjustment_set_upper(int upper) override + { + disable_notify_events(); + gtk_adjustment_set_upper(m_pAdjustment, upper); + enable_notify_events(); + } + + virtual int adjustment_get_lower() const override + { + return gtk_adjustment_get_lower(m_pAdjustment); + } + + virtual void adjustment_set_lower(int lower) override + { + disable_notify_events(); + gtk_adjustment_set_lower(m_pAdjustment, lower); + enable_notify_events(); + } + + virtual int adjustment_get_page_size() const override + { + return gtk_adjustment_get_page_size(m_pAdjustment); + } + + virtual void adjustment_set_page_size(int size) override + { + gtk_adjustment_set_page_size(m_pAdjustment, size); + } + + virtual int adjustment_get_page_increment() const override + { + return gtk_adjustment_get_page_increment(m_pAdjustment); + } + + virtual void adjustment_set_page_increment(int size) override + { + gtk_adjustment_set_page_increment(m_pAdjustment, size); + } + + virtual int adjustment_get_step_increment() const override + { + return gtk_adjustment_get_step_increment(m_pAdjustment); + } + + virtual void adjustment_set_step_increment(int size) override + { + gtk_adjustment_set_step_increment(m_pAdjustment, size); + } + + virtual void disable_notify_events() override + { + g_signal_handler_block(m_pAdjustment, m_nAdjustChangedSignalId); + GtkInstanceWidget::disable_notify_events(); + } + + virtual void enable_notify_events() override + { + GtkInstanceWidget::enable_notify_events(); + g_signal_handler_unblock(m_pAdjustment, m_nAdjustChangedSignalId); + } + + virtual ScrollType get_scroll_type() const override + { + // tdf#153049 want a mousewheel spin to be treated as DontKnow + return has_grab() ? ScrollType::Drag : ScrollType::DontKnow; + } + + virtual int get_scroll_thickness() const override + { + if (gtk_orientable_get_orientation(GTK_ORIENTABLE(m_pScrollbar)) == GTK_ORIENTATION_HORIZONTAL) + return gtk_widget_get_allocated_height(GTK_WIDGET(m_pScrollbar)); + return gtk_widget_get_allocated_width(GTK_WIDGET(m_pScrollbar)); + } + + virtual void set_scroll_thickness(int nThickness) override + { + GtkStyleContext *pStyleContext = gtk_widget_get_style_context(GTK_WIDGET(m_pScrollbar)); + + if (m_pThicknessCssProvider) + { + gtk_style_context_remove_provider(pStyleContext, GTK_STYLE_PROVIDER(m_pThicknessCssProvider)); + m_pThicknessCssProvider = nullptr; + } + + m_pThicknessCssProvider = gtk_css_provider_new(); + int nSlider = nThickness > 6 ? nThickness - 6 : 1; + const OString sData = "slider { min-height: " + OString::number(nSlider) + "px;" + " min-width: " + OString::number(nSlider) + "px; }"; + css_provider_load_from_data(m_pThicknessCssProvider, sData.getStr(), sData.getLength()); + gtk_style_context_add_provider(pStyleContext, GTK_STYLE_PROVIDER(m_pThicknessCssProvider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + + if (gtk_orientable_get_orientation(GTK_ORIENTABLE(m_pScrollbar)) == GTK_ORIENTATION_HORIZONTAL) + gtk_widget_set_size_request(GTK_WIDGET(m_pScrollbar), -1, nThickness); + else + gtk_widget_set_size_request(GTK_WIDGET(m_pScrollbar), nThickness, -1); + } + + virtual ~GtkInstanceScrollbar() override + { + g_signal_handler_disconnect(m_pAdjustment, m_nAdjustChangedSignalId); + if (m_pThicknessCssProvider) + { + GtkStyleContext *pStyleContext = gtk_widget_get_style_context(GTK_WIDGET(m_pScrollbar)); + gtk_style_context_remove_provider(pStyleContext, GTK_STYLE_PROVIDER(m_pThicknessCssProvider)); + } + } +}; + +} + +namespace { + +class GtkInstanceNotebook : public GtkInstanceWidget, public virtual weld::Notebook +{ +private: + GtkNotebook* m_pNotebook; + GtkBox* m_pOverFlowBox; + GtkNotebook* m_pOverFlowNotebook; + gulong m_nSwitchPageSignalId; + gulong m_nOverFlowSwitchPageSignalId; +#if GTK_CHECK_VERSION(4, 0, 0) + NotifyingLayout* m_pLayout; +#else + gulong m_nNotebookSizeAllocateSignalId; + gulong m_nFocusSignalId; +#endif + 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); + OUString 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) + { + OUString 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, u"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(); + + // coverity[pass_freed_arg : FALSE] - m_pNotebook is not freed here + gtk_notebook_set_current_page(m_pNotebook, nNewPage); + + enable_notify_events(); + + // trigger main notebook switch-page callback + OUString sNewIdent(get_page_ident(m_pNotebook, nNewPage)); + m_aEnterPageHdl.Call(sNewIdent); + } + + static OUString get_page_ident(GtkNotebook *pNotebook, guint nPage) + { + const GtkWidget* pTabWidget = gtk_notebook_get_tab_label(pNotebook, gtk_notebook_get_nth_page(pNotebook, nPage)); + return ::get_buildable_id(GTK_BUILDABLE(pTabWidget)); + } + + static gint get_page_number(GtkNotebook *pNotebook, std::u16string_view ident) + { + 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)); + OUString sBuildableName = ::get_buildable_id(GTK_BUILDABLE(pTabWidget)); + if (sBuildableName == ident) + return i; + } + return -1; + } + + int remove_page(GtkNotebook *pNotebook, std::u16string_view ident) + { + disable_notify_events(); + int nPageNumber = get_page_number(pNotebook, ident); + assert(nPageNumber != -1 && "asked to remove page that doesn't exist"); + 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(); + ::set_buildable_id(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 OUString& rIdent, const OUString& rLabel, GtkWidget *pChild, int nPos) + { + disable_notify_events(); + + GtkWidget *pTabWidget = gtk_label_new_with_mnemonic(MapToGtkAccelerator(rLabel).getStr()); + ::set_buildable_id(GTK_BUILDABLE(pTabWidget), rIdent); + gtk_notebook_insert_page(pNotebook, pChild, pTabWidget, nPos); + gtk_widget_show(pChild); + gtk_widget_show(pTabWidget); + + if (nPos != -1) + { + unsigned int nPageIndex = static_cast<unsigned int>(nPos); + if (nPageIndex < m_aPages.size()) + m_aPages.insert(m_aPages.begin() + nPageIndex, nullptr); + } + + enable_notify_events(); + } + + 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)); + container_add(pParent, GTK_WIDGET(m_pOverFlowBox)); +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_box_append(m_pOverFlowBox, GTK_WIDGET(m_pOverFlowNotebook)); +#else + gtk_box_pack_start(m_pOverFlowBox, GTK_WIDGET(m_pOverFlowNotebook), false, false, 0); +#endif + g_object_ref(m_pNotebook); + container_remove(pParent, GTK_WIDGET(m_pNotebook)); +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_box_append(m_pOverFlowBox, GTK_WIDGET(m_pNotebook)); +#else + gtk_box_pack_start(m_pOverFlowBox, GTK_WIDGET(m_pNotebook), true, true, 0); +#endif + // coverity[freed_arg : FALSE] - this does not free m_pNotebook , it is reffed by pParent + 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 + // coverity[pass_freed_arg : FALSE] - m_pNotebook is not freed here + gtk_notebook_set_scrollable(m_pNotebook, false); + +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_freeze_child_notify(GTK_WIDGET(m_pNotebook)); + gtk_widget_freeze_child_notify(GTK_WIDGET(m_pOverFlowNotebook)); +#else + g_object_freeze_notify(G_OBJECT(m_pNotebook)); + g_object_freeze_notify(G_OBJECT(m_pOverFlowNotebook)); +#endif + + 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) + { + OUString 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, u"useless"); + +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_thaw_child_notify(GTK_WIDGET(m_pOverFlowNotebook)); + gtk_widget_thaw_child_notify(GTK_WIDGET(m_pNotebook)); +#else + g_object_thaw_notify(G_OBJECT(m_pOverFlowNotebook)); + g_object_thaw_notify(G_OBJECT(m_pNotebook)); +#endif + + 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_CHECK_VERSION(4, 0, 0) + bool bTabVisible = gtk_widget_get_child_visible(gtk_widget_get_parent(pTabWidget)); +#else + bool bTabVisible = gtk_widget_get_child_visible(pTabWidget); +#endif + if (!bTabVisible) + { + m_nLaunchSplitTimeoutId = g_timeout_add_full(G_PRIORITY_HIGH_IDLE, 0, reinterpret_cast<GSourceFunc>(launch_split_notebooks), this, nullptr); + break; + } + } + } + enable_notify_events(); + } + +#if GTK_CHECK_VERSION(4, 0, 0) + DECL_LINK(SizeAllocateHdl, void*, void); +#else + static void signalSizeAllocate(GtkWidget*, GdkRectangle*, gpointer widget) + { + GtkInstanceNotebook* pThis = static_cast<GtkInstanceNotebook*>(widget); + pThis->signal_notebook_size_allocate(); + } +#endif + + 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; + } + +#if !GTK_CHECK_VERSION(4, 0, 0) + 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; + } +#endif + + // 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) + : GtkInstanceWidget(GTK_WIDGET(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)) +#if GTK_CHECK_VERSION(4, 0, 0) + , m_pLayout(nullptr) +#else + , m_nNotebookSizeAllocateSignalId(0) + , m_nFocusSignalId(g_signal_connect(pNotebook, "focus", G_CALLBACK(signalFocus), this)) +#endif + , 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) + { +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_add_events(GTK_WIDGET(pNotebook), GDK_SCROLL_MASK); +#endif + gint nPages = gtk_notebook_get_n_pages(m_pNotebook); + if (nPages > 6) + { +#if !GTK_CHECK_VERSION(4, 0, 0) + m_nNotebookSizeAllocateSignalId = g_signal_connect_after(pNotebook, "size-allocate", G_CALLBACK(signalSizeAllocate), this); +#else + m_pLayout = NOTIFYING_LAYOUT(g_object_new(notifying_layout_get_type(), nullptr)); + notifying_layout_start_watch(m_pLayout, GTK_WIDGET(pNotebook), LINK(this, GtkInstanceNotebook, SizeAllocateHdl)); +#endif + } + 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 via the 'overflow' class themed by global CreateStyleProvider + GtkStyleContext *pNotebookContext = gtk_widget_get_style_context(GTK_WIDGET(m_pOverFlowNotebook)); + gtk_style_context_add_class(pNotebookContext, "overflow"); + } + + 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 OUString 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 OUString get_current_page_ident() const override + { + const int nPage = get_current_page(); + return nPage != -1 ? get_page_ident(nPage) : OUString(); + } + + virtual int get_page_index(const OUString& rIdent) const override + { + 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; + } + } + } + + virtual weld::Container* get_page(const OUString& rIdent) const override + { + int nPage = get_page_index(rIdent); + if (nPage < 0) + return nullptr; + + GtkWidget* pChild; + if (m_bOverFlowBoxIsStart) + { + auto nOverFlowLen = m_bOverFlowBoxActive ? gtk_notebook_get_n_pages(m_pOverFlowNotebook) - 1 : 0; + if (nPage < nOverFlowLen) + pChild = gtk_notebook_get_nth_page(m_pOverFlowNotebook, nPage); + else + { + nPage -= nOverFlowLen; + pChild = gtk_notebook_get_nth_page(m_pNotebook, nPage); + } + } + else + { + auto nMainLen = gtk_notebook_get_n_pages(m_pNotebook); + if (nPage < nMainLen) + pChild = gtk_notebook_get_nth_page(m_pNotebook, nPage); + else + { + nPage -= nMainLen; + pChild = 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 !GTK_CHECK_VERSION(4, 0, 0) + if (!m_aPages[nPageIndex]) + m_aPages[nPageIndex].reset(new GtkInstanceContainer(GTK_CONTAINER(pChild), m_pBuilder, false)); +#else + if (!m_aPages[nPageIndex]) + m_aPages[nPageIndex].reset(new GtkInstanceContainer(pChild, m_pBuilder, false)); +#endif + 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 OUString& rIdent) override + { + gint nPage = get_page_index(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 OUString& 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 OUString& 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 set_show_tabs(bool bShow) override + { + if (m_bOverFlowBoxActive) + { + unsplit_notebooks(); + reset_split_data(); + } + + gtk_notebook_set_show_tabs(m_pNotebook, bShow); + gtk_notebook_set_show_tabs(m_pOverFlowNotebook, bShow); + } + + virtual void disable_notify_events() override + { + g_signal_handler_block(m_pNotebook, m_nSwitchPageSignalId); +#if !GTK_CHECK_VERSION(4, 0, 0) + g_signal_handler_block(m_pNotebook, m_nFocusSignalId); +#endif + g_signal_handler_block(m_pNotebook, m_nChangeCurrentPageId); + g_signal_handler_block(m_pOverFlowNotebook, m_nOverFlowSwitchPageSignalId); +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_freeze_child_notify(GTK_WIDGET(m_pOverFlowNotebook)); +#endif + g_object_freeze_notify(G_OBJECT(m_pOverFlowNotebook)); + GtkInstanceWidget::disable_notify_events(); + } + + virtual void enable_notify_events() override + { + GtkInstanceWidget::enable_notify_events(); + g_object_thaw_notify(G_OBJECT(m_pOverFlowNotebook)); +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_thaw_child_notify(GTK_WIDGET(m_pOverFlowNotebook)); +#endif + g_signal_handler_unblock(m_pOverFlowNotebook, m_nOverFlowSwitchPageSignalId); + g_signal_handler_unblock(m_pNotebook, m_nSwitchPageSignalId); +#if !GTK_CHECK_VERSION(4, 0, 0) + g_signal_handler_unblock(m_pNotebook, m_nFocusSignalId); +#endif + 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 OUString& 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 OUString& 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 !GTK_CHECK_VERSION(4, 0, 0) + if (m_nNotebookSizeAllocateSignalId) + g_signal_handler_disconnect(m_pNotebook, m_nNotebookSizeAllocateSignalId); +#else + if (m_pLayout) + { + // put it back how we found it initially + notifying_layout_stop_watch(m_pLayout); + } +#endif + g_signal_handler_disconnect(m_pNotebook, m_nSwitchPageSignalId); +#if !GTK_CHECK_VERSION(4, 0, 0) + g_signal_handler_disconnect(m_pNotebook, m_nFocusSignalId); +#endif + g_signal_handler_disconnect(m_pNotebook, m_nChangeCurrentPageId); + g_signal_handler_disconnect(m_pOverFlowNotebook, m_nOverFlowSwitchPageSignalId); +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_destroy(GTK_WIDGET(m_pOverFlowNotebook)); +#else + GtkWidget* pOverFlowWidget = GTK_WIDGET(m_pOverFlowNotebook); + g_clear_pointer(&pOverFlowWidget, gtk_widget_unparent); +#endif + if (!m_pOverFlowBox) + return; + + // put it back to how we found it initially + GtkWidget* pParent = gtk_widget_get_parent(GTK_WIDGET(m_pOverFlowBox)); + g_object_ref(m_pNotebook); + container_remove(GTK_WIDGET(m_pOverFlowBox), GTK_WIDGET(m_pNotebook)); + container_add(GTK_WIDGET(pParent), GTK_WIDGET(m_pNotebook)); + g_object_unref(m_pNotebook); + +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_destroy(GTK_WIDGET(m_pOverFlowBox)); +#else + GtkWidget* pOverFlowBox = GTK_WIDGET(m_pOverFlowBox); + g_clear_pointer(&pOverFlowBox, gtk_widget_unparent); +#endif + } +}; + +#if GTK_CHECK_VERSION(4, 0, 0) +IMPL_LINK_NOARG(GtkInstanceNotebook, SizeAllocateHdl, void*, void) +{ + signal_notebook_size_allocate(); +} +#endif + + +OUString vcl_font_to_css(const vcl::Font& rFont) +{ + OUStringBuffer sCSS( + "font-family: \"" + rFont.GetFamilyName() + "\"; " + "font-size: " + OUString::number(rFont.GetFontSize().Height()) + "pt; "); + switch (rFont.GetItalic()) + { + case ITALIC_NONE: + sCSS.append("font-style: normal; "); + break; + case ITALIC_NORMAL: + sCSS.append("font-style: italic; "); + break; + case ITALIC_OBLIQUE: + sCSS.append("font-style: oblique; "); + break; + default: + break; + } + switch (rFont.GetWeight()) + { + case WEIGHT_ULTRALIGHT: + sCSS.append("font-weight: 200; "); + break; + case WEIGHT_LIGHT: + sCSS.append("font-weight: 300; "); + break; + case WEIGHT_NORMAL: + sCSS.append("font-weight: 400; "); + break; + case WEIGHT_BOLD: + sCSS.append("font-weight: 700; "); + break; + case WEIGHT_ULTRABOLD: + sCSS.append("font-weight: 800; "); + break; + default: + break; + } + switch (rFont.GetWidthType()) + { + case WIDTH_ULTRA_CONDENSED: + sCSS.append("font-stretch: ultra-condensed; "); + break; + case WIDTH_EXTRA_CONDENSED: + sCSS.append("font-stretch: extra-condensed; "); + break; + case WIDTH_CONDENSED: + sCSS.append("font-stretch: condensed; "); + break; + case WIDTH_SEMI_CONDENSED: + sCSS.append("font-stretch: semi-condensed; "); + break; + case WIDTH_NORMAL: + sCSS.append("font-stretch: normal; "); + break; + case WIDTH_SEMI_EXPANDED: + sCSS.append("font-stretch: semi-expanded; "); + break; + case WIDTH_EXPANDED: + sCSS.append("font-stretch: expanded; "); + break; + case WIDTH_EXTRA_EXPANDED: + sCSS.append("font-stretch: extra-expanded; "); + break; + case WIDTH_ULTRA_EXPANDED: + sCSS.append("font-stretch: ultra-expanded; "); + break; + default: + break; + } + return sCSS.toString(); +} + +void update_attr_list(PangoAttrList* pAttrList, const vcl::Font& rFont) +{ + pango_attr_list_change(pAttrList, pango_attr_family_new(OUStringToOString(rFont.GetFamilyName(), RTL_TEXTENCODING_UTF8).getStr())); + pango_attr_list_change(pAttrList, pango_attr_size_new(rFont.GetFontSize().Height() * PANGO_SCALE)); + + switch (rFont.GetItalic()) + { + case ITALIC_NONE: + pango_attr_list_change(pAttrList, pango_attr_style_new(PANGO_STYLE_NORMAL)); + break; + case ITALIC_NORMAL: + pango_attr_list_change(pAttrList, pango_attr_style_new(PANGO_STYLE_ITALIC)); + break; + case ITALIC_OBLIQUE: + pango_attr_list_change(pAttrList, pango_attr_style_new(PANGO_STYLE_OBLIQUE)); + break; + default: + break; + } + switch (rFont.GetWeight()) + { + case WEIGHT_ULTRALIGHT: + pango_attr_list_change(pAttrList, pango_attr_weight_new(PANGO_WEIGHT_ULTRALIGHT)); + break; + case WEIGHT_LIGHT: + pango_attr_list_change(pAttrList, pango_attr_weight_new(PANGO_WEIGHT_LIGHT)); + break; + case WEIGHT_NORMAL: + pango_attr_list_change(pAttrList, pango_attr_weight_new(PANGO_WEIGHT_NORMAL)); + break; + case WEIGHT_BOLD: + pango_attr_list_change(pAttrList, pango_attr_weight_new(PANGO_WEIGHT_BOLD)); + break; + case WEIGHT_ULTRABOLD: + pango_attr_list_change(pAttrList, pango_attr_weight_new(PANGO_WEIGHT_ULTRABOLD)); + break; + default: + break; + } + switch (rFont.GetWidthType()) + { + case WIDTH_ULTRA_CONDENSED: + pango_attr_list_change(pAttrList, pango_attr_stretch_new(PANGO_STRETCH_ULTRA_CONDENSED)); + break; + case WIDTH_EXTRA_CONDENSED: + pango_attr_list_change(pAttrList, pango_attr_stretch_new(PANGO_STRETCH_EXTRA_CONDENSED)); + break; + case WIDTH_CONDENSED: + pango_attr_list_change(pAttrList, pango_attr_stretch_new(PANGO_STRETCH_CONDENSED)); + break; + case WIDTH_SEMI_CONDENSED: + pango_attr_list_change(pAttrList, pango_attr_stretch_new(PANGO_STRETCH_SEMI_CONDENSED)); + break; + case WIDTH_NORMAL: + pango_attr_list_change(pAttrList, pango_attr_stretch_new(PANGO_STRETCH_NORMAL)); + break; + case WIDTH_SEMI_EXPANDED: + pango_attr_list_change(pAttrList, pango_attr_stretch_new(PANGO_STRETCH_SEMI_EXPANDED)); + break; + case WIDTH_EXPANDED: + pango_attr_list_change(pAttrList, pango_attr_stretch_new(PANGO_STRETCH_EXPANDED)); + break; + case WIDTH_EXTRA_EXPANDED: + pango_attr_list_change(pAttrList, pango_attr_stretch_new(PANGO_STRETCH_EXTRA_EXPANDED)); + break; + case WIDTH_ULTRA_EXPANDED: + pango_attr_list_change(pAttrList, pango_attr_stretch_new(PANGO_STRETCH_ULTRA_EXPANDED)); + break; + default: + break; + } +} + +gboolean filter_pango_attrs(PangoAttribute *attr, gpointer data) +{ + PangoAttrType* pFilterAttrs = static_cast<PangoAttrType*>(data); + while (*pFilterAttrs) + { + if (attr->klass->type == *pFilterAttrs) + return true; + ++pFilterAttrs; + } + return false; +} + +void set_font(GtkLabel* pLabel, const vcl::Font& rFont) +{ + PangoAttrList* pOrigList = gtk_label_get_attributes(pLabel); + PangoAttrList* pAttrList = pOrigList ? pango_attr_list_copy(pOrigList) : pango_attr_list_new(); + + if (pOrigList) + { + // tdf#143443 remove both PANGO_ATTR_ABSOLUTE_SIZE and PANGO_ATTR_SIZE + // because pango_attr_list_change(..., pango_attr_size_new...) isn't + // sufficient on its own to ensure a new size sticks. + PangoAttrType aFilterAttrs[] = {PANGO_ATTR_ABSOLUTE_SIZE, PANGO_ATTR_SIZE, PANGO_ATTR_INVALID}; + PangoAttrList* pRemovedAttrs = pango_attr_list_filter(pAttrList, filter_pango_attrs, &aFilterAttrs); + pango_attr_list_unref(pRemovedAttrs); + } + + update_attr_list(pAttrList, rFont); + gtk_label_set_attributes(pLabel, pAttrList); + pango_attr_list_unref(pAttrList); +} + +} + +namespace { + +class WidgetBackground +{ +private: + GtkWidget* m_pWidget; + GtkCssProvider* m_pCustomCssProvider; + std::unique_ptr<utl::TempFileNamed> m_xCustomImage; + +public: + // See: https://developer.gnome.org/Buttons/ + void use_custom_content(const VirtualDevice* pDevice) + { + GtkStyleContext *pWidgetContext = gtk_widget_get_style_context(m_pWidget); + + if (m_pCustomCssProvider) + { + gtk_style_context_remove_provider(pWidgetContext, GTK_STYLE_PROVIDER(m_pCustomCssProvider)); + m_pCustomCssProvider = nullptr; + } + + m_xCustomImage.reset(); + + if (!pDevice) + return; + + m_xCustomImage.reset(new utl::TempFileNamed); + m_xCustomImage->EnableKillingFile(true); + + cairo_surface_t* surface = get_underlying_cairo_surface(*pDevice); + Size aSize = pDevice->GetOutputSizePixel(); + cairo_surface_write_to_png(surface, OUStringToOString(m_xCustomImage->GetFileName(), osl_getThreadTextEncoding()).getStr()); + + m_pCustomCssProvider = gtk_css_provider_new(); + OUString aBuffer = "* { background-image: url(\"" + m_xCustomImage->GetURL() + "\"); " + "background-size: " + OUString::number(aSize.Width()) + "px " + OUString::number(aSize.Height()) + "px; " + "border-radius: 0; border-width: 0; }"; + OString aResult = OUStringToOString(aBuffer, RTL_TEXTENCODING_UTF8); + css_provider_load_from_data(m_pCustomCssProvider, aResult.getStr(), aResult.getLength()); + gtk_style_context_add_provider(pWidgetContext, GTK_STYLE_PROVIDER(m_pCustomCssProvider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + } + +public: + WidgetBackground(GtkWidget* pWidget) + : m_pWidget(pWidget) + , m_pCustomCssProvider(nullptr) + { + } + + ~WidgetBackground() + { + if (m_pCustomCssProvider) + use_custom_content(nullptr); + assert(!m_pCustomCssProvider); + } +}; + +class WidgetFont +{ +private: + GtkWidget* m_pWidget; + GtkCssProvider* m_pFontCssProvider; + std::unique_ptr<vcl::Font> m_xFont; +public: + WidgetFont(GtkWidget* pWidget) + : m_pWidget(pWidget) + , m_pFontCssProvider(nullptr) + { + } + + void use_custom_font(const vcl::Font* pFont, std::u16string_view rCSSSelector) + { + GtkStyleContext *pWidgetContext = gtk_widget_get_style_context(m_pWidget); + if (m_pFontCssProvider) + { + gtk_style_context_remove_provider(pWidgetContext, GTK_STYLE_PROVIDER(m_pFontCssProvider)); + m_pFontCssProvider = nullptr; + } + + m_xFont.reset(); + + if (!pFont) + return; + + m_xFont.reset(new vcl::Font(*pFont)); + m_pFontCssProvider = gtk_css_provider_new(); + OUString aBuffer = rCSSSelector + OUString::Concat(" { ") + vcl_font_to_css(*pFont) + OUString::Concat(" }"); + OString aResult = OUStringToOString(aBuffer, RTL_TEXTENCODING_UTF8); + css_provider_load_from_data(m_pFontCssProvider, aResult.getStr(), aResult.getLength()); + gtk_style_context_add_provider(pWidgetContext, GTK_STYLE_PROVIDER(m_pFontCssProvider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + } + + const vcl::Font* get_custom_font() const + { + return m_xFont.get(); + } + + ~WidgetFont() + { + if (m_pFontCssProvider) + use_custom_font(nullptr, u""); + assert(!m_pFontCssProvider); + } +}; + +class GtkInstanceButton : public GtkInstanceWidget, public virtual weld::Button +{ +private: + GtkButton* m_pButton; + gulong m_nSignalId; + std::optional<vcl::Font> m_xFont; + WidgetBackground m_aCustomBackground; + + static void signalClicked(GtkButton*, gpointer widget) + { + GtkInstanceButton* pThis = static_cast<GtkInstanceButton*>(widget); + SolarMutexGuard aGuard; + pThis->signal_clicked(); + } + + virtual void ensureMouseEventWidget() override + { + // The GtkButton is sufficient to get mouse events without an intermediate GtkEventBox + if (!m_pMouseEventBox) + m_pMouseEventBox = m_pWidget; + } + +public: + GtkInstanceButton(GtkButton* pButton, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : GtkInstanceWidget(GTK_WIDGET(pButton), pBuilder, bTakeOwnership) + , m_pButton(pButton) + , m_nSignalId(g_signal_connect(pButton, "clicked", G_CALLBACK(signalClicked), this)) + , m_aCustomBackground(GTK_WIDGET(pButton)) + { + g_object_set_data(G_OBJECT(m_pButton), "g-lo-GtkInstanceButton", this); + } + + virtual void set_label(const OUString& rText) override + { + ::button_set_label(m_pButton, rText); + } + + virtual void set_image(VirtualDevice* pDevice) override + { + ::button_set_image(m_pButton, pDevice); + } + + virtual void set_from_icon_name(const OUString& rIconName) override + { + ::button_set_from_icon_name(m_pButton, rIconName); + } + + virtual void set_image(const css::uno::Reference<css::graphic::XGraphic>& rImage) override + { + ::button_set_image(m_pButton, rImage); + } + + virtual void set_custom_button(VirtualDevice* pDevice) override + { + m_aCustomBackground.use_custom_content(pDevice); + } + + virtual OUString get_label() const override + { + return ::button_get_label(m_pButton); + } + + virtual void set_font(const vcl::Font& rFont) override + { + m_xFont = rFont; + GtkLabel* pChild = ::get_label_widget(GTK_WIDGET(m_pButton)); + ::set_font(pChild, rFont); + } + + virtual vcl::Font get_font() override + { + if (m_xFont) + return *m_xFont; + return GtkInstanceWidget::get_font(); + } + + // 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); + GtkInstanceWidget::disable_notify_events(); + } + + virtual void enable_notify_events() override + { + GtkInstanceWidget::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) +{ + SolarMutexGuard aGuard; + + 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; + + if (aFunc) + 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() +{ + // tdf#150723 "run" will make the dialog visible so drop m_aPosWhileInvis like show + m_aPosWhileInvis.reset(); + +#if !GTK_CHECK_VERSION(4, 0, 0) + if (GTK_IS_DIALOG(m_pDialog)) + sort_native_button_order(GTK_BOX(gtk_dialog_get_action_area(GTK_DIALOG(m_pDialog)))); +#endif + 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 +{ +protected: + GtkToggleButton* m_pToggleButton; + gulong m_nToggledSignalId; +private: + 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_nToggledSignalId(g_signal_connect(m_pToggleButton, "toggled", G_CALLBACK(signalToggled), this)) + { + } + + virtual void set_active(bool active) override + { + disable_notify_events(); + set_inconsistent(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 + { +#if GTK_CHECK_VERSION(4, 0, 0) + if (inconsistent) + gtk_widget_set_state_flags(GTK_WIDGET(m_pToggleButton), GTK_STATE_FLAG_INCONSISTENT, false); + else + gtk_widget_unset_state_flags(GTK_WIDGET(m_pToggleButton), GTK_STATE_FLAG_INCONSISTENT); +#else + gtk_toggle_button_set_inconsistent(m_pToggleButton, inconsistent); +#endif + } + + virtual bool get_inconsistent() const override + { +#if GTK_CHECK_VERSION(4, 0, 0) + return gtk_widget_get_state_flags(GTK_WIDGET(m_pToggleButton)) & GTK_STATE_FLAG_INCONSISTENT; +#else + return gtk_toggle_button_get_inconsistent(m_pToggleButton); +#endif + } + + virtual void disable_notify_events() override + { + g_signal_handler_block(m_pToggleButton, m_nToggledSignalId); + GtkInstanceButton::disable_notify_events(); + } + + virtual void enable_notify_events() override + { + GtkInstanceButton::enable_notify_events(); + g_signal_handler_unblock(m_pToggleButton, m_nToggledSignalId); + } + + virtual ~GtkInstanceToggleButton() override + { + g_signal_handler_disconnect(m_pToggleButton, m_nToggledSignalId); + } +}; + +} + +#if !GTK_CHECK_VERSION(4, 0, 0) + +namespace { + +void do_grab(GtkWidget* pWidget) +{ + GdkDisplay *pDisplay = gtk_widget_get_display(pWidget); + GdkSeat* pSeat = gdk_display_get_default_seat(pDisplay); + gdk_seat_grab(pSeat, widget_get_surface(pWidget), + GDK_SEAT_CAPABILITY_KEYBOARD, true, nullptr, nullptr, nullptr, nullptr); +} + +void do_ungrab(GtkWidget* pWidget) +{ + GdkDisplay *pDisplay = gtk_widget_get_display(pWidget); + GdkSeat* pSeat = gdk_display_get_default_seat(pDisplay); + gdk_seat_ungrab(pSeat); +} + +GtkPositionType show_menu_older_gtk(GtkWidget* pMenuButton, GtkWindow* pMenu, const GdkRectangle& rAnchor, + weld::Placement ePlace, bool bTryShrink) +{ + //place the toplevel just below its launcher button + GtkWidget* pToplevel = widget_get_toplevel(pMenuButton); + gtk_coord x, y, absx, absy; + gtk_widget_translate_coordinates(pMenuButton, pToplevel, rAnchor.x, rAnchor.y, &x, &y); + GdkSurface* pWindow = widget_get_surface(pToplevel); + gdk_window_get_position(pWindow, &absx, &absy); + + x += absx; + y += absy; + + gint nButtonHeight = rAnchor.height; + gint nButtonWidth = rAnchor.width; + if (ePlace == weld::Placement::Under) + y += nButtonHeight; + else + x += nButtonWidth; + + gtk_window_group_add_window(gtk_window_get_group(GTK_WINDOW(pToplevel)), pMenu); + gtk_window_set_transient_for(pMenu, GTK_WINDOW(pToplevel)); + + gint nMenuWidth, nMenuHeight; + gtk_widget_get_size_request(GTK_WIDGET(pMenu), &nMenuWidth, &nMenuHeight); + + if (nMenuWidth == -1 || nMenuHeight == -1) + { + GtkRequisition req; + gtk_widget_get_preferred_size(GTK_WIDGET(pMenu), nullptr, &req); + if (nMenuWidth == -1) + nMenuWidth = req.width; + if (nMenuHeight == -1) + nMenuHeight = req.height; + } + + bool bSwapForRTL = SwapForRTL(pMenuButton); + if (bSwapForRTL) + { + if (ePlace == weld::Placement::Under) + x += nButtonWidth; + else + x -= nButtonWidth; + x -= nMenuWidth; + } + + 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); + aWorkArea.AdjustLeft(8); + aWorkArea.AdjustRight(-8); + + GtkPositionType ePosUsed; + + if (ePlace == weld::Placement::Under) + { + gint endx = x + nMenuWidth; + if (endx > aWorkArea.Right()) + x -= endx - aWorkArea.Right(); + if (x < 0) + x = 0; + + ePosUsed = GTK_POS_BOTTOM; + gint endy = y + nMenuHeight; + gint nMissingBelow = endy - aWorkArea.Bottom(); + if (nMissingBelow > 0) + { + gint nNewY = y - (nButtonHeight + nMenuHeight); + gint nMissingAbove = aWorkArea.Top() - nNewY; + if (nMissingAbove > 0) + { + if (bTryShrink) + { + 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 + { + if (nMissingBelow <= nMissingAbove) + y -= nMissingBelow; + else + { + y = aWorkArea.Top(); + ePosUsed = GTK_POS_TOP; + } + } + } + else + { + y = nNewY; + ePosUsed = GTK_POS_TOP; + } + } + } + else + { + if (!bSwapForRTL) + { + ePosUsed = GTK_POS_RIGHT; + gint endx = x + nMenuWidth; + gint nMissingAfter = endx - aWorkArea.Right(); + if (nMissingAfter > 0) + { + gint nNewX = x - (nButtonWidth + nMenuWidth); + if (nNewX >= aWorkArea.Left()) + { + x = nNewX; + ePosUsed = GTK_POS_LEFT; + } + } + } + else + { + ePosUsed = GTK_POS_LEFT; + gint startx = x; + gint nMissingBefore = aWorkArea.Left() - startx; + if (nMissingBefore > 0) + { + gint nNewX = x + (nButtonWidth + nMenuWidth); + if (nNewX + nMenuWidth < aWorkArea.Right()) + { + x = nNewX; + ePosUsed = GTK_POS_RIGHT; + } + } + } + } + + gtk_window_move(pMenu, x, y); + + return ePosUsed; +} + +bool show_menu_newer_gtk(GtkWidget* pComboBox, GtkWindow* pMenu, const GdkRectangle &rAnchor, + weld::Placement ePlace, bool bTryShrink) +{ + 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; + + // 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; + + //place the toplevel just below its launcher button + GtkWidget* pToplevel = widget_get_toplevel(pComboBox); + gtk_coord x, y; + gtk_widget_translate_coordinates(pComboBox, pToplevel, rAnchor.x, rAnchor.y, &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)); + + bool bSwapForRTL = SwapForRTL(GTK_WIDGET(pComboBox)); + + GdkGravity rect_anchor; + GdkGravity menu_anchor; + + if (ePlace == weld::Placement::Under) + { + rect_anchor = !bSwapForRTL ? GDK_GRAVITY_SOUTH_WEST : GDK_GRAVITY_SOUTH_EAST; + menu_anchor = !bSwapForRTL ? GDK_GRAVITY_NORTH_WEST : GDK_GRAVITY_NORTH_EAST; + } + else + { + rect_anchor = !bSwapForRTL ? GDK_GRAVITY_NORTH_EAST : GDK_GRAVITY_NORTH_WEST; + menu_anchor = !bSwapForRTL ? GDK_GRAVITY_NORTH_WEST : GDK_GRAVITY_NORTH_EAST; + } + + GdkAnchorHints anchor_hints = static_cast<GdkAnchorHints>(GDK_ANCHOR_FLIP | GDK_ANCHOR_SLIDE); + if (bTryShrink) + anchor_hints = static_cast<GdkAnchorHints>(anchor_hints | GDK_ANCHOR_RESIZE); + GdkRectangle rect {x, y, rAnchor.width, rAnchor.height}; + GdkSurface* toplevel = widget_get_surface(GTK_WIDGET(pMenu)); + + window_move_to_rect(toplevel, &rect, rect_anchor, menu_anchor, anchor_hints, + 0, 0); + + return true; +} + +GtkPositionType show_menu(GtkWidget* pMenuButton, GtkWindow* pMenu, const GdkRectangle& rAnchor, + weld::Placement ePlace, bool bTryShrink) +{ + // 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 = 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, rAnchor, ePlace, bTryShrink)) + ePosUsed = show_menu_older_gtk(pMenuButton, pMenu, rAnchor, ePlace, bTryShrink); + gtk_widget_show_all(GTK_WIDGET(pMenu)); + gtk_widget_grab_focus(GTK_WIDGET(pMenu)); + do_grab(GTK_WIDGET(pMenu)); + + return ePosUsed; +} + +} +#endif + +namespace { + +#if !GTK_CHECK_VERSION(4, 0, 0) +bool button_event_is_outside(GtkWidget* pMenuHack, 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 window_x, window_y; + GdkSurface* pWindow = widget_get_surface(pMenuHack); + gdk_window_get_position(pWindow, &window_x, &window_y); + + GtkAllocation alloc; + gtk_widget_get_allocation(pMenuHack, &alloc); + gint x1 = window_x; + gint y1 = window_y; + gint x2 = x1 + alloc.width; + gint y2 = y1 + alloc.height; + + if (x > x1 && x < x2 && y > y1 && y < y2) + return false; + + return true; +} + +GtkPositionType MovePopoverContentsToWindow(GtkWidget* pPopover, GtkWindow* pMenuHack, GtkWidget* pAnchor, + const GdkRectangle& rAnchor, weld::Placement ePlace) +{ + //set border width + gtk_container_set_border_width(GTK_CONTAINER(pMenuHack), gtk_container_get_border_width(GTK_CONTAINER(pPopover))); + + //steal popover contents and smuggle into toplevel display window + GtkWidget* pChild = gtk_bin_get_child(GTK_BIN(pPopover)); + g_object_ref(pChild); + gtk_container_remove(GTK_CONTAINER(pPopover), pChild); + gtk_container_add(GTK_CONTAINER(pMenuHack), pChild); + g_object_unref(pChild); + + GtkPositionType eRet = show_menu(pAnchor, pMenuHack, rAnchor, ePlace, false); + + gtk_grab_add(GTK_WIDGET(pMenuHack)); + + GdkSurface* pSurface = widget_get_surface(GTK_WIDGET(pMenuHack)); + g_object_set_data(G_OBJECT(pSurface), "g-lo-InstancePopup", GINT_TO_POINTER(true)); + + return eRet; +} + +void MoveWindowContentsToPopover(GtkWindow* pMenuHack, GtkWidget* pPopover, GtkWidget* pAnchor) +{ + bool bHadFocus = gtk_window_has_toplevel_focus(pMenuHack); + + do_ungrab(GTK_WIDGET(pMenuHack)); + + gtk_grab_remove(GTK_WIDGET(pMenuHack)); + + gtk_widget_hide(GTK_WIDGET(pMenuHack)); + //put contents back from where the came from + GtkWidget* pChild = gtk_bin_get_child(GTK_BIN(pMenuHack)); + g_object_ref(pChild); + gtk_container_remove(GTK_CONTAINER(pMenuHack), pChild); + gtk_container_add(GTK_CONTAINER(pPopover), pChild); + g_object_unref(pChild); + + GdkSurface* pSurface = widget_get_surface(GTK_WIDGET(pMenuHack)); + g_object_set_data(G_OBJECT(pSurface), "g-lo-InstancePopup", GINT_TO_POINTER(false)); + + // so gdk_window_move_to_rect will work again the next time + gtk_widget_unrealize(GTK_WIDGET(pMenuHack)); + + gtk_widget_set_size_request(GTK_WIDGET(pMenuHack), -1, -1); + + // undo show_menu tooltip blocking + GtkWidget* pParent = widget_get_toplevel(pAnchor); + GtkSalFrame* pFrame = pParent ? GtkSalFrame::getFromWindow(pParent) : nullptr; + if (pFrame) + pFrame->UnblockTooltip(); + + if (bHadFocus) + { + GdkSurface* pParentSurface = pParent ? widget_get_surface(pParent) : nullptr; + void* pParentIsPopover = pParentSurface ? g_object_get_data(G_OBJECT(pParentSurface), "g-lo-InstancePopup") : nullptr; + if (pParentIsPopover) + do_grab(pAnchor); + gtk_widget_grab_focus(pAnchor); + } +} + +#endif + +/* four types of uses of this + a) textual menubutton, always with pan-down symbol, e.g. math, format, font, modify + b) image + text, always with additional pan-down symbol, e.g. writer, format, watermark + c) gear menu, never with text and without pan-down symbol where there is a replacement + icon for pan-down, e.g. file, new, templates + d) image, always with additional pan-down symbol, e.g. calc, insert, header/footer */ +#if !GTK_CHECK_VERSION(4, 0, 0) +class GtkInstanceMenuButton : public GtkInstanceToggleButton, public MenuHelper, public virtual weld::MenuButton +#else +class GtkInstanceMenuButton : public GtkInstanceWidget, public MenuHelper, public virtual weld::MenuButton +#endif +{ +protected: + GtkMenuButton* m_pMenuButton; +private: + GtkBox* m_pBox; +#if !GTK_CHECK_VERSION(4, 0, 0) + GtkImage* m_pImage; +#else + GtkPicture* m_pImage; + GtkToggleButton* m_pMenuButtonToggleButton; +#endif + GtkWidget* m_pLabel; +#if !GTK_CHECK_VERSION(4, 0, 0) + //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; + bool m_nButtonPressSeen; + gulong m_nSignalId; +#endif + GtkWidget* m_pPopover; +#if GTK_CHECK_VERSION(4, 0, 0) + gulong m_nToggledSignalId; + std::optional<vcl::Font> m_xFont; + WidgetBackground m_aCustomBackground; +#endif + +#if !GTK_CHECK_VERSION(4, 0, 0) + static void signalMenuButtonToggled(GtkWidget*, gpointer widget) + { + GtkInstanceMenuButton* pThis = static_cast<GtkInstanceMenuButton*>(widget); + SolarMutexGuard aGuard; + pThis->menu_toggled(); + } +#endif + +#if !GTK_CHECK_VERSION(4, 0, 0) + void menu_toggled() + { + if (!m_pMenuHack) + return; + if (!get_active()) + { + m_nButtonPressSeen = false; + MoveWindowContentsToPopover(m_pMenuHack, m_pPopover, GTK_WIDGET(m_pMenuButton)); + } + else + { + GtkWidget* pAnchor = m_pMenuHackAlign ? m_pMenuHackAlign : GTK_WIDGET(m_pMenuButton); + GdkRectangle aAnchor {0, 0, gtk_widget_get_allocated_width(pAnchor), gtk_widget_get_allocated_height(pAnchor) }; + GtkPositionType ePosUsed = MovePopoverContentsToWindow(m_pPopover, m_pMenuHack, pAnchor, aAnchor, weld::Placement::Under); + // 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); + } + } +#endif + +#if !GTK_CHECK_VERSION(4, 0, 0) + 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 if (!g_object_get_data(G_OBJECT(event->grab_window), "g-lo-InstancePopup")) // another LibreOffice popover took a grab + { + //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 signalButtonPress(GtkWidget* /*pWidget*/, GdkEventButton* /*pEvent*/, gpointer widget) + { + GtkInstanceMenuButton* pThis = static_cast<GtkInstanceMenuButton*>(widget); + pThis->m_nButtonPressSeen = true; + return false; + } + + static gboolean signalButtonRelease(GtkWidget* /*pWidget*/, GdkEventButton* pEvent, gpointer widget) + { + GtkInstanceMenuButton* pThis = static_cast<GtkInstanceMenuButton*>(widget); + if (pThis->m_nButtonPressSeen && button_event_is_outside(GTK_WIDGET(pThis->m_pMenuHack), pEvent)) + pThis->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; + } +#endif + + void ensure_image_widget() + { + if (m_pImage) + return; + +#if !GTK_CHECK_VERSION(4, 0, 0) + 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); +#else + m_pImage = GTK_PICTURE(gtk_picture_new()); + gtk_widget_set_halign(GTK_WIDGET(m_pImage), GTK_ALIGN_CENTER); + gtk_widget_set_valign(GTK_WIDGET(m_pImage), GTK_ALIGN_CENTER); + gtk_box_prepend(m_pBox, GTK_WIDGET(m_pImage)); + gtk_widget_set_halign(m_pLabel, GTK_ALIGN_START); +#endif + gtk_widget_show(GTK_WIDGET(m_pImage)); + } + + static void signalFlagsChanged(GtkToggleButton* pToggleButton, GtkStateFlags flags, gpointer widget) + { + GtkInstanceMenuButton* pThis = static_cast<GtkInstanceMenuButton*>(widget); + bool bOldChecked = flags & GTK_STATE_FLAG_CHECKED; + bool bNewChecked = gtk_widget_get_state_flags(GTK_WIDGET(pToggleButton)) & GTK_STATE_FLAG_CHECKED; + if (bOldChecked == bNewChecked) + return; + if (bOldChecked && gtk_widget_get_focus_on_click(GTK_WIDGET(pToggleButton))) + { + // grab focus back to the toggle button if the menu was popped down + gtk_widget_grab_focus(GTK_WIDGET(pToggleButton)); + } + SolarMutexGuard aGuard; + pThis->signal_toggled(); + } + +public: +#if !GTK_CHECK_VERSION(4, 0, 0) + GtkInstanceMenuButton(GtkMenuButton* pMenuButton, GtkWidget* pMenuAlign, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : GtkInstanceToggleButton(GTK_TOGGLE_BUTTON(pMenuButton), pBuilder, bTakeOwnership) + , MenuHelper(gtk_menu_button_get_popup(pMenuButton), false) +#else + GtkInstanceMenuButton(GtkMenuButton* pMenuButton, GtkWidget* pMenuAlign, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : GtkInstanceWidget(GTK_WIDGET(pMenuButton), pBuilder, bTakeOwnership) + , MenuHelper(GTK_POPOVER_MENU(gtk_menu_button_get_popover(pMenuButton)), false) +#endif + , m_pMenuButton(pMenuButton) + , m_pImage(nullptr) +#if !GTK_CHECK_VERSION(4, 0, 0) + , m_pMenuHack(nullptr) + , m_pMenuHackAlign(pMenuAlign) + , m_nButtonPressSeen(true) + , m_nSignalId(0) +#endif + , m_pPopover(nullptr) +#if GTK_CHECK_VERSION(4, 0, 0) + , m_aCustomBackground(GTK_WIDGET(pMenuButton)) +#endif + { +#if !GTK_CHECK_VERSION(4, 0, 0) + // tdf#142924 "toggled" is too late to use to populate changes to the menu, + // so use "state-flag-changed" on GTK_STATE_FLAG_CHECKED instead which + // happens before "toggled" + g_signal_handler_disconnect(m_pToggleButton, m_nToggledSignalId); + m_nToggledSignalId = g_signal_connect(m_pToggleButton, "state-flags-changed", G_CALLBACK(signalFlagsChanged), this); + + m_pLabel = gtk_bin_get_child(GTK_BIN(m_pMenuButton)); + m_pImage = get_image_widget(GTK_WIDGET(m_pMenuButton)); + m_pBox = formatMenuButton(m_pLabel); +#else + GtkWidget* pToggleButton = gtk_widget_get_first_child(GTK_WIDGET(m_pMenuButton)); + assert(GTK_IS_TOGGLE_BUTTON(pToggleButton)); + m_pMenuButtonToggleButton = GTK_TOGGLE_BUTTON(pToggleButton); + m_nToggledSignalId = g_signal_connect(m_pMenuButtonToggleButton, "state-flags-changed", G_CALLBACK(signalFlagsChanged), this); + GtkWidget* pChild = gtk_button_get_child(GTK_BUTTON(pToggleButton)); + m_pBox = GTK_IS_BOX(pChild) ? GTK_BOX(pChild) : nullptr; + m_pLabel = m_pBox ? gtk_widget_get_first_child(GTK_WIDGET(m_pBox)) : nullptr; + (void)pMenuAlign; +#endif + +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_insert_action_group(GTK_WIDGET(m_pMenuButton), "menu", m_pActionGroup); + + update_action_group_from_popover_model(); +#endif + } + + 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 GTK_CHECK_VERSION(4, 0, 0) + picture_set_from_virtual_device(m_pImage, pDevice); +#else + image_set_from_virtual_device(m_pImage, pDevice); +#endif + } + + virtual void set_image(const css::uno::Reference<css::graphic::XGraphic>& rImage) override + { + ensure_image_widget(); +#if GTK_CHECK_VERSION(4, 0, 0) + picture_set_from_xgraphic(m_pImage, rImage); +#else + image_set_from_xgraphic(m_pImage, rImage); +#endif + } + +#if GTK_CHECK_VERSION(4, 0, 0) + virtual void set_from_icon_name(const OUString& rIconName) override + { + ensure_image_widget(); + picture_set_from_icon_name(m_pImage, rIconName); + } + + virtual void set_custom_button(VirtualDevice* pDevice) override + { + m_aCustomBackground.use_custom_content(pDevice); + } + + virtual void set_inconsistent(bool inconsistent) override + { + if (inconsistent) + gtk_widget_set_state_flags(GTK_WIDGET(m_pMenuButton), GTK_STATE_FLAG_INCONSISTENT, false); + else + gtk_widget_unset_state_flags(GTK_WIDGET(m_pMenuButton), GTK_STATE_FLAG_INCONSISTENT); + } + + virtual bool get_inconsistent() const override + { + return gtk_widget_get_state_flags(GTK_WIDGET(m_pMenuButton)) & GTK_STATE_FLAG_INCONSISTENT; + } + + virtual void set_active(bool active) override + { + disable_notify_events(); + set_inconsistent(false); + if (active) + gtk_menu_button_popup(m_pMenuButton); + else + gtk_menu_button_popdown(m_pMenuButton); + enable_notify_events(); + } + + virtual bool get_active() const override + { + GtkPopover* pPopover = gtk_menu_button_get_popover(m_pMenuButton); + return pPopover && gtk_widget_get_visible(GTK_WIDGET(pPopover)); + } + + virtual void set_font(const vcl::Font& rFont) override + { + m_xFont = rFont; + GtkLabel* pChild = ::get_label_widget(GTK_WIDGET(m_pMenuButton)); + ::set_font(pChild, rFont); + } + + virtual vcl::Font get_font() override + { + if (m_xFont) + return *m_xFont; + return GtkInstanceWidget::get_font(); + } +#else + virtual void set_active(bool bActive) override + { + bool bWasActive = get_active(); + GtkInstanceToggleButton::set_active(bActive); + if (bWasActive && !bActive && gtk_widget_get_focus_on_click(GTK_WIDGET(m_pMenuButton))) + { + // grab focus back to the toggle button if the menu was popped down + gtk_widget_grab_focus(GTK_WIDGET(m_pMenuButton)); + } + } +#endif + + 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 OUString& rId) override + { + MenuHelper::remove_item(rId); + } + + virtual void clear() override + { + MenuHelper::clear_items(); + } + + virtual void set_item_active(const OUString& rIdent, bool bActive) override + { + MenuHelper::set_item_active(rIdent, bActive); + } + + virtual void set_item_sensitive(const OUString& rIdent, bool bSensitive) override + { + MenuHelper::set_item_sensitive(rIdent, bSensitive); + } + + virtual void set_item_label(const OUString& rIdent, const OUString& rLabel) override + { + MenuHelper::set_item_label(rIdent, rLabel); + } + + virtual OUString get_item_label(const OUString& rIdent) const override + { + return MenuHelper::get_item_label(rIdent); + } + + virtual void set_item_visible(const OUString& rIdent, bool bVisible) override + { + MenuHelper::set_item_visible(rIdent, bVisible); + } + + virtual void signal_item_activate(const OUString& rIdent) override + { + signal_selected(rIdent); + } + + virtual void set_popover(weld::Widget* pPopover) override + { + GtkInstanceWidget* pPopoverWidget = dynamic_cast<GtkInstanceWidget*>(pPopover); + m_pPopover = pPopoverWidget ? pPopoverWidget->getWidget() : nullptr; + +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_menu_button_set_popover(m_pMenuButton, m_pPopover); + update_action_group_from_popover_model(); + return; +#else + + if (!m_pPopover) + { + gtk_menu_button_set_popover(m_pMenuButton, nullptr); + return; + } + + 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) && gtk_popover_get_constrain_to(GTK_POPOVER(m_pPopover)) == GTK_POPOVER_CONSTRAINT_NONE) + { + m_pMenuHack = GTK_WINDOW(gtk_window_new(GTK_WINDOW_POPUP)); + gtk_window_set_type_hint(m_pMenuHack, GDK_WINDOW_TYPE_HINT_COMBO); + // See writer "format, watermark" for true here. Can't interact with the replacement popover otherwise. + 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(signalMenuButtonToggled), this); + g_signal_connect(m_pMenuHack, "key-press-event", G_CALLBACK(keyPress), this); + g_signal_connect(m_pMenuHack, "grab-broken-event", G_CALLBACK(signalGrabBroken), this); + g_signal_connect(m_pMenuHack, "button-press-event", G_CALLBACK(signalButtonPress), this); + g_signal_connect(m_pMenuHack, "button-release-event", G_CALLBACK(signalButtonRelease), this); + } + } + + 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; }"; + css_provider_load_from_data(pProvider, data, -1); + 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); + gtk_widget_show_all(m_pPopover); + } +#endif + } + + void set_menu(weld::Menu* pMenu); + + static GtkBox* formatMenuButton(GtkWidget* pLabel) + { + // format the GtkMenuButton "manually" so we can have the dropdown image in GtkMenuButtons shown + // on the right at the same time as an image is shown on the left + g_object_ref(pLabel); + GtkWidget* pContainer = gtk_widget_get_parent(pLabel); +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_container_remove(GTK_CONTAINER(pContainer), pLabel); +#else + gtk_box_remove(GTK_BOX(pContainer), pLabel); +#endif + + gint nImageSpacing(2); +#if !GTK_CHECK_VERSION(4, 0, 0) + GtkStyleContext *pContext = gtk_widget_get_style_context(pContainer); + gtk_style_context_get_style(pContext, "image-spacing", &nImageSpacing, nullptr); +#endif + GtkBox* pBox = GTK_BOX(gtk_box_new(GTK_ORIENTATION_HORIZONTAL, nImageSpacing)); + +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_box_pack_start(pBox, pLabel, true, true, 0); +#else + gtk_widget_set_halign(pLabel, GTK_ALIGN_START); + gtk_box_prepend(pBox, pLabel); +#endif + g_object_unref(pLabel); + +#if !GTK_CHECK_VERSION(4, 0, 0) + if (gtk_toggle_button_get_mode(GTK_TOGGLE_BUTTON(pContainer))) + gtk_box_pack_end(pBox, gtk_image_new_from_icon_name("pan-down-symbolic", GTK_ICON_SIZE_BUTTON), false, false, 0); +#endif + +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_container_add(GTK_CONTAINER(pContainer), GTK_WIDGET(pBox)); +#else + gtk_box_prepend(GTK_BOX(pContainer), GTK_WIDGET(pBox)); +#endif +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_show_all(GTK_WIDGET(pBox)); +#else + gtk_widget_show(GTK_WIDGET(pBox)); +#endif + + return pBox; + } + +#if GTK_CHECK_VERSION(4, 0, 0) + virtual void disable_notify_events() override + { + g_signal_handler_block(m_pMenuButtonToggleButton, m_nToggledSignalId); + GtkInstanceWidget::disable_notify_events(); + } + + virtual void enable_notify_events() override + { + GtkInstanceWidget::enable_notify_events(); + g_signal_handler_unblock(m_pMenuButtonToggleButton, m_nToggledSignalId); + } +#endif + + virtual ~GtkInstanceMenuButton() override + { +#if GTK_CHECK_VERSION(4, 0, 0) + g_signal_handler_disconnect(m_pMenuButtonToggleButton, m_nToggledSignalId); + gtk_widget_insert_action_group(GTK_WIDGET(m_pMenuButton), "menu", nullptr); +#else + 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)); + } +#endif + } +}; + +class GtkInstanceMenuToggleButton : public GtkInstanceToggleButton, public MenuHelper + , public virtual weld::MenuToggleButton +{ +private: + GtkBox* m_pContainer; + GtkButton* m_pToggleMenuButton; + GtkMenuButton* m_pMenuButton; + gulong m_nMenuBtnClickedId; + gulong m_nToggleStateFlagsChangedId; + gulong m_nMenuBtnStateFlagsChangedId; + + static void signalToggleStateFlagsChanged(GtkWidget* pWidget, GtkStateFlags /*eFlags*/, gpointer widget) + { + GtkInstanceMenuToggleButton* pThis = static_cast<GtkInstanceMenuToggleButton*>(widget); + // mirror togglebutton state to menubutton + gtk_widget_set_state_flags(GTK_WIDGET(pThis->m_pToggleMenuButton), gtk_widget_get_state_flags(pWidget), true); + } + + static void signalMenuBtnStateFlagsChanged(GtkWidget* pWidget, GtkStateFlags /*eFlags*/, gpointer widget) + { + GtkInstanceMenuToggleButton* pThis = static_cast<GtkInstanceMenuToggleButton*>(widget); + // mirror menubutton to togglebutton, keeping depressed state of menubutton + GtkStateFlags eToggleFlags = gtk_widget_get_state_flags(GTK_WIDGET(pThis->m_pToggleButton)); + GtkStateFlags eFlags = gtk_widget_get_state_flags(pWidget); + GtkStateFlags eFinalFlags = static_cast<GtkStateFlags>((eFlags & ~GTK_STATE_FLAG_ACTIVE) | + (eToggleFlags & GTK_STATE_FLAG_ACTIVE)); + gtk_widget_set_state_flags(GTK_WIDGET(pThis->m_pToggleButton), eFinalFlags, true); + } + + static void signalMenuBtnClicked(GtkButton*, gpointer widget) + { + GtkInstanceMenuToggleButton* pThis = static_cast<GtkInstanceMenuToggleButton*>(widget); + pThis->launch_menu(); + } + + void launch_menu() + { + gtk_widget_set_state_flags(GTK_WIDGET(m_pToggleMenuButton), gtk_widget_get_state_flags(GTK_WIDGET(m_pToggleButton)), true); + GtkWidget* pWidget = GTK_WIDGET(m_pToggleButton); + + //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); + +#if GTK_CHECK_VERSION(4, 0, 0) + gulong nSignalId = g_signal_connect_swapped(G_OBJECT(m_pMenu), "closed", G_CALLBACK(g_main_loop_quit), pLoop); + + g_object_ref(m_pMenu); + gtk_menu_button_set_popover(m_pMenuButton, nullptr); + gtk_widget_set_parent(GTK_WIDGET(m_pMenu), pWidget); + gtk_popover_set_position(GTK_POPOVER(m_pMenu), GTK_POS_BOTTOM); + gtk_popover_popup(GTK_POPOVER(m_pMenu)); +#else + 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) + { + // 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 + // Fixed in GTK 2.34 + GdkEvent *pKeyEvent = GtkSalFrame::makeFakeKeyPress(pWidget); + gtk_main_do_event(pKeyEvent); + + GdkEvent *pTriggerEvent = gtk_get_current_event(); + if (!pTriggerEvent) + pTriggerEvent = pKeyEvent; + + gtk_menu_popup_at_widget(m_pMenu, pWidget, GDK_GRAVITY_SOUTH_WEST, GDK_GRAVITY_NORTH_WEST, pTriggerEvent); + + gdk_event_free(pKeyEvent); + } + 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(); + } + + gtk_menu_popup(m_pMenu, nullptr, nullptr, nullptr, nullptr, nButton, nTime); + } +#endif + + if (g_main_loop_is_running(pLoop)) + main_loop_run(pLoop); + + g_main_loop_unref(pLoop); + g_signal_handler_disconnect(m_pMenu, nSignalId); + +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_unparent(GTK_WIDGET(m_pMenu)); + gtk_menu_button_set_popover(m_pMenuButton, GTK_WIDGET(m_pMenu)); + g_object_unref(m_pMenu); +#endif + + } + + static gboolean signalMenuToggleButton(GtkWidget*, gboolean bGroupCycling, gpointer widget) + { + GtkInstanceMenuToggleButton* pThis = static_cast<GtkInstanceMenuToggleButton*>(widget); + return gtk_widget_mnemonic_activate(GTK_WIDGET(pThis->m_pToggleButton), bGroupCycling); + } + +public: + GtkInstanceMenuToggleButton(GtkBuilder* pMenuToggleButtonBuilder, GtkMenuButton* pMenuButton, + GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : GtkInstanceToggleButton(GTK_TOGGLE_BUTTON(gtk_builder_get_object(pMenuToggleButtonBuilder, "togglebutton")), + pBuilder, bTakeOwnership) +#if !GTK_CHECK_VERSION(4, 0, 0) + , MenuHelper(gtk_menu_button_get_popup(pMenuButton), false) +#else + , MenuHelper(GTK_POPOVER_MENU(gtk_menu_button_get_popover(pMenuButton)), false) +#endif + , m_pContainer(GTK_BOX(gtk_builder_get_object(pMenuToggleButtonBuilder, "box"))) + , m_pToggleMenuButton(GTK_BUTTON(gtk_builder_get_object(pMenuToggleButtonBuilder, "menubutton"))) + , m_pMenuButton(pMenuButton) + , m_nMenuBtnClickedId(g_signal_connect(m_pToggleMenuButton, "clicked", G_CALLBACK(signalMenuBtnClicked), this)) + , m_nToggleStateFlagsChangedId(g_signal_connect(m_pToggleButton, "state-flags-changed", G_CALLBACK(signalToggleStateFlagsChanged), this)) + , m_nMenuBtnStateFlagsChangedId(g_signal_connect(m_pToggleMenuButton, "state-flags-changed", G_CALLBACK(signalMenuBtnStateFlagsChanged), this)) + { +#if !GTK_CHECK_VERSION(4, 0, 0) + GtkInstanceMenuButton::formatMenuButton(gtk_bin_get_child(GTK_BIN(m_pMenuButton))); +#endif + + insertAsParent(GTK_WIDGET(m_pMenuButton), GTK_WIDGET(m_pContainer)); + gtk_widget_hide(GTK_WIDGET(m_pMenuButton)); + + // move the first GtkMenuButton child, as created by GtkInstanceMenuButton ctor, into the GtkToggleButton + // instead, leaving just the indicator behind in the GtkMenuButton +#if !GTK_CHECK_VERSION(4, 0, 0) + GtkWidget* pButtonBox = gtk_bin_get_child(GTK_BIN(m_pMenuButton)); + GList* pChildren = gtk_container_get_children(GTK_CONTAINER(pButtonBox)); + int nGroup = 0; + for (GList* pChild = g_list_first(pChildren); pChild && nGroup < 2; pChild = g_list_next(pChild), ++nGroup) + { + GtkWidget* pWidget = static_cast<GtkWidget*>(pChild->data); + g_object_ref(pWidget); + gtk_container_remove(GTK_CONTAINER(pButtonBox), pWidget); + if (nGroup == 0) + gtk_container_add(GTK_CONTAINER(m_pToggleButton), pWidget); + else + gtk_container_add(GTK_CONTAINER(m_pToggleMenuButton), pWidget); + gtk_widget_show_all(pWidget); + g_object_unref(pWidget); + } + g_list_free(pChildren); +#else + GtkWidget* pChild; + if (gtk_check_version(4, 5, 0) == nullptr) + { + pChild = gtk_widget_get_first_child(GTK_WIDGET(m_pMenuButton)); + pChild = gtk_widget_get_first_child(pChild); + pChild = gtk_widget_get_first_child(pChild); + } + else + pChild = gtk_widget_get_last_child(GTK_WIDGET(m_pMenuButton)); + g_object_ref(pChild); + gtk_widget_unparent(pChild); + gtk_button_set_child(GTK_BUTTON(m_pToggleButton), pChild); + g_object_unref(pChild); +#endif + + // match the GtkToggleButton relief to the GtkMenuButton +#if !GTK_CHECK_VERSION(4, 0, 0) + const GtkReliefStyle eStyle = gtk_button_get_relief(GTK_BUTTON(m_pMenuButton)); + gtk_button_set_relief(GTK_BUTTON(m_pToggleButton), eStyle); + gtk_button_set_relief(GTK_BUTTON(m_pToggleMenuButton), eStyle); +#else + const bool bStyle = gtk_menu_button_get_has_frame(GTK_MENU_BUTTON(m_pMenuButton)); + gtk_button_set_has_frame(GTK_BUTTON(m_pToggleButton), bStyle); + gtk_button_set_has_frame(GTK_BUTTON(m_pToggleMenuButton), bStyle); +#endif + + // move the GtkMenuButton margins up to the new parent + gtk_widget_set_margin_top(GTK_WIDGET(m_pContainer), + gtk_widget_get_margin_top(GTK_WIDGET(m_pMenuButton))); + gtk_widget_set_margin_bottom(GTK_WIDGET(m_pContainer), + gtk_widget_get_margin_bottom(GTK_WIDGET(m_pMenuButton))); + gtk_widget_set_margin_start(GTK_WIDGET(m_pContainer), + gtk_widget_get_margin_start(GTK_WIDGET(m_pMenuButton))); + gtk_widget_set_margin_end(GTK_WIDGET(m_pContainer), + gtk_widget_get_margin_end(GTK_WIDGET(m_pMenuButton))); + +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_menu_detach(m_pMenu); + gtk_menu_attach_to_widget(m_pMenu, GTK_WIDGET(m_pToggleButton), nullptr); +#else + gtk_widget_insert_action_group(GTK_WIDGET(m_pContainer), "menu", m_pActionGroup); + + update_action_group_from_popover_model(); +#endif + + g_signal_connect(m_pContainer, "mnemonic-activate", G_CALLBACK(signalMenuToggleButton), this); + } + + virtual void disable_notify_events() override + { + g_signal_handler_block(m_pToggleMenuButton, m_nMenuBtnClickedId); + GtkInstanceToggleButton::disable_notify_events(); + } + + virtual void enable_notify_events() override + { + GtkInstanceToggleButton::enable_notify_events(); + g_signal_handler_unblock(m_pToggleMenuButton, m_nMenuBtnClickedId); + } + + virtual ~GtkInstanceMenuToggleButton() + { + g_signal_handler_disconnect(m_pToggleButton, m_nToggleStateFlagsChangedId); + g_signal_handler_disconnect(m_pToggleMenuButton, m_nMenuBtnStateFlagsChangedId); + g_signal_handler_disconnect(m_pToggleMenuButton, m_nMenuBtnClickedId); + +#if GTK_CHECK_VERSION(4, 0, 0) + GtkWidget* pChild = gtk_button_get_child(GTK_BUTTON(m_pToggleButton)); + g_object_ref(pChild); + gtk_button_set_child(GTK_BUTTON(m_pToggleButton), nullptr); + gtk_widget_unparent(pChild); + gtk_widget_set_parent(pChild, GTK_WIDGET(m_pMenuButton)); + g_object_unref(pChild); +#endif + } + + 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 OUString& rId) override + { + MenuHelper::remove_item(rId); + } + + virtual void clear() override + { + MenuHelper::clear_items(); + } + + virtual void set_item_active(const OUString& rIdent, bool bActive) override + { + MenuHelper::set_item_active(rIdent, bActive); + } + + virtual void set_item_sensitive(const OUString& rIdent, bool bSensitive) override + { + MenuHelper::set_item_sensitive(rIdent, bSensitive); + } + + virtual void set_item_label(const OUString& rIdent, const OUString& rLabel) override + { + MenuHelper::set_item_label(rIdent, rLabel); + } + + virtual OUString get_item_label(const OUString& rIdent) const override + { + return MenuHelper::get_item_label(rIdent); + } + + virtual void set_item_visible(const OUString& rIdent, bool bVisible) override + { + MenuHelper::set_item_visible(rIdent, bVisible); + } + + virtual void signal_item_activate(const OUString& rIdent) override + { + signal_selected(rIdent); + } + + virtual void set_popover(weld::Widget* /*pPopover*/) override + { + assert(false && "not implemented"); + } +}; + +class GtkInstanceMenu : public MenuHelper, public virtual weld::Menu +{ +protected: +#if !GTK_CHECK_VERSION(4, 0, 0) + std::vector<GtkMenuItem*> m_aExtraItems; +#endif + OUString m_sActivated; +#if !GTK_CHECK_VERSION(4, 0, 0) + MenuHelper* m_pTopLevelMenuHelper; +#endif + +private: + virtual void signal_item_activate(const OUString& rIdent) override + { + m_sActivated = rIdent; + weld::Menu::signal_activate(m_sActivated); + } + +#if !GTK_CHECK_VERSION(4, 0, 0) + 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(); + } +#endif + +public: +#if !GTK_CHECK_VERSION(4, 0, 0) + GtkInstanceMenu(GtkMenu* pMenu, bool bTakeOwnership) +#else + GtkInstanceMenu(GtkPopoverMenu* pMenu, bool bTakeOwnership) +#endif + : MenuHelper(pMenu, bTakeOwnership) +#if !GTK_CHECK_VERSION(4, 0, 0) + , m_pTopLevelMenuHelper(nullptr) +#endif + { + g_object_set_data(G_OBJECT(m_pMenu), "g-lo-GtkInstanceMenu", this); +#if !GTK_CHECK_VERSION(4, 0, 0) + // 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) + return; + + // 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); + } +#else + update_action_group_from_popover_model(); +#endif + } + + virtual OUString popup_at_rect(weld::Widget* pParent, const tools::Rectangle& rRect, weld::Placement ePlace) override + { + m_sActivated.clear(); + + GtkInstanceWidget* pGtkWidget = dynamic_cast<GtkInstanceWidget*>(pParent); + assert(pGtkWidget); + GtkWidget* pWidget = pGtkWidget->getWidget(); + + //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); + +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_insert_action_group(pWidget, "menu", m_pActionGroup); + + gulong nSignalId = g_signal_connect_swapped(G_OBJECT(m_pMenu), "closed", G_CALLBACK(g_main_loop_quit), pLoop); + + GdkRectangle aRect; + pWidget = getPopupRect(pWidget, rRect, aRect); + + GtkWidget* pOrigParent = gtk_widget_get_parent(GTK_WIDGET(m_pMenu)); + gtk_widget_set_parent(GTK_WIDGET(m_pMenu), pWidget); + gtk_popover_set_pointing_to(GTK_POPOVER(m_pMenu), &aRect); + if (ePlace == weld::Placement::Under) + gtk_popover_set_position(GTK_POPOVER(m_pMenu), GTK_POS_BOTTOM); + else + { + if (SwapForRTL(pWidget)) + gtk_popover_set_position(GTK_POPOVER(m_pMenu), GTK_POS_LEFT); + else + gtk_popover_set_position(GTK_POPOVER(m_pMenu), GTK_POS_RIGHT); + } + gtk_popover_popup(GTK_POPOVER(m_pMenu)); +#else + 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; + pWidget = getPopupRect(pWidget, rRect, aRect); + gtk_menu_attach_to_widget(m_pMenu, pWidget, nullptr); + + // 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 + // Fixed in GTK 2.34 + GdkEvent *pKeyEvent = GtkSalFrame::makeFakeKeyPress(pWidget); + gtk_main_do_event(pKeyEvent); + + GdkEvent *pTriggerEvent = gtk_get_current_event(); + if (!pTriggerEvent) + pTriggerEvent = pKeyEvent; + + bool bSwapForRTL = SwapForRTL(pWidget); + + if (ePlace == weld::Placement::Under) + { + if (bSwapForRTL) + gtk_menu_popup_at_rect(m_pMenu, widget_get_surface(pWidget), &aRect, GDK_GRAVITY_SOUTH_EAST, GDK_GRAVITY_NORTH_EAST, pTriggerEvent); + else + gtk_menu_popup_at_rect(m_pMenu, widget_get_surface(pWidget), &aRect, GDK_GRAVITY_SOUTH_WEST, GDK_GRAVITY_NORTH_WEST, pTriggerEvent); + } + else + { + if (bSwapForRTL) + gtk_menu_popup_at_rect(m_pMenu, widget_get_surface(pWidget), &aRect, GDK_GRAVITY_NORTH_WEST, GDK_GRAVITY_NORTH_EAST, pTriggerEvent); + else + gtk_menu_popup_at_rect(m_pMenu, widget_get_surface(pWidget), &aRect, GDK_GRAVITY_NORTH_EAST, GDK_GRAVITY_NORTH_WEST, pTriggerEvent); + } + + gdk_event_free(pKeyEvent); + } + else +#else + (void) rRect; +#endif + { + gtk_menu_attach_to_widget(m_pMenu, pWidget, nullptr); + + 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) + { + if (!gdk_event_get_button(pEvent, &nButton)) + nButton = 0; + nTime = gdk_event_get_time(pEvent); + } + else + { + nButton = 0; + nTime = GtkSalFrame::GetLastInputEventTime(); + } + + gtk_menu_popup(m_pMenu, nullptr, nullptr, nullptr, nullptr, nButton, nTime); + } +#endif + + if (g_main_loop_is_running(pLoop)) + main_loop_run(pLoop); + + g_main_loop_unref(pLoop); + g_signal_handler_disconnect(m_pMenu, nSignalId); + +#if GTK_CHECK_VERSION(4, 0, 0) + if (!pOrigParent) + gtk_widget_unparent(GTK_WIDGET(m_pMenu)); + else + gtk_widget_set_parent(GTK_WIDGET(m_pMenu), pOrigParent); + + gtk_widget_insert_action_group(pWidget, "menu", nullptr); +#else + gtk_menu_detach(m_pMenu); +#endif + + return m_sActivated; + } + + virtual void set_sensitive(const OUString& rIdent, bool bSensitive) override + { + set_item_sensitive(rIdent, bSensitive); + } + + virtual bool get_sensitive(const OUString& rIdent) const override + { + return get_item_sensitive(rIdent); + } + + virtual void set_active(const OUString& rIdent, bool bActive) override + { + set_item_active(rIdent, bActive); + } + + virtual bool get_active(const OUString& rIdent) const override + { + return get_item_active(rIdent); + } + + virtual void set_visible(const OUString& rIdent, bool bShow) override + { + set_item_visible(rIdent, bShow); + } + + virtual void set_label(const OUString& rIdent, const OUString& rLabel) override + { + set_item_label(rIdent, rLabel); + } + + virtual OUString get_label(const OUString& 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 + { +#if !GTK_CHECK_VERSION(4, 0, 0) + clear_extras(); +#endif + MenuHelper::clear_items(); + } + + virtual void insert(int pos, const OUString& rId, const OUString& rStr, + const OUString* pIconName, VirtualDevice* pImageSurface, + const css::uno::Reference<css::graphic::XGraphic>& rGraphic, + TriState eCheckRadioFalse) override + { +#if !GTK_CHECK_VERSION(4, 0, 0) + GtkWidget* pImage = nullptr; + if (pIconName) + pImage = image_new_from_icon_name(*pIconName); + else if (pImageSurface) + { + pImage = image_new_from_virtual_device(*pImageSurface); + } + else if (rGraphic) + { + pImage = image_new_from_xgraphic(rGraphic, false); + } + + GtkWidget *pItem; + if (pImage) + { + GtkBox *pBox = GTK_BOX(gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6)); + GtkWidget *pLabel = gtk_label_new_with_mnemonic(MapToGtkAccelerator(rStr).getStr()); + gtk_label_set_xalign(GTK_LABEL(pLabel), 0.0); + pItem = eCheckRadioFalse != TRISTATE_INDET ? gtk_check_menu_item_new() : gtk_menu_item_new(); + gtk_box_pack_start(pBox, pImage, false, true, 0); + gtk_box_pack_start(pBox, pLabel, true, true, 0); + gtk_container_add(GTK_CONTAINER(pItem), GTK_WIDGET(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); + + ::set_buildable_id(GTK_BUILDABLE(pItem), rId); + 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); +#else + SAL_WARN("vcl.gtk", "needs to be implemented for gtk4"); + (void)pIconName; + (void)pImageSurface; + (void)rGraphic; + + if (GMenuModel* pMenuModel = m_pMenu ? gtk_popover_menu_get_menu_model(m_pMenu) : nullptr) + { + auto aSectionAndPos = get_section_and_pos_for(pMenuModel, pos); + GMenu* pMenu = G_MENU(aSectionAndPos.first); + // action with a target value ... the action name and target value are separated by a double + // colon ... For example: "app.action::target" + OUString sActionAndTarget; + if (eCheckRadioFalse == TRISTATE_INDET) + sActionAndTarget = "menu.normal." + rId + "::" + rId; + else + sActionAndTarget = "menu.radio." + rId + "::" + rId; + g_menu_insert(pMenu, aSectionAndPos.second, MapToGtkAccelerator(rStr).getStr(), sActionAndTarget.toUtf8().getStr()); + + assert(eCheckRadioFalse == TRISTATE_INDET); // come back to this later + + // TODO not redo entire group + update_action_group_from_popover_model(); + } + +#endif + } + + virtual OUString get_id(int pos) const override + { + return get_item_id(pos); + } + + virtual int n_children() const override + { + return get_n_children(); + } + + virtual void set_item_help_id(const OUString& rIdent, const OUString& rHelpId) override + { +#if !GTK_CHECK_VERSION(4, 0, 0) + ::set_help_id(GTK_WIDGET(m_aMap[rIdent]), rHelpId); +#else + (void)rIdent; + (void)rHelpId; +#endif + } + + void remove(const OUString& rIdent) override + { +#if !GTK_CHECK_VERSION(4, 0, 0) + 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()) + { + if (m_pTopLevelMenuHelper) + m_pTopLevelMenuHelper->remove_from_map(pMenuItem); + m_aExtraItems.erase(iter); + } + } +#endif + MenuHelper::remove_item(rIdent); + } + + virtual ~GtkInstanceMenu() override + { +#if !GTK_CHECK_VERSION(4, 0, 0) + clear_extras(); +#endif + g_object_steal_data(G_OBJECT(m_pMenu), "g-lo-GtkInstanceMenu"); + } +}; + +#if !GTK_CHECK_VERSION(4, 0, 0) + vcl::ImageType GtkToVcl(GtkIconSize eSize) + { + vcl::ImageType eRet; + switch (eSize) + { +#if !GTK_CHECK_VERSION(4, 0, 0) + 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; +#else + case GTK_ICON_SIZE_LARGE: + eRet = vcl::ImageType::Size32; + break; + case GTK_ICON_SIZE_NORMAL: + default: + eRet = vcl::ImageType::Size16; + break; +#endif + } + return eRet; + } + + GtkIconSize VclToGtk(vcl::ImageType eSize) + { + GtkIconSize eRet; +#if !GTK_CHECK_VERSION(4, 0, 0) + 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; + } +#else + switch (eSize) + { + case vcl::ImageType::Size26: + case vcl::ImageType::Size32: + eRet = GTK_ICON_SIZE_LARGE; + break; + case vcl::ImageType::Size16: + default: + eRet = GTK_ICON_SIZE_NORMAL; + break; + } +#endif + return eRet; + } + + // tdf#153885 for wayland if the popover window is the application + // window, constrain it within the application window so it won't + // be cut off screen. Leave dialog hosted ones alone, like + // format, watermark, which are likely presented in the middle + // of the screen and are too small to constrain the popover inside. + void ConstrainApplicationWindowPopovers(GtkToggleButton* pItem) + { +#if defined(GDK_WINDOWING_WAYLAND) + GdkDisplay *pDisplay = gtk_widget_get_display(GTK_WIDGET(pItem)); + if (DLSYM_GDK_IS_WAYLAND_DISPLAY(pDisplay) && GTK_IS_MENU_BUTTON(pItem)) + { + GtkMenuButton* pMenuButton = GTK_MENU_BUTTON(pItem); + if (GtkPopover* pPopover = gtk_menu_button_get_popover(pMenuButton)) + { + if (gtk_popover_get_constrain_to(pPopover) == GTK_POPOVER_CONSTRAINT_NONE) + { + GtkWidget* pTopLevel = widget_get_toplevel(GTK_WIDGET(pItem)); + GtkSalFrame* pFrame = pTopLevel ? GtkSalFrame::getFromWindow(pTopLevel) : nullptr; + if (pFrame) + { + // the toplevel is an application window + gtk_popover_set_constrain_to(pPopover, GTK_POPOVER_CONSTRAINT_WINDOW); + } + } + } + } +#else + (void)pItem; +#endif + } + +#endif +} + +void GtkInstanceMenuButton::set_menu(weld::Menu* pMenu) +{ + GtkInstanceMenu* pPopoverWidget = dynamic_cast<GtkInstanceMenu*>(pMenu); + m_pPopover = nullptr; + m_pMenu = pPopoverWidget ? pPopoverWidget->getMenu() : nullptr; + +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_menu_button_set_popup(m_pMenuButton, GTK_WIDGET(m_pMenu)); +#else + gtk_menu_button_set_popover(m_pMenuButton, GTK_WIDGET(m_pMenu)); + update_action_group_from_popover_model(); +#endif +} + +namespace { + +class GtkInstanceToolbar : public GtkInstanceWidget, public virtual weld::Toolbar +{ +private: +#if !GTK_CHECK_VERSION(4, 0, 0) + GtkToolbar* m_pToolbar; +#else + GtkBox* m_pToolbar; + vcl::ImageType m_eImageType; +#endif + GtkCssProvider *m_pMenuButtonProvider; + + std::map<OUString, GtkWidget*> m_aMap; + std::map<OUString, std::unique_ptr<GtkInstanceMenuButton>> m_aMenuButtonMap; + std::map<OUString, bool> m_aMirroredMap; + +#if !GTK_CHECK_VERSION(4, 0, 0) + // 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); + } +#endif + + static void collect(GtkWidget* pItem, gpointer widget) + { +#if !GTK_CHECK_VERSION(4, 0, 0) + if (!GTK_IS_TOOL_ITEM(pItem)) + return; +#endif + GtkInstanceToolbar* pThis = static_cast<GtkInstanceToolbar*>(widget); + + GtkMenuButton* pMenuButton = nullptr; +#if !GTK_CHECK_VERSION(4, 0, 0) + if (GTK_IS_MENU_TOOL_BUTTON(pItem)) + find_menu_button(pItem, &pMenuButton); +#else + if (GTK_IS_MENU_BUTTON(pItem)) + pMenuButton = GTK_MENU_BUTTON(pItem); +#endif + + pThis->add_to_map(pItem, pMenuButton); + } + + void add_to_map(GtkWidget* pToolItem, GtkMenuButton* pMenuButton) + { + OUString id = ::get_buildable_id(GTK_BUILDABLE(pToolItem)); + 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 +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_button_set_focus_on_click(GTK_BUTTON(pMenuButton), false); + g_signal_connect(pMenuButton, "toggled", G_CALLBACK(signalItemToggled), this); +#else + gtk_widget_set_focus_on_click(GTK_WIDGET(pMenuButton), false); + + GtkWidget* pToggleButton = gtk_widget_get_first_child(GTK_WIDGET(pMenuButton)); + assert(GTK_IS_TOGGLE_BUTTON(pToggleButton)); + g_signal_connect(pToggleButton, "toggled", G_CALLBACK(signalItemToggled), this); +#endif + + // 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;" + "}"; + css_provider_load_from_data(m_pMenuButtonProvider, data, -1); + } + + gtk_style_context_add_provider(pButtonContext, + GTK_STYLE_PROVIDER(m_pMenuButtonProvider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + } +#if !GTK_CHECK_VERSION(4, 0, 0) + if (!GTK_IS_TOOL_BUTTON(pToolItem)) +#else + if (!GTK_IS_BUTTON(pToolItem)) +#endif + { + return; + } + g_signal_connect(pToolItem, "clicked", G_CALLBACK(signalItemClicked), this); + } + +#if !GTK_CHECK_VERSION(4, 0, 0) + static void signalItemClicked(GtkToolButton* pItem, gpointer widget) +#else + static void signalItemClicked(GtkButton* pItem, gpointer widget) +#endif + { + GtkInstanceToolbar* pThis = static_cast<GtkInstanceToolbar*>(widget); + SolarMutexGuard aGuard; + pThis->signal_item_clicked(pItem); + } + +#if !GTK_CHECK_VERSION(4, 0, 0) + void signal_item_clicked(GtkToolButton* pItem) +#else + void signal_item_clicked(GtkButton* pItem) +#endif + { + signal_clicked(::get_buildable_id(GTK_BUILDABLE(pItem))); + } + + static void signalItemToggled(GtkToggleButton* pItem, gpointer widget) + { +#if !GTK_CHECK_VERSION(4, 0, 0) + ConstrainApplicationWindowPopovers(pItem); +#endif + GtkInstanceToolbar* pThis = static_cast<GtkInstanceToolbar*>(widget); + SolarMutexGuard aGuard; + pThis->signal_item_toggled(pItem); + } + + void signal_item_toggled(GtkToggleButton* pItem) + { + for (const auto& a : m_aMenuButtonMap) + { +#if !GTK_CHECK_VERSION(4, 0, 0) + if (a.second->getWidget() == GTK_WIDGET(pItem)) +#else + if (a.second->getWidget() == gtk_widget_get_parent(GTK_WIDGET(pItem))) +#endif + { + signal_toggle_menu(a.first); + break; + } + } + } + +#if GTK_CHECK_VERSION(4, 0, 0) + static void set_item_image(GtkWidget* pItem, GtkWidget* pImage) + { + if (GTK_IS_BUTTON(pItem)) + gtk_button_set_child(GTK_BUTTON(pItem), pImage); + else if (GTK_IS_MENU_BUTTON(pItem)) + { + // TODO after gtk 4.6 is released require that version and drop this + static auto menu_button_set_child = reinterpret_cast<void (*) (GtkMenuButton*, GtkWidget*)>(dlsym(nullptr, "gtk_menu_button_set_child")); + if (menu_button_set_child) + menu_button_set_child(GTK_MENU_BUTTON(pItem), pImage); + } + // versions of gtk4 > 4.2.1 might do this on their own + gtk_widget_remove_css_class(pItem, "text-button"); + } +#endif + +#if !GTK_CHECK_VERSION(4, 0, 0) + static void set_item_image(GtkToolButton* pItem, const css::uno::Reference<css::graphic::XGraphic>& rIcon, bool bMirror) +#else + static void set_item_image(GtkWidget* pItem, const css::uno::Reference<css::graphic::XGraphic>& rIcon, bool bMirror) +#endif + { + GtkWidget* pImage = image_new_from_xgraphic(rIcon, bMirror); + if (pImage) + gtk_widget_show(pImage); +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_tool_button_set_icon_widget(pItem, pImage); +#else + set_item_image(pItem, pImage); +#endif + } + +#if !GTK_CHECK_VERSION(4, 0, 0) + void set_item_image(GtkToolButton* pItem, const VirtualDevice* pDevice) +#else + void set_item_image(GtkWidget* pItem, const VirtualDevice* pDevice) +#endif + { + GtkWidget* pImage = nullptr; + + if (pDevice) + { +#if GTK_CHECK_VERSION(4, 0, 0) + pImage = picture_new_from_virtual_device(*pDevice); +#else + pImage = image_new_from_virtual_device(*pDevice); +#endif + gtk_widget_show(pImage); + } + +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_tool_button_set_icon_widget(pItem, pImage); +#else + set_item_image(pItem, pImage); +#endif + gtk_widget_queue_draw(GTK_WIDGET(m_pToolbar)); + } + +#if !GTK_CHECK_VERSION(4, 0, 0) + GtkWidget* toolbar_get_nth_item(int nIndex) const + { + return GTK_WIDGET(gtk_toolbar_get_nth_item(m_pToolbar, nIndex)); + } +#else + GtkWidget* toolbar_get_nth_item(int nIndex) const + { + int i = 0; + for (GtkWidget* pChild = gtk_widget_get_first_child(GTK_WIDGET(m_pToolbar)); + pChild; pChild = gtk_widget_get_next_sibling(pChild)) + { + if (i == nIndex) + return pChild; + ++i; + } + return nullptr; + } +#endif +public: +#if !GTK_CHECK_VERSION(4, 0, 0) + GtkInstanceToolbar(GtkToolbar* pToolbar, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) +#else + GtkInstanceToolbar(GtkBox* pToolbar, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) +#endif + : GtkInstanceWidget(GTK_WIDGET(pToolbar), pBuilder, bTakeOwnership) + , m_pToolbar(pToolbar) +#if GTK_CHECK_VERSION(4, 0, 0) + , m_eImageType(vcl::ImageType::Size16) +#endif + , m_pMenuButtonProvider(nullptr) + { +#if GTK_CHECK_VERSION(4, 0, 0) + for (GtkWidget* pChild = gtk_widget_get_first_child(GTK_WIDGET(pToolbar)); + pChild; pChild = gtk_widget_get_next_sibling(pChild)) + { + collect(pChild, this); + } +#else + gtk_container_foreach(GTK_CONTAINER(pToolbar), collect, this); +#endif + } + + 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 OUString& 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 OUString& rIdent) const override + { + return gtk_widget_get_sensitive(GTK_WIDGET(m_aMap.find(rIdent)->second)); + } + + virtual void set_item_visible(const OUString& 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 OUString& rIdent, const OUString& rHelpId) override + { + ::set_help_id(GTK_WIDGET(m_aMap[rIdent]), rHelpId); + } + + virtual bool get_item_visible(const OUString& rIdent) const override + { + return gtk_widget_get_visible(GTK_WIDGET(m_aMap.find(rIdent)->second)); + } + + virtual void set_item_active(const OUString& rIdent, bool bActive) override + { + disable_item_notify_events(); + + GtkWidget* pToolButton = m_aMap.find(rIdent)->second; + +#if !GTK_CHECK_VERSION(4, 0, 0) + 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); + } + } +#else + GtkWidget* pWidget; + if (GTK_IS_MENU_BUTTON(pToolButton)) + { + pWidget = gtk_widget_get_first_child(pToolButton); + assert(GTK_IS_TOGGLE_BUTTON(pWidget)); + } + else + pWidget = pToolButton; + auto eState = gtk_widget_get_state_flags(pWidget) & ~GTK_STATE_FLAG_CHECKED; + if (bActive) + eState |= GTK_STATE_FLAG_CHECKED; + gtk_widget_set_state_flags(pWidget, static_cast<GtkStateFlags>(eState), true); +#endif + + enable_item_notify_events(); + } + + virtual bool get_item_active(const OUString& rIdent) const override + { + GtkWidget* pToolButton = m_aMap.find(rIdent)->second; + +#if !GTK_CHECK_VERSION(4, 0, 0) + 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; + } + } +#else + GtkWidget* pWidget; + if (GTK_IS_MENU_BUTTON(pToolButton)) + { + pWidget = gtk_widget_get_first_child(pToolButton); + assert(GTK_IS_TOGGLE_BUTTON(pWidget)); + } + else + pWidget = pToolButton; + return gtk_widget_get_state_flags(pWidget) & GTK_STATE_FLAG_CHECKED; +#endif + + return false; + } + + virtual void set_menu_item_active(const OUString& 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 OUString& rIdent) const override + { + auto aFind = m_aMenuButtonMap.find(rIdent); + assert (aFind != m_aMenuButtonMap.end()); + return aFind->second->get_active(); + } + + virtual void insert_item(int pos, const OUString& rId) override + { +#if !GTK_CHECK_VERSION(4, 0, 0) + GtkToolItem* pItem = gtk_tool_button_new(nullptr, OUStringToOString(rId, RTL_TEXTENCODING_UTF8).getStr()); +#else + GtkWidget* pItem = gtk_button_new(); +#endif + ::set_buildable_id(GTK_BUILDABLE(pItem), rId); +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_toolbar_insert(m_pToolbar, pItem, pos); +#else + gtk_box_insert_child_after(m_pToolbar, pItem, toolbar_get_nth_item(pos - 1)); +#endif + gtk_widget_show(GTK_WIDGET(pItem)); + add_to_map(GTK_WIDGET(pItem), nullptr); + } + + virtual void insert_separator(int pos, const OUString& rId) override + { +#if !GTK_CHECK_VERSION(4, 0, 0) + GtkToolItem* pItem = gtk_separator_tool_item_new(); +#else + GtkWidget* pItem = gtk_separator_new(GTK_ORIENTATION_VERTICAL); +#endif + ::set_buildable_id(GTK_BUILDABLE(pItem), rId); +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_toolbar_insert(m_pToolbar, pItem, pos); +#else + gtk_box_insert_child_after(m_pToolbar, pItem, toolbar_get_nth_item(pos - 1)); +#endif + gtk_widget_show(GTK_WIDGET(pItem)); + } + + virtual void set_item_popover(const OUString& rIdent, weld::Widget* pPopover) override + { + m_aMenuButtonMap[rIdent]->set_popover(pPopover); + } + + virtual void set_item_menu(const OUString& rIdent, weld::Menu* pMenu) override + { + m_aMenuButtonMap[rIdent]->set_menu(pMenu); + } + + virtual int get_n_items() const override + { +#if !GTK_CHECK_VERSION(4, 0, 0) + return gtk_toolbar_get_n_items(m_pToolbar); +#else + int n_items = 0; + for (GtkWidget* pChild = gtk_widget_get_first_child(GTK_WIDGET(m_pToolbar)); + pChild; pChild = gtk_widget_get_next_sibling(pChild)) + { + ++n_items; + } + return n_items; +#endif + } + + virtual OUString get_item_ident(int nIndex) const override + { + auto* pItem = toolbar_get_nth_item(nIndex); + return ::get_buildable_id(GTK_BUILDABLE(pItem)); + } + + virtual void set_item_ident(int nIndex, const OUString& rIdent) override + { + OUString sOldIdent(get_item_ident(nIndex)); + m_aMap.erase(m_aMap.find(sOldIdent)); + + auto* pItem = toolbar_get_nth_item(nIndex); + ::set_buildable_id(GTK_BUILDABLE(pItem), rIdent); + + // to keep the ids unique, if the new id is already in use by an item, + // change the id of that item to the now unused old ident of this item + auto aFind = m_aMap.find(rIdent); + if (aFind != m_aMap.end()) + { + GtkWidget* pDupIdItem = aFind->second; + ::set_buildable_id(GTK_BUILDABLE(pDupIdItem), sOldIdent); + m_aMap[sOldIdent] = pDupIdItem; + } + + m_aMap[rIdent] = pItem; + } + + virtual void set_item_label(int nIndex, const OUString& rLabel) override + { + auto* pItem = toolbar_get_nth_item(nIndex); +#if !GTK_CHECK_VERSION(4, 0, 0) + if (!GTK_IS_TOOL_BUTTON(pItem)) + return; + gtk_tool_button_set_label(GTK_TOOL_BUTTON(pItem), MapToGtkAccelerator(rLabel).getStr()); +#else + if (!GTK_IS_BUTTON(pItem)) + return; + ::button_set_label(GTK_BUTTON(pItem), rLabel); +#endif + } + + virtual void set_item_label(const OUString& rIdent, const OUString& rLabel) override + { + GtkWidget* pItem = m_aMap[rIdent]; +#if !GTK_CHECK_VERSION(4, 0, 0) + if (!pItem || !GTK_IS_TOOL_BUTTON(pItem)) + return; + gtk_tool_button_set_label(GTK_TOOL_BUTTON(pItem), MapToGtkAccelerator(rLabel).getStr()); +#else + if (!pItem || !GTK_IS_BUTTON(pItem)) + return; + ::button_set_label(GTK_BUTTON(pItem), rLabel); +#endif + } + + OUString get_item_label(const OUString& rIdent) const override + { +#if !GTK_CHECK_VERSION(4, 0, 0) + const gchar* pText = gtk_tool_button_get_label(GTK_TOOL_BUTTON(m_aMap.find(rIdent)->second)); +#else + const gchar* pText = gtk_button_get_label(GTK_BUTTON(m_aMap.find(rIdent)->second)); +#endif + return OUString(pText, pText ? strlen(pText) : 0, RTL_TEXTENCODING_UTF8); + } + + virtual void set_item_icon_name(const OUString& rIdent, const OUString& rIconName) override + { + GtkWidget* pItem = m_aMap[rIdent]; +#if !GTK_CHECK_VERSION(4, 0, 0) + if (!pItem || !GTK_IS_TOOL_BUTTON(pItem)) + return; +#else + if (!pItem || !GTK_IS_BUTTON(pItem)) + return; +#endif + + GtkWidget* pImage = image_new_from_icon_name(rIconName); + if (pImage) + gtk_widget_show(pImage); + +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_tool_button_set_icon_widget(GTK_TOOL_BUTTON(pItem), pImage); +#else + gtk_button_set_child(GTK_BUTTON(pItem), pImage); + // versions of gtk4 > 4.2.1 might do this on their own + gtk_widget_remove_css_class(GTK_WIDGET(pItem), "text-button"); +#endif + } + + virtual void set_item_image_mirrored(const OUString& rIdent, bool bMirrored) override + { + m_aMirroredMap[rIdent] = bMirrored; + } + + virtual void set_item_image(const OUString& rIdent, const css::uno::Reference<css::graphic::XGraphic>& rIcon) override + { + GtkWidget* pItem = m_aMap[rIdent]; + auto it = m_aMirroredMap.find(rIdent); + bool bMirrored = it != m_aMirroredMap.end() && it->second; +#if !GTK_CHECK_VERSION(4, 0, 0) + if (!pItem || !GTK_IS_TOOL_BUTTON(pItem)) + return; + set_item_image(GTK_TOOL_BUTTON(pItem), rIcon, bMirrored); +#else + if (!pItem) + return; + set_item_image(pItem, rIcon, bMirrored); +#endif + } + + virtual void set_item_image(const OUString& rIdent, VirtualDevice* pDevice) override + { + GtkWidget* pItem = m_aMap[rIdent]; +#if !GTK_CHECK_VERSION(4, 0, 0) + if (!pItem || !GTK_IS_TOOL_BUTTON(pItem)) + return; + set_item_image(GTK_TOOL_BUTTON(pItem), pDevice); +#else + if (!pItem) + return; + set_item_image(pItem, pDevice); +#endif + } + + virtual void set_item_image(int nIndex, const css::uno::Reference<css::graphic::XGraphic>& rIcon) override + { + auto* pItem = toolbar_get_nth_item(nIndex); +#if !GTK_CHECK_VERSION(4, 0, 0) + if (!GTK_IS_TOOL_BUTTON(pItem)) + return; + set_item_image(GTK_TOOL_BUTTON(pItem), rIcon, false); +#else + set_item_image(pItem, rIcon, false); +#endif + } + + virtual void set_item_tooltip_text(int nIndex, const OUString& rTip) override + { + auto* pItem = toolbar_get_nth_item(nIndex); + gtk_widget_set_tooltip_text(GTK_WIDGET(pItem), OUStringToOString(rTip, RTL_TEXTENCODING_UTF8).getStr()); + } + + virtual void set_item_tooltip_text(const OUString& rIdent, const OUString& rTip) override + { + GtkWidget* pItem = GTK_WIDGET(m_aMap[rIdent]); + gtk_widget_set_tooltip_text(pItem, OUStringToOString(rTip, RTL_TEXTENCODING_UTF8).getStr()); + } + + virtual OUString get_item_tooltip_text(const OUString& rIdent) const override + { + GtkWidget* pItem = GTK_WIDGET(m_aMap.find(rIdent)->second); + const gchar* pStr = gtk_widget_get_tooltip_text(pItem); + return OUString(pStr, pStr ? strlen(pStr) : 0, RTL_TEXTENCODING_UTF8); + } + + virtual vcl::ImageType get_icon_size() const override + { +#if GTK_CHECK_VERSION(4, 0, 0) + return m_eImageType; +#else + return GtkToVcl(gtk_toolbar_get_icon_size(m_pToolbar)); +#endif + } + + virtual void set_icon_size(vcl::ImageType eType) override + { +#if GTK_CHECK_VERSION(4, 0, 0) + m_eImageType = eType; +#else + gtk_toolbar_set_icon_size(m_pToolbar, VclToGtk(eType)); +#endif + } + + virtual sal_uInt16 get_modifier_state() const override + { +#if GTK_CHECK_VERSION(4, 0, 0) + GdkDisplay* pDisplay = gtk_widget_get_display(GTK_WIDGET(m_pToolbar)); + GdkSeat* pSeat = gdk_display_get_default_seat(pDisplay); + GdkDevice* pDevice = gdk_seat_get_keyboard(pSeat); + guint nState = gdk_device_get_modifier_state(pDevice); +#else + GdkKeymap* pKeymap = gdk_keymap_get_default(); + guint nState = gdk_keymap_get_modifier_state(pKeymap); +#endif + return GtkSalFrame::GetKeyModCode(nState); + } + + virtual int get_drop_index(const Point& rPoint) const override + { +#if !GTK_CHECK_VERSION(4, 0, 0) + return gtk_toolbar_get_drop_index(m_pToolbar, rPoint.X(), rPoint.Y()); +#else + GtkWidget* pToolbar = GTK_WIDGET(m_pToolbar); + GtkWidget* pTarget = gtk_widget_pick(pToolbar, rPoint.X(), rPoint.Y(), GTK_PICK_DEFAULT); + if (!pTarget || pTarget == pToolbar) + return -1; + int i = 0; + for (GtkWidget* pChild = gtk_widget_get_first_child(GTK_WIDGET(m_pToolbar)); + pChild; pChild = gtk_widget_get_next_sibling(pChild)) + { + if (pChild == pTarget) + return i; + ++i; + } + return -1; +#endif + } + + virtual bool has_focus() const override + { + if (gtk_widget_has_focus(m_pWidget)) + return true; + + GtkWidget* pTopLevel = widget_get_toplevel(m_pWidget); + if (!GTK_IS_WINDOW(pTopLevel)) + return false; + GtkWidget* pFocus = gtk_window_get_focus(GTK_WINDOW(pTopLevel)); + if (!pFocus) + return false; + return gtk_widget_is_ancestor(pFocus, m_pWidget); + } + + virtual void grab_focus() override + { + if (has_focus()) + return; + gtk_widget_grab_focus(m_pWidget); +#if GTK_CHECK_VERSION(4, 0, 0) + bool bHasFocusChild = gtk_widget_get_focus_child(m_pWidget); +#else + bool bHasFocusChild = gtk_container_get_focus_child(GTK_CONTAINER(m_pWidget)); +#endif + if (!bHasFocusChild) + { + if (auto* pItem = toolbar_get_nth_item(0)) + { +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_set_focus_child(m_pWidget, GTK_WIDGET(pItem)); +#else + gtk_container_set_focus_child(GTK_CONTAINER(m_pWidget), GTK_WIDGET(pItem)); +#endif + bHasFocusChild = true; + } + } + if (bHasFocusChild) + { +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_child_focus(gtk_widget_get_focus_child(m_pWidget), GTK_DIR_TAB_FORWARD); +#else + gtk_widget_child_focus(gtk_container_get_focus_child(GTK_CONTAINER(m_pWidget)), GTK_DIR_TAB_FORWARD); +#endif + } + } + + virtual ~GtkInstanceToolbar() override + { + for (auto& a : m_aMap) + g_signal_handlers_disconnect_by_data(a.second, this); + } +}; + +} + +namespace { + +class GtkInstanceLinkButton : public GtkInstanceWidget, 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) + : GtkInstanceWidget(GTK_WIDGET(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 + { + ::button_set_label(GTK_BUTTON(m_pButton), rText); + } + + virtual OUString get_label() const override + { + return ::button_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 void set_label_wrap(bool bWrap) override + { + GtkLabel* pChild = ::get_label_widget(GTK_WIDGET(m_pButton)); + ::set_label_wrap(pChild, bWrap); + gtk_label_set_max_width_chars(pChild, 1); + } + + 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); + GtkInstanceWidget::disable_notify_events(); + } + + virtual void enable_notify_events() override + { + GtkInstanceWidget::enable_notify_events(); + g_signal_handler_unblock(m_pButton, m_nSignalId); + } + + virtual ~GtkInstanceLinkButton() override + { + g_signal_handler_disconnect(m_pButton, m_nSignalId); + } +}; + +} + +namespace { + +class GtkInstanceCheckButton : public GtkInstanceWidget, public virtual weld::CheckButton +{ +private: + GtkCheckButton* m_pCheckButton; + gulong m_nSignalId; + + static void signalToggled(void*, gpointer widget) + { + GtkInstanceCheckButton* pThis = static_cast<GtkInstanceCheckButton*>(widget); + SolarMutexGuard aGuard; + pThis->signal_toggled(); + } + +public: + GtkInstanceCheckButton(GtkCheckButton* pButton, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : GtkInstanceWidget(GTK_WIDGET(pButton), pBuilder, bTakeOwnership) + , m_pCheckButton(pButton) + , m_nSignalId(g_signal_connect(m_pCheckButton, "toggled", G_CALLBACK(signalToggled), this)) + { + } + + virtual void set_active(bool active) override + { + disable_notify_events(); +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_check_button_set_inconsistent(m_pCheckButton, false); + gtk_check_button_set_active(m_pCheckButton, active); +#else + gtk_toggle_button_set_inconsistent(GTK_TOGGLE_BUTTON(m_pCheckButton), false); + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(m_pCheckButton), active); +#endif + enable_notify_events(); + } + + virtual bool get_active() const override + { +#if GTK_CHECK_VERSION(4, 0, 0) + return gtk_check_button_get_active(m_pCheckButton); +#else + return gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(m_pCheckButton)); +#endif + } + + virtual void set_inconsistent(bool inconsistent) override + { +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_check_button_set_inconsistent(m_pCheckButton, inconsistent); +#else + gtk_toggle_button_set_inconsistent(GTK_TOGGLE_BUTTON(m_pCheckButton), inconsistent); +#endif + } + + virtual bool get_inconsistent() const override + { +#if GTK_CHECK_VERSION(4, 0, 0) + return gtk_check_button_get_inconsistent(m_pCheckButton); +#else + return gtk_toggle_button_get_inconsistent(GTK_TOGGLE_BUTTON(m_pCheckButton)); +#endif + } + + virtual void set_label(const OUString& rText) override + { +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_check_button_set_label(m_pCheckButton, MapToGtkAccelerator(rText).getStr()); +#else + ::button_set_label(GTK_BUTTON(m_pCheckButton), rText); +#endif + } + + virtual OUString get_label() const override + { +#if GTK_CHECK_VERSION(4, 0, 0) + const gchar* pStr = gtk_check_button_get_label(m_pCheckButton); + return OUString(pStr, pStr ? strlen(pStr) : 0, RTL_TEXTENCODING_UTF8); +#else + return ::button_get_label(GTK_BUTTON(m_pCheckButton)); +#endif + } + + virtual void set_label_wrap(bool bWrap) override + { + GtkLabel* pChild = ::get_label_widget(GTK_WIDGET(m_pCheckButton)); + ::set_label_wrap(pChild, bWrap); + } + + virtual void disable_notify_events() override + { + g_signal_handler_block(m_pCheckButton, m_nSignalId); + GtkInstanceWidget::disable_notify_events(); + } + + virtual void enable_notify_events() override + { + GtkInstanceWidget::enable_notify_events(); + g_signal_handler_unblock(m_pCheckButton, m_nSignalId); + } + + virtual ~GtkInstanceCheckButton() override + { + g_signal_handler_disconnect(m_pCheckButton, m_nSignalId); + } +}; + +class GtkInstanceRadioButton : public GtkInstanceCheckButton, public virtual weld::RadioButton +{ +public: +#if GTK_CHECK_VERSION(4, 0, 0) + GtkInstanceRadioButton(GtkCheckButton* pButton, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : GtkInstanceCheckButton(pButton, pBuilder, bTakeOwnership) +#else + GtkInstanceRadioButton(GtkRadioButton* pButton, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : GtkInstanceCheckButton(GTK_CHECK_BUTTON(pButton), pBuilder, bTakeOwnership) +#endif + { + } +}; + +} + +namespace { + +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 GtkInstanceLevelBar : public GtkInstanceWidget, public virtual weld::LevelBar +{ +private: + GtkLevelBar* m_pLevelBar; + +public: + GtkInstanceLevelBar(GtkLevelBar* pLevelBar, GtkInstanceBuilder* pBuilder, + bool bTakeOwnership) + : GtkInstanceWidget(GTK_WIDGET(pLevelBar), pBuilder, bTakeOwnership) + , m_pLevelBar(pLevelBar) + { + } + + virtual void set_percentage(double fPercentage) override + { + gtk_level_bar_set_value(m_pLevelBar, fPercentage / 100.0); + } +}; + +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 + { + image_set_from_icon_name(m_pImage, rIconName); + } + + virtual void set_image(VirtualDevice* pDevice) override + { + image_set_from_virtual_device(m_pImage, pDevice); + } + + virtual void set_image(const css::uno::Reference<css::graphic::XGraphic>& rImage) override + { + image_set_from_xgraphic(m_pImage, rImage); + } +}; + +#if GTK_CHECK_VERSION(4, 0, 0) +class GtkInstancePicture: public GtkInstanceWidget, public virtual weld::Image +{ +private: + GtkPicture* m_pPicture; + +public: + GtkInstancePicture(GtkPicture* pPicture, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : GtkInstanceWidget(GTK_WIDGET(pPicture), pBuilder, bTakeOwnership) + , m_pPicture(pPicture) + { + gtk_picture_set_can_shrink(m_pPicture, true); + } + + virtual void set_from_icon_name(const OUString& rIconName) override + { + picture_set_from_icon_name(m_pPicture, rIconName); + } + + virtual void set_image(VirtualDevice* pDevice) override + { + picture_set_from_virtual_device(m_pPicture, pDevice); + } + + virtual void set_image(const css::uno::Reference<css::graphic::XGraphic>& rPicture) override + { + picture_set_from_xgraphic(m_pPicture, rPicture); + } +}; +#endif + +class GtkInstanceCalendar : public GtkInstanceWidget, public virtual weld::Calendar +{ +private: + GtkCalendar* m_pCalendar; +#if GTK_CHECK_VERSION(4, 0, 0) + GtkEventController* m_pKeyController; +#endif + gulong m_nDaySelectedSignalId; + gulong m_nDaySelectedDoubleClickSignalId; + gulong m_nKeyPressEventSignalId; +#if !GTK_CHECK_VERSION(4, 0, 0) + gulong m_nButtonPressEventSignalId; +#endif + + static void signalDaySelected(GtkCalendar*, gpointer widget) + { + GtkInstanceCalendar* pThis = static_cast<GtkInstanceCalendar*>(widget); + SolarMutexGuard aGuard; + pThis->signal_selected(); + } + + static void signalDaySelectedDoubleClick(GtkCalendar*, gpointer widget) + { + GtkInstanceCalendar* pThis = static_cast<GtkInstanceCalendar*>(widget); + SolarMutexGuard aGuard; + pThis->signal_activated(); + } + + bool signal_key_press(guint nKeyVal) + { + if (nKeyVal == GDK_KEY_Return || nKeyVal == GDK_KEY_KP_Enter) + { + SolarMutexGuard aGuard; + signal_activated(); + return true; + } + return false; + } + +#if GTK_CHECK_VERSION(4, 0, 0) + static gboolean signalKeyPress(GtkEventControllerKey*, guint nKeyVal, guint /*nKeyCode*/, GdkModifierType, gpointer widget) + { + GtkInstanceCalendar* pThis = static_cast<GtkInstanceCalendar*>(widget); + return pThis->signal_key_press(nKeyVal); + } +#else + static gboolean signalKeyPress(GtkWidget*, GdkEventKey* pEvent, gpointer widget) + { + GtkInstanceCalendar* pThis = static_cast<GtkInstanceCalendar*>(widget); + return pThis->signal_key_press(pEvent->keyval); + } +#endif + +#if !GTK_CHECK_VERSION(4, 0, 0) + static gboolean signalButton(GtkWidget*, GdkEventButton*, gpointer) + { + // don't let button press get to parent window, for the case of the + // ImplCFieldFloatWin floating window belonging to CalendarField where + // the click on the calendar continues to the parent GtkWindow and + // closePopup is called by GtkSalFrame::signalButton because the click + // window isn't that of the floating parent GtkWindow + return true; + } +#endif + +public: + GtkInstanceCalendar(GtkCalendar* pCalendar, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : GtkInstanceWidget(GTK_WIDGET(pCalendar), pBuilder, bTakeOwnership) + , m_pCalendar(pCalendar) +#if GTK_CHECK_VERSION(4, 0, 0) + , m_pKeyController(gtk_event_controller_key_new()) +#endif + , 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)) +#if GTK_CHECK_VERSION(4, 0, 0) + , m_nKeyPressEventSignalId(g_signal_connect(m_pKeyController, "key-pressed", G_CALLBACK(signalKeyPress), this)) +#else + , m_nKeyPressEventSignalId(g_signal_connect(pCalendar, "key-press-event", G_CALLBACK(signalKeyPress), this)) + , m_nButtonPressEventSignalId(g_signal_connect_after(pCalendar, "button-press-event", G_CALLBACK(signalButton), this)) +#endif + { +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_add_controller(GTK_WIDGET(m_pCalendar), m_pKeyController); +#endif + } + + virtual void set_date(const Date& rDate) override + { + if (!rDate.IsValidAndGregorian()) + return; + + disable_notify_events(); +#if GTK_CHECK_VERSION(4, 0, 0) + GDateTime* pDateTime = g_date_time_new_local(rDate.GetYear(), rDate.GetMonth(), rDate.GetDay(), 0, 0, 0); + gtk_calendar_select_day(m_pCalendar, pDateTime); + g_date_time_unref(pDateTime); +#else + gtk_calendar_select_month(m_pCalendar, rDate.GetMonth() - 1, rDate.GetYear()); + gtk_calendar_select_day(m_pCalendar, rDate.GetDay()); +#endif + enable_notify_events(); + } + + virtual Date get_date() const override + { +#if GTK_CHECK_VERSION(4, 0, 0) + GDateTime* pDateTime = gtk_calendar_get_date(m_pCalendar); + Date aDate(g_date_time_get_day_of_month(pDateTime), + g_date_time_get_month(pDateTime), + g_date_time_get_year(pDateTime)); + g_date_time_unref(pDateTime); + return aDate; +#else + guint year, month, day; + gtk_calendar_get_date(m_pCalendar, &year, &month, &day); + return Date(day, month + 1, year); +#endif + } + + 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 + { +#if GTK_CHECK_VERSION(4, 0, 0) + g_signal_handler_disconnect(m_pKeyController, m_nKeyPressEventSignalId); +#else + g_signal_handler_disconnect(m_pCalendar, m_nButtonPressEventSignalId); + g_signal_handler_disconnect(m_pCalendar, m_nKeyPressEventSignalId); +#endif + g_signal_handler_disconnect(m_pCalendar, m_nDaySelectedDoubleClickSignalId); + g_signal_handler_disconnect(m_pCalendar, m_nDaySelectedSignalId); + } +}; + +} + +namespace +{ + // CSS nodes: entry[.flat][.warning][.error] + void set_widget_css_message_type(GtkWidget* pWidget, weld::EntryMessageType eType) + { +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_remove_css_class(pWidget, "error"); + gtk_widget_remove_css_class(pWidget, "warning"); +#else + GtkStyleContext *pWidgetContext = gtk_widget_get_style_context(pWidget); + gtk_style_context_remove_class(pWidgetContext, "error"); + gtk_style_context_remove_class(pWidgetContext, "warning"); +#endif + + switch (eType) + { + case weld::EntryMessageType::Normal: + break; + case weld::EntryMessageType::Warning: +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_add_css_class(pWidget, "warning"); +#else + gtk_style_context_add_class(pWidgetContext, "warning"); +#endif + break; + case weld::EntryMessageType::Error: +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_add_css_class(pWidget, "error"); +#else + gtk_style_context_add_class(pWidgetContext, "error"); +#endif + break; + } + } + + void set_entry_message_type(GtkEntry* pEntry, weld::EntryMessageType eType) + { + set_widget_css_message_type(GTK_WIDGET(pEntry), eType); + switch (eType) + { + case weld::EntryMessageType::Normal: + gtk_entry_set_icon_from_icon_name(pEntry, GTK_ENTRY_ICON_SECONDARY, nullptr); + break; + case weld::EntryMessageType::Warning: + gtk_entry_set_icon_from_icon_name(pEntry, GTK_ENTRY_ICON_SECONDARY, "dialog-warning"); + break; + case weld::EntryMessageType::Error: + gtk_entry_set_icon_from_icon_name(pEntry, GTK_ENTRY_ICON_SECONDARY, "dialog-error"); + break; + } + } +} + +namespace +{ + +class GtkInstanceEditable : public GtkInstanceWidget, public virtual weld::Entry +{ +protected: + GtkEditable* m_pEditable; + GtkWidget* m_pDelegate; + WidgetFont m_aCustomFont; +private: + gulong m_nChangedSignalId; + gulong m_nInsertTextSignalId; + gulong m_nCursorPosSignalId; + gulong m_nSelectionPosSignalId; + gulong m_nActivateSignalId; + + static void signalChanged(GtkEditable*, gpointer widget) + { + GtkInstanceEditable* pThis = static_cast<GtkInstanceEditable*>(widget); + SolarMutexGuard aGuard; + pThis->signal_changed(); + } + + static void signalInsertText(GtkEditable* pEditable, const gchar* pNewText, gint nNewTextLength, + gint* position, gpointer widget) + { + GtkInstanceEditable* pThis = static_cast<GtkInstanceEditable*>(widget); + SolarMutexGuard aGuard; + pThis->signal_insert_text(pEditable, pNewText, nNewTextLength, position); + } + + void signal_insert_text(GtkEditable* pEditable, 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(pEditable, reinterpret_cast<gpointer>(signalInsertText), this); + gtk_editable_insert_text(pEditable, sFinalText.getStr(), sFinalText.getLength(), position); + g_signal_handlers_unblock_by_func(pEditable, reinterpret_cast<gpointer>(signalInsertText), this); + } + g_signal_stop_emission_by_name(pEditable, "insert-text"); + } + + static void signalCursorPosition(void*, GParamSpec*, gpointer widget) + { + GtkInstanceEditable* pThis = static_cast<GtkInstanceEditable*>(widget); + pThis->signal_cursor_position(); + } + + static void signalActivate(void*, gpointer widget) + { + GtkInstanceEditable* pThis = static_cast<GtkInstanceEditable*>(widget); + pThis->signal_activate(); + } + + virtual void ensureMouseEventWidget() override + { + // The GtkEntry is sufficient to get mouse events without an intermediate GtkEventBox + if (!m_pMouseEventBox) + m_pMouseEventBox = m_pDelegate; + } + +protected: + + virtual void signal_activate() + { + if (m_aActivateHdl.IsSet()) + { + SolarMutexGuard aGuard; + if (m_aActivateHdl.Call(*this)) + g_signal_stop_emission_by_name(m_pDelegate, "activate"); + } + } + + PangoAttrList* get_attributes() + { +#if GTK_CHECK_VERSION(4, 0, 0) + return gtk_text_get_attributes(GTK_TEXT(m_pDelegate)); +#else + return gtk_entry_get_attributes(GTK_ENTRY(m_pDelegate)); +#endif + } + + void set_attributes(PangoAttrList* pAttrs) + { +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_text_set_attributes(GTK_TEXT(m_pDelegate), pAttrs); +#else + gtk_entry_set_attributes(GTK_ENTRY(m_pDelegate), pAttrs); +#endif + } + +public: + GtkInstanceEditable(GtkWidget* pWidget, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : GtkInstanceWidget(pWidget, pBuilder, bTakeOwnership) + , m_pEditable(GTK_EDITABLE(pWidget)) +#if GTK_CHECK_VERSION(4, 0, 0) + , m_pDelegate(GTK_WIDGET(gtk_editable_get_delegate(m_pEditable))) +#else + , m_pDelegate(pWidget) +#endif + , m_aCustomFont(m_pWidget) + , m_nChangedSignalId(g_signal_connect(m_pEditable, "changed", G_CALLBACK(signalChanged), this)) + , m_nInsertTextSignalId(g_signal_connect(m_pEditable, "insert-text", G_CALLBACK(signalInsertText), this)) + , m_nCursorPosSignalId(g_signal_connect(m_pEditable, "notify::cursor-position", G_CALLBACK(signalCursorPosition), this)) + , m_nSelectionPosSignalId(g_signal_connect(m_pEditable, "notify::selection-bound", G_CALLBACK(signalCursorPosition), this)) + , m_nActivateSignalId(g_signal_connect(m_pDelegate, "activate", G_CALLBACK(signalActivate), this)) + { + } + + virtual void set_text(const OUString& rText) override + { + disable_notify_events(); +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_editable_set_text(m_pEditable, OUStringToOString(rText, RTL_TEXTENCODING_UTF8).getStr()); +#else + gtk_entry_set_text(GTK_ENTRY(m_pDelegate), OUStringToOString(rText, RTL_TEXTENCODING_UTF8).getStr()); +#endif + enable_notify_events(); + } + + virtual OUString get_text() const override + { +#if GTK_CHECK_VERSION(4, 0, 0) + const gchar* pText = gtk_editable_get_text(m_pEditable); +#else + const gchar* pText = gtk_entry_get_text(GTK_ENTRY(m_pDelegate)); +#endif + OUString sRet(pText, pText ? strlen(pText) : 0, RTL_TEXTENCODING_UTF8); + return sRet; + } + + virtual void set_width_chars(int nChars) override + { + disable_notify_events(); +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_editable_set_width_chars(m_pEditable, nChars); + gtk_editable_set_max_width_chars(m_pEditable, nChars); +#else + gtk_entry_set_width_chars(GTK_ENTRY(m_pDelegate), nChars); + gtk_entry_set_max_width_chars(GTK_ENTRY(m_pDelegate), nChars); +#endif + enable_notify_events(); + } + + virtual int get_width_chars() const override + { +#if GTK_CHECK_VERSION(4, 0, 0) + return gtk_editable_get_width_chars(m_pEditable); +#else + return gtk_entry_get_width_chars(GTK_ENTRY(m_pDelegate)); +#endif + } + + virtual void set_max_length(int nChars) override + { + disable_notify_events(); +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_text_set_max_length(GTK_TEXT(m_pDelegate), nChars); +#else + gtk_entry_set_max_length(GTK_ENTRY(m_pDelegate), nChars); +#endif + enable_notify_events(); + } + + virtual void select_region(int nStartPos, int nEndPos) override + { + disable_notify_events(); + gtk_editable_select_region(m_pEditable, nStartPos, nEndPos); + enable_notify_events(); + } + + bool get_selection_bounds(int& rStartPos, int& rEndPos) override + { + return gtk_editable_get_selection_bounds(m_pEditable, &rStartPos, &rEndPos); + } + + virtual void replace_selection(const OUString& rText) override + { + disable_notify_events(); + gtk_editable_delete_selection(m_pEditable); + OString sText(OUStringToOString(rText, RTL_TEXTENCODING_UTF8)); + gint position = gtk_editable_get_position(m_pEditable); + gtk_editable_insert_text(m_pEditable, sText.getStr(), sText.getLength(), + &position); + enable_notify_events(); + } + + virtual void set_position(int nCursorPos) override + { + disable_notify_events(); + gtk_editable_set_position(m_pEditable, nCursorPos); + enable_notify_events(); + } + + virtual int get_position() const override + { + return gtk_editable_get_position(m_pEditable); + } + + virtual void set_editable(bool bEditable) override + { + gtk_editable_set_editable(m_pEditable, bEditable); + } + + virtual bool get_editable() const override + { + return gtk_editable_get_editable(m_pEditable); + } + + virtual void set_overwrite_mode(bool bOn) override + { +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_text_set_overwrite_mode(GTK_TEXT(m_pDelegate), bOn); +#else + gtk_entry_set_overwrite_mode(GTK_ENTRY(m_pDelegate), bOn); +#endif + } + + virtual bool get_overwrite_mode() const override + { +#if GTK_CHECK_VERSION(4, 0, 0) + return gtk_text_get_overwrite_mode(GTK_TEXT(m_pDelegate)); +#else + return gtk_entry_get_overwrite_mode(GTK_ENTRY(m_pDelegate)); +#endif + } + + virtual void set_message_type(weld::EntryMessageType eType) override + { +#if GTK_CHECK_VERSION(4, 0, 0) + if (!GTK_IS_ENTRY(m_pDelegate)) + { + ::set_widget_css_message_type(m_pDelegate, eType); + return; + } +#endif + ::set_entry_message_type(GTK_ENTRY(m_pDelegate), eType); + } + + virtual void disable_notify_events() override + { + g_signal_handler_block(m_pDelegate, m_nActivateSignalId); + g_signal_handler_block(m_pEditable, m_nSelectionPosSignalId); + g_signal_handler_block(m_pEditable, m_nCursorPosSignalId); + g_signal_handler_block(m_pEditable, m_nInsertTextSignalId); + g_signal_handler_block(m_pEditable, m_nChangedSignalId); + GtkInstanceWidget::disable_notify_events(); + } + + virtual void enable_notify_events() override + { + GtkInstanceWidget::enable_notify_events(); + g_signal_handler_unblock(m_pEditable, m_nChangedSignalId); + g_signal_handler_unblock(m_pEditable, m_nInsertTextSignalId); + g_signal_handler_unblock(m_pEditable, m_nCursorPosSignalId); + g_signal_handler_unblock(m_pEditable, m_nSelectionPosSignalId); + g_signal_handler_unblock(m_pDelegate, m_nActivateSignalId); + } + + virtual vcl::Font get_font() override + { + if (const vcl::Font* pFont = m_aCustomFont.get_custom_font()) + return *pFont; + return GtkInstanceWidget::get_font(); + } + + void set_font_color(const Color& rColor) override + { + PangoAttrList* pOrigList = get_attributes(); + if (rColor == COL_AUTO && !pOrigList) // nothing to do + return; + + PangoAttrType aFilterAttrs[] = {PANGO_ATTR_FOREGROUND, PANGO_ATTR_INVALID}; + + PangoAttrList* pAttrs = pOrigList ? pango_attr_list_copy(pOrigList) : pango_attr_list_new(); + PangoAttrList* pRemovedAttrs = pOrigList ? pango_attr_list_filter(pAttrs, filter_pango_attrs, &aFilterAttrs) : nullptr; + + if (rColor != COL_AUTO) + pango_attr_list_insert(pAttrs, pango_attr_foreground_new(rColor.GetRed()/255.0, rColor.GetGreen()/255.0, rColor.GetBlue()/255.0)); + + set_attributes(pAttrs); + pango_attr_list_unref(pAttrs); + pango_attr_list_unref(pRemovedAttrs); + } + + void fire_signal_changed() + { + signal_changed(); + } + + virtual void cut_clipboard() override + { +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_activate_action(m_pDelegate, "cut.clipboard", nullptr); +#else + gtk_editable_cut_clipboard(m_pEditable); +#endif + } + + virtual void copy_clipboard() override + { +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_activate_action(m_pDelegate, "copy.clipboard", nullptr); +#else + gtk_editable_copy_clipboard(m_pEditable); +#endif + } + + virtual void paste_clipboard() override + { +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_activate_action(m_pDelegate, "paste.clipboard", nullptr); +#else + gtk_editable_paste_clipboard(m_pEditable); +#endif + } + + virtual void set_placeholder_text(const OUString& rText) override + { +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_text_set_placeholder_text(GTK_TEXT(m_pDelegate), rText.toUtf8().getStr()); +#else + gtk_entry_set_placeholder_text(GTK_ENTRY(m_pDelegate), rText.toUtf8().getStr()); +#endif + } + + virtual void grab_focus() override + { + if (has_focus()) + return; +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_text_grab_focus_without_selecting(GTK_TEXT(m_pDelegate)); +#else + gtk_entry_grab_focus_without_selecting(GTK_ENTRY(m_pDelegate)); +#endif + } + + virtual void set_alignment(TxtAlign eXAlign) override + { + gfloat xalign = 0; + switch (eXAlign) + { + case TxtAlign::Left: + xalign = 0.0; + break; + case TxtAlign::Center: + xalign = 0.5; + break; + case TxtAlign::Right: + xalign = 1.0; + break; + } +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_editable_set_alignment(m_pEditable, xalign); +#else + gtk_entry_set_alignment(GTK_ENTRY(m_pDelegate), xalign); +#endif + } + + virtual ~GtkInstanceEditable() override + { + g_signal_handler_disconnect(m_pDelegate, m_nActivateSignalId); + g_signal_handler_disconnect(m_pEditable, m_nSelectionPosSignalId); + g_signal_handler_disconnect(m_pEditable, m_nCursorPosSignalId); + g_signal_handler_disconnect(m_pEditable, m_nInsertTextSignalId); + g_signal_handler_disconnect(m_pEditable, m_nChangedSignalId); + } +}; + +class GtkInstanceEntry : public GtkInstanceEditable +{ +private: +#if !GTK_CHECK_VERSION(4, 0, 0) + GtkEntry* m_pEntry; + GtkOverlay* m_pPlaceHolderReplacement; + GtkLabel* m_pPlaceHolderLabel; + gulong m_nEntryFocusInSignalId; + gulong m_nEntryFocusOutSignalId; + gulong m_nEntryTextLengthSignalId; + gulong m_nEntryScrollOffsetSignalId; + guint m_nUpdatePlaceholderReplacementIdle; + + static gboolean do_update_placeholder_replacement(gpointer widget) + { + GtkInstanceEntry* pThis = static_cast<GtkInstanceEntry*>(widget); + pThis->update_placeholder_replacement(); + return false; + } + + void update_placeholder_replacement() + { + m_nUpdatePlaceholderReplacementIdle = 0; + + const char* placeholder_text = gtk_entry_get_placeholder_text(m_pEntry); + const bool bShow = placeholder_text && !gtk_entry_get_text_length(m_pEntry) && + gtk_widget_has_focus(GTK_WIDGET(m_pEntry)); + if (bShow) + { + GdkRectangle text_area; + gtk_entry_get_text_area(m_pEntry, &text_area); + gint x; + gtk_entry_get_layout_offsets(m_pEntry, &x, nullptr); + gtk_widget_set_margin_start(GTK_WIDGET(m_pPlaceHolderLabel), x); + gtk_widget_set_margin_end(GTK_WIDGET(m_pPlaceHolderLabel), x); + gtk_label_set_text(m_pPlaceHolderLabel, placeholder_text); + gtk_widget_show(GTK_WIDGET(m_pPlaceHolderLabel)); + } + else + gtk_widget_hide(GTK_WIDGET(m_pPlaceHolderLabel)); + } + + void launch_update_placeholder_replacement() + { + // do it in the next event cycle so the GtkEntry has done its layout + // and gtk_entry_get_layout_offsets returns the right results + if (m_nUpdatePlaceholderReplacementIdle) + return; + // G_PRIORITY_LOW so gtk's idles are run before this + m_nUpdatePlaceholderReplacementIdle = g_idle_add_full(G_PRIORITY_LOW, do_update_placeholder_replacement, this, nullptr); + } + + static gboolean signalEntryFocusIn(GtkWidget*, GdkEvent*, gpointer widget) + { + GtkInstanceEntry* pThis = static_cast<GtkInstanceEntry*>(widget); + pThis->launch_update_placeholder_replacement(); + return false; + } + + static gboolean signalEntryFocusOut(GtkWidget*, GdkEvent*, gpointer widget) + { + GtkInstanceEntry* pThis = static_cast<GtkInstanceEntry*>(widget); + pThis->launch_update_placeholder_replacement(); + return false; + } + + static void signalEntryTextLength(void*, GParamSpec*, gpointer widget) + { + GtkInstanceEntry* pThis = static_cast<GtkInstanceEntry*>(widget); + pThis->launch_update_placeholder_replacement(); + } + + static void signalEntryScrollOffset(void*, GParamSpec*, gpointer widget) + { + // this property affects the x-position of the text area + GtkInstanceEntry* pThis = static_cast<GtkInstanceEntry*>(widget); + pThis->launch_update_placeholder_replacement(); + } + +#endif + +public: + GtkInstanceEntry(GtkEntry* pEntry, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : GtkInstanceEditable(GTK_WIDGET(pEntry), pBuilder, bTakeOwnership) +#if !GTK_CHECK_VERSION(4, 0, 0) + , m_pEntry(pEntry) + , m_pPlaceHolderReplacement(nullptr) + , m_pPlaceHolderLabel(nullptr) + , m_nEntryFocusInSignalId(0) + , m_nEntryFocusOutSignalId(0) + , m_nEntryTextLengthSignalId(0) + , m_nEntryScrollOffsetSignalId(0) + , m_nUpdatePlaceholderReplacementIdle(0) +#endif + { +#if !GTK_CHECK_VERSION(4, 0, 0) + // tdf#150810 fake getting placeholders visible even when GtkEntry has focus in gtk3. + // In gtk4 this works out of the box, for gtk3 fake it by having a GtkLabel in an + // overlay and show that label if the placeholder would be shown if there was + // no focus + const char* pPlaceHolderText = gtk_entry_get_placeholder_text(m_pEntry); + if (pPlaceHolderText ? strlen(pPlaceHolderText) : 0) + { + m_pPlaceHolderReplacement = GTK_OVERLAY(gtk_overlay_new()); + m_pPlaceHolderLabel = GTK_LABEL(gtk_label_new(nullptr)); + + GtkStyleContext *pStyleContext = gtk_widget_get_style_context(GTK_WIDGET(m_pEntry)); + GdkRGBA fg = { 0.5, 0.5, 0.5, 0.0 }; + gtk_style_context_lookup_color(pStyleContext, "placeholder_text_color", &fg); + + auto red = std::clamp(fg.red * 65535 + 0.5, 0.0, 65535.0); + auto green = std::clamp(fg.green * 65535 + 0.5, 0.0, 65535.0); + auto blue = std::clamp(fg.blue * 65535 + 0.5, 0.0, 65535.0); + + PangoAttribute *pAttr = pango_attr_foreground_new(red, green, blue); + pAttr->start_index = 0; + pAttr->end_index = G_MAXINT; + PangoAttrList* pAttrList = pango_attr_list_new(); + pango_attr_list_insert(pAttrList, pAttr); + gtk_label_set_attributes(m_pPlaceHolderLabel, pAttrList); + pango_attr_list_unref(pAttrList); + + // The GtkEntry will have the placeholder as the text to analyze here, assumes there is no initial text, just placeholder + const bool bRTL = PANGO_DIRECTION_RTL == pango_context_get_base_dir(pango_layout_get_context(gtk_entry_get_layout(m_pEntry))); + SAL_WARN_IF(gtk_entry_get_text_length(m_pEntry), "vcl.gtk", "don't have a placeholder set, but also initial text"); + gtk_label_set_xalign(m_pPlaceHolderLabel, bRTL ? 1.0 : 0.0); + + gtk_overlay_add_overlay(m_pPlaceHolderReplacement, GTK_WIDGET(m_pPlaceHolderLabel)); + insertAsParent(GTK_WIDGET(m_pEntry), GTK_WIDGET(m_pPlaceHolderReplacement)); + m_nEntryFocusInSignalId = g_signal_connect_after(m_pEntry, "focus-in-event", G_CALLBACK(signalEntryFocusIn), this); + m_nEntryFocusOutSignalId = g_signal_connect_after(m_pEntry, "focus-out-event", G_CALLBACK(signalEntryFocusOut), this); + m_nEntryTextLengthSignalId = g_signal_connect(m_pEntry, "notify::text-length", G_CALLBACK(signalEntryTextLength), this); + m_nEntryScrollOffsetSignalId = g_signal_connect(m_pEntry, "notify::scroll-offset", G_CALLBACK(signalEntryScrollOffset), this); + } +#endif + } + + virtual void set_font(const vcl::Font& rFont) override + { + m_aCustomFont.use_custom_font(&rFont, u"entry"); + } + +#if !GTK_CHECK_VERSION(4, 0, 0) + + virtual void show() override + { + GtkInstanceEditable::show(); + if (m_pPlaceHolderReplacement) + gtk_widget_show(GTK_WIDGET(m_pPlaceHolderReplacement)); + } + + virtual void hide() override + { + if (m_pPlaceHolderReplacement) + gtk_widget_hide(GTK_WIDGET(m_pPlaceHolderReplacement)); + GtkInstanceEditable::hide(); + } + + virtual ~GtkInstanceEntry() override + { + if (m_nUpdatePlaceholderReplacementIdle) + g_source_remove(m_nUpdatePlaceholderReplacementIdle); + if (m_nEntryFocusInSignalId) + g_signal_handler_disconnect(m_pEntry, m_nEntryFocusInSignalId); + if (m_nEntryFocusOutSignalId) + g_signal_handler_disconnect(m_pEntry, m_nEntryFocusOutSignalId); + if (m_nEntryTextLengthSignalId) + g_signal_handler_disconnect(m_pEntry, m_nEntryTextLengthSignalId); + if (m_nEntryScrollOffsetSignalId) + g_signal_handler_disconnect(m_pEntry, m_nEntryScrollOffsetSignalId); + } +#endif +}; + +} + +namespace +{ + + struct Search + { + OString str; + int index; + int col; + Search(std::u16string_view 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, std::u16string_view 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) +{ + // gtk4: _TREE_VIEW_VERTICAL_SEPARATOR define in gtk/gtktreeview.c + gint nVerticalSeparator = 2; +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_style_get(GTK_WIDGET(pTreeView), "vertical-separator", &nVerticalSeparator, nullptr); +#else + (void)pTreeView; +#endif + 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) / 2; +} + +#if !GTK_CHECK_VERSION(4, 0, 0) +int get_height_rows(int nRowHeight, int nSeparatorHeight, int nRows) +{ + return (nRowHeight * nRows) + (nSeparatorHeight * (nRows + 1)); +} +#endif + +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; +} + +struct GtkTreeRowReferenceDeleter +{ + void operator()(GtkTreeRowReference* p) const + { + gtk_tree_row_reference_free(p); + } +}; + +bool separator_function(const GtkTreePath* path, const std::vector<std::unique_ptr<GtkTreeRowReference, GtkTreeRowReferenceDeleter>>& rSeparatorRows) +{ + bool bFound = false; + for (auto& a : rSeparatorRows) + { + 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; +} + +void tree_store_set(GtkTreeModel* pTreeModel, GtkTreeIter *pIter, ...) +{ + va_list args; + + va_start(args, pIter); + gtk_tree_store_set_valist(GTK_TREE_STORE(pTreeModel), pIter, args); + va_end(args); +} + +void list_store_set(GtkTreeModel* pTreeModel, GtkTreeIter *pIter, ...) +{ + va_list args; + + va_start(args, pIter); + gtk_list_store_set_valist(GTK_LIST_STORE(pTreeModel), pIter, args); + va_end(args); +} + +void tree_store_insert_with_values(GtkTreeModel* pTreeModel, GtkTreeIter *pIter, GtkTreeIter *pParent, gint nPos, + gint nTextCol, const gchar* pText, + gint nIdCol, const gchar* pId) +{ + gtk_tree_store_insert_with_values(GTK_TREE_STORE(pTreeModel), pIter, pParent, nPos, + nTextCol, pText, nIdCol, pId, -1); +} + +void list_store_insert_with_values(GtkTreeModel* pTreeModel, GtkTreeIter *pIter, GtkTreeIter *pParent, gint nPos, + gint nTextCol, const gchar* pText, + gint nIdCol, const gchar* pId) +{ + assert(!pParent); (void)pParent; + gtk_list_store_insert_with_values(GTK_LIST_STORE(pTreeModel), pIter, nPos, + nTextCol, pText, nIdCol, pId, -1); +} + +void tree_store_prepend(GtkTreeModel* pTreeModel, GtkTreeIter *pIter, GtkTreeIter *pParent) +{ + gtk_tree_store_prepend(GTK_TREE_STORE(pTreeModel), pIter, pParent); +} + +void list_store_prepend(GtkTreeModel* pTreeModel, GtkTreeIter *pIter, GtkTreeIter *pParent) +{ + assert(!pParent); (void)pParent; + gtk_list_store_prepend(GTK_LIST_STORE(pTreeModel), pIter); +} + +void tree_store_insert(GtkTreeModel* pTreeModel, GtkTreeIter *pIter, GtkTreeIter *pParent, gint nPosition) +{ + gtk_tree_store_insert(GTK_TREE_STORE(pTreeModel), pIter, pParent, nPosition); +} + +void list_store_insert(GtkTreeModel* pTreeModel, GtkTreeIter *pIter, GtkTreeIter *pParent, gint nPosition) +{ + assert(!pParent); (void)pParent; + gtk_list_store_insert(GTK_LIST_STORE(pTreeModel), pIter, nPosition); +} + +void tree_store_clear(GtkTreeModel* pTreeModel) +{ + gtk_tree_store_clear(GTK_TREE_STORE(pTreeModel)); +} + +void list_store_clear(GtkTreeModel* pTreeModel) +{ + gtk_list_store_clear(GTK_LIST_STORE(pTreeModel)); +} + +bool tree_store_remove(GtkTreeModel* pTreeModel, GtkTreeIter *pIter) +{ + return gtk_tree_store_remove(GTK_TREE_STORE(pTreeModel), pIter); +} + +bool list_store_remove(GtkTreeModel* pTreeModel, GtkTreeIter *pIter) +{ + return gtk_list_store_remove(GTK_LIST_STORE(pTreeModel), pIter); +} + +void tree_store_swap(GtkTreeModel* pTreeModel, GtkTreeIter* pIter1, GtkTreeIter* pIter2) +{ + gtk_tree_store_swap(GTK_TREE_STORE(pTreeModel), pIter1, pIter2); +} + +void list_store_swap(GtkTreeModel* pTreeModel, GtkTreeIter* pIter1, GtkTreeIter* pIter2) +{ + gtk_list_store_swap(GTK_LIST_STORE(pTreeModel), pIter1, pIter2); +} + +void tree_store_set_value(GtkTreeModel* pTreeModel, GtkTreeIter* pIter, gint nColumn, GValue* pValue) +{ + gtk_tree_store_set_value(GTK_TREE_STORE(pTreeModel), pIter, nColumn, pValue); +} + +void list_store_set_value(GtkTreeModel* pTreeModel, GtkTreeIter* pIter, gint nColumn, GValue* pValue) +{ + gtk_list_store_set_value(GTK_LIST_STORE(pTreeModel), pIter, nColumn, pValue); +} + +int promote_arg(bool bArg) +{ + return static_cast<int>(bArg); +} + +class GtkInstanceTreeView : public GtkInstanceWidget, public virtual weld::TreeView +{ +private: + GtkTreeView* m_pTreeView; + GtkTreeModel* m_pTreeModel; + + typedef void(*setterFnc)(GtkTreeModel*, GtkTreeIter*, ...); + setterFnc m_Setter; + + typedef void(*insertWithValuesFnc)(GtkTreeModel*, GtkTreeIter*, GtkTreeIter*, gint, gint, const gchar*, gint, const gchar*); + insertWithValuesFnc m_InsertWithValues; + + typedef void(*insertFnc)(GtkTreeModel*, GtkTreeIter*, GtkTreeIter*, gint); + insertFnc m_Insert; + + typedef void(*prependFnc)(GtkTreeModel*, GtkTreeIter*, GtkTreeIter*); + prependFnc m_Prepend; + + typedef void(*clearFnc)(GtkTreeModel*); + clearFnc m_Clear; + + typedef bool(*removeFnc)(GtkTreeModel*, GtkTreeIter*); + removeFnc m_Remove; + + typedef void(*swapFnc)(GtkTreeModel*, GtkTreeIter*, GtkTreeIter*); + swapFnc m_Swap; + + typedef void(*setValueFnc)(GtkTreeModel*, GtkTreeIter*, gint, GValue*); + setValueFnc m_SetValue; + + 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; + // which rows are separators (rare) + std::vector<std::unique_ptr<GtkTreeRowReference, GtkTreeRowReferenceDeleter>> m_aSeparatorRows; + std::vector<GtkSortType> m_aSavedSortTypes; + std::vector<int> m_aSavedSortColumns; + bool m_bWorkAroundBadDragRegion; + bool m_bInDrag; + bool m_bChangedByMouse; + gint m_nTextCol; + gint m_nTextView; + gint m_nImageCol; + gint m_nExpanderToggleCol; + 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; +#if !GTK_CHECK_VERSION(4, 0, 0) + gulong m_nPopupMenuSignalId; + gulong m_nKeyPressSignalId; + gulong m_nCrossingSignalid; +#endif + 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); + +#if !GTK_CHECK_VERSION(4, 0, 0) + GdkEvent *pEvent = gtk_get_current_event(); + m_bChangedByMouse = pEvent && categorizeEvent(pEvent) == VclInputFlags::MOUSE; +#else + //TODO maybe iterate over gtk_widget_observe_controllers looking for a motion controller +#endif + + 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(m_pTreeModel, &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) + { + m_InsertWithValues(m_pTreeModel, &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()); + + if (pIconName) + { + GdkPixbuf* pixbuf = getPixbuf(*pIconName); + m_Setter(m_pTreeModel, &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); + + m_Setter(m_pTreeModel, &iter, m_nImageCol, target, -1); + cairo_surface_destroy(target); + } + } + + bool separator_function(const GtkTreePath* path) + { + return ::separator_function(path, m_aSeparatorRows); + } + + static gboolean separatorFunction(GtkTreeModel* pTreeModel, GtkTreeIter* pIter, gpointer widget) + { + GtkInstanceTreeView* pThis = static_cast<GtkInstanceTreeView*>(widget); + GtkTreePath* path = gtk_tree_model_get_path(pTreeModel, pIter); + bool bRet = pThis->separator_function(path); + gtk_tree_path_free(path); + return bRet; + } + + OUString get(const GtkTreeIter& iter, int col) const + { + gchar* pStr; + gtk_tree_model_get(m_pTreeModel, 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; + GtkTreeIter iter; + if (gtk_tree_model_iter_nth_child(m_pTreeModel, &iter, nullptr, pos)) + sRet = get(iter, col); + return sRet; + } + + gint get_int(const GtkTreeIter& iter, int col) const + { + gint nRet(-1); + gtk_tree_model_get(m_pTreeModel, const_cast<GtkTreeIter*>(&iter), col, &nRet, -1); + return nRet; + } + + gint get_int(int pos, int col) const + { + gint nRet(-1); + GtkTreeIter iter; + if (gtk_tree_model_iter_nth_child(m_pTreeModel, &iter, nullptr, pos)) + nRet = get_int(iter, col); + gtk_tree_model_get(m_pTreeModel, &iter, col, &nRet, -1); + return nRet; + } + + bool get_bool(const GtkTreeIter& iter, int col) const + { + gboolean bRet(false); + gtk_tree_model_get(m_pTreeModel, const_cast<GtkTreeIter*>(&iter), col, &bRet, -1); + return bRet; + } + + bool get_bool(int pos, int col) const + { + bool bRet(false); + GtkTreeIter iter; + if (gtk_tree_model_iter_nth_child(m_pTreeModel, &iter, nullptr, pos)) + bRet = get_bool(iter, col); + return bRet; + } + + void set_toggle(const GtkTreeIter& iter, TriState eState, int col) + { + if (col == -1) + col = m_nExpanderToggleCol; + else + col = to_internal_model(col); + + if (eState == TRISTATE_INDET) + { + m_Setter(m_pTreeModel, const_cast<GtkTreeIter*>(&iter), + m_aToggleVisMap[col], promote_arg(true), // checkbuttons are invisible until toggled on or off + m_aToggleTriStateMap[col], promote_arg(true), // tristate on + -1); + } + else + { + m_Setter(m_pTreeModel, const_cast<GtkTreeIter*>(&iter), + m_aToggleVisMap[col], promote_arg(true), // checkbuttons are invisible until toggled on or off + m_aToggleTriStateMap[col], promote_arg(false), // tristate off + col, promote_arg(eState == TRISTATE_TRUE), // set toggle state + -1); + } + } + + void set(const GtkTreeIter& iter, int col, std::u16string_view rText) + { + OString aStr(OUStringToOString(rText, RTL_TEXTENCODING_UTF8)); + m_Setter(m_pTreeModel, const_cast<GtkTreeIter*>(&iter), col, aStr.getStr(), -1); + } + + void set(int pos, int col, std::u16string_view rText) + { + GtkTreeIter iter; + if (gtk_tree_model_iter_nth_child(m_pTreeModel, &iter, nullptr, pos)) + set(iter, col, rText); + } + + void set(const GtkTreeIter& iter, int col, bool bOn) + { + m_Setter(m_pTreeModel, const_cast<GtkTreeIter*>(&iter), col, promote_arg(bOn), -1); + } + + void set(int pos, int col, bool bOn) + { + GtkTreeIter iter; + if (gtk_tree_model_iter_nth_child(m_pTreeModel, &iter, nullptr, pos)) + set(iter, col, bOn); + } + + void set(const GtkTreeIter& iter, int col, gint bInt) + { + m_Setter(m_pTreeModel, const_cast<GtkTreeIter*>(&iter), col, bInt, -1); + } + + void set(int pos, int col, gint bInt) + { + GtkTreeIter iter; + if (gtk_tree_model_iter_nth_child(m_pTreeModel, &iter, nullptr, pos)) + set(iter, col, bInt); + } + + void set(const GtkTreeIter& iter, int col, double fValue) + { + m_Setter(m_pTreeModel, const_cast<GtkTreeIter*>(&iter), col, fValue, -1); + } + + void set(int pos, int col, double fValue) + { + GtkTreeIter iter; + if (gtk_tree_model_iter_nth_child(m_pTreeModel, &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 + { + GtkTreePath* pPath = gtk_tree_model_get_path(m_pTreeModel, &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(m_pTreeModel, &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) + { + m_Remove(m_pTreeModel, &aIter.iter); + + pPlaceHolderPath = gtk_tree_model_get_path(m_pTreeModel, &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); + } + m_aExpandingPlaceHolderParents.erase(pPlaceHolderPath); + gtk_tree_path_free(pPlaceHolderPath); + } + + enable_notify_events(); + return bRet; + } + + bool signal_test_collapse_row(const 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); + + // additionally set the cursor into the row the toggled element is in + gtk_tree_view_set_cursor(m_pTreeView, tree_path, nullptr, false); + + GtkTreeIter iter; + gtk_tree_model_get_iter(m_pTreeModel, &iter, tree_path); + + gboolean bRet(false); + gtk_tree_model_get(m_pTreeModel, &iter, nCol, &bRet, -1); + bRet = !bRet; + m_Setter(m_pTreeModel, &iter, nCol, bRet, -1); + + set(iter, m_aToggleTriStateMap[nCol], false); + + signal_toggled(iter_col(GtkInstanceTreeIter(iter), to_external_model(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); + + GtkInstanceTreeIter aGtkIter(nullptr); + gtk_tree_model_get_iter(m_pTreeModel, &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); + + GtkInstanceTreeIter aGtkIter(nullptr); + gtk_tree_model_get_iter(m_pTreeModel, &aGtkIter.iter, tree_path); + gtk_tree_path_free(tree_path); + + OUString sText(pNewText, pNewText ? strlen(pNewText) : 0, RTL_TEXTENCODING_UTF8); + if (signal_editing_done(iter_string(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(); + } + + // The outside concept of a column maps to a gtk CellRenderer, rather than + // a TreeViewColumn. If the first TreeViewColumn has a leading Toggle Renderer + // and/or a leading Image Renderer, those are considered special expander + // columns and precede index 0 and can be accessed via outside index -1 + int to_external_model(int modelcol) const + { + if (m_nExpanderToggleCol != -1) + --modelcol; + if (m_nExpanderImageCol != -1) + --modelcol; + return modelcol; + } + + int to_internal_model(int modelcol) const + { + if (m_nExpanderToggleCol != -1) + ++modelcol; + if (m_nExpanderImageCol != -1) + ++modelcol; + return modelcol; + } + + void set_column_editable(int nCol, bool bEditable) + { + nCol = to_internal_model(nCol); + + 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()); + } + +#if !GTK_CHECK_VERSION(4, 0, 0) + 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(m_pTreeModel, &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); + } +#endif + + 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_CHECK_VERSION(4, 0, 0) + if (!gtk_tree_view_get_tooltip_context(pTreeView, x, y, keyboard_tip, &pModel, &pPath, &iter)) + return false; +#else + if (!gtk_tree_view_get_tooltip_context(pTreeView, &x, &y, keyboard_tip, &pModel, &pPath, &iter)) + return false; +#endif + OUString aTooltip = pThis->signal_query_tooltip(GtkInstanceTreeIter(iter)); + if (!aTooltip.isEmpty()) + { + 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 !aTooltip.isEmpty(); + } + + 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) + m_Setter(m_pTreeModel, const_cast<GtkTreeIter*>(&iter), m_nIdCol + 1, nullptr, -1); + else + { + GdkRGBA aColor{rColor.GetRed()/255.0f, rColor.GetGreen()/255.0f, rColor.GetBlue()/255.0f, 0}; + m_Setter(m_pTreeModel, const_cast<GtkTreeIter*>(&iter), m_nIdCol + 1, &aColor, -1); + } + } + + int get_expander_size() const + { + // gtk4: _TREE_VIEW_EXPANDER_SIZE define in gtk/gtktreeview.c + gint nExpanderSize = 16; + // gtk4: _TREE_VIEW_HORIZONTAL_SEPARATOR define in gtk/gtktreeview.c + gint nHorizontalSeparator = 4; + +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_style_get(GTK_WIDGET(m_pTreeView), + "expander-size", &nExpanderSize, + "horizontal-separator", &nHorizontalSeparator, + nullptr); +#endif + + 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); + GtkTreeIter tmp; + GtkTreeIter iter = rGtkIter.iter; + + bool ret = gtk_tree_model_iter_children(m_pTreeModel, &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(m_pTreeModel, &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(m_pTreeModel, &tmp, &iter)) + { + iter = tmp; + if (gtk_tree_model_iter_next(m_pTreeModel, &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; + } + +#if !GTK_CHECK_VERSION(4, 0, 0) + // tdf#154565 ignore the crossing event if it was triggered ultimately by a + // key stroke which is likely from exiting the search box. This way we can + // avoid the problem that with hover-selection that after return is used in + // the search box, selecting a matching row, that during teardown of the + // widget the box is hidden, and the crossing notification triggers + // selection of a different row under the mouse. If needs be this could be + // refined further to only happen for a specific key or other details of + // the triggering event + static gboolean signalCrossing(GtkWidget*, GdkEventCrossing*, gpointer) + { + if (GdkEvent *pEvent = gtk_get_current_event()) + { + const bool bCrossingTriggeredByKeyStroke = gdk_event_get_event_type(pEvent) == GDK_KEY_PRESS; + gdk_event_free(pEvent); + return bCrossingTriggeredByKeyStroke; + } + + return false; + } +#endif + +public: + GtkInstanceTreeView(GtkTreeView* pTreeView, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : GtkInstanceWidget(GTK_WIDGET(pTreeView), pBuilder, bTakeOwnership) + , m_pTreeView(pTreeView) + , m_pTreeModel(gtk_tree_view_get_model(m_pTreeView)) + , m_bWorkAroundBadDragRegion(false) + , m_bInDrag(false) + , m_bChangedByMouse(false) + , m_nTextCol(-1) + , m_nTextView(-1) + , m_nImageCol(-1) + , m_nExpanderToggleCol(-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) +#if !GTK_CHECK_VERSION(4, 0, 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_nCrossingSignalid(g_signal_connect(pTreeView, "enter-notify-event", G_CALLBACK(signalCrossing), this)) +#endif + , m_nQueryTooltipSignalId(0) + , m_pVAdjustment(gtk_scrollable_get_vadjustment(GTK_SCROLLABLE(pTreeView))) + , m_pChangeEvent(nullptr) + { + if (GTK_IS_TREE_STORE(m_pTreeModel)) + { + m_Setter = tree_store_set; + m_InsertWithValues = tree_store_insert_with_values; + m_Insert = tree_store_insert; + m_Prepend = tree_store_prepend; + m_Remove = tree_store_remove; + m_Swap = tree_store_swap; + m_SetValue = tree_store_set_value; + m_Clear = tree_store_clear; + } + else + { + /* + tdf#136559 see: https://gitlab.gnome.org/GNOME/gtk/-/issues/2693 + If we only need a list and not a tree we can get a performance boost from using a ListStore + */ + assert(!gtk_tree_view_get_show_expanders(m_pTreeView) && "a liststore can only be used if no tree structure is needed"); + m_Setter = list_store_set; + m_InsertWithValues = list_store_insert_with_values; + m_Insert = list_store_insert; + m_Prepend = list_store_prepend; + m_Remove = list_store_remove; + m_Swap = list_store_swap; + m_SetValue = list_store_set_value; + m_Clear = list_store_clear; + } + + /* The outside concept of a column maps to a gtk CellRenderer, rather than + a TreeViewColumn. If the first TreeViewColumn has a leading Toggle Renderer + and/or a leading Image Renderer, those are considered special expander + columns and precede index 0 and can be accessed via outside index -1 + */ + m_pColumns = gtk_tree_view_get_columns(m_pTreeView); + int nIndex(0); + int nViewColumn(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); + if (GTK_IS_CELL_RENDERER_TEXT(pCellRenderer)) + { + if (m_nTextCol == -1) + { + m_nTextCol = nIndex; + m_nTextView = nViewColumn; + } + 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)) + { + const bool bExpander = nIndex == 0 || (nIndex == 1 && m_nExpanderImageCol == 0); + if (bExpander) + m_nExpanderToggleCol = nIndex; + 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; + } + g_object_set_data(G_OBJECT(pCellRenderer), "g-lo-CellIndex", reinterpret_cast<gpointer>(nIndex)); + ++nIndex; + } + g_list_free(pRenderers); + ++nViewColumn; + } + + 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(); + + m_nRowDeletedSignalId = g_signal_connect(m_pTreeModel, "row-deleted", G_CALLBACK(signalRowDeleted), this); + m_nRowInsertedSignalId = g_signal_connect(m_pTreeModel, "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"); + + GtkCellRenderer* pExpander = nullptr; + GtkCellRenderer* pToggle = nullptr; + + // migrate existing editable setting to the new renderer + gboolean is_editable(false); + void* pEditCellData(nullptr); + 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"); + auto nCellIndex = reinterpret_cast<sal_IntPtr>(pData); + + if (GTK_IS_CELL_RENDERER_TEXT(pCellRenderer)) + { + g_object_get(pCellRenderer, "editable", &is_editable, nullptr); + pEditCellData = pData; + break; + } + else if (GTK_IS_CELL_RENDERER_TOGGLE(pCellRenderer)) + { + if (nCellIndex == m_nExpanderToggleCol) + { + pToggle = pCellRenderer; + g_object_ref(pToggle); + } + } + else if (GTK_IS_CELL_RENDERER_PIXBUF(pCellRenderer)) + { + if (nCellIndex == m_nExpanderImageCol) + { + pExpander = pCellRenderer; + g_object_ref(pExpander); + } + } + + } + g_list_free(pRenderers); + + GtkCellRenderer* pRenderer; + + gtk_cell_layout_clear(GTK_CELL_LAYOUT(pColumn)); + if (pExpander) + { + gtk_tree_view_column_pack_start(pColumn, pExpander, false); + gtk_tree_view_column_add_attribute(pColumn, pExpander, "pixbuf", m_nExpanderImageCol); + g_object_unref(pExpander); + } + if (pToggle) + { + gtk_tree_view_column_pack_start(pColumn, pToggle, false); + gtk_tree_view_column_add_attribute(pColumn, pToggle, "active", m_nExpanderToggleCol); + gtk_tree_view_column_add_attribute(pColumn, pToggle, "active", m_nExpanderToggleCol); + gtk_tree_view_column_add_attribute(pColumn, pToggle, "visible", m_aToggleTriStateMap[m_nExpanderToggleCol]); + g_object_unref(pToggle); + } + + if (bEnable) + { + pRenderer = custom_cell_renderer_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 + { + 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); + } + + if (is_editable) + { + g_object_set(pRenderer, "editable", true, "editable-set", true, nullptr); + g_object_set_data(G_OBJECT(pRenderer), "g-lo-CellIndex", pEditCellData); + g_signal_connect(pRenderer, "editing-started", G_CALLBACK(signalCellEditingStarted), this); + g_signal_connect(pRenderer, "editing-canceled", G_CALLBACK(signalCellEditingCanceled), this); + g_signal_connect(pRenderer, "edited", G_CALLBACK(signalCellEdited), this); + } + } + + virtual void queue_draw() override + { + gtk_widget_queue_draw(GTK_WIDGET(m_pTreeView)); + } + + virtual void insert(const weld::TreeIter* pParent, int pos, const OUString* pText, const OUString* pId, const OUString* pIconName, + VirtualDevice* pImageSurface, + 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); + if (bChildrenOnDemand) + { + GtkTreeIter subiter; + OUString sDummy("<dummy>"); + insert_row(subiter, &iter, -1, nullptr, &sDummy, nullptr, nullptr); + } + if (pRet) + { + GtkInstanceTreeIter* pGtkRetIter = static_cast<GtkInstanceTreeIter*>(pRet); + pGtkRetIter->iter = iter; + } + enable_notify_events(); + } + + virtual void insert_separator(int pos, const OUString& rId) override + { + 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(iter, nullptr, pos, &rId, nullptr, nullptr, nullptr); + GtkTreePath* pPath = gtk_tree_model_get_path(m_pTreeModel, &iter); + m_aSeparatorRows.emplace_back(gtk_tree_row_reference_new(m_pTreeModel, pPath)); + gtk_tree_path_free(pPath); + enable_notify_events(); + } + + virtual void set_font_color(int pos, const Color& rColor) override + { + GtkTreeIter iter; + gtk_tree_model_iter_nth_child(m_pTreeModel, &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(m_pTreeModel, &iter, nullptr, pos); + m_Remove(m_pTreeModel, &iter); + enable_notify_events(); + } + + virtual int find_text(const OUString& rText) const override + { + Search aSearch(rText, m_nTextCol); + gtk_tree_model_foreach(m_pTreeModel, foreach_find, &aSearch); + return aSearch.index; + } + + virtual int find_id(const OUString& rId) const override + { + Search aSearch(rId, m_nIdCol); + gtk_tree_model_foreach(m_pTreeModel, foreach_find, &aSearch); + return aSearch.index; + } + + virtual void bulk_insert_for_each(int nSourceCount, const std::function<void(weld::TreeIter&, int nSourceIndex)>& func, + const weld::TreeIter* pParent, + const std::vector<int>* pFixedWidths) override + { + GtkInstanceTreeIter* pGtkIter = const_cast<GtkInstanceTreeIter*>(static_cast<const GtkInstanceTreeIter*>(pParent)); + + freeze(); + if (!pGtkIter) + clear(); + else + { + GtkTreeIter restore(pGtkIter->iter); + + if (iter_children(*pGtkIter)) + while (m_Remove(m_pTreeModel, &pGtkIter->iter)); + + pGtkIter->iter = restore; + } + GtkInstanceTreeIter aGtkIter(nullptr); + + if (pFixedWidths) + set_column_fixed_widths(*pFixedWidths); + + while (nSourceCount) + { + // tdf#125241 inserting backwards is massively faster + m_Prepend(m_pTreeModel, &aGtkIter.iter, pGtkIter ? &pGtkIter->iter : nullptr); + func(aGtkIter, --nSourceCount); + } + + thaw(); + } + + virtual void swap(int pos1, int pos2) override + { + disable_notify_events(); + + GtkTreeIter iter1; + gtk_tree_model_iter_nth_child(m_pTreeModel, &iter1, nullptr, pos1); + + GtkTreeIter iter2; + gtk_tree_model_iter_nth_child(m_pTreeModel, &iter2, nullptr, pos2); + + m_Swap(m_pTreeModel, &iter1, &iter2); + + enable_notify_events(); + } + + virtual void clear() override + { + disable_notify_events(); + gtk_tree_view_set_row_separator_func(m_pTreeView, nullptr, nullptr, nullptr); + m_aSeparatorRows.clear(); + m_Clear(m_pTreeModel); + 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_pTreeModel); + 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_pTreeModel); + 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_pTreeModel); + 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 + { + int nSortColumn; + GtkSortType eSortType; + + GtkTreeSortable* pSortable = GTK_TREE_SORTABLE(m_pTreeModel); + gtk_tree_sortable_get_sort_column_id(pSortable, &nSortColumn, &eSortType); + return nSortColumn != GTK_TREE_SORTABLE_UNSORTED_SORT_COLUMN_ID && eSortType == GTK_SORT_ASCENDING; + } + + virtual void set_sort_indicator(TriState eState, int col) override + { + assert(col >= 0 && "cannot sort on expander column"); + + 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 + { + assert(col >= 0 && "cannot sort on expander column"); + + 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_pTreeModel); + gint sort_column_id(0); + if (!gtk_tree_sortable_get_sort_column_id(pSortable, &sort_column_id, nullptr)) + return -1; + return to_external_model(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_pTreeModel); + gtk_tree_sortable_get_sort_column_id(pSortable, nullptr, &eSortType); + int nSortCol = to_internal_model(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_pTreeModel); + gtk_tree_sortable_sort_column_changed(pSortable); + } + + virtual int n_children() const override + { + return gtk_tree_model_iter_n_children(m_pTreeModel, 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(m_pTreeModel, 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; + if (pos != -1) + { + path = gtk_tree_path_new_from_indices(pos, -1); + gtk_tree_view_scroll_to_cell(m_pTreeView, path, nullptr, false, 0, 0); + } + else + path = gtk_tree_path_new_from_indices(G_MAXINT, -1); + 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, true, 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(m_pTreeModel, &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 + { + g_object_freeze_notify(G_OBJECT(m_pTreeModel)); + + GtkInstanceTreeIter aGtkIter(nullptr); + if (get_iter_first(aGtkIter)) + { + do + { + if (func(aGtkIter)) + break; + } while (iter_next(aGtkIter)); + } + + g_object_thaw_notify(G_OBJECT(m_pTreeModel)); + } + + virtual void selected_foreach(const std::function<bool(weld::TreeIter&)>& func) override + { + g_object_freeze_notify(G_OBJECT(m_pTreeModel)); + + 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)); + + g_object_thaw_notify(G_OBJECT(m_pTreeModel)); + } + + virtual void visible_foreach(const std::function<bool(weld::TreeIter&)>& func) override + { + g_object_freeze_notify(G_OBJECT(m_pTreeModel)); + + GtkTreePath* start_path; + GtkTreePath* end_path; + + if (!gtk_tree_view_get_visible_range(m_pTreeView, &start_path, &end_path)) + return; + + GtkInstanceTreeIter aGtkIter(nullptr); + gtk_tree_model_get_iter(m_pTreeModel, &aGtkIter.iter, start_path); + + do + { + if (func(aGtkIter)) + break; + GtkTreePath* path = gtk_tree_model_get_path(m_pTreeModel, &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); + + g_object_thaw_notify(G_OBJECT(m_pTreeModel)); + } + + 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) + col = m_nTextCol; + else + col = to_internal_model(col); + return get(pos, col); + } + + virtual void set_text(int pos, const OUString& rText, int col) override + { + if (col == -1) + col = m_nTextCol; + else + col = to_internal_model(col); + set(pos, col, rText); + } + + virtual TriState get_toggle(int pos, int col) const override + { + if (col == -1) + col = m_nExpanderToggleCol; + else + col = to_internal_model(col); + + const auto iter = m_aToggleTriStateMap.find(col); + assert(iter != m_aToggleTriStateMap.end()); + if (get_bool(pos, iter->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 + { + if (col == -1) + col = m_nExpanderToggleCol; + else + col = to_internal_model(col); + + const GtkInstanceTreeIter& rGtkIter = static_cast<const GtkInstanceTreeIter&>(rIter); + const auto iter = m_aToggleTriStateMap.find(col); + assert(iter != m_aToggleTriStateMap.end()); + if (get_bool(rGtkIter.iter, iter->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); + set_toggle(rGtkIter.iter, eState, col); + } + + virtual void set_toggle(int pos, TriState eState, int col) override + { + GtkTreeIter iter; + if (gtk_tree_model_iter_nth_child(m_pTreeModel, &iter, nullptr, pos)) + set_toggle(iter, eState, col); + } + + virtual void enable_toggle_buttons(weld::ColumnToggleType eType) 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); + if (!GTK_IS_CELL_RENDERER_TOGGLE(pCellRenderer)) + continue; + GtkCellRendererToggle* pToggle = GTK_CELL_RENDERER_TOGGLE(pCellRenderer); + gtk_cell_renderer_toggle_set_radio(pToggle, eType == weld::ColumnToggleType::Radio); + } + g_list_free(pRenderers); + } + } + + virtual void set_clicks_to_toggle(int /*nToggleBehavior*/) override + { + } + + 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); + auto weight = bOn ? PANGO_WEIGHT_BOLD : PANGO_WEIGHT_NORMAL; + if (col == -1) + { + for (const auto& elem : m_aWeightMap) + set(rGtkIter.iter, elem.second, weight); + return; + } + col = to_internal_model(col); + set(rGtkIter.iter, m_aWeightMap[col], weight); + } + + virtual void set_text_emphasis(int pos, bool bOn, int col) override + { + auto weight = bOn ? PANGO_WEIGHT_BOLD : PANGO_WEIGHT_NORMAL; + if (col == -1) + { + for (const auto& elem : m_aWeightMap) + set(pos, elem.second, weight); + return; + } + col = to_internal_model(col); + set(pos, m_aWeightMap[col], weight); + } + + virtual bool get_text_emphasis(const weld::TreeIter& rIter, int col) const override + { + const GtkInstanceTreeIter& rGtkIter = static_cast<const GtkInstanceTreeIter&>(rIter); + col = to_internal_model(col); + const auto iter = m_aWeightMap.find(col); + assert(iter != m_aWeightMap.end()); + return get_int(rGtkIter.iter, iter->second) == PANGO_WEIGHT_BOLD; + } + + virtual bool get_text_emphasis(int pos, int col) const override + { + col = to_internal_model(col); + const auto iter = m_aWeightMap.find(col); + assert(iter != m_aWeightMap.end()); + return get_int(pos, iter->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 = to_internal_model(col); + set(rGtkIter.iter, m_aAlignMap[col], fAlign); + } + + virtual void set_text_align(int pos, double fAlign, int col) override + { + col = to_internal_model(col); + set(pos, m_aAlignMap[col], fAlign); + } + + using GtkInstanceWidget::set_sensitive; + using GtkInstanceWidget::get_sensitive; + + virtual void set_sensitive(int pos, bool bSensitive, int col) override + { + if (col == -1) + { + for (const auto& elem : m_aSensitiveMap) + set(pos, elem.second, bSensitive); + } + else + { + col = to_internal_model(col); + set(pos, m_aSensitiveMap[col], bSensitive); + } + } + + virtual bool get_sensitive(int pos, int col) const override + { + col = to_internal_model(col); + const auto iter = m_aSensitiveMap.find(col); + assert(iter != m_aSensitiveMap.end()); + return get_bool(pos, iter->second); + } + + virtual void set_sensitive(const weld::TreeIter& rIter, bool bSensitive, int col) override + { + const GtkInstanceTreeIter& rGtkIter = static_cast<const GtkInstanceTreeIter&>(rIter); + if (col == -1) + { + for (const auto& elem : m_aSensitiveMap) + set(rGtkIter.iter, elem.second, bSensitive); + } + else + { + col = to_internal_model(col); + set(rGtkIter.iter, m_aSensitiveMap[col], bSensitive); + } + } + + virtual bool get_sensitive(const weld::TreeIter& rIter, int col) const override + { + const GtkInstanceTreeIter& rGtkIter = static_cast<const GtkInstanceTreeIter&>(rIter); + col = to_internal_model(col); + const auto iter = m_aSensitiveMap.find(col); + assert(iter != m_aSensitiveMap.end()); + return get_bool(rGtkIter.iter, iter->second); + } + + void set_image(const GtkTreeIter& iter, int col, GdkPixbuf* pixbuf) + { + if (col == -1) + col = m_nExpanderImageCol; + else + col = to_internal_model(col); + m_Setter(m_pTreeModel, const_cast<GtkTreeIter*>(&iter), col, pixbuf, -1); + if (pixbuf) + g_object_unref(pixbuf); + } + + void set_image(int pos, GdkPixbuf* pixbuf, int col) + { + GtkTreeIter iter; + if (gtk_tree_model_iter_nth_child(m_pTreeModel, &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); + + GtkTreePath* path = gtk_tree_model_get_path(m_pTreeModel, 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); + + GtkTreePath* pathA = gtk_tree_model_get_path(m_pTreeModel, const_cast<GtkTreeIter*>(&rGtkIterA.iter)); + GtkTreePath* pathB = gtk_tree_model_get_path(m_pTreeModel, 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) + { + int nCols = gtk_tree_model_get_n_columns(m_pTreeModel); + GValue value; + + GtkTreeIter toiter; + m_Insert(m_pTreeModel, &toiter, pGtkParentIter, nIndexInNewParent); + + for (int i = 0; i < nCols; ++i) + { + memset(&value, 0, sizeof(GValue)); + gtk_tree_model_get_value(m_pTreeModel, &rFromIter, i, &value); + m_SetValue(m_pTreeModel, &toiter, i, &value); + g_value_unset(&value); + } + + GtkTreeIter tmpfromiter; + if (gtk_tree_model_iter_children(m_pTreeModel, &tmpfromiter, &rFromIter)) + { + int j = 0; + do + { + move_subtree(tmpfromiter, &toiter, j++); + } while (gtk_tree_model_iter_next(m_pTreeModel, &tmpfromiter)); + } + + m_Remove(m_pTreeModel, &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) + { + gtk_tree_model_get_iter(m_pTreeModel, &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); + GtkTreeIter Iter; + if (gtk_tree_model_iter_parent(m_pTreeModel, &Iter, const_cast<GtkTreeIter*>(&rGtkIter.iter))) + { + GtkTreePath* path = gtk_tree_model_get_path(m_pTreeModel, &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(m_pTreeModel, 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); + return gtk_tree_model_get_iter_first(m_pTreeModel, &rGtkIter.iter); + } + + virtual bool iter_next_sibling(weld::TreeIter& rIter) const override + { + GtkInstanceTreeIter& rGtkIter = static_cast<GtkInstanceTreeIter&>(rIter); + return gtk_tree_model_iter_next(m_pTreeModel, &rGtkIter.iter); + } + + virtual bool iter_previous_sibling(weld::TreeIter& rIter) const override + { + GtkInstanceTreeIter& rGtkIter = static_cast<GtkInstanceTreeIter&>(rIter); + return gtk_tree_model_iter_previous(m_pTreeModel, &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); + GtkTreeIter iter = rGtkIter.iter; + GtkTreeIter tmp = iter; + if (gtk_tree_model_iter_previous(m_pTreeModel, &tmp)) + { + // Move down level(s) until we find the level where the last node exists. + int nChildren = gtk_tree_model_iter_n_children(m_pTreeModel, &tmp); + if (!nChildren) + rGtkIter.iter = tmp; + else + last_child(m_pTreeModel, &rGtkIter.iter, &tmp, nChildren); + ret = true; + } + else + { + // Move up level + if (gtk_tree_model_iter_parent(m_pTreeModel, &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_children(weld::TreeIter& rIter) const override + { + GtkInstanceTreeIter& rGtkIter = static_cast<GtkInstanceTreeIter&>(rIter); + GtkTreeIter tmp; + bool ret = gtk_tree_model_iter_children(m_pTreeModel, &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); + GtkTreeIter tmp; + bool ret = gtk_tree_model_iter_parent(m_pTreeModel, &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); + m_Remove(m_pTreeModel, 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) + m_Remove(m_pTreeModel, &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); + GtkTreePath* path = gtk_tree_model_get_path(m_pTreeModel, const_cast<GtkTreeIter*>(&rGtkIter.iter)); + gtk_tree_view_expand_to_path(m_pTreeView, path); + gtk_tree_view_scroll_to_cell(m_pTreeView, path, nullptr, true, 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); + GtkTreePath* path = gtk_tree_model_get_path(m_pTreeModel, 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 + { + GtkInstanceTreeIter aTempCopy(static_cast<const GtkInstanceTreeIter*>(&rIter)); + return iter_children(aTempCopy); + } + + virtual bool get_row_expanded(const weld::TreeIter& rIter) const override + { + const GtkInstanceTreeIter& rGtkIter = static_cast<const GtkInstanceTreeIter&>(rIter); + GtkTreePath* path = gtk_tree_model_get_path(m_pTreeModel, 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); + } + 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); + GtkTreePath* path = gtk_tree_model_get_path(m_pTreeModel, 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); + GtkTreePath* path = gtk_tree_model_get_path(m_pTreeModel, 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 = to_internal_model(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 = to_internal_model(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(); + bool bIsFirstFreeze = IsFirstFreeze(); + GtkInstanceWidget::freeze(); + if (bIsFirstFreeze) + { + g_object_ref(m_pTreeModel); + gtk_tree_view_set_model(m_pTreeView, nullptr); + g_object_freeze_notify(G_OBJECT(m_pTreeModel)); + if (m_xSorter) + { + int nSortColumn; + GtkSortType eSortType; + GtkTreeSortable* pSortable = GTK_TREE_SORTABLE(m_pTreeModel); + 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 (IsLastThaw()) + { + if (m_xSorter) + { + GtkTreeSortable* pSortable = GTK_TREE_SORTABLE(m_pTreeModel); + gtk_tree_sortable_set_sort_column_id(pSortable, m_aSavedSortColumns.back(), m_aSavedSortTypes.back()); + m_aSavedSortTypes.pop_back(); + m_aSavedSortColumns.pop_back(); + } + g_object_thaw_notify(G_OBJECT(m_pTreeModel)); + gtk_tree_view_set_model(m_pTreeView, GTK_TREE_MODEL(m_pTreeModel)); + g_object_unref(m_pTreeModel); + } + GtkInstanceWidget::thaw(); + 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; +#if !GTK_CHECK_VERSION(4, 0, 0) + // sometimes gtk gives a bad outcome for gtk_widget_get_preferred_size if GtkTreeView's + // do_validate_rows hasn't been run before querying the preferred size, if we call + // gtk_widget_get_preferred_width first, we can guarantee do_validate_rows gets called + gtk_widget_get_preferred_width(m_pWidget, nullptr, &size.width); +#endif + 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); + } + +#if !GTK_CHECK_VERSION(4, 0, 0) + virtual void drag_source_set(const std::vector<GtkTargetEntry>& rGtkTargets, GdkDragAction eDragAction) override + { + if (rGtkTargets.empty() && !eDragAction) + gtk_tree_view_unset_rows_drag_source(m_pTreeView); + else + gtk_tree_view_enable_model_drag_source(m_pTreeView, GDK_BUTTON1_MASK, rGtkTargets.data(), rGtkTargets.size(), eDragAction); + } +#endif + + 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 nStartRow, bool bCaseSensitive) + { + return ::starts_with(m_pTreeModel, rStr, m_nTextCol, 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); + + g_signal_handler_block(m_pTreeModel, m_nRowDeletedSignalId); + g_signal_handler_block(m_pTreeModel, m_nRowInsertedSignalId); + + GtkInstanceWidget::disable_notify_events(); + } + + virtual void enable_notify_events() override + { + GtkInstanceWidget::enable_notify_events(); + + g_signal_handler_unblock(m_pTreeModel, m_nRowDeletedSignalId); + g_signal_handler_unblock(m_pTreeModel, 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 bDnDMode, bool bAutoScroll) override + { + if (rPos.X() < 0 || rPos.Y() < 0) + { + // short-circuit to avoid "gtk_tree_view_get_dest_row_at_pos: assertion 'drag_x >= 0'" g_assert + return false; + } + + 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) + { +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_unset_state_flags(GTK_WIDGET(m_pTreeView), GTK_STATE_FLAG_DROP_ACTIVE); +#else + gtk_drag_unhighlight(GTK_WIDGET(m_pTreeView)); +#endif + } + + 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 + GtkTreePath *lastpath = get_path_of_last_entry(m_pTreeModel); + + 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 (bDnDMode && 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(m_pTreeModel, &rGtkIter.iter, path); + } + + if (m_bInDrag && bDnDMode) + { + // 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); + + if (bAutoScroll) + { + // 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); + GtkTreePath* pPath = gtk_tree_model_get_path(m_pTreeModel, 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 + { + const GtkInstanceTreeIter& rGtkIter = static_cast<const GtkInstanceTreeIter&>(rIter); + GtkTreePath* path = gtk_tree_model_get_path(m_pTreeModel, const_cast<GtkTreeIter*>(&rGtkIter.iter)); + + GtkTreeViewColumn* pColumn = nullptr; + + for (GList* pEntry = g_list_first(m_pColumns); pEntry; pEntry = g_list_next(pEntry)) + { + GtkTreeViewColumn* pTestColumn = GTK_TREE_VIEW_COLUMN(pEntry->data); + + // see if this column is editable + gboolean is_editable(false); + GList *pRenderers = gtk_cell_layout_get_cells(GTK_CELL_LAYOUT(pTestColumn)); + 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)) + { + g_object_get(pCellRenderer, "editable", &is_editable, nullptr); + if (is_editable) + { + pColumn = pTestColumn; + break; + } + } + } + g_list_free(pRenderers); + + if (is_editable) + break; + } + + // if nothing explicit editable, 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 + if (!pColumn) + { + pColumn = GTK_TREE_VIEW_COLUMN(g_list_nth_data(m_pColumns, m_nTextView)); + assert(pColumn && "wrong column"); + + 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)) + { + 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; + } + +#if GTK_CHECK_VERSION(4, 0, 0) + virtual void drag_set_icon(GtkDragSource*) override + { + } +#else + virtual void drag_set_icon(GdkDragContext* context) override + { + GtkTreeSelection *selection = gtk_tree_view_get_selection(m_pTreeView); + if (gtk_tree_selection_get_mode(selection) == GTK_SELECTION_MULTIPLE) + { + int nWidth = 0; + int nHeight = 0; + + GList* pList = gtk_tree_selection_get_selected_rows(gtk_tree_view_get_selection(m_pTreeView), nullptr); + std::vector<cairo_surface_t*> surfaces; + std::vector<int> heights; + for (GList* pItem = g_list_first(pList); pItem; pItem = g_list_next(pItem)) + { + GtkTreePath* pPath = static_cast<GtkTreePath*>(pItem->data); + + surfaces.push_back(gtk_tree_view_create_row_drag_icon(m_pTreeView, pPath)); + + double x1, x2, y1, y2; + cairo_t* cr = cairo_create(surfaces.back()); + cairo_clip_extents(cr, &x1, &y1, &x2, &y2); + cairo_destroy(cr); + + heights.push_back(y2 - y1); + + nWidth = std::max(nWidth, static_cast<int>(x2 - x1)); + nHeight += heights.back(); + } + g_list_free_full(pList, reinterpret_cast<GDestroyNotify>(gtk_tree_path_free)); + + // if it's just one, then don't do anything and leave the default dnd icon as-is + if (surfaces.size() > 1) + { + cairo_surface_t* target = cairo_surface_create_similar(surfaces[0], + cairo_surface_get_content(surfaces[0]), + nWidth, + nHeight); + + cairo_t* cr = cairo_create(target); + + double y_pos = 0; + for (size_t i = 0; i < surfaces.size(); ++i) + { + cairo_set_source_surface(cr, surfaces[i], 2, y_pos + 2); + cairo_rectangle(cr, 0, y_pos, nWidth, heights[i]); + cairo_fill(cr); + y_pos += heights[i]; + } + + cairo_destroy(cr); + + double fXScale, fYScale; + dl_cairo_surface_get_device_scale(target, &fXScale, &fYScale); + cairo_surface_set_device_offset(target, + - m_nPressStartX * fXScale, + 0); + + gtk_drag_set_icon_surface(context, target); + cairo_surface_destroy(target); + } + + for (auto surface : surfaces) + cairo_surface_destroy(surface); + } + } +#endif + + 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)) + { +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_unset_state_flags(pWidget, GTK_STATE_FLAG_DROP_ACTIVE); + gtk_widget_set_state_flags(pParent, GTK_STATE_FLAG_DROP_ACTIVE, false); +#else + gtk_drag_unhighlight(pWidget); + gtk_drag_highlight(pParent); +#endif + 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); +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_unset_state_flags(pParent, GTK_STATE_FLAG_DROP_ACTIVE); +#else + gtk_drag_unhighlight(pParent); +#endif + 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 void set_show_expanders(bool bShow) override + { + gtk_tree_view_set_show_expanders(m_pTreeView, bShow); + } + + virtual bool changed_by_hover() const override + { + return m_bChangedByMouse; + } + + virtual ~GtkInstanceTreeView() override + { + if (m_pChangeEvent) + Application::RemoveUserEvent(m_pChangeEvent); + if (m_nQueryTooltipSignalId) + g_signal_handler_disconnect(m_pTreeView, m_nQueryTooltipSignalId); +#if !GTK_CHECK_VERSION(4, 0, 0) + g_signal_handler_disconnect(m_pTreeView, m_nCrossingSignalid); + g_signal_handler_disconnect(m_pTreeView, m_nKeyPressSignalId); + g_signal_handler_disconnect(m_pTreeView, m_nPopupMenuSignalId); +#endif + g_signal_handler_disconnect(m_pTreeModel, m_nRowDeletedSignalId); + g_signal_handler_disconnect(m_pTreeModel, 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); + + if (g_DragSource == this) + g_DragSource = nullptr; + + 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(pCellRenderer)) + continue; + g_object_set_property(G_OBJECT(pCellRenderer), "instance", &value); + } + g_list_free(pRenderers); + } + g_list_free(m_pColumns); + } +}; + +} + +IMPL_LINK_NOARG(GtkInstanceTreeView, async_signal_changed, void*, void) +{ + m_pChangeEvent = nullptr; + signal_changed(); + m_bChangedByMouse = false; +} + +IMPL_LINK_NOARG(GtkInstanceTreeView, async_stop_cell_editing, void*, void) +{ + end_editing(); +} + +namespace { + +class GtkInstanceIconView : public GtkInstanceWidget, 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; +#if !GTK_CHECK_VERSION(4, 0, 0) + gulong m_nPopupMenu; +#endif + gulong m_nQueryTooltipSignalId = 0; + ImplSVEvent* m_pSelectionChangeEvent; + + DECL_LINK(async_signal_selection_changed, void*, void); + + bool signal_command(const CommandEvent& rCEvt) + { + return m_aCommandHdl.Call(rCEvt); + } + + virtual bool signal_popup_menu(const CommandEvent& rCEvt) override + { + return signal_command(rCEvt); + } + + 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(); + } + + static gboolean signalQueryTooltip(GtkWidget* /*pGtkWidget*/, gint x, gint y, + gboolean keyboard_tip, GtkTooltip* tooltip, + gpointer widget) + { + GtkInstanceIconView* pThis = static_cast<GtkInstanceIconView*>(widget); + GtkTreeIter iter; + GtkIconView* pIconView = pThis->m_pIconView; + GtkTreeModel* pModel = gtk_icon_view_get_model(pIconView); + GtkTreePath* pPath = nullptr; +#if GTK_CHECK_VERSION(4, 0, 0) + if (!gtk_icon_view_get_tooltip_context(pIconView, x, y, keyboard_tip, &pModel, &pPath, &iter)) + return false; +#else + if (!gtk_icon_view_get_tooltip_context(pIconView, &x, &y, keyboard_tip, &pModel, &pPath, &iter)) + return false; +#endif + OUString aTooltip = pThis->signal_query_tooltip(GtkInstanceTreeIter(iter)); + if (!aTooltip.isEmpty()) + { + gtk_tooltip_set_text(tooltip, OUStringToOString(aTooltip, RTL_TEXTENCODING_UTF8).getStr()); + gtk_icon_view_set_tooltip_item(pIconView, tooltip, pPath); + } + gtk_tree_path_free(pPath); + return !aTooltip.isEmpty(); + } + + /* Set the item's tooltip text as its accessible description as well. */ + void set_item_accessible_description_from_tooltip(GtkTreeIter& iter) + { +#if GTK_CHECK_VERSION(4, 0, 0) + (void)iter; +#else + AtkObject* pAtkObject = gtk_widget_get_accessible(GTK_WIDGET(m_pIconView)); + assert(pAtkObject); + GtkTreePath* pPath = gtk_tree_model_get_path(GTK_TREE_MODEL(m_pTreeStore), &iter); + assert(gtk_tree_path_get_depth(pPath) == 1); + int* indices = gtk_tree_path_get_indices(pPath); + const int nIndex = indices[0]; + assert(nIndex < atk_object_get_n_accessible_children(pAtkObject) + && "item index too high for ItemView's accessible child count"); + + const OUString sTooltipText = signal_query_tooltip(GtkInstanceTreeIter(iter)); + AtkObject* pChild = atk_object_ref_accessible_child(pAtkObject, nIndex); + atk_object_set_description(pChild, + OUStringToOString(sTooltipText, RTL_TEXTENCODING_UTF8).getStr()); + g_object_unref(pChild); +#endif + } + + void insert_item(GtkTreeIter& iter, int pos, const OUString* pId, const OUString* pText, const OUString* pIconName) + { + // m_nTextCol may be -1, so pass it last, to not terminate the sequence before the Id value + gtk_tree_store_insert_with_values(m_pTreeStore, &iter, nullptr, pos, + m_nIdCol, !pId ? nullptr : OUStringToOString(*pId, RTL_TEXTENCODING_UTF8).getStr(), + m_nTextCol, !pText ? nullptr : OUStringToOString(*pText, 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); + } + + set_item_accessible_description_from_tooltip(iter); + } + + void insert_item(GtkTreeIter& iter, int pos, const OUString* pId, const OUString* pText, const VirtualDevice* pIcon) + { + // m_nTextCol may be -1, so pass it last, to not terminate the sequence before the Id value + gtk_tree_store_insert_with_values(m_pTreeStore, &iter, nullptr, pos, + m_nIdCol, !pId ? nullptr : OUStringToOString(*pId, RTL_TEXTENCODING_UTF8).getStr(), + m_nTextCol, !pText ? nullptr : OUStringToOString(*pText, RTL_TEXTENCODING_UTF8).getStr(), + -1); + if (pIcon) + { + GdkPixbuf* pixbuf = getPixbuf(*pIcon); + gtk_tree_store_set(m_pTreeStore, &iter, m_nImageCol, pixbuf, -1); + if (pixbuf) + g_object_unref(pixbuf); + } + + set_item_accessible_description_from_tooltip(iter); + } + + 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) + : GtkInstanceWidget(GTK_WIDGET(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)) // May be -1 + , 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)) +#if !GTK_CHECK_VERSION(4, 0, 0) + , m_nPopupMenu(g_signal_connect(pIconView, "popup-menu", G_CALLBACK(signalPopupMenu), this)) +#endif + , m_pSelectionChangeEvent(nullptr) + { + m_nIdCol = std::max(m_nTextCol, m_nImageCol) + 1; + } + + virtual int get_item_width() const override + { + return gtk_icon_view_get_item_width(m_pIconView); + } + + virtual void set_item_width(int width) override + { + gtk_icon_view_set_item_width(m_pIconView, width); + } + + 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 void insert(int pos, const OUString* pText, const OUString* pId, const VirtualDevice* pIcon, weld::TreeIter* pRet) override + { + disable_notify_events(); + GtkTreeIter iter; + insert_item(iter, pos, pId, pText, pIcon); + if (pRet) + { + GtkInstanceTreeIter* pGtkRetIter = static_cast<GtkInstanceTreeIter*>(pRet); + pGtkRetIter->iter = iter; + } + enable_notify_events(); + } + + virtual void insert_separator(int /* pos */, const OUString* /* pId */) override + { + // TODO: can't just copy from GtkInstanceTreeView, since there's + // no IconView analog for gtk_tree_view_get_row_separator_func + } + + virtual void connect_query_tooltip(const Link<const weld::TreeIter&, OUString>& rLink) override + { + weld::IconView::connect_query_tooltip(rLink); + m_nQueryTooltipSignalId = g_signal_connect(m_pIconView, "query-tooltip", G_CALLBACK(signalQueryTooltip), this); + gtk_widget_set_has_tooltip(GTK_WIDGET(m_pIconView), true); + } + + virtual void connect_get_property_tree_elem(const Link<const weld::json_prop_query&, bool>& /*rLink*/) override + { + //not implemented for the gtk variant + } + + 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(); + bool bIsFirstFreeze = IsFirstFreeze(); + GtkInstanceWidget::freeze(); + if (bIsFirstFreeze) + g_object_freeze_notify(G_OBJECT(m_pTreeStore)); + enable_notify_events(); + } + + virtual void thaw() override + { + disable_notify_events(); + if (IsLastThaw()) + g_object_thaw_notify(G_OBJECT(m_pTreeStore)); + GtkInstanceWidget::thaw(); + 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 OUString get_text(const weld::TreeIter& rIter) const override + { + const GtkInstanceTreeIter& rGtkIter = static_cast<const GtkInstanceTreeIter&>(rIter); + return get(rGtkIter.iter, m_nTextCol); + } + + virtual void disable_notify_events() override + { + g_signal_handler_block(m_pIconView, m_nSelectionChangedSignalId); + g_signal_handler_block(m_pIconView, m_nItemActivatedSignalId); + + GtkInstanceWidget::disable_notify_events(); + } + + virtual void enable_notify_events() override + { + GtkInstanceWidget::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); + + if (m_nQueryTooltipSignalId) + g_signal_handler_disconnect(m_pIconView, m_nQueryTooltipSignalId); + + g_signal_handler_disconnect(m_pIconView, m_nItemActivatedSignalId); + g_signal_handler_disconnect(m_pIconView, m_nSelectionChangedSignalId); +#if !GTK_CHECK_VERSION(4, 0, 0) + g_signal_handler_disconnect(m_pIconView, m_nPopupMenu); +#endif + } +}; + +} + +IMPL_LINK_NOARG(GtkInstanceIconView, async_signal_selection_changed, void*, void) +{ + m_pSelectionChangeEvent = nullptr; + signal_selection_changed(); +} + +namespace { + +void signalDestroyFlag(GtkWidget*, gpointer destroyed) +{ + bool* pDestroyed = static_cast<bool*>(destroyed); + *pDestroyed = true; +} + +class GtkInstanceSpinButton : public GtkInstanceEditable, 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 + { + bool bActivateDestroy(false); + gulong nDestroySignalId = g_signal_connect(m_pButton, "destroy", G_CALLBACK(signalDestroyFlag), &bActivateDestroy); + gtk_spin_button_update(m_pButton); + if (bActivateDestroy) + return; + g_signal_handler_disconnect(m_pButton, nDestroySignalId); + GtkInstanceEditable::signal_activate(); + } + + double toGtk(sal_Int64 nValue) const + { + return static_cast<double>(nValue) / Power10(get_digits()); + } + + sal_Int64 fromGtk(double fValue) const + { + return FRound(fValue * Power10(get_digits())); + } + +#if !GTK_CHECK_VERSION(4, 0, 0) + static gboolean signalScroll(GtkWidget* pWidget, GdkEventScroll* /*pEvent*/, gpointer /*widget*/) + { + // tdf#149823 follow WheelBehavior setting, so if we don't have focus + // we don't react to the scroll-event. + MouseWheelBehaviour nWheelBehavior(Application::GetSettings().GetMouseSettings().GetWheelBehavior()); + switch (nWheelBehavior) + { + case MouseWheelBehaviour::ALWAYS: + break; + case MouseWheelBehaviour::Disable: + g_signal_stop_emission_by_name(pWidget, "scroll-event"); + break; + case MouseWheelBehaviour::FocusOnly: + if (!gtk_widget_has_focus(pWidget)) + g_signal_stop_emission_by_name(pWidget, "scroll-event"); + break; + } + return false; + } +#endif + +public: + GtkInstanceSpinButton(GtkSpinButton* pButton, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : GtkInstanceEditable(GTK_WIDGET(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) + { +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_text_set_activates_default(GTK_TEXT(m_pDelegate), true); +#endif +#if !GTK_CHECK_VERSION(4, 0, 0) + g_signal_connect(pButton, "scroll-event", G_CALLBACK(signalScroll), this); +#endif + } + + virtual sal_Int64 get_value() const override + { + return fromGtk(gtk_spin_button_get_value(m_pButton)); + } + + virtual void set_value(sal_Int64 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) + { +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_editable_set_text(m_pEditable, OUStringToOString(rText, RTL_TEXTENCODING_UTF8).getStr()); +#else + gtk_entry_set_text(GTK_ENTRY(m_pButton), OUStringToOString(rText, RTL_TEXTENCODING_UTF8).getStr()); +#endif + + 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) + { +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_editable_set_text(m_pEditable, OUStringToOString(rText, RTL_TEXTENCODING_UTF8).getStr()); +#else + gtk_entry_set_text(GTK_ENTRY(m_pButton), OUStringToOString(rText, RTL_TEXTENCODING_UTF8).getStr()); +#endif + m_bBlank = false; + } + } + enable_notify_events(); + } + + virtual void set_range(sal_Int64 min, sal_Int64 max) override + { + disable_notify_events(); + gtk_spin_button_set_range(m_pButton, toGtk(min), toGtk(max)); + enable_notify_events(); + } + + virtual void get_range(sal_Int64& min, sal_Int64& 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 set_font(const vcl::Font& rFont) override + { + m_aCustomFont.use_custom_font(&rFont, u"spinbutton"); + } + + virtual void disable_notify_events() override + { + g_signal_handler_block(m_pButton, m_nValueChangedSignalId); + GtkInstanceEditable::disable_notify_events(); + } + + virtual void enable_notify_events() override + { + GtkInstanceEditable::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); + } +}; + +} + +namespace { + +class GtkInstanceFormattedSpinButton : public GtkInstanceEditable, public virtual weld::FormattedSpinButton +{ +private: + GtkSpinButton* m_pButton; + std::unique_ptr<weld::EntryFormatter> m_xOwnFormatter; + weld::EntryFormatter* m_pFormatter; + gulong m_nValueChangedSignalId; + gulong m_nOutputSignalId; + gulong m_nInputSignalId; + bool m_bEmptyField; + bool m_bSyncingValue; + double m_dValueWhenEmpty; + + bool signal_output() + { + double fValue = gtk_spin_button_get_value(m_pButton); + m_bEmptyField &= fValue == m_dValueWhenEmpty; + if (!m_bEmptyField) + GetFormatter().SetValue(fValue); + 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) + { + Formatter& rFormatter = GetFormatter(); + rFormatter.Modify(); + // if the blank-mode is enabled then if the input is empty don't parse + // the input but keep the value as it is. store what the value the + // blank is associated with and until the value is changed, or the text + // is updated from the outside, don't output that value + m_bEmptyField = rFormatter.IsEmptyFieldEnabled() && get_text().isEmpty(); + if (m_bEmptyField) + { + m_dValueWhenEmpty = gtk_spin_button_get_value(m_pButton); + *value = m_dValueWhenEmpty; + } + else + *value = rFormatter.GetValue(); + 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) + : GtkInstanceEditable(GTK_WIDGET(pButton), pBuilder, bTakeOwnership) + , m_pButton(pButton) + , m_pFormatter(nullptr) + , 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_bEmptyField(false) + , m_bSyncingValue(false) + , m_dValueWhenEmpty(0.0) + { + } + + virtual void set_text(const OUString& rText) override + { + GtkInstanceEditable::set_text(rText); + Formatter& rFormatter = GetFormatter(); + m_bEmptyField = rFormatter.IsEmptyFieldEnabled() && rText.isEmpty(); + if (m_bEmptyField) + m_dValueWhenEmpty = gtk_spin_button_get_value(m_pButton); + } + + virtual void connect_changed(const Link<weld::Entry&, void>& rLink) override + { + if (!m_pFormatter) // once a formatter is set, it takes over "changed" + { + GtkInstanceEditable::connect_changed(rLink); + return; + } + m_pFormatter->connect_changed(rLink); + } + + virtual void connect_focus_out(const Link<weld::Widget&, void>& rLink) override + { + if (!m_pFormatter) // once a formatter is set, it takes over "focus-out" + { + GtkInstanceEditable::connect_focus_out(rLink); + return; + } + m_pFormatter->connect_focus_out(rLink); + } + + virtual void SetFormatter(weld::EntryFormatter* pFormatter) override + { + m_xOwnFormatter.reset(); + m_pFormatter = pFormatter; + sync_range_from_formatter(); + sync_value_from_formatter(); + sync_increments_from_formatter(); + } + + virtual weld::EntryFormatter& GetFormatter() override + { + if (!m_pFormatter) + { + auto aFocusOutHdl = m_aFocusOutHdl; + m_aFocusOutHdl = Link<weld::Widget&, void>(); + auto aChangeHdl = m_aChangeHdl; + m_aChangeHdl = Link<weld::Entry&, void>(); + + double fValue = gtk_spin_button_get_value(m_pButton); + double fMin, fMax; + gtk_spin_button_get_range(m_pButton, &fMin, &fMax); + double fStep; + gtk_spin_button_get_increments(m_pButton, &fStep, nullptr); + m_xOwnFormatter.reset(new weld::EntryFormatter(*this)); + m_xOwnFormatter->SetMinValue(fMin); + m_xOwnFormatter->SetMaxValue(fMax); + m_xOwnFormatter->SetSpinSize(fStep); + m_xOwnFormatter->SetValue(fValue); + + m_xOwnFormatter->connect_focus_out(aFocusOutHdl); + m_xOwnFormatter->connect_changed(aChangeHdl); + + m_pFormatter = m_xOwnFormatter.get(); + } + return *m_pFormatter; + } + + virtual void sync_value_from_formatter() override + { + if (!m_pFormatter) + return; + // tdf#135317 avoid reenterence + if (m_bSyncingValue) + return; + m_bSyncingValue = true; + disable_notify_events(); + // tdf#138519 use gtk_adjustment_set_value instead of gtk_spin_button_set_value because the + // latter doesn't change the value if the new value is less than an EPSILON diff of 1e-10 + // from the old value + gtk_adjustment_set_value(gtk_spin_button_get_adjustment(m_pButton), m_pFormatter->GetValue()); + enable_notify_events(); + m_bSyncingValue = false; + } + + virtual void sync_range_from_formatter() override + { + if (!m_pFormatter) + return; + disable_notify_events(); + double fMin = m_pFormatter->HasMinValue() ? m_pFormatter->GetMinValue() : std::numeric_limits<double>::lowest(); + double fMax = m_pFormatter->HasMaxValue() ? m_pFormatter->GetMaxValue() : std::numeric_limits<double>::max(); + gtk_spin_button_set_range(m_pButton, fMin, fMax); + enable_notify_events(); + } + + virtual void sync_increments_from_formatter() override + { + if (!m_pFormatter) + return; + disable_notify_events(); + double fSpinSize = m_pFormatter->GetSpinSize(); + gtk_spin_button_set_increments(m_pButton, fSpinSize, fSpinSize * 10); + enable_notify_events(); + } + + virtual void set_font(const vcl::Font& rFont) override + { + m_aCustomFont.use_custom_font(&rFont, u"spinbutton"); + } + + virtual void disable_notify_events() override + { + g_signal_handler_block(m_pButton, m_nValueChangedSignalId); + GtkInstanceEditable::disable_notify_events(); + } + + virtual void enable_notify_events() override + { + GtkInstanceEditable::enable_notify_events(); + g_signal_handler_unblock(m_pButton, m_nValueChangedSignalId); + } + + 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); + + m_pFormatter = nullptr; + m_xOwnFormatter.reset(); + } +}; + +} + +namespace { + +class GtkInstanceLabel : public GtkInstanceWidget, public virtual weld::Label +{ +private: + GtkLabel* m_pLabel; + + void set_text_background_color(const Color& rColor) + { + guint16 nRed = rColor.GetRed() << 8; + guint16 nGreen = rColor.GetGreen() << 8; + guint16 nBlue = rColor.GetBlue() << 8; + + PangoAttrType aFilterAttrs[] = {PANGO_ATTR_BACKGROUND, PANGO_ATTR_INVALID}; + + PangoAttrList* pOrigList = gtk_label_get_attributes(m_pLabel); + PangoAttrList* pAttrs = pOrigList ? pango_attr_list_copy(pOrigList) : pango_attr_list_new(); + PangoAttrList* pRemovedAttrs = pOrigList ? pango_attr_list_filter(pAttrs, filter_pango_attrs, &aFilterAttrs) : nullptr; + pango_attr_list_insert(pAttrs, pango_attr_background_new(nRed, nGreen, nBlue)); + gtk_label_set_attributes(m_pLabel, pAttrs); + pango_attr_list_unref(pAttrs); + pango_attr_list_unref(pRemovedAttrs); + } + + void set_text_foreground_color(const Color& rColor, bool bSetBold) + { + guint16 nRed = rColor.GetRed() << 8; + guint16 nGreen = rColor.GetGreen() << 8; + guint16 nBlue = rColor.GetBlue() << 8; + + PangoAttrType aFilterAttrs[] = {PANGO_ATTR_FOREGROUND, PANGO_ATTR_WEIGHT, PANGO_ATTR_INVALID}; + + if (!bSetBold) + aFilterAttrs[1] = PANGO_ATTR_INVALID; + + PangoAttrList* pOrigList = gtk_label_get_attributes(m_pLabel); + PangoAttrList* pAttrs = pOrigList ? pango_attr_list_copy(pOrigList) : pango_attr_list_new(); + PangoAttrList* pRemovedAttrs = pOrigList ? pango_attr_list_filter(pAttrs, filter_pango_attrs, &aFilterAttrs) : nullptr; + if (rColor != COL_AUTO) + pango_attr_list_insert(pAttrs, pango_attr_foreground_new(nRed, nGreen, nBlue)); + if (bSetBold) + pango_attr_list_insert(pAttrs, pango_attr_weight_new(PANGO_WEIGHT_BOLD)); + gtk_label_set_attributes(m_pLabel, pAttrs); + pango_attr_list_unref(pAttrs); + pango_attr_list_unref(pRemovedAttrs); + } + +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_label_type(weld::LabelType eType) override + { + switch (eType) + { + case weld::LabelType::Normal: + gtk_label_set_attributes(m_pLabel, nullptr); + break; + case weld::LabelType::Warning: + set_text_background_color(Application::GetSettings().GetStyleSettings().GetWarningColor()); + break; + case weld::LabelType::Error: + set_text_background_color(Application::GetSettings().GetStyleSettings().GetHighlightColor()); + break; + case weld::LabelType::Title: + set_text_foreground_color(Application::GetSettings().GetStyleSettings().GetLightColor(), true); + break; + } + } + + virtual void set_font(const vcl::Font& rFont) override + { + ::set_font(m_pLabel, rFont); + } + + virtual void set_font_color(const Color& rColor) override + { + set_text_foreground_color(rColor, false); + } +}; + +} + +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 { + +GdkClipboard* widget_get_clipboard(GtkWidget* pWidget) +{ +#if GTK_CHECK_VERSION(4, 0, 0) + return gtk_widget_get_clipboard(pWidget); +#else + return gtk_widget_get_clipboard(pWidget, GDK_SELECTION_CLIPBOARD); +#endif +} + +class GtkInstanceTextView : public GtkInstanceWidget, public virtual weld::TextView +{ +private: + GtkTextView* m_pTextView; + GtkTextBuffer* m_pTextBuffer; + GtkAdjustment* m_pVAdjustment; + GtkCssProvider* m_pFgCssProvider; + WidgetFont m_aCustomFont; + int m_nMaxTextLength; + gulong m_nChangedSignalId; // we don't disable/enable this one, it's to implement max-length + gulong m_nInsertTextSignalId; + gulong m_nCursorPosSignalId; + gulong m_nHasSelectionSignalId; // we don't disable/enable this one, it's to implement + // auto-scroll to cursor on losing selection + gulong m_nVAdjustChangedSignalId; +#if !GTK_CHECK_VERSION(4, 0, 0) + gulong m_nButtonPressEvent; // we don't disable/enable this one, it's to block mouse + // click down from getting to (potential) toplevel + // GtkSalFrame parent, which grabs focus away + + static gboolean signalButtonPressEvent(GtkWidget*, GdkEventButton*, gpointer) + { + // e.g. on clicking on the help TextView in OTableDesignHelpBar the currently displayed text shouldn't disappear + return true; + } +#endif + + static void signalChanged(GtkTextBuffer*, gpointer widget) + { + GtkInstanceTextView* pThis = static_cast<GtkInstanceTextView*>(widget); + SolarMutexGuard aGuard; + pThis->signal_changed(); + } + + static void signalInserText(GtkTextBuffer *pBuffer, GtkTextIter *pLocation, gchar* /*pText*/, gint /*nLen*/, gpointer widget) + { + GtkInstanceTextView* pThis = static_cast<GtkInstanceTextView*>(widget); + pThis->insert_text(pBuffer, pLocation); + } + + void insert_text(GtkTextBuffer *pBuffer, GtkTextIter *pLocation) + { + if (m_nMaxTextLength) + { + gint nCount = gtk_text_buffer_get_char_count(pBuffer); + if (nCount > m_nMaxTextLength) + { + GtkTextIter nStart, nEnd; + gtk_text_buffer_get_iter_at_offset(m_pTextBuffer, &nStart, m_nMaxTextLength); + gtk_text_buffer_get_end_iter(m_pTextBuffer, &nEnd); + gtk_text_buffer_delete(m_pTextBuffer, &nStart, &nEnd); + gtk_text_iter_assign(pLocation, &nStart); + } + } + } + + static void signalCursorPosition(GtkTextBuffer*, GParamSpec*, gpointer widget) + { + GtkInstanceTextView* pThis = static_cast<GtkInstanceTextView*>(widget); + pThis->signal_cursor_position(); + } + + static void signalHasSelection(GtkTextBuffer*, GParamSpec*, gpointer widget) + { + GtkInstanceTextView* pThis = static_cast<GtkInstanceTextView*>(widget); + pThis->signal_has_selection(); + } + + void signal_has_selection() + { + /* + in the data browser (Data Sources, shift+ctrl+f4), entering a + multiline cell selects all, on cursoring to the right, the selection + is lost and the cursor is at the end but gtk doesn't auto-scroll to + the cursor so if the text needs scrolling to see the cursor it is off + screen, another cursor makes gtk auto-scroll as wanted. So on losing + selection help gtk out and do the initial scroll ourselves here + */ + if (!gtk_text_buffer_get_has_selection(m_pTextBuffer)) + { + GtkTextMark* pMark = gtk_text_buffer_get_insert(m_pTextBuffer); + gtk_text_view_scroll_mark_onscreen(m_pTextView, pMark); + } + } + + 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) + : GtkInstanceWidget(GTK_WIDGET(pTextView), pBuilder, bTakeOwnership) + , m_pTextView(pTextView) + , m_pTextBuffer(gtk_text_view_get_buffer(pTextView)) + , m_pVAdjustment(gtk_scrollable_get_vadjustment(GTK_SCROLLABLE(pTextView))) + , m_pFgCssProvider(nullptr) + , m_aCustomFont(m_pWidget) + , m_nMaxTextLength(0) + , m_nChangedSignalId(g_signal_connect(m_pTextBuffer, "changed", G_CALLBACK(signalChanged), this)) + , m_nInsertTextSignalId(g_signal_connect_after(m_pTextBuffer, "insert-text", G_CALLBACK(signalInserText), this)) + , m_nCursorPosSignalId(g_signal_connect(m_pTextBuffer, "notify::cursor-position", G_CALLBACK(signalCursorPosition), this)) + , m_nHasSelectionSignalId(g_signal_connect(m_pTextBuffer, "notify::has-selection", G_CALLBACK(signalHasSelection), this)) + , m_nVAdjustChangedSignalId(g_signal_connect(m_pVAdjustment, "value-changed", G_CALLBACK(signalVAdjustValueChanged), this)) +#if !GTK_CHECK_VERSION(4, 0, 0) + , m_nButtonPressEvent(g_signal_connect_after(m_pTextView, "button-press-event", G_CALLBACK(signalButtonPressEvent), this)) +#endif + { + } + + 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(); + OString sText(OUStringToOString(rText, RTL_TEXTENCODING_UTF8)); + gtk_text_buffer_set_text(m_pTextBuffer, sText.getStr(), sText.getLength()); + enable_notify_events(); + } + + virtual OUString get_text() const override + { + GtkTextIter start, end; + gtk_text_buffer_get_bounds(m_pTextBuffer, &start, &end); + char* pStr = gtk_text_buffer_get_text(m_pTextBuffer, &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(); + gtk_text_buffer_delete_selection(m_pTextBuffer, false, gtk_text_view_get_editable(m_pTextView)); + OString sText(OUStringToOString(rText, RTL_TEXTENCODING_UTF8)); + gtk_text_buffer_insert_at_cursor(m_pTextBuffer, sText.getStr(), sText.getLength()); + enable_notify_events(); + } + + virtual bool get_selection_bounds(int& rStartPos, int& rEndPos) override + { + GtkTextIter start, end; + gtk_text_buffer_get_selection_bounds(m_pTextBuffer, &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(); + GtkTextIter start, end; + gtk_text_buffer_get_iter_at_offset(m_pTextBuffer, &start, nStartPos); + gtk_text_buffer_get_iter_at_offset(m_pTextBuffer, &end, nEndPos); + gtk_text_buffer_select_range(m_pTextBuffer, &start, &end); + GtkTextMark* mark = gtk_text_buffer_create_mark(m_pTextBuffer, "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 bool get_editable() const override + { + return gtk_text_view_get_editable(m_pTextView); + } + + virtual void set_max_length(int nChars) override + { + m_nMaxTextLength = nChars; + } + + virtual void set_monospace(bool bMonospace) override + { + gtk_text_view_set_monospace(m_pTextView, bMonospace); + } + + virtual void set_font_color(const Color& rColor) override + { + const bool bRemoveColor = rColor == COL_AUTO; + if (bRemoveColor && !m_pFgCssProvider) + return; + GtkStyleContext *pWidgetContext = gtk_widget_get_style_context(GTK_WIDGET(m_pTextView)); + if (m_pFgCssProvider) + { + gtk_style_context_remove_provider(pWidgetContext, GTK_STYLE_PROVIDER(m_pFgCssProvider)); + m_pFgCssProvider = nullptr; + } + if (bRemoveColor) + return; + OUString sColor = rColor.AsRGBHexString(); + m_pFgCssProvider = gtk_css_provider_new(); + OUString aBuffer = "textview text { color: #" + sColor + "; }"; + OString aResult = OUStringToOString(aBuffer, RTL_TEXTENCODING_UTF8); + css_provider_load_from_data(m_pFgCssProvider, aResult.getStr(), aResult.getLength()); + gtk_style_context_add_provider(pWidgetContext, GTK_STYLE_PROVIDER(m_pFgCssProvider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + } + + virtual void set_font(const vcl::Font& rFont) override + { + m_aCustomFont.use_custom_font(&rFont, u"textview"); + } + + virtual vcl::Font get_font() override + { + if (const vcl::Font* pFont = m_aCustomFont.get_custom_font()) + return *pFont; + return GtkInstanceWidget::get_font(); + } + + 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); + GtkInstanceWidget::disable_notify_events(); + } + + virtual void enable_notify_events() override + { + GtkInstanceWidget::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); + } + + // in gtk, 'up' when on the first line, will jump to the start of the line + // if not there already + virtual bool can_move_cursor_with_up() const override + { + GtkTextIter start, end; + gtk_text_buffer_get_selection_bounds(m_pTextBuffer, &start, &end); + return !gtk_text_iter_equal(&start, &end) || !gtk_text_iter_is_start(&start); + } + + // in gtk, 'down' when on the first line, will jump to the end of the line + // if not there already + virtual bool can_move_cursor_with_down() const override + { + GtkTextIter start, end; + gtk_text_buffer_get_selection_bounds(m_pTextBuffer, &start, &end); + return !gtk_text_iter_equal(&start, &end) || !gtk_text_iter_is_end(&end); + } + + virtual void cut_clipboard() override + { + GdkClipboard *pClipboard = widget_get_clipboard(GTK_WIDGET(m_pTextView)); + gtk_text_buffer_cut_clipboard(m_pTextBuffer, pClipboard, get_editable()); + } + + virtual void copy_clipboard() override + { + GdkClipboard *pClipboard = widget_get_clipboard(GTK_WIDGET(m_pTextView)); + gtk_text_buffer_copy_clipboard(m_pTextBuffer, pClipboard); + } + + virtual void paste_clipboard() override + { + GdkClipboard *pClipboard = widget_get_clipboard(GTK_WIDGET(m_pTextView)); + gtk_text_buffer_paste_clipboard(m_pTextBuffer, pClipboard, nullptr, get_editable()); + } + + virtual void set_alignment(TxtAlign eXAlign) override + { + GtkJustification eJust = GTK_JUSTIFY_LEFT; + switch (eXAlign) + { + case TxtAlign::Left: + eJust = GTK_JUSTIFY_LEFT; + break; + case TxtAlign::Center: + eJust = GTK_JUSTIFY_CENTER; + break; + case TxtAlign::Right: + eJust = GTK_JUSTIFY_RIGHT; + break; + } + gtk_text_view_set_justification(m_pTextView, eJust); + } + + 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 + { +#if !GTK_CHECK_VERSION(4, 0, 0) + g_signal_handler_disconnect(m_pTextView, m_nButtonPressEvent); +#endif + g_signal_handler_disconnect(m_pVAdjustment, m_nVAdjustChangedSignalId); + g_signal_handler_disconnect(m_pTextBuffer, m_nInsertTextSignalId); + g_signal_handler_disconnect(m_pTextBuffer, m_nChangedSignalId); + g_signal_handler_disconnect(m_pTextBuffer, m_nCursorPosSignalId); + g_signal_handler_disconnect(m_pTextBuffer, m_nHasSelectionSignalId); + } +}; + +} + +namespace { + +class GtkInstanceDrawingArea; + +// IMHandler +class IMHandler +{ +private: + GtkInstanceDrawingArea* m_pArea; +#if GTK_CHECK_VERSION(4, 0, 0) + GtkEventController* m_pFocusController; +#endif + GtkIMContext* m_pIMContext; + OUString m_sPreeditText; + gulong m_nFocusInSignalId; + gulong m_nFocusOutSignalId; + bool m_bExtTextInput; + +public: + IMHandler(GtkInstanceDrawingArea* pArea); + + void signalFocus(bool bIn); + +#if GTK_CHECK_VERSION(4, 0, 0) + static void signalFocusIn(GtkEventControllerFocus*, gpointer im_handler); +#else + static gboolean signalFocusIn(GtkWidget*, GdkEvent*, gpointer im_handler); +#endif + +#if GTK_CHECK_VERSION(4, 0, 0) + static void signalFocusOut(GtkEventControllerFocus*, gpointer im_handler); +#else + static gboolean signalFocusOut(GtkWidget*, GdkEvent*, gpointer im_handler); +#endif + + ~IMHandler(); + + void updateIMSpotLocation(); + + void set_cursor_location(const tools::Rectangle& rRect); + + static void signalIMCommit(GtkIMContext* /*pContext*/, gchar* pText, gpointer im_handler); + + static void signalIMPreeditChanged(GtkIMContext* pIMContext, gpointer im_handler); + + static gboolean signalIMRetrieveSurrounding(GtkIMContext* pContext, gpointer im_handler); + + static gboolean signalIMDeleteSurrounding(GtkIMContext*, gint nOffset, gint nChars, + gpointer im_handler); + + void StartExtTextInput(); + + static void signalIMPreeditStart(GtkIMContext*, gpointer im_handler); + + void EndExtTextInput(); + + static void signalIMPreeditEnd(GtkIMContext*, gpointer im_handler); + +#if !GTK_CHECK_VERSION(4, 0, 0) + bool im_context_filter_keypress(const GdkEventKey* pEvent); +#endif +}; + +#if !GTK_CHECK_VERSION(4, 0, 0) +AtkObject* (*default_drawing_area_get_accessible)(GtkWidget *widget); +#endif + +class GtkInstanceDrawingArea : public GtkInstanceWidget, public virtual weld::DrawingArea +{ +private: + GtkDrawingArea* m_pDrawingArea; + a11yref m_xAccessible; +#if !GTK_CHECK_VERSION(4, 0, 0) + AtkObject *m_pAccessible; +#endif + ScopedVclPtrInstance<VirtualDevice> m_xDevice; + std::unique_ptr<IMHandler> m_xIMHandler; + cairo_surface_t* m_pSurface; +#if !GTK_CHECK_VERSION(4, 0, 0) + gulong m_nDrawSignalId; +#endif + gulong m_nQueryTooltip; +#if !GTK_CHECK_VERSION(4, 0, 0) + gulong m_nPopupMenu; + gulong m_nScrollEvent; +#endif + GtkGesture *m_pZoomGesture; + +#if GTK_CHECK_VERSION(4, 0, 0) + static void signalDraw(GtkDrawingArea*, cairo_t *cr, int /*width*/, int /*height*/, gpointer widget) +#else + static gboolean signalDraw(GtkWidget*, cairo_t* cr, gpointer widget) +#endif + { + GtkInstanceDrawingArea* pThis = static_cast<GtkInstanceDrawingArea*>(widget); + SolarMutexGuard aGuard; + pThis->signal_draw(cr); +#if !GTK_CHECK_VERSION(4, 0, 0) + return false; +#endif + } + void signal_draw(cairo_t* cr) + { + if (!m_pSurface) + return; + + GdkRectangle rect; +#if GTK_CHECK_VERSION(4, 0, 0) + double clip_x1, clip_x2, clip_y1, clip_y2; + cairo_clip_extents(cr, &clip_x1, &clip_y1, &clip_x2, &clip_y2); + rect.x = clip_x1; + rect.y = clip_y1; + rect.width = clip_x2 - clip_x1; + rect.height = clip_y2 - clip_y1; + if (rect.width <= 0 || rect.height <= 0) + return; +#else + if (!gdk_cairo_get_clip_rectangle(cr, &rect)) + return; +#endif + + 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 + { + Size aNewSize(nWidth, nHeight); + if (m_pSurface && aNewSize == m_xDevice->GetOutputSizePixel()) + { + // unchanged + return; + } + m_xDevice->SetOutputSizePixel(Size(nWidth, nHeight)); + m_pSurface = get_underlying_cairo_surface(*m_xDevice); + GtkInstanceWidget::signal_size_allocate(nWidth, nHeight); + } + 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); + } +#if !GTK_CHECK_VERSION(4, 0, 0) + bool signal_scroll(const 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); + } +#endif + + bool handleSignalZoom(GtkGesture* gesture, GdkEventSequence* sequence, + GestureEventZoomType eEventType) + { + gdouble x = 0; + gdouble y = 0; + gtk_gesture_get_point(gesture, sequence, &x, &y); + + double fScaleDelta = gtk_gesture_zoom_get_scale_delta(GTK_GESTURE_ZOOM(gesture)); + + CommandGestureZoomData aGestureData(x, y, eEventType, fScaleDelta); + CommandEvent aCEvt(Point(x, y), CommandEventId::GestureZoom, true, &aGestureData); + return m_aCommandHdl.Call(aCEvt); + } + + static bool signalZoomBegin(GtkGesture* gesture, GdkEventSequence* sequence, gpointer widget) + { + GtkInstanceDrawingArea* pThis = static_cast<GtkInstanceDrawingArea*>(widget); + return pThis->handleSignalZoom(gesture, sequence, GestureEventZoomType::Begin); + } + + static bool signalZoomUpdate(GtkGesture* gesture, GdkEventSequence* sequence, gpointer widget) + { + GtkInstanceDrawingArea* pThis = static_cast<GtkInstanceDrawingArea*>(widget); + return pThis->handleSignalZoom(gesture, sequence, GestureEventZoomType::Update); + } + + static bool signalZoomEnd(GtkGesture* gesture, GdkEventSequence* sequence, gpointer widget) + { + GtkInstanceDrawingArea* pThis = static_cast<GtkInstanceDrawingArea*>(widget); + return pThis->handleSignalZoom(gesture, sequence, GestureEventZoomType::End); + } + +#if GTK_CHECK_VERSION(4, 0, 0) + static void signalResize(GtkDrawingArea*, int nWidth, int nHeight, gpointer widget) + { + GtkInstanceWidget* pThis = static_cast<GtkInstanceWidget*>(widget); + SolarMutexGuard aGuard; + pThis->signal_size_allocate(nWidth, nHeight); + } +#endif + +public: + GtkInstanceDrawingArea(GtkDrawingArea* pDrawingArea, GtkInstanceBuilder* pBuilder, a11yref xA11y, bool bTakeOwnership) + : GtkInstanceWidget(GTK_WIDGET(pDrawingArea), pBuilder, bTakeOwnership) + , m_pDrawingArea(pDrawingArea) + , m_xAccessible(std::move(xA11y)) +#if !GTK_CHECK_VERSION(4, 0, 0) + , m_pAccessible(nullptr) +#endif + , m_xDevice(DeviceFormat::WITHOUT_ALPHA) + , m_pSurface(nullptr) + , m_nQueryTooltip(g_signal_connect(m_pDrawingArea, "query-tooltip", G_CALLBACK(signalQueryTooltip), this)) +#if !GTK_CHECK_VERSION(4, 0, 0) + , 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)) +#endif + { +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_drawing_area_set_draw_func(m_pDrawingArea, signalDraw, this, nullptr); +#else + m_nDrawSignalId = g_signal_connect(m_pDrawingArea, "draw", G_CALLBACK(signalDraw), this); + gtk_widget_add_events(GTK_WIDGET(pDrawingArea), GDK_TOUCHPAD_GESTURE_MASK); +#endif + + ensureMouseEventWidget(); +#if GTK_CHECK_VERSION(4,0,0) + m_pZoomGesture = gtk_gesture_zoom_new(); + gtk_widget_add_controller(m_pMouseEventBox, GTK_EVENT_CONTROLLER(m_pZoomGesture)); +#else + m_pZoomGesture = gtk_gesture_zoom_new(m_pMouseEventBox); +#endif + gtk_event_controller_set_propagation_phase(GTK_EVENT_CONTROLLER(m_pZoomGesture), + GTK_PHASE_TARGET); + // Note that the default zoom gesture signal handler needs to run first to setup correct + // scale delta. Otherwise the first "begin" event will always contain scale delta of infinity. + g_signal_connect_after(m_pZoomGesture, "begin", G_CALLBACK(signalZoomBegin), this); + g_signal_connect_after(m_pZoomGesture, "update", G_CALLBACK(signalZoomUpdate), this); + g_signal_connect_after(m_pZoomGesture, "end", G_CALLBACK(signalZoomEnd), 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()); + } + +#if !GTK_CHECK_VERSION(4, 0, 0) + 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; + } +#endif + +#if GTK_CHECK_VERSION(4, 0, 0) + virtual void connect_size_allocate(const Link<const Size&, void>& rLink) override + { + m_nSizeAllocateSignalId = g_signal_connect(m_pWidget, "resize", G_CALLBACK(signalResize), this); + weld::Widget::connect_size_allocate(rLink); + } +#endif + + virtual void connect_mouse_press(const Link<const MouseEvent&, bool>& rLink) override + { +#if !GTK_CHECK_VERSION(4, 0, 0) + if (!(gtk_widget_get_events(m_pWidget) & GDK_BUTTON_PRESS_MASK)) + gtk_widget_add_events(m_pWidget, GDK_BUTTON_PRESS_MASK); +#endif + GtkInstanceWidget::connect_mouse_press(rLink); + } + + virtual void connect_mouse_release(const Link<const MouseEvent&, bool>& rLink) override + { +#if !GTK_CHECK_VERSION(4, 0, 0) + if (!(gtk_widget_get_events(m_pWidget) & GDK_BUTTON_RELEASE_MASK)) + gtk_widget_add_events(m_pWidget, GDK_BUTTON_RELEASE_MASK); +#endif + GtkInstanceWidget::connect_mouse_release(rLink); + } + + 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)); + widget_set_cursor(GTK_WIDGET(m_pDrawingArea), pCursor); + } + + virtual Point get_pointer_position() const override + { + GdkDisplay *pDisplay = gtk_widget_get_display(m_pWidget); + GdkSeat* pSeat = gdk_display_get_default_seat(pDisplay); + GdkDevice* pPointer = gdk_seat_get_pointer(pSeat); + double x(-1), y(-1); + GdkSurface* pWin = widget_get_surface(m_pWidget); + surface_get_device_position(pWin, pPointer, x, y, nullptr); + return Point(x, y); + } + + 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); + } + +#if !GTK_CHECK_VERSION(4, 0, 0) + virtual bool do_signal_key_press(const GdkEventKey* pEvent) override; + virtual bool do_signal_key_release(const GdkEventKey* pEvent) override; +#endif + + 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 + { +#if !GTK_CHECK_VERSION(4, 0, 0) + 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()); +#else + (void)x; (void)y; (void)width; (void)height; + queue_draw(); +#endif + } + + 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_relation_set should only be called on a vcl impl"); + return uno::Reference<css::accessibility::XAccessibleRelationSet>(); + } + + virtual AbsoluteScreenPixelPoint get_accessible_location_on_screen() override + { +#if !GTK_CHECK_VERSION(4, 0, 0) + AtkObject* pAtkObject = default_drawing_area_get_accessible(m_pWidget); +#endif + gint x(0), y(0); +#if !GTK_CHECK_VERSION(4, 0, 0) + if (pAtkObject && ATK_IS_COMPONENT(pAtkObject)) + atk_component_get_extents(ATK_COMPONENT(pAtkObject), &x, &y, nullptr, nullptr, ATK_XY_SCREEN); +#endif + return AbsoluteScreenPixelPoint(x, y); + } + + virtual void set_accessible_name(const OUString& rName) override + { +#if !GTK_CHECK_VERSION(4, 0, 0) + AtkObject* pAtkObject = default_drawing_area_get_accessible(m_pWidget); + if (!pAtkObject) + return; + atk_object_set_name(pAtkObject, OUStringToOString(rName, RTL_TEXTENCODING_UTF8).getStr()); +#else + (void)rName; +#endif + } + + virtual OUString get_accessible_name() const override + { +#if !GTK_CHECK_VERSION(4, 0, 0) + 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); +#else + return OUString(); +#endif + } + + virtual OUString get_accessible_description() const override + { +#if !GTK_CHECK_VERSION(4, 0, 0) + 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); +#else + return OUString(); +#endif + } + + 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 + { +#if GTK_CHECK_VERSION(4,0,0) + gtk_widget_remove_controller(m_pMouseEventBox, GTK_EVENT_CONTROLLER(m_pZoomGesture)); +#else + g_clear_object(&m_pZoomGesture); +#endif + + g_object_steal_data(G_OBJECT(m_pDrawingArea), "g-lo-GtkInstanceDrawingArea"); +#if !GTK_CHECK_VERSION(4, 0, 0) + if (m_pAccessible) + g_object_unref(m_pAccessible); +#endif + css::uno::Reference<css::lang::XComponent> xComp(m_xAccessible, css::uno::UNO_QUERY); + if (xComp.is()) + xComp->dispose(); +#if !GTK_CHECK_VERSION(4, 0, 0) + g_signal_handler_disconnect(m_pDrawingArea, m_nScrollEvent); +#endif +#if !GTK_CHECK_VERSION(4, 0, 0) + g_signal_handler_disconnect(m_pDrawingArea, m_nPopupMenu); +#endif + g_signal_handler_disconnect(m_pDrawingArea, m_nQueryTooltip); +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_drawing_area_set_draw_func(m_pDrawingArea, nullptr, nullptr, nullptr); +#else + g_signal_handler_disconnect(m_pDrawingArea, m_nDrawSignalId); +#endif + } + + virtual OutputDevice& get_ref_device() override + { + return *m_xDevice; + } + + bool signal_command(const CommandEvent& rCEvt) + { + return m_aCommandHdl.Call(rCEvt); + } + + virtual void click(const Point& rPos) override + { + MouseEvent aEvent(rPos); + m_aMousePressHdl.Call(aEvent); + m_aMouseReleaseHdl.Call(aEvent); + } +}; + +IMHandler::IMHandler(GtkInstanceDrawingArea* pArea) + : m_pArea(pArea) + , m_pIMContext(gtk_im_multicontext_new()) + , m_bExtTextInput(false) +{ + GtkWidget* pWidget = m_pArea->getWidget(); + +#if GTK_CHECK_VERSION(4, 0, 0) + m_pFocusController = gtk_event_controller_focus_new(); + gtk_widget_add_controller(pWidget, m_pFocusController); + + m_nFocusInSignalId = g_signal_connect(m_pFocusController, "enter", G_CALLBACK(signalFocusIn), this); + m_nFocusOutSignalId = g_signal_connect(m_pFocusController, "leave", G_CALLBACK(signalFocusOut), this); +#else + m_nFocusInSignalId = g_signal_connect(pWidget, "focus-in-event", G_CALLBACK(signalFocusIn), this); + m_nFocusOutSignalId = g_signal_connect(pWidget, "focus-out-event", G_CALLBACK(signalFocusOut), this); +#endif + + 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); + + if (!gtk_widget_get_realized(pWidget)) + gtk_widget_realize(pWidget); + im_context_set_client_widget(m_pIMContext, pWidget); + if (gtk_widget_has_focus(m_pArea->getWidget())) + gtk_im_context_focus_in(m_pIMContext); +} + +void IMHandler::signalFocus(bool bIn) +{ + if (bIn) + gtk_im_context_focus_in(m_pIMContext); + else + gtk_im_context_focus_out(m_pIMContext); +} + +#if GTK_CHECK_VERSION(4, 0, 0) +void IMHandler::signalFocusIn(GtkEventControllerFocus*, gpointer im_handler) +#else +gboolean IMHandler::signalFocusIn(GtkWidget*, GdkEvent*, gpointer im_handler) +#endif +{ + IMHandler* pThis = static_cast<IMHandler*>(im_handler); + pThis->signalFocus(true); +#if !GTK_CHECK_VERSION(4, 0, 0) + return false; +#endif +} + +#if GTK_CHECK_VERSION(4, 0, 0) +void IMHandler::signalFocusOut(GtkEventControllerFocus*, gpointer im_handler) +#else +gboolean IMHandler::signalFocusOut(GtkWidget*, GdkEvent*, gpointer im_handler) +#endif +{ + IMHandler* pThis = static_cast<IMHandler*>(im_handler); + pThis->signalFocus(false); +#if !GTK_CHECK_VERSION(4, 0, 0) + return false; +#endif +} + +IMHandler::~IMHandler() +{ + EndExtTextInput(); + +#if GTK_CHECK_VERSION(4, 0, 0) + g_signal_handler_disconnect(m_pFocusController, m_nFocusOutSignalId); + g_signal_handler_disconnect(m_pFocusController, m_nFocusInSignalId); +#else + g_signal_handler_disconnect(m_pArea->getWidget(), m_nFocusOutSignalId); + g_signal_handler_disconnect(m_pArea->getWidget(), m_nFocusInSignalId); +#endif + + if (gtk_widget_has_focus(m_pArea->getWidget())) + gtk_im_context_focus_out(m_pIMContext); + + // first give IC a chance to deinitialize + im_context_set_client_widget(m_pIMContext, nullptr); + // destroy old IC + g_object_unref(m_pIMContext); +} + +void IMHandler::updateIMSpotLocation() +{ + CommandEvent aCEvt(Point(), CommandEventId::CursorPos); + // we expect set_cursor_location to get triggered by this + m_pArea->signal_command(aCEvt); +} + +void IMHandler::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); +} + +void IMHandler::signalIMCommit(GtkIMContext* /*pContext*/, gchar* pText, gpointer im_handler) +{ + IMHandler* pThis = static_cast<IMHandler*>(im_handler); + + SolarMutexGuard aGuard; + + // 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(); +} + +void IMHandler::signalIMPreeditChanged(GtkIMContext* pIMContext, gpointer im_handler) +{ + IMHandler* pThis = static_cast<IMHandler*>(im_handler); + + SolarMutexGuard aGuard; + + 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(); +} + +gboolean IMHandler::signalIMRetrieveSurrounding(GtkIMContext* pContext, gpointer im_handler) +{ + IMHandler* pThis = static_cast<IMHandler*>(im_handler); + + SolarMutexGuard aGuard; + + OUString sSurroundingText; + int nCursorIndex = pThis->m_pArea->im_context_get_surrounding(sSurroundingText); + + if (nCursorIndex != -1) + { + OString sUTF = OUStringToOString(sSurroundingText, RTL_TEXTENCODING_UTF8); + std::u16string_view sCursorText(sSurroundingText.subView(0, nCursorIndex)); + gtk_im_context_set_surrounding(pContext, sUTF.getStr(), sUTF.getLength(), + OUStringToOString(sCursorText, RTL_TEXTENCODING_UTF8).getLength()); + } + + return true; +} + +gboolean IMHandler::signalIMDeleteSurrounding(GtkIMContext*, gint nOffset, gint nChars, + gpointer im_handler) +{ + bool bRet = false; + + IMHandler* pThis = static_cast<IMHandler*>(im_handler); + + SolarMutexGuard aGuard; + + OUString sSurroundingText; + sal_Int32 nCursorIndex = pThis->m_pArea->im_context_get_surrounding(sSurroundingText); + + Selection aSelection = SalFrame::CalcDeleteSurroundingSelection(sSurroundingText, nCursorIndex, nOffset, nChars); + if (aSelection != Selection(SAL_MAX_UINT32, SAL_MAX_UINT32)) + bRet = pThis->m_pArea->im_context_delete_surrounding(aSelection); + return bRet; +} + +void IMHandler::StartExtTextInput() +{ + if (m_bExtTextInput) + return; + CommandEvent aCEvt(Point(), CommandEventId::StartExtTextInput); + m_pArea->signal_command(aCEvt); + m_bExtTextInput = true; +} + +void IMHandler::signalIMPreeditStart(GtkIMContext*, gpointer im_handler) +{ + IMHandler* pThis = static_cast<IMHandler*>(im_handler); + SolarMutexGuard aGuard; + pThis->StartExtTextInput(); + pThis->updateIMSpotLocation(); +} + +void IMHandler::EndExtTextInput() +{ + if (!m_bExtTextInput) + return; + CommandEvent aCEvt(Point(), CommandEventId::EndExtTextInput); + m_pArea->signal_command(aCEvt); + m_bExtTextInput = false; +} + +void IMHandler::signalIMPreeditEnd(GtkIMContext*, gpointer im_handler) +{ + IMHandler* pThis = static_cast<IMHandler*>(im_handler); + SolarMutexGuard aGuard; + pThis->updateIMSpotLocation(); + pThis->EndExtTextInput(); +} + +#if !GTK_CHECK_VERSION(4, 0, 0) +bool IMHandler::im_context_filter_keypress(const GdkEventKey* pEvent) +{ + return gtk_im_context_filter_keypress(m_pIMContext, const_cast<GdkEventKey*>(pEvent)); +} +#endif + +#if !GTK_CHECK_VERSION(4, 0, 0) +bool GtkInstanceDrawingArea::do_signal_key_press(const GdkEventKey* pEvent) +{ + if (m_xIMHandler && m_xIMHandler->im_context_filter_keypress(pEvent)) + return true; + return GtkInstanceWidget::do_signal_key_press(pEvent); +} + +bool GtkInstanceDrawingArea::do_signal_key_release(const GdkEventKey* pEvent) +{ + if (m_xIMHandler && m_xIMHandler->im_context_filter_keypress(pEvent)) + return true; + return GtkInstanceWidget::do_signal_key_release(pEvent); +} +#endif + +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); +} + +} + +#if !GTK_CHECK_VERSION(4, 0, 0) + +static void InsertSpecialChar(GtkEntry *pEntry) +{ + if (auto pImplFncGetSpecialChars = vcl::GetGetSpecialCharsFunction()) + { + weld::Window* pDialogParent = nullptr; + + GtkWidget* pTopLevel = widget_get_toplevel(GTK_WIDGET(pEntry)); + if (GtkSalFrame* pFrame = pTopLevel ? GtkSalFrame::getFromWindow(pTopLevel) : nullptr) + pDialogParent = pFrame->GetFrameWeld(); + + std::unique_ptr<GtkInstanceWindow> xFrameWeld; + if (!pDialogParent && pTopLevel) + { + xFrameWeld.reset(new GtkInstanceWindow(GTK_WINDOW(pTopLevel), nullptr, false)); + pDialogParent = xFrameWeld.get(); + } + + OUString aChars = pImplFncGetSpecialChars(pDialogParent, ::get_font(GTK_WIDGET(pEntry))); + if (!aChars.isEmpty()) + { + gtk_editable_delete_selection(GTK_EDITABLE(pEntry)); + gint position = gtk_editable_get_position(GTK_EDITABLE(pEntry)); + OString sText(OUStringToOString(aChars, RTL_TEXTENCODING_UTF8)); + gtk_editable_insert_text(GTK_EDITABLE(pEntry), sText.getStr(), sText.getLength(), + &position); + gtk_editable_set_position(GTK_EDITABLE(pEntry), position); + } + } +} + +static gboolean signalEntryInsertSpecialCharKeyPress(GtkEntry* pEntry, GdkEventKey* pEvent, gpointer) +{ + if ((pEvent->keyval == GDK_KEY_S || pEvent->keyval == GDK_KEY_s) && + (pEvent->state & GDK_MODIFIER_MASK) == static_cast<GdkModifierType>(GDK_SHIFT_MASK|GDK_CONTROL_MASK)) + { + InsertSpecialChar(pEntry); + return true; + } + return false; +} + +static void signalActivateEntryInsertSpecialChar(GtkEntry *pEntry) +{ + InsertSpecialChar(pEntry); +} + +static void signalEntryPopulatePopup(GtkEntry* pEntry, GtkWidget* pMenu, gpointer) +{ + if (!GTK_IS_MENU(pMenu)) + return; + + if (!vcl::GetGetSpecialCharsFunction()) + return; + + GtkWidget *item = gtk_menu_item_new_with_mnemonic(MapToGtkAccelerator(VclResId(STR_SPECIAL_CHARACTER_MENU_ENTRY)).getStr()); + gtk_widget_show(item); + g_signal_connect_swapped(item, "activate", G_CALLBACK(signalActivateEntryInsertSpecialChar), pEntry); + gtk_menu_shell_append(GTK_MENU_SHELL(pMenu), item); +} + +#endif + +namespace { + +GtkBuilder* makeMenuToggleButtonBuilder() +{ +#if !GTK_CHECK_VERSION(4, 0, 0) + OUString aUri(AllSettings::GetUIRootDir() + "vcl/ui/menutogglebutton3.ui"); +#else + OUString aUri(AllSettings::GetUIRootDir() + "vcl/ui/menutogglebutton4.ui"); +#endif + OUString aPath; + osl::FileBase::getSystemPathFromFileURL(aUri, aPath); + return gtk_builder_new_from_file(OUStringToOString(aPath, RTL_TEXTENCODING_UTF8).getStr()); +} + +#if !GTK_CHECK_VERSION(4, 0, 0) + +GtkBuilder* makeComboBoxBuilder() +{ + OUString aUri(AllSettings::GetUIRootDir() + "vcl/ui/combobox.ui"); + OUString aPath; + osl::FileBase::getSystemPathFromFileURL(aUri, aPath); + return gtk_builder_new_from_file(OUStringToOString(aPath, RTL_TEXTENCODING_UTF8).getStr()); +} + +// 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_item_activate(const OUString& /*rIdent*/) override + { + gtk_toggle_button_set_active(m_pComboBox, false); + } +}; + +#endif + +gboolean signalTooltipQuery(GtkWidget* pWidget, gint /*x*/, gint /*y*/, + gboolean /*keyboard_mode*/, GtkTooltip *tooltip) +{ + const ImplSVHelpData& aHelpData = ImplGetSVHelpData(); + if (aHelpData.mbBalloonHelp) // extended tips + { +#if !GTK_CHECK_VERSION(4, 0, 0) + // by default use accessible description + 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; + } +#endif + + // fallback to the mechanism which needs help installed + OUString sHelpId = ::get_help_id(pWidget); + Help* pHelp = !sHelpId.isEmpty() ? Application::GetHelp() : nullptr; + if (pHelp) + { + OUString sHelpText = pHelp->GetHelpText(sHelpId, static_cast<weld::Widget*>(nullptr)); + if (!sHelpText.isEmpty()) + { + gtk_tooltip_set_text(tooltip, OUStringToOString(sHelpText, RTL_TEXTENCODING_UTF8).getStr()); + 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; +} + +#if GTK_CHECK_VERSION(4, 0, 0) + +class GtkInstanceComboBox : public GtkInstanceWidget, public vcl::ISearchableStringList, public virtual weld::ComboBox +{ +private: + GtkComboBox* m_pComboBox; +// GtkOverlay* m_pOverlay; +// GtkTreeView* m_pTreeView; +// GtkMenuButton* m_pOverlayButton; // button that the StyleDropdown uses on an active row + GtkWidget* m_pMenuWindow; + GtkTreeModel* m_pTreeModel; + GtkCellRenderer* m_pButtonTextRenderer; + GtkWidget* m_pEntry; + GtkEditable* m_pEditable; +// GtkCellView* m_pCellView; + GtkEventController* m_pKeyController; + GtkEventController* m_pEntryKeyController; + GtkEventController* m_pMenuKeyController; + GtkEventController* m_pEntryFocusController; +// std::unique_ptr<CustomRenderMenuButtonHelper> m_xCustomMenuButtonHelper; + WidgetFont m_aCustomFont; + std::optional<vcl::Font> m_xEntryFont; + std::unique_ptr<comphelper::string::NaturalStringSorter> m_xSorter; + vcl::QuickSelectionEngine m_aQuickSelectionEngine; + std::vector<std::unique_ptr<GtkTreeRowReference, GtkTreeRowReferenceDeleter>> m_aSeparatorRows; +#if 0 + OUString m_sMenuButtonRow; +#endif +// bool m_bHoverSelection; +// bool m_bMouseInOverlayButton; + bool m_bPopupActive; + bool m_bAutoComplete; + bool m_bAutoCompleteCaseSensitive; + bool m_bChangedByMenu; + bool m_bCustomRenderer; + bool m_bUserSelectEntry; + 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) + { + SolarMutexGuard aGuard; + set_active_including_mru(nPos, true); + } + 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) + { + if (m_bPopupActive) // not entered by the user + return; + + // 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(GtkComboBox*, gpointer widget) + { + GtkInstanceComboBox* pThis = static_cast<GtkInstanceComboBox*>(widget); + SolarMutexGuard aGuard; + pThis->fire_signal_changed(); + } + + void fire_signal_changed() + { + m_bUserSelectEntry = true; + m_bChangedByMenu = m_bPopupActive; + signal_changed(); + m_bChangedByMenu = false; + } + + static void signalPopupToggled(GObject*, GParamSpec*, gpointer widget) + { + GtkInstanceComboBox* pThis = static_cast<GtkInstanceComboBox*>(widget); + SolarMutexGuard aGuard; + pThis->signal_popup_toggled(); + } + +#if 0 + 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; + } +#endif + + bool toggle_button_get_active() + { + GValue value = G_VALUE_INIT; + g_value_init(&value, G_TYPE_BOOLEAN); + g_object_get_property(G_OBJECT(m_pComboBox), "popup-shown", &value); + return g_value_get_boolean(&value); + } + + void menu_toggled() + { + if (!m_bPopupActive) + { +#if 0 + 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; + } +#endif + + if (!m_bUserSelectEntry) + set_active_including_mru(m_nPrePopupCursorPos, true); + +#if 0 + // undo show_menu tooltip blocking + GtkWidget* pParent = widget_get_toplevel(m_pToggleButton); + GtkSalFrame* pFrame = pParent ? GtkSalFrame::getFromWindow(pParent) : nullptr; + if (pFrame) + pFrame->UnblockTooltip(); +#endif + } + else + { + m_nPrePopupCursorPos = get_active(); + + m_bUserSelectEntry = false; + + // if we are in mru mode always start with the cursor at the top of the menu + if (m_nMaxMRUCount) + set_active_including_mru(0, true); + } + } + + virtual void signal_popup_toggled() override + { + m_aQuickSelectionEngine.Reset(); + + bool bOldPopupActive = m_bPopupActive; + m_bPopupActive = toggle_button_get_active(); + + menu_toggled(); + + if (bOldPopupActive != m_bPopupActive) + { + ComboBox::signal_popup_toggled(); + // restore focus to the GtkEntry when the popup is gone, which + // is what the vcl case does, to ease the transition a little, + // but don't do it if the focus was moved out of togglebutton + // by something else already (e.g. font combobox in toolbar + // on a "direct pick" from the menu which moves focus into + // the main document + if (!m_bPopupActive && m_pEntry && has_child_focus()) + { + disable_notify_events(); + gtk_widget_grab_focus(m_pEntry); + enable_notify_events(); + } + } + } + +#if GTK_CHECK_VERSION(4, 0, 0) + static void signalEntryFocusIn(GtkEventControllerFocus*, gpointer widget) + { + GtkInstanceComboBox* pThis = static_cast<GtkInstanceComboBox*>(widget); + SolarMutexGuard aGuard; + pThis->signal_entry_focus_in(); + } +#else + static gboolean signalEntryFocusIn(GtkWidget*, GdkEvent*, gpointer widget) + { + GtkInstanceComboBox* pThis = static_cast<GtkInstanceComboBox*>(widget); + pThis->signal_entry_focus_in(); + return false; + } +#endif + + void signal_entry_focus_in() + { + signal_focus_in(); + } + +#if GTK_CHECK_VERSION(4, 0, 0) + static void signalEntryFocusOut(GtkEventControllerFocus*, gpointer widget) + { + GtkInstanceComboBox* pThis = static_cast<GtkInstanceComboBox*>(widget); + SolarMutexGuard aGuard; + pThis->signal_entry_focus_out(); + } +#else + static gboolean signalEntryFocusOut(GtkWidget*, GdkEvent*, gpointer widget) + { + GtkInstanceComboBox* pThis = static_cast<GtkInstanceComboBox*>(widget); + pThis->signal_entry_focus_out(); + return false; + } +#endif + + 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, std::u16string_view 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(std::u16string_view 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)); + 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(const GtkTreePath* path) + { + return ::separator_function(path, m_aSeparatorRows); + } + + 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(GtkEventControllerKey*, guint keyval, guint keycode, GdkModifierType state, gpointer widget) + { + GtkInstanceComboBox* pThis = static_cast<GtkInstanceComboBox*>(widget); + SolarMutexGuard aGuard; + return pThis->signal_key_press(CreateKeyEvent(keyval, keycode, state, 0)); + } + + // 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_pComboBox); + GtkWidget *pToplevel = 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 != pComboBox && gtk_widget_get_sensitive(pDefaultWidget)) + bDone = gtk_widget_activate(pDefaultWidget); + return bDone; + } + + static gboolean signalEntryKeyPress(GtkEventControllerKey*, guint keyval, guint keycode, GdkModifierType state, gpointer widget) + { + GtkInstanceComboBox* pThis = static_cast<GtkInstanceComboBox*>(widget); + LocalizeDecimalSeparator(keyval); + return pThis->signal_entry_key_press(CreateKeyEvent(keyval, keycode, state, 0)); + } + + bool signal_entry_key_press(const KeyEvent& rKEvt) + { + vcl::KeyCode aKeyCode = rKEvt.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_combo_box_popup(m_pComboBox); + 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 KeyEvent& rKEvt) + { +#if 0 + 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; + } +#endif + + vcl::KeyCode aKeyCode = rKEvt.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 deactivate + if (nCode == KEY_RETURN && !nKeyMod) + { + if (!m_bPopupActive) + bDone = combobox_activate(); + else + { + // treat 'return' as if the active entry was clicked on + signalChanged(m_pComboBox, this); + gtk_combo_box_popdown(m_pComboBox); + bDone = true; + } + } + else if (nCode == KEY_UP && nKeyMod == KEY_MOD2 && m_bPopupActive) + { + gtk_combo_box_popdown(m_pComboBox); + bDone = true; + } + else if (nCode == KEY_DOWN && nKeyMod == KEY_MOD2 && !m_bPopupActive) + { + gtk_combo_box_popup(m_pComboBox); + bDone = true; + } + break; + } + case KEY_ESCAPE: + { + m_aQuickSelectionEngine.Reset(); + if (m_bPopupActive) + { + gtk_combo_box_popdown(m_pComboBox); + 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(rKEvt); + break; + } + + if (!bDone) + { + if (!m_pEntry) + bDone = signal_entry_key_press(rKEvt); + else + { + // with gtk4-4.2.1 the unconsumed keystrokes don't appear to get to + // the GtkEntry for up/down to move to the next entry without this extra help + // (which means this extra indirection is probably effectively + // the same as if calling signal_entry_key_press directly here) + bDone = gtk_event_controller_key_forward(GTK_EVENT_CONTROLLER_KEY(m_pMenuKeyController), m_pEntry); + } + } + + 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; + } + + int tree_view_get_cursor() const + { + int nRet = -1; +#if 0 + 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); + } +#endif + + 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) + { + 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); + } + +#if 0 + 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 if (!g_object_get_data(G_OBJECT(event->grab_window), "g-lo-InstancePopup")) // another LibreOffice popover took a grab + { + //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(widget_get_surface(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; + } + } +#endif + + static void signalRowActivated(GtkTreeView*, GtkTreePath*, GtkTreeViewColumn*, gpointer widget) + { + GtkInstanceComboBox* pThis = static_cast<GtkInstanceComboBox*>(widget); + pThis->handle_row_activated(); + } + + void handle_row_activated() + { + m_bUserSelectEntry = true; + m_bChangedByMenu = true; + disable_notify_events(); + int nActive = get_active(); + if (m_pEditable) + gtk_editable_set_text(m_pEditable, OUStringToOString(get_text(nActive), RTL_TEXTENCODING_UTF8).getStr()); +#if 0 + else + tree_view_set_cursor(nActive); +#endif + 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_combo_box_set_row_separator_func(m_pComboBox, 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 gtk_combo_box_get_active(m_pComboBox); + } + + void set_active_including_mru(int pos, bool bInteractive) + { + disable_notify_events(); + + gtk_combo_box_set_active(m_pComboBox, pos); + + m_bChangedByMenu = false; + enable_notify_events(); + + if (bInteractive && !m_bPopupActive) + signal_changed(); + } + + int find_text_including_mru(std::u16string_view rStr, bool bSearchMRU) const + { + return find(rStr, m_nTextCol, bSearchMRU); + } + + int find_id_including_mru(std::u16string_view 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, std::u16string_view 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_combo_box_get_row_separator_func(m_pComboBox)) + gtk_combo_box_set_row_separator_func(m_pComboBox, separatorFunction, this, nullptr); + insert_row(GTK_LIST_STORE(m_pTreeModel), iter, pos, &rId, u"", 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, std::u16string_view rText, const OUString* pId, const OUString* pIconName, const VirtualDevice* pImageSurface) + { + disable_notify_events(); + GtkTreeIter iter; + insert_row(GTK_LIST_STORE(m_pTreeModel), iter, pos, pId, rText, pIconName, pImageSurface); + enable_notify_events(); + } + +#if 0 + 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) + return; + + 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 + } +#endif + + int include_mru(int pos) + { + if (m_nMRUCount && pos != -1) + pos += (m_nMRUCount + 1); + return pos; + } + +public: + GtkInstanceComboBox(GtkComboBox* pComboBox, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : GtkInstanceWidget(GTK_WIDGET(pComboBox), pBuilder, bTakeOwnership) + , 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(nullptr) + , m_pTreeModel(gtk_combo_box_get_model(pComboBox)) + , m_pButtonTextRenderer(nullptr) +// , m_pToggleButton(GTK_WIDGET(gtk_builder_get_object(pComboBuilder, "button"))) + , m_pEntry(GTK_IS_ENTRY(gtk_combo_box_get_child(pComboBox)) ? gtk_combo_box_get_child(pComboBox) : nullptr) + , m_pEditable(GTK_EDITABLE(m_pEntry)) + , m_aCustomFont(m_pWidget) +// , 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_bUserSelectEntry(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_pComboBox, "changed", G_CALLBACK(signalChanged), this)) + , m_nPopupShownSignalId(g_signal_connect(m_pComboBox, "notify::popup-shown", G_CALLBACK(signalPopupToggled), this)) + , m_nAutoCompleteIdleId(0) +// , m_nNonCustomLineHeight(-1) + , m_nPrePopupCursorPos(-1) + , m_nMRUCount(0) + , m_nMaxMRUCount(0) + { + for (GtkWidget* pChild = gtk_widget_get_first_child(GTK_WIDGET(m_pComboBox)); + pChild; pChild = gtk_widget_get_next_sibling(pChild)) + { + if (GTK_IS_POPOVER(pChild)) + { + m_pMenuWindow = pChild; + break; + } + } + SAL_WARN_IF(!m_pMenuWindow, "vcl.gtk", "GtkInstanceComboBox: couldn't find popup menu"); + + bool bHasEntry = gtk_combo_box_get_has_entry(m_pComboBox); + bool bPixbufUsedSurface = gtk_tree_model_get_n_columns(m_pTreeModel) == 4; + + bool bFindButtonTextRenderer = !bHasEntry; + GtkCellLayout* pCellLayout = GTK_CELL_LAYOUT(m_pComboBox); + GList* cells = gtk_cell_layout_get_cells(pCellLayout); + guint i = g_list_length(cells) - 1;; + // reorder the cell renderers + for (GList* pRenderer = g_list_first(cells); pRenderer; pRenderer = g_list_next(pRenderer)) + { + GtkCellRenderer* pCellRenderer = GTK_CELL_RENDERER(pRenderer->data); + gtk_cell_layout_reorder(pCellLayout, pCellRenderer, i--); + if (bFindButtonTextRenderer) + { + m_pButtonTextRenderer = pCellRenderer; + bFindButtonTextRenderer = false; + } + } + + // Seeing as GtkCellRendererPixbuf no longer takes a surface, then insert our own replacement + // to render that instead here + if (bPixbufUsedSurface) + { + GtkCellRenderer* pSurfaceRenderer = surface_cell_renderer_new(); + gtk_cell_layout_pack_start(pCellLayout, pSurfaceRenderer, false); + gtk_cell_layout_reorder(pCellLayout, pSurfaceRenderer, 0); + gtk_cell_layout_set_attributes(pCellLayout, pSurfaceRenderer, "surface", 3, nullptr); + } + + if (bHasEntry) + { + m_bAutoComplete = true; + m_nEntryInsertTextSignalId = g_signal_connect(m_pEditable, "insert-text", G_CALLBACK(signalEntryInsertText), this); + m_nEntryActivateSignalId = g_signal_connect(m_pEntry, "activate", G_CALLBACK(signalEntryActivate), this); + m_pEntryFocusController = GTK_EVENT_CONTROLLER(gtk_event_controller_focus_new()); + m_nEntryFocusInSignalId = g_signal_connect(m_pEntryFocusController, "enter", G_CALLBACK(signalEntryFocusIn), this); + m_nEntryFocusOutSignalId = g_signal_connect(m_pEntryFocusController, "leave", G_CALLBACK(signalEntryFocusOut), this); + gtk_widget_add_controller(m_pEntry, m_pEntryFocusController); + m_pEntryKeyController = GTK_EVENT_CONTROLLER(gtk_event_controller_key_new()); + m_nEntryKeyPressEventSignalId = g_signal_connect(m_pEntryKeyController, "key-pressed", G_CALLBACK(signalEntryKeyPress), this); + gtk_widget_add_controller(m_pEntry, m_pEntryKeyController); + m_nKeyPressEventSignalId = 0; + m_pKeyController = nullptr; + } + else + { + m_nEntryInsertTextSignalId = 0; + m_nEntryActivateSignalId = 0; + m_pEntryFocusController = nullptr; + m_nEntryFocusInSignalId = 0; + m_nEntryFocusOutSignalId = 0; + m_pEntryKeyController = nullptr; + m_nEntryKeyPressEventSignalId = 0; + m_pKeyController = GTK_EVENT_CONTROLLER(gtk_event_controller_key_new()); + m_nKeyPressEventSignalId = g_signal_connect(m_pKeyController, "key-pressed", G_CALLBACK(signalKeyPress), this); + gtk_widget_add_controller(GTK_WIDGET(m_pComboBox), m_pKeyController); + } + +// 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. + if (m_pMenuWindow) + { + m_pMenuKeyController = GTK_EVENT_CONTROLLER(gtk_event_controller_key_new()); + g_signal_connect(m_pMenuKeyController, "key-pressed", G_CALLBACK(signalKeyPress), this); + gtk_widget_add_controller(m_pMenuWindow, m_pMenuKeyController); + } + else + m_pMenuKeyController = nullptr; +#if 0 + 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); +#endif + } + + 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 + { + set_active_including_mru(include_mru(pos), false); + } + + virtual OUString get_active_text() const override + { + if (m_pEditable) + { + const gchar* pText = gtk_editable_get_text(m_pEditable); + 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(); + + int nInsertionPoint; + if (!bKeepExisting) + { + clear(); + nInsertionPoint = 0; + } + else + nInsertionPoint = get_count(); + + GtkTreeIter iter; + // tdf#125241 inserting backwards is faster + for (auto aI = rItems.rbegin(); aI != rItems.rend(); ++aI) + { + const auto& rItem = *aI; + insert_row(GTK_LIST_STORE(m_pTreeModel), iter, nInsertionPoint, 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 + { + insert_including_mru(include_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_pEditable); + disable_notify_events(); + gtk_editable_set_text(m_pEditable, OUStringToOString(rText, RTL_TEXTENCODING_UTF8).getStr()); + enable_notify_events(); + } + + virtual void set_entry_width_chars(int nChars) override + { + assert(m_pEditable); + disable_notify_events(); + gtk_editable_set_width_chars(m_pEditable, nChars); + gtk_editable_set_max_width_chars(m_pEditable, 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_pEditable); + disable_notify_events(); + gtk_editable_select_region(m_pEditable, nStartPos, nEndPos); + enable_notify_events(); + } + + virtual bool get_entry_selection_bounds(int& rStartPos, int &rEndPos) override + { + assert(m_pEditable); + return gtk_editable_get_selection_bounds(m_pEditable, &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_pEditable); + gtk_editable_set_editable(m_pEditable, bEditable); + } + + virtual void cut_entry_clipboard() override + { + assert(m_pEntry); + gtk_widget_activate_action(m_pEntry, "cut.clipboard", nullptr); + } + + virtual void copy_entry_clipboard() override + { + assert(m_pEntry); + gtk_widget_activate_action(m_pEntry, "copy.clipboard", nullptr); + } + + virtual void paste_entry_clipboard() override + { + assert(m_pEntry); + gtk_widget_activate_action(m_pEntry, "paste.clipboard", nullptr); + } + + virtual void set_font(const vcl::Font& rFont) override + { + m_aCustomFont.use_custom_font(&rFont, u"combobox"); + } + + virtual vcl::Font get_font() override + { + if (const vcl::Font* pFont = m_aCustomFont.get_custom_font()) + return *pFont; + return GtkInstanceWidget::get_font(); + } + + virtual void set_entry_font(const vcl::Font& rFont) override + { + m_xEntryFont = rFont; + assert(m_pEntry); + PangoAttrList* pOrigList = gtk_entry_get_attributes(GTK_ENTRY(m_pEntry)); + PangoAttrList* pAttrList = pOrigList ? pango_attr_list_copy(pOrigList) : pango_attr_list_new(); + update_attr_list(pAttrList, rFont); + gtk_entry_set_attributes(GTK_ENTRY(m_pEntry), pAttrList); + pango_attr_list_unref(pAttrList); + } + + virtual vcl::Font get_entry_font() override + { + if (m_xEntryFont) + return *m_xEntryFont; + 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_pEditable) + { + g_signal_handler_block(m_pEditable, m_nEntryInsertTextSignalId); + g_signal_handler_block(m_pEntry, m_nEntryActivateSignalId); + g_signal_handler_block(m_pEntryFocusController, m_nEntryFocusInSignalId); + g_signal_handler_block(m_pEntryFocusController, m_nEntryFocusOutSignalId); + g_signal_handler_block(m_pEntryKeyController, m_nEntryKeyPressEventSignalId); + } + else + g_signal_handler_block(m_pKeyController, 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_pComboBox, m_nPopupShownSignalId); + g_signal_handler_block(m_pComboBox, m_nChangedSignalId); + GtkInstanceWidget::disable_notify_events(); + } + + virtual void enable_notify_events() override + { + GtkInstanceWidget::enable_notify_events(); + g_signal_handler_unblock(m_pComboBox, m_nChangedSignalId); + g_signal_handler_unblock(m_pComboBox, 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_pEditable) + { + g_signal_handler_unblock(m_pEntry, m_nEntryActivateSignalId); + g_signal_handler_unblock(m_pEntryFocusController, m_nEntryFocusInSignalId); + g_signal_handler_unblock(m_pEntryFocusController, m_nEntryFocusOutSignalId); + g_signal_handler_unblock(m_pEntryKeyController, m_nEntryKeyPressEventSignalId); + g_signal_handler_unblock(m_pEditable, m_nEntryInsertTextSignalId); + } + else + g_signal_handler_unblock(m_pKeyController, m_nKeyPressEventSignalId); + } + + virtual void freeze() override + { + disable_notify_events(); + bool bIsFirstFreeze = IsFirstFreeze(); + GtkInstanceWidget::freeze(); + if (bIsFirstFreeze) + { + g_object_ref(m_pTreeModel); +// gtk_tree_view_set_model(m_pTreeView, nullptr); + g_object_freeze_notify(G_OBJECT(m_pTreeModel)); + 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 (IsLastThaw()) + { + if (m_xSorter) + { + GtkTreeSortable* pSortable = GTK_TREE_SORTABLE(m_pTreeModel); + gtk_tree_sortable_set_sort_column_id(pSortable, m_nTextCol, GTK_SORT_ASCENDING); + } + g_object_thaw_notify(G_OBJECT(m_pTreeModel)); +// gtk_tree_view_set_model(m_pTreeView, m_pTreeModel); + g_object_unref(m_pTreeModel); + } + GtkInstanceWidget::thaw(); + 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_after(m_pToggleButton, "focus-in-event", G_CALLBACK(signalFocusIn), this); + GtkInstanceWidget::connect_focus_in(rLink); + } + + virtual void connect_focus_out(const Link<Widget&, void>& rLink) override + { +// if (!m_nToggleFocusOutSignalId) +// m_nToggleFocusOutSignalId = g_signal_connect_after(m_pToggleButton, "focus-out-event", G_CALLBACK(signalFocusOut), this); + GtkInstanceWidget::connect_focus_out(rLink); + } + + virtual void grab_focus() override + { + if (has_focus()) + return; + if (m_pEntry) + gtk_widget_grab_focus(m_pEntry); + else + { +// gtk_widget_grab_focus(m_pToggleButton); + gtk_widget_grab_focus(GTK_WIDGET(m_pComboBox)); + } + } + + 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 0 + 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; + } +#endif + + 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; +#if 0 + 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_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; +#endif + } + + 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 OUString& rIdent, weld::Menu* pMenu) override + { +#if 0 + 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 = rIdent; +#else + (void)rIdent; (void)pMenu; +#endif + } + + 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 + { +#if 0 + 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; +#else + return 0; +#endif + } + + virtual ~GtkInstanceComboBox() override + { +// m_xCustomMenuButtonHelper.reset(); + do_clear(); + if (m_nAutoCompleteIdleId) + g_source_remove(m_nAutoCompleteIdleId); + if (m_pEditable) + { + g_signal_handler_disconnect(m_pEditable, m_nEntryInsertTextSignalId); + g_signal_handler_disconnect(m_pEntry, m_nEntryActivateSignalId); + g_signal_handler_disconnect(m_pEntryFocusController, m_nEntryFocusInSignalId); + g_signal_handler_disconnect(m_pEntryFocusController, m_nEntryFocusOutSignalId); + g_signal_handler_disconnect(m_pEntryKeyController, m_nEntryKeyPressEventSignalId); + } + else + g_signal_handler_disconnect(m_pKeyController, 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_pComboBox, m_nPopupShownSignalId); + g_signal_handler_disconnect(m_pComboBox, m_nChangedSignalId); + +// gtk_tree_view_set_model(m_pTreeView, nullptr); + + } +}; + +#else + +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; + WidgetFont m_aCustomFont; + std::unique_ptr<CustomRenderMenuButtonHelper> m_xCustomMenuButtonHelper; + std::optional<vcl::Font> m_xEntryFont; + 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; + gulong m_nEntryPopulatePopupMenuSignalId; + 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) + { + SolarMutexGuard aGuard; + set_active_including_mru(nPos, true); + } + 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 menu_toggled() + { + 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; + } + + bool bHadFocus = gtk_window_has_toplevel_focus(m_pMenuWindow); + + do_ungrab(GTK_WIDGET(m_pMenuWindow)); + + gtk_widget_hide(GTK_WIDGET(m_pMenuWindow)); + + GdkSurface* pSurface = widget_get_surface(GTK_WIDGET(m_pMenuWindow)); + g_object_set_data(G_OBJECT(pSurface), "g-lo-InstancePopup", GINT_TO_POINTER(false)); + + // 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 = widget_get_toplevel(m_pToggleButton); + GtkSalFrame* pFrame = pParent ? GtkSalFrame::getFromWindow(pParent) : nullptr; + if (pFrame) + pFrame->UnblockTooltip(); + + if (bHadFocus) + { + GdkSurface* pParentSurface = pParent ? widget_get_surface(pParent) : nullptr; + void* pParentIsPopover = pParentSurface ? g_object_get_data(G_OBJECT(pParentSurface), "g-lo-InstancePopup") : nullptr; + if (pParentIsPopover) + do_grab(m_pToggleButton); + gtk_widget_grab_focus(m_pToggleButton); + } + } + 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); + + GdkRectangle aAnchor {0, 0, gtk_widget_get_allocated_width(pComboBox), gtk_widget_get_allocated_height(pComboBox) }; + show_menu(pComboBox, m_pMenuWindow, aAnchor, weld::Placement::Under, true); + GdkSurface* pSurface = widget_get_surface(GTK_WIDGET(m_pMenuWindow)); + g_object_set_data(G_OBJECT(pSurface), "g-lo-InstancePopup", GINT_TO_POINTER(true)); + } + } + + virtual void signal_popup_toggled() override + { + m_aQuickSelectionEngine.Reset(); + + menu_toggled(); + + bool bIsShown = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(m_pToggleButton)); + if (m_bPopupActive == bIsShown) + return; + + m_bPopupActive = bIsShown; + ComboBox::signal_popup_toggled(); + if (!m_bPopupActive && m_pEntry) + { + disable_notify_events(); + //restore focus to the GtkEntry when the popup is gone, which + //is what the vcl case does, to ease the transition a little + gtk_widget_grab_focus(m_pEntry); + enable_notify_events(); + } + } + + 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, std::u16string_view 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(std::u16string_view 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)); + 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(const GtkTreePath* path) + { + return ::separator_function(path, m_aSeparatorRows); + } + + 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 = 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(GtkEntry* pEntry, GdkEventKey* pEvent, gpointer widget) + { + GtkInstanceComboBox* pThis = static_cast<GtkInstanceComboBox*>(widget); + LocalizeDecimalSeparator(pEvent->keyval); + if (signalEntryInsertSpecialCharKeyPress(pEntry, pEvent, nullptr)) + return true; + 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 || !m_nMRUCount ? 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 || !m_nMaxMRUCount ? 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 deactivate + 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) + { + GtkTreePath* path; + if (pos == -1) + { + path = gtk_tree_path_new_from_indices(G_MAXINT, -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 + { + 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); + if (m_pCellView) + gtk_cell_view_set_displayed_row(m_pCellView, path); + } + gtk_tree_view_set_cursor(m_pTreeView, path, nullptr, false); + 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 if (!g_object_get_data(G_OBJECT(event->grab_window), "g-lo-InstancePopup")) // another LibreOffice popover took a grab + { + //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*, GdkEventButton* pEvent, gpointer widget) + { + GtkInstanceComboBox* pThis = static_cast<GtkInstanceComboBox*>(widget); + return pThis->button_press(pEvent); + } + + bool button_press(GdkEventButton* pEvent) + { + //we want to pop down if the button was pressed outside our popup + if (button_event_is_outside(GTK_WIDGET(m_pMenuWindow), pEvent)) + 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) + { + assert(gtk_tree_view_get_model(m_pTreeView) && "don't set_active when frozen, set_active after thaw. Note selection doesn't survive a freeze"); + + 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(std::u16string_view rStr, bool bSearchMRU) const + { + return find(rStr, m_nTextCol, bSearchMRU); + } + + int find_id_including_mru(std::u16string_view 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, std::u16string_view 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, u"", 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, std::u16string_view rText, const OUString* pId, const OUString* pIconName, const 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) + return; + + 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; + } + + static gboolean signalComboTooltipQuery(GtkWidget* /*pWidget*/, gint x, gint y, + gboolean keyboard_mode, GtkTooltip *tooltip, + gpointer widget) + { + GtkInstanceComboBox* pThis = static_cast<GtkInstanceComboBox*>(widget); + return signalTooltipQuery(GTK_WIDGET(pThis->m_pComboBox), x, y, keyboard_mode, tooltip); + } + + int include_mru(int pos) + { + if (m_nMRUCount && pos != -1) + pos += (m_nMRUCount + 1); + return pos; + } + +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_aCustomFont(m_pWidget) + , 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); + + if (gtk_style_context_has_class(gtk_widget_get_style_context(GTK_WIDGET(m_pComboBox)), "small-button")) + gtk_style_context_add_class(gtk_widget_get_style_context(GTK_WIDGET(getContainer())), "small-button"); + + if (gtk_widget_get_has_tooltip(GTK_WIDGET(m_pComboBox))) + { + gtk_widget_set_has_tooltip(GTK_WIDGET(getContainer()), true); + g_signal_connect(getContainer(), "query-tooltip", G_CALLBACK(signalComboTooltipQuery), this); + } + + 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_nEntryPopulatePopupMenuSignalId = g_signal_connect(m_pEntry, "populate-popup", G_CALLBACK(signalEntryPopulatePopup), nullptr); + 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_nEntryPopulatePopupMenuSignalId = 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 + { + set_active_including_mru(include_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(); + + int nInsertionPoint; + if (!bKeepExisting) + { + clear(); + nInsertionPoint = 0; + } + else + nInsertionPoint = get_count(); + + GtkTreeIter iter; + // tdf#125241 inserting backwards is faster + for (auto aI = rItems.rbegin(); aI != rItems.rend(); ++aI) + { + const auto& rItem = *aI; + insert_row(GTK_LIST_STORE(m_pTreeModel), iter, nInsertionPoint, 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 + { + insert_including_mru(include_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_font(const vcl::Font& rFont) override + { + m_aCustomFont.use_custom_font(&rFont, u"box#combobox"); + } + + virtual vcl::Font get_font() override + { + if (const vcl::Font* pFont = m_aCustomFont.get_custom_font()) + return *pFont; + return GtkInstanceWidget::get_font(); + } + + virtual void set_entry_font(const vcl::Font& rFont) override + { + m_xEntryFont = rFont; + assert(m_pEntry); + PangoAttrList* pOrigList = gtk_entry_get_attributes(GTK_ENTRY(m_pEntry)); + PangoAttrList* pAttrList = pOrigList ? pango_attr_list_copy(pOrigList) : pango_attr_list_new(); + update_attr_list(pAttrList, rFont); + gtk_entry_set_attributes(GTK_ENTRY(m_pEntry), pAttrList); + pango_attr_list_unref(pAttrList); + } + + virtual vcl::Font get_entry_font() override + { + if (m_xEntryFont) + return *m_xEntryFont; + 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(); + bool bIsFirstFreeze = IsFirstFreeze(); + GtkInstanceContainer::freeze(); + if (bIsFirstFreeze) + { + g_object_ref(m_pTreeModel); + gtk_tree_view_set_model(m_pTreeView, nullptr); + g_object_freeze_notify(G_OBJECT(m_pTreeModel)); + 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 (IsLastThaw()) + { + if (m_xSorter) + { + GtkTreeSortable* pSortable = GTK_TREE_SORTABLE(m_pTreeModel); + gtk_tree_sortable_set_sort_column_id(pSortable, m_nTextCol, GTK_SORT_ASCENDING); + } + g_object_thaw_notify(G_OBJECT(m_pTreeModel)); + gtk_tree_view_set_model(m_pTreeView, m_pTreeModel); + g_object_unref(m_pTreeModel); + } + GtkInstanceContainer::thaw(); + 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_after(m_pToggleButton, "focus-in-event", G_CALLBACK(signalFocusIn), this); + GtkInstanceContainer::connect_focus_in(rLink); + } + + virtual void connect_focus_out(const Link<Widget&, void>& rLink) override + { + if (!m_nToggleFocusOutSignalId) + m_nToggleFocusOutSignalId = g_signal_connect_after(m_pToggleButton, "focus-out-event", G_CALLBACK(signalFocusOut), this); + GtkInstanceContainer::connect_focus_out(rLink); + } + + virtual void grab_focus() override + { + if (has_focus()) + return; + if (m_pEntry) + gtk_widget_grab_focus(m_pEntry); + else + gtk_widget_grab_focus(m_pToggleButton); + } + + 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_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 OUString& 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 = 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); + g_signal_handler_disconnect(m_pEntry, m_nEntryPopulatePopupMenuSignalId); + } + 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 + { + DisconnectMouseEvents(); + + 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); + } +}; + +#endif + +} + +void custom_cell_renderer_ensure_device(CustomCellRenderer *cellsurface, gpointer user_data) +{ + if (!cellsurface->device) + { + cellsurface->device = VclPtr<VirtualDevice>::Create(); + cellsurface->device->SetBackground(COL_TRANSPARENT); + GtkInstanceWidget* pWidget = static_cast<GtkInstanceWidget*>(user_data); + // expand the point size of the desired font to the equivalent pixel size + weld::SetPointFont(*cellsurface->device, pWidget->get_font()); + } +} + +Size custom_cell_renderer_get_size(VirtualDevice& rDevice, const OUString& rCellId, gpointer user_data) +{ + GtkInstanceWidget* pWidget = static_cast<GtkInstanceWidget*>(user_data); + if (GtkInstanceTreeView* pTreeView = dynamic_cast<GtkInstanceTreeView*>(pWidget)) + return pTreeView->call_signal_custom_get_size(rDevice, rCellId); + else if (GtkInstanceComboBox* pComboBox = dynamic_cast<GtkInstanceComboBox*>(pWidget)) + return pComboBox->call_signal_custom_get_size(rDevice); + return Size(); +} + +void custom_cell_renderer_render(VirtualDevice& rDevice, const tools::Rectangle& rRect, bool bSelected, const OUString& rCellId, gpointer user_data) +{ + GtkInstanceWidget* pWidget = static_cast<GtkInstanceWidget*>(user_data); + if (GtkInstanceTreeView* pTreeView = dynamic_cast<GtkInstanceTreeView*>(pWidget)) + pTreeView->call_signal_custom_render(rDevice, rRect, bSelected, rCellId); + else if (GtkInstanceComboBox* pComboBox = dynamic_cast<GtkInstanceComboBox*>(pWidget)) + pComboBox->call_signal_custom_render(rDevice, rRect, bSelected, rCellId); +} + +namespace { + +class GtkInstanceEntryTreeView : public GtkInstanceContainer, public virtual weld::EntryTreeView +{ +private: + GtkInstanceEntry* m_pEntry; + GtkInstanceTreeView* m_pTreeView; +#if !GTK_CHECK_VERSION(4, 0, 0) + gulong m_nKeyPressSignalId; +#endif + gulong m_nEntryInsertTextSignalId; + guint m_nAutoCompleteIdleId; + bool m_bAutoCompleteCaseSensitive; + bool m_bTreeChange; + +#if !GTK_CHECK_VERSION(4, 0, 0) + 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); + } +#endif + + 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, nStart, true); + if (nPos == -1 && nStart != 0) + { + // Try match case insensitive, but from start + nPos = m_pTreeView->starts_with(aStartText, 0, true); + } + + if (!m_bAutoCompleteCaseSensitive) + { + // Try match case insensitive from current position + nPos = m_pTreeView->starts_with(aStartText, nStart, false); + if (nPos == -1 && nStart != 0) + { + // Try match case insensitive, but from start + nPos = m_pTreeView->starts_with(aStartText, 0, false); + } + } + + if (nPos == -1) + { + // Try match case sensitive from current position + nPos = m_pTreeView->starts_with(aStartText, nStart, true); + if (nPos == -1 && nStart != 0) + { + // Try match case sensitive, but from start + nPos = m_pTreeView->starts_with(aStartText, 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: +#if GTK_CHECK_VERSION(4, 0, 0) + GtkInstanceEntryTreeView(GtkWidget* pContainer, GtkInstanceBuilder* pBuilder, bool bTakeOwnership, + std::unique_ptr<weld::Entry> xEntry, std::unique_ptr<weld::TreeView> xTreeView) +#else + GtkInstanceEntryTreeView(GtkContainer* pContainer, GtkInstanceBuilder* pBuilder, bool bTakeOwnership, + std::unique_ptr<weld::Entry> xEntry, std::unique_ptr<weld::TreeView> xTreeView) +#endif + : 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(); +#if !GTK_CHECK_VERSION(4, 0, 0) + m_nKeyPressSignalId = g_signal_connect(pWidget, "key-press-event", G_CALLBACK(signalKeyPress), this); +#endif + 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_font(const vcl::Font&) override + { + assert(false && "not implemented"); + } + + 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); +#if !GTK_CHECK_VERSION(4, 0, 0) + g_signal_handler_block(pWidget, m_nKeyPressSignalId); +#endif + m_pTreeView->disable_notify_events(); + GtkInstanceContainer::disable_notify_events(); + } + + virtual void enable_notify_events() override + { + GtkWidget* pWidget = m_pEntry->getWidget(); +#if !GTK_CHECK_VERSION(4, 0, 0) + g_signal_handler_unblock(pWidget, m_nKeyPressSignalId); +#endif + g_signal_handler_unblock(pWidget, m_nEntryInsertTextSignalId); + m_pTreeView->enable_notify_events(); + GtkInstanceContainer::enable_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 OUString&, 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(); +#if !GTK_CHECK_VERSION(4, 0, 0) + g_signal_handler_disconnect(pWidget, m_nKeyPressSignalId); +#endif + g_signal_handler_disconnect(pWidget, m_nEntryInsertTextSignalId); + } +}; + +} + +namespace { + +class GtkInstanceExpander : public GtkInstanceWidget, public virtual weld::Expander +{ +private: + GtkExpander* m_pExpander; + gulong m_nSignalId; +#if !GTK_CHECK_VERSION(4, 0, 0) + gulong m_nButtonPressEventSignalId; + gulong m_nMappedSignalId; +#endif + + static void signalExpanded(GtkExpander* /*pExpander*/, GParamSpec*, gpointer widget) + { + GtkInstanceExpander* pThis = static_cast<GtkInstanceExpander*>(widget); + SolarMutexGuard aGuard; + pThis->signal_expanded(); + } + +#if !GTK_CHECK_VERSION(4, 0, 0) + static gboolean signalButton(GtkWidget*, GdkEventButton*, gpointer) + { + // don't let button press get to parent window, for the case of the + // an expander in a sidebar where otherwise single click to expand + // doesn't work + return true; + } + + /* tdf#141186 if the expander is initially collapsed then when mapped all its + children are mapped too. If they are mapped then the mnemonics of the + children are taken into account on shortcuts and non-visible children in a + collapsed expander can be triggered which is confusing. + + If the expander is expanded and collapsed the child is unmapped and the + problem doesn't occur. + + So to avoid the problem of an initially collapsed expander, listen to + the map event and if the expander is mapped but collapsed then unmap the + child of the expander. + + This problem was seen in gtk3-3.24.33 and not with gtk4-4.6.4 so a gtk3 + fix only needed. + */ + static void signalMap(GtkWidget*, gpointer widget) + { + GtkInstanceExpander* pThis = static_cast<GtkInstanceExpander*>(widget); + if (!gtk_expander_get_expanded(pThis->m_pExpander)) + { + if (GtkWidget* pChild = gtk_bin_get_child(GTK_BIN(pThis->m_pExpander))) + gtk_widget_unmap(pChild); + } + } +#endif + +public: + GtkInstanceExpander(GtkExpander* pExpander, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : GtkInstanceWidget(GTK_WIDGET(pExpander), pBuilder, bTakeOwnership) + , m_pExpander(pExpander) + , m_nSignalId(g_signal_connect(m_pExpander, "notify::expanded", G_CALLBACK(signalExpanded), this)) +#if !GTK_CHECK_VERSION(4, 0, 0) + , m_nButtonPressEventSignalId(g_signal_connect_after(m_pExpander, "button-press-event", G_CALLBACK(signalButton), this)) + , m_nMappedSignalId(g_signal_connect_after(m_pExpander, "map", G_CALLBACK(signalMap), this)) +#endif + { + } + + virtual void set_label(const OUString& rText) override + { + ::set_label(GTK_LABEL(gtk_expander_get_label_widget(m_pExpander)), rText); + } + + virtual OUString get_label() const override + { + return ::get_label(GTK_LABEL(gtk_expander_get_label_widget(m_pExpander))); + } + + 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 + { +#if !GTK_CHECK_VERSION(4, 0, 0) + g_signal_handler_disconnect(m_pExpander, m_nMappedSignalId); + g_signal_handler_disconnect(m_pExpander, m_nButtonPressEventSignalId); +#endif + g_signal_handler_disconnect(m_pExpander, m_nSignalId); + } +}; + +} + +namespace { + +class GtkInstancePopover : public GtkInstanceContainer, public virtual weld::Popover +{ +private: +#if !GTK_CHECK_VERSION(4, 0, 0) + //popover cannot escape dialog under X so we might need to stick up own window instead + GtkWindow* m_pMenuHack; + bool m_bMenuPoppedUp; + bool m_nButtonPressSeen; +#endif + GtkPopover* m_pPopover; + gulong m_nSignalId; + ImplSVEvent* m_pClosedEvent; + + static void signalClosed(GtkPopover*, gpointer widget) + { + GtkInstancePopover* pThis = static_cast<GtkInstancePopover*>(widget); + // call signal-closed async so the closed callback isn't called + // while the GtkPopover handler is still in-execution + pThis->launch_signal_closed(); + } + + DECL_LINK(async_signal_closed, void*, void); + + void launch_signal_closed() + { + if (m_pClosedEvent) + Application::RemoveUserEvent(m_pClosedEvent); + m_pClosedEvent = Application::PostUserEvent(LINK(this, GtkInstancePopover, async_signal_closed)); + } + +#if !GTK_CHECK_VERSION(4, 0, 0) + static gboolean keyPress(GtkWidget*, GdkEventKey* pEvent, gpointer widget) + { + GtkInstancePopover* pThis = static_cast<GtkInstancePopover*>(widget); + return pThis->key_press(pEvent); + } + + bool key_press(const GdkEventKey* pEvent) + { + if (pEvent->keyval == GDK_KEY_Escape) + { + popdown(); + return true; + } + return false; + } + + static gboolean signalButtonPress(GtkWidget* /*pWidget*/, GdkEventButton* /*pEvent*/, gpointer widget) + { + GtkInstancePopover* pThis = static_cast<GtkInstancePopover*>(widget); + pThis->m_nButtonPressSeen = true; + return false; + } + + static gboolean signalButtonRelease(GtkWidget* /*pWidget*/, GdkEventButton* pEvent, gpointer widget) + { + GtkInstancePopover* pThis = static_cast<GtkInstancePopover*>(widget); + if (pThis->m_nButtonPressSeen && button_event_is_outside(GTK_WIDGET(pThis->m_pMenuHack), pEvent)) + pThis->popdown(); + return false; + } + + bool forward_event_if_popup_under_mouse(GdkEvent* pEvent) + { + GtkWidget* pEventWidget = gtk_get_event_widget(pEvent); + GtkWidget* pTopLevel = widget_get_toplevel(pEventWidget); + + if (pTopLevel == GTK_WIDGET(m_pMenuHack)) + return false; + + GdkSurface* pSurface = widget_get_surface(pTopLevel); + void* pMouseEnteredAnotherPopup = g_object_get_data(G_OBJECT(pSurface), "g-lo-InstancePopup"); + if (!pMouseEnteredAnotherPopup) + return false; + + return gtk_widget_event(pEventWidget, pEvent); + } + + static gboolean signalButtonCrossing(GtkWidget*, GdkEvent* pEvent, gpointer widget) + { + GtkInstancePopover* pThis = static_cast<GtkInstancePopover*>(widget); + return pThis->forward_event_if_popup_under_mouse(pEvent); + } + + static gboolean signalMotion(GtkWidget*, GdkEvent* pEvent, gpointer widget) + { + GtkInstancePopover* pThis = static_cast<GtkInstancePopover*>(widget); + return pThis->forward_event_if_popup_under_mouse(pEvent); + } + + static void signalGrabBroken(GtkWidget*, GdkEventGrabBroken *pEvent, gpointer widget) + { + GtkInstancePopover* pThis = static_cast<GtkInstancePopover*>(widget); + pThis->grab_broken(pEvent); + } + + void grab_broken(const GdkEventGrabBroken *event) + { + if (event->grab_window == nullptr) + { + popdown(); + } + else if (!g_object_get_data(G_OBJECT(event->grab_window), "g-lo-InstancePopup")) // another LibreOffice popover took a grab + { + //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)); + } + } + +#endif + +public: + GtkInstancePopover(GtkPopover* pPopover, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) +#if !GTK_CHECK_VERSION(4, 0, 0) + : GtkInstanceContainer(GTK_CONTAINER(pPopover), pBuilder, bTakeOwnership) + , m_pMenuHack(nullptr) + , m_bMenuPoppedUp(false) + , m_nButtonPressSeen(false) +#else + : GtkInstanceContainer(GTK_WIDGET(pPopover), pBuilder, bTakeOwnership) +#endif + , m_pPopover(pPopover) + , m_nSignalId(g_signal_connect(m_pPopover, "closed", G_CALLBACK(signalClosed), this)) + , m_pClosedEvent(nullptr) + { +#if !GTK_CHECK_VERSION(4, 0, 0) + //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(GTK_WIDGET(m_pPopover)); + 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_resizable(m_pMenuHack, false); + g_signal_connect(m_pMenuHack, "key-press-event", G_CALLBACK(keyPress), this); + g_signal_connect(m_pMenuHack, "grab-broken-event", G_CALLBACK(signalGrabBroken), this); + g_signal_connect(m_pMenuHack, "button-press-event", G_CALLBACK(signalButtonPress), this); + g_signal_connect(m_pMenuHack, "button-release-event", G_CALLBACK(signalButtonRelease), this); + // to emulate a modeless popover we forward the leave/enter/motion events to the widgets + // they would have gone to a if we were really modeless + if (!gtk_popover_get_modal(m_pPopover)) + { + g_signal_connect(m_pMenuHack, "leave-notify-event", G_CALLBACK(signalButtonCrossing), this); + g_signal_connect(m_pMenuHack, "enter-notify-event", G_CALLBACK(signalButtonCrossing), this); + g_signal_connect(m_pMenuHack, "motion-notify-event", G_CALLBACK(signalMotion), this); + } + } +#endif + } + + virtual void popup_at_rect(weld::Widget* pParent, const tools::Rectangle& rRect, weld::Placement ePlace) override + { + GtkInstanceWidget* pGtkWidget = dynamic_cast<GtkInstanceWidget*>(pParent); + assert(pGtkWidget); + + GtkWidget* pWidget = pGtkWidget->getWidget(); + + GdkRectangle aRect; + pWidget = getPopupRect(pWidget, rRect, aRect); + +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_set_parent(GTK_WIDGET(m_pPopover), pWidget); +#else + gtk_popover_set_relative_to(m_pPopover, pWidget); +#endif + gtk_popover_set_pointing_to(m_pPopover, &aRect); + + if (ePlace == weld::Placement::Under) + gtk_popover_set_position(m_pPopover, GTK_POS_BOTTOM); + else + { + if (::SwapForRTL(pWidget)) + gtk_popover_set_position(m_pPopover, GTK_POS_LEFT); + else + gtk_popover_set_position(m_pPopover, GTK_POS_RIGHT); + } + +#if !GTK_CHECK_VERSION(4, 0, 0) + //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(GTK_WIDGET(m_pPopover)); + if (DLSYM_GDK_IS_X11_DISPLAY(pDisplay)) + { + if (!m_bMenuPoppedUp) + { + MovePopoverContentsToWindow(GTK_WIDGET(m_pPopover), m_pMenuHack, pWidget, aRect, ePlace); + m_bMenuPoppedUp = true; + } + return; + } +#endif + + gtk_popover_popup(m_pPopover); + } + +#if !GTK_CHECK_VERSION(4, 0, 0) + virtual bool get_visible() const override + { + if (m_pMenuHack) + return gtk_widget_get_visible(GTK_WIDGET(m_pMenuHack)); + return gtk_widget_get_visible(m_pWidget); + } + + virtual void ensureMouseEventWidget() override + { + if (!m_pMouseEventBox && m_pMenuHack) + { + m_pMouseEventBox = GTK_WIDGET(m_pMenuHack); + return; + } + GtkInstanceContainer::ensureMouseEventWidget(); + } +#endif + + virtual void popdown() override + { +#if !GTK_CHECK_VERSION(4, 0, 0) + //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(GTK_WIDGET(m_pPopover)); + if (DLSYM_GDK_IS_X11_DISPLAY(pDisplay)) + { + if (m_bMenuPoppedUp) + { + m_nButtonPressSeen = false; + MoveWindowContentsToPopover(m_pMenuHack, GTK_WIDGET(m_pPopover), gtk_popover_get_relative_to(m_pPopover)); + m_bMenuPoppedUp = false; + signal_closed(); + } + return; + } +#endif + + gtk_popover_popdown(m_pPopover); + } + + void PopdownAndFlushClosedSignal() + { + if (get_visible()) + popdown(); + if (m_pClosedEvent) + { + Application::RemoveUserEvent(m_pClosedEvent); + async_signal_closed(nullptr); + } + } + + virtual void resize_to_request() override + { + // resizing to request is what gtk does automatically + } + + virtual ~GtkInstancePopover() override + { + PopdownAndFlushClosedSignal(); + DisconnectMouseEvents(); +#if !GTK_CHECK_VERSION(4, 0, 0) + if (m_pMenuHack) + gtk_widget_destroy(GTK_WIDGET(m_pMenuHack)); +#endif + g_signal_handler_disconnect(m_pPopover, m_nSignalId); + } +}; + +IMPL_LINK_NOARG(GtkInstancePopover, async_signal_closed, void*, void) +{ + m_pClosedEvent = nullptr; + signal_closed(); +} + +} + +#if !GTK_CHECK_VERSION(4, 0, 0) + +namespace +{ + +AtkObject* drawing_area_get_accessible(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_accessible; + 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; + } +} + +} +#endif + +namespace { + +bool IsAllowedBuiltInIcon(std::u16string_view iconName) +{ + // limit the named icons to those known by VclBuilder + return VclBuilder::mapStockToSymbol(iconName) != SymbolType::DONTKNOW; +} + +} + +namespace { + +#if !GTK_CHECK_VERSION(4, 0, 0) +void silence_gwarning(const gchar* /*log_domain*/, + GLogLevelFlags /*log_level*/, + const gchar* /*message*/, + gpointer /*user_data*/) +{ +} +#endif + +void load_ui_file(GtkBuilder* pBuilder, const OUString& rUri) +{ +#if GTK_CHECK_VERSION(4, 0, 0) + builder_add_from_gtk3_file(pBuilder, rUri); +#else + guint nLogHandlerId = 0; + GLogLevelFlags nFatalMask(static_cast<GLogLevelFlags>(G_LOG_FLAG_RECURSION|G_LOG_LEVEL_ERROR)); + if (rUri.endsWith("sfx/ui/tabbarcontents.ui")) + { + // gtk unhelpfully has a bogus warning for the accelerator in this .ui because it assumes menus with accelerators + // if attached to something are attached to a MenuShell, but it's a MenuButton in this case. Turn off warnings, and + // in the case of fatal-warnings temp disable fatal warnings, for this case. + nLogHandlerId = g_log_set_handler("GLib-GObject", + static_cast<GLogLevelFlags>(G_LOG_LEVEL_MASK | G_LOG_FLAG_FATAL | G_LOG_FLAG_RECURSION), + silence_gwarning, nullptr); + nFatalMask = g_log_set_always_fatal(nFatalMask); + } + + OUString aPath; + osl::FileBase::getSystemPathFromFileURL(rUri, aPath); + GError *err = nullptr; + auto rc = gtk_builder_add_from_file(pBuilder, OUStringToOString(aPath, RTL_TEXTENCODING_UTF8).getStr(), &err); + + if (nLogHandlerId) + { + g_log_remove_handler("GLib-GObject", nLogHandlerId); + g_log_set_always_fatal(nFatalMask); + } + + if (!rc) + { + SAL_WARN( "vcl.gtk", "GtkInstanceBuilder: error when calling gtk_builder_add_from_file: " << err->message); + g_error_free(err); + } + assert(rc && "could not load UI file"); +#endif +} + +#if !GTK_CHECK_VERSION(4, 0, 0) +void fix_expander(GtkExpander* pExpander, GParamSpec*, gpointer) +{ + if (gtk_expander_get_resize_toplevel(pExpander)) + { + GtkWidget *pToplevel = 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 (gtk_expander_get_expanded(pExpander)) + nToplevelHeight += nChildHeight; + else + nToplevelHeight -= nChildHeight; + + gtk_window_resize(GTK_WINDOW(pToplevel), nToplevelWidth, nToplevelHeight); + } + } +} +#endif + +class GtkInstanceBuilder : public weld::Builder +{ +private: + ResHookProc m_pStringReplace; + OUString m_aHelpRoot; + OUString m_aIconTheme; + OUString m_aUILang; + GtkBuilder* m_pBuilder; + GSList* m_pObjectList; + GtkWidget* m_pParentWidget; + gulong m_nNotifySignalId; + std::vector<GtkButton*> m_aMnemonicButtons; +#if GTK_CHECK_VERSION(4, 0, 0) + std::vector<GtkCheckButton*> m_aMnemonicCheckButtons; +#endif + std::vector<GtkLabel*> m_aMnemonicLabels; + + VclPtr<SystemChildWindow> m_xInterimGlue; + bool m_bAllowCycleFocusOut; + + 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); + if (const gchar* icon_name = image_get_icon_name(pImage)) + { + OUString aIconName(icon_name, strlen(icon_name), RTL_TEXTENCODING_UTF8); + if (!IsAllowedBuiltInIcon(aIconName)) + image_set_from_icon_name_theme_lang(pImage, aIconName, m_aIconTheme, m_aUILang); + } + } +#if GTK_CHECK_VERSION(4, 0, 0) + else if (GTK_IS_PICTURE(pWidget)) + { + GtkPicture* pPicture = GTK_PICTURE(pWidget); + if (GFile* icon_file = gtk_picture_get_file(pPicture)) + { + char* icon_name = g_file_get_uri(icon_file); + OUString aIconName(icon_name, strlen(icon_name), RTL_TEXTENCODING_UTF8); + g_free(icon_name); + assert(aIconName.startsWith("private:///graphicrepository/")); + aIconName.startsWith("private:///graphicrepository/", &aIconName); + picture_set_from_icon_name_theme_lang(GTK_PICTURE(pWidget), aIconName, m_aIconTheme, m_aUILang); + } + } +#endif +#if !GTK_CHECK_VERSION(4, 0, 0) + else if (GTK_IS_TOOL_BUTTON(pWidget)) + { + GtkToolButton* pToolButton = GTK_TOOL_BUTTON(pWidget); + if (const gchar* icon_name = gtk_tool_button_get_icon_name(pToolButton)) + { + OUString aIconName(icon_name, strlen(icon_name), RTL_TEXTENCODING_UTF8); + if (!IsAllowedBuiltInIcon(aIconName)) + { + if (GtkWidget* pImage = image_new_from_icon_name_theme_lang(aIconName, m_aIconTheme, m_aUILang)) + { + 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); + } + } + else if (GTK_IS_EXPANDER(pWidget)) + { + g_signal_connect(pWidget, "notify::expanded", G_CALLBACK(fix_expander), this); + } +#else + else if (GTK_IS_BUTTON(pWidget)) + { + GtkButton* pButton = GTK_BUTTON(pWidget); + if (const gchar* icon_name = gtk_button_get_icon_name(pButton)) + { + OUString aIconName(icon_name, strlen(icon_name), RTL_TEXTENCODING_UTF8); + if (!IsAllowedBuiltInIcon(aIconName)) + { + if (GtkWidget* pImage = image_new_from_icon_name_theme_lang(aIconName, m_aIconTheme, m_aUILang)) + { + gtk_widget_set_halign(pImage, GTK_ALIGN_CENTER); + gtk_widget_set_valign(pImage, GTK_ALIGN_CENTER); + gtk_button_set_child(pButton, pImage); + gtk_widget_show(pImage); + } + } + } + } + else if (GTK_IS_MENU_BUTTON(pWidget)) + { + GtkMenuButton* pButton = GTK_MENU_BUTTON(pWidget); + if (const gchar* icon_name = gtk_menu_button_get_icon_name(pButton)) + { + OUString aIconName(icon_name, strlen(icon_name), RTL_TEXTENCODING_UTF8); + if (!IsAllowedBuiltInIcon(aIconName)) + { + if (GtkWidget* pImage = image_new_from_icon_name_theme_lang(aIconName, m_aIconTheme, m_aUILang)) + { + gtk_widget_set_halign(pImage, GTK_ALIGN_CENTER); + gtk_widget_set_valign(pImage, GTK_ALIGN_CENTER); + // TODO after gtk 4.6 is released require that version and drop this + static auto menu_button_set_child = reinterpret_cast<void (*) (GtkMenuButton*, GtkWidget*)>(dlsym(nullptr, "gtk_menu_button_set_child")); + if (menu_button_set_child) + menu_button_set_child(pButton, pImage); + gtk_widget_show(pImage); + } + } + } + } +#endif + + //set helpids + OUString sBuildableName = ::get_buildable_id(GTK_BUILDABLE(pWidget)); + if (!sBuildableName.isEmpty()) + { + OUString sHelpId = m_aHelpRoot + 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); + } + + if (m_pStringReplace) + { + // tdf#136498 %PRODUCTNAME shown in tool tips + const char* pTooltip = gtk_widget_get_tooltip_text(pWidget); + if (pTooltip && pTooltip[0]) + { + OUString aTooltip(pTooltip, strlen(pTooltip), RTL_TEXTENCODING_UTF8); + aTooltip = (*m_pStringReplace)(aTooltip); + gtk_widget_set_tooltip_text(pWidget, OUStringToOString(aTooltip, RTL_TEXTENCODING_UTF8).getStr()); + } + } + + // expand placeholder and collect potentially missing mnemonics + if (GTK_IS_BUTTON(pWidget)) + { + GtkButton* pButton = GTK_BUTTON(pWidget); + if (m_pStringReplace) + { + OUString aLabel(button_get_label(pButton)); + if (!aLabel.isEmpty()) + button_set_label(pButton, (*m_pStringReplace)(aLabel)); + } + if (gtk_button_get_use_underline(pButton)) + m_aMnemonicButtons.push_back(pButton); + } +#if GTK_CHECK_VERSION(4, 0, 0) + else if (GTK_IS_CHECK_BUTTON(pWidget)) + { + GtkCheckButton* pButton = GTK_CHECK_BUTTON(pWidget); + if (m_pStringReplace) + { + OUString aLabel(get_label(pButton)); + if (!aLabel.isEmpty()) + set_label(pButton, (*m_pStringReplace)(aLabel)); + } + if (gtk_check_button_get_use_underline(pButton)) + m_aMnemonicCheckButtons.push_back(pButton); + } +#endif + else if (GTK_IS_LABEL(pWidget)) + { + GtkLabel* pLabel = GTK_LABEL(pWidget); + if (m_pStringReplace) + { + 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) + { + 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); + } + } +#if !GTK_CHECK_VERSION(4, 0, 0) + else if (GTK_IS_ENTRY(pWidget)) + { + g_signal_connect(pWidget, "key-press-event", G_CALLBACK(signalEntryInsertSpecialCharKeyPress), nullptr); + g_signal_connect(pWidget, "populate-popup", G_CALLBACK(signalEntryPopulatePopup), nullptr); + } +#endif + else if (GTK_IS_WINDOW(pWidget)) + { + if (m_pStringReplace) + { + 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)); + } + + void DisallowCycleFocusOut() + { + assert(!m_bAllowCycleFocusOut); // we only expect this to be called when this holds + + GtkWidget* pTopLevel = widget_get_toplevel(m_pParentWidget); + assert(pTopLevel); + GtkSalFrame* pFrame = GtkSalFrame::getFromWindow(pTopLevel); + assert(pFrame); + // unhook handler and let gtk cycle its own way through this widget's + // children because it has no non-gtk siblings + pFrame->DisallowCycleFocusOut(); + } + + static void signalMap(GtkWidget*, gpointer user_data) + { + GtkInstanceBuilder* pThis = static_cast<GtkInstanceBuilder*>(user_data); + // tdf#138047 wait until map to do this because the final SalFrame may + // not be the same as at ctor time + pThis->DisallowCycleFocusOut(); + } + + void AllowCycleFocusOut() + { + assert(!m_bAllowCycleFocusOut); // we only expect this to be called when this holds + + GtkWidget* pTopLevel = widget_get_toplevel(m_pParentWidget); + assert(pTopLevel); + GtkSalFrame* pFrame = GtkSalFrame::getFromWindow(pTopLevel); + assert(pFrame); + // rehook handler and let vcl cycle its own way through this widget's + // children + pFrame->AllowCycleFocusOut(); + + // tdf#145567 if the focus is in this hierarchy then, now that we are tearing down, + // move focus to the usual focus candidate for the frame + GtkWindow* pFocusWin = get_active_window(); + GtkWidget* pFocus = pFocusWin ? gtk_window_get_focus(pFocusWin) : nullptr; + bool bHasFocus = pFocus && gtk_widget_is_ancestor(pFocus, pTopLevel); + if (bHasFocus) + pFrame->GrabFocus(); + } + + static void signalUnmap(GtkWidget*, gpointer user_data) + { + GtkInstanceBuilder* pThis = static_cast<GtkInstanceBuilder*>(user_data); + pThis->AllowCycleFocusOut(); + } + +public: + GtkInstanceBuilder(GtkWidget* pParent, std::u16string_view rUIRoot, const OUString& rUIFile, + SystemChildWindow* pInterimGlue, bool bAllowCycleFocusOut) + : weld::Builder() + , m_pStringReplace(Translate::GetReadStringHook()) + , m_pParentWidget(pParent) + , m_nNotifySignalId(0) + , m_xInterimGlue(pInterimGlue) + , m_bAllowCycleFocusOut(bAllowCycleFocusOut) + { + OUString sHelpRoot(rUIFile); +#if !GTK_CHECK_VERSION(4, 0, 0) + ensure_intercept_drawing_area_accessibility(); + ensure_disable_ctrl_page_up_down_bindings(); +#endif + + sal_Int32 nIdx = sHelpRoot.lastIndexOf('.'); + if (nIdx != -1) + sHelpRoot = sHelpRoot.copy(0, nIdx); + sHelpRoot += "/"; + m_aHelpRoot = sHelpRoot; + m_aIconTheme = Application::GetSettings().GetStyleSettings().DetermineIconTheme(); + m_aUILang = Application::GetSettings().GetUILanguageTag().getBcp47(); + + OUString aUri(rUIRoot + rUIFile); + + m_pBuilder = gtk_builder_new(); + m_nNotifySignalId = g_signal_connect_data(G_OBJECT(m_pBuilder), "notify", G_CALLBACK(signalNotify), this, nullptr, G_CONNECT_AFTER); + + load_ui_file(m_pBuilder, aUri); + + 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()); + + if (!m_bAllowCycleFocusOut) + { + g_signal_connect(G_OBJECT(m_pParentWidget), "map", G_CALLBACK(signalMap), this); + g_signal_connect(G_OBJECT(m_pParentWidget), "unmap", G_CALLBACK(signalUnmap), this); + } + } + } + + void GenerateMissingMnemonics() + { + MnemonicGenerator aMnemonicGenerator('_'); + for (const auto a : m_aMnemonicButtons) + aMnemonicGenerator.RegisterMnemonic(button_get_label(a)); +#if GTK_CHECK_VERSION(4, 0, 0) + for (const auto a : m_aMnemonicCheckButtons) + aMnemonicGenerator.RegisterMnemonic(get_label(a)); +#endif + for (const auto a : m_aMnemonicLabels) + aMnemonicGenerator.RegisterMnemonic(get_label(a)); + + for (const auto a : m_aMnemonicButtons) + { + OUString aLabel(button_get_label(a)); + OUString aNewLabel = aMnemonicGenerator.CreateMnemonic(aLabel); + if (aLabel == aNewLabel) + continue; + button_set_label(a, aNewLabel); + } +#if GTK_CHECK_VERSION(4, 0, 0) + for (const auto a : m_aMnemonicCheckButtons) + { + OUString aLabel(get_label(a)); + OUString aNewLabel = aMnemonicGenerator.CreateMnemonic(aLabel); + if (aLabel == aNewLabel) + continue; + set_label(a, aNewLabel); + } +#endif + 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(); +#if GTK_CHECK_VERSION(4, 0, 0) + m_aMnemonicCheckButtons.clear(); +#endif + m_aMnemonicButtons.clear(); + } + + OUString get_current_page_help_id() + { + OUString 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")); + if (xNotebook) + { + if (GtkInstanceContainer* pPage = dynamic_cast<GtkInstanceContainer*>(xNotebook->get_page(xNotebook->get_current_page_ident()))) + { + GtkWidget* pContainer = pPage->getWidget(); + if (GtkWidget* pPageWidget = widget_get_first_child(pContainer)) + sPageHelpId = ::get_help_id(pPageWidget); + } + } + return sPageHelpId; + } + + virtual ~GtkInstanceBuilder() override + { + g_slist_free(m_pObjectList); + g_object_unref(m_pBuilder); + + if (m_xInterimGlue && !m_bAllowCycleFocusOut) + AllowCycleFocusOut(); + + 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_IS_POPOVER(pWidget)) + return; + if (GTK_IS_WINDOW(pWidget)) + return; +#if GTK_CHECK_VERSION(4, 0, 0) + if (!gtk_widget_get_parent(pWidget)) + gtk_widget_set_parent(pWidget, m_pParentWidget); +#else + if (widget_get_toplevel(pWidget) == pWidget) + gtk_container_add(GTK_CONTAINER(m_pParentWidget), pWidget); +#endif + } + + virtual std::unique_ptr<weld::MessageDialog> weld_message_dialog(const OUString &id) override + { + GtkMessageDialog* pMessageDialog = GTK_MESSAGE_DIALOG(gtk_builder_get_object(m_pBuilder, OUStringToOString(id, RTL_TEXTENCODING_UTF8).getStr())); + if (!pMessageDialog) + return nullptr; + gtk_window_set_transient_for(GTK_WINDOW(pMessageDialog), GTK_WINDOW(widget_get_toplevel(m_pParentWidget))); + return std::make_unique<GtkInstanceMessageDialog>(pMessageDialog, this, true); + } + + virtual std::unique_ptr<weld::Assistant> weld_assistant(const OUString &id) override + { + GtkAssistant* pAssistant = GTK_ASSISTANT(gtk_builder_get_object(m_pBuilder, OUStringToOString(id, RTL_TEXTENCODING_UTF8).getStr())); + if (!pAssistant) + return nullptr; + if (m_pParentWidget) + gtk_window_set_transient_for(GTK_WINDOW(pAssistant), GTK_WINDOW(widget_get_toplevel(m_pParentWidget))); + return std::make_unique<GtkInstanceAssistant>(pAssistant, this, true); + } + + virtual std::unique_ptr<weld::Dialog> weld_dialog(const OUString &id) override + { + GtkWindow* pDialog = GTK_WINDOW(gtk_builder_get_object(m_pBuilder, OUStringToOString(id, RTL_TEXTENCODING_UTF8).getStr())); + if (!pDialog) + return nullptr; + if (m_pParentWidget) + gtk_window_set_transient_for(pDialog, GTK_WINDOW(widget_get_toplevel(m_pParentWidget))); + return std::make_unique<GtkInstanceDialog>(pDialog, this, true); + } + + 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)); +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_container_add(GTK_CONTAINER(pContentArea), pTopLevel); + gtk_widget_show_all(pTopLevel); +#else + gtk_box_append(GTK_BOX(pContentArea), pTopLevel); + gtk_widget_show(pTopLevel); +#endif + } + + if (m_pParentWidget) + gtk_window_set_transient_for(pDialog, GTK_WINDOW(widget_get_toplevel(m_pParentWidget))); + return std::make_unique<GtkInstanceDialog>(pDialog, this, true); + } + + virtual std::unique_ptr<weld::Widget> weld_widget(const OUString &id) override + { + GtkWidget* pWidget = GTK_WIDGET(gtk_builder_get_object(m_pBuilder, OUStringToOString(id, RTL_TEXTENCODING_UTF8).getStr())); + if (!pWidget) + return nullptr; + auto_add_parentless_widgets_to_container(pWidget); + return std::make_unique<GtkInstanceWidget>(pWidget, this, false); + } + + virtual std::unique_ptr<weld::Container> weld_container(const OUString &id) override + { +#if !GTK_CHECK_VERSION(4, 0, 0) + GtkContainer* pContainer = GTK_CONTAINER(gtk_builder_get_object(m_pBuilder, OUStringToOString(id, RTL_TEXTENCODING_UTF8).getStr())); +#else + GtkWidget* pContainer = GTK_WIDGET(gtk_builder_get_object(m_pBuilder, OUStringToOString(id, RTL_TEXTENCODING_UTF8).getStr())); +#endif + if (!pContainer) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pContainer)); + return std::make_unique<GtkInstanceContainer>(pContainer, this, false); + } + + virtual std::unique_ptr<weld::Box> weld_box(const OUString &id) override + { + GtkBox* pBox = GTK_BOX(gtk_builder_get_object(m_pBuilder, OUStringToOString(id, RTL_TEXTENCODING_UTF8).getStr())); + if (!pBox) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pBox)); + return std::make_unique<GtkInstanceBox>(pBox, this, false); + } + + virtual std::unique_ptr<weld::Paned> weld_paned(const OUString &id) override + { + GtkPaned* pPaned = GTK_PANED(gtk_builder_get_object(m_pBuilder, OUStringToOString(id, RTL_TEXTENCODING_UTF8).getStr())); + if (!pPaned) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pPaned)); + return std::make_unique<GtkInstancePaned>(pPaned, this, false); + } + + virtual std::unique_ptr<weld::Frame> weld_frame(const OUString &id) override + { + GtkFrame* pFrame = GTK_FRAME(gtk_builder_get_object(m_pBuilder, OUStringToOString(id, RTL_TEXTENCODING_UTF8).getStr())); + if (!pFrame) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pFrame)); + return std::make_unique<GtkInstanceFrame>(pFrame, this, false); + } + + virtual std::unique_ptr<weld::ScrolledWindow> weld_scrolled_window(const OUString &id, bool bUserManagedScrolling = false) override + { + GtkScrolledWindow* pScrolledWindow = GTK_SCROLLED_WINDOW(gtk_builder_get_object(m_pBuilder, OUStringToOString(id, RTL_TEXTENCODING_UTF8).getStr())); + if (!pScrolledWindow) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pScrolledWindow)); + return std::make_unique<GtkInstanceScrolledWindow>(pScrolledWindow, this, false, bUserManagedScrolling); + } + + virtual std::unique_ptr<weld::Notebook> weld_notebook(const OUString &id) override + { + GtkNotebook* pNotebook = GTK_NOTEBOOK(gtk_builder_get_object(m_pBuilder, OUStringToOString(id, RTL_TEXTENCODING_UTF8).getStr())); + if (!pNotebook) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pNotebook)); + return std::make_unique<GtkInstanceNotebook>(pNotebook, this, false); + } + + virtual std::unique_ptr<weld::Button> weld_button(const OUString &id) override + { + GtkButton* pButton = GTK_BUTTON(gtk_builder_get_object(m_pBuilder, OUStringToOString(id, RTL_TEXTENCODING_UTF8).getStr())); + if (!pButton) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pButton)); + return std::make_unique<GtkInstanceButton>(pButton, this, false); + } + + virtual std::unique_ptr<weld::MenuButton> weld_menu_button(const OUString &id) override + { + GtkMenuButton* pButton = GTK_MENU_BUTTON(gtk_builder_get_object(m_pBuilder, OUStringToOString(id, RTL_TEXTENCODING_UTF8).getStr())); + if (!pButton) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pButton)); + return std::make_unique<GtkInstanceMenuButton>(pButton, nullptr, this, false); + } + + virtual std::unique_ptr<weld::MenuToggleButton> weld_menu_toggle_button(const OUString &id) override + { + GtkMenuButton* pButton = GTK_MENU_BUTTON(gtk_builder_get_object(m_pBuilder, OUStringToOString(id, RTL_TEXTENCODING_UTF8).getStr())); + if (!pButton) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pButton)); + // gtk doesn't come with exactly the same concept + GtkBuilder* pMenuToggleButton = makeMenuToggleButtonBuilder(); + return std::make_unique<GtkInstanceMenuToggleButton>(pMenuToggleButton, pButton, this, false); + } + + virtual std::unique_ptr<weld::LinkButton> weld_link_button(const OUString &id) override + { + GtkLinkButton* pButton = GTK_LINK_BUTTON(gtk_builder_get_object(m_pBuilder, OUStringToOString(id, RTL_TEXTENCODING_UTF8).getStr())); + if (!pButton) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pButton)); + return std::make_unique<GtkInstanceLinkButton>(pButton, this, false); + } + + virtual std::unique_ptr<weld::ToggleButton> weld_toggle_button(const OUString &id) override + { + GtkToggleButton* pToggleButton = GTK_TOGGLE_BUTTON(gtk_builder_get_object(m_pBuilder, OUStringToOString(id, RTL_TEXTENCODING_UTF8).getStr())); + if (!pToggleButton) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pToggleButton)); + return std::make_unique<GtkInstanceToggleButton>(pToggleButton, this, false); + } + + virtual std::unique_ptr<weld::RadioButton> weld_radio_button(const OUString &id) override + { +#if GTK_CHECK_VERSION(4, 0, 0) + GtkCheckButton* pRadioButton = GTK_CHECK_BUTTON(gtk_builder_get_object(m_pBuilder, OUStringToOString(id, RTL_TEXTENCODING_UTF8).getStr())); +#else + GtkRadioButton* pRadioButton = GTK_RADIO_BUTTON(gtk_builder_get_object(m_pBuilder, OUStringToOString(id, RTL_TEXTENCODING_UTF8).getStr())); +#endif + if (!pRadioButton) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pRadioButton)); + return std::make_unique<GtkInstanceRadioButton>(pRadioButton, this, false); + } + + virtual std::unique_ptr<weld::CheckButton> weld_check_button(const OUString &id) override + { + GtkCheckButton* pCheckButton = GTK_CHECK_BUTTON(gtk_builder_get_object(m_pBuilder, OUStringToOString(id, RTL_TEXTENCODING_UTF8).getStr())); + if (!pCheckButton) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pCheckButton)); + return std::make_unique<GtkInstanceCheckButton>(pCheckButton, this, false); + } + + virtual std::unique_ptr<weld::Scale> weld_scale(const OUString &id) override + { + GtkScale* pScale = GTK_SCALE(gtk_builder_get_object(m_pBuilder, OUStringToOString(id, RTL_TEXTENCODING_UTF8).getStr())); + if (!pScale) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pScale)); + return std::make_unique<GtkInstanceScale>(pScale, this, false); + } + + virtual std::unique_ptr<weld::ProgressBar> weld_progress_bar(const OUString &id) override + { + GtkProgressBar* pProgressBar = GTK_PROGRESS_BAR(gtk_builder_get_object(m_pBuilder, OUStringToOString(id, RTL_TEXTENCODING_UTF8).getStr())); + if (!pProgressBar) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pProgressBar)); + return std::make_unique<GtkInstanceProgressBar>(pProgressBar, this, false); + } + + virtual std::unique_ptr<weld::LevelBar> weld_level_bar(const OUString& id) override + { + GtkLevelBar* pLevelBar = GTK_LEVEL_BAR(gtk_builder_get_object( + m_pBuilder, OUStringToOString(id, RTL_TEXTENCODING_UTF8).getStr())); + if (!pLevelBar) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pLevelBar)); + return std::make_unique<GtkInstanceLevelBar>(pLevelBar, this, false); + } + + virtual std::unique_ptr<weld::Spinner> weld_spinner(const OUString &id) override + { + GtkSpinner* pSpinner = GTK_SPINNER(gtk_builder_get_object(m_pBuilder, OUStringToOString(id, RTL_TEXTENCODING_UTF8).getStr())); + if (!pSpinner) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pSpinner)); + return std::make_unique<GtkInstanceSpinner>(pSpinner, this, false); + } + + virtual std::unique_ptr<weld::Image> weld_image(const OUString &id) override + { + GtkWidget* pWidget = GTK_WIDGET(gtk_builder_get_object(m_pBuilder, OUStringToOString(id, RTL_TEXTENCODING_UTF8).getStr())); + if (!pWidget) + return nullptr; + if (GTK_IS_IMAGE(pWidget)) + { + auto_add_parentless_widgets_to_container(pWidget); + return std::make_unique<GtkInstanceImage>(GTK_IMAGE(pWidget), this, false); + } +#if GTK_CHECK_VERSION(4, 0, 0) + if (GTK_IS_PICTURE(pWidget)) + { + auto_add_parentless_widgets_to_container(pWidget); + return std::make_unique<GtkInstancePicture>(GTK_PICTURE(pWidget), this, false); + } +#endif + return nullptr; + } + + virtual std::unique_ptr<weld::Calendar> weld_calendar(const OUString &id) override + { + GtkCalendar* pCalendar = GTK_CALENDAR(gtk_builder_get_object(m_pBuilder, OUStringToOString(id, RTL_TEXTENCODING_UTF8).getStr())); + if (!pCalendar) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pCalendar)); + return std::make_unique<GtkInstanceCalendar>(pCalendar, this, false); + } + + virtual std::unique_ptr<weld::Entry> weld_entry(const OUString &id) override + { + GtkEntry* pEntry = GTK_ENTRY(gtk_builder_get_object(m_pBuilder, OUStringToOString(id, RTL_TEXTENCODING_UTF8).getStr())); + if (!pEntry) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pEntry)); + return std::make_unique<GtkInstanceEntry>(pEntry, this, false); + } + + virtual std::unique_ptr<weld::SpinButton> weld_spin_button(const OUString &id) override + { + GtkSpinButton* pSpinButton = GTK_SPIN_BUTTON(gtk_builder_get_object(m_pBuilder, OUStringToOString(id, RTL_TEXTENCODING_UTF8).getStr())); + if (!pSpinButton) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pSpinButton)); + return std::make_unique<GtkInstanceSpinButton>(pSpinButton, this, false); + } + + virtual std::unique_ptr<weld::MetricSpinButton> weld_metric_spin_button(const OUString& id, FieldUnit eUnit) override + { + return std::make_unique<weld::MetricSpinButton>(weld_spin_button(id), eUnit); + } + + virtual std::unique_ptr<weld::FormattedSpinButton> weld_formatted_spin_button(const OUString &id) override + { + GtkSpinButton* pSpinButton = GTK_SPIN_BUTTON(gtk_builder_get_object(m_pBuilder, OUStringToOString(id, RTL_TEXTENCODING_UTF8).getStr())); + if (!pSpinButton) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pSpinButton)); + return std::make_unique<GtkInstanceFormattedSpinButton>(pSpinButton, this, false); + } + + virtual std::unique_ptr<weld::ComboBox> weld_combo_box(const OUString &id) override + { + GtkComboBox* pComboBox = GTK_COMBO_BOX(gtk_builder_get_object(m_pBuilder, OUStringToOString(id, RTL_TEXTENCODING_UTF8).getStr())); + if (!pComboBox) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pComboBox)); + +#if GTK_CHECK_VERSION(4, 0, 0) + return std::make_unique<GtkInstanceComboBox>(pComboBox, this, false); +#else + /* 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, false); +#endif + } + + virtual std::unique_ptr<weld::TreeView> weld_tree_view(const OUString &id) override + { + GtkTreeView* pTreeView = GTK_TREE_VIEW(gtk_builder_get_object(m_pBuilder, OUStringToOString(id, RTL_TEXTENCODING_UTF8).getStr())); + if (!pTreeView) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pTreeView)); + return std::make_unique<GtkInstanceTreeView>(pTreeView, this, false); + } + + virtual std::unique_ptr<weld::IconView> weld_icon_view(const OUString &id) override + { + GtkIconView* pIconView = GTK_ICON_VIEW(gtk_builder_get_object(m_pBuilder, OUStringToOString(id, RTL_TEXTENCODING_UTF8).getStr())); + if (!pIconView) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pIconView)); + return std::make_unique<GtkInstanceIconView>(pIconView, this, false); + } + + virtual std::unique_ptr<weld::EntryTreeView> weld_entry_tree_view(const OUString& containerid, const OUString& entryid, const OUString& treeviewid) override + { +#if GTK_CHECK_VERSION(4, 0, 0) + GtkWidget* pContainer = GTK_WIDGET(gtk_builder_get_object(m_pBuilder, OUStringToOString(containerid, RTL_TEXTENCODING_UTF8).getStr())); +#else + GtkContainer* pContainer = GTK_CONTAINER(gtk_builder_get_object(m_pBuilder, OUStringToOString(containerid, RTL_TEXTENCODING_UTF8).getStr())); +#endif + if (!pContainer) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pContainer)); + return std::make_unique<GtkInstanceEntryTreeView>(pContainer, this, false, + weld_entry(entryid), + weld_tree_view(treeviewid)); + } + + virtual std::unique_ptr<weld::Label> weld_label(const OUString &id) override + { + GtkLabel* pLabel = GTK_LABEL(gtk_builder_get_object(m_pBuilder, OUStringToOString(id, RTL_TEXTENCODING_UTF8).getStr())); + if (!pLabel) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pLabel)); + return std::make_unique<GtkInstanceLabel>(pLabel, this, false); + } + + virtual std::unique_ptr<weld::TextView> weld_text_view(const OUString &id) override + { + GtkTextView* pTextView = GTK_TEXT_VIEW(gtk_builder_get_object(m_pBuilder, OUStringToOString(id, RTL_TEXTENCODING_UTF8).getStr())); + if (!pTextView) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pTextView)); + return std::make_unique<GtkInstanceTextView>(pTextView, this, false); + } + + virtual std::unique_ptr<weld::Expander> weld_expander(const OUString &id) override + { + GtkExpander* pExpander = GTK_EXPANDER(gtk_builder_get_object(m_pBuilder, OUStringToOString(id, RTL_TEXTENCODING_UTF8).getStr())); + if (!pExpander) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pExpander)); + return std::make_unique<GtkInstanceExpander>(pExpander, this, false); + } + + virtual std::unique_ptr<weld::DrawingArea> weld_drawing_area(const OUString &id, const a11yref& rA11y, + FactoryFunction /*pUITestFactoryFunction*/, void* /*pUserData*/) override + { + GtkDrawingArea* pDrawingArea = GTK_DRAWING_AREA(gtk_builder_get_object(m_pBuilder, OUStringToOString(id, RTL_TEXTENCODING_UTF8).getStr())); + if (!pDrawingArea) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pDrawingArea)); + return std::make_unique<GtkInstanceDrawingArea>(pDrawingArea, this, rA11y, false); + } + + virtual std::unique_ptr<weld::Menu> weld_menu(const OUString &id) override + { +#if GTK_CHECK_VERSION(4, 0, 0) + GtkPopoverMenu* pMenu = GTK_POPOVER_MENU(gtk_builder_get_object(m_pBuilder, OUStringToOString(id, RTL_TEXTENCODING_UTF8).getStr())); +#else + GtkMenu* pMenu = GTK_MENU(gtk_builder_get_object(m_pBuilder, OUStringToOString(id, RTL_TEXTENCODING_UTF8).getStr())); +#endif + if (!pMenu) + return nullptr; + return std::make_unique<GtkInstanceMenu>(pMenu, true); + } + + virtual std::unique_ptr<weld::Popover> weld_popover(const OUString &id) override + { + GtkPopover* pPopover = GTK_POPOVER(gtk_builder_get_object(m_pBuilder, OUStringToOString(id, RTL_TEXTENCODING_UTF8).getStr())); + if (!pPopover) + return nullptr; +#if GTK_CHECK_VERSION(4, 0, 0) + return std::make_unique<GtkInstancePopover>(pPopover, this, false); +#else + return std::make_unique<GtkInstancePopover>(pPopover, this, true); +#endif + } + + virtual std::unique_ptr<weld::Toolbar> weld_toolbar(const OUString &id) override + { +#if GTK_CHECK_VERSION(4, 0, 0) + GtkBox* pToolbar = GTK_BOX(gtk_builder_get_object(m_pBuilder, OUStringToOString(id, RTL_TEXTENCODING_UTF8).getStr())); +#else + GtkToolbar* pToolbar = GTK_TOOLBAR(gtk_builder_get_object(m_pBuilder, OUStringToOString(id, RTL_TEXTENCODING_UTF8).getStr())); +#endif + if (!pToolbar) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pToolbar)); + return std::make_unique<GtkInstanceToolbar>(pToolbar, this, false); + } + + virtual std::unique_ptr<weld::Scrollbar> weld_scrollbar(const OUString &id) override + { + GtkScrollbar* pScrollbar = GTK_SCROLLBAR(gtk_builder_get_object(m_pBuilder, OUStringToOString(id, RTL_TEXTENCODING_UTF8).getStr())); + if (!pScrollbar) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pScrollbar)); + return std::make_unique<GtkInstanceScrollbar>(pScrollbar, this, false); + } + + 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); + OUString 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) + return; + +#if !GTK_CHECK_VERSION(4, 0, 0) + // 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")) + { + OUString 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) + { + GtkWidget* pContentWidget = widget_get_first_child(GTK_WIDGET(pContainer)); + if (pContentWidget) + sHelpId = ::get_help_id(pContentWidget); + } + } + } +#endif + pHelp->Start(sHelpId, 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 OUString&)>& func) +{ + GtkWidget* pParent = m_pWidget; + while ((pParent = gtk_widget_get_parent(pParent))) + { + if (func(::get_help_id(pParent))) + return; + } +} + +std::unique_ptr<weld::Builder> GtkInstance::CreateBuilder(weld::Widget* pParent, const OUString& rUIRoot, const OUString& rUIFile) +{ + GtkInstanceWidget* pParentWidget = dynamic_cast<GtkInstanceWidget*>(pParent); + GtkWidget* pBuilderParent = pParentWidget ? pParentWidget->getWidget() : nullptr; + return std::make_unique<GtkInstanceBuilder>(pBuilderParent, rUIRoot, rUIFile, nullptr, true); +} + +#if !GTK_CHECK_VERSION(4, 0, 0) +// 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); + OUString 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(sHelpId, pChildWindow); + return true; + } + + if (!pWidget) + return true; + std::unique_ptr<weld::Widget> xTemp(new GtkInstanceWidget(pWidget, nullptr, false)); + pHelp->Start(sHelpId, xTemp.get()); + return true; +} +#endif + +std::unique_ptr<weld::Builder> GtkInstance::CreateInterimBuilder(vcl::Window* pParent, const OUString& rUIRoot, const OUString& rUIFile, + bool bAllowCycleFocusOut, sal_uInt64) +{ + // 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); +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_show_all(pWindow); +#else + gtk_widget_show(pWindow); +#endif + + // build the widget tree as a child of the GtkEventBox GtkGrid parent + return std::make_unique<GtkInstanceBuilder>(pWindow, rUIRoot, rUIFile, xEmbedWindow.get(), bAllowCycleFocusOut); +} + +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(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); +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_container_add(GTK_CONTAINER(pParent), pGstWidget); +#endif + g_object_unref(pGstWidget); +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_show_all(pParent); +#else + gtk_widget_show(pParent); +#endif + + return pVideosink; +#else + (void)pWindow; + return nullptr; +#endif +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ |