Update git submodules
[LibreOffice.git] / vcl / unx / gtk3 / gtksalmenu.cxx
bloba1b20c5c26d3f7f8624b68acd0ec24717f84438f
1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2 /*
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/.
8 */
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>
23 #include <window.h>
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);
36 return aCommand;
39 static OString GetCommandForItem(GtkSalMenuItem* pSalMenuItem)
41 return GetCommandForItem(pSalMenuItem->mpParentMenu,
42 pSalMenuItem->mnId);
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 ) );
65 g_free( aCommand );
67 g_lo_menu_remove_from_section( pMenu, nSection, nSectionItems );
71 typedef std::pair<GtkSalMenu*, sal_uInt16> MenuAndId;
73 namespace
75 MenuAndId decode_command(const gchar *action_name)
77 std::string_view sCommand(action_name);
79 sal_Int32 nIndex = 0;
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);
87 (void) sWindow;
89 return MenuAndId(pSalSubMenu, o3tl::toInt32(sItemId));
93 static void RemoveDisabledItemsFromNativeMenu(GLOMenu* pMenu, GList** pOldCommandList,
94 sal_Int32 nSection, GActionGroup* pActionGroup)
96 while (nSection >= 0)
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);
104 if (!bRemove)
106 //also remove any empty submenus
107 GLOMenu* pSubMenuModel = g_lo_menu_get_submenu_from_item_in_section(pMenu, nSection, nSectionItems);
108 if (pSubMenuModel)
110 gint nSubMenuSections = g_menu_model_get_n_items(G_MENU_MODEL(pSubMenuModel));
111 if (nSubMenuSections == 0)
112 bRemove = true;
113 else if (nSubMenuSections == 1)
115 gint nItems = g_lo_menu_get_n_items_from_section(pSubMenuModel, 0);
116 if (nItems == 0)
117 bRemove = true;
118 else if (nItems == 1)
120 //If the only entry is the "No Selection Possible" entry, then we are allowed
121 //to removed it
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;
125 g_free(pSubCommand);
128 g_object_unref(pSubMenuModel);
132 if (bRemove)
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");
140 if (bRemove)
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);
147 g_free(pCommand);
149 --nSection;
153 static void RemoveSpareSectionsFromNativeMenu( GLOMenu* pMenu, GList** pOldCommandList, sal_Int32 nLastSection )
155 if ( pMenu == nullptr || pOldCommandList == nullptr )
156 return;
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 );
178 return;
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");
217 if( !PrepUpdate() )
218 return;
220 if (mbNeedsUpdate)
222 mbNeedsUpdate = false;
223 if (mbMenuBar && maUpdateMenuBarIdle.IsActive())
225 maUpdateMenuBarIdle.Stop();
226 // tdf#124391 Prevent doubled menus in global menu
227 if (!bUnityMode)
229 maUpdateMenuBarIdle.Invoke();
230 return;
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;
250 sal_Int32 nItem;
252 for ( nItem = 0; nItem < static_cast<sal_Int32>(GetItemCount()); nItem++ ) {
253 if ( !IsItemVisible( nItem ) )
254 continue;
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.
261 if ( nId == 0xFFFF )
262 continue;
264 if ( pSalMenuItem->mnType == MenuItemType::SEPARATOR )
266 // Delete extra items from current section.
267 RemoveSpareItemsFromNativeMenu( pLOMenu, &pOldCommandList, nSection, validItems );
269 nSection++;
270 nItemPos = 0;
271 validItems = 0;
273 if ( nLOMenuSize <= nSection )
275 g_lo_menu_new_section( pLOMenu, nSection, nullptr );
276 nLOMenuSize++;
279 continue;
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()));
315 else
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 );
350 ++nItemPos;
351 ++validItems;
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);
376 if (nItemsCount)
377 break;
379 if (!nItemsCount)
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);
401 else
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);
411 *x = pPos->X();
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;
418 *y = pPos->Y();
419 *push_in = false;
421 #endif
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
440 UpdateFull();
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);
445 #else
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);
449 #endif
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);
459 #else
460 g_signal_connect(G_OBJECT(mpMenuWidget), "deactivate", G_CALLBACK(MenuClosed), pLoop);
461 #endif
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
467 // visible tooltip.
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);
487 else
488 gtk_popover_set_position(GTK_POPOVER(mpMenuWidget), GTK_POS_BOTTOM);
490 gtk_popover_popup(GTK_POPOVER(mpMenuWidget));
491 #else
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);
520 else
521 #endif
523 guint nButton;
524 guint32 nTime;
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
528 //doesn't)
529 GdkEvent *pEvent = gtk_get_current_event();
530 if (pEvent)
532 gdk_event_get_button(pEvent, &nButton);
533 nTime = gdk_event_get_time(pEvent);
535 else
537 nButton = 0;
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);
551 #endif
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);
565 #else
566 gtk_widget_unparent(mpMenuWidget);
567 #endif
568 mpMenuWidget = nullptr;
570 gtk_widget_insert_action_group(mpFrame->getMouseEventWidget(), "win", nullptr);
572 // undo tooltip blocking
573 mpFrame->UnblockTooltip();
575 mpFrame = nullptr;
577 return true;
581 * GtkSalMenu
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 ),
598 mpFrame( 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!");
612 if (!mpFrame)
613 return;
614 SetFrame(mpFrame);
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
628 // structure update
629 if (mbMenuBar && !maUpdateMenuBarIdle.IsActive())
630 maUpdateMenuBarIdle.Start();
633 void GtkSalMenu::SetMenuModel(GMenuModel* pMenuModel)
635 if (mpMenuModel)
636 g_object_unref(mpMenuModel);
637 mpMenuModel = pMenuModel;
638 if (mpMenuModel)
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();
652 if (mpMenuModel)
653 g_object_unref(mpMenuModel);
655 if (mpFrame)
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 );
671 else
672 maItems.insert( maItems.begin() + nPos, pItem );
674 pItem->mpParentMenu = this;
676 SetNeedsUpdate();
679 void GtkSalMenu::RemoveItem( unsigned nPos )
681 SolarMutexGuard aGuard;
683 // tdf#140225 clear associated action when the item is removed
684 if (mpActionGroup)
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 );
692 SetNeedsUpdate();
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 )
702 return;
704 pGtkSubMenu->mpParentSalMenu = this;
705 pItem->mpSubMenu = pGtkSubMenu;
707 SetNeedsUpdate();
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);
722 #else
723 gtk_button_set_has_frame(GTK_BUTTON(pButton), false);
724 gtk_widget_set_focus_on_click(pButton, false);
725 #endif
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);
741 #else
742 gtk_button_set_child(GTK_BUTTON(pButton), pImage);
743 #endif
744 return pButton;
747 void GtkSalMenu::ShowCloseButton(bool bShow)
749 assert(mbMenuBar);
750 if (!mpMenuBarContainerWidget)
751 return;
753 if (!bShow)
755 if (mpCloseButton)
757 #if !GTK_CHECK_VERSION(4, 0, 0)
758 gtk_widget_destroy(mpCloseButton);
759 #else
760 g_clear_pointer(&mpCloseButton, gtk_widget_unparent);
761 #endif
762 mpCloseButton = nullptr;
764 return;
767 if (mpCloseButton)
768 return;
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);
773 #else
774 GtkWidget* pImage = gtk_image_new_from_gicon(pIcon);
775 #endif
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);
791 namespace
793 void DestroyMemoryStream(gpointer data)
795 SvMemoryStream* pMemStm = static_cast<SvMemoryStream*>(data);
796 delete pMemStm;
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)
808 if (!mbMenuBar)
809 return false;
811 if (!mpMenuBarContainerWidget)
812 return false;
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(),
823 pMemStm->TellEnd(),
824 DestroyMemoryStream,
825 pMemStm);
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);
830 #else
831 pImage = gtk_image_new_from_gicon(pIcon);
832 #endif
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);
848 if (mpCloseButton)
850 gtk_grid_insert_next_to(GTK_GRID(mpMenuBarContainerWidget), mpCloseButton, GTK_POS_LEFT);
851 gtk_grid_attach_next_to(GTK_GRID(mpMenuBarContainerWidget), pButton, mpCloseButton,
852 GTK_POS_LEFT, 1, 1);
854 else
855 gtk_grid_attach(GTK_GRID(mpMenuBarContainerWidget), pButton, 1, 0, 1, 1);
857 return true;
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())
865 return;
867 gint nAttach(0);
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);
871 #else
872 gtk_grid_query_child(GTK_GRID(mpMenuBarContainerWidget), it->second, &nAttach, nullptr, nullptr, nullptr);
873 g_clear_pointer(&(it->second), gtk_widget_unparent);
874 #endif
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);
893 gtk_coord x, y;
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()
911 if (mbAddedGrab)
913 #if !GTK_CHECK_VERSION(4, 0, 0)
914 gtk_grab_remove(mpMenuBarWidget);
915 #endif
916 mbAddedGrab = false;
918 if (!mbReturnFocusToDocument)
919 gtk_widget_grab_focus(mpFrame->getMouseEventWidget());
920 else
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
935 //to the next pane
937 return false;
939 #endif
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)
956 return false;
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
969 //auto-popped down
970 gtk_grab_add(mpMenuBarWidget);
972 mbAddedGrab = true;
973 gtk_menu_shell_select_first(GTK_MENU_SHELL(mpMenuBarWidget), false);
974 gtk_menu_shell_deselect(GTK_MENU_SHELL(mpMenuBarWidget));
975 #endif
976 mbReturnFocusToDocument = true;
977 return 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);
993 #endif
995 void GtkSalMenu::CreateMenuBarWidget()
997 if (mpMenuBarContainerWidget)
998 return;
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);
1012 #else
1013 mpMenuAllowShrinkWidget = gtk_scrolled_window_new();
1014 gtk_scrolled_window_set_has_frame(GTK_SCROLLED_WINDOW(mpMenuAllowShrinkWidget), false);
1015 #endif
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);
1025 #else
1026 mpMenuBarWidget = gtk_popover_menu_bar_new_from_model(mpMenuModel);
1027 #endif
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);
1034 #else
1035 gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(mpMenuAllowShrinkWidget), mpMenuBarWidget);
1036 #endif
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);
1041 #endif
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)
1053 return;
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);
1061 #else
1062 g_clear_pointer(&mpMenuBarContainerWidget, gtk_widget_unparent);
1063 #endif
1064 mpMenuBarContainerWidget = nullptr;
1065 mpMenuBarWidget = nullptr;
1066 mpCloseButton = nullptr;
1069 void GtkSalMenu::SetFrame(const SalFrame* pFrame)
1071 SolarMutexGuard aGuard;
1072 assert(mbMenuBar);
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
1078 // actiongroup
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);
1090 if ( pMenuModel )
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() );
1098 if ( pActionGroup )
1100 g_lo_action_group_clear( pActionGroup );
1101 mpActionGroup = G_ACTION_GROUP( pActionGroup );
1104 // Generate the main menu structure.
1105 if ( PrepUpdate() )
1106 UpdateFull();
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 )
1131 return;
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( "" );
1142 else
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 );
1155 else
1157 g_variant_unref (pCheckValue);
1161 if ( pCurrentState != nullptr )
1162 g_variant_unref( pCurrentState );
1165 if ( aCommand )
1166 g_free( aCommand );
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 );
1193 if ( aLabel )
1194 g_free( aLabel );
1197 void GtkSalMenu::NativeSetItemIcon( unsigned nSection, unsigned nItemPos, const Image& rImage )
1199 #if GLIB_CHECK_VERSION(2,38,0)
1200 if (!rImage && mbHasNullItemIcon)
1201 return;
1203 SolarMutexGuard aGuard;
1205 if (!!rImage)
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(),
1213 pMemStm->TellEnd(),
1214 DestroyMemoryStream,
1215 pMemStm);
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;
1224 else
1226 g_lo_menu_set_icon_to_item_in_section( G_LO_MENU( mpMenuModel ), nSection, nItemPos, nullptr );
1227 mbHasNullItemIcon = true;
1229 #else
1230 (void)nSection;
1231 (void)nItemPos;
1232 (void)rImage;
1233 #endif
1236 void GtkSalMenu::NativeSetAccelerator( unsigned nSection, unsigned nItemPos, const vcl::KeyCode& rKeyCode, std::u16string_view rKeyName )
1238 SolarMutexGuard aGuard;
1240 if ( rKeyName.empty() )
1241 return;
1243 guint nKeyCode;
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,
1259 unsigned nItemPos,
1260 sal_uInt16 nId,
1261 const gchar* aCommand,
1262 MenuItemBits nBits,
1263 bool bChecked,
1264 bool bIsSubmenu )
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 );
1294 else
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);
1318 g_free(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 );
1327 if ( bIsSubmenu )
1328 g_lo_menu_set_submenu_action_to_item_in_section( pMenu, nSection, nItemPos, aItemCommand );
1329 else
1331 g_lo_menu_set_action_and_target_value_to_item_in_section( pMenu, nSection, nItemPos, aItemCommand, pTarget );
1332 pTarget = nullptr;
1334 if (bOldHasSubmenu)
1335 g_object_unref(pSubMenuModel);
1337 g_free( aItemCommand );
1340 if ( aCurrentCommand )
1341 g_free( aCurrentCommand );
1343 if (pTarget)
1344 g_variant_unref(pTarget);
1346 return bSubMenuAddedOrRemoved;
1349 GtkSalMenu* GtkSalMenu::GetTopLevel()
1351 GtkSalMenu *pMenu = this;
1352 while (pMenu->mpParentSalMenu)
1353 pMenu = pMenu->mpParentSalMenu;
1354 return pMenu;
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));
1375 #endif
1377 if (pTopLevel->mpMenuWidget)
1379 #if GTK_CHECK_VERSION(4, 0, 0)
1380 gtk_popover_popdown(GTK_POPOVER(pTopLevel->mpMenuWidget));
1381 #else
1382 gtk_menu_shell_deactivate(GTK_MENU_SHELL(pTopLevel->mpMenuWidget));
1383 #endif
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)
1395 return;
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);
1407 Update();
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())
1430 return;
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())
1447 return;
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());
1460 if (bEnable)
1462 DestroyMenuBarWidget();
1463 UpdateFull();
1464 if (!bDisplayable)
1465 ShowMenuBar(false);
1467 else
1469 Update();
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,
1479 if (bUnityMode)
1481 if (bVisible)
1482 Update();
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);
1486 else if (bVisible)
1487 CreateMenuBarWidget();
1488 else
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;
1500 return bVisible;
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 )
1524 Update();
1528 void GtkSalMenu::SetItemText( unsigned nPos, SalMenuItem* pSalMenuItem, const OUString& rText )
1530 SolarMutexGuard aGuard;
1531 if ( !bUnityMode || mbInActivateCallback || mbNeedsUpdate || !GetTopLevel()->mbMenuBar || ( nPos >= maItems.size() ) )
1532 return;
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 );
1548 return;
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;
1570 * GtkSalMenuItem
1573 GtkSalMenuItem::GtkSalMenuItem( const SalItemParams* pItemData ) :
1574 mpParentMenu( nullptr ),
1575 mpSubMenu( nullptr ),
1576 mnType( pItemData->eType ),
1577 mnId( pItemData->nId ),
1578 mbVisible( true )
1582 GtkSalMenuItem::~GtkSalMenuItem()
1586 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */