Mypal/widget/gtk/nsMenuObject.cpp

635 lines
17 KiB
C++

/* 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;
}
}