gecko/dom/html/HTMLMenuItemElement.cpp

497 lines
13 KiB
C++
Raw Normal View History

/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2012-05-21 04:12:37 -07:00
/* 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 "mozilla/dom/HTMLMenuItemElement.h"
#include "mozilla/BasicEvents.h"
#include "mozilla/EventDispatcher.h"
#include "mozilla/dom/HTMLMenuItemElementBinding.h"
Bug 760331: Coalesce data for inline style across nodes. r=bz This patch enables sharing of an nsAttrValue's MiscContainer between nodes for style rules. MiscContainers of type eCSSStyleRule are now refcounted (with some clever struct packing to ensure that the amount of memory allocated for MiscContainer remains unchanged on 32 and 64 bit). This infrastructure can be used to share most MiscContainer types in the future if we find advantages to sharing other types than just eCSSStyleRuley. A cache mapping strings to MiscContainers has been added to nsHTMLCSSStyleSheet. MiscContainers can be shared between nsAttrValues when one nsAttrValue is SetTo another nsAttrValue or when there is a cache hit in this cache. This patch also adds the ability to tell a style rule that it belongs to an nsHTMLCSSStyleSheet, with appropriate accessor functions to separate that from the existing case of belonging to an nsCSSStyleSheet. The primary use case is to reduce memory use for pages that have lots of inline style attributes with the same value. This can happen easily with large pages that are automatically generated. An (admittedly pathological) testcase in Bug 686975 sees over 250 MB of memory savings with this change. Reusing the same MiscContainer for multiple nodes saves the overhead of maintaining separate copies of the string containing the serialized value of the style attribute and of creating separate style rules for each node. Eliminating duplicate style rules enables further savings in layout through style context sharing. The testcase sees the amount of memory used by style contexts go from over 250 MB to 10 KB. Because the cache is based on the text value of the style attribute, it will not handle attributes that have different text values but are parsed into identical style rules. We also do not attempt to share MiscContainers when the node's base URI differs from the document URI. The effect of these limitations is expected to be low.
2012-09-30 09:40:24 -07:00
#include "nsAttrValueInlines.h"
#include "nsContentUtils.h"
NS_IMPL_NS_NEW_HTML_ELEMENT_CHECK_PARSER(MenuItem)
namespace mozilla {
namespace dom {
// First bits are needed for the menuitem type.
#define NS_CHECKED_IS_TOGGLED (1 << 2)
#define NS_ORIGINAL_CHECKED_VALUE (1 << 3)
#define NS_MENUITEM_TYPE(bits) ((bits) & ~( \
NS_CHECKED_IS_TOGGLED | NS_ORIGINAL_CHECKED_VALUE))
enum CmdType
{
CMD_TYPE_MENUITEM = 1,
CMD_TYPE_CHECKBOX,
CMD_TYPE_RADIO
};
static const nsAttrValue::EnumTable kMenuItemTypeTable[] = {
{ "menuitem", CMD_TYPE_MENUITEM },
{ "checkbox", CMD_TYPE_CHECKBOX },
{ "radio", CMD_TYPE_RADIO },
{ 0 }
};
static const nsAttrValue::EnumTable* kMenuItemDefaultType =
&kMenuItemTypeTable[0];
// A base class inherited by all radio visitors.
class Visitor
{
public:
Visitor() { }
virtual ~Visitor() { }
/**
* Visit a node in the tree. This is meant to be called on all radios in a
* group, sequentially. If the method returns false then the iteration is
* stopped.
*/
virtual bool Visit(HTMLMenuItemElement* aMenuItem) = 0;
};
// Find the selected radio, see GetSelectedRadio().
class GetCheckedVisitor : public Visitor
{
public:
explicit GetCheckedVisitor(HTMLMenuItemElement** aResult)
: mResult(aResult)
{ }
virtual bool Visit(HTMLMenuItemElement* aMenuItem)
{
if (aMenuItem->IsChecked()) {
*mResult = aMenuItem;
return false;
}
return true;
}
protected:
HTMLMenuItemElement** mResult;
};
// Deselect all radios except the one passed to the constructor.
class ClearCheckedVisitor : public Visitor
{
public:
explicit ClearCheckedVisitor(HTMLMenuItemElement* aExcludeMenuItem)
: mExcludeMenuItem(aExcludeMenuItem)
{ }
virtual bool Visit(HTMLMenuItemElement* aMenuItem)
{
if (aMenuItem != mExcludeMenuItem && aMenuItem->IsChecked()) {
aMenuItem->ClearChecked();
}
return true;
}
protected:
HTMLMenuItemElement* mExcludeMenuItem;
};
// Get current value of the checked dirty flag. The same value is stored on all
// radios in the group, so we need to check only the first one.
class GetCheckedDirtyVisitor : public Visitor
{
public:
GetCheckedDirtyVisitor(bool* aCheckedDirty,
HTMLMenuItemElement* aExcludeMenuItem)
: mCheckedDirty(aCheckedDirty),
mExcludeMenuItem(aExcludeMenuItem)
{ }
virtual bool Visit(HTMLMenuItemElement* aMenuItem)
{
if (aMenuItem == mExcludeMenuItem) {
return true;
}
*mCheckedDirty = aMenuItem->IsCheckedDirty();
return false;
}
protected:
bool* mCheckedDirty;
HTMLMenuItemElement* mExcludeMenuItem;
};
// Set checked dirty to true on all radios in the group.
class SetCheckedDirtyVisitor : public Visitor
{
public:
SetCheckedDirtyVisitor()
{ }
virtual bool Visit(HTMLMenuItemElement* aMenuItem)
{
aMenuItem->SetCheckedDirty();
return true;
}
};
// A helper visitor that is used to combine two operations (visitors) to avoid
// iterating over radios twice.
class CombinedVisitor : public Visitor
{
public:
CombinedVisitor(Visitor* aVisitor1, Visitor* aVisitor2)
: mVisitor1(aVisitor1), mVisitor2(aVisitor2),
mContinue1(true), mContinue2(true)
{ }
virtual bool Visit(HTMLMenuItemElement* aMenuItem)
{
if (mContinue1) {
mContinue1 = mVisitor1->Visit(aMenuItem);
}
if (mContinue2) {
mContinue2 = mVisitor2->Visit(aMenuItem);
}
return mContinue1 || mContinue2;
}
protected:
Visitor* mVisitor1;
Visitor* mVisitor2;
bool mContinue1;
bool mContinue2;
};
HTMLMenuItemElement::HTMLMenuItemElement(
already_AddRefed<mozilla::dom::NodeInfo>& aNodeInfo, FromParser aFromParser)
: nsGenericHTMLElement(aNodeInfo),
mType(kMenuItemDefaultType->value),
mParserCreating(false),
mShouldInitChecked(false),
mCheckedDirty(false),
mChecked(false)
{
mParserCreating = aFromParser;
}
HTMLMenuItemElement::~HTMLMenuItemElement()
{
}
NS_IMPL_ISUPPORTS_INHERITED(HTMLMenuItemElement, nsGenericHTMLElement,
nsIDOMHTMLMenuItemElement)
//NS_IMPL_ELEMENT_CLONE(HTMLMenuItemElement)
nsresult
HTMLMenuItemElement::Clone(mozilla::dom::NodeInfo *aNodeInfo, nsINode **aResult) const
{
*aResult = nullptr;
already_AddRefed<mozilla::dom::NodeInfo> ni = nsRefPtr<mozilla::dom::NodeInfo>(aNodeInfo).forget();
nsRefPtr<HTMLMenuItemElement> it =
new HTMLMenuItemElement(ni, NOT_FROM_PARSER);
nsresult rv = const_cast<HTMLMenuItemElement*>(this)->CopyInnerTo(it);
if (NS_SUCCEEDED(rv)) {
switch (mType) {
case CMD_TYPE_CHECKBOX:
case CMD_TYPE_RADIO:
if (mCheckedDirty) {
// We no longer have our original checked state. Set our
// checked state on the clone.
it->mCheckedDirty = true;
it->mChecked = mChecked;
}
break;
}
it.forget(aResult);
}
return rv;
}
NS_IMPL_ENUM_ATTR_DEFAULT_VALUE(HTMLMenuItemElement, Type, type,
kMenuItemDefaultType->tag)
// GetText returns a whitespace compressed .textContent value.
NS_IMPL_STRING_ATTR_WITH_FALLBACK(HTMLMenuItemElement, Label, label, GetText)
NS_IMPL_URI_ATTR(HTMLMenuItemElement, Icon, icon)
NS_IMPL_BOOL_ATTR(HTMLMenuItemElement, Disabled, disabled)
NS_IMPL_BOOL_ATTR(HTMLMenuItemElement, DefaultChecked, checked)
//NS_IMPL_BOOL_ATTR(HTMLMenuItemElement, Checked, checked)
NS_IMPL_STRING_ATTR(HTMLMenuItemElement, Radiogroup, radiogroup)
NS_IMETHODIMP
HTMLMenuItemElement::GetChecked(bool* aChecked)
{
*aChecked = mChecked;
return NS_OK;
}
NS_IMETHODIMP
HTMLMenuItemElement::SetChecked(bool aChecked)
{
bool checkedChanged = mChecked != aChecked;
mChecked = aChecked;
if (mType == CMD_TYPE_RADIO) {
if (checkedChanged) {
if (mCheckedDirty) {
ClearCheckedVisitor visitor(this);
WalkRadioGroup(&visitor);
} else {
ClearCheckedVisitor visitor1(this);
SetCheckedDirtyVisitor visitor2;
CombinedVisitor visitor(&visitor1, &visitor2);
WalkRadioGroup(&visitor);
}
} else if (!mCheckedDirty) {
SetCheckedDirtyVisitor visitor;
WalkRadioGroup(&visitor);
}
} else {
mCheckedDirty = true;
}
return NS_OK;
}
nsresult
HTMLMenuItemElement::PreHandleEvent(EventChainPreVisitor& aVisitor)
{
if (aVisitor.mEvent->message == NS_MOUSE_CLICK) {
bool originalCheckedValue = false;
switch (mType) {
case CMD_TYPE_CHECKBOX:
originalCheckedValue = mChecked;
SetChecked(!originalCheckedValue);
aVisitor.mItemFlags |= NS_CHECKED_IS_TOGGLED;
break;
case CMD_TYPE_RADIO:
nsCOMPtr<nsIDOMHTMLMenuItemElement> selectedRadio = GetSelectedRadio();
aVisitor.mItemData = selectedRadio;
originalCheckedValue = mChecked;
if (!originalCheckedValue) {
SetChecked(true);
aVisitor.mItemFlags |= NS_CHECKED_IS_TOGGLED;
}
break;
}
if (originalCheckedValue) {
aVisitor.mItemFlags |= NS_ORIGINAL_CHECKED_VALUE;
}
// We must cache type because mType may change during JS event.
aVisitor.mItemFlags |= mType;
}
return nsGenericHTMLElement::PreHandleEvent(aVisitor);
}
nsresult
HTMLMenuItemElement::PostHandleEvent(EventChainPostVisitor& aVisitor)
{
// Check to see if the event was cancelled.
if (aVisitor.mEvent->message == NS_MOUSE_CLICK &&
aVisitor.mItemFlags & NS_CHECKED_IS_TOGGLED &&
aVisitor.mEventStatus == nsEventStatus_eConsumeNoDefault) {
bool originalCheckedValue =
!!(aVisitor.mItemFlags & NS_ORIGINAL_CHECKED_VALUE);
uint8_t oldType = NS_MENUITEM_TYPE(aVisitor.mItemFlags);
nsCOMPtr<nsIDOMHTMLMenuItemElement> selectedRadio =
do_QueryInterface(aVisitor.mItemData);
if (selectedRadio) {
selectedRadio->SetChecked(true);
if (mType != CMD_TYPE_RADIO) {
SetChecked(false);
}
} else if (oldType == CMD_TYPE_CHECKBOX) {
SetChecked(originalCheckedValue);
}
}
return NS_OK;
}
nsresult
HTMLMenuItemElement::BindToTree(nsIDocument* aDocument, nsIContent* aParent,
nsIContent* aBindingParent,
bool aCompileEventHandlers)
{
nsresult rv = nsGenericHTMLElement::BindToTree(aDocument, aParent,
aBindingParent,
aCompileEventHandlers);
if (NS_SUCCEEDED(rv) && aDocument && mType == CMD_TYPE_RADIO) {
AddedToRadioGroup();
}
return rv;
}
bool
HTMLMenuItemElement::ParseAttribute(int32_t aNamespaceID,
nsIAtom* aAttribute,
const nsAString& aValue,
nsAttrValue& aResult)
{
if (aNamespaceID == kNameSpaceID_None) {
if (aAttribute == nsGkAtoms::type) {
bool success = aResult.ParseEnumValue(aValue, kMenuItemTypeTable,
false);
if (success) {
mType = aResult.GetEnumValue();
} else {
mType = kMenuItemDefaultType->value;
}
return success;
}
if (aAttribute == nsGkAtoms::radiogroup) {
aResult.ParseAtom(aValue);
return true;
}
}
return nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue,
aResult);
}
void
HTMLMenuItemElement::DoneCreatingElement()
{
mParserCreating = false;
if (mShouldInitChecked) {
InitChecked();
mShouldInitChecked = false;
}
}
void
HTMLMenuItemElement::GetText(nsAString& aText)
{
nsAutoString text;
if (!nsContentUtils::GetNodeTextContent(this, false, text)) {
NS_RUNTIMEABORT("OOM");
}
text.CompressWhitespace(true, true);
aText = text;
}
nsresult
HTMLMenuItemElement::AfterSetAttr(int32_t aNameSpaceID, nsIAtom* aName,
const nsAttrValue* aValue, bool aNotify)
{
if (aNameSpaceID == kNameSpaceID_None) {
if ((aName == nsGkAtoms::radiogroup || aName == nsGkAtoms::type) &&
mType == CMD_TYPE_RADIO &&
!mParserCreating) {
if (IsInDoc() && GetParent()) {
AddedToRadioGroup();
}
}
// Checked must be set no matter what type of menuitem it is, since
// GetChecked() must reflect the new value
if (aName == nsGkAtoms::checked &&
!mCheckedDirty) {
if (mParserCreating) {
mShouldInitChecked = true;
} else {
InitChecked();
}
}
}
return nsGenericHTMLElement::AfterSetAttr(aNameSpaceID, aName, aValue,
aNotify);
}
void
HTMLMenuItemElement::WalkRadioGroup(Visitor* aVisitor)
{
nsIContent* parent = GetParent();
if (!parent) {
aVisitor->Visit(this);
return;
}
nsAttrInfo info1(GetAttrInfo(kNameSpaceID_None,
nsGkAtoms::radiogroup));
bool info1Empty = !info1.mValue || info1.mValue->IsEmptyString();
for (nsIContent* cur = parent->GetFirstChild();
cur;
cur = cur->GetNextSibling()) {
HTMLMenuItemElement* menuitem = HTMLMenuItemElement::FromContent(cur);
if (!menuitem || menuitem->GetType() != CMD_TYPE_RADIO) {
continue;
}
nsAttrInfo info2(menuitem->GetAttrInfo(kNameSpaceID_None,
nsGkAtoms::radiogroup));
bool info2Empty = !info2.mValue || info2.mValue->IsEmptyString();
if (info1Empty != info2Empty ||
(info1.mValue && info2.mValue && !info1.mValue->Equals(*info2.mValue))) {
continue;
}
if (!aVisitor->Visit(menuitem)) {
break;
}
}
}
HTMLMenuItemElement*
HTMLMenuItemElement::GetSelectedRadio()
{
HTMLMenuItemElement* result = nullptr;
GetCheckedVisitor visitor(&result);
WalkRadioGroup(&visitor);
return result;
}
void
HTMLMenuItemElement::AddedToRadioGroup()
{
bool checkedDirty = mCheckedDirty;
if (mChecked) {
ClearCheckedVisitor visitor1(this);
GetCheckedDirtyVisitor visitor2(&checkedDirty, this);
CombinedVisitor visitor(&visitor1, &visitor2);
WalkRadioGroup(&visitor);
} else {
GetCheckedDirtyVisitor visitor(&checkedDirty, this);
WalkRadioGroup(&visitor);
}
mCheckedDirty = checkedDirty;
}
void
HTMLMenuItemElement::InitChecked()
{
bool defaultChecked;
GetDefaultChecked(&defaultChecked);
mChecked = defaultChecked;
if (mType == CMD_TYPE_RADIO) {
ClearCheckedVisitor visitor(this);
WalkRadioGroup(&visitor);
}
}
JSObject*
Bug 1117172 part 3. Change the wrappercached WrapObject methods to allow passing in aGivenProto. r=peterv The only manual changes here are to BindingUtils.h, BindingUtils.cpp, Codegen.py, Element.cpp, IDBFileRequest.cpp, IDBObjectStore.cpp, dom/workers/Navigator.cpp, WorkerPrivate.cpp, DeviceStorageRequestChild.cpp, Notification.cpp, nsGlobalWindow.cpp, MessagePort.cpp, nsJSEnvironment.cpp, Sandbox.cpp, XPCConvert.cpp, ExportHelpers.cpp, and DataStoreService.cpp. The rest of this diff was generated by running the following commands: find . -name "*.h" -o -name "*.cpp" | xargs perl -pi -e 'BEGIN { $/ = undef } s/(WrapObjectInternal\(JSContext *\* *(?:aCx|cx|aContext|aCtx|js))\)/\1, JS::Handle<JSObject*> aGivenProto)/g' find . -name "*.h" -o -name "*.cpp" | xargs perl -pi -e 'BEGIN { $/ = undef } s/(WrapObjectInternal\((?:aCx|cx|aContext|aCtx|js))\)/\1, aGivenProto)/g' find . -name "*.h" -o -name "*.cpp" | xargs perl -pi -e 'BEGIN { $/ = undef } s/(WrapNode\(JSContext *\* *(?:aCx|cx|aContext|aCtx|js))\)/\1, JS::Handle<JSObject*> aGivenProto)/g' find . -name "*.h" -o -name "*.cpp" | xargs perl -pi -e 'BEGIN { $/ = undef } s/(WrapNode\((?:aCx|cx|aContext|aCtx|js))\)/\1, aGivenProto)/g' find . -name "*.h" -o -name "*.cpp" | xargs perl -pi -e 'BEGIN { $/ = undef } s/(WrapObject\(JSContext *\* *(?:aCx|cx|aContext|aCtx|js))\)/\1, JS::Handle<JSObject*> aGivenProto)/g' find . -name "*.h" -o -name "*.cpp" | xargs perl -pi -e 'BEGIN { $/ = undef } s/(Binding(?:_workers)?::Wrap\((?:aCx|cx|aContext|aCtx|js), [^,)]+)\)/\1, aGivenProto)/g'
2015-03-19 07:13:33 -07:00
HTMLMenuItemElement::WrapNode(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
{
Bug 1117172 part 3. Change the wrappercached WrapObject methods to allow passing in aGivenProto. r=peterv The only manual changes here are to BindingUtils.h, BindingUtils.cpp, Codegen.py, Element.cpp, IDBFileRequest.cpp, IDBObjectStore.cpp, dom/workers/Navigator.cpp, WorkerPrivate.cpp, DeviceStorageRequestChild.cpp, Notification.cpp, nsGlobalWindow.cpp, MessagePort.cpp, nsJSEnvironment.cpp, Sandbox.cpp, XPCConvert.cpp, ExportHelpers.cpp, and DataStoreService.cpp. The rest of this diff was generated by running the following commands: find . -name "*.h" -o -name "*.cpp" | xargs perl -pi -e 'BEGIN { $/ = undef } s/(WrapObjectInternal\(JSContext *\* *(?:aCx|cx|aContext|aCtx|js))\)/\1, JS::Handle<JSObject*> aGivenProto)/g' find . -name "*.h" -o -name "*.cpp" | xargs perl -pi -e 'BEGIN { $/ = undef } s/(WrapObjectInternal\((?:aCx|cx|aContext|aCtx|js))\)/\1, aGivenProto)/g' find . -name "*.h" -o -name "*.cpp" | xargs perl -pi -e 'BEGIN { $/ = undef } s/(WrapNode\(JSContext *\* *(?:aCx|cx|aContext|aCtx|js))\)/\1, JS::Handle<JSObject*> aGivenProto)/g' find . -name "*.h" -o -name "*.cpp" | xargs perl -pi -e 'BEGIN { $/ = undef } s/(WrapNode\((?:aCx|cx|aContext|aCtx|js))\)/\1, aGivenProto)/g' find . -name "*.h" -o -name "*.cpp" | xargs perl -pi -e 'BEGIN { $/ = undef } s/(WrapObject\(JSContext *\* *(?:aCx|cx|aContext|aCtx|js))\)/\1, JS::Handle<JSObject*> aGivenProto)/g' find . -name "*.h" -o -name "*.cpp" | xargs perl -pi -e 'BEGIN { $/ = undef } s/(Binding(?:_workers)?::Wrap\((?:aCx|cx|aContext|aCtx|js), [^,)]+)\)/\1, aGivenProto)/g'
2015-03-19 07:13:33 -07:00
return HTMLMenuItemElementBinding::Wrap(aCx, this, aGivenProto);
}
} // namespace dom
} // namespace mozilla
#undef NS_ORIGINAL_CHECKED_VALUE