summaryrefslogtreecommitdiffstats
path: root/widget/gtk/nsMenuObject.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'widget/gtk/nsMenuObject.cpp')
-rw-r--r--widget/gtk/nsMenuObject.cpp634
1 files changed, 634 insertions, 0 deletions
diff --git a/widget/gtk/nsMenuObject.cpp b/widget/gtk/nsMenuObject.cpp
new file mode 100644
index 000000000..58d1716fd
--- /dev/null
+++ b/widget/gtk/nsMenuObject.cpp
@@ -0,0 +1,634 @@
+/* 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 <gdk/gdk.h>
+#include <glib-object.h>
+#include <pango/pango.h>
+
+#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<imgRequestProxy> mImageRequest;
+ nsCOMPtr<nsIURI> 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<imgIContainer> 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<imgIContainer> 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<nsIURI> 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<nsILoadGroup> loadGroup = doc->GetDocumentLoadGroup();
+ RefPtr<imgLoader> 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<nsStyleContext>
+nsMenuObject::GetStyleContext() {
+ nsIPresShell* shell = mContent->OwnerDoc()->GetShell();
+ if (!shell) {
+ return nullptr;
+ }
+
+ RefPtr<nsStyleContext> sc =
+ nsComputedDOMStyle::GetStyleContextForElementNoFlush(
+ mContent->AsElement(), nullptr, shell);
+
+ return sc.forget();
+}
+
+void
+nsMenuObject::InitializeNativeData() {
+}
+
+nsMenuObject::PropertyFlags
+nsMenuObject::SupportedProperties() const {
+ return static_cast<nsMenuObject::PropertyFlags>(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<PropertyFlags>(1);
+
+ for (uint32_t i = 0; gPropertyStrings[i]; ++i) {
+ if (!(mask & supported)) {
+ dbusmenu_menuitem_property_remove(mNativeData, gPropertyStrings[i]);
+ }
+ mask = static_cast<PropertyFlags>(mask << 1);
+ }
+
+ InitializeNativeData();
+ if (mParent && mParent->IsBeingDisplayed()) {
+ ContainerIsOpening();
+ }
+
+ mListener->RegisterForContentChanges(mContent, this);
+
+ return NS_OK;
+}
+
+void
+nsMenuObject::ContainerIsOpening() {
+ MOZ_ASSERT(nsContentUtils::IsSafeToRunScript());
+
+ UpdateContentAttributes();
+
+ RefPtr<nsStyleContext> 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;
+ }
+}