1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
3 * This file is part of the LibreOffice project.
5 * This Source Code Form is subject to the terms of the Mozilla Public
6 * License, v. 2.0. If a copy of the MPL was not distributed with this
7 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
10 #include <unx/gtk/gtksalmenu.hxx>
12 #include <unx/gtk/gtkdata.hxx>
13 #include <unx/gtk/glomenu.h>
14 #include <unx/gtk/gloactiongroup.h>
15 #include <vcl/toolkit/floatwin.hxx>
16 #include <vcl/menu.hxx>
17 #include <vcl/filter/PngImageWriter.hxx>
18 #include <vcl/pdfwriter.hxx> // for escapeStringXML
20 #include <o3tl/string_view.hxx>
21 #include <sal/log.hxx>
22 #include <tools/stream.hxx>
24 #include <strings.hrc>
26 static bool bUnityMode
= false;
29 * This function generates a unique command name for each menu item
31 static gchar
* GetCommandForItem(GtkSalMenu
* pParentMenu
, sal_uInt16 nItemId
)
33 OString aCommand
= "window-" +
34 OString::number(reinterpret_cast<sal_uIntPtr
>(pParentMenu
)) +
35 "-" + OString::number(nItemId
);
36 return g_strdup(aCommand
.getStr());
39 static gchar
* GetCommandForItem(GtkSalMenuItem
* pSalMenuItem
)
41 return GetCommandForItem(pSalMenuItem
->mpParentMenu
,
45 bool GtkSalMenu::PrepUpdate() const
47 return mpMenuModel
&& mpActionGroup
;
51 * Menu updating methods
54 static void RemoveSpareItemsFromNativeMenu( GLOMenu
* pMenu
, GList
** pOldCommandList
, unsigned nSection
, unsigned nValidItems
)
56 sal_Int32 nSectionItems
= g_lo_menu_get_n_items_from_section( pMenu
, nSection
);
58 while ( nSectionItems
> static_cast<sal_Int32
>(nValidItems
) )
60 gchar
* aCommand
= g_lo_menu_get_command_from_item_in_section( pMenu
, nSection
, --nSectionItems
);
62 if ( aCommand
!= nullptr && pOldCommandList
!= nullptr )
63 *pOldCommandList
= g_list_append( *pOldCommandList
, g_strdup( aCommand
) );
67 g_lo_menu_remove_from_section( pMenu
, nSection
, nSectionItems
);
71 typedef std::pair
<GtkSalMenu
*, sal_uInt16
> MenuAndId
;
75 MenuAndId
decode_command(const gchar
*action_name
)
77 std::string_view
sCommand(action_name
);
80 std::string_view sWindow
= o3tl::getToken(sCommand
, 0, '-', nIndex
);
81 std::string_view sGtkSalMenu
= o3tl::getToken(sCommand
, 0, '-', nIndex
);
82 std::string_view sItemId
= o3tl::getToken(sCommand
, 0, '-', nIndex
);
84 GtkSalMenu
* pSalSubMenu
= reinterpret_cast<GtkSalMenu
*>(o3tl::toInt64(sGtkSalMenu
));
86 assert(sWindow
== "window" && pSalSubMenu
);
89 return MenuAndId(pSalSubMenu
, o3tl::toInt32(sItemId
));
93 static void RemoveDisabledItemsFromNativeMenu(GLOMenu
* pMenu
, GList
** pOldCommandList
,
94 sal_Int32 nSection
, GActionGroup
* pActionGroup
)
98 sal_Int32 nSectionItems
= g_lo_menu_get_n_items_from_section( pMenu
, nSection
);
99 while (nSectionItems
--)
101 gchar
* pCommand
= g_lo_menu_get_command_from_item_in_section(pMenu
, nSection
, nSectionItems
);
102 // remove disabled entries
103 bool bRemove
= !g_action_group_get_action_enabled(pActionGroup
, pCommand
);
106 //also remove any empty submenus
107 GLOMenu
* pSubMenuModel
= g_lo_menu_get_submenu_from_item_in_section(pMenu
, nSection
, nSectionItems
);
110 gint nSubMenuSections
= g_menu_model_get_n_items(G_MENU_MODEL(pSubMenuModel
));
111 if (nSubMenuSections
== 0)
113 else if (nSubMenuSections
== 1)
115 gint nItems
= g_lo_menu_get_n_items_from_section(pSubMenuModel
, 0);
118 else if (nItems
== 1)
120 //If the only entry is the "No Selection Possible" entry, then we are allowed
122 gchar
* pSubCommand
= g_lo_menu_get_command_from_item_in_section(pSubMenuModel
, 0, 0);
123 MenuAndId
aMenuAndId(decode_command(pSubCommand
));
124 bRemove
= aMenuAndId
.second
== 0xFFFF;
133 //but tdf#86850 Always display clipboard functions
134 bRemove
= g_strcmp0(pCommand
, ".uno:Cut") &&
135 g_strcmp0(pCommand
, ".uno:Copy") &&
136 g_strcmp0(pCommand
, ".uno:Paste");
141 if (pCommand
!= nullptr && pOldCommandList
!= nullptr)
142 *pOldCommandList
= g_list_append(*pOldCommandList
, g_strdup(pCommand
));
143 g_lo_menu_remove_from_section(pMenu
, nSection
, nSectionItems
);
152 static void RemoveSpareSectionsFromNativeMenu( GLOMenu
* pMenu
, GList
** pOldCommandList
, sal_Int32 nLastSection
)
154 if ( pMenu
== nullptr || pOldCommandList
== nullptr )
157 sal_Int32 n
= g_menu_model_get_n_items( G_MENU_MODEL( pMenu
) ) - 1;
159 for ( ; n
> nLastSection
; n
--)
161 RemoveSpareItemsFromNativeMenu( pMenu
, pOldCommandList
, n
, 0 );
162 g_lo_menu_remove( pMenu
, n
);
166 static gint
CompareStr( gpointer str1
, gpointer str2
)
168 return g_strcmp0( static_cast<const gchar
*>(str1
), static_cast<const gchar
*>(str2
) );
171 static void RemoveUnusedCommands( GLOActionGroup
* pActionGroup
, GList
* pOldCommandList
, GList
* pNewCommandList
)
173 if ( pActionGroup
== nullptr || pOldCommandList
== nullptr )
175 g_list_free_full( pOldCommandList
, g_free
);
176 g_list_free_full( pNewCommandList
, g_free
);
180 while ( pNewCommandList
!= nullptr )
182 GList
* pNewCommand
= g_list_first( pNewCommandList
);
183 pNewCommandList
= g_list_remove_link( pNewCommandList
, pNewCommand
);
185 gpointer aCommand
= g_list_nth_data( pNewCommand
, 0 );
187 GList
* pOldCommand
= g_list_find_custom( pOldCommandList
, aCommand
, reinterpret_cast<GCompareFunc
>(CompareStr
) );
189 if ( pOldCommand
!= nullptr )
191 pOldCommandList
= g_list_remove_link( pOldCommandList
, pOldCommand
);
192 g_list_free_full( pOldCommand
, g_free
);
195 g_list_free_full( pNewCommand
, g_free
);
198 while ( pOldCommandList
!= nullptr )
200 GList
* pCommand
= g_list_first( pOldCommandList
);
201 pOldCommandList
= g_list_remove_link( pOldCommandList
, pCommand
);
203 gchar
* aCommand
= static_cast<gchar
*>(g_list_nth_data( pCommand
, 0 ));
205 g_lo_action_group_remove( pActionGroup
, aCommand
);
207 g_list_free_full( pCommand
, g_free
);
211 void GtkSalMenu::ImplUpdate(bool bRecurse
, bool bRemoveDisabledEntries
)
213 SolarMutexGuard aGuard
;
215 SAL_INFO("vcl.unity", "ImplUpdate pre PrepUpdate");
221 mbNeedsUpdate
= false;
222 if (mbMenuBar
&& maUpdateMenuBarIdle
.IsActive())
224 maUpdateMenuBarIdle
.Stop();
225 // tdf#124391 Prevent doubled menus in global menu
228 maUpdateMenuBarIdle
.Invoke();
234 Menu
* pVCLMenu
= mpVCLMenu
;
235 GLOMenu
* pLOMenu
= G_LO_MENU( mpMenuModel
);
236 GLOActionGroup
* pActionGroup
= G_LO_ACTION_GROUP( mpActionGroup
);
237 SAL_INFO("vcl.unity", "Syncing vcl menu " << pVCLMenu
<< " to menu model " << pLOMenu
<< " and action group " << pActionGroup
);
238 GList
*pOldCommandList
= nullptr;
239 GList
*pNewCommandList
= nullptr;
241 sal_uInt16 nLOMenuSize
= g_menu_model_get_n_items( G_MENU_MODEL( pLOMenu
) );
243 if ( nLOMenuSize
== 0 )
244 g_lo_menu_new_section( pLOMenu
, 0, nullptr );
246 sal_Int32 nSection
= 0;
247 sal_Int32 nItemPos
= 0;
248 sal_Int32 validItems
= 0;
251 for ( nItem
= 0; nItem
< static_cast<sal_Int32
>(GetItemCount()); nItem
++ ) {
252 if ( !IsItemVisible( nItem
) )
255 GtkSalMenuItem
*pSalMenuItem
= GetItemAtPos( nItem
);
256 sal_uInt16 nId
= pSalMenuItem
->mnId
;
258 // PopupMenu::ImplExecute might add <No Selection Possible> entry to top-level
259 // popup menu, but we have our own implementation below, so skip that one.
263 if ( pSalMenuItem
->mnType
== MenuItemType::SEPARATOR
)
265 // Delete extra items from current section.
266 RemoveSpareItemsFromNativeMenu( pLOMenu
, &pOldCommandList
, nSection
, validItems
);
272 if ( nLOMenuSize
<= nSection
)
274 g_lo_menu_new_section( pLOMenu
, nSection
, nullptr );
281 if ( nItemPos
>= g_lo_menu_get_n_items_from_section( pLOMenu
, nSection
) )
282 g_lo_menu_insert_in_section( pLOMenu
, nSection
, nItemPos
, "EMPTY STRING" );
284 // Get internal menu item values.
285 OUString aText
= pVCLMenu
->GetItemText( nId
);
286 Image aImage
= pVCLMenu
->GetItemImage( nId
);
287 bool bEnabled
= pVCLMenu
->IsItemEnabled( nId
);
288 vcl::KeyCode nAccelKey
= pVCLMenu
->GetAccelKey( nId
);
289 bool bChecked
= pVCLMenu
->IsItemChecked( nId
);
290 MenuItemBits itemBits
= pVCLMenu
->GetItemBits( nId
);
292 // Store current item command in command list.
293 gchar
*aCurrentCommand
= g_lo_menu_get_command_from_item_in_section( pLOMenu
, nSection
, nItemPos
);
295 if ( aCurrentCommand
!= nullptr )
296 pOldCommandList
= g_list_append( pOldCommandList
, aCurrentCommand
);
298 // Get the new command for the item.
299 gchar
* aNativeCommand
= GetCommandForItem(pSalMenuItem
);
301 // Force updating of native menu labels.
302 NativeSetItemText( nSection
, nItemPos
, aText
);
303 NativeSetItemIcon( nSection
, nItemPos
, aImage
);
304 NativeSetAccelerator(nSection
, nItemPos
, nAccelKey
, nAccelKey
.GetName());
306 if ( g_strcmp0( aNativeCommand
, "" ) != 0 && pSalMenuItem
->mpSubMenu
== nullptr )
308 NativeSetItemCommand( nSection
, nItemPos
, nId
, aNativeCommand
, itemBits
, bChecked
, false );
309 NativeCheckItem( nSection
, nItemPos
, itemBits
, bChecked
);
310 NativeSetEnableItem( aNativeCommand
, bEnabled
);
312 pNewCommandList
= g_list_append( pNewCommandList
, g_strdup( aNativeCommand
) );
315 GtkSalMenu
* pSubmenu
= pSalMenuItem
->mpSubMenu
;
317 if ( pSubmenu
&& pSubmenu
->GetMenu() )
319 bool bNonMenuChangedToMenu
= NativeSetItemCommand( nSection
, nItemPos
, nId
, aNativeCommand
, itemBits
, false, true );
320 pNewCommandList
= g_list_append( pNewCommandList
, g_strdup( aNativeCommand
) );
322 GLOMenu
* pSubMenuModel
= g_lo_menu_get_submenu_from_item_in_section( pLOMenu
, nSection
, nItemPos
);
324 if ( pSubMenuModel
== nullptr )
326 g_lo_menu_new_submenu_in_item_in_section( pLOMenu
, nSection
, nItemPos
);
327 pSubMenuModel
= g_lo_menu_get_submenu_from_item_in_section( pLOMenu
, nSection
, nItemPos
);
330 assert(pSubMenuModel
);
332 if (bRecurse
|| bNonMenuChangedToMenu
)
334 SAL_INFO("vcl.unity", "preparing submenu " << pSubMenuModel
<< " to menu model " << G_MENU_MODEL(pSubMenuModel
) << " and action group " << G_ACTION_GROUP(pActionGroup
));
335 pSubmenu
->SetMenuModel( G_MENU_MODEL( pSubMenuModel
) );
336 pSubmenu
->SetActionGroup( G_ACTION_GROUP( pActionGroup
) );
337 pSubmenu
->ImplUpdate(true, bRemoveDisabledEntries
);
340 g_object_unref( pSubMenuModel
);
343 g_free( aNativeCommand
);
349 if (bRemoveDisabledEntries
)
351 // Delete disabled items in last section.
352 RemoveDisabledItemsFromNativeMenu(pLOMenu
, &pOldCommandList
, nSection
, G_ACTION_GROUP(pActionGroup
));
355 // Delete extra items in last section.
356 RemoveSpareItemsFromNativeMenu( pLOMenu
, &pOldCommandList
, nSection
, validItems
);
358 // Delete extra sections.
359 RemoveSpareSectionsFromNativeMenu( pLOMenu
, &pOldCommandList
, nSection
);
361 // Delete unused commands.
362 RemoveUnusedCommands( pActionGroup
, pOldCommandList
, pNewCommandList
);
364 // Resolves: tdf#103166 if the menu is empty, add a disabled
365 // <No Selection Possible> placeholder.
366 sal_Int32 nSectionsCount
= g_menu_model_get_n_items(G_MENU_MODEL(pLOMenu
));
367 gint nItemsCount
= 0;
368 for (nSection
= 0; nSection
< nSectionsCount
; ++nSection
)
370 nItemsCount
+= g_lo_menu_get_n_items_from_section(pLOMenu
, nSection
);
376 gchar
* aNativeCommand
= GetCommandForItem(this, 0xFFFF);
377 OUString
aPlaceholderText(VclResId(SV_RESID_STRING_NOSELECTIONPOSSIBLE
));
378 g_lo_menu_insert_in_section(pLOMenu
, nSection
-1, 0,
379 OUStringToOString(aPlaceholderText
, RTL_TEXTENCODING_UTF8
).getStr());
380 NativeSetItemCommand(nSection
-1, 0, 0xFFFF, aNativeCommand
, MenuItemBits::NONE
, false, false);
381 NativeSetEnableItem(aNativeCommand
, false);
382 g_free(aNativeCommand
);
386 void GtkSalMenu::Update()
388 //find out if top level is a menubar or not, if not, then it's a popup menu
389 //hierarchy and in those we hide (most) disabled entries
390 const GtkSalMenu
* pMenu
= this;
391 while (pMenu
->mpParentSalMenu
)
392 pMenu
= pMenu
->mpParentSalMenu
;
394 bool bAlwaysShowDisabledEntries
;
395 if (pMenu
->mbMenuBar
)
396 bAlwaysShowDisabledEntries
= true;
398 bAlwaysShowDisabledEntries
= bool(mpVCLMenu
->GetMenuFlags() & MenuFlags::AlwaysShowDisabledEntries
);
400 ImplUpdate(false, !bAlwaysShowDisabledEntries
);
403 #if !GTK_CHECK_VERSION(4, 0, 0)
404 static void MenuPositionFunc(GtkMenu
* menu
, gint
* x
, gint
* y
, gboolean
* push_in
, gpointer user_data
)
406 Point
*pPos
= static_cast<Point
*>(user_data
);
408 if (gtk_widget_get_default_direction() == GTK_TEXT_DIR_RTL
)
410 GtkRequisition natural_size
;
411 gtk_widget_get_preferred_size(GTK_WIDGET(menu
), nullptr, &natural_size
);
412 *x
-= natural_size
.width
;
419 static void MenuClosed(GtkPopover
* pWidget
, GMainLoop
* pLoop
)
421 // gtk4 4.4.0: click on an entry in a submenu of a menu crashes without this workaround
422 gtk_widget_grab_focus(gtk_widget_get_parent(GTK_WIDGET(pWidget
)));
423 g_main_loop_quit(pLoop
);
426 bool GtkSalMenu::ShowNativePopupMenu(FloatingWindow
* pWin
, const tools::Rectangle
& rRect
,
427 FloatWinPopupFlags nFlags
)
429 VclPtr
<vcl::Window
> xParent
= pWin
->ImplGetWindowImpl()->mpRealParent
;
430 mpFrame
= static_cast<GtkSalFrame
*>(xParent
->ImplGetFrame());
432 GLOActionGroup
* pActionGroup
= g_lo_action_group_new();
433 mpActionGroup
= G_ACTION_GROUP(pActionGroup
);
434 mpMenuModel
= G_MENU_MODEL(g_lo_menu_new());
435 // Generate the main menu structure, populates mpMenuModel
438 #if !GTK_CHECK_VERSION(4, 0, 0)
439 mpMenuWidget
= gtk_menu_new_from_model(mpMenuModel
);
440 gtk_menu_attach_to_widget(GTK_MENU(mpMenuWidget
), mpFrame
->getMouseEventWidget(), nullptr);
442 mpMenuWidget
= gtk_popover_menu_new_from_model(mpMenuModel
);
443 gtk_widget_set_parent(mpMenuWidget
, mpFrame
->getMouseEventWidget());
444 gtk_popover_set_has_arrow(GTK_POPOVER(mpMenuWidget
), false);
446 gtk_widget_insert_action_group(mpFrame
->getMouseEventWidget(), "win", mpActionGroup
);
448 //run in a sub main loop because we need to keep vcl PopupMenu alive to use
449 //it during DispatchCommand, returning now to the outer loop causes the
450 //launching PopupMenu to be destroyed, instead run the subloop here
451 //until the gtk menu is destroyed
452 GMainLoop
* pLoop
= g_main_loop_new(nullptr, true);
453 #if GTK_CHECK_VERSION(4, 0, 0)
454 g_signal_connect(G_OBJECT(mpMenuWidget
), "closed", G_CALLBACK(MenuClosed
), pLoop
);
456 g_signal_connect(G_OBJECT(mpMenuWidget
), "deactivate", G_CALLBACK(MenuClosed
), pLoop
);
460 // tdf#120764 It isn't allowed under wayland to have two visible popups that share
461 // the same top level parent. The problem is that since gtk 3.24 tooltips are also
462 // implemented as popups, which means that we cannot show any popup if there is a
464 // hide any current tooltip
465 mpFrame
->HideTooltip();
466 // don't allow any more to appear until menu is dismissed
467 mpFrame
->BlockTooltip();
469 #if GTK_CHECK_VERSION(4, 0, 0)
470 tools::Rectangle aFloatRect
= FloatingWindow::ImplConvertToAbsPos(xParent
, rRect
);
471 aFloatRect
.Move(-mpFrame
->maGeometry
.x(), -mpFrame
->maGeometry
.y());
472 GdkRectangle rect
{static_cast<int>(aFloatRect
.Left()), static_cast<int>(aFloatRect
.Top()),
473 static_cast<int>(aFloatRect
.GetWidth()), static_cast<int>(aFloatRect
.GetHeight())};
475 gtk_popover_set_pointing_to(GTK_POPOVER(mpMenuWidget
), &rect
);
477 if (nFlags
& FloatWinPopupFlags::Left
)
478 gtk_popover_set_position(GTK_POPOVER(mpMenuWidget
), GTK_POS_LEFT
);
479 else if (nFlags
& FloatWinPopupFlags::Up
)
480 gtk_popover_set_position(GTK_POPOVER(mpMenuWidget
), GTK_POS_TOP
);
481 else if (nFlags
& FloatWinPopupFlags::Right
)
482 gtk_popover_set_position(GTK_POPOVER(mpMenuWidget
), GTK_POS_RIGHT
);
484 gtk_popover_set_position(GTK_POPOVER(mpMenuWidget
), GTK_POS_BOTTOM
);
486 gtk_popover_popup(GTK_POPOVER(mpMenuWidget
));
488 #if GTK_CHECK_VERSION(3,22,0)
489 if (gtk_check_version(3, 22, 0) == nullptr)
491 tools::Rectangle aFloatRect
= FloatingWindow::ImplConvertToAbsPos(xParent
, rRect
);
492 aFloatRect
.Move(-mpFrame
->maGeometry
.x(), -mpFrame
->maGeometry
.y());
493 GdkRectangle rect
{static_cast<int>(aFloatRect
.Left()), static_cast<int>(aFloatRect
.Top()),
494 static_cast<int>(aFloatRect
.GetWidth()), static_cast<int>(aFloatRect
.GetHeight())};
496 GdkGravity rect_anchor
= GDK_GRAVITY_SOUTH_WEST
, menu_anchor
= GDK_GRAVITY_NORTH_WEST
;
498 if (nFlags
& FloatWinPopupFlags::Left
)
500 rect_anchor
= GDK_GRAVITY_NORTH_WEST
;
501 menu_anchor
= GDK_GRAVITY_NORTH_EAST
;
503 else if (nFlags
& FloatWinPopupFlags::Up
)
505 rect_anchor
= GDK_GRAVITY_NORTH_WEST
;
506 menu_anchor
= GDK_GRAVITY_SOUTH_WEST
;
508 else if (nFlags
& FloatWinPopupFlags::Right
)
510 rect_anchor
= GDK_GRAVITY_NORTH_EAST
;
513 GdkSurface
* gdkWindow
= widget_get_surface(mpFrame
->getMouseEventWidget());
514 gtk_menu_popup_at_rect(GTK_MENU(mpMenuWidget
), gdkWindow
, &rect
, rect_anchor
, menu_anchor
, nullptr);
522 //typically there is an event, and we can then distinguish if this was
523 //launched from the keyboard (gets auto-mnemoniced) or the mouse (which
525 GdkEvent
*pEvent
= gtk_get_current_event();
528 gdk_event_get_button(pEvent
, &nButton
);
529 nTime
= gdk_event_get_time(pEvent
);
534 nTime
= GtkSalFrame::GetLastInputEventTime();
537 // do the same strange semantics as vcl popup windows to arrive at a frame geometry
538 // in mirrored UI case; best done by actually executing the same code
539 sal_uInt16 nArrangeIndex
;
540 Point aPos
= FloatingWindow::ImplCalcPos(pWin
, rRect
, nFlags
, nArrangeIndex
);
541 aPos
= FloatingWindow::ImplConvertToAbsPos(xParent
, aPos
);
543 gtk_menu_popup(GTK_MENU(mpMenuWidget
), nullptr, nullptr, MenuPositionFunc
,
544 &aPos
, nButton
, nTime
);
548 if (g_main_loop_is_running(pLoop
))
549 main_loop_run(pLoop
);
551 g_main_loop_unref(pLoop
);
553 mpVCLMenu
->Deactivate();
555 g_object_unref(mpActionGroup
);
556 ClearActionGroupAndMenuModel();
558 #if !GTK_CHECK_VERSION(4, 0, 0)
559 gtk_widget_destroy(mpMenuWidget
);
561 gtk_widget_unparent(mpMenuWidget
);
563 mpMenuWidget
= nullptr;
565 gtk_widget_insert_action_group(mpFrame
->getMouseEventWidget(), "win", nullptr);
567 // undo tooltip blocking
568 mpFrame
->UnblockTooltip();
579 GtkSalMenu::GtkSalMenu( bool bMenuBar
) :
580 maUpdateMenuBarIdle("Native Gtk Menu Update Idle"),
581 mbInActivateCallback( false ),
582 mbMenuBar( bMenuBar
),
583 mbNeedsUpdate( false ),
584 mbReturnFocusToDocument( false ),
585 mbAddedGrab( false ),
586 mpMenuBarContainerWidget( nullptr ),
587 mpMenuAllowShrinkWidget( nullptr ),
588 mpMenuBarWidget( nullptr ),
589 mpMenuWidget( nullptr ),
590 mpMenuBarContainerProvider( nullptr ),
591 mpMenuBarProvider( nullptr ),
592 mpCloseButton( nullptr ),
593 mpVCLMenu( nullptr ),
594 mpParentSalMenu( nullptr ),
596 mpMenuModel( nullptr ),
597 mpActionGroup( nullptr )
599 //typically this only gets called after the menu has been customized on the
600 //next idle slot, in the normal case of a new menubar SetFrame is called
601 //directly long before this idle would get called.
602 maUpdateMenuBarIdle
.SetPriority(TaskPriority::HIGHEST
);
603 maUpdateMenuBarIdle
.SetInvokeHandler(LINK(this, GtkSalMenu
, MenuBarHierarchyChangeHandler
));
606 IMPL_LINK_NOARG(GtkSalMenu
, MenuBarHierarchyChangeHandler
, Timer
*, void)
608 SAL_WARN_IF(!mpFrame
, "vcl.gtk", "MenuBar layout changed, but no frame for some reason!");
614 void GtkSalMenu::SetNeedsUpdate()
616 GtkSalMenu
* pMenu
= this;
617 // start that the menu and its parents are in need of an update
618 // on the next activation
619 while (pMenu
&& !pMenu
->mbNeedsUpdate
)
621 pMenu
->mbNeedsUpdate
= true;
622 pMenu
= pMenu
->mpParentSalMenu
;
624 // only if a menubar is directly updated do we force in a full
626 if (mbMenuBar
&& !maUpdateMenuBarIdle
.IsActive())
627 maUpdateMenuBarIdle
.Start();
630 void GtkSalMenu::SetMenuModel(GMenuModel
* pMenuModel
)
633 g_object_unref(mpMenuModel
);
634 mpMenuModel
= pMenuModel
;
636 g_object_ref(mpMenuModel
);
639 GtkSalMenu::~GtkSalMenu()
641 SolarMutexGuard aGuard
;
643 // tdf#140225 we expect all items to be removed by Menu::dispose
644 // before this dtor is called
645 assert(maItems
.empty());
647 DestroyMenuBarWidget();
650 g_object_unref(mpMenuModel
);
653 mpFrame
->SetMenu(nullptr);
656 bool GtkSalMenu::VisibleMenuBar()
658 return mbMenuBar
&& (bUnityMode
|| mpMenuBarContainerWidget
);
661 void GtkSalMenu::InsertItem( SalMenuItem
* pSalMenuItem
, unsigned nPos
)
663 SolarMutexGuard aGuard
;
664 GtkSalMenuItem
*pItem
= static_cast<GtkSalMenuItem
*>( pSalMenuItem
);
666 if ( nPos
== MENU_APPEND
)
667 maItems
.push_back( pItem
);
669 maItems
.insert( maItems
.begin() + nPos
, pItem
);
671 pItem
->mpParentMenu
= this;
676 void GtkSalMenu::RemoveItem( unsigned nPos
)
678 SolarMutexGuard aGuard
;
680 // tdf#140225 clear associated action when the item is removed
683 GLOActionGroup
* pActionGroup
= G_LO_ACTION_GROUP(mpActionGroup
);
684 gchar
* pCommand
= GetCommandForItem(maItems
[nPos
]);
685 g_lo_action_group_remove(pActionGroup
, pCommand
);
689 maItems
.erase( maItems
.begin() + nPos
);
693 void GtkSalMenu::SetSubMenu( SalMenuItem
* pSalMenuItem
, SalMenu
* pSubMenu
, unsigned )
695 SolarMutexGuard aGuard
;
696 GtkSalMenuItem
*pItem
= static_cast< GtkSalMenuItem
* >( pSalMenuItem
);
697 GtkSalMenu
*pGtkSubMenu
= static_cast< GtkSalMenu
* >( pSubMenu
);
699 if ( pGtkSubMenu
== nullptr )
702 pGtkSubMenu
->mpParentSalMenu
= this;
703 pItem
->mpSubMenu
= pGtkSubMenu
;
708 static void CloseMenuBar(GtkWidget
*, gpointer pMenu
)
710 Application::PostUserEvent(static_cast<MenuBar
*>(pMenu
)->GetCloseButtonClickHdl());
713 GtkWidget
* GtkSalMenu::AddButton(GtkWidget
*pImage
)
715 GtkWidget
* pButton
= gtk_button_new();
717 #if !GTK_CHECK_VERSION(4, 0, 0)
718 gtk_button_set_relief(GTK_BUTTON(pButton
), GTK_RELIEF_NONE
);
719 gtk_button_set_focus_on_click(GTK_BUTTON(pButton
), false);
721 gtk_button_set_has_frame(GTK_BUTTON(pButton
), false);
722 gtk_widget_set_focus_on_click(pButton
, false);
725 gtk_widget_set_can_focus(pButton
, false);
727 GtkStyleContext
*pButtonContext
= gtk_widget_get_style_context(GTK_WIDGET(pButton
));
729 gtk_style_context_add_class(pButtonContext
, "flat");
730 gtk_style_context_add_class(pButtonContext
, "small-button");
732 gtk_widget_show(pImage
);
734 gtk_widget_set_valign(pButton
, GTK_ALIGN_CENTER
);
736 #if !GTK_CHECK_VERSION(4, 0, 0)
737 gtk_container_add(GTK_CONTAINER(pButton
), pImage
);
738 gtk_widget_show_all(pButton
);
740 gtk_button_set_child(GTK_BUTTON(pButton
), pImage
);
745 void GtkSalMenu::ShowCloseButton(bool bShow
)
748 if (!mpMenuBarContainerWidget
)
755 #if !GTK_CHECK_VERSION(4, 0, 0)
756 gtk_widget_destroy(mpCloseButton
);
758 g_clear_pointer(&mpCloseButton
, gtk_widget_unparent
);
760 mpCloseButton
= nullptr;
768 GIcon
* pIcon
= g_themed_icon_new_with_default_fallbacks("window-close-symbolic");
769 #if !GTK_CHECK_VERSION(4, 0, 0)
770 GtkWidget
* pImage
= gtk_image_new_from_gicon(pIcon
, GTK_ICON_SIZE_MENU
);
772 GtkWidget
* pImage
= gtk_image_new_from_gicon(pIcon
);
774 g_object_unref(pIcon
);
776 mpCloseButton
= AddButton(pImage
);
778 gtk_widget_set_margin_end(mpCloseButton
, 8);
780 OUString
sToolTip(VclResId(SV_HELPTEXT_CLOSEDOCUMENT
));
781 gtk_widget_set_tooltip_text(mpCloseButton
, sToolTip
.toUtf8().getStr());
783 MenuBar
*pVclMenuBar
= static_cast<MenuBar
*>(mpVCLMenu
.get());
784 g_signal_connect(mpCloseButton
, "clicked", G_CALLBACK(CloseMenuBar
), pVclMenuBar
);
786 gtk_grid_attach(GTK_GRID(mpMenuBarContainerWidget
), mpCloseButton
, 1, 0, 1, 1);
791 void DestroyMemoryStream(gpointer data
)
793 SvMemoryStream
* pMemStm
= static_cast<SvMemoryStream
*>(data
);
798 static void MenuButtonClicked(GtkWidget
* pWidget
, gpointer pMenu
)
800 OUString
aId(get_buildable_id(GTK_BUILDABLE(pWidget
)));
801 static_cast<MenuBar
*>(pMenu
)->HandleMenuButtonEvent(aId
.toUInt32());
804 bool GtkSalMenu::AddMenuBarButton(const SalMenuButtonItem
& rNewItem
)
809 if (!mpMenuBarContainerWidget
)
812 GtkWidget
* pImage
= nullptr;
813 if (!!rNewItem
.maImage
)
815 SvMemoryStream
* pMemStm
= new SvMemoryStream
;
816 auto aBitmapEx
= rNewItem
.maImage
.GetBitmapEx();
817 vcl::PngImageWriter
aWriter(*pMemStm
);
818 aWriter
.write(aBitmapEx
);
820 GBytes
*pBytes
= g_bytes_new_with_free_func(pMemStm
->GetData(),
825 GIcon
*pIcon
= g_bytes_icon_new(pBytes
);
826 #if !GTK_CHECK_VERSION(4, 0, 0)
827 pImage
= gtk_image_new_from_gicon(pIcon
, GTK_ICON_SIZE_MENU
);
829 pImage
= gtk_image_new_from_gicon(pIcon
);
831 g_object_unref(pIcon
);
834 GtkWidget
* pButton
= AddButton(pImage
);
836 maExtraButtons
.emplace_back(rNewItem
.mnId
, pButton
);
838 set_buildable_id(GTK_BUILDABLE(pButton
), OUString::number(rNewItem
.mnId
));
840 gtk_widget_set_tooltip_text(pButton
, rNewItem
.maToolTipText
.toUtf8().getStr());
842 MenuBar
*pVclMenuBar
= static_cast<MenuBar
*>(mpVCLMenu
.get());
843 g_signal_connect(pButton
, "clicked", G_CALLBACK(MenuButtonClicked
), pVclMenuBar
);
847 gtk_grid_insert_next_to(GTK_GRID(mpMenuBarContainerWidget
), mpCloseButton
, GTK_POS_LEFT
);
848 gtk_grid_attach_next_to(GTK_GRID(mpMenuBarContainerWidget
), pButton
, mpCloseButton
,
852 gtk_grid_attach(GTK_GRID(mpMenuBarContainerWidget
), pButton
, 1, 0, 1, 1);
857 void GtkSalMenu::RemoveMenuBarButton( sal_uInt16 nId
)
859 const auto it
= std::find_if(maExtraButtons
.begin(), maExtraButtons
.end(), [&nId
](const auto &item
) {
860 return item
.first
== nId
; });
861 if (it
== maExtraButtons
.end())
865 #if !GTK_CHECK_VERSION(4, 0, 0)
866 gtk_container_child_get(GTK_CONTAINER(mpMenuBarContainerWidget
), it
->second
, "left-attach", &nAttach
, nullptr);
867 gtk_widget_destroy(it
->second
);
869 gtk_grid_query_child(GTK_GRID(mpMenuBarContainerWidget
), it
->second
, &nAttach
, nullptr, nullptr, nullptr);
870 g_clear_pointer(&(it
->second
), gtk_widget_unparent
);
872 gtk_grid_remove_column(GTK_GRID(mpMenuBarContainerWidget
), nAttach
);
873 maExtraButtons
.erase(it
);
876 tools::Rectangle
GtkSalMenu::GetMenuBarButtonRectPixel(sal_uInt16 nId
, SalFrame
* pReferenceFrame
)
878 if (!pReferenceFrame
)
879 return tools::Rectangle();
881 const auto it
= std::find_if(maExtraButtons
.begin(), maExtraButtons
.end(), [&nId
](const auto &item
) {
882 return item
.first
== nId
; });
883 if (it
== maExtraButtons
.end())
884 return tools::Rectangle();
886 GtkWidget
* pButton
= it
->second
;
888 GtkSalFrame
* pFrame
= static_cast<GtkSalFrame
*>(pReferenceFrame
);
891 if (!gtk_widget_translate_coordinates(pButton
, GTK_WIDGET(pFrame
->getMouseEventWidget()), 0, 0, &x
, &y
))
892 return tools::Rectangle();
894 return tools::Rectangle(Point(x
, y
), Size(gtk_widget_get_allocated_width(pButton
),
895 gtk_widget_get_allocated_height(pButton
)));
898 //Typically when the menubar is deactivated we want the focus to return
899 //to where it came from. If the menubar was activated because of F6
900 //moving focus into the associated VCL menubar then on pressing ESC
901 //or any other normal reason for deactivation we want focus to return
902 //to the document, definitely not still stuck in the associated
903 //VCL menubar. But if F6 is pressed while the menubar is activated
904 //we want to pass that F6 back to the VCL menubar which will move
905 //focus to the next pane by itself.
906 void GtkSalMenu::ReturnFocus()
910 #if !GTK_CHECK_VERSION(4, 0, 0)
911 gtk_grab_remove(mpMenuBarWidget
);
915 if (!mbReturnFocusToDocument
)
916 gtk_widget_grab_focus(mpFrame
->getMouseEventWidget());
918 mpFrame
->GetWindow()->GrabFocusToDocument();
919 mbReturnFocusToDocument
= false;
922 #if !GTK_CHECK_VERSION(4, 0, 0)
923 gboolean
GtkSalMenu::SignalKey(GdkEventKey
const * pEvent
)
925 if (pEvent
->keyval
== GDK_KEY_F6
)
927 mbReturnFocusToDocument
= false;
928 gtk_menu_shell_cancel(GTK_MENU_SHELL(mpMenuBarWidget
));
929 //because we return false here, the keypress will continue
930 //to propagate and in the case that vcl focus is in
931 //the vcl menubar then that will also process F6 and move
938 //The GtkSalMenu is owned by a Vcl Menu/MenuBar. In the menubar
939 //case the vcl menubar is present and "visible", but with a 0 height
940 //so it not apparent. Normally it acts as though it is not there when
941 //a Native menubar is active. If we return true here, then for keyboard
942 //activation and traversal with F6 through panes then the vcl menubar
943 //acts as though it *is* present and we translate its take focus and F6
944 //traversal key events into the gtk menubar equivalents.
945 bool GtkSalMenu::CanGetFocus() const
947 return mpMenuBarWidget
!= nullptr;
950 bool GtkSalMenu::TakeFocus()
952 if (!mpMenuBarWidget
)
955 #if !GTK_CHECK_VERSION(4, 0, 0)
956 //Send a keyboard event to the gtk menubar to let it know it has been
957 //activated via the keyboard. Doesn't do anything except cause the gtk
958 //menubar "keyboard_mode" member to get set to true, so typically mnemonics
959 //are shown which will serve as indication that the menubar has focus
960 //(given that we want to show it with no menus popped down)
961 GdkEvent
*event
= GtkSalFrame::makeFakeKeyPress(mpMenuBarWidget
);
962 gtk_widget_event(mpMenuBarWidget
, event
);
963 gdk_event_free(event
);
965 //this pairing results in a menubar with keyboard focus with no menus
967 gtk_grab_add(mpMenuBarWidget
);
970 gtk_menu_shell_select_first(GTK_MENU_SHELL(mpMenuBarWidget
), false);
971 gtk_menu_shell_deselect(GTK_MENU_SHELL(mpMenuBarWidget
));
973 mbReturnFocusToDocument
= true;
977 #if !GTK_CHECK_VERSION(4, 0, 0)
978 static void MenuBarReturnFocus(GtkMenuShell
*, gpointer menu
)
980 GtkSalFrame::UpdateLastInputEventTime(gtk_get_current_event_time());
981 GtkSalMenu
* pMenu
= static_cast<GtkSalMenu
*>(menu
);
982 pMenu
->ReturnFocus();
985 static gboolean
MenuBarSignalKey(GtkWidget
*, GdkEventKey
* pEvent
, gpointer menu
)
987 GtkSalMenu
* pMenu
= static_cast<GtkSalMenu
*>(menu
);
988 return pMenu
->SignalKey(pEvent
);
992 void GtkSalMenu::CreateMenuBarWidget()
994 if (mpMenuBarContainerWidget
)
997 GtkGrid
* pGrid
= mpFrame
->getTopLevelGridWidget();
998 mpMenuBarContainerWidget
= gtk_grid_new();
1000 gtk_widget_set_hexpand(GTK_WIDGET(mpMenuBarContainerWidget
), true);
1001 gtk_grid_insert_row(pGrid
, 0);
1002 gtk_grid_attach(pGrid
, mpMenuBarContainerWidget
, 0, 0, 1, 1);
1004 #if !GTK_CHECK_VERSION(4, 0, 0)
1005 mpMenuAllowShrinkWidget
= gtk_scrolled_window_new(nullptr, nullptr);
1006 gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(mpMenuAllowShrinkWidget
), GTK_SHADOW_NONE
);
1007 // tdf#129634 don't allow this scrolled window as a candidate to tab into
1008 gtk_widget_set_can_focus(GTK_WIDGET(mpMenuAllowShrinkWidget
), false);
1010 mpMenuAllowShrinkWidget
= gtk_scrolled_window_new();
1011 gtk_scrolled_window_set_has_frame(GTK_SCROLLED_WINDOW(mpMenuAllowShrinkWidget
), false);
1013 // tdf#116290 external policy on scrolledwindow will not show a scrollbar,
1014 // but still allow scrolled window to not be sized to the child content.
1015 // So the menubar can be shrunk past its nominal smallest width.
1016 // Unlike a hack using GtkFixed/GtkLayout the correct placement of the menubar occurs under RTL
1017 gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(mpMenuAllowShrinkWidget
), GTK_POLICY_EXTERNAL
, GTK_POLICY_NEVER
);
1018 gtk_grid_attach(GTK_GRID(mpMenuBarContainerWidget
), mpMenuAllowShrinkWidget
, 0, 0, 1, 1);
1020 #if !GTK_CHECK_VERSION(4, 0, 0)
1021 mpMenuBarWidget
= gtk_menu_bar_new_from_model(mpMenuModel
);
1023 mpMenuBarWidget
= gtk_popover_menu_bar_new_from_model(mpMenuModel
);
1026 gtk_widget_insert_action_group(mpMenuBarWidget
, "win", mpActionGroup
);
1027 gtk_widget_set_hexpand(GTK_WIDGET(mpMenuBarWidget
), true);
1028 gtk_widget_set_hexpand(mpMenuAllowShrinkWidget
, true);
1029 #if !GTK_CHECK_VERSION(4, 0, 0)
1030 gtk_container_add(GTK_CONTAINER(mpMenuAllowShrinkWidget
), mpMenuBarWidget
);
1032 gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(mpMenuAllowShrinkWidget
), mpMenuBarWidget
);
1035 #if !GTK_CHECK_VERSION(4, 0, 0)
1036 g_signal_connect(G_OBJECT(mpMenuBarWidget
), "deactivate", G_CALLBACK(MenuBarReturnFocus
), this);
1037 g_signal_connect(G_OBJECT(mpMenuBarWidget
), "key-press-event", G_CALLBACK(MenuBarSignalKey
), this);
1040 gtk_widget_show(mpMenuBarWidget
);
1041 gtk_widget_show(mpMenuAllowShrinkWidget
);
1042 gtk_widget_show(mpMenuBarContainerWidget
);
1044 ShowCloseButton( static_cast<MenuBar
*>(mpVCLMenu
.get())->HasCloseButton() );
1049 void GtkSalMenu::ApplyPersona()
1051 if (!mpMenuBarContainerWidget
)
1054 // I'm dubious about the persona theming feature, but as it exists, lets try and support
1055 // it, apply the image to the mpMenuBarContainerWidget
1056 const BitmapEx
& rPersonaBitmap
= Application::GetSettings().GetStyleSettings().GetPersonaHeader();
1058 GtkStyleContext
*pMenuBarContainerContext
= gtk_widget_get_style_context(GTK_WIDGET(mpMenuBarContainerWidget
));
1059 if (mpMenuBarContainerProvider
)
1061 gtk_style_context_remove_provider(pMenuBarContainerContext
, GTK_STYLE_PROVIDER(mpMenuBarContainerProvider
));
1062 mpMenuBarContainerProvider
= nullptr;
1064 GtkStyleContext
*pMenuBarContext
= gtk_widget_get_style_context(GTK_WIDGET(mpMenuBarWidget
));
1065 if (mpMenuBarProvider
)
1067 gtk_style_context_remove_provider(pMenuBarContext
, GTK_STYLE_PROVIDER(mpMenuBarProvider
));
1068 mpMenuBarProvider
= nullptr;
1071 if (!rPersonaBitmap
.IsEmpty())
1073 if (maPersonaBitmap
!= rPersonaBitmap
)
1075 mxPersonaImage
.reset(new utl::TempFileNamed
);
1076 mxPersonaImage
->EnableKillingFile(true);
1077 SvStream
* pStream
= mxPersonaImage
->GetStream(StreamMode::WRITE
);
1078 vcl::PngImageWriter
aPNGWriter(*pStream
);
1079 aPNGWriter
.write(rPersonaBitmap
);
1080 mxPersonaImage
->CloseStream();
1083 mpMenuBarContainerProvider
= gtk_css_provider_new();
1084 OUString aBuffer
= "* { background-image: url(\"" + mxPersonaImage
->GetURL() + "\"); background-position: top right; }";
1085 OString aResult
= OUStringToOString(aBuffer
, RTL_TEXTENCODING_UTF8
);
1086 css_provider_load_from_data(mpMenuBarContainerProvider
, aResult
.getStr(), aResult
.getLength());
1087 gtk_style_context_add_provider(pMenuBarContainerContext
, GTK_STYLE_PROVIDER(mpMenuBarContainerProvider
),
1088 GTK_STYLE_PROVIDER_PRIORITY_APPLICATION
);
1091 // force the menubar to be transparent when persona is active otherwise for
1092 // me the menubar becomes gray when its in the backdrop
1093 mpMenuBarProvider
= gtk_css_provider_new();
1094 static const gchar data
[] = "* { "
1095 "background-image: none;"
1096 "background-color: transparent;"
1098 css_provider_load_from_data(mpMenuBarProvider
, data
, -1);
1099 gtk_style_context_add_provider(pMenuBarContext
,
1100 GTK_STYLE_PROVIDER(mpMenuBarProvider
),
1101 GTK_STYLE_PROVIDER_PRIORITY_APPLICATION
);
1103 maPersonaBitmap
= rPersonaBitmap
;
1106 void GtkSalMenu::DestroyMenuBarWidget()
1108 if (!mpMenuBarContainerWidget
)
1111 #if !GTK_CHECK_VERSION(4, 0, 0)
1112 // tdf#140225 call cancel before destroying it in case there are some
1113 // active menus popped open
1114 gtk_menu_shell_cancel(GTK_MENU_SHELL(mpMenuBarWidget
));
1116 gtk_widget_destroy(mpMenuBarContainerWidget
);
1118 g_clear_pointer(&mpMenuBarContainerWidget
, gtk_widget_unparent
);
1120 mpMenuBarContainerWidget
= nullptr;
1121 mpMenuBarWidget
= nullptr;
1122 mpCloseButton
= nullptr;
1125 void GtkSalMenu::SetFrame(const SalFrame
* pFrame
)
1127 SolarMutexGuard aGuard
;
1129 SAL_INFO("vcl.unity", "GtkSalMenu set to frame");
1130 mpFrame
= const_cast<GtkSalFrame
*>(static_cast<const GtkSalFrame
*>(pFrame
));
1132 // if we had a menu on the GtkSalMenu we have to free it as we generate a
1133 // full menu anyway and we might need to reuse an existing model and
1135 mpFrame
->SetMenu( this );
1136 mpFrame
->EnsureAppMenuWatch();
1138 // Clean menu model and action group if needed.
1139 GtkWidget
* pWidget
= mpFrame
->getWindow();
1140 GdkSurface
* gdkWindow
= widget_get_surface(pWidget
);
1142 GLOMenu
* pMenuModel
= G_LO_MENU( g_object_get_data( G_OBJECT( gdkWindow
), "g-lo-menubar" ) );
1143 GLOActionGroup
* pActionGroup
= G_LO_ACTION_GROUP( g_object_get_data( G_OBJECT( gdkWindow
), "g-lo-action-group" ) );
1144 SAL_INFO("vcl.unity", "Found menu model: " << pMenuModel
<< " and action group: " << pActionGroup
);
1148 if ( g_menu_model_get_n_items( G_MENU_MODEL( pMenuModel
) ) > 0 )
1149 g_lo_menu_remove( pMenuModel
, 0 );
1151 mpMenuModel
= G_MENU_MODEL( g_lo_menu_new() );
1156 g_lo_action_group_clear( pActionGroup
);
1157 mpActionGroup
= G_ACTION_GROUP( pActionGroup
);
1160 // Generate the main menu structure.
1164 g_lo_menu_insert_section( pMenuModel
, 0, nullptr, mpMenuModel
);
1166 if (!bUnityMode
&& static_cast<MenuBar
*>(mpVCLMenu
.get())->IsDisplayable())
1168 DestroyMenuBarWidget();
1169 CreateMenuBarWidget();
1173 const GtkSalFrame
* GtkSalMenu::GetFrame() const
1175 SolarMutexGuard aGuard
;
1176 const GtkSalMenu
* pMenu
= this;
1177 while( pMenu
&& ! pMenu
->mpFrame
)
1178 pMenu
= pMenu
->mpParentSalMenu
;
1179 return pMenu
? pMenu
->mpFrame
: nullptr;
1182 void GtkSalMenu::NativeCheckItem( unsigned nSection
, unsigned nItemPos
, MenuItemBits bits
, gboolean bCheck
)
1184 SolarMutexGuard aGuard
;
1186 if ( mpActionGroup
== nullptr )
1189 gchar
* aCommand
= g_lo_menu_get_command_from_item_in_section( G_LO_MENU( mpMenuModel
), nSection
, nItemPos
);
1191 if ( aCommand
!= nullptr || g_strcmp0( aCommand
, "" ) != 0 )
1193 GVariant
*pCheckValue
= nullptr;
1194 GVariant
*pCurrentState
= g_action_group_get_action_state( mpActionGroup
, aCommand
);
1196 if ( bits
& MenuItemBits::RADIOCHECK
)
1197 pCheckValue
= bCheck
? g_variant_new_string( aCommand
) : g_variant_new_string( "" );
1200 // By default, all checked items are checkmark buttons.
1201 if (bCheck
|| pCurrentState
!= nullptr)
1202 pCheckValue
= g_variant_new_boolean( bCheck
);
1205 if ( pCheckValue
!= nullptr )
1207 if ( pCurrentState
== nullptr || g_variant_equal( pCurrentState
, pCheckValue
) == FALSE
)
1209 g_action_group_change_action_state( mpActionGroup
, aCommand
, pCheckValue
);
1213 g_variant_unref (pCheckValue
);
1217 if ( pCurrentState
!= nullptr )
1218 g_variant_unref( pCurrentState
);
1225 void GtkSalMenu::NativeSetEnableItem( gchar
const * aCommand
, gboolean bEnable
)
1227 SolarMutexGuard aGuard
;
1228 GLOActionGroup
* pActionGroup
= G_LO_ACTION_GROUP( mpActionGroup
);
1230 if ( g_action_group_get_action_enabled( G_ACTION_GROUP( pActionGroup
), aCommand
) != bEnable
)
1231 g_lo_action_group_set_action_enabled( pActionGroup
, aCommand
, bEnable
);
1234 void GtkSalMenu::NativeSetItemText( unsigned nSection
, unsigned nItemPos
, const OUString
& rText
)
1236 SolarMutexGuard aGuard
;
1237 // Escape all underscores so that they don't get interpreted as hotkeys
1238 OUString aText
= rText
.replaceAll( "_", "__" );
1239 // Replace the LibreOffice hotkey identifier with an underscore
1240 aText
= aText
.replace( '~', '_' );
1241 OString aConvertedText
= OUStringToOString( aText
, RTL_TEXTENCODING_UTF8
);
1243 // Update item text only when necessary.
1244 gchar
* aLabel
= g_lo_menu_get_label_from_item_in_section( G_LO_MENU( mpMenuModel
), nSection
, nItemPos
);
1246 if ( aLabel
== nullptr || g_strcmp0( aLabel
, aConvertedText
.getStr() ) != 0 )
1247 g_lo_menu_set_label_to_item_in_section( G_LO_MENU( mpMenuModel
), nSection
, nItemPos
, aConvertedText
.getStr() );
1253 void GtkSalMenu::NativeSetItemIcon( unsigned nSection
, unsigned nItemPos
, const Image
& rImage
)
1255 #if GLIB_CHECK_VERSION(2,38,0)
1256 if (!rImage
&& mbHasNullItemIcon
)
1259 SolarMutexGuard aGuard
;
1263 SvMemoryStream
* pMemStm
= new SvMemoryStream
;
1264 auto aBitmapEx
= rImage
.GetBitmapEx();
1265 vcl::PngImageWriter
aWriter(*pMemStm
);
1266 aWriter
.write(aBitmapEx
);
1268 GBytes
*pBytes
= g_bytes_new_with_free_func(pMemStm
->GetData(),
1270 DestroyMemoryStream
,
1273 GIcon
*pIcon
= g_bytes_icon_new(pBytes
);
1275 g_lo_menu_set_icon_to_item_in_section( G_LO_MENU( mpMenuModel
), nSection
, nItemPos
, pIcon
);
1276 g_object_unref(pIcon
);
1277 g_bytes_unref(pBytes
);
1278 mbHasNullItemIcon
= false;
1282 g_lo_menu_set_icon_to_item_in_section( G_LO_MENU( mpMenuModel
), nSection
, nItemPos
, nullptr );
1283 mbHasNullItemIcon
= true;
1292 void GtkSalMenu::NativeSetAccelerator( unsigned nSection
, unsigned nItemPos
, const vcl::KeyCode
& rKeyCode
, std::u16string_view rKeyName
)
1294 SolarMutexGuard aGuard
;
1296 if ( rKeyName
.empty() )
1300 GdkModifierType nModifiers
;
1301 GtkSalFrame::KeyCodeToGdkKey(rKeyCode
, &nKeyCode
, &nModifiers
);
1303 gchar
* aAccelerator
= gtk_accelerator_name( nKeyCode
, nModifiers
);
1305 gchar
* aCurrentAccel
= g_lo_menu_get_accelerator_from_item_in_section( G_LO_MENU( mpMenuModel
), nSection
, nItemPos
);
1307 if ( aCurrentAccel
== nullptr && g_strcmp0( aCurrentAccel
, aAccelerator
) != 0 )
1308 g_lo_menu_set_accelerator_to_item_in_section ( G_LO_MENU( mpMenuModel
), nSection
, nItemPos
, aAccelerator
);
1310 g_free( aAccelerator
);
1311 g_free( aCurrentAccel
);
1314 bool GtkSalMenu::NativeSetItemCommand( unsigned nSection
,
1317 const gchar
* aCommand
,
1322 bool bSubMenuAddedOrRemoved
= false;
1324 SolarMutexGuard aGuard
;
1325 GLOActionGroup
* pActionGroup
= G_LO_ACTION_GROUP( mpActionGroup
);
1327 GVariant
*pTarget
= nullptr;
1329 if (g_action_group_has_action(mpActionGroup
, aCommand
))
1330 g_lo_action_group_remove(pActionGroup
, aCommand
);
1332 if ( ( nBits
& MenuItemBits::CHECKABLE
) || bIsSubmenu
)
1334 // Item is a checkmark button.
1335 GVariantType
* pStateType
= g_variant_type_new( reinterpret_cast<gchar
const *>(G_VARIANT_TYPE_BOOLEAN
) );
1336 GVariant
* pState
= g_variant_new_boolean( bChecked
);
1338 g_lo_action_group_insert_stateful( pActionGroup
, aCommand
, nId
, bIsSubmenu
, nullptr, pStateType
, nullptr, pState
);
1340 else if ( nBits
& MenuItemBits::RADIOCHECK
)
1342 // Item is a radio button.
1343 GVariantType
* pParameterType
= g_variant_type_new( reinterpret_cast<gchar
const *>(G_VARIANT_TYPE_STRING
) );
1344 GVariantType
* pStateType
= g_variant_type_new( reinterpret_cast<gchar
const *>(G_VARIANT_TYPE_STRING
) );
1345 GVariant
* pState
= g_variant_new_string( "" );
1346 pTarget
= g_variant_new_string( aCommand
);
1348 g_lo_action_group_insert_stateful( pActionGroup
, aCommand
, nId
, FALSE
, pParameterType
, pStateType
, nullptr, pState
);
1352 // Item is not special, so insert a stateless action.
1353 g_lo_action_group_insert( pActionGroup
, aCommand
, nId
, FALSE
);
1356 GLOMenu
* pMenu
= G_LO_MENU( mpMenuModel
);
1358 // Menu item is not updated unless it's necessary.
1359 gchar
* aCurrentCommand
= g_lo_menu_get_command_from_item_in_section( pMenu
, nSection
, nItemPos
);
1361 if ( aCurrentCommand
== nullptr || g_strcmp0( aCurrentCommand
, aCommand
) != 0 )
1363 bool bOldHasSubmenu
= g_lo_menu_get_submenu_from_item_in_section(pMenu
, nSection
, nItemPos
) != nullptr;
1364 bSubMenuAddedOrRemoved
= bOldHasSubmenu
!= bIsSubmenu
;
1365 if (bSubMenuAddedOrRemoved
)
1367 //tdf#98636 it's not good enough to unset the "submenu-action" attribute to change something
1368 //from a submenu to a non-submenu item, so remove the old one entirely and re-add it to
1369 //support achieving that
1370 gchar
* pLabel
= g_lo_menu_get_label_from_item_in_section(pMenu
, nSection
, nItemPos
);
1371 g_lo_menu_remove_from_section(pMenu
, nSection
, nItemPos
);
1372 g_lo_menu_insert_in_section(pMenu
, nSection
, nItemPos
, pLabel
);
1376 g_lo_menu_set_command_to_item_in_section( pMenu
, nSection
, nItemPos
, aCommand
);
1378 gchar
* aItemCommand
= g_strconcat("win.", aCommand
, nullptr );
1381 g_lo_menu_set_submenu_action_to_item_in_section( pMenu
, nSection
, nItemPos
, aItemCommand
);
1384 g_lo_menu_set_action_and_target_value_to_item_in_section( pMenu
, nSection
, nItemPos
, aItemCommand
, pTarget
);
1388 g_free( aItemCommand
);
1391 if ( aCurrentCommand
)
1392 g_free( aCurrentCommand
);
1395 g_variant_unref(pTarget
);
1397 return bSubMenuAddedOrRemoved
;
1400 GtkSalMenu
* GtkSalMenu::GetTopLevel()
1402 GtkSalMenu
*pMenu
= this;
1403 while (pMenu
->mpParentSalMenu
)
1404 pMenu
= pMenu
->mpParentSalMenu
;
1408 void GtkSalMenu::DispatchCommand(const gchar
*pCommand
)
1410 SolarMutexGuard aGuard
;
1411 MenuAndId aMenuAndId
= decode_command(pCommand
);
1412 GtkSalMenu
* pSalSubMenu
= aMenuAndId
.first
;
1413 GtkSalMenu
* pTopLevel
= pSalSubMenu
->GetTopLevel();
1415 // tdf#125803 spacebar will toggle radios and checkbuttons without automatically
1416 // closing the menu. To handle this properly I imagine we need to set groups for the
1417 // radiobuttons so the others visually untoggle when the active one is toggled and
1418 // we would further need to teach vcl that the state can change more than once.
1420 // or we could unconditionally deactivate the menus if regardless of what particular
1421 // type of menu item got activated
1422 if (pTopLevel
->mpMenuBarWidget
)
1424 #if !GTK_CHECK_VERSION(4, 0, 0)
1425 gtk_menu_shell_deactivate(GTK_MENU_SHELL(pTopLevel
->mpMenuBarWidget
));
1428 if (pTopLevel
->mpMenuWidget
)
1430 #if GTK_CHECK_VERSION(4, 0, 0)
1431 gtk_popover_popdown(GTK_POPOVER(pTopLevel
->mpMenuWidget
));
1433 gtk_menu_shell_deactivate(GTK_MENU_SHELL(pTopLevel
->mpMenuWidget
));
1437 pTopLevel
->GetMenu()->HandleMenuCommandEvent(pSalSubMenu
->GetMenu(), aMenuAndId
.second
);
1440 void GtkSalMenu::ActivateAllSubmenus(Menu
* pMenuBar
)
1442 // We can re-enter this method via the new event loop that gets created
1443 // in GtkClipboardTransferable::getTransferDataFlavorsAsVector, so use the InActivateCallback
1444 // flag to detect that and skip some startup work.
1445 if (mbInActivateCallback
)
1448 mbInActivateCallback
= true;
1449 pMenuBar
->HandleMenuActivateEvent(GetMenu());
1450 mbInActivateCallback
= false;
1451 for (GtkSalMenuItem
* pSalItem
: maItems
)
1453 if ( pSalItem
->mpSubMenu
!= nullptr )
1455 pSalItem
->mpSubMenu
->ActivateAllSubmenus(pMenuBar
);
1459 pMenuBar
->HandleMenuDeActivateEvent(GetMenu());
1462 void GtkSalMenu::ClearActionGroupAndMenuModel()
1464 SetMenuModel(nullptr);
1465 mpActionGroup
= nullptr;
1466 for (GtkSalMenuItem
* pSalItem
: maItems
)
1468 if ( pSalItem
->mpSubMenu
!= nullptr )
1470 pSalItem
->mpSubMenu
->ClearActionGroupAndMenuModel();
1475 void GtkSalMenu::Activate(const gchar
* pCommand
)
1477 MenuAndId aMenuAndId
= decode_command(pCommand
);
1478 GtkSalMenu
* pSalMenu
= aMenuAndId
.first
;
1479 Menu
* pVclMenu
= pSalMenu
->GetMenu();
1480 if (pVclMenu
->isDisposed())
1482 GtkSalMenu
* pTopLevel
= pSalMenu
->GetTopLevel();
1483 Menu
* pVclSubMenu
= pVclMenu
->GetPopupMenu(aMenuAndId
.second
);
1484 GtkSalMenu
* pSubMenu
= pSalMenu
->GetItemAtPos(pVclMenu
->GetItemPos(aMenuAndId
.second
))->mpSubMenu
;
1486 pSubMenu
->mbInActivateCallback
= true;
1487 pTopLevel
->GetMenu()->HandleMenuActivateEvent(pVclSubMenu
);
1488 pSubMenu
->mbInActivateCallback
= false;
1489 pVclSubMenu
->UpdateNativeMenu();
1492 void GtkSalMenu::Deactivate(const gchar
* pCommand
)
1494 MenuAndId aMenuAndId
= decode_command(pCommand
);
1495 GtkSalMenu
* pSalMenu
= aMenuAndId
.first
;
1496 Menu
* pVclMenu
= pSalMenu
->GetMenu();
1497 if (pVclMenu
->isDisposed())
1499 GtkSalMenu
* pTopLevel
= pSalMenu
->GetTopLevel();
1500 Menu
* pVclSubMenu
= pVclMenu
->GetPopupMenu(aMenuAndId
.second
);
1501 pTopLevel
->GetMenu()->HandleMenuDeActivateEvent(pVclSubMenu
);
1504 void GtkSalMenu::EnableUnity(bool bEnable
)
1506 bUnityMode
= bEnable
;
1508 MenuBar
* pMenuBar(static_cast<MenuBar
*>(mpVCLMenu
.get()));
1509 bool bDisplayable(pMenuBar
->IsDisplayable());
1513 DestroyMenuBarWidget();
1521 ShowMenuBar(bDisplayable
);
1524 pMenuBar
->LayoutChanged();
1527 void GtkSalMenu::ShowMenuBar( bool bVisible
)
1529 // Unity tdf#106271: Can't hide global menu, so empty it instead when user wants to hide menubar,
1534 else if (mpMenuModel
&& g_menu_model_get_n_items(G_MENU_MODEL(mpMenuModel
)) > 0)
1535 g_lo_menu_remove(G_LO_MENU(mpMenuModel
), 0);
1538 CreateMenuBarWidget();
1540 DestroyMenuBarWidget();
1543 bool GtkSalMenu::IsItemVisible( unsigned nPos
)
1545 SolarMutexGuard aGuard
;
1546 bool bVisible
= false;
1548 if ( nPos
< maItems
.size() )
1549 bVisible
= maItems
[ nPos
]->mbVisible
;
1554 void GtkSalMenu::CheckItem( unsigned, bool )
1558 void GtkSalMenu::EnableItem( unsigned nPos
, bool bEnable
)
1560 SolarMutexGuard aGuard
;
1561 if ( bUnityMode
&& !mbInActivateCallback
&& !mbNeedsUpdate
&& GetTopLevel()->mbMenuBar
&& ( nPos
< maItems
.size() ) )
1563 gchar
* pCommand
= GetCommandForItem( GetItemAtPos( nPos
) );
1564 NativeSetEnableItem( pCommand
, bEnable
);
1569 void GtkSalMenu::ShowItem( unsigned nPos
, bool bShow
)
1571 SolarMutexGuard aGuard
;
1572 if ( nPos
< maItems
.size() )
1574 maItems
[ nPos
]->mbVisible
= bShow
;
1575 if ( bUnityMode
&& !mbInActivateCallback
&& !mbNeedsUpdate
&& GetTopLevel()->mbMenuBar
)
1580 void GtkSalMenu::SetItemText( unsigned nPos
, SalMenuItem
* pSalMenuItem
, const OUString
& rText
)
1582 SolarMutexGuard aGuard
;
1583 if ( !bUnityMode
|| mbInActivateCallback
|| mbNeedsUpdate
|| !GetTopLevel()->mbMenuBar
|| ( nPos
>= maItems
.size() ) )
1586 gchar
* pCommand
= GetCommandForItem( static_cast< GtkSalMenuItem
* >( pSalMenuItem
) );
1588 gint nSectionsCount
= g_menu_model_get_n_items( mpMenuModel
);
1589 for ( gint nSection
= 0; nSection
< nSectionsCount
; ++nSection
)
1591 gint nItemsCount
= g_lo_menu_get_n_items_from_section( G_LO_MENU( mpMenuModel
), nSection
);
1592 for ( gint nItem
= 0; nItem
< nItemsCount
; ++nItem
)
1594 gchar
* pCommandFromModel
= g_lo_menu_get_command_from_item_in_section( G_LO_MENU( mpMenuModel
), nSection
, nItem
);
1596 if ( !g_strcmp0( pCommandFromModel
, pCommand
) )
1598 NativeSetItemText( nSection
, nItem
, rText
);
1599 g_free( pCommandFromModel
);
1604 g_free( pCommandFromModel
);
1611 void GtkSalMenu::SetItemImage( unsigned, SalMenuItem
*, const Image
& )
1615 void GtkSalMenu::SetAccelerator( unsigned, SalMenuItem
*, const vcl::KeyCode
&, const OUString
& )
1619 void GtkSalMenu::GetSystemMenuData( SystemMenuData
* )
1623 int GtkSalMenu::GetMenuBarHeight() const
1625 return mpMenuBarWidget
? gtk_widget_get_allocated_height(mpMenuBarWidget
) : 0;
1632 GtkSalMenuItem::GtkSalMenuItem( const SalItemParams
* pItemData
) :
1633 mpParentMenu( nullptr ),
1634 mpSubMenu( nullptr ),
1635 mnType( pItemData
->eType
),
1636 mnId( pItemData
->nId
),
1641 GtkSalMenuItem::~GtkSalMenuItem()
1645 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */