diff options
Diffstat (limited to 'widget/gtk/nsMenu.cpp')
-rw-r--r-- | widget/gtk/nsMenu.cpp | 800 |
1 files changed, 800 insertions, 0 deletions
diff --git a/widget/gtk/nsMenu.cpp b/widget/gtk/nsMenu.cpp new file mode 100644 index 0000000000..073a4acf65 --- /dev/null +++ b/widget/gtk/nsMenu.cpp @@ -0,0 +1,800 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#define _IMPL_NS_LAYOUT + +#include "mozilla/dom/Element.h" +#include "mozilla/Assertions.h" +#include "mozilla/GuardObjects.h" +#include "mozilla/MouseEvents.h" +#include "mozilla/Move.h" +#include "mozilla/StyleSetHandleInlines.h" +#include "nsAutoPtr.h" +#include "nsBindingManager.h" +#include "nsComponentManagerUtils.h" +#include "nsContentUtils.h" +#include "nsCSSValue.h" +#include "nsGkAtoms.h" +#include "nsGtkUtils.h" +#include "nsIAtom.h" +#include "nsIContent.h" +#include "nsIDocument.h" +#include "nsIPresShell.h" +#include "nsIRunnable.h" +#include "nsITimer.h" +#include "nsString.h" +#include "nsStyleContext.h" +#include "nsStyleSet.h" +#include "nsStyleStruct.h" +#include "nsThreadUtils.h" +#include "nsXBLBinding.h" +#include "nsXBLService.h" + +#include "nsNativeMenuAtoms.h" +#include "nsNativeMenuDocListener.h" + +#include <glib-object.h> + +#include "nsMenu.h" + +using namespace mozilla; + +class nsMenuContentInsertedEvent : public Runnable { +public: + nsMenuContentInsertedEvent(nsMenu* aMenu, + nsIContent* aContainer, + nsIContent* aChild, + nsIContent* aPrevSibling) : + mWeakMenu(aMenu), + mContainer(aContainer), + mChild(aChild), + mPrevSibling(aPrevSibling) { } + + NS_IMETHODIMP Run() { + if (!mWeakMenu) { + return NS_OK; + } + + static_cast<nsMenu *>(mWeakMenu.get())->HandleContentInserted(mContainer, + mChild, + mPrevSibling); + return NS_OK; + } + +private: + nsWeakMenuObject mWeakMenu; + + nsCOMPtr<nsIContent> mContainer; + nsCOMPtr<nsIContent> mChild; + nsCOMPtr<nsIContent> mPrevSibling; +}; + +class nsMenuContentRemovedEvent : public Runnable { +public: + nsMenuContentRemovedEvent(nsMenu* aMenu, + nsIContent* aContainer, + nsIContent* aChild) : + mWeakMenu(aMenu), + mContainer(aContainer), + mChild(aChild) { } + + NS_IMETHODIMP Run() { + if (!mWeakMenu) { + return NS_OK; + } + + static_cast<nsMenu *>(mWeakMenu.get())->HandleContentRemoved(mContainer, + mChild); + return NS_OK; + } + +private: + nsWeakMenuObject mWeakMenu; + + nsCOMPtr<nsIContent> mContainer; + nsCOMPtr<nsIContent> mChild; +}; + +static void +DispatchMouseEvent(nsIContent* aTarget, mozilla::EventMessage aMsg) { + if (!aTarget) { + return; + } + + WidgetMouseEvent event(true, aMsg, nullptr, WidgetMouseEvent::eReal); + aTarget->DispatchDOMEvent(&event, nullptr, nullptr, nullptr); +} + +static void +AttachXBLBindings(nsIContent* aContent) { + nsIDocument* doc = aContent->OwnerDoc(); + nsIPresShell* shell = doc->GetShell(); + if (!shell) { + return; + } + + RefPtr<nsStyleContext> sc = + shell->StyleSet()->AsGecko()->ResolveStyleFor(aContent->AsElement(), + nullptr); + if (!sc) { + return; + } + + const nsStyleDisplay* display = sc->StyleDisplay(); + if (!display->mBinding) { + return; + } + + nsXBLService* xbl = nsXBLService::GetInstance(); + if (!xbl) { + return; + } + + RefPtr<nsXBLBinding> binding; + bool dummy; + nsresult rv = xbl->LoadBindings(aContent, display->mBinding->GetURI(), + display->mBinding->mOriginPrincipal, + getter_AddRefs(binding), &dummy); + if ((NS_FAILED(rv) && rv != NS_ERROR_XBL_BLOCKED) || !binding) { + return; + } + + doc->BindingManager()->AddToAttachedQueue(binding); +} + +void +nsMenu::SetPopupState(EPopupState aState) { + mPopupState = aState; + + if (!mPopupContent) { + return; + } + + nsAutoString state; + switch (aState) { + case ePopupState_Showing: + state.Assign(NS_LITERAL_STRING("showing")); + break; + case ePopupState_Open: + state.Assign(NS_LITERAL_STRING("open")); + break; + case ePopupState_Hiding: + state.Assign(NS_LITERAL_STRING("hiding")); + break; + default: + break; + } + + if (state.IsEmpty()) { + mPopupContent->UnsetAttr(kNameSpaceID_None, + nsNativeMenuAtoms::_moz_nativemenupopupstate, + false); + } else { + mPopupContent->SetAttr(kNameSpaceID_None, + nsNativeMenuAtoms::_moz_nativemenupopupstate, + state, false); + } +} + +/* static */ void +nsMenu::DoOpenCallback(nsITimer* aTimer, void* aClosure) { + nsMenu* self = static_cast<nsMenu *>(aClosure); + + dbusmenu_menuitem_show_to_user(self->GetNativeData(), 0); + + self->mOpenDelayTimer = nullptr; +} + +/* static */ void +nsMenu::menu_event_cb(DbusmenuMenuitem* menu, + const gchar* name, + GVariant* value, + guint timestamp, + gpointer user_data) { + nsMenu* self = static_cast<nsMenu *>(user_data); + + nsAutoCString event(name); + + if (event.Equals(NS_LITERAL_CSTRING("closed"))) { + self->OnClose(); + return; + } + + if (event.Equals(NS_LITERAL_CSTRING("opened"))) { + self->OnOpen(); + return; + } +} + +void +nsMenu::MaybeAddPlaceholderItem() { + MOZ_ASSERT(!IsInBatchedUpdate(), + "Shouldn't be modifying the native menu structure now"); + + GList* children = dbusmenu_menuitem_get_children(GetNativeData()); + if (!children) { + MOZ_ASSERT(!mPlaceholderItem); + + mPlaceholderItem = dbusmenu_menuitem_new(); + if (!mPlaceholderItem) { + return; + } + + dbusmenu_menuitem_property_set_bool(mPlaceholderItem, + DBUSMENU_MENUITEM_PROP_VISIBLE, + false); + + MOZ_ALWAYS_TRUE( + dbusmenu_menuitem_child_append(GetNativeData(), mPlaceholderItem)); + } +} + +void +nsMenu::EnsureNoPlaceholderItem() { + MOZ_ASSERT(!IsInBatchedUpdate(), + "Shouldn't be modifying the native menu structure now"); + + if (!mPlaceholderItem) { + return; + } + + MOZ_ALWAYS_TRUE( + dbusmenu_menuitem_child_delete(GetNativeData(), mPlaceholderItem)); + MOZ_ASSERT(!dbusmenu_menuitem_get_children(GetNativeData())); + + g_object_unref(mPlaceholderItem); + mPlaceholderItem = nullptr; +} + +void +nsMenu::OnOpen() { + if (mNeedsRebuild) { + Build(); + } + + nsWeakMenuObject self(this); + nsCOMPtr<nsIContent> origPopupContent(mPopupContent); + { + nsNativeMenuDocListener::BlockUpdatesScope updatesBlocker; + + SetPopupState(ePopupState_Showing); + DispatchMouseEvent(mPopupContent, eXULPopupShowing); + + ContentNode()->SetAttr(kNameSpaceID_None, nsGkAtoms::open, + NS_LITERAL_STRING("true"), true); + } + + if (!self) { + // We were deleted! + return; + } + + // I guess that the popup could have changed + if (origPopupContent != mPopupContent) { + return; + } + + nsNativeMenuDocListener::BlockUpdatesScope updatesBlocker; + + size_t count = ChildCount(); + for (size_t i = 0; i < count; ++i) { + ChildAt(i)->ContainerIsOpening(); + } + + SetPopupState(ePopupState_Open); + DispatchMouseEvent(mPopupContent, eXULPopupShown); +} + +void +nsMenu::Build() { + mNeedsRebuild = false; + + while (ChildCount() > 0) { + RemoveChildAt(0); + } + + InitializePopup(); + + if (!mPopupContent) { + return; + } + + uint32_t count = mPopupContent->GetChildCount(); + for (uint32_t i = 0; i < count; ++i) { + nsIContent* childContent = mPopupContent->GetChildAt(i); + + UniquePtr<nsMenuObject> child = CreateChild(childContent); + + if (!child) { + continue; + } + + AppendChild(Move(child)); + } +} + +void +nsMenu::InitializePopup() { + nsCOMPtr<nsIContent> oldPopupContent; + oldPopupContent.swap(mPopupContent); + + for (uint32_t i = 0; i < ContentNode()->GetChildCount(); ++i) { + nsIContent* child = ContentNode()->GetChildAt(i); + + int32_t dummy; + nsCOMPtr<nsIAtom> tag = child->OwnerDoc()->BindingManager()->ResolveTag(child, &dummy); + if (tag == nsGkAtoms::menupopup) { + mPopupContent = child; + break; + } + } + + if (oldPopupContent == mPopupContent) { + return; + } + + // The popup has changed + + if (oldPopupContent) { + DocListener()->UnregisterForContentChanges(oldPopupContent); + } + + SetPopupState(ePopupState_Closed); + + if (!mPopupContent) { + return; + } + + AttachXBLBindings(mPopupContent); + + DocListener()->RegisterForContentChanges(mPopupContent, this); +} + +void +nsMenu::RemoveChildAt(size_t aIndex) { + MOZ_ASSERT(IsInBatchedUpdate() || !mPlaceholderItem, + "Shouldn't have a placeholder menuitem"); + + nsMenuContainer::RemoveChildAt(aIndex, !IsInBatchedUpdate()); + StructureMutated(); + + if (!IsInBatchedUpdate()) { + MaybeAddPlaceholderItem(); + } +} + +void +nsMenu::RemoveChild(nsIContent* aChild) { + size_t index = IndexOf(aChild); + if (index == NoIndex) { + return; + } + + RemoveChildAt(index); +} + +void +nsMenu::InsertChildAfter(UniquePtr<nsMenuObject> aChild, + nsIContent* aPrevSibling) { + if (!IsInBatchedUpdate()) { + EnsureNoPlaceholderItem(); + } + + nsMenuContainer::InsertChildAfter(Move(aChild), aPrevSibling, + !IsInBatchedUpdate()); + StructureMutated(); +} + +void +nsMenu::AppendChild(UniquePtr<nsMenuObject> aChild) { + if (!IsInBatchedUpdate()) { + EnsureNoPlaceholderItem(); + } + + nsMenuContainer::AppendChild(Move(aChild), !IsInBatchedUpdate()); + StructureMutated(); +} + +bool +nsMenu::IsInBatchedUpdate() const { + return mBatchedUpdateState != eBatchedUpdateState_Inactive; +} + +void +nsMenu::StructureMutated() { + if (!IsInBatchedUpdate()) { + return; + } + + mBatchedUpdateState = eBatchedUpdateState_DidMutate; +} + +bool +nsMenu::CanOpen() const { + bool isVisible = dbusmenu_menuitem_property_get_bool(GetNativeData(), + DBUSMENU_MENUITEM_PROP_VISIBLE); + bool isDisabled = ContentNode()->AttrValueIs(kNameSpaceID_None, + nsGkAtoms::disabled, + nsGkAtoms::_true, + eCaseMatters); + + return (isVisible && !isDisabled); +} + +void +nsMenu::HandleContentInserted(nsIContent* aContainer, + nsIContent* aChild, + nsIContent* aPrevSibling) { + if (aContainer == mPopupContent) { + UniquePtr<nsMenuObject> child = CreateChild(aChild); + + if (child) { + InsertChildAfter(Move(child), aPrevSibling); + } + } else { + Build(); + } +} + +void +nsMenu::HandleContentRemoved(nsIContent* aContainer, nsIContent* aChild) { + if (aContainer == mPopupContent) { + RemoveChild(aChild); + } else { + Build(); + } +} + +void +nsMenu::InitializeNativeData() { + // Dbusmenu provides an "about-to-show" signal, and also "opened" and + // "closed" events. However, Unity is the only thing that sends + // both "about-to-show" and "opened" events. Unity 2D and the HUD only + // send "opened" events, so we ignore "about-to-show" (I don't think + // there's any real difference between them anyway). + // To complicate things, there are certain conditions where we don't + // get a "closed" event, so we need to be able to handle this :/ + g_signal_connect(G_OBJECT(GetNativeData()), "event", + G_CALLBACK(menu_event_cb), this); + + mNeedsRebuild = true; + mNeedsUpdate = true; + + MaybeAddPlaceholderItem(); + + AttachXBLBindings(ContentNode()); +} + +void +nsMenu::Update(nsStyleContext* aStyleContext) { + if (mNeedsUpdate) { + mNeedsUpdate = false; + + UpdateLabel(); + UpdateSensitivity(); + } + + UpdateVisibility(aStyleContext); + UpdateIcon(aStyleContext); +} + +nsMenuObject::PropertyFlags +nsMenu::SupportedProperties() const { + return static_cast<nsMenuObject::PropertyFlags>( + nsMenuObject::ePropLabel | + nsMenuObject::ePropEnabled | + nsMenuObject::ePropVisible | + nsMenuObject::ePropIconData | + nsMenuObject::ePropChildDisplay + ); +} + +void +nsMenu::OnAttributeChanged(nsIContent* aContent, nsIAtom* aAttribute) { + MOZ_ASSERT(aContent == ContentNode() || aContent == mPopupContent, + "Received an event that wasn't meant for us!"); + + if (mNeedsUpdate) { + return; + } + + if (aContent != ContentNode()) { + return; + } + + if (!Parent()->IsBeingDisplayed()) { + mNeedsUpdate = true; + return; + } + + if (aAttribute == nsGkAtoms::disabled) { + UpdateSensitivity(); + } else if (aAttribute == nsGkAtoms::label || + aAttribute == nsGkAtoms::accesskey || + aAttribute == nsGkAtoms::crop) { + UpdateLabel(); + } else if (aAttribute == nsGkAtoms::hidden || + aAttribute == nsGkAtoms::collapsed) { + RefPtr<nsStyleContext> sc = GetStyleContext(); + UpdateVisibility(sc); + } else if (aAttribute == nsGkAtoms::image) { + RefPtr<nsStyleContext> sc = GetStyleContext(); + UpdateIcon(sc); + } +} + +void +nsMenu::OnContentInserted(nsIContent* aContainer, nsIContent* aChild, + nsIContent* aPrevSibling) { + MOZ_ASSERT(aContainer == ContentNode() || aContainer == mPopupContent, + "Received an event that wasn't meant for us!"); + + if (mNeedsRebuild) { + return; + } + + if (mPopupState == ePopupState_Closed) { + mNeedsRebuild = true; + return; + } + + nsContentUtils::AddScriptRunner( + new nsMenuContentInsertedEvent(this, aContainer, aChild, + aPrevSibling)); +} + +void +nsMenu::OnContentRemoved(nsIContent* aContainer, nsIContent* aChild) { + MOZ_ASSERT(aContainer == ContentNode() || aContainer == mPopupContent, + "Received an event that wasn't meant for us!"); + + if (mNeedsRebuild) { + return; + } + + if (mPopupState == ePopupState_Closed) { + mNeedsRebuild = true; + return; + } + + nsContentUtils::AddScriptRunner( + new nsMenuContentRemovedEvent(this, aContainer, aChild)); +} + +/* + * Some menus (eg, the History menu in Firefox) refresh themselves on + * opening by removing all children and then re-adding new ones. As this + * happens whilst the menu is opening in Unity, it causes some flickering + * as the menu popup is resized multiple times. To avoid this, we try to + * reuse native menu items when the menu structure changes during a + * batched update. If we can handle menu structure changes from Goanna + * just by updating properties of native menu items (rather than destroying + * and creating new ones), then we eliminate any flickering that occurs as + * the menu is opened. To do this, we don't modify any native menu items + * until the end of the update batch. + */ + +void +nsMenu::OnBeginUpdates(nsIContent* aContent) { + MOZ_ASSERT(aContent == ContentNode() || aContent == mPopupContent, + "Received an event that wasn't meant for us!"); + MOZ_ASSERT(!IsInBatchedUpdate(), "Already in an update batch!"); + + if (aContent != mPopupContent) { + return; + } + + mBatchedUpdateState = eBatchedUpdateState_Active; +} + +void +nsMenu::OnEndUpdates() { + if (!IsInBatchedUpdate()) { + return; + } + + bool didMutate = mBatchedUpdateState == eBatchedUpdateState_DidMutate; + mBatchedUpdateState = eBatchedUpdateState_Inactive; + + /* Optimize for the case where we only had attribute changes */ + if (!didMutate) { + return; + } + + EnsureNoPlaceholderItem(); + + GList* nextNativeChild = dbusmenu_menuitem_get_children(GetNativeData()); + DbusmenuMenuitem* nextOwnedNativeChild = nullptr; + + size_t count = ChildCount(); + + // Find the first native menu item that is `owned` by a corresponding + // Goanna menuitem + for (size_t i = 0; i < count; ++i) { + if (ChildAt(i)->GetNativeData()) { + nextOwnedNativeChild = ChildAt(i)->GetNativeData(); + break; + } + } + + // Now iterate over all Goanna menuitems + for (size_t i = 0; i < count; ++i) { + nsMenuObject* child = ChildAt(i); + + if (child->GetNativeData()) { + // This child already has a corresponding native menuitem. + // Remove all preceding orphaned native items. At this point, we + // modify the native menu structure. + while (nextNativeChild && + nextNativeChild->data != nextOwnedNativeChild) { + + DbusmenuMenuitem* data = + static_cast<DbusmenuMenuitem *>(nextNativeChild->data); + nextNativeChild = nextNativeChild->next; + + MOZ_ALWAYS_TRUE(dbusmenu_menuitem_child_delete(GetNativeData(), + data)); + } + + if (nextNativeChild) { + nextNativeChild = nextNativeChild->next; + } + + // Now find the next native menu item that is `owned` + nextOwnedNativeChild = nullptr; + for (size_t j = i + 1; j < count; ++j) { + if (ChildAt(j)->GetNativeData()) { + nextOwnedNativeChild = ChildAt(j)->GetNativeData(); + break; + } + } + } else { + // This child is new, and doesn't have a native menu item. Find one! + if (nextNativeChild && + nextNativeChild->data != nextOwnedNativeChild) { + + DbusmenuMenuitem* data = + static_cast<DbusmenuMenuitem *>(nextNativeChild->data); + + if (NS_SUCCEEDED(child->AdoptNativeData(data))) { + nextNativeChild = nextNativeChild->next; + } + } + + // There wasn't a suitable one available, so create a new one. + // At this point, we modify the native menu structure. + if (!child->GetNativeData()) { + child->CreateNativeData(); + MOZ_ALWAYS_TRUE( + dbusmenu_menuitem_child_add_position(GetNativeData(), + child->GetNativeData(), + i)); + } + } + } + + while (nextNativeChild) { + DbusmenuMenuitem* data = + static_cast<DbusmenuMenuitem *>(nextNativeChild->data); + nextNativeChild = nextNativeChild->next; + + MOZ_ALWAYS_TRUE(dbusmenu_menuitem_child_delete(GetNativeData(), data)); + } + + MaybeAddPlaceholderItem(); +} + +nsMenu::nsMenu(nsMenuContainer* aParent, nsIContent* aContent) : + nsMenuContainer(aParent, aContent), + mNeedsRebuild(false), + mNeedsUpdate(false), + mPlaceholderItem(nullptr), + mPopupState(ePopupState_Closed), + mBatchedUpdateState(eBatchedUpdateState_Inactive) { + MOZ_COUNT_CTOR(nsMenu); +} + +nsMenu::~nsMenu() { + if (IsInBatchedUpdate()) { + OnEndUpdates(); + } + + // Although nsTArray will take care of this in its destructor, + // we have to manually ensure children are removed from our native menu + // item, just in case our parent recycles us + while (ChildCount() > 0) { + RemoveChildAt(0); + } + + EnsureNoPlaceholderItem(); + + if (DocListener() && mPopupContent) { + DocListener()->UnregisterForContentChanges(mPopupContent); + } + + if (GetNativeData()) { + g_signal_handlers_disconnect_by_func(GetNativeData(), + FuncToGpointer(menu_event_cb), + this); + } + + MOZ_COUNT_DTOR(nsMenu); +} + +nsMenuObject::EType +nsMenu::Type() const { + return eType_Menu; +} + +bool +nsMenu::IsBeingDisplayed() const { + return mPopupState == ePopupState_Open; +} + +bool +nsMenu::NeedsRebuild() const { + return mNeedsRebuild; +} + +void +nsMenu::OpenMenu() { + if (!CanOpen()) { + return; + } + + if (mOpenDelayTimer) { + return; + } + + // Here, we synchronously fire popupshowing and popupshown events and then + // open the menu after a short delay. This allows the menu to refresh before + // it's shown, and avoids an issue where keyboard focus is not on the first + // item of the history menu in Firefox when opening it with the keyboard, + // because extra items to appear at the top of the menu + + OnOpen(); + + mOpenDelayTimer = do_CreateInstance(NS_TIMER_CONTRACTID); + if (!mOpenDelayTimer) { + return; + } + + if (NS_FAILED(mOpenDelayTimer->InitWithFuncCallback(DoOpenCallback, + this, + 100, + nsITimer::TYPE_ONE_SHOT))) { + mOpenDelayTimer = nullptr; + } +} + +void +nsMenu::OnClose() { + if (mPopupState == ePopupState_Closed) { + return; + } + + MOZ_ASSERT(nsContentUtils::IsSafeToRunScript()); + + // We do this to avoid mutating our view of the menu until + // after we have finished + nsNativeMenuDocListener::BlockUpdatesScope updatesBlocker; + + SetPopupState(ePopupState_Hiding); + DispatchMouseEvent(mPopupContent, eXULPopupHiding); + + // Sigh, make sure all of our descendants are closed, as we don't + // always get closed events for submenus when scrubbing quickly through + // the menu + size_t count = ChildCount(); + for (size_t i = 0; i < count; ++i) { + if (ChildAt(i)->Type() == nsMenuObject::eType_Menu) { + static_cast<nsMenu *>(ChildAt(i))->OnClose(); + } + } + + SetPopupState(ePopupState_Closed); + DispatchMouseEvent(mPopupContent, eXULPopupHidden); + + ContentNode()->UnsetAttr(kNameSpaceID_None, nsGkAtoms::open, true); +} |