/* 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/. */ #include "ImageOps.h" #include "imgIContainer.h" #include "imgINotificationObserver.h" #include "imgLoader.h" #include "imgRequestProxy.h" #include "mozilla/ArrayUtils.h" #include "mozilla/Assertions.h" #include "mozilla/dom/Element.h" #include "mozilla/Preferences.h" #include "nsAttrValue.h" #include "nsComputedDOMStyle.h" #include "nsContentUtils.h" #include "nsGkAtoms.h" #include "nsIContent.h" #include "nsIContentPolicy.h" #include "nsIDocument.h" #include "nsILoadGroup.h" #include "nsImageToPixbuf.h" #include "nsIPresShell.h" #include "nsIURI.h" #include "nsNetUtil.h" #include "nsPresContext.h" #include "nsRect.h" #include "nsServiceManagerUtils.h" #include "nsString.h" #include "nsStyleConsts.h" #include "nsStyleContext.h" #include "nsStyleStruct.h" #include "nsUnicharUtils.h" #include "nsMenuContainer.h" #include "nsNativeMenuAtoms.h" #include "nsNativeMenuDocListener.h" #include #include #include #include "nsMenuObject.h" // X11's None clashes with StyleDisplay::None #include "X11UndefineNone.h" #undef None using namespace mozilla; using mozilla::image::ImageOps; #define MAX_WIDTH 350000 const char* gPropertyStrings[] = { #define DBUSMENU_PROPERTY(e, s, b) s, DBUSMENU_PROPERTIES #undef DBUSMENU_PROPERTY nullptr }; nsWeakMenuObject* nsWeakMenuObject::sHead; PangoLayout* gPangoLayout = nullptr; class nsMenuObjectIconLoader final : public imgINotificationObserver { public: NS_DECL_ISUPPORTS NS_DECL_IMGINOTIFICATIONOBSERVER nsMenuObjectIconLoader(nsMenuObject* aOwner) : mOwner(aOwner) { }; void LoadIcon(nsStyleContext* aStyleContext); void Destroy(); private: ~nsMenuObjectIconLoader() { }; nsMenuObject* mOwner; RefPtr mImageRequest; nsCOMPtr mURI; nsIntRect mImageRect; }; NS_IMPL_ISUPPORTS(nsMenuObjectIconLoader, imgINotificationObserver) NS_IMETHODIMP nsMenuObjectIconLoader::Notify(imgIRequest* aProxy, int32_t aType, const nsIntRect* aRect) { if (!mOwner) { return NS_OK; } if (aProxy != mImageRequest) { return NS_ERROR_FAILURE; } if (aType == imgINotificationObserver::LOAD_COMPLETE) { uint32_t status = imgIRequest::STATUS_ERROR; if (NS_FAILED(mImageRequest->GetImageStatus(&status)) || (status & imgIRequest::STATUS_ERROR)) { mImageRequest->Cancel(NS_BINDING_ABORTED); mImageRequest = nullptr; return NS_ERROR_FAILURE; } nsCOMPtr image; mImageRequest->GetImage(getter_AddRefs(image)); MOZ_ASSERT(image); // Ask the image to decode at its intrinsic size. int32_t width = 0, height = 0; image->GetWidth(&width); image->GetHeight(&height); image->RequestDecodeForSize(nsIntSize(width, height), imgIContainer::FLAG_NONE); return NS_OK; } if (aType == imgINotificationObserver::DECODE_COMPLETE) { mImageRequest->Cancel(NS_BINDING_ABORTED); mImageRequest = nullptr; return NS_OK; } if (aType != imgINotificationObserver::FRAME_COMPLETE) { return NS_OK; } nsCOMPtr img; mImageRequest->GetImage(getter_AddRefs(img)); if (!img) { return NS_ERROR_FAILURE; } if (!mImageRect.IsEmpty()) { img = ImageOps::Clip(img, mImageRect); } int32_t width, height; img->GetWidth(&width); img->GetHeight(&height); if (width <= 0 || height <= 0) { mOwner->ClearIcon(); return NS_OK; } if (width > 100 || height > 100) { // The icon data needs to go across DBus. Make sure the icon // data isn't too large, else our connection gets terminated and // GDbus helpfully aborts the application. Thank you :) NS_WARNING("Icon data too large"); mOwner->ClearIcon(); return NS_OK; } GdkPixbuf* pixbuf = nsImageToPixbuf::ImageToPixbuf(img); if (pixbuf) { dbusmenu_menuitem_property_set_image(mOwner->GetNativeData(), DBUSMENU_MENUITEM_PROP_ICON_DATA, pixbuf); g_object_unref(pixbuf); } return NS_OK; } void nsMenuObjectIconLoader::LoadIcon(nsStyleContext* aStyleContext) { nsIDocument* doc = mOwner->ContentNode()->OwnerDoc(); nsCOMPtr uri; nsIntRect imageRect; imgRequestProxy* imageRequest = nullptr; nsAutoString uriString; if (mOwner->ContentNode()->GetAttr(kNameSpaceID_None, nsGkAtoms::image, uriString)) { NS_NewURI(getter_AddRefs(uri), uriString); } else { nsIPresShell* shell = doc->GetShell(); if (!shell) { return; } nsPresContext* pc = shell->GetPresContext(); if (!pc || !aStyleContext) { return; } const nsStyleList* list = aStyleContext->StyleList(); imageRequest = list->GetListStyleImage(); if (imageRequest) { imageRequest->GetURI(getter_AddRefs(uri)); imageRect = list->mImageRegion.ToNearestPixels( pc->AppUnitsPerDevPixel()); } } if (!uri) { mOwner->ClearIcon(); mURI = nullptr; if (mImageRequest) { mImageRequest->Cancel(NS_BINDING_ABORTED); mImageRequest = nullptr; } return; } bool same; if (mURI && NS_SUCCEEDED(mURI->Equals(uri, &same)) && same && (!imageRequest || imageRect == mImageRect)) { return; } if (mImageRequest) { mImageRequest->Cancel(NS_BINDING_ABORTED); mImageRequest = nullptr; } mURI = uri; if (imageRequest) { mImageRect = imageRect; imageRequest->Clone(this, getter_AddRefs(mImageRequest)); } else { mImageRect.SetEmpty(); nsCOMPtr loadGroup = doc->GetDocumentLoadGroup(); RefPtr loader = nsContentUtils::GetImgLoaderForDocument(doc); if (!loader || !loadGroup) { NS_WARNING("Failed to get loader or load group for image load"); return; } loader->LoadImage(uri, nullptr, nullptr, mozilla::net::RP_Unset, nullptr, loadGroup, this, nullptr, nullptr, nsIRequest::LOAD_NORMAL, nullptr, nsIContentPolicy::TYPE_IMAGE, EmptyString(), getter_AddRefs(mImageRequest)); } } void nsMenuObjectIconLoader::Destroy() { if (mImageRequest) { mImageRequest->CancelAndForgetObserver(NS_BINDING_ABORTED); mImageRequest = nullptr; } mOwner = nullptr; } static int CalculateTextWidth(const nsAString& aText) { if (!gPangoLayout) { PangoFontMap* fontmap = pango_cairo_font_map_get_default(); PangoContext* ctx = pango_font_map_create_context(fontmap); gPangoLayout = pango_layout_new(ctx); g_object_unref(ctx); } pango_layout_set_text(gPangoLayout, NS_ConvertUTF16toUTF8(aText).get(), -1); int width, dummy; pango_layout_get_size(gPangoLayout, &width, &dummy); return width; } static const nsDependentString GetEllipsis() { static char16_t sBuf[4] = { 0, 0, 0, 0 }; if (!sBuf[0]) { nsAdoptingString ellipsis = Preferences::GetLocalizedString("intl.ellipsis"); if (!ellipsis.IsEmpty()) { uint32_t l = ellipsis.Length(); const nsAdoptingString::char_type* c = ellipsis.BeginReading(); uint32_t i = 0; while (i < 3 && i < l) { sBuf[i++] =* (c++); } } else { sBuf[0] = '.'; sBuf[1] = '.'; sBuf[2] = '.'; } } return nsDependentString(sBuf); } static int GetEllipsisWidth() { static int sEllipsisWidth = -1; if (sEllipsisWidth == -1) { sEllipsisWidth = CalculateTextWidth(GetEllipsis()); } return sEllipsisWidth; } nsMenuObject::nsMenuObject(nsMenuContainer* aParent, nsIContent* aContent) : mContent(aContent), mListener(aParent->DocListener()), mParent(aParent), mNativeData(nullptr) { MOZ_ASSERT(mContent); MOZ_ASSERT(mListener); MOZ_ASSERT(mParent); } nsMenuObject::nsMenuObject(nsNativeMenuDocListener* aListener, nsIContent* aContent) : mContent(aContent), mListener(aListener), mParent(nullptr), mNativeData(nullptr) { MOZ_ASSERT(mContent); MOZ_ASSERT(mListener); } void nsMenuObject::UpdateLabel() { // Goanna stores the label and access key in separate attributes // so we need to convert label="Foo_Bar"/accesskey="F" in to // label="_Foo__Bar" for dbusmenu nsAutoString label; mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::label, label); nsAutoString accesskey; mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::accesskey, accesskey); const nsAutoString::char_type* akey = accesskey.BeginReading(); char16_t keyLower = ToLowerCase(*akey); char16_t keyUpper = ToUpperCase(*akey); const nsAutoString::char_type* iter = label.BeginReading(); const nsAutoString::char_type* end = label.EndReading(); uint32_t length = label.Length(); uint32_t pos = 0; bool foundAccessKey = false; while (iter != end) { if (*iter != char16_t('_')) { if ((*iter != keyLower &&* iter != keyUpper) || foundAccessKey) { ++iter; ++pos; continue; } foundAccessKey = true; } label.SetLength(++length); iter = label.BeginReading() + pos; end = label.EndReading(); nsAutoString::char_type* cur = label.BeginWriting() + pos; memmove(cur + 1, cur, (length - 1 - pos)* sizeof(nsAutoString::char_type)); * cur = nsAutoString::char_type('_'); iter += 2; pos += 2; } if (CalculateTextWidth(label) <= MAX_WIDTH) { dbusmenu_menuitem_property_set(mNativeData, DBUSMENU_MENUITEM_PROP_LABEL, NS_ConvertUTF16toUTF8(label).get()); return; } // This sucks. // This should be done at the point where the menu is drawn (hello Unity), // but unfortunately it doesn't do that and will happily fill your entire // screen width with a menu if you have a bookmark with a really long title. // This leaves us with no other option but to ellipsize here, with no proper // knowledge of Unity's render path, font size etc. This is better than nothing nsAutoString truncated; int target = MAX_WIDTH - GetEllipsisWidth(); length = label.Length(); static nsIContent::AttrValuesArray strings[] = { &nsGkAtoms::left, &nsGkAtoms::start, &nsGkAtoms::center, &nsGkAtoms::right, &nsGkAtoms::end, nullptr }; int32_t type = mContent->FindAttrValueIn(kNameSpaceID_None, nsGkAtoms::crop, strings, eCaseMatters); switch (type) { case 0: case 1: // FIXME: Implement left cropping case 2: // FIXME: Implement center cropping case 3: case 4: default: for (uint32_t i = 0; i < length; i++) { truncated.Append(label.CharAt(i)); if (CalculateTextWidth(truncated) > target) { break; } } truncated.Append(GetEllipsis()); } dbusmenu_menuitem_property_set(mNativeData, DBUSMENU_MENUITEM_PROP_LABEL, NS_ConvertUTF16toUTF8(truncated).get()); } void nsMenuObject::UpdateVisibility(nsStyleContext* aStyleContext) { bool vis = true; if (aStyleContext && (aStyleContext->StyleDisplay()->mDisplay == StyleDisplay::None || aStyleContext->StyleVisibility()->mVisible == NS_STYLE_VISIBILITY_COLLAPSE)) { vis = false; } dbusmenu_menuitem_property_set_bool(mNativeData, DBUSMENU_MENUITEM_PROP_VISIBLE, vis); } void nsMenuObject::UpdateSensitivity() { bool disabled = mContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::disabled, nsGkAtoms::_true, eCaseMatters); dbusmenu_menuitem_property_set_bool(mNativeData, DBUSMENU_MENUITEM_PROP_ENABLED, !disabled); } void nsMenuObject::UpdateIcon(nsStyleContext* aStyleContext) { if (ShouldShowIcon()) { if (!mIconLoader) { mIconLoader = new nsMenuObjectIconLoader(this); } mIconLoader->LoadIcon(aStyleContext); } else { if (mIconLoader) { mIconLoader->Destroy(); mIconLoader = nullptr; } ClearIcon(); } } already_AddRefed nsMenuObject::GetStyleContext() { nsIPresShell* shell = mContent->OwnerDoc()->GetShell(); if (!shell) { return nullptr; } RefPtr sc = nsComputedDOMStyle::GetStyleContextForElementNoFlush( mContent->AsElement(), nullptr, shell); return sc.forget(); } void nsMenuObject::InitializeNativeData() { } nsMenuObject::PropertyFlags nsMenuObject::SupportedProperties() const { return static_cast(0); } bool nsMenuObject::IsCompatibleWithNativeData(DbusmenuMenuitem* aNativeData) const { return true; } void nsMenuObject::UpdateContentAttributes() { } void nsMenuObject::Update(nsStyleContext* aStyleContext) { } bool nsMenuObject::ShouldShowIcon() const { // Ideally we want to know the visibility of the anonymous XUL image in // our menuitem, but this isn't created because we don't have a frame. // The following works by default (because xul.css hides images in menuitems // that don't have the "menuitem-with-favicon" class). It's possible a third // party theme could override this, but, oh well... const nsAttrValue* classes = mContent->AsElement()->GetClasses(); if (!classes) { return false; } for (uint32_t i = 0; i < classes->GetAtomCount(); ++i) { if (classes->AtomAt(i) == nsNativeMenuAtoms::menuitem_with_favicon) { return true; } } return false; } void nsMenuObject::ClearIcon() { dbusmenu_menuitem_property_remove(mNativeData, DBUSMENU_MENUITEM_PROP_ICON_DATA); } nsMenuObject::~nsMenuObject() { nsWeakMenuObject::NotifyDestroyed(this); if (mIconLoader) { mIconLoader->Destroy(); } if (mListener) { mListener->UnregisterForContentChanges(mContent); } if (mNativeData) { g_object_unref(mNativeData); mNativeData = nullptr; } } void nsMenuObject::CreateNativeData() { MOZ_ASSERT(mNativeData == nullptr, "This node already has a DbusmenuMenuitem. The old one will be leaked"); mNativeData = dbusmenu_menuitem_new(); InitializeNativeData(); if (mParent && mParent->IsBeingDisplayed()) { ContainerIsOpening(); } mListener->RegisterForContentChanges(mContent, this); } nsresult nsMenuObject::AdoptNativeData(DbusmenuMenuitem* aNativeData) { MOZ_ASSERT(mNativeData == nullptr, "This node already has a DbusmenuMenuitem. The old one will be leaked"); if (!IsCompatibleWithNativeData(aNativeData)) { return NS_ERROR_FAILURE; } mNativeData = aNativeData; g_object_ref(mNativeData); PropertyFlags supported = SupportedProperties(); PropertyFlags mask = static_cast(1); for (uint32_t i = 0; gPropertyStrings[i]; ++i) { if (!(mask & supported)) { dbusmenu_menuitem_property_remove(mNativeData, gPropertyStrings[i]); } mask = static_cast(mask << 1); } InitializeNativeData(); if (mParent && mParent->IsBeingDisplayed()) { ContainerIsOpening(); } mListener->RegisterForContentChanges(mContent, this); return NS_OK; } void nsMenuObject::ContainerIsOpening() { MOZ_ASSERT(nsContentUtils::IsSafeToRunScript()); UpdateContentAttributes(); RefPtr sc = GetStyleContext(); Update(sc); } /* static */ void nsWeakMenuObject::AddWeakReference(nsWeakMenuObject* aWeak) { aWeak->mPrev = sHead; sHead = aWeak; } /* static */ void nsWeakMenuObject::RemoveWeakReference(nsWeakMenuObject* aWeak) { if (aWeak == sHead) { sHead = aWeak->mPrev; return; } nsWeakMenuObject* weak = sHead; while (weak && weak->mPrev != aWeak) { weak = weak->mPrev; } if (weak) { weak->mPrev = aWeak->mPrev; } } /* static */ void nsWeakMenuObject::NotifyDestroyed(nsMenuObject* aMenuObject) { nsWeakMenuObject* weak = sHead; while (weak) { if (weak->mMenuObject == aMenuObject) { weak->mMenuObject = nullptr; } weak = weak->mPrev; } }