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 OString
GetCommandForItem(GtkSalMenu
* pParentMenu
, sal_uInt16 nItemId
)
33 OString aCommand
= "window-" +
34 OString::number(reinterpret_cast<sal_uIntPtr
>(pParentMenu
)) +
35 "-" + OString::number(nItemId
);
39 static OString
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;
128 g_object_unref(pSubMenuModel
);
134 //but tdf#86850 Always display clipboard functions
135 bRemove
= g_strcmp0(pCommand
, ".uno:Cut") &&
136 g_strcmp0(pCommand
, ".uno:Copy") &&
137 g_strcmp0(pCommand
, ".uno:Paste");
142 if (pCommand
!= nullptr && pOldCommandList
!= nullptr)
143 *pOldCommandList
= g_list_append(*pOldCommandList
, g_strdup(pCommand
));
144 g_lo_menu_remove_from_section(pMenu
, nSection
, nSectionItems
);
153 static void RemoveSpareSectionsFromNativeMenu( GLOMenu
* pMenu
, GList
** pOldCommandList
, sal_Int32 nLastSection
)
155 if ( pMenu
== nullptr || pOldCommandList
== nullptr )
158 sal_Int32 n
= g_menu_model_get_n_items( G_MENU_MODEL( pMenu
) ) - 1;
160 for ( ; n
> nLastSection
; n
--)
162 RemoveSpareItemsFromNativeMenu( pMenu
, pOldCommandList
, n
, 0 );
163 g_lo_menu_remove( pMenu
, n
);
167 static gint
CompareStr( gpointer str1
, gpointer str2
)
169 return g_strcmp0( static_cast<const gchar
*>(str1
), static_cast<const gchar
*>(str2
) );
172 static void RemoveUnusedCommands( GLOActionGroup
* pActionGroup
, GList
* pOldCommandList
, GList
* pNewCommandList
)
174 if ( pActionGroup
== nullptr || pOldCommandList
== nullptr )
176 g_list_free_full( pOldCommandList
, g_free
);
177 g_list_free_full( pNewCommandList
, g_free
);
181 while ( pNewCommandList
!= nullptr )
183 GList
* pNewCommand
= g_list_first( pNewCommandList
);
184 pNewCommandList
= g_list_remove_link( pNewCommandList
, pNewCommand
);
186 gpointer aCommand
= g_list_nth_data( pNewCommand
, 0 );
188 GList
* pOldCommand
= g_list_find_custom( pOldCommandList
, aCommand
, reinterpret_cast<GCompareFunc
>(CompareStr
) );
190 if ( pOldCommand
!= nullptr )
192 pOldCommandList
= g_list_remove_link( pOldCommandList
, pOldCommand
);
193 g_list_free_full( pOldCommand
, g_free
);
196 g_list_free_full( pNewCommand
, g_free
);
199 while ( pOldCommandList
!= nullptr )
201 GList
* pCommand
= g_list_first( pOldCommandList
);
202 pOldCommandList
= g_list_remove_link( pOldCommandList
, pCommand
);
204 gchar
* aCommand
= static_cast<gchar
*>(g_list_nth_data( pCommand
, 0 ));
206 g_lo_action_group_remove( pActionGroup
, aCommand
);
208 g_list_free_full( pCommand
, g_free
);
212 void GtkSalMenu::ImplUpdate(bool bRecurse
, bool bRemoveDisabledEntries
)
214 SolarMutexGuard aGuard
;
216 SAL_INFO("vcl.unity", "ImplUpdate pre PrepUpdate");
222 mbNeedsUpdate
= false;
223 if (mbMenuBar
&& maUpdateMenuBarIdle
.IsActive())
225 maUpdateMenuBarIdle
.Stop();
226 // tdf#124391 Prevent doubled menus in global menu
229 maUpdateMenuBarIdle
.Invoke();
235 Menu
* pVCLMenu
= mpVCLMenu
;
236 GLOMenu
* pLOMenu
= G_LO_MENU( mpMenuModel
);
237 GLOActionGroup
* pActionGroup
= G_LO_ACTION_GROUP( mpActionGroup
);
238 SAL_INFO("vcl.unity", "Syncing vcl menu " << pVCLMenu
<< " to menu model " << pLOMenu
<< " and action group " << pActionGroup
);
239 GList
*pOldCommandList
= nullptr;
240 GList
*pNewCommandList
= nullptr;
242 sal_uInt16 nLOMenuSize
= g_menu_model_get_n_items( G_MENU_MODEL( pLOMenu
) );
244 if ( nLOMenuSize
== 0 )
245 g_lo_menu_new_section( pLOMenu
, 0, nullptr );
247 sal_Int32 nSection
= 0;
248 sal_Int32 nItemPos
= 0;
249 sal_Int32 validItems
= 0;
252 for ( nItem
= 0; nItem
< static_cast<sal_Int32
>(GetItemCount()); nItem
++ ) {
253 if ( !IsItemVisible( nItem
) )
256 GtkSalMenuItem
*pSalMenuItem
= GetItemAtPos( nItem
);
257 sal_uInt16 nId
= pSalMenuItem
->mnId
;
259 // PopupMenu::ImplExecute might add <No Selection Possible> entry to top-level
260 // popup menu, but we have our own implementation below, so skip that one.
264 if ( pSalMenuItem
->mnType
== MenuItemType::SEPARATOR
)
266 // Delete extra items from current section.
267 RemoveSpareItemsFromNativeMenu( pLOMenu
, &pOldCommandList
, nSection
, validItems
);
273 if ( nLOMenuSize
<= nSection
)
275 g_lo_menu_new_section( pLOMenu
, nSection
, nullptr );
282 if ( nItemPos
>= g_lo_menu_get_n_items_from_section( pLOMenu
, nSection
) )
283 g_lo_menu_insert_in_section( pLOMenu
, nSection
, nItemPos
, "EMPTY STRING" );
285 // Get internal menu item values.
286 OUString aText
= pVCLMenu
->GetItemText( nId
);
287 Image aImage
= pVCLMenu
->GetItemImage( nId
);
288 bool bEnabled
= pVCLMenu
->IsItemEnabled( nId
);
289 vcl::KeyCode nAccelKey
= pVCLMenu
->GetAccelKey( nId
);
290 bool bChecked
= pVCLMenu
->IsItemChecked( nId
);
291 MenuItemBits itemBits
= pVCLMenu
->GetItemBits( nId
);
293 // Store current item command in command list.
294 gchar
*aCurrentCommand
= g_lo_menu_get_command_from_item_in_section( pLOMenu
, nSection
, nItemPos
);
296 if ( aCurrentCommand
!= nullptr )
297 pOldCommandList
= g_list_append( pOldCommandList
, aCurrentCommand
);
299 // Get the new command for the item.
300 OString sNativeCommand
= GetCommandForItem(pSalMenuItem
);
302 // Force updating of native menu labels.
304 if (!sNativeCommand
.isEmpty() && pSalMenuItem
->mpSubMenu
== nullptr)
306 NativeSetItemText( nSection
, nItemPos
, aText
, false );
307 NativeSetItemIcon( nSection
, nItemPos
, aImage
);
308 NativeSetAccelerator(nSection
, nItemPos
, nAccelKey
, nAccelKey
.GetName());
309 NativeSetItemCommand(nSection
, nItemPos
, nId
, sNativeCommand
.getStr(), itemBits
, bChecked
, false);
310 NativeCheckItem( nSection
, nItemPos
, itemBits
, bChecked
);
311 NativeSetEnableItem(sNativeCommand
, bEnabled
);
313 pNewCommandList
= g_list_append(pNewCommandList
, g_strdup(sNativeCommand
.getStr()));
317 NativeSetItemText( nSection
, nItemPos
, aText
);
318 NativeSetItemIcon( nSection
, nItemPos
, aImage
);
319 NativeSetAccelerator(nSection
, nItemPos
, nAccelKey
, nAccelKey
.GetName());
322 GtkSalMenu
* pSubmenu
= pSalMenuItem
->mpSubMenu
;
324 if ( pSubmenu
&& pSubmenu
->GetMenu() )
326 bool bNonMenuChangedToMenu
= NativeSetItemCommand(nSection
, nItemPos
, nId
, sNativeCommand
.getStr(), itemBits
, false, true);
327 pNewCommandList
= g_list_append(pNewCommandList
, g_strdup(sNativeCommand
.getStr()));
329 GLOMenu
* pSubMenuModel
= g_lo_menu_get_submenu_from_item_in_section( pLOMenu
, nSection
, nItemPos
);
331 if ( pSubMenuModel
== nullptr )
333 g_lo_menu_new_submenu_in_item_in_section( pLOMenu
, nSection
, nItemPos
);
334 pSubMenuModel
= g_lo_menu_get_submenu_from_item_in_section( pLOMenu
, nSection
, nItemPos
);
337 assert(pSubMenuModel
);
339 if (bRecurse
|| bNonMenuChangedToMenu
)
341 SAL_INFO("vcl.unity", "preparing submenu " << pSubMenuModel
<< " to menu model " << G_MENU_MODEL(pSubMenuModel
) << " and action group " << G_ACTION_GROUP(pActionGroup
));
342 pSubmenu
->SetMenuModel( G_MENU_MODEL( pSubMenuModel
) );
343 pSubmenu
->SetActionGroup( G_ACTION_GROUP( pActionGroup
) );
344 pSubmenu
->ImplUpdate(true, bRemoveDisabledEntries
);
347 g_object_unref( pSubMenuModel
);
354 if (bRemoveDisabledEntries
)
356 // Delete disabled items in last section.
357 RemoveDisabledItemsFromNativeMenu(pLOMenu
, &pOldCommandList
, nSection
, G_ACTION_GROUP(pActionGroup
));
360 // Delete extra items in last section.
361 RemoveSpareItemsFromNativeMenu( pLOMenu
, &pOldCommandList
, nSection
, validItems
);
363 // Delete extra sections.
364 RemoveSpareSectionsFromNativeMenu( pLOMenu
, &pOldCommandList
, nSection
);
366 // Delete unused commands.
367 RemoveUnusedCommands( pActionGroup
, pOldCommandList
, pNewCommandList
);
369 // Resolves: tdf#103166 if the menu is empty, add a disabled
370 // <No Selection Possible> placeholder.
371 sal_Int32 nSectionsCount
= g_menu_model_get_n_items(G_MENU_MODEL(pLOMenu
));
372 gint nItemsCount
= 0;
373 for (nSection
= 0; nSection
< nSectionsCount
; ++nSection
)
375 nItemsCount
+= g_lo_menu_get_n_items_from_section(pLOMenu
, nSection
);
381 OString sNativeCommand
= GetCommandForItem(this, 0xFFFF);
382 OUString
aPlaceholderText(VclResId(SV_RESID_STRING_NOSELECTIONPOSSIBLE
));
383 g_lo_menu_insert_in_section(pLOMenu
, nSection
-1, 0,
384 OUStringToOString(aPlaceholderText
, RTL_TEXTENCODING_UTF8
).getStr());
385 NativeSetItemCommand(nSection
- 1, 0, 0xFFFF, sNativeCommand
.getStr(), MenuItemBits::NONE
, false, false);
386 NativeSetEnableItem(sNativeCommand
, false);
390 void GtkSalMenu::Update()
392 //find out if top level is a menubar or not, if not, then it's a popup menu
393 //hierarchy and in those we hide (most) disabled entries
394 const GtkSalMenu
* pMenu
= this;
395 while (pMenu
->mpParentSalMenu
)
396 pMenu
= pMenu
->mpParentSalMenu
;
398 bool bAlwaysShowDisabledEntries
;
399 if (pMenu
->mbMenuBar
)
400 bAlwaysShowDisabledEntries
= !bool(mpVCLMenu
->GetMenuFlags() & MenuFlags::HideDisabledEntries
);
402 bAlwaysShowDisabledEntries
= bool(mpVCLMenu
->GetMenuFlags() & MenuFlags::AlwaysShowDisabledEntries
);
404 ImplUpdate(false, !bAlwaysShowDisabledEntries
);
407 #if !GTK_CHECK_VERSION(4, 0, 0)
408 static void MenuPositionFunc(GtkMenu
* menu
, gint
* x
, gint
* y
, gboolean
* push_in
, gpointer user_data
)
410 Point
*pPos
= static_cast<Point
*>(user_data
);
412 if (gtk_widget_get_default_direction() == GTK_TEXT_DIR_RTL
)
414 GtkRequisition natural_size
;
415 gtk_widget_get_preferred_size(GTK_WIDGET(menu
), nullptr, &natural_size
);
416 *x
-= natural_size
.width
;
423 static void MenuClosed(GtkPopover
* pWidget
, GMainLoop
* pLoop
)
425 // gtk4 4.4.0: click on an entry in a submenu of a menu crashes without this workaround
426 gtk_widget_grab_focus(gtk_widget_get_parent(GTK_WIDGET(pWidget
)));
427 g_main_loop_quit(pLoop
);
430 bool GtkSalMenu::ShowNativePopupMenu(FloatingWindow
* pWin
, const tools::Rectangle
& rRect
,
431 FloatWinPopupFlags nFlags
)
433 VclPtr
<vcl::Window
> xParent
= pWin
->ImplGetWindowImpl()->mpRealParent
;
434 mpFrame
= static_cast<GtkSalFrame
*>(xParent
->ImplGetFrame());
436 GLOActionGroup
* pActionGroup
= g_lo_action_group_new();
437 mpActionGroup
= G_ACTION_GROUP(pActionGroup
);
438 mpMenuModel
= G_MENU_MODEL(g_lo_menu_new());
439 // Generate the main menu structure, populates mpMenuModel
442 #if !GTK_CHECK_VERSION(4, 0, 0)
443 mpMenuWidget
= gtk_menu_new_from_model(mpMenuModel
);
444 gtk_menu_attach_to_widget(GTK_MENU(mpMenuWidget
), mpFrame
->getMouseEventWidget(), nullptr);
446 mpMenuWidget
= gtk_popover_menu_new_from_model(mpMenuModel
);
447 gtk_widget_set_parent(mpMenuWidget
, mpFrame
->getMouseEventWidget());
448 gtk_popover_set_has_arrow(GTK_POPOVER(mpMenuWidget
), false);
450 gtk_widget_insert_action_group(mpFrame
->getMouseEventWidget(), "win", mpActionGroup
);
452 //run in a sub main loop because we need to keep vcl PopupMenu alive to use
453 //it during DispatchCommand, returning now to the outer loop causes the
454 //launching PopupMenu to be destroyed, instead run the subloop here
455 //until the gtk menu is destroyed
456 GMainLoop
* pLoop
= g_main_loop_new(nullptr, true);
457 #if GTK_CHECK_VERSION(4, 0, 0)
458 g_signal_connect(G_OBJECT(mpMenuWidget
), "closed", G_CALLBACK(MenuClosed
), pLoop
);
460 g_signal_connect(G_OBJECT(mpMenuWidget
), "deactivate", G_CALLBACK(MenuClosed
), pLoop
);
464 // tdf#120764 It isn't allowed under wayland to have two visible popups that share
465 // the same top level parent. The problem is that since gtk 3.24 tooltips are also
466 // implemented as popups, which means that we cannot show any popup if there is a
468 // hide any current tooltip
469 mpFrame
->HideTooltip();
470 // don't allow any more to appear until menu is dismissed
471 mpFrame
->BlockTooltip();
473 #if GTK_CHECK_VERSION(4, 0, 0)
474 AbsoluteScreenPixelRectangle aFloatRect
= FloatingWindow::ImplConvertToAbsPos(xParent
, rRect
);
475 aFloatRect
.Move(-mpFrame
->GetUnmirroredGeometry().x(), -mpFrame
->GetUnmirroredGeometry().y());
476 GdkRectangle rect
{static_cast<int>(aFloatRect
.Left()), static_cast<int>(aFloatRect
.Top()),
477 static_cast<int>(aFloatRect
.GetWidth()), static_cast<int>(aFloatRect
.GetHeight())};
479 gtk_popover_set_pointing_to(GTK_POPOVER(mpMenuWidget
), &rect
);
481 if (nFlags
& FloatWinPopupFlags::Left
)
482 gtk_popover_set_position(GTK_POPOVER(mpMenuWidget
), GTK_POS_LEFT
);
483 else if (nFlags
& FloatWinPopupFlags::Up
)
484 gtk_popover_set_position(GTK_POPOVER(mpMenuWidget
), GTK_POS_TOP
);
485 else if (nFlags
& FloatWinPopupFlags::Right
)
486 gtk_popover_set_position(GTK_POPOVER(mpMenuWidget
), GTK_POS_RIGHT
);
488 gtk_popover_set_position(GTK_POPOVER(mpMenuWidget
), GTK_POS_BOTTOM
);
490 gtk_popover_popup(GTK_POPOVER(mpMenuWidget
));
492 #if GTK_CHECK_VERSION(3,22,0)
493 if (gtk_check_version(3, 22, 0) == nullptr)
495 AbsoluteScreenPixelRectangle aFloatRect
= FloatingWindow::ImplConvertToAbsPos(xParent
, rRect
);
496 aFloatRect
.Move(-mpFrame
->GetUnmirroredGeometry().x(), -mpFrame
->GetUnmirroredGeometry().y());
497 GdkRectangle rect
{static_cast<int>(aFloatRect
.Left()), static_cast<int>(aFloatRect
.Top()),
498 static_cast<int>(aFloatRect
.GetWidth()), static_cast<int>(aFloatRect
.GetHeight())};
500 GdkGravity rect_anchor
= GDK_GRAVITY_SOUTH_WEST
, menu_anchor
= GDK_GRAVITY_NORTH_WEST
;
502 if (nFlags
& FloatWinPopupFlags::Left
)
504 rect_anchor
= GDK_GRAVITY_NORTH_WEST
;
505 menu_anchor
= GDK_GRAVITY_NORTH_EAST
;
507 else if (nFlags
& FloatWinPopupFlags::Up
)
509 rect_anchor
= GDK_GRAVITY_NORTH_WEST
;
510 menu_anchor
= GDK_GRAVITY_SOUTH_WEST
;
512 else if (nFlags
& FloatWinPopupFlags::Right
)
514 rect_anchor
= GDK_GRAVITY_NORTH_EAST
;
517 GdkSurface
* gdkWindow
= widget_get_surface(mpFrame
->getMouseEventWidget());
518 gtk_menu_popup_at_rect(GTK_MENU(mpMenuWidget
), gdkWindow
, &rect
, rect_anchor
, menu_anchor
, nullptr);
526 //typically there is an event, and we can then distinguish if this was
527 //launched from the keyboard (gets auto-mnemoniced) or the mouse (which
529 GdkEvent
*pEvent
= gtk_get_current_event();
532 gdk_event_get_button(pEvent
, &nButton
);
533 nTime
= gdk_event_get_time(pEvent
);
538 nTime
= GtkSalFrame::GetLastInputEventTime();
541 // Do the same strange semantics as vcl popup windows to arrive at a frame geometry
542 // in mirrored UI case; best done by actually executing the same code.
543 // (see code in FloatingWindow::StartPopupMode)
544 sal_uInt16 nArrangeIndex
;
545 Point aPos
= FloatingWindow::ImplCalcPos(pWin
, rRect
, nFlags
, nArrangeIndex
);
546 AbsoluteScreenPixelPoint aPosAbs
= FloatingWindow::ImplConvertToAbsPos(xParent
, aPos
);
548 gtk_menu_popup(GTK_MENU(mpMenuWidget
), nullptr, nullptr, MenuPositionFunc
,
549 &aPosAbs
, nButton
, nTime
);
553 if (g_main_loop_is_running(pLoop
))
554 main_loop_run(pLoop
);
556 g_main_loop_unref(pLoop
);
558 mpVCLMenu
->Deactivate();
560 g_object_unref(mpActionGroup
);
561 ClearActionGroupAndMenuModel();
563 #if !GTK_CHECK_VERSION(4, 0, 0)
564 gtk_widget_destroy(mpMenuWidget
);
566 gtk_widget_unparent(mpMenuWidget
);
568 mpMenuWidget
= nullptr;
570 gtk_widget_insert_action_group(mpFrame
->getMouseEventWidget(), "win", nullptr);
572 // undo tooltip blocking
573 mpFrame
->UnblockTooltip();
584 GtkSalMenu::GtkSalMenu( bool bMenuBar
) :
585 maUpdateMenuBarIdle("Native Gtk Menu Update Idle"),
586 mbInActivateCallback( false ),
587 mbMenuBar( bMenuBar
),
588 mbNeedsUpdate( false ),
589 mbReturnFocusToDocument( false ),
590 mbAddedGrab( false ),
591 mpMenuBarContainerWidget( nullptr ),
592 mpMenuAllowShrinkWidget( nullptr ),
593 mpMenuBarWidget( nullptr ),
594 mpMenuWidget( nullptr ),
595 mpCloseButton( nullptr ),
596 mpVCLMenu( nullptr ),
597 mpParentSalMenu( nullptr ),
599 mpMenuModel( nullptr ),
600 mpActionGroup( nullptr )
602 //typically this only gets called after the menu has been customized on the
603 //next idle slot, in the normal case of a new menubar SetFrame is called
604 //directly long before this idle would get called.
605 maUpdateMenuBarIdle
.SetPriority(TaskPriority::HIGHEST
);
606 maUpdateMenuBarIdle
.SetInvokeHandler(LINK(this, GtkSalMenu
, MenuBarHierarchyChangeHandler
));
609 IMPL_LINK_NOARG(GtkSalMenu
, MenuBarHierarchyChangeHandler
, Timer
*, void)
611 SAL_WARN_IF(!mpFrame
, "vcl.gtk", "MenuBar layout changed, but no frame for some reason!");
617 void GtkSalMenu::SetNeedsUpdate()
619 GtkSalMenu
* pMenu
= this;
620 // start that the menu and its parents are in need of an update
621 // on the next activation
622 while (pMenu
&& !pMenu
->mbNeedsUpdate
)
624 pMenu
->mbNeedsUpdate
= true;
625 pMenu
= pMenu
->mpParentSalMenu
;
627 // only if a menubar is directly updated do we force in a full
629 if (mbMenuBar
&& !maUpdateMenuBarIdle
.IsActive())
630 maUpdateMenuBarIdle
.Start();
633 void GtkSalMenu::SetMenuModel(GMenuModel
* pMenuModel
)
636 g_object_unref(mpMenuModel
);
637 mpMenuModel
= pMenuModel
;
639 g_object_ref(mpMenuModel
);
642 GtkSalMenu::~GtkSalMenu()
644 SolarMutexGuard aGuard
;
646 // tdf#140225 we expect all items to be removed by Menu::dispose
647 // before this dtor is called
648 assert(maItems
.empty());
650 DestroyMenuBarWidget();
653 g_object_unref(mpMenuModel
);
656 mpFrame
->SetMenu(nullptr);
659 bool GtkSalMenu::VisibleMenuBar()
661 return mbMenuBar
&& (bUnityMode
|| mpMenuBarContainerWidget
);
664 void GtkSalMenu::InsertItem( SalMenuItem
* pSalMenuItem
, unsigned nPos
)
666 SolarMutexGuard aGuard
;
667 GtkSalMenuItem
*pItem
= static_cast<GtkSalMenuItem
*>( pSalMenuItem
);
669 if ( nPos
== MENU_APPEND
)
670 maItems
.push_back( pItem
);
672 maItems
.insert( maItems
.begin() + nPos
, pItem
);
674 pItem
->mpParentMenu
= this;
679 void GtkSalMenu::RemoveItem( unsigned nPos
)
681 SolarMutexGuard aGuard
;
683 // tdf#140225 clear associated action when the item is removed
686 GLOActionGroup
* pActionGroup
= G_LO_ACTION_GROUP(mpActionGroup
);
687 OString sCommand
= GetCommandForItem(maItems
[nPos
]);
688 g_lo_action_group_remove(pActionGroup
, sCommand
.getStr());
691 maItems
.erase( maItems
.begin() + nPos
);
695 void GtkSalMenu::SetSubMenu( SalMenuItem
* pSalMenuItem
, SalMenu
* pSubMenu
, unsigned )
697 SolarMutexGuard aGuard
;
698 GtkSalMenuItem
*pItem
= static_cast< GtkSalMenuItem
* >( pSalMenuItem
);
699 GtkSalMenu
*pGtkSubMenu
= static_cast< GtkSalMenu
* >( pSubMenu
);
701 if ( pGtkSubMenu
== nullptr )
704 pGtkSubMenu
->mpParentSalMenu
= this;
705 pItem
->mpSubMenu
= pGtkSubMenu
;
710 static void CloseMenuBar(GtkWidget
*, gpointer pMenu
)
712 Application::PostUserEvent(static_cast<MenuBar
*>(pMenu
)->GetCloseButtonClickHdl());
715 GtkWidget
* GtkSalMenu::AddButton(GtkWidget
*pImage
)
717 GtkWidget
* pButton
= gtk_button_new();
719 #if !GTK_CHECK_VERSION(4, 0, 0)
720 gtk_button_set_relief(GTK_BUTTON(pButton
), GTK_RELIEF_NONE
);
721 gtk_button_set_focus_on_click(GTK_BUTTON(pButton
), false);
723 gtk_button_set_has_frame(GTK_BUTTON(pButton
), false);
724 gtk_widget_set_focus_on_click(pButton
, false);
727 gtk_widget_set_can_focus(pButton
, false);
729 GtkStyleContext
*pButtonContext
= gtk_widget_get_style_context(GTK_WIDGET(pButton
));
731 gtk_style_context_add_class(pButtonContext
, "flat");
732 gtk_style_context_add_class(pButtonContext
, "small-button");
734 gtk_widget_show(pImage
);
736 gtk_widget_set_valign(pButton
, GTK_ALIGN_CENTER
);
738 #if !GTK_CHECK_VERSION(4, 0, 0)
739 gtk_container_add(GTK_CONTAINER(pButton
), pImage
);
740 gtk_widget_show_all(pButton
);
742 gtk_button_set_child(GTK_BUTTON(pButton
), pImage
);
747 void GtkSalMenu::ShowCloseButton(bool bShow
)
750 if (!mpMenuBarContainerWidget
)
757 #if !GTK_CHECK_VERSION(4, 0, 0)
758 gtk_widget_destroy(mpCloseButton
);
760 g_clear_pointer(&mpCloseButton
, gtk_widget_unparent
);
762 mpCloseButton
= nullptr;
770 GIcon
* pIcon
= g_themed_icon_new_with_default_fallbacks("window-close-symbolic");
771 #if !GTK_CHECK_VERSION(4, 0, 0)
772 GtkWidget
* pImage
= gtk_image_new_from_gicon(pIcon
, GTK_ICON_SIZE_MENU
);
774 GtkWidget
* pImage
= gtk_image_new_from_gicon(pIcon
);
776 g_object_unref(pIcon
);
778 mpCloseButton
= AddButton(pImage
);
780 gtk_widget_set_margin_end(mpCloseButton
, 8);
782 OUString
sToolTip(VclResId(SV_HELPTEXT_CLOSEDOCUMENT
));
783 gtk_widget_set_tooltip_text(mpCloseButton
, sToolTip
.toUtf8().getStr());
785 MenuBar
*pVclMenuBar
= static_cast<MenuBar
*>(mpVCLMenu
.get());
786 g_signal_connect(mpCloseButton
, "clicked", G_CALLBACK(CloseMenuBar
), pVclMenuBar
);
788 gtk_grid_attach(GTK_GRID(mpMenuBarContainerWidget
), mpCloseButton
, 1, 0, 1, 1);
793 void DestroyMemoryStream(gpointer data
)
795 SvMemoryStream
* pMemStm
= static_cast<SvMemoryStream
*>(data
);
800 static void MenuButtonClicked(GtkWidget
* pWidget
, gpointer pMenu
)
802 OUString
aId(get_buildable_id(GTK_BUILDABLE(pWidget
)));
803 static_cast<MenuBar
*>(pMenu
)->HandleMenuButtonEvent(aId
.toUInt32());
806 bool GtkSalMenu::AddMenuBarButton(const SalMenuButtonItem
& rNewItem
)
811 if (!mpMenuBarContainerWidget
)
814 GtkWidget
* pImage
= nullptr;
815 if (!!rNewItem
.maImage
)
817 SvMemoryStream
* pMemStm
= new SvMemoryStream
;
818 auto aBitmapEx
= rNewItem
.maImage
.GetBitmapEx();
819 vcl::PngImageWriter
aWriter(*pMemStm
);
820 aWriter
.write(aBitmapEx
);
822 GBytes
*pBytes
= g_bytes_new_with_free_func(pMemStm
->GetData(),
827 GIcon
*pIcon
= g_bytes_icon_new(pBytes
);
828 #if !GTK_CHECK_VERSION(4, 0, 0)
829 pImage
= gtk_image_new_from_gicon(pIcon
, GTK_ICON_SIZE_MENU
);
831 pImage
= gtk_image_new_from_gicon(pIcon
);
833 g_object_unref(pIcon
);
834 g_bytes_unref(pBytes
);
837 GtkWidget
* pButton
= AddButton(pImage
);
839 maExtraButtons
.emplace_back(rNewItem
.mnId
, pButton
);
841 set_buildable_id(GTK_BUILDABLE(pButton
), OUString::number(rNewItem
.mnId
));
843 gtk_widget_set_tooltip_text(pButton
, rNewItem
.maToolTipText
.toUtf8().getStr());
845 MenuBar
*pVclMenuBar
= static_cast<MenuBar
*>(mpVCLMenu
.get());
846 g_signal_connect(pButton
, "clicked", G_CALLBACK(MenuButtonClicked
), pVclMenuBar
);
850 gtk_grid_insert_next_to(GTK_GRID(mpMenuBarContainerWidget
), mpCloseButton
, GTK_POS_LEFT
);
851 gtk_grid_attach_next_to(GTK_GRID(mpMenuBarContainerWidget
), pButton
, mpCloseButton
,
855 gtk_grid_attach(GTK_GRID(mpMenuBarContainerWidget
), pButton
, 1, 0, 1, 1);
860 void GtkSalMenu::RemoveMenuBarButton( sal_uInt16 nId
)
862 const auto it
= std::find_if(maExtraButtons
.begin(), maExtraButtons
.end(), [&nId
](const auto &item
) {
863 return item
.first
== nId
; });
864 if (it
== maExtraButtons
.end())
868 #if !GTK_CHECK_VERSION(4, 0, 0)
869 gtk_container_child_get(GTK_CONTAINER(mpMenuBarContainerWidget
), it
->second
, "left-attach", &nAttach
, nullptr);
870 gtk_widget_destroy(it
->second
);
872 gtk_grid_query_child(GTK_GRID(mpMenuBarContainerWidget
), it
->second
, &nAttach
, nullptr, nullptr, nullptr);
873 g_clear_pointer(&(it
->second
), gtk_widget_unparent
);
875 gtk_grid_remove_column(GTK_GRID(mpMenuBarContainerWidget
), nAttach
);
876 maExtraButtons
.erase(it
);
879 tools::Rectangle
GtkSalMenu::GetMenuBarButtonRectPixel(sal_uInt16 nId
, SalFrame
* pReferenceFrame
)
881 if (!pReferenceFrame
)
882 return tools::Rectangle();
884 const auto it
= std::find_if(maExtraButtons
.begin(), maExtraButtons
.end(), [&nId
](const auto &item
) {
885 return item
.first
== nId
; });
886 if (it
== maExtraButtons
.end())
887 return tools::Rectangle();
889 GtkWidget
* pButton
= it
->second
;
891 GtkSalFrame
* pFrame
= static_cast<GtkSalFrame
*>(pReferenceFrame
);
894 if (!gtk_widget_translate_coordinates(pButton
, GTK_WIDGET(pFrame
->getMouseEventWidget()), 0, 0, &x
, &y
))
895 return tools::Rectangle();
897 return tools::Rectangle(Point(x
, y
), Size(gtk_widget_get_allocated_width(pButton
),
898 gtk_widget_get_allocated_height(pButton
)));
901 //Typically when the menubar is deactivated we want the focus to return
902 //to where it came from. If the menubar was activated because of F6
903 //moving focus into the associated VCL menubar then on pressing ESC
904 //or any other normal reason for deactivation we want focus to return
905 //to the document, definitely not still stuck in the associated
906 //VCL menubar. But if F6 is pressed while the menubar is activated
907 //we want to pass that F6 back to the VCL menubar which will move
908 //focus to the next pane by itself.
909 void GtkSalMenu::ReturnFocus()
913 #if !GTK_CHECK_VERSION(4, 0, 0)
914 gtk_grab_remove(mpMenuBarWidget
);
918 if (!mbReturnFocusToDocument
)
919 gtk_widget_grab_focus(mpFrame
->getMouseEventWidget());
921 mpFrame
->GetWindow()->GrabFocusToDocument();
922 mbReturnFocusToDocument
= false;
925 #if !GTK_CHECK_VERSION(4, 0, 0)
926 gboolean
GtkSalMenu::SignalKey(GdkEventKey
const * pEvent
)
928 if (pEvent
->keyval
== GDK_KEY_F6
)
930 mbReturnFocusToDocument
= false;
931 gtk_menu_shell_cancel(GTK_MENU_SHELL(mpMenuBarWidget
));
932 //because we return false here, the keypress will continue
933 //to propagate and in the case that vcl focus is in
934 //the vcl menubar then that will also process F6 and move
941 //The GtkSalMenu is owned by a Vcl Menu/MenuBar. In the menubar
942 //case the vcl menubar is present and "visible", but with a 0 height
943 //so it not apparent. Normally it acts as though it is not there when
944 //a Native menubar is active. If we return true here, then for keyboard
945 //activation and traversal with F6 through panes then the vcl menubar
946 //acts as though it *is* present and we translate its take focus and F6
947 //traversal key events into the gtk menubar equivalents.
948 bool GtkSalMenu::CanGetFocus() const
950 return mpMenuBarWidget
!= nullptr;
953 bool GtkSalMenu::TakeFocus()
955 if (!mpMenuBarWidget
)
958 #if !GTK_CHECK_VERSION(4, 0, 0)
959 //Send a keyboard event to the gtk menubar to let it know it has been
960 //activated via the keyboard. Doesn't do anything except cause the gtk
961 //menubar "keyboard_mode" member to get set to true, so typically mnemonics
962 //are shown which will serve as indication that the menubar has focus
963 //(given that we want to show it with no menus popped down)
964 GdkEvent
*event
= GtkSalFrame::makeFakeKeyPress(mpMenuBarWidget
);
965 gtk_widget_event(mpMenuBarWidget
, event
);
966 gdk_event_free(event
);
968 //this pairing results in a menubar with keyboard focus with no menus
970 gtk_grab_add(mpMenuBarWidget
);
973 gtk_menu_shell_select_first(GTK_MENU_SHELL(mpMenuBarWidget
), false);
974 gtk_menu_shell_deselect(GTK_MENU_SHELL(mpMenuBarWidget
));
976 mbReturnFocusToDocument
= true;
980 #if !GTK_CHECK_VERSION(4, 0, 0)
981 static void MenuBarReturnFocus(GtkMenuShell
*, gpointer menu
)
983 GtkSalFrame::UpdateLastInputEventTime(gtk_get_current_event_time());
984 GtkSalMenu
* pMenu
= static_cast<GtkSalMenu
*>(menu
);
985 pMenu
->ReturnFocus();
988 static gboolean
MenuBarSignalKey(GtkWidget
*, GdkEventKey
* pEvent
, gpointer menu
)
990 GtkSalMenu
* pMenu
= static_cast<GtkSalMenu
*>(menu
);
991 return pMenu
->SignalKey(pEvent
);
995 void GtkSalMenu::CreateMenuBarWidget()
997 if (mpMenuBarContainerWidget
)
1000 GtkGrid
* pGrid
= mpFrame
->getTopLevelGridWidget();
1001 mpMenuBarContainerWidget
= gtk_grid_new();
1003 gtk_widget_set_hexpand(GTK_WIDGET(mpMenuBarContainerWidget
), true);
1004 gtk_grid_insert_row(pGrid
, 0);
1005 gtk_grid_attach(pGrid
, mpMenuBarContainerWidget
, 0, 0, 1, 1);
1007 #if !GTK_CHECK_VERSION(4, 0, 0)
1008 mpMenuAllowShrinkWidget
= gtk_scrolled_window_new(nullptr, nullptr);
1009 gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(mpMenuAllowShrinkWidget
), GTK_SHADOW_NONE
);
1010 // tdf#129634 don't allow this scrolled window as a candidate to tab into
1011 gtk_widget_set_can_focus(GTK_WIDGET(mpMenuAllowShrinkWidget
), false);
1013 mpMenuAllowShrinkWidget
= gtk_scrolled_window_new();
1014 gtk_scrolled_window_set_has_frame(GTK_SCROLLED_WINDOW(mpMenuAllowShrinkWidget
), false);
1016 // tdf#116290 external policy on scrolledwindow will not show a scrollbar,
1017 // but still allow scrolled window to not be sized to the child content.
1018 // So the menubar can be shrunk past its nominal smallest width.
1019 // Unlike a hack using GtkFixed/GtkLayout the correct placement of the menubar occurs under RTL
1020 gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(mpMenuAllowShrinkWidget
), GTK_POLICY_EXTERNAL
, GTK_POLICY_NEVER
);
1021 gtk_grid_attach(GTK_GRID(mpMenuBarContainerWidget
), mpMenuAllowShrinkWidget
, 0, 0, 1, 1);
1023 #if !GTK_CHECK_VERSION(4, 0, 0)
1024 mpMenuBarWidget
= gtk_menu_bar_new_from_model(mpMenuModel
);
1026 mpMenuBarWidget
= gtk_popover_menu_bar_new_from_model(mpMenuModel
);
1029 gtk_widget_insert_action_group(mpMenuBarWidget
, "win", mpActionGroup
);
1030 gtk_widget_set_hexpand(GTK_WIDGET(mpMenuBarWidget
), true);
1031 gtk_widget_set_hexpand(mpMenuAllowShrinkWidget
, true);
1032 #if !GTK_CHECK_VERSION(4, 0, 0)
1033 gtk_container_add(GTK_CONTAINER(mpMenuAllowShrinkWidget
), mpMenuBarWidget
);
1035 gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(mpMenuAllowShrinkWidget
), mpMenuBarWidget
);
1038 #if !GTK_CHECK_VERSION(4, 0, 0)
1039 g_signal_connect(G_OBJECT(mpMenuBarWidget
), "deactivate", G_CALLBACK(MenuBarReturnFocus
), this);
1040 g_signal_connect(G_OBJECT(mpMenuBarWidget
), "key-press-event", G_CALLBACK(MenuBarSignalKey
), this);
1043 gtk_widget_show(mpMenuBarWidget
);
1044 gtk_widget_show(mpMenuAllowShrinkWidget
);
1045 gtk_widget_show(mpMenuBarContainerWidget
);
1047 ShowCloseButton( static_cast<MenuBar
*>(mpVCLMenu
.get())->HasCloseButton() );
1050 void GtkSalMenu::DestroyMenuBarWidget()
1052 if (!mpMenuBarContainerWidget
)
1055 #if !GTK_CHECK_VERSION(4, 0, 0)
1056 // tdf#140225 call cancel before destroying it in case there are some
1057 // active menus popped open
1058 gtk_menu_shell_cancel(GTK_MENU_SHELL(mpMenuBarWidget
));
1060 gtk_widget_destroy(mpMenuBarContainerWidget
);
1062 g_clear_pointer(&mpMenuBarContainerWidget
, gtk_widget_unparent
);
1064 mpMenuBarContainerWidget
= nullptr;
1065 mpMenuBarWidget
= nullptr;
1066 mpCloseButton
= nullptr;
1069 void GtkSalMenu::SetFrame(const SalFrame
* pFrame
)
1071 SolarMutexGuard aGuard
;
1073 SAL_INFO("vcl.unity", "GtkSalMenu set to frame");
1074 mpFrame
= const_cast<GtkSalFrame
*>(static_cast<const GtkSalFrame
*>(pFrame
));
1076 // if we had a menu on the GtkSalMenu we have to free it as we generate a
1077 // full menu anyway and we might need to reuse an existing model and
1079 mpFrame
->SetMenu( this );
1080 mpFrame
->EnsureAppMenuWatch();
1082 // Clean menu model and action group if needed.
1083 GtkWidget
* pWidget
= mpFrame
->getWindow();
1084 GdkSurface
* gdkWindow
= widget_get_surface(pWidget
);
1086 GLOMenu
* pMenuModel
= G_LO_MENU( g_object_get_data( G_OBJECT( gdkWindow
), "g-lo-menubar" ) );
1087 GLOActionGroup
* pActionGroup
= G_LO_ACTION_GROUP( g_object_get_data( G_OBJECT( gdkWindow
), "g-lo-action-group" ) );
1088 SAL_INFO("vcl.unity", "Found menu model: " << pMenuModel
<< " and action group: " << pActionGroup
);
1092 if ( g_menu_model_get_n_items( G_MENU_MODEL( pMenuModel
) ) > 0 )
1093 g_lo_menu_remove( pMenuModel
, 0 );
1095 mpMenuModel
= G_MENU_MODEL( g_lo_menu_new() );
1100 g_lo_action_group_clear( pActionGroup
);
1101 mpActionGroup
= G_ACTION_GROUP( pActionGroup
);
1104 // Generate the main menu structure.
1108 g_lo_menu_insert_section( pMenuModel
, 0, nullptr, mpMenuModel
);
1110 if (!bUnityMode
&& static_cast<MenuBar
*>(mpVCLMenu
.get())->IsDisplayable())
1112 DestroyMenuBarWidget();
1113 CreateMenuBarWidget();
1117 const GtkSalFrame
* GtkSalMenu::GetFrame() const
1119 SolarMutexGuard aGuard
;
1120 const GtkSalMenu
* pMenu
= this;
1121 while( pMenu
&& ! pMenu
->mpFrame
)
1122 pMenu
= pMenu
->mpParentSalMenu
;
1123 return pMenu
? pMenu
->mpFrame
: nullptr;
1126 void GtkSalMenu::NativeCheckItem( unsigned nSection
, unsigned nItemPos
, MenuItemBits bits
, gboolean bCheck
)
1128 SolarMutexGuard aGuard
;
1130 if ( mpActionGroup
== nullptr )
1133 gchar
* aCommand
= g_lo_menu_get_command_from_item_in_section( G_LO_MENU( mpMenuModel
), nSection
, nItemPos
);
1135 if ( aCommand
!= nullptr || g_strcmp0( aCommand
, "" ) != 0 )
1137 GVariant
*pCheckValue
= nullptr;
1138 GVariant
*pCurrentState
= g_action_group_get_action_state( mpActionGroup
, aCommand
);
1140 if ( bits
& MenuItemBits::RADIOCHECK
)
1141 pCheckValue
= bCheck
? g_variant_new_string( aCommand
) : g_variant_new_string( "" );
1144 // By default, all checked items are checkmark buttons.
1145 if (bCheck
|| pCurrentState
!= nullptr)
1146 pCheckValue
= g_variant_new_boolean( bCheck
);
1149 if ( pCheckValue
!= nullptr )
1151 if ( pCurrentState
== nullptr || g_variant_equal( pCurrentState
, pCheckValue
) == FALSE
)
1153 g_action_group_change_action_state( mpActionGroup
, aCommand
, pCheckValue
);
1157 g_variant_unref (pCheckValue
);
1161 if ( pCurrentState
!= nullptr )
1162 g_variant_unref( pCurrentState
);
1169 void GtkSalMenu::NativeSetEnableItem(const OString
& sCommand
, gboolean bEnable
)
1171 SolarMutexGuard aGuard
;
1172 GLOActionGroup
* pActionGroup
= G_LO_ACTION_GROUP( mpActionGroup
);
1174 if (g_action_group_get_action_enabled(G_ACTION_GROUP(pActionGroup
), sCommand
.getStr()) != bEnable
)
1175 g_lo_action_group_set_action_enabled(pActionGroup
, sCommand
.getStr(), bEnable
);
1178 void GtkSalMenu::NativeSetItemText( unsigned nSection
, unsigned nItemPos
, const OUString
& rText
, bool bFireEvent
)
1180 SolarMutexGuard aGuard
;
1181 // Escape all underscores so that they don't get interpreted as hotkeys
1182 OUString aText
= rText
.replaceAll( "_", "__" );
1183 // Replace the LibreOffice hotkey identifier with an underscore
1184 aText
= aText
.replace( '~', '_' );
1185 OString aConvertedText
= OUStringToOString( aText
, RTL_TEXTENCODING_UTF8
);
1187 // Update item text only when necessary.
1188 gchar
* aLabel
= g_lo_menu_get_label_from_item_in_section( G_LO_MENU( mpMenuModel
), nSection
, nItemPos
);
1190 if ( aLabel
== nullptr || g_strcmp0( aLabel
, aConvertedText
.getStr() ) != 0 )
1191 g_lo_menu_set_label_to_item_in_section( G_LO_MENU( mpMenuModel
), nSection
, nItemPos
, aConvertedText
.getStr(), bFireEvent
);
1197 void GtkSalMenu::NativeSetItemIcon( unsigned nSection
, unsigned nItemPos
, const Image
& rImage
)
1199 #if GLIB_CHECK_VERSION(2,38,0)
1200 if (!rImage
&& mbHasNullItemIcon
)
1203 SolarMutexGuard aGuard
;
1207 SvMemoryStream
* pMemStm
= new SvMemoryStream
;
1208 auto aBitmapEx
= rImage
.GetBitmapEx();
1209 vcl::PngImageWriter
aWriter(*pMemStm
);
1210 aWriter
.write(aBitmapEx
);
1212 GBytes
*pBytes
= g_bytes_new_with_free_func(pMemStm
->GetData(),
1214 DestroyMemoryStream
,
1217 GIcon
*pIcon
= g_bytes_icon_new(pBytes
);
1219 g_lo_menu_set_icon_to_item_in_section( G_LO_MENU( mpMenuModel
), nSection
, nItemPos
, pIcon
);
1220 g_object_unref(pIcon
);
1221 g_bytes_unref(pBytes
);
1222 mbHasNullItemIcon
= false;
1226 g_lo_menu_set_icon_to_item_in_section( G_LO_MENU( mpMenuModel
), nSection
, nItemPos
, nullptr );
1227 mbHasNullItemIcon
= true;
1236 void GtkSalMenu::NativeSetAccelerator( unsigned nSection
, unsigned nItemPos
, const vcl::KeyCode
& rKeyCode
, std::u16string_view rKeyName
)
1238 SolarMutexGuard aGuard
;
1240 if ( rKeyName
.empty() )
1244 GdkModifierType nModifiers
;
1245 GtkSalFrame::KeyCodeToGdkKey(rKeyCode
, &nKeyCode
, &nModifiers
);
1247 gchar
* aAccelerator
= gtk_accelerator_name( nKeyCode
, nModifiers
);
1249 gchar
* aCurrentAccel
= g_lo_menu_get_accelerator_from_item_in_section( G_LO_MENU( mpMenuModel
), nSection
, nItemPos
);
1251 if ( aCurrentAccel
== nullptr && g_strcmp0( aCurrentAccel
, aAccelerator
) != 0 )
1252 g_lo_menu_set_accelerator_to_item_in_section ( G_LO_MENU( mpMenuModel
), nSection
, nItemPos
, aAccelerator
);
1254 g_free( aAccelerator
);
1255 g_free( aCurrentAccel
);
1258 bool GtkSalMenu::NativeSetItemCommand( unsigned nSection
,
1261 const gchar
* aCommand
,
1266 bool bSubMenuAddedOrRemoved
= false;
1268 SolarMutexGuard aGuard
;
1269 GLOActionGroup
* pActionGroup
= G_LO_ACTION_GROUP( mpActionGroup
);
1271 GVariant
*pTarget
= nullptr;
1273 if (g_action_group_has_action(mpActionGroup
, aCommand
))
1274 g_lo_action_group_remove(pActionGroup
, aCommand
);
1276 if ( ( nBits
& MenuItemBits::CHECKABLE
) || bIsSubmenu
)
1278 // Item is a checkmark button.
1279 GVariantType
* pStateType
= g_variant_type_new( reinterpret_cast<gchar
const *>(G_VARIANT_TYPE_BOOLEAN
) );
1280 GVariant
* pState
= g_variant_new_boolean( bChecked
);
1282 g_lo_action_group_insert_stateful( pActionGroup
, aCommand
, nId
, bIsSubmenu
, nullptr, pStateType
, nullptr, pState
);
1284 else if ( nBits
& MenuItemBits::RADIOCHECK
)
1286 // Item is a radio button.
1287 GVariantType
* pParameterType
= g_variant_type_new( reinterpret_cast<gchar
const *>(G_VARIANT_TYPE_STRING
) );
1288 GVariantType
* pStateType
= g_variant_type_new( reinterpret_cast<gchar
const *>(G_VARIANT_TYPE_STRING
) );
1289 GVariant
* pState
= g_variant_new_string( "" );
1290 pTarget
= g_variant_new_string( aCommand
);
1292 g_lo_action_group_insert_stateful( pActionGroup
, aCommand
, nId
, FALSE
, pParameterType
, pStateType
, nullptr, pState
);
1296 // Item is not special, so insert a stateless action.
1297 g_lo_action_group_insert( pActionGroup
, aCommand
, nId
, FALSE
);
1300 GLOMenu
* pMenu
= G_LO_MENU( mpMenuModel
);
1302 // Menu item is not updated unless it's necessary.
1303 gchar
* aCurrentCommand
= g_lo_menu_get_command_from_item_in_section( pMenu
, nSection
, nItemPos
);
1305 if ( aCurrentCommand
== nullptr || g_strcmp0( aCurrentCommand
, aCommand
) != 0 )
1307 GLOMenu
* pSubMenuModel
= g_lo_menu_get_submenu_from_item_in_section(pMenu
, nSection
, nItemPos
);
1308 bool bOldHasSubmenu
= pSubMenuModel
!= nullptr;
1309 bSubMenuAddedOrRemoved
= bOldHasSubmenu
!= bIsSubmenu
;
1310 if (bSubMenuAddedOrRemoved
)
1312 //tdf#98636 it's not good enough to unset the "submenu-action" attribute to change something
1313 //from a submenu to a non-submenu item, so remove the old one entirely and re-add it to
1314 //support achieving that
1315 gchar
* pLabel
= g_lo_menu_get_label_from_item_in_section(pMenu
, nSection
, nItemPos
);
1316 g_lo_menu_remove_from_section(pMenu
, nSection
, nItemPos
);
1317 g_lo_menu_insert_in_section(pMenu
, nSection
, nItemPos
, pLabel
);
1321 // suppress event firing here, we will do so anyway in the g_lo_menu_set_action_and_target_value_to_item_in_section call,
1322 // speeds up constructing menus
1323 g_lo_menu_set_command_to_item_in_section( pMenu
, nSection
, nItemPos
, aCommand
, /*fire_event*/false );
1325 gchar
* aItemCommand
= g_strconcat("win.", aCommand
, nullptr );
1328 g_lo_menu_set_submenu_action_to_item_in_section( pMenu
, nSection
, nItemPos
, aItemCommand
);
1331 g_lo_menu_set_action_and_target_value_to_item_in_section( pMenu
, nSection
, nItemPos
, aItemCommand
, pTarget
);
1335 g_object_unref(pSubMenuModel
);
1337 g_free( aItemCommand
);
1340 if ( aCurrentCommand
)
1341 g_free( aCurrentCommand
);
1344 g_variant_unref(pTarget
);
1346 return bSubMenuAddedOrRemoved
;
1349 GtkSalMenu
* GtkSalMenu::GetTopLevel()
1351 GtkSalMenu
*pMenu
= this;
1352 while (pMenu
->mpParentSalMenu
)
1353 pMenu
= pMenu
->mpParentSalMenu
;
1357 void GtkSalMenu::DispatchCommand(const gchar
*pCommand
)
1359 SolarMutexGuard aGuard
;
1360 MenuAndId aMenuAndId
= decode_command(pCommand
);
1361 GtkSalMenu
* pSalSubMenu
= aMenuAndId
.first
;
1362 GtkSalMenu
* pTopLevel
= pSalSubMenu
->GetTopLevel();
1364 // tdf#125803 spacebar will toggle radios and checkbuttons without automatically
1365 // closing the menu. To handle this properly I imagine we need to set groups for the
1366 // radiobuttons so the others visually untoggle when the active one is toggled and
1367 // we would further need to teach vcl that the state can change more than once.
1369 // or we could unconditionally deactivate the menus if regardless of what particular
1370 // type of menu item got activated
1371 if (pTopLevel
->mpMenuBarWidget
)
1373 #if !GTK_CHECK_VERSION(4, 0, 0)
1374 gtk_menu_shell_deactivate(GTK_MENU_SHELL(pTopLevel
->mpMenuBarWidget
));
1377 if (pTopLevel
->mpMenuWidget
)
1379 #if GTK_CHECK_VERSION(4, 0, 0)
1380 gtk_popover_popdown(GTK_POPOVER(pTopLevel
->mpMenuWidget
));
1382 gtk_menu_shell_deactivate(GTK_MENU_SHELL(pTopLevel
->mpMenuWidget
));
1386 pTopLevel
->GetMenu()->HandleMenuCommandEvent(pSalSubMenu
->GetMenu(), aMenuAndId
.second
);
1389 void GtkSalMenu::ActivateAllSubmenus(Menu
* pMenuBar
)
1391 // We can re-enter this method via the new event loop that gets created
1392 // in GtkClipboardTransferable::getTransferDataFlavorsAsVector, so use the InActivateCallback
1393 // flag to detect that and skip some startup work.
1394 if (mbInActivateCallback
)
1397 mbInActivateCallback
= true;
1398 pMenuBar
->HandleMenuActivateEvent(GetMenu());
1399 mbInActivateCallback
= false;
1400 for (GtkSalMenuItem
* pSalItem
: maItems
)
1402 if ( pSalItem
->mpSubMenu
!= nullptr )
1404 pSalItem
->mpSubMenu
->ActivateAllSubmenus(pMenuBar
);
1408 pMenuBar
->HandleMenuDeActivateEvent(GetMenu());
1411 void GtkSalMenu::ClearActionGroupAndMenuModel()
1413 SetMenuModel(nullptr);
1414 mpActionGroup
= nullptr;
1415 for (GtkSalMenuItem
* pSalItem
: maItems
)
1417 if ( pSalItem
->mpSubMenu
!= nullptr )
1419 pSalItem
->mpSubMenu
->ClearActionGroupAndMenuModel();
1424 void GtkSalMenu::Activate(const gchar
* pCommand
)
1426 MenuAndId aMenuAndId
= decode_command(pCommand
);
1427 GtkSalMenu
* pSalMenu
= aMenuAndId
.first
;
1428 Menu
* pVclMenu
= pSalMenu
->GetMenu();
1429 if (pVclMenu
->isDisposed())
1431 GtkSalMenu
* pTopLevel
= pSalMenu
->GetTopLevel();
1432 Menu
* pVclSubMenu
= pVclMenu
->GetPopupMenu(aMenuAndId
.second
);
1433 GtkSalMenu
* pSubMenu
= pSalMenu
->GetItemAtPos(pVclMenu
->GetItemPos(aMenuAndId
.second
))->mpSubMenu
;
1435 pSubMenu
->mbInActivateCallback
= true;
1436 pTopLevel
->GetMenu()->HandleMenuActivateEvent(pVclSubMenu
);
1437 pSubMenu
->mbInActivateCallback
= false;
1438 pVclSubMenu
->UpdateNativeMenu();
1441 void GtkSalMenu::Deactivate(const gchar
* pCommand
)
1443 MenuAndId aMenuAndId
= decode_command(pCommand
);
1444 GtkSalMenu
* pSalMenu
= aMenuAndId
.first
;
1445 Menu
* pVclMenu
= pSalMenu
->GetMenu();
1446 if (pVclMenu
->isDisposed())
1448 GtkSalMenu
* pTopLevel
= pSalMenu
->GetTopLevel();
1449 Menu
* pVclSubMenu
= pVclMenu
->GetPopupMenu(aMenuAndId
.second
);
1450 pTopLevel
->GetMenu()->HandleMenuDeActivateEvent(pVclSubMenu
);
1453 void GtkSalMenu::EnableUnity(bool bEnable
)
1455 bUnityMode
= bEnable
;
1457 MenuBar
* pMenuBar(static_cast<MenuBar
*>(mpVCLMenu
.get()));
1458 bool bDisplayable(pMenuBar
->IsDisplayable());
1462 DestroyMenuBarWidget();
1470 ShowMenuBar(bDisplayable
);
1473 pMenuBar
->LayoutChanged();
1476 void GtkSalMenu::ShowMenuBar( bool bVisible
)
1478 // Unity tdf#106271: Can't hide global menu, so empty it instead when user wants to hide menubar,
1483 else if (mpMenuModel
&& g_menu_model_get_n_items(G_MENU_MODEL(mpMenuModel
)) > 0)
1484 g_lo_menu_remove(G_LO_MENU(mpMenuModel
), 0);
1487 CreateMenuBarWidget();
1489 DestroyMenuBarWidget();
1492 bool GtkSalMenu::IsItemVisible( unsigned nPos
)
1494 SolarMutexGuard aGuard
;
1495 bool bVisible
= false;
1497 if ( nPos
< maItems
.size() )
1498 bVisible
= maItems
[ nPos
]->mbVisible
;
1503 void GtkSalMenu::CheckItem( unsigned, bool )
1507 void GtkSalMenu::EnableItem( unsigned nPos
, bool bEnable
)
1509 SolarMutexGuard aGuard
;
1510 if ( bUnityMode
&& !mbInActivateCallback
&& !mbNeedsUpdate
&& GetTopLevel()->mbMenuBar
&& ( nPos
< maItems
.size() ) )
1512 OString sCommand
= GetCommandForItem(GetItemAtPos(nPos
));
1513 NativeSetEnableItem(sCommand
, bEnable
);
1517 void GtkSalMenu::ShowItem( unsigned nPos
, bool bShow
)
1519 SolarMutexGuard aGuard
;
1520 if ( nPos
< maItems
.size() )
1522 maItems
[ nPos
]->mbVisible
= bShow
;
1523 if ( bUnityMode
&& !mbInActivateCallback
&& !mbNeedsUpdate
&& GetTopLevel()->mbMenuBar
)
1528 void GtkSalMenu::SetItemText( unsigned nPos
, SalMenuItem
* pSalMenuItem
, const OUString
& rText
)
1530 SolarMutexGuard aGuard
;
1531 if ( !bUnityMode
|| mbInActivateCallback
|| mbNeedsUpdate
|| !GetTopLevel()->mbMenuBar
|| ( nPos
>= maItems
.size() ) )
1534 OString sCommand
= GetCommandForItem(static_cast<GtkSalMenuItem
*>(pSalMenuItem
));
1536 gint nSectionsCount
= g_menu_model_get_n_items( mpMenuModel
);
1537 for ( gint nSection
= 0; nSection
< nSectionsCount
; ++nSection
)
1539 gint nItemsCount
= g_lo_menu_get_n_items_from_section( G_LO_MENU( mpMenuModel
), nSection
);
1540 for ( gint nItem
= 0; nItem
< nItemsCount
; ++nItem
)
1542 gchar
* pCommandFromModel
= g_lo_menu_get_command_from_item_in_section( G_LO_MENU( mpMenuModel
), nSection
, nItem
);
1544 if (pCommandFromModel
== sCommand
)
1546 NativeSetItemText( nSection
, nItem
, rText
);
1547 g_free( pCommandFromModel
);
1551 g_free( pCommandFromModel
);
1556 void GtkSalMenu::SetItemImage( unsigned, SalMenuItem
*, const Image
& )
1560 void GtkSalMenu::SetAccelerator( unsigned, SalMenuItem
*, const vcl::KeyCode
&, const OUString
& )
1564 int GtkSalMenu::GetMenuBarHeight() const
1566 return mpMenuBarWidget
? gtk_widget_get_allocated_height(mpMenuBarWidget
) : 0;
1573 GtkSalMenuItem::GtkSalMenuItem( const SalItemParams
* pItemData
) :
1574 mpParentMenu( nullptr ),
1575 mpSubMenu( nullptr ),
1576 mnType( pItemData
->eType
),
1577 mnId( pItemData
->nId
),
1582 GtkSalMenuItem::~GtkSalMenuItem()
1586 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */