/* 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 #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(mWeakMenu.get())->HandleContentInserted(mContainer, mChild, mPrevSibling); return NS_OK; } private: nsWeakMenuObject mWeakMenu; nsCOMPtr mContainer; nsCOMPtr mChild; nsCOMPtr 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(mWeakMenu.get())->HandleContentRemoved(mContainer, mChild); return NS_OK; } private: nsWeakMenuObject mWeakMenu; nsCOMPtr mContainer; nsCOMPtr 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 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 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(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(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 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 child = CreateChild(childContent); if (!child) { continue; } AppendChild(Move(child)); } } void nsMenu::InitializePopup() { nsCOMPtr oldPopupContent; oldPopupContent.swap(mPopupContent); for (uint32_t i = 0; i < ContentNode()->GetChildCount(); ++i) { nsIContent* child = ContentNode()->GetChildAt(i); int32_t dummy; nsCOMPtr 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 aChild, nsIContent* aPrevSibling) { if (!IsInBatchedUpdate()) { EnsureNoPlaceholderItem(); } nsMenuContainer::InsertChildAfter(Move(aChild), aPrevSibling, !IsInBatchedUpdate()); StructureMutated(); } void nsMenu::AppendChild(UniquePtr 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 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::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 sc = GetStyleContext(); UpdateVisibility(sc); } else if (aAttribute == nsGkAtoms::image) { RefPtr 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(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(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(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(ChildAt(i))->OnClose(); } } SetPopupState(ePopupState_Closed); DispatchMouseEvent(mPopupContent, eXULPopupHidden); ContentNode()->UnsetAttr(kNameSpaceID_None, nsGkAtoms::open, true); }