635 lines
17 KiB
C++
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;
|
|
}
|
|
}
|