diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 01:13:27 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 01:13:27 +0000 |
commit | 40a355a42d4a9444dc753c04c6608dade2f06a23 (patch) | |
tree | 871fc667d2de662f171103ce5ec067014ef85e61 /widget/gtk/NativeMenuGtk.cpp | |
parent | Adding upstream version 124.0.1. (diff) | |
download | firefox-adbda400be353e676059e335c3c0aaf99e719475.tar.xz firefox-adbda400be353e676059e335c3c0aaf99e719475.zip |
Adding upstream version 125.0.1.upstream/125.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'widget/gtk/NativeMenuGtk.cpp')
-rw-r--r-- | widget/gtk/NativeMenuGtk.cpp | 451 |
1 files changed, 427 insertions, 24 deletions
diff --git a/widget/gtk/NativeMenuGtk.cpp b/widget/gtk/NativeMenuGtk.cpp index 9d413d475e..3f8aeb1940 100644 --- a/widget/gtk/NativeMenuGtk.cpp +++ b/widget/gtk/NativeMenuGtk.cpp @@ -4,6 +4,9 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "NativeMenuGtk.h" +#include "AsyncDBus.h" +#include "gdk/gdkkeysyms-compat.h" +#include "mozilla/BasicEvents.h" #include "mozilla/dom/Document.h" #include "mozilla/dom/DocumentInlines.h" #include "mozilla/dom/XULCommandEvent.h" @@ -15,6 +18,10 @@ #include "nsStubMutationObserver.h" #include "mozilla/dom/Element.h" #include "mozilla/StaticPrefs_widget.h" +#include "DBusMenu.h" +#include "nsLayoutUtils.h" +#include "nsGtkUtils.h" +#include "nsGtkKeyUtils.h" #include <dlfcn.h> #include <gtk/gtk.h> @@ -35,7 +42,8 @@ static bool IsDisabled(const dom::Element& aElement) { } static bool NodeIsRelevant(const nsINode& aNode) { return aNode.IsAnyOfXULElements(nsGkAtoms::menu, nsGkAtoms::menuseparator, - nsGkAtoms::menuitem, nsGkAtoms::menugroup); + nsGkAtoms::menuitem, nsGkAtoms::menugroup, + nsGkAtoms::menubar); } // If this is a radio / checkbox menuitem, get the current value. @@ -155,7 +163,7 @@ void Actions::Clear() { mNextActionIndex = 0; } -class MenuModel final : public nsStubMutationObserver { +class MenuModel : public nsStubMutationObserver { NS_DECL_ISUPPORTS NS_DECL_NSIMUTATIONOBSERVER_CONTENTREMOVED @@ -166,41 +174,60 @@ class MenuModel final : public nsStubMutationObserver { public: explicit MenuModel(dom::Element* aElement) : mElement(aElement) { mElement->AddMutationObserver(this); - mGMenu = dont_AddRef(g_menu_new()); - mActions.mGroup = dont_AddRef(g_simple_action_group_new()); - } - - GMenuModel* GetModel() { return G_MENU_MODEL(mGMenu.get()); } - GActionGroup* GetActionGroup() { - return G_ACTION_GROUP(mActions.mGroup.get()); } dom::Element* Element() { return mElement; } - void RecomputeModelIfNeeded(); + void RecomputeModelIfNeeded() { + if (!mDirty) { + return; + } + RecomputeModel(); + mDirty = false; + } - bool IsShowing() { return mPoppedUp; } + bool IsShowing() { return mShowing; } void WillShow() { - mPoppedUp = true; + mShowing = true; RecomputeModelIfNeeded(); } - void DidHide() { mPoppedUp = false; } + void DidHide() { mShowing = false; } - private: + protected: + virtual void RecomputeModel() = 0; virtual ~MenuModel() { mElement->RemoveMutationObserver(this); } void DirtyModel() { mDirty = true; - if (mPoppedUp) { + if (mShowing) { RecomputeModelIfNeeded(); } } RefPtr<dom::Element> mElement; + bool mDirty = true; + bool mShowing = false; +}; + +class MenuModelGMenu final : public MenuModel { + public: + explicit MenuModelGMenu(dom::Element* aElement) : MenuModel(aElement) { + mGMenu = dont_AddRef(g_menu_new()); + mActions.mGroup = dont_AddRef(g_simple_action_group_new()); + } + + GMenuModel* GetModel() { return G_MENU_MODEL(mGMenu.get()); } + GActionGroup* GetActionGroup() { + return G_ACTION_GROUP(mActions.mGroup.get()); + } + + protected: + void RecomputeModel() override; + static void RecomputeModelFor(GMenu* aMenu, Actions& aActions, + const dom::Element& aElement); + RefPtr<GMenu> mGMenu; Actions mActions; - bool mDirty = true; - bool mPoppedUp = false; }; NS_IMPL_ISUPPORTS(MenuModel, nsIMutationObserver) @@ -243,8 +270,8 @@ static const dom::Element* GetMenuPopupChild(const dom::Element& aElement) { return nullptr; } -static void RecomputeModelFor(GMenu* aMenu, Actions& aActions, - const dom::Element& aElement) { +void MenuModelGMenu::RecomputeModelFor(GMenu* aMenu, Actions& aActions, + const dom::Element& aElement) { RefPtr<GMenu> sectionMenu; auto FlushSectionMenu = [&] { if (sectionMenu) { @@ -305,10 +332,7 @@ static void RecomputeModelFor(GMenu* aMenu, Actions& aActions, FlushSectionMenu(); } -void MenuModel::RecomputeModelIfNeeded() { - if (!mDirty) { - return; - } +void MenuModelGMenu::RecomputeModel() { mActions.Clear(); g_menu_remove_all(mGMenu.get()); RecomputeModelFor(mGMenu.get(), mActions, *mElement); @@ -341,7 +365,7 @@ METHOD_SIGNAL(Unmap); #undef METHOD_SIGNAL NativeMenuGtk::NativeMenuGtk(dom::Element* aElement) - : mMenuModel(MakeRefPtr<MenuModel>(aElement)) { + : mMenuModel(MakeRefPtr<MenuModelGMenu>(aElement)) { // Floating, so no need to dont_AddRef. mNativeMenu = gtk_menu_new_from_model(mMenuModel->GetModel()); gtk_widget_insert_action_group(mNativeMenu.get(), "menu", @@ -421,4 +445,383 @@ void NativeMenuGtk::CloseSubmenu(dom::Element*) { // TODO: For testing mostly. } +#ifdef MOZ_ENABLE_DBUS + +class MenubarModelDBus final : public MenuModel { + public: + explicit MenubarModelDBus(dom::Element* aElement) : MenuModel(aElement) { + mRoot = dont_AddRef(dbusmenu_menuitem_new()); + dbusmenu_menuitem_set_root(mRoot.get(), true); + mShowing = true; + } + + DbusmenuMenuitem* Root() const { return mRoot.get(); } + + protected: + void RecomputeModel() override; + static void AppendMenuItem(DbusmenuMenuitem* aParent, + const dom::Element* aElement); + static void AppendSeparator(DbusmenuMenuitem* aParent); + static void AppendSubmenu(DbusmenuMenuitem* aParent, + const dom::Element* aMenu, + const dom::Element* aPopup); + static uint RecomputeModelFor(DbusmenuMenuitem* aParent, + const dom::Element& aElement); + + RefPtr<DbusmenuMenuitem> mRoot; +}; + +void MenubarModelDBus::RecomputeModel() { + while (GList* children = dbusmenu_menuitem_get_children(mRoot.get())) { + auto* first = static_cast<DbusmenuMenuitem*>(children->data); + if (!first) { + break; + } + dbusmenu_menuitem_child_delete(mRoot.get(), first); + } + RecomputeModelFor(mRoot, *Element()); +} + +static const dom::Element* RelevantElementForKeys( + const dom::Element* aElement) { + nsAutoString key; + aElement->GetAttr(nsGkAtoms::key, key); + if (!key.IsEmpty()) { + dom::Document* document = aElement->OwnerDoc(); + dom::Element* element = document->GetElementById(key); + if (element) { + return element; + } + } + return aElement; +} + +static uint32_t ParseKey(const nsAString& aKey, const nsAString& aKeyCode) { + guint key = 0; + if (!aKey.IsEmpty()) { + key = gdk_unicode_to_keyval(*aKey.BeginReading()); + } + + if (key == 0 && !aKeyCode.IsEmpty()) { + key = KeymapWrapper::ConvertGeckoKeyCodeToGDKKeyval(aKeyCode); + } + + return key; +} + +static uint32_t KeyFrom(const dom::Element* aElement) { + const auto* element = RelevantElementForKeys(aElement); + + nsAutoString key; + nsAutoString keycode; + element->GetAttr(nsGkAtoms::key, key); + element->GetAttr(nsGkAtoms::keycode, keycode); + + return ParseKey(key, keycode); +} + +// TODO(emilio): Unify with nsMenuUtilsX::GeckoModifiersForNodeAttribute (or +// at least switch to strtok_r). +static uint32_t ParseModifiers(const nsAString& aModifiers) { + if (aModifiers.IsEmpty()) { + return 0; + } + + uint32_t modifier = 0; + char* str = ToNewUTF8String(aModifiers); + char* token = strtok(str, ", \t"); + while (token) { + if (nsCRT::strcmp(token, "shift") == 0) { + modifier |= GDK_SHIFT_MASK; + } else if (nsCRT::strcmp(token, "alt") == 0) { + modifier |= GDK_MOD1_MASK; + } else if (nsCRT::strcmp(token, "meta") == 0) { + modifier |= GDK_META_MASK; + } else if (nsCRT::strcmp(token, "control") == 0) { + modifier |= GDK_CONTROL_MASK; + } else if (nsCRT::strcmp(token, "accel") == 0) { + auto accel = WidgetInputEvent::AccelModifier(); + if (accel == MODIFIER_META) { + modifier |= GDK_META_MASK; + } else if (accel == MODIFIER_ALT) { + modifier |= GDK_MOD1_MASK; + } else if (accel == MODIFIER_CONTROL) { + modifier |= GDK_CONTROL_MASK; + } + } + + token = strtok(nullptr, ", \t"); + } + + free(str); + + return modifier; +} + +static uint32_t ModifiersFrom(const dom::Element* aContent) { + const auto* element = RelevantElementForKeys(aContent); + + nsAutoString modifiers; + element->GetAttr(nsGkAtoms::modifiers, modifiers); + + return ParseModifiers(modifiers); +} + +static void UpdateAccel(DbusmenuMenuitem* aItem, const nsIContent* aContent) { + uint32_t key = KeyFrom(aContent->AsElement()); + if (key != 0) { + dbusmenu_menuitem_property_set_shortcut( + aItem, key, + static_cast<GdkModifierType>(ModifiersFrom(aContent->AsElement()))); + } +} + +static void UpdateRadioOrCheck(DbusmenuMenuitem* aItem, + const dom::Element* aContent) { + static mozilla::dom::Element::AttrValuesArray attrs[] = { + nsGkAtoms::checkbox, nsGkAtoms::radio, nullptr}; + int32_t type = aContent->FindAttrValueIn(kNameSpaceID_None, nsGkAtoms::type, + attrs, eCaseMatters); + + if (type < 0 || type >= 2) { + return; + } + + if (type == 0) { + dbusmenu_menuitem_property_set(aItem, DBUSMENU_MENUITEM_PROP_TOGGLE_TYPE, + DBUSMENU_MENUITEM_TOGGLE_CHECK); + } else { + dbusmenu_menuitem_property_set(aItem, DBUSMENU_MENUITEM_PROP_TOGGLE_TYPE, + DBUSMENU_MENUITEM_TOGGLE_RADIO); + } + + bool isChecked = aContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::checked, + nsGkAtoms::_true, eCaseMatters); + dbusmenu_menuitem_property_set_int( + aItem, DBUSMENU_MENUITEM_PROP_TOGGLE_STATE, + isChecked ? DBUSMENU_MENUITEM_TOGGLE_STATE_CHECKED + : DBUSMENU_MENUITEM_TOGGLE_STATE_UNCHECKED); +} + +static void UpdateEnabled(DbusmenuMenuitem* aItem, const nsIContent* aContent) { + bool disabled = aContent->AsElement()->AttrValueIs( + kNameSpaceID_None, nsGkAtoms::disabled, nsGkAtoms::_true, eCaseMatters); + + dbusmenu_menuitem_property_set_bool(aItem, DBUSMENU_MENUITEM_PROP_ENABLED, + !disabled); +} + +// we rebuild the dbus model when elements are removed from the DOM, +// so this isn't going to trigger for asynchronous +static MOZ_CAN_RUN_SCRIPT void DBusActivationCallback( + DbusmenuMenuitem* aMenuitem, guint aTimestamp, gpointer aUserData) { + RefPtr element = static_cast<dom::Element*>(aUserData); + ActivateItem(*element); +} + +static void ConnectActivated(DbusmenuMenuitem* aItem, + const dom::Element* aContent) { + g_signal_connect(G_OBJECT(aItem), DBUSMENU_MENUITEM_SIGNAL_ITEM_ACTIVATED, + G_CALLBACK(DBusActivationCallback), + const_cast<dom::Element*>(aContent)); +} + +static MOZ_CAN_RUN_SCRIPT void DBusAboutToShowCallback( + DbusmenuMenuitem* aMenuitem, gpointer aUserData) { + RefPtr element = static_cast<dom::Element*>(aUserData); + FireEvent(element, eXULPopupShowing); + FireEvent(element, eXULPopupShown); +} + +static void ConnectAboutToShow(DbusmenuMenuitem* aItem, + const dom::Element* aContent) { + g_signal_connect(G_OBJECT(aItem), DBUSMENU_MENUITEM_SIGNAL_ABOUT_TO_SHOW, + G_CALLBACK(DBusAboutToShowCallback), + const_cast<dom::Element*>(aContent)); +} + +void MenubarModelDBus::AppendMenuItem(DbusmenuMenuitem* aParent, + const dom::Element* aChild) { + nsAutoString label; + aChild->GetAttr(nsGkAtoms::label, label); + if (label.IsEmpty()) { + aChild->GetAttr(nsGkAtoms::aria_label, label); + } + RefPtr<DbusmenuMenuitem> child = dont_AddRef(dbusmenu_menuitem_new()); + dbusmenu_menuitem_property_set(child, DBUSMENU_MENUITEM_PROP_LABEL, + NS_ConvertUTF16toUTF8(label).get()); + dbusmenu_menuitem_child_append(aParent, child); + UpdateAccel(child, aChild); + UpdateRadioOrCheck(child, aChild); + UpdateEnabled(child, aChild); + ConnectActivated(child, aChild); + // TODO: icons +} + +void MenubarModelDBus::AppendSeparator(DbusmenuMenuitem* aParent) { + RefPtr<DbusmenuMenuitem> child = dont_AddRef(dbusmenu_menuitem_new()); + dbusmenu_menuitem_property_set(child, DBUSMENU_MENUITEM_PROP_TYPE, + "separator"); + dbusmenu_menuitem_child_append(aParent, child); +} + +void MenubarModelDBus::AppendSubmenu(DbusmenuMenuitem* aParent, + const dom::Element* aMenu, + const dom::Element* aPopup) { + RefPtr<DbusmenuMenuitem> submenu = dont_AddRef(dbusmenu_menuitem_new()); + if (RecomputeModelFor(submenu, *aPopup) == 0) { + RefPtr<DbusmenuMenuitem> placeholder = dont_AddRef(dbusmenu_menuitem_new()); + dbusmenu_menuitem_child_append(submenu, placeholder); + } + nsAutoString label; + aMenu->GetAttr(nsGkAtoms::label, label); + ConnectAboutToShow(submenu, aPopup); + dbusmenu_menuitem_property_set(submenu, DBUSMENU_MENUITEM_PROP_LABEL, + NS_ConvertUTF16toUTF8(label).get()); + dbusmenu_menuitem_child_append(aParent, submenu); +} + +uint MenubarModelDBus::RecomputeModelFor(DbusmenuMenuitem* aParent, + const dom::Element& aElement) { + uint childCount = 0; + for (const nsIContent* child = aElement.GetFirstChild(); child; + child = child->GetNextSibling()) { + if (child->IsXULElement(nsGkAtoms::menuitem) && + !IsDisabled(*child->AsElement())) { + AppendMenuItem(aParent, child->AsElement()); + childCount++; + continue; + } + if (child->IsXULElement(nsGkAtoms::menuseparator)) { + AppendSeparator(aParent); + childCount++; + continue; + } + if (child->IsXULElement(nsGkAtoms::menu) && + !IsDisabled(*child->AsElement())) { + if (const auto* popup = GetMenuPopupChild(*child->AsElement())) { + childCount++; + AppendSubmenu(aParent, child->AsElement(), popup); + } + } + } + return childCount; +} + +void DBusMenuBar::NameOwnerChangedCallback(GObject*, GParamSpec*, + gpointer user_data) { + static_cast<DBusMenuBar*>(user_data)->OnNameOwnerChanged(); +} + +void DBusMenuBar::OnNameOwnerChanged() { + GUniquePtr<gchar> nameOwner(g_dbus_proxy_get_name_owner(mProxy)); + if (!nameOwner) { + return; + } + + RefPtr win = mMenuModel->Element()->OwnerDoc()->GetInnerWindow(); + if (NS_WARN_IF(!win)) { + return; + } + nsIWidget* widget = nsGlobalWindowInner::Cast(win.get())->GetNearestWidget(); + if (NS_WARN_IF(!widget)) { + return; + } + auto* gdkWin = + static_cast<GdkWindow*>(widget->GetNativeData(NS_NATIVE_WINDOW)); + if (NS_WARN_IF(!gdkWin)) { + return; + } + +# ifdef MOZ_WAYLAND + if (auto* display = widget::WaylandDisplayGet()) { + if (!StaticPrefs::widget_gtk_global_menu_wayland_enabled()) { + return; + } + xdg_dbus_annotation_manager_v1* annotationManager = + display->GetXdgDbusAnnotationManager(); + if (NS_WARN_IF(!annotationManager)) { + return; + } + + wl_surface* surface = gdk_wayland_window_get_wl_surface(gdkWin); + if (NS_WARN_IF(!surface)) { + return; + } + + GDBusConnection* connection = g_dbus_proxy_get_connection(mProxy); + const char* myServiceName = g_dbus_connection_get_unique_name(connection); + if (NS_WARN_IF(!myServiceName)) { + return; + } + + // FIXME(emilio, bug 1883209): Nothing deletes this as of right now. + mAnnotation = xdg_dbus_annotation_manager_v1_create_surface( + annotationManager, "com.canonical.dbusmenu", surface); + + xdg_dbus_annotation_v1_set_address(mAnnotation, myServiceName, + mObjectPath.get()); + return; + } +# endif +# ifdef MOZ_X11 + // legacy path + auto xid = GDK_WINDOW_XID(gdkWin); + widget::DBusProxyCall(mProxy, "RegisterWindow", + g_variant_new("(uo)", xid, mObjectPath.get()), + G_DBUS_CALL_FLAGS_NONE) + ->Then( + GetCurrentSerialEventTarget(), __func__, + [self = RefPtr{this}](RefPtr<GVariant>&& aResult) { + self->mMenuModel->Element()->SetBoolAttr(nsGkAtoms::hidden, true); + }, + [self = RefPtr{this}](GUniquePtr<GError>&& aError) { + g_printerr("Failed to register window menubar: %s\n", + aError->message); + self->mMenuModel->Element()->SetBoolAttr(nsGkAtoms::hidden, false); + }); +# endif +} + +static unsigned sID = 0; + +DBusMenuBar::DBusMenuBar(dom::Element* aElement) + : mObjectPath(nsPrintfCString("/com/canonical/menu/%u", sID++)), + mMenuModel(MakeRefPtr<MenubarModelDBus>(aElement)), + mServer(dont_AddRef(dbusmenu_server_new(mObjectPath.get()))) { + mMenuModel->RecomputeModelIfNeeded(); + dbusmenu_server_set_root(mServer.get(), mMenuModel->Root()); +} + +RefPtr<DBusMenuBar> DBusMenuBar::Create(dom::Element* aElement) { + RefPtr<DBusMenuBar> self = new DBusMenuBar(aElement); + widget::CreateDBusProxyForBus( + G_BUS_TYPE_SESSION, + GDBusProxyFlags(G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES | + G_DBUS_PROXY_FLAGS_DO_NOT_CONNECT_SIGNALS | + G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START), + nullptr, "com.canonical.AppMenu.Registrar", + "/com/canonical/AppMenu/Registrar", "com.canonical.AppMenu.Registrar") + ->Then( + GetCurrentSerialEventTarget(), __func__, + [self](RefPtr<GDBusProxy>&& aProxy) { + self->mProxy = std::move(aProxy); + g_signal_connect(self->mProxy, "notify::g-name-owner", + G_CALLBACK(NameOwnerChangedCallback), self.get()); + self->OnNameOwnerChanged(); + }, + [](GUniquePtr<GError>&& aError) { + g_printerr("Failed to create DBUS proxy for menubar: %s\n", + aError->message); + }); + return self; +} + +DBusMenuBar::~DBusMenuBar() { +# ifdef MOZ_WAYLAND + MozClearPointer(mAnnotation, xdg_dbus_annotation_v1_destroy); +# endif +} +#endif + } // namespace mozilla::widget |