From 36d22d82aa202bb199967e9512281e9a53db42c9 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 21:33:14 +0200 Subject: Adding upstream version 115.7.0esr. Signed-off-by: Daniel Baumann --- dom/xul/ChromeObserver.cpp | 229 +++ dom/xul/ChromeObserver.h | 43 + dom/xul/MenuBarListener.cpp | 489 +++++ dom/xul/MenuBarListener.h | 78 + dom/xul/XULBroadcastManager.cpp | 592 ++++++ dom/xul/XULBroadcastManager.h | 91 + dom/xul/XULButtonElement.cpp | 802 ++++++++ dom/xul/XULButtonElement.h | 123 ++ dom/xul/XULFrameElement.cpp | 200 ++ dom/xul/XULFrameElement.h | 84 + dom/xul/XULMenuBarElement.cpp | 102 + dom/xul/XULMenuBarElement.h | 59 + dom/xul/XULMenuElement.cpp | 82 + dom/xul/XULMenuElement.h | 37 + dom/xul/XULMenuParentElement.cpp | 398 ++++ dom/xul/XULMenuParentElement.h | 80 + dom/xul/XULPersist.cpp | 247 +++ dom/xul/XULPersist.h | 50 + dom/xul/XULPopupElement.cpp | 339 ++++ dom/xul/XULPopupElement.h | 118 ++ dom/xul/XULResizerElement.cpp | 354 ++++ dom/xul/XULResizerElement.h | 62 + dom/xul/XULTextElement.cpp | 49 + dom/xul/XULTextElement.h | 50 + dom/xul/XULTooltipElement.cpp | 104 + dom/xul/XULTooltipElement.h | 35 + dom/xul/XULTreeElement.cpp | 411 ++++ dom/xul/XULTreeElement.h | 129 ++ dom/xul/crashtests/107518-1.xml | 52 + dom/xul/crashtests/253479-1.xhtml | 6 + dom/xul/crashtests/253479-2.xhtml | 4 + dom/xul/crashtests/326204-1.xhtml | 0 dom/xul/crashtests/326644-1-inner.xhtml | 34 + dom/xul/crashtests/326644-1.html | 9 + dom/xul/crashtests/326875-1.xhtml | 27 + dom/xul/crashtests/329982-1.xhtml | 42 + dom/xul/crashtests/336096-1.xhtml | 41 + dom/xul/crashtests/344215-1.xhtml | 7 + dom/xul/crashtests/354611-1.html | 20 + dom/xul/crashtests/360078-1.xhtml | 42 + dom/xul/crashtests/363791-1.xhtml | 44 + dom/xul/crashtests/384877-1-inner.xhtml | 12 + dom/xul/crashtests/384877-1.html | 9 + dom/xul/crashtests/386914-1-inner.xhtml | 10 + dom/xul/crashtests/386914-1.html | 9 + dom/xul/crashtests/425821-1.xhtml | 15 + dom/xul/crashtests/428951-1.xhtml | 21 + dom/xul/crashtests/431906-1-inner.xhtml | 19 + dom/xul/crashtests/431906-1.html | 9 + dom/xul/crashtests/461917-1.xhtml | 6 + dom/xul/crashtests/crashtests.list | 17 + dom/xul/moz.build | 94 + dom/xul/nsIBrowserController.idl | 20 + dom/xul/nsIController.idl | 39 + dom/xul/nsIControllers.idl | 34 + dom/xul/nsXULCommandDispatcher.cpp | 435 +++++ dom/xul/nsXULCommandDispatcher.h | 82 + dom/xul/nsXULContentSink.cpp | 869 +++++++++ dom/xul/nsXULContentSink.h | 143 ++ dom/xul/nsXULContentUtils.cpp | 89 + dom/xul/nsXULContentUtils.h | 45 + dom/xul/nsXULControllers.cpp | 214 +++ dom/xul/nsXULControllers.h | 51 + dom/xul/nsXULElement.cpp | 2008 ++++++++++++++++++++ dom/xul/nsXULElement.h | 538 ++++++ dom/xul/nsXULPopupListener.cpp | 285 +++ dom/xul/nsXULPopupListener.h | 59 + dom/xul/nsXULPrototypeCache.cpp | 500 +++++ dom/xul/nsXULPrototypeCache.h | 163 ++ dom/xul/nsXULPrototypeDocument.cpp | 511 +++++ dom/xul/nsXULPrototypeDocument.h | 127 ++ dom/xul/nsXULSortService.cpp | 395 ++++ dom/xul/nsXULSortService.h | 42 + dom/xul/test/398289-resource.xhtml | 49 + dom/xul/test/chrome.ini | 26 + dom/xul/test/file_bug236853.rdf | 14 + dom/xul/test/mochitest.ini | 3 + dom/xul/test/test_accesskey.xhtml | 57 + .../test/test_bug1070049_throw_from_script.xhtml | 40 + dom/xul/test/test_bug1290965.xhtml | 38 + dom/xul/test/test_bug1686822.xhtml | 35 + dom/xul/test/test_bug199692.xhtml | 102 + dom/xul/test/test_bug311681.xhtml | 107 ++ dom/xul/test/test_bug391002.xhtml | 41 + dom/xul/test/test_bug398289.html | 41 + dom/xul/test/test_bug403868.xhtml | 83 + dom/xul/test/test_bug418216.xhtml | 46 + dom/xul/test/test_bug445177.xhtml | 66 + dom/xul/test/test_bug468176.xhtml | 84 + dom/xul/test/test_bug583948.xhtml | 41 + dom/xul/test/test_bug749367.xhtml | 39 + dom/xul/test/test_bug757137.xhtml | 37 + dom/xul/test/test_bug775972.xhtml | 35 + dom/xul/test/test_disable_scroll_frame_plain.html | 19 + dom/xul/test/test_html_template.xhtml | 18 + dom/xul/test/test_import_xul_to_content.xhtml | 68 + dom/xul/test/test_xul_tabindex_focus.xhtml | 44 + dom/xul/test/window_bug1686822.xhtml | 7 + dom/xul/test/window_bug583948.xhtml | 8 + dom/xul/test/window_bug757137.xhtml | 6 + 100 files changed, 13789 insertions(+) create mode 100644 dom/xul/ChromeObserver.cpp create mode 100644 dom/xul/ChromeObserver.h create mode 100644 dom/xul/MenuBarListener.cpp create mode 100644 dom/xul/MenuBarListener.h create mode 100644 dom/xul/XULBroadcastManager.cpp create mode 100644 dom/xul/XULBroadcastManager.h create mode 100644 dom/xul/XULButtonElement.cpp create mode 100644 dom/xul/XULButtonElement.h create mode 100644 dom/xul/XULFrameElement.cpp create mode 100644 dom/xul/XULFrameElement.h create mode 100644 dom/xul/XULMenuBarElement.cpp create mode 100644 dom/xul/XULMenuBarElement.h create mode 100644 dom/xul/XULMenuElement.cpp create mode 100644 dom/xul/XULMenuElement.h create mode 100644 dom/xul/XULMenuParentElement.cpp create mode 100644 dom/xul/XULMenuParentElement.h create mode 100644 dom/xul/XULPersist.cpp create mode 100644 dom/xul/XULPersist.h create mode 100644 dom/xul/XULPopupElement.cpp create mode 100644 dom/xul/XULPopupElement.h create mode 100644 dom/xul/XULResizerElement.cpp create mode 100644 dom/xul/XULResizerElement.h create mode 100644 dom/xul/XULTextElement.cpp create mode 100644 dom/xul/XULTextElement.h create mode 100644 dom/xul/XULTooltipElement.cpp create mode 100644 dom/xul/XULTooltipElement.h create mode 100644 dom/xul/XULTreeElement.cpp create mode 100644 dom/xul/XULTreeElement.h create mode 100644 dom/xul/crashtests/107518-1.xml create mode 100644 dom/xul/crashtests/253479-1.xhtml create mode 100644 dom/xul/crashtests/253479-2.xhtml create mode 100644 dom/xul/crashtests/326204-1.xhtml create mode 100644 dom/xul/crashtests/326644-1-inner.xhtml create mode 100644 dom/xul/crashtests/326644-1.html create mode 100644 dom/xul/crashtests/326875-1.xhtml create mode 100644 dom/xul/crashtests/329982-1.xhtml create mode 100644 dom/xul/crashtests/336096-1.xhtml create mode 100644 dom/xul/crashtests/344215-1.xhtml create mode 100644 dom/xul/crashtests/354611-1.html create mode 100644 dom/xul/crashtests/360078-1.xhtml create mode 100644 dom/xul/crashtests/363791-1.xhtml create mode 100644 dom/xul/crashtests/384877-1-inner.xhtml create mode 100644 dom/xul/crashtests/384877-1.html create mode 100644 dom/xul/crashtests/386914-1-inner.xhtml create mode 100644 dom/xul/crashtests/386914-1.html create mode 100644 dom/xul/crashtests/425821-1.xhtml create mode 100644 dom/xul/crashtests/428951-1.xhtml create mode 100644 dom/xul/crashtests/431906-1-inner.xhtml create mode 100644 dom/xul/crashtests/431906-1.html create mode 100644 dom/xul/crashtests/461917-1.xhtml create mode 100644 dom/xul/crashtests/crashtests.list create mode 100644 dom/xul/moz.build create mode 100644 dom/xul/nsIBrowserController.idl create mode 100644 dom/xul/nsIController.idl create mode 100644 dom/xul/nsIControllers.idl create mode 100644 dom/xul/nsXULCommandDispatcher.cpp create mode 100644 dom/xul/nsXULCommandDispatcher.h create mode 100644 dom/xul/nsXULContentSink.cpp create mode 100644 dom/xul/nsXULContentSink.h create mode 100644 dom/xul/nsXULContentUtils.cpp create mode 100644 dom/xul/nsXULContentUtils.h create mode 100644 dom/xul/nsXULControllers.cpp create mode 100644 dom/xul/nsXULControllers.h create mode 100644 dom/xul/nsXULElement.cpp create mode 100644 dom/xul/nsXULElement.h create mode 100644 dom/xul/nsXULPopupListener.cpp create mode 100644 dom/xul/nsXULPopupListener.h create mode 100644 dom/xul/nsXULPrototypeCache.cpp create mode 100644 dom/xul/nsXULPrototypeCache.h create mode 100644 dom/xul/nsXULPrototypeDocument.cpp create mode 100644 dom/xul/nsXULPrototypeDocument.h create mode 100644 dom/xul/nsXULSortService.cpp create mode 100644 dom/xul/nsXULSortService.h create mode 100644 dom/xul/test/398289-resource.xhtml create mode 100644 dom/xul/test/chrome.ini create mode 100644 dom/xul/test/file_bug236853.rdf create mode 100644 dom/xul/test/mochitest.ini create mode 100644 dom/xul/test/test_accesskey.xhtml create mode 100644 dom/xul/test/test_bug1070049_throw_from_script.xhtml create mode 100644 dom/xul/test/test_bug1290965.xhtml create mode 100644 dom/xul/test/test_bug1686822.xhtml create mode 100644 dom/xul/test/test_bug199692.xhtml create mode 100644 dom/xul/test/test_bug311681.xhtml create mode 100644 dom/xul/test/test_bug391002.xhtml create mode 100644 dom/xul/test/test_bug398289.html create mode 100644 dom/xul/test/test_bug403868.xhtml create mode 100644 dom/xul/test/test_bug418216.xhtml create mode 100644 dom/xul/test/test_bug445177.xhtml create mode 100644 dom/xul/test/test_bug468176.xhtml create mode 100644 dom/xul/test/test_bug583948.xhtml create mode 100644 dom/xul/test/test_bug749367.xhtml create mode 100644 dom/xul/test/test_bug757137.xhtml create mode 100644 dom/xul/test/test_bug775972.xhtml create mode 100644 dom/xul/test/test_disable_scroll_frame_plain.html create mode 100644 dom/xul/test/test_html_template.xhtml create mode 100644 dom/xul/test/test_import_xul_to_content.xhtml create mode 100644 dom/xul/test/test_xul_tabindex_focus.xhtml create mode 100644 dom/xul/test/window_bug1686822.xhtml create mode 100644 dom/xul/test/window_bug583948.xhtml create mode 100644 dom/xul/test/window_bug757137.xhtml (limited to 'dom/xul') diff --git a/dom/xul/ChromeObserver.cpp b/dom/xul/ChromeObserver.cpp new file mode 100644 index 0000000000..092641d493 --- /dev/null +++ b/dom/xul/ChromeObserver.cpp @@ -0,0 +1,229 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 https://mozilla.org/MPL/2.0/. */ + +#include "ChromeObserver.h" + +#include "nsIBaseWindow.h" +#include "nsIWidget.h" +#include "nsIFrame.h" + +#include "nsContentUtils.h" +#include "nsView.h" +#include "nsPresContext.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/DocumentInlines.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/MutationEventBinding.h" +#include "nsXULElement.h" + +namespace mozilla::dom { + +NS_IMPL_ISUPPORTS(ChromeObserver, nsIMutationObserver) + +ChromeObserver::ChromeObserver(Document* aDocument) + : nsStubMutationObserver(), mDocument(aDocument) {} + +ChromeObserver::~ChromeObserver() = default; + +void ChromeObserver::Init() { + mDocument->AddMutationObserver(this); + Element* rootElement = mDocument->GetRootElement(); + if (!rootElement) { + return; + } + nsAutoScriptBlocker scriptBlocker; + uint32_t attributeCount = rootElement->GetAttrCount(); + for (uint32_t i = 0; i < attributeCount; i++) { + BorrowedAttrInfo info = rootElement->GetAttrInfoAt(i); + const nsAttrName* name = info.mName; + if (name->LocalName() == nsGkAtoms::chromemargin) { + // Some linux windows managers have an issue when the chrome margin is + // applied while the browser is loading (bug 1598848). For now, skip + // applying this attribute when initializing. + continue; + } + AttributeChanged(rootElement, name->NamespaceID(), name->LocalName(), + MutationEvent_Binding::ADDITION, nullptr); + } +} + +nsIWidget* ChromeObserver::GetWindowWidget() { + // only top level chrome documents can set the titlebar color + if (mDocument && mDocument->IsRootDisplayDocument()) { + nsCOMPtr container = mDocument->GetContainer(); + nsCOMPtr baseWindow = do_QueryInterface(container); + if (baseWindow) { + nsCOMPtr mainWidget; + baseWindow->GetMainWidget(getter_AddRefs(mainWidget)); + return mainWidget; + } + } + return nullptr; +} + +class SetDrawInTitleBarEvent : public Runnable { + public: + SetDrawInTitleBarEvent(nsIWidget* aWidget, bool aState) + : mozilla::Runnable("SetDrawInTitleBarEvent"), + mWidget(aWidget), + mState(aState) {} + + NS_IMETHOD Run() override { + NS_ASSERTION(mWidget, + "You shouldn't call this runnable with a null widget!"); + + mWidget->SetDrawsInTitlebar(mState); + return NS_OK; + } + + private: + nsCOMPtr mWidget; + bool mState; +}; + +void ChromeObserver::SetDrawsInTitlebar(bool aState) { + nsIWidget* mainWidget = GetWindowWidget(); + if (mainWidget) { + nsContentUtils::AddScriptRunner( + new SetDrawInTitleBarEvent(mainWidget, aState)); + } +} + +void ChromeObserver::SetDrawsTitle(bool aState) { + nsIWidget* mainWidget = GetWindowWidget(); + if (mainWidget) { + // We can do this synchronously because SetDrawsTitle doesn't have any + // synchronous effects apart from a harmless invalidation. + mainWidget->SetDrawsTitle(aState); + } +} + +class MarginSetter : public Runnable { + public: + explicit MarginSetter(nsIWidget* aWidget) + : mozilla::Runnable("MarginSetter"), + mWidget(aWidget), + mMargin(-1, -1, -1, -1) {} + MarginSetter(nsIWidget* aWidget, const LayoutDeviceIntMargin& aMargin) + : mozilla::Runnable("MarginSetter"), mWidget(aWidget), mMargin(aMargin) {} + + NS_IMETHOD Run() override { + // SetNonClientMargins can dispatch native events, hence doing + // it off a script runner. + mWidget->SetNonClientMargins(mMargin); + return NS_OK; + } + + private: + nsCOMPtr mWidget; + LayoutDeviceIntMargin mMargin; +}; + +void ChromeObserver::SetChromeMargins(const nsAttrValue* aValue) { + if (!aValue) return; + + nsIWidget* mainWidget = GetWindowWidget(); + if (!mainWidget) return; + + // top, right, bottom, left - see nsAttrValue + nsAutoString tmp; + aValue->ToString(tmp); + nsIntMargin margins; + if (nsContentUtils::ParseIntMarginValue(tmp, margins)) { + nsContentUtils::AddScriptRunner(new MarginSetter( + mainWidget, LayoutDeviceIntMargin::FromUnknownMargin(margins))); + } +} + +void ChromeObserver::AttributeChanged(dom::Element* aElement, + int32_t aNamespaceID, nsAtom* aName, + int32_t aModType, + const nsAttrValue* aOldValue) { + // We only care about changes to the root element. + if (!mDocument || aElement != mDocument->GetRootElement()) { + return; + } + + const nsAttrValue* value = aElement->GetParsedAttr(aName, aNamespaceID); + if (value) { + // Hide chrome if needed + if (aName == nsGkAtoms::hidechrome) { + HideWindowChrome(value->Equals(u"true"_ns, eCaseMatters)); + } else if (aName == nsGkAtoms::chromemargin) { + SetChromeMargins(value); + } + // title and drawintitlebar are settable on + // any root node (windows, dialogs, etc) + else if (aName == nsGkAtoms::title) { + mDocument->NotifyPossibleTitleChange(false); + } else if (aName == nsGkAtoms::drawintitlebar) { + SetDrawsInTitlebar(value->Equals(u"true"_ns, eCaseMatters)); + } else if (aName == nsGkAtoms::drawtitle) { + SetDrawsTitle(value->Equals(u"true"_ns, eCaseMatters)); + } else if (aName == nsGkAtoms::localedir) { + // if the localedir changed on the root element, reset the document + // direction + mDocument->ResetDocumentDirection(); + } else if (aName == nsGkAtoms::lwtheme) { + // if the lwtheme changed, make sure to reset the document lwtheme + // cache + mDocument->ResetDocumentLWTheme(); + } + } else { + if (aName == nsGkAtoms::hidechrome) { + HideWindowChrome(false); + } else if (aName == nsGkAtoms::chromemargin) { + ResetChromeMargins(); + } else if (aName == nsGkAtoms::localedir) { + // if the localedir changed on the root element, reset the document + // direction + mDocument->ResetDocumentDirection(); + } else if (aName == nsGkAtoms::lwtheme) { + // if the lwtheme changed, make sure to restyle appropriately + mDocument->ResetDocumentLWTheme(); + } else if (aName == nsGkAtoms::drawintitlebar) { + SetDrawsInTitlebar(false); + } else if (aName == nsGkAtoms::drawtitle) { + SetDrawsTitle(false); + } + } +} + +void ChromeObserver::NodeWillBeDestroyed(nsINode* aNode) { + mDocument = nullptr; +} + +void ChromeObserver::ResetChromeMargins() { + nsIWidget* mainWidget = GetWindowWidget(); + if (!mainWidget) return; + // See nsIWidget + nsContentUtils::AddScriptRunner(new MarginSetter(mainWidget)); +} + +nsresult ChromeObserver::HideWindowChrome(bool aShouldHide) { + // only top level chrome documents can hide the window chrome + if (!mDocument->IsRootDisplayDocument()) return NS_OK; + + nsPresContext* presContext = mDocument->GetPresContext(); + + if (presContext && presContext->IsChrome()) { + nsIFrame* frame = mDocument->GetDocumentElement()->GetPrimaryFrame(); + + if (frame) { + nsView* view = frame->GetClosestView(); + + if (view) { + nsIWidget* w = view->GetWidget(); + NS_ENSURE_STATE(w); + w->HideWindowChrome(aShouldHide); + } + } + } + + return NS_OK; +} + +} // namespace mozilla::dom diff --git a/dom/xul/ChromeObserver.h b/dom/xul/ChromeObserver.h new file mode 100644 index 0000000000..af897c4420 --- /dev/null +++ b/dom/xul/ChromeObserver.h @@ -0,0 +1,43 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_dom_ChromeObserver_h +#define mozilla_dom_ChromeObserver_h + +#include "nsStubMutationObserver.h" + +class nsIWidget; + +namespace mozilla::dom { +class Document; + +class ChromeObserver final : public nsStubMutationObserver { + public: + NS_DECL_ISUPPORTS + + explicit ChromeObserver(Document* aDocument); + void Init(); + + NS_DECL_NSIMUTATIONOBSERVER_ATTRIBUTECHANGED + NS_DECL_NSIMUTATIONOBSERVER_NODEWILLBEDESTROYED + + protected: + nsIWidget* GetWindowWidget(); + void SetDrawsInTitlebar(bool aState); + void SetDrawsTitle(bool aState); + void SetChromeMargins(const nsAttrValue* aValue); + nsresult HideWindowChrome(bool aShouldHide); + + private: + void ResetChromeMargins(); + ~ChromeObserver(); + // A weak pointer cleared when the element will be destroyed. + Document* MOZ_NON_OWNING_REF mDocument; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_ChromeObserver_h diff --git a/dom/xul/MenuBarListener.cpp b/dom/xul/MenuBarListener.cpp new file mode 100644 index 0000000000..995ec422c5 --- /dev/null +++ b/dom/xul/MenuBarListener.cpp @@ -0,0 +1,489 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 "MenuBarListener.h" +#include "XULButtonElement.h" +#include "mozilla/Attributes.h" +#include "nsISound.h" + +// Drag & Drop, Clipboard +#include "nsWidgetsCID.h" +#include "nsCOMPtr.h" + +#include "nsContentUtils.h" +#include "nsPIWindowRoot.h" +#include "nsIFrame.h" +#include "mozilla/BasicEvents.h" +#include "mozilla/Preferences.h" +#include "mozilla/LookAndFeel.h" +#include "mozilla/StaticPrefs_ui.h" +#include "mozilla/TextEvents.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/Event.h" +#include "mozilla/dom/EventBinding.h" +#include "mozilla/dom/KeyboardEvent.h" +#include "mozilla/dom/KeyboardEventBinding.h" +#include "mozilla/dom/XULButtonElement.h" +#include "mozilla/dom/XULMenuBarElement.h" +#include "mozilla/dom/XULMenuParentElement.h" +#include "nsXULPopupManager.h" + +namespace mozilla::dom { + +NS_IMPL_ISUPPORTS(MenuBarListener, nsIDOMEventListener) + +MenuBarListener::MenuBarListener(XULMenuBarElement& aElement) + : mMenuBar(&aElement), + mEventTarget(aElement.GetComposedDoc()), + mAccessKeyDown(false), + mAccessKeyDownCanceled(false) { + MOZ_ASSERT(mEventTarget); + MOZ_ASSERT(mMenuBar); + + // Hook up the menubar as a key listener on the whole document. This will + // see every keypress that occurs, but after everyone else does. + + // Also hook up the listener to the window listening for focus events. This + // is so we can keep proper state as the user alt-tabs through processes. + + mEventTarget->AddSystemEventListener(u"keypress"_ns, this, false); + mEventTarget->AddSystemEventListener(u"keydown"_ns, this, false); + mEventTarget->AddSystemEventListener(u"keyup"_ns, this, false); + mEventTarget->AddSystemEventListener(u"mozaccesskeynotfound"_ns, this, false); + // Need a capturing event listener if the user has blocked pages from + // overriding system keys so that we can prevent menu accesskeys from being + // cancelled. + mEventTarget->AddEventListener(u"keydown"_ns, this, true); + + // mousedown event should be handled in all phase + mEventTarget->AddEventListener(u"mousedown"_ns, this, true); + mEventTarget->AddEventListener(u"mousedown"_ns, this, false); + mEventTarget->AddEventListener(u"blur"_ns, this, true); + + mEventTarget->AddEventListener(u"MozDOMFullscreen:Entered"_ns, this, false); + + // Needs to listen to the deactivate event of the window. + RefPtr top = nsContentUtils::GetWindowRoot(mEventTarget); + if (!NS_WARN_IF(!top)) { + top->AddSystemEventListener(u"deactivate"_ns, this, true); + } +} + +//////////////////////////////////////////////////////////////////////// +MenuBarListener::~MenuBarListener() { + MOZ_ASSERT(!mEventTarget, "Should've detached always"); +} + +void MenuBarListener::Detach() { + if (!mMenuBar) { + MOZ_ASSERT(!mEventTarget); + return; + } + mEventTarget->RemoveSystemEventListener(u"keypress"_ns, this, false); + mEventTarget->RemoveSystemEventListener(u"keydown"_ns, this, false); + mEventTarget->RemoveSystemEventListener(u"keyup"_ns, this, false); + mEventTarget->RemoveSystemEventListener(u"mozaccesskeynotfound"_ns, this, + false); + mEventTarget->RemoveEventListener(u"keydown"_ns, this, true); + + mEventTarget->RemoveEventListener(u"mousedown"_ns, this, true); + mEventTarget->RemoveEventListener(u"mousedown"_ns, this, false); + mEventTarget->RemoveEventListener(u"blur"_ns, this, true); + + mEventTarget->RemoveEventListener(u"MozDOMFullscreen:Entered"_ns, this, + false); + RefPtr top = nsContentUtils::GetWindowRoot(mEventTarget); + if (!NS_WARN_IF(!top)) { + top->RemoveSystemEventListener(u"deactivate"_ns, this, true); + } + mMenuBar = nullptr; + mEventTarget = nullptr; +} + +void MenuBarListener::ToggleMenuActiveState(ByKeyboard aByKeyboard) { + RefPtr menuBar = mMenuBar; + if (menuBar->IsActive()) { + menuBar->SetActive(false); + } else { + if (aByKeyboard == ByKeyboard::Yes) { + menuBar->SetActiveByKeyboard(); + } + // This will activate the menubar if needed. + menuBar->SelectFirstItem(); + } +} + +//////////////////////////////////////////////////////////////////////// +nsresult MenuBarListener::KeyUp(Event* aKeyEvent) { + WidgetKeyboardEvent* nativeKeyEvent = + aKeyEvent->WidgetEventPtr()->AsKeyboardEvent(); + if (!nativeKeyEvent) { + return NS_OK; + } + + // handlers shouldn't be triggered by non-trusted events. + if (!nativeKeyEvent->IsTrusted()) { + return NS_OK; + } + + const auto accessKey = LookAndFeel::GetMenuAccessKey(); + if (!accessKey || !StaticPrefs::ui_key_menuAccessKeyFocuses()) { + return NS_OK; + } + + // On a press of the ALT key by itself, we toggle the menu's + // active/inactive state. + if (!nativeKeyEvent->DefaultPrevented() && mAccessKeyDown && + !mAccessKeyDownCanceled && nativeKeyEvent->mKeyCode == accessKey) { + // The access key was down and is now up, and no other + // keys were pressed in between. + bool toggleMenuActiveState = true; + if (!mMenuBar->IsActive()) { + // If the focused content is in a remote process, we should allow the + // focused web app to prevent to activate the menubar. + if (nativeKeyEvent->WillBeSentToRemoteProcess()) { + nativeKeyEvent->StopImmediatePropagation(); + nativeKeyEvent->MarkAsWaitingReplyFromRemoteProcess(); + return NS_OK; + } + // First, close all existing popups because other popups shouldn't + // handle key events when menubar is active and IME should be + // disabled. + if (nsXULPopupManager* pm = nsXULPopupManager::GetInstance()) { + pm->Rollup({}); + } + // If menubar active state is changed or the menubar is destroyed + // during closing the popups, we should do nothing anymore. + toggleMenuActiveState = !Destroyed() && !mMenuBar->IsActive(); + } + if (toggleMenuActiveState) { + ToggleMenuActiveState(ByKeyboard::Yes); + } + } + + mAccessKeyDown = false; + mAccessKeyDownCanceled = false; + + if (!Destroyed() && mMenuBar->IsActive()) { + nativeKeyEvent->StopPropagation(); + nativeKeyEvent->PreventDefault(); + } + + return NS_OK; +} + +//////////////////////////////////////////////////////////////////////// +nsresult MenuBarListener::KeyPress(Event* aKeyEvent) { + // if event has already been handled, bail + if (!aKeyEvent || aKeyEvent->DefaultPrevented()) { + return NS_OK; // don't consume event + } + + // handlers shouldn't be triggered by non-trusted events. + if (!aKeyEvent->IsTrusted()) { + return NS_OK; + } + + const auto accessKey = LookAndFeel::GetMenuAccessKey(); + if (!accessKey) { + return NS_OK; + } + // If accesskey handling was forwarded to a child process, wait for + // the mozaccesskeynotfound event before handling accesskeys. + WidgetKeyboardEvent* nativeKeyEvent = + aKeyEvent->WidgetEventPtr()->AsKeyboardEvent(); + if (!nativeKeyEvent) { + return NS_OK; + } + + RefPtr keyEvent = aKeyEvent->AsKeyboardEvent(); + uint32_t keyCode = keyEvent->KeyCode(); + + // Cancel the access key flag unless we are pressing the access key. + if (keyCode != accessKey) { + mAccessKeyDownCanceled = true; + } + +#ifndef XP_MACOSX + // Need to handle F10 specially on Non-Mac platform. + if (nativeKeyEvent->mMessage == eKeyPress && keyCode == NS_VK_F10) { + if ((keyEvent->GetModifiersForMenuAccessKey() & ~MODIFIER_CONTROL) == 0) { + // If the keyboard event should activate the menubar and will be + // sent to a remote process, it should be executed with reply + // event from the focused remote process. Note that if the menubar + // is active, the event is already marked as "stop cross + // process dispatching". So, in that case, this won't wait + // reply from the remote content. + if (nativeKeyEvent->WillBeSentToRemoteProcess()) { + nativeKeyEvent->StopImmediatePropagation(); + nativeKeyEvent->MarkAsWaitingReplyFromRemoteProcess(); + return NS_OK; + } + // The F10 key just went down by itself or with ctrl pressed. + // In Windows, both of these activate the menu bar. + ToggleMenuActiveState(ByKeyboard::Yes); + + if (mMenuBar && mMenuBar->IsActive()) { +# ifdef MOZ_WIDGET_GTK + if (RefPtr child = mMenuBar->GetActiveMenuChild()) { + // In GTK, this also opens the first menu. + child->OpenMenuPopup(false); + } +# endif + aKeyEvent->StopPropagation(); + aKeyEvent->PreventDefault(); + } + } + + return NS_OK; + } +#endif // !XP_MACOSX + + RefPtr menuForKey = GetMenuForKeyEvent(*keyEvent); + if (!menuForKey) { +#ifdef XP_WIN + // Behavior on Windows - this item is on the menu bar, beep and deactivate + // the menu bar. + // TODO(emilio): This is rather odd, and I cannot get the beep to work, + // but this matches what old code was doing... + if (mMenuBar && mMenuBar->IsActive() && mMenuBar->IsActiveByKeyboard()) { + if (nsCOMPtr sound = do_GetService("@mozilla.org/sound;1")) { + sound->Beep(); + } + ToggleMenuActiveState(ByKeyboard::Yes); + } +#endif + return NS_OK; + } + + // If the keyboard event matches with a menu item's accesskey and + // will be sent to a remote process, it should be executed with + // reply event from the focused remote process. Note that if the + // menubar is active, the event is already marked as "stop cross + // process dispatching". So, in that case, this won't wait + // reply from the remote content. + if (nativeKeyEvent->WillBeSentToRemoteProcess()) { + nativeKeyEvent->StopImmediatePropagation(); + nativeKeyEvent->MarkAsWaitingReplyFromRemoteProcess(); + return NS_OK; + } + + RefPtr menuBar = mMenuBar; + menuBar->SetActiveByKeyboard(); + // This will activate the menubar as needed. + menuForKey->OpenMenuPopup(true); + + // The opened menu will listen next keyup event. + // Therefore, we should clear the keydown flags here. + mAccessKeyDown = mAccessKeyDownCanceled = false; + + aKeyEvent->StopPropagation(); + aKeyEvent->PreventDefault(); + return NS_OK; +} + +dom::XULButtonElement* MenuBarListener::GetMenuForKeyEvent( + KeyboardEvent& aKeyEvent) { + if (!aKeyEvent.IsMenuAccessKeyPressed()) { + return nullptr; + } + + uint32_t charCode = aKeyEvent.CharCode(); + bool hasAccessKeyCandidates = charCode != 0; + if (!hasAccessKeyCandidates) { + WidgetKeyboardEvent* nativeKeyEvent = + aKeyEvent.WidgetEventPtr()->AsKeyboardEvent(); + AutoTArray keys; + nativeKeyEvent->GetAccessKeyCandidates(keys); + hasAccessKeyCandidates = !keys.IsEmpty(); + } + + if (!hasAccessKeyCandidates) { + return nullptr; + } + // Do shortcut navigation. + // A letter was pressed. We want to see if a shortcut gets matched. If + // so, we'll know the menu got activated. + return mMenuBar->FindMenuWithShortcut(aKeyEvent); +} + +void MenuBarListener::ReserveKeyIfNeeded(Event* aKeyEvent) { + WidgetKeyboardEvent* nativeKeyEvent = + aKeyEvent->WidgetEventPtr()->AsKeyboardEvent(); + if (nsContentUtils::ShouldBlockReservedKeys(nativeKeyEvent)) { + nativeKeyEvent->MarkAsReservedByChrome(); + } +} + +//////////////////////////////////////////////////////////////////////// +nsresult MenuBarListener::KeyDown(Event* aKeyEvent) { + // handlers shouldn't be triggered by non-trusted events. + if (!aKeyEvent || !aKeyEvent->IsTrusted()) { + return NS_OK; + } + + RefPtr keyEvent = aKeyEvent->AsKeyboardEvent(); + if (!keyEvent) { + return NS_OK; + } + + uint32_t theChar = keyEvent->KeyCode(); + uint16_t eventPhase = keyEvent->EventPhase(); + bool capturing = (eventPhase == dom::Event_Binding::CAPTURING_PHASE); + +#ifndef XP_MACOSX + if (capturing && !mAccessKeyDown && theChar == NS_VK_F10 && + (keyEvent->GetModifiersForMenuAccessKey() & ~MODIFIER_CONTROL) == 0) { + ReserveKeyIfNeeded(aKeyEvent); + } +#endif + + const auto accessKey = LookAndFeel::GetMenuAccessKey(); + if (accessKey && StaticPrefs::ui_key_menuAccessKeyFocuses()) { + bool defaultPrevented = aKeyEvent->DefaultPrevented(); + + // No other modifiers can be down. + // Especially CTRL. CTRL+ALT == AltGR, and we'll break on non-US + // enhanced 102-key keyboards if we don't check this. + bool isAccessKeyDownEvent = + (theChar == accessKey && + (keyEvent->GetModifiersForMenuAccessKey() & + ~LookAndFeel::GetMenuAccessKeyModifiers()) == 0); + + if (!capturing && !mAccessKeyDown) { + // If accesskey isn't being pressed and the key isn't the accesskey, + // ignore the event. + if (!isAccessKeyDownEvent) { + return NS_OK; + } + + // Otherwise, accept the accesskey state. + mAccessKeyDown = true; + // If default is prevented already, cancel the access key down. + mAccessKeyDownCanceled = defaultPrevented; + return NS_OK; + } + + // If the pressed accesskey was canceled already or the event was + // consumed already, ignore the event. + if (mAccessKeyDownCanceled || defaultPrevented) { + return NS_OK; + } + + // Some key other than the access key just went down, + // so we won't activate the menu bar when the access key is released. + mAccessKeyDownCanceled = !isAccessKeyDownEvent; + } + + if (capturing && accessKey) { + if (GetMenuForKeyEvent(*keyEvent)) { + ReserveKeyIfNeeded(aKeyEvent); + } + } + + return NS_OK; // means I am NOT consuming event +} + +//////////////////////////////////////////////////////////////////////// + +nsresult MenuBarListener::Blur(Event* aEvent) { + if (!IsMenuOpen() && mMenuBar->IsActive()) { + ToggleMenuActiveState(ByKeyboard::No); + mAccessKeyDown = false; + mAccessKeyDownCanceled = false; + } + return NS_OK; // means I am NOT consuming event +} + +//////////////////////////////////////////////////////////////////////// + +nsresult MenuBarListener::OnWindowDeactivated(Event* aEvent) { + // Reset the accesskey state because we cannot receive the keyup event for + // the pressing accesskey. + mAccessKeyDown = false; + mAccessKeyDownCanceled = false; + return NS_OK; // means I am NOT consuming event +} + +bool MenuBarListener::IsMenuOpen() const { + auto* activeChild = mMenuBar->GetActiveMenuChild(); + return activeChild && activeChild->IsMenuPopupOpen(); +} + +//////////////////////////////////////////////////////////////////////// +nsresult MenuBarListener::MouseDown(Event* aMouseEvent) { + // NOTE: MouseDown method listens all phases + + // Even if the mousedown event is canceled, it means the user don't want + // to activate the menu. Therefore, we need to record it at capturing (or + // target) phase. + if (mAccessKeyDown) { + mAccessKeyDownCanceled = true; + } + + // Don't do anything at capturing phase, any behavior should be cancelable. + if (aMouseEvent->EventPhase() == dom::Event_Binding::CAPTURING_PHASE) { + return NS_OK; + } + + if (!IsMenuOpen() && mMenuBar->IsActive()) { + ToggleMenuActiveState(ByKeyboard::No); + } + + return NS_OK; // means I am NOT consuming event +} + +//////////////////////////////////////////////////////////////////////// + +nsresult MenuBarListener::Fullscreen(Event* aEvent) { + if (mMenuBar->IsActive()) { + ToggleMenuActiveState(ByKeyboard::No); + } + return NS_OK; +} + +//////////////////////////////////////////////////////////////////////// +MOZ_CAN_RUN_SCRIPT_BOUNDARY nsresult +MenuBarListener::HandleEvent(Event* aEvent) { + // If the menu bar is collapsed, don't do anything. + if (!mMenuBar || !mMenuBar->GetPrimaryFrame() || + !mMenuBar->GetPrimaryFrame()->StyleVisibility()->IsVisible()) { + return NS_OK; + } + + nsAutoString eventType; + aEvent->GetType(eventType); + + if (eventType.EqualsLiteral("keyup")) { + return KeyUp(aEvent); + } + if (eventType.EqualsLiteral("keydown")) { + return KeyDown(aEvent); + } + if (eventType.EqualsLiteral("keypress")) { + return KeyPress(aEvent); + } + if (eventType.EqualsLiteral("mozaccesskeynotfound")) { + return KeyPress(aEvent); + } + if (eventType.EqualsLiteral("blur")) { + return Blur(aEvent); + } + if (eventType.EqualsLiteral("deactivate")) { + return OnWindowDeactivated(aEvent); + } + if (eventType.EqualsLiteral("mousedown")) { + return MouseDown(aEvent); + } + if (eventType.EqualsLiteral("MozDOMFullscreen:Entered")) { + return Fullscreen(aEvent); + } + + MOZ_ASSERT_UNREACHABLE("Unexpected eventType"); + return NS_OK; +} + +} // namespace mozilla::dom diff --git a/dom/xul/MenuBarListener.h b/dom/xul/MenuBarListener.h new file mode 100644 index 0000000000..2985a77079 --- /dev/null +++ b/dom/xul/MenuBarListener.h @@ -0,0 +1,78 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ +#ifndef mozilla_dom_MenuBarListener_h +#define mozilla_dom_MenuBarListener_h + +#include "mozilla/Attributes.h" +#include "mozilla/EventForwards.h" +#include "nsIDOMEventListener.h" + +namespace mozilla::dom { +class Document; +class EventTarget; +class KeyboardEvent; +class XULMenuBarElement; +class XULButtonElement; + +class MenuBarListener final : public nsIDOMEventListener { + public: + explicit MenuBarListener(XULMenuBarElement&); + + NS_DECL_ISUPPORTS + + NS_DECL_NSIDOMEVENTLISTENER + + // Should be called when unbound from the document and so on. + void Detach(); + + protected: + virtual ~MenuBarListener(); + + bool IsMenuOpen() const; + + MOZ_CAN_RUN_SCRIPT nsresult KeyUp(Event* aMouseEvent); + MOZ_CAN_RUN_SCRIPT nsresult KeyDown(Event* aMouseEvent); + MOZ_CAN_RUN_SCRIPT nsresult KeyPress(Event* aMouseEvent); + MOZ_CAN_RUN_SCRIPT nsresult Blur(Event* aEvent); + MOZ_CAN_RUN_SCRIPT nsresult OnWindowDeactivated(Event* aEvent); + MOZ_CAN_RUN_SCRIPT nsresult MouseDown(Event* aMouseEvent); + MOZ_CAN_RUN_SCRIPT nsresult Fullscreen(Event* aEvent); + + /** + * Given a key event for an Alt+shortcut combination, + * return the menu, if any, that would be opened. If aPeek + * is false, then play a beep and deactivate the menubar on Windows. + */ + XULButtonElement* GetMenuForKeyEvent(KeyboardEvent& aKeyEvent); + + /** + * Call MarkAsReservedByChrome if the user's preferences indicate that + * the key should be chrome-only. + */ + void ReserveKeyIfNeeded(Event* aKeyEvent); + + // This should only be called by the MenuBarListener during event dispatch. + enum class ByKeyboard : bool { No, Yes }; + MOZ_CAN_RUN_SCRIPT void ToggleMenuActiveState(ByKeyboard); + + bool Destroyed() const { return !mMenuBar; } + + // The menu bar object. Safe because it keeps us alive. + XULMenuBarElement* mMenuBar; + // The event target to listen to the events. + // + // Weak reference is safe because we clear the listener on unbind from the + // document. + Document* mEventTarget; + // Whether or not the ALT key is currently down. + bool mAccessKeyDown = false; + // Whether or not the ALT key down is canceled by other action. + bool mAccessKeyDownCanceled = false; +}; + +} // namespace mozilla::dom + +#endif // #ifndef mozilla_dom_MenuBarListener_h diff --git a/dom/xul/XULBroadcastManager.cpp b/dom/xul/XULBroadcastManager.cpp new file mode 100644 index 0000000000..7347036f1c --- /dev/null +++ b/dom/xul/XULBroadcastManager.cpp @@ -0,0 +1,592 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=4 sw=2 et tw=80: */ +/* 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 "XULBroadcastManager.h" +#include "nsCOMPtr.h" +#include "nsContentUtils.h" +#include "mozilla/EventDispatcher.h" +#include "mozilla/Logging.h" +#include "mozilla/dom/DocumentInlines.h" +#include "nsXULElement.h" + +struct BroadcastListener { + nsWeakPtr mListener; + RefPtr mAttribute; +}; + +struct BroadcasterMapEntry : public PLDHashEntryHdr { + mozilla::dom::Element* mBroadcaster; // [WEAK] + nsTArray + mListeners; // [OWNING] of BroadcastListener objects +}; + +struct nsAttrNameInfo { + nsAttrNameInfo(int32_t aNamespaceID, nsAtom* aName, nsAtom* aPrefix) + : mNamespaceID(aNamespaceID), mName(aName), mPrefix(aPrefix) {} + nsAttrNameInfo(const nsAttrNameInfo& aOther) = delete; + nsAttrNameInfo(nsAttrNameInfo&& aOther) = default; + + int32_t mNamespaceID; + RefPtr mName; + RefPtr mPrefix; +}; + +static void ClearBroadcasterMapEntry(PLDHashTable* aTable, + PLDHashEntryHdr* aEntry) { + BroadcasterMapEntry* entry = static_cast(aEntry); + for (size_t i = entry->mListeners.Length() - 1; i != (size_t)-1; --i) { + delete entry->mListeners[i]; + } + entry->mListeners.Clear(); + + // N.B. that we need to manually run the dtor because we + // constructed the nsTArray object in-place. + entry->mListeners.~nsTArray(); +} + +static bool CanBroadcast(int32_t aNameSpaceID, nsAtom* aAttribute) { + // Don't push changes to the |id|, |persist|, |command| or + // |observes| attribute. + if (aNameSpaceID == kNameSpaceID_None) { + if ((aAttribute == nsGkAtoms::id) || (aAttribute == nsGkAtoms::persist) || + (aAttribute == nsGkAtoms::command) || + (aAttribute == nsGkAtoms::observes)) { + return false; + } + } + return true; +} + +namespace mozilla::dom { +static LazyLogModule sXULBroadCastManager("XULBroadcastManager"); + +class XULBroadcastManager::nsDelayedBroadcastUpdate { + public: + nsDelayedBroadcastUpdate(Element* aBroadcaster, Element* aListener, + const nsAString& aAttr) + : mBroadcaster(aBroadcaster), + mListener(aListener), + mAttr(aAttr), + mSetAttr(false), + mNeedsAttrChange(false) {} + + nsDelayedBroadcastUpdate(Element* aBroadcaster, Element* aListener, + nsAtom* aAttrName, const nsAString& aAttr, + bool aSetAttr, bool aNeedsAttrChange) + : mBroadcaster(aBroadcaster), + mListener(aListener), + mAttr(aAttr), + mAttrName(aAttrName), + mSetAttr(aSetAttr), + mNeedsAttrChange(aNeedsAttrChange) {} + + nsDelayedBroadcastUpdate(const nsDelayedBroadcastUpdate& aOther) = delete; + nsDelayedBroadcastUpdate(nsDelayedBroadcastUpdate&& aOther) = default; + + RefPtr mBroadcaster; + RefPtr mListener; + // Note if mAttrName isn't used, this is the name of the attr, otherwise + // this is the value of the attribute. + nsString mAttr; + RefPtr mAttrName; + bool mSetAttr; + bool mNeedsAttrChange; + + class Comparator { + public: + static bool Equals(const nsDelayedBroadcastUpdate& a, + const nsDelayedBroadcastUpdate& b) { + return a.mBroadcaster == b.mBroadcaster && a.mListener == b.mListener && + a.mAttrName == b.mAttrName; + } + }; +}; + +/* static */ +bool XULBroadcastManager::MayNeedListener(const Element& aElement) { + if (aElement.NodeInfo()->Equals(nsGkAtoms::observes, kNameSpaceID_XUL)) { + return true; + } + if (aElement.HasAttr(nsGkAtoms::observes)) { + return true; + } + if (aElement.HasAttr(nsGkAtoms::command) && + !(aElement.NodeInfo()->Equals(nsGkAtoms::menuitem, kNameSpaceID_XUL) || + aElement.NodeInfo()->Equals(nsGkAtoms::key, kNameSpaceID_XUL))) { + return true; + } + return false; +} + +XULBroadcastManager::XULBroadcastManager(Document* aDocument) + : mDocument(aDocument), + mBroadcasterMap(nullptr), + mHandlingDelayedAttrChange(false), + mHandlingDelayedBroadcasters(false) {} + +XULBroadcastManager::~XULBroadcastManager() { delete mBroadcasterMap; } + +void XULBroadcastManager::DropDocumentReference(void) { mDocument = nullptr; } + +void XULBroadcastManager::SynchronizeBroadcastListener(Element* aBroadcaster, + Element* aListener, + const nsAString& aAttr) { + if (!nsContentUtils::IsSafeToRunScript()) { + mDelayedBroadcasters.EmplaceBack(aBroadcaster, aListener, aAttr); + MaybeBroadcast(); + return; + } + bool notify = mHandlingDelayedBroadcasters; + + if (aAttr.EqualsLiteral("*")) { + uint32_t count = aBroadcaster->GetAttrCount(); + nsTArray attributes(count); + for (uint32_t i = 0; i < count; ++i) { + const nsAttrName* attrName = aBroadcaster->GetAttrNameAt(i); + int32_t nameSpaceID = attrName->NamespaceID(); + nsAtom* name = attrName->LocalName(); + + // _Don't_ push the |id|, |ref|, or |persist| attribute's value! + if (!CanBroadcast(nameSpaceID, name)) continue; + + attributes.AppendElement( + nsAttrNameInfo(nameSpaceID, name, attrName->GetPrefix())); + } + + count = attributes.Length(); + while (count-- > 0) { + int32_t nameSpaceID = attributes[count].mNamespaceID; + nsAtom* name = attributes[count].mName; + nsAutoString value; + if (aBroadcaster->GetAttr(nameSpaceID, name, value)) { + aListener->SetAttr(nameSpaceID, name, attributes[count].mPrefix, value, + notify); + } + +#if 0 + // XXX we don't fire the |onbroadcast| handler during + // initial hookup: doing so would potentially run the + // |onbroadcast| handler before the |onload| handler, + // which could define JS properties that mask XBL + // properties, etc. + ExecuteOnBroadcastHandlerFor(aBroadcaster, aListener, name); +#endif + } + } else { + // Find out if the attribute is even present at all. + RefPtr name = NS_Atomize(aAttr); + + nsAutoString value; + if (aBroadcaster->GetAttr(kNameSpaceID_None, name, value)) { + aListener->SetAttr(kNameSpaceID_None, name, value, notify); + } else { + aListener->UnsetAttr(kNameSpaceID_None, name, notify); + } + +#if 0 + // XXX we don't fire the |onbroadcast| handler during initial + // hookup: doing so would potentially run the |onbroadcast| + // handler before the |onload| handler, which could define JS + // properties that mask XBL properties, etc. + ExecuteOnBroadcastHandlerFor(aBroadcaster, aListener, name); +#endif + } +} + +void XULBroadcastManager::AddListenerFor(Element& aBroadcaster, + Element& aListener, + const nsAString& aAttr, + ErrorResult& aRv) { + if (!mDocument) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + nsresult rv = nsContentUtils::CheckSameOrigin(mDocument, &aBroadcaster); + + if (NS_FAILED(rv)) { + aRv.Throw(rv); + return; + } + + rv = nsContentUtils::CheckSameOrigin(mDocument, &aListener); + + if (NS_FAILED(rv)) { + aRv.Throw(rv); + return; + } + + static const PLDHashTableOps gOps = { + PLDHashTable::HashVoidPtrKeyStub, PLDHashTable::MatchEntryStub, + PLDHashTable::MoveEntryStub, ClearBroadcasterMapEntry, nullptr}; + + if (!mBroadcasterMap) { + mBroadcasterMap = new PLDHashTable(&gOps, sizeof(BroadcasterMapEntry)); + } + + auto entry = + static_cast(mBroadcasterMap->Search(&aBroadcaster)); + if (!entry) { + entry = static_cast( + mBroadcasterMap->Add(&aBroadcaster, fallible)); + + if (!entry) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + + entry->mBroadcaster = &aBroadcaster; + + // N.B. placement new to construct the nsTArray object in-place + new (&entry->mListeners) nsTArray(); + } + + // Only add the listener if it's not there already! + RefPtr attr = NS_Atomize(aAttr); + + for (size_t i = entry->mListeners.Length() - 1; i != (size_t)-1; --i) { + BroadcastListener* bl = entry->mListeners[i]; + nsCOMPtr blListener = do_QueryReferent(bl->mListener); + + if (blListener == &aListener && bl->mAttribute == attr) return; + } + + BroadcastListener* bl = new BroadcastListener; + bl->mListener = do_GetWeakReference(&aListener); + bl->mAttribute = attr; + + entry->mListeners.AppendElement(bl); + + SynchronizeBroadcastListener(&aBroadcaster, &aListener, aAttr); +} + +void XULBroadcastManager::RemoveListenerFor(Element& aBroadcaster, + Element& aListener, + const nsAString& aAttr) { + // If we haven't added any broadcast listeners, then there sure + // aren't any to remove. + if (!mBroadcasterMap) return; + + auto entry = + static_cast(mBroadcasterMap->Search(&aBroadcaster)); + if (entry) { + RefPtr attr = NS_Atomize(aAttr); + for (size_t i = entry->mListeners.Length() - 1; i != (size_t)-1; --i) { + BroadcastListener* bl = entry->mListeners[i]; + nsCOMPtr blListener = do_QueryReferent(bl->mListener); + + if (blListener == &aListener && bl->mAttribute == attr) { + entry->mListeners.RemoveElementAt(i); + delete bl; + + if (entry->mListeners.IsEmpty()) mBroadcasterMap->RemoveEntry(entry); + + break; + } + } + } +} + +nsresult XULBroadcastManager::ExecuteOnBroadcastHandlerFor( + Element* aBroadcaster, Element* aListener, nsAtom* aAttr) { + if (!mDocument) { + return NS_OK; + } + // Now we execute the onchange handler in the context of the + // observer. We need to find the observer in order to + // execute the handler. + + for (nsCOMPtr child = aListener->GetFirstChild(); child; + child = child->GetNextSibling()) { + // Look for an element beneath the listener. This + // ought to have an |element| attribute that refers to + // aBroadcaster, and an |attribute| element that tells us what + // attriubtes we're listening for. + if (!child->IsXULElement(nsGkAtoms::observes)) continue; + + // Is this the element that was listening to us? + nsAutoString listeningToID; + child->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::element, + listeningToID); + + nsAutoString broadcasterID; + aBroadcaster->GetAttr(kNameSpaceID_None, nsGkAtoms::id, broadcasterID); + + if (listeningToID != broadcasterID) continue; + + // We are observing the broadcaster, but is this the right + // attribute? + nsAutoString listeningToAttribute; + child->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::attribute, + listeningToAttribute); + + if (!aAttr->Equals(listeningToAttribute) && + !listeningToAttribute.EqualsLiteral("*")) { + continue; + } + + // This is the right element. Execute the + // |onbroadcast| event handler + WidgetEvent event(true, eXULBroadcast); + + if (RefPtr presContext = mDocument->GetPresContext()) { + // Handle the DOM event + nsEventStatus status = nsEventStatus_eIgnore; + EventDispatcher::Dispatch(child, presContext, &event, nullptr, &status); + } + } + + return NS_OK; +} + +void XULBroadcastManager::AttributeChanged(Element* aElement, + int32_t aNameSpaceID, + nsAtom* aAttribute) { + if (!mDocument) { + return; + } + NS_ASSERTION(aElement->OwnerDoc() == mDocument, "unexpected doc"); + + // Synchronize broadcast listeners + if (mBroadcasterMap && CanBroadcast(aNameSpaceID, aAttribute)) { + auto entry = + static_cast(mBroadcasterMap->Search(aElement)); + + if (entry) { + // We've got listeners: push the value. + nsAutoString value; + bool attrSet = aElement->GetAttr(kNameSpaceID_None, aAttribute, value); + + for (size_t i = entry->mListeners.Length() - 1; i != (size_t)-1; --i) { + BroadcastListener* bl = entry->mListeners[i]; + if ((bl->mAttribute == aAttribute) || + (bl->mAttribute == nsGkAtoms::_asterisk)) { + nsCOMPtr listenerEl = do_QueryReferent(bl->mListener); + if (listenerEl) { + nsAutoString currentValue; + bool hasAttr = listenerEl->GetAttr(kNameSpaceID_None, aAttribute, + currentValue); + // We need to update listener only if we're + // (1) removing an existing attribute, + // (2) adding a new attribute or + // (3) changing the value of an attribute. + bool needsAttrChange = + attrSet != hasAttr || !value.Equals(currentValue); + nsDelayedBroadcastUpdate delayedUpdate(aElement, listenerEl, + aAttribute, value, attrSet, + needsAttrChange); + + size_t index = mDelayedAttrChangeBroadcasts.IndexOf( + delayedUpdate, 0, nsDelayedBroadcastUpdate::Comparator()); + if (index != mDelayedAttrChangeBroadcasts.NoIndex) { + if (mHandlingDelayedAttrChange) { + NS_WARNING("Broadcasting loop!"); + continue; + } + mDelayedAttrChangeBroadcasts.RemoveElementAt(index); + } + + mDelayedAttrChangeBroadcasts.AppendElement( + std::move(delayedUpdate)); + } + } + } + } + } +} + +void XULBroadcastManager::MaybeBroadcast() { + // Only broadcast when not in an update and when safe to run scripts. + if (mDocument && mDocument->UpdateNestingLevel() == 0 && + (mDelayedAttrChangeBroadcasts.Length() || + mDelayedBroadcasters.Length())) { + if (!nsContentUtils::IsSafeToRunScript()) { + if (mDocument) { + nsContentUtils::AddScriptRunner( + NewRunnableMethod("dom::XULBroadcastManager::MaybeBroadcast", this, + &XULBroadcastManager::MaybeBroadcast)); + } + return; + } + if (!mHandlingDelayedAttrChange) { + mHandlingDelayedAttrChange = true; + for (uint32_t i = 0; i < mDelayedAttrChangeBroadcasts.Length(); ++i) { + RefPtr attrName = mDelayedAttrChangeBroadcasts[i].mAttrName; + RefPtr listener = mDelayedAttrChangeBroadcasts[i].mListener; + if (mDelayedAttrChangeBroadcasts[i].mNeedsAttrChange) { + const nsString& value = mDelayedAttrChangeBroadcasts[i].mAttr; + if (mDelayedAttrChangeBroadcasts[i].mSetAttr) { + listener->SetAttr(kNameSpaceID_None, attrName, value, true); + } else { + listener->UnsetAttr(kNameSpaceID_None, attrName, true); + } + } + RefPtr broadcaster = + mDelayedAttrChangeBroadcasts[i].mBroadcaster; + ExecuteOnBroadcastHandlerFor(broadcaster, listener, attrName); + } + mDelayedAttrChangeBroadcasts.Clear(); + mHandlingDelayedAttrChange = false; + } + + uint32_t length = mDelayedBroadcasters.Length(); + if (length) { + bool oldValue = mHandlingDelayedBroadcasters; + mHandlingDelayedBroadcasters = true; + nsTArray delayedBroadcasters = + std::move(mDelayedBroadcasters); + for (uint32_t i = 0; i < length; ++i) { + SynchronizeBroadcastListener(delayedBroadcasters[i].mBroadcaster, + delayedBroadcasters[i].mListener, + delayedBroadcasters[i].mAttr); + } + mHandlingDelayedBroadcasters = oldValue; + } + } +} + +nsresult XULBroadcastManager::FindBroadcaster(Element* aElement, + Element** aListener, + nsString& aBroadcasterID, + nsString& aAttribute, + Element** aBroadcaster) { + NodeInfo* ni = aElement->NodeInfo(); + *aListener = nullptr; + *aBroadcaster = nullptr; + + if (ni->Equals(nsGkAtoms::observes, kNameSpaceID_XUL)) { + // It's an element, which means that the actual + // listener is the _parent_ node. This element should have an + // 'element' attribute that specifies the ID of the + // broadcaster element, and an 'attribute' element, which + // specifies the name of the attribute to observe. + nsIContent* parent = aElement->GetParent(); + if (!parent) { + // is the root element + return NS_FINDBROADCASTER_NOT_FOUND; + } + + *aListener = Element::FromNode(parent); + NS_IF_ADDREF(*aListener); + + aElement->GetAttr(kNameSpaceID_None, nsGkAtoms::element, aBroadcasterID); + if (aBroadcasterID.IsEmpty()) { + return NS_FINDBROADCASTER_NOT_FOUND; + } + aElement->GetAttr(kNameSpaceID_None, nsGkAtoms::attribute, aAttribute); + } else { + // It's a generic element, which means that we'll use the + // value of the 'observes' attribute to determine the ID of + // the broadcaster element, and we'll watch _all_ of its + // values. + aElement->GetAttr(kNameSpaceID_None, nsGkAtoms::observes, aBroadcasterID); + + // Bail if there's no aBroadcasterID + if (aBroadcasterID.IsEmpty()) { + // Try the command attribute next. + aElement->GetAttr(kNameSpaceID_None, nsGkAtoms::command, aBroadcasterID); + if (!aBroadcasterID.IsEmpty()) { + // We've got something in the command attribute. We + // only treat this as a normal broadcaster if we are + // not a menuitem or a key. + + if (ni->Equals(nsGkAtoms::menuitem, kNameSpaceID_XUL) || + ni->Equals(nsGkAtoms::key, kNameSpaceID_XUL)) { + return NS_FINDBROADCASTER_NOT_FOUND; + } + } else { + return NS_FINDBROADCASTER_NOT_FOUND; + } + } + + *aListener = aElement; + NS_ADDREF(*aListener); + + aAttribute.Assign('*'); + } + + // Make sure we got a valid listener. + NS_ENSURE_TRUE(*aListener, NS_ERROR_UNEXPECTED); + + // Try to find the broadcaster element in the document. + Document* doc = aElement->GetComposedDoc(); + if (doc) { + *aBroadcaster = doc->GetElementById(aBroadcasterID); + } + + // The broadcaster element is missing. + if (!*aBroadcaster) { + return NS_FINDBROADCASTER_NOT_FOUND; + } + + NS_ADDREF(*aBroadcaster); + + return NS_FINDBROADCASTER_FOUND; +} + +nsresult XULBroadcastManager::UpdateListenerHookup(Element* aElement, + HookupAction aAction) { + // Resolve a broadcaster hookup. Look at the element that we're + // trying to resolve: it could be an '' element, or just + // a vanilla element with an 'observes' attribute on it. + nsresult rv; + + nsCOMPtr listener; + nsAutoString broadcasterID; + nsAutoString attribute; + nsCOMPtr broadcaster; + + rv = FindBroadcaster(aElement, getter_AddRefs(listener), broadcasterID, + attribute, getter_AddRefs(broadcaster)); + switch (rv) { + case NS_FINDBROADCASTER_NOT_FOUND: + return NS_OK; + case NS_FINDBROADCASTER_FOUND: + break; + default: + return rv; + } + + NS_ENSURE_ARG(broadcaster && listener); + if (aAction == eHookupAdd) { + ErrorResult domRv; + AddListenerFor(*broadcaster, *listener, attribute, domRv); + if (domRv.Failed()) { + return domRv.StealNSResult(); + } + } else { + RemoveListenerFor(*broadcaster, *listener, attribute); + } + + // Tell the world we succeeded + if (MOZ_LOG_TEST(sXULBroadCastManager, LogLevel::Debug)) { + nsCOMPtr content = listener; + NS_ASSERTION(content != nullptr, "not an nsIContent"); + if (!content) { + return rv; + } + + nsAutoCString attributeC, broadcasteridC; + LossyCopyUTF16toASCII(attribute, attributeC); + LossyCopyUTF16toASCII(broadcasterID, broadcasteridC); + MOZ_LOG(sXULBroadCastManager, LogLevel::Debug, + ("xul: broadcaster hookup <%s attribute='%s'> to %s", + nsAtomCString(content->NodeInfo()->NameAtom()).get(), + attributeC.get(), broadcasteridC.get())); + } + + return NS_OK; +} + +nsresult XULBroadcastManager::AddListener(Element* aElement) { + return UpdateListenerHookup(aElement, eHookupAdd); +} + +nsresult XULBroadcastManager::RemoveListener(Element* aElement) { + return UpdateListenerHookup(aElement, eHookupRemove); +} + +} // namespace mozilla::dom diff --git a/dom/xul/XULBroadcastManager.h b/dom/xul/XULBroadcastManager.h new file mode 100644 index 0000000000..77fe6168eb --- /dev/null +++ b/dom/xul/XULBroadcastManager.h @@ -0,0 +1,91 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_dom_XULBroadcastManager_h +#define mozilla_dom_XULBroadcastManager_h + +#include "nsAtom.h" +#include "nsTArray.h" + +class PLDHashTable; +class nsXULElement; + +namespace mozilla { + +class ErrorResult; + +namespace dom { + +class Document; +class Element; + +class XULBroadcastManager final { + public: + explicit XULBroadcastManager(Document* aDocument); + + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(XULBroadcastManager) + + /** + * Checks whether an element uses any of the special broadcaster attributes + * or is an observes element. This mimics the logic in FindBroadcaster, but + * is intended to be a lighter weight check and doesn't actually guarantee + * that the element will need a listener. + */ + static bool MayNeedListener(const Element& aElement); + + nsresult AddListener(Element* aElement); + nsresult RemoveListener(Element* aElement); + void AttributeChanged(Element* aElement, int32_t aNameSpaceID, + nsAtom* aAttribute); + // TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230) + MOZ_CAN_RUN_SCRIPT_BOUNDARY void MaybeBroadcast(); + void DropDocumentReference(); // notification that doc is going away + protected: + enum HookupAction { eHookupAdd = 0, eHookupRemove }; + + nsresult UpdateListenerHookup(Element* aElement, HookupAction aAction); + + void RemoveListenerFor(Element& aBroadcaster, Element& aListener, + const nsAString& aAttr); + void AddListenerFor(Element& aBroadcaster, Element& aListener, + const nsAString& aAttr, ErrorResult& aRv); + + MOZ_CAN_RUN_SCRIPT nsresult ExecuteOnBroadcastHandlerFor( + Element* aBroadcaster, Element* aListener, nsAtom* aAttr); + // The out params of FindBroadcaster only have values that make sense when + // the method returns NS_FINDBROADCASTER_FOUND. In all other cases, the + // values of the out params should not be relied on (though *aListener and + // *aBroadcaster do need to be released if non-null, of course). + nsresult FindBroadcaster(Element* aElement, Element** aListener, + nsString& aBroadcasterID, nsString& aAttribute, + Element** aBroadcaster); + + void SynchronizeBroadcastListener(Element* aBroadcaster, Element* aListener, + const nsAString& aAttr); + + // This reference is nulled by the Document in it's destructor through + // DropDocumentReference(). + Document* MOZ_NON_OWNING_REF mDocument; + + /** + * A map from a broadcaster element to a list of listener elements. + */ + PLDHashTable* mBroadcasterMap; + + class nsDelayedBroadcastUpdate; + nsTArray mDelayedBroadcasters; + nsTArray mDelayedAttrChangeBroadcasts; + bool mHandlingDelayedAttrChange; + bool mHandlingDelayedBroadcasters; + + private: + ~XULBroadcastManager(); +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_XULBroadcastManager_h diff --git a/dom/xul/XULButtonElement.cpp b/dom/xul/XULButtonElement.cpp new file mode 100644 index 0000000000..2fc0690945 --- /dev/null +++ b/dom/xul/XULButtonElement.cpp @@ -0,0 +1,802 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 "XULButtonElement.h" +#include "XULMenuParentElement.h" +#include "XULPopupElement.h" +#include "mozilla/Assertions.h" +#include "mozilla/Attributes.h" +#include "mozilla/EventDispatcher.h" +#include "mozilla/EventStateManager.h" +#include "mozilla/LookAndFeel.h" +#include "mozilla/TextEvents.h" +#include "mozilla/TimeStamp.h" +#include "mozilla/dom/DocumentInlines.h" +#include "mozilla/dom/MouseEventBinding.h" +#include "mozilla/dom/NameSpaceConstants.h" +#include "mozilla/dom/AncestorIterator.h" +#include "mozilla/dom/XULMenuBarElement.h" +#include "nsGkAtoms.h" +#include "nsITimer.h" +#include "nsLayoutUtils.h" +#include "nsCaseTreatment.h" +#include "nsChangeHint.h" +#include "nsMenuPopupFrame.h" +#include "nsPlaceholderFrame.h" +#include "nsPresContext.h" +#include "nsXULPopupManager.h" +#include "nsIDOMXULButtonElement.h" +#include "nsISound.h" + +namespace mozilla::dom { + +XULButtonElement::XULButtonElement( + already_AddRefed&& aNodeInfo) + : nsXULElement(std::move(aNodeInfo)), + mIsAlwaysMenu(IsAnyOfXULElements(nsGkAtoms::menu, nsGkAtoms::menulist, + nsGkAtoms::menuitem)) {} + +XULButtonElement::~XULButtonElement() { + StopBlinking(); + KillMenuOpenTimer(); +} + +nsChangeHint XULButtonElement::GetAttributeChangeHint(const nsAtom* aAttribute, + int32_t aModType) const { + if (aAttribute == nsGkAtoms::type && + IsAnyOfXULElements(nsGkAtoms::button, nsGkAtoms::toolbarbutton)) { + // type=menu switches to a menu frame. + return nsChangeHint_ReconstructFrame; + } + return nsXULElement::GetAttributeChangeHint(aAttribute, aModType); +} + +// This global flag is used to record the timestamp when a menu was opened or +// closed and is used to ignore the mousemove and mouseup events that would fire +// on the menu after the mousedown occurred. +static TimeStamp gMenuJustOpenedOrClosedTime = TimeStamp(); + +void XULButtonElement::PopupOpened() { + if (!IsMenu()) { + return; + } + gMenuJustOpenedOrClosedTime = TimeStamp::Now(); + SetAttr(kNameSpaceID_None, nsGkAtoms::open, u"true"_ns, true); +} + +void XULButtonElement::PopupClosed(bool aDeselectMenu) { + if (!IsMenu()) { + return; + } + nsContentUtils::AddScriptRunner( + new nsUnsetAttrRunnable(this, nsGkAtoms::open)); + + if (aDeselectMenu) { + if (RefPtr parent = GetMenuParent()) { + if (parent->GetActiveMenuChild() == this) { + parent->SetActiveMenuChild(nullptr); + } + } + } +} + +bool XULButtonElement::IsMenuActive() const { + if (XULMenuParentElement* menu = GetMenuParent()) { + return menu->GetActiveMenuChild() == this; + } + return false; +} + +void XULButtonElement::HandleEnterKeyPress(WidgetEvent& aEvent) { + if (IsDisabled()) { +#ifdef XP_WIN + if (XULPopupElement* popup = GetContainingPopupElement()) { + if (nsXULPopupManager* pm = nsXULPopupManager::GetInstance()) { + pm->HidePopup( + popup, {HidePopupOption::HideChain, HidePopupOption::DeselectMenu, + HidePopupOption::Async}); + } + } +#endif + return; + } + if (IsMenuPopupOpen()) { + return; + } + // The enter key press applies to us. + if (IsMenuItem()) { + ExecuteMenu(aEvent); + } else { + OpenMenuPopup(true); + } +} + +bool XULButtonElement::IsMenuPopupOpen() { + nsMenuPopupFrame* popupFrame = GetMenuPopup(FlushType::None); + return popupFrame && popupFrame->IsOpen(); +} + +bool XULButtonElement::IsOnMenu() const { + auto* popup = XULPopupElement::FromNodeOrNull(GetMenuParent()); + return popup && popup->IsMenu(); +} + +bool XULButtonElement::IsOnMenuList() const { + if (XULMenuParentElement* menu = GetMenuParent()) { + return menu->GetParent() && + menu->GetParent()->IsXULElement(nsGkAtoms::menulist); + } + return false; +} + +bool XULButtonElement::IsOnMenuBar() const { + if (XULMenuParentElement* menu = GetMenuParent()) { + return menu->IsMenuBar(); + } + return false; +} + +nsMenuPopupFrame* XULButtonElement::GetContainingPopupWithoutFlushing() const { + if (XULPopupElement* popup = GetContainingPopupElement()) { + return do_QueryFrame(popup->GetPrimaryFrame()); + } + return nullptr; +} + +XULPopupElement* XULButtonElement::GetContainingPopupElement() const { + return XULPopupElement::FromNodeOrNull(GetMenuParent()); +} + +bool XULButtonElement::IsOnContextMenu() const { + if (nsMenuPopupFrame* popup = GetContainingPopupWithoutFlushing()) { + return popup->IsContextMenu(); + } + return false; +} + +void XULButtonElement::ToggleMenuState() { + if (IsMenuPopupOpen()) { + CloseMenuPopup(false); + } else { + OpenMenuPopup(false); + } +} + +void XULButtonElement::KillMenuOpenTimer() { + if (mMenuOpenTimer) { + mMenuOpenTimer->Cancel(); + mMenuOpenTimer = nullptr; + } +} + +void XULButtonElement::OpenMenuPopup(bool aSelectFirstItem) { + nsXULPopupManager* pm = nsXULPopupManager::GetInstance(); + if (!pm) { + return; + } + + pm->KillMenuTimer(); + if (!pm->MayShowMenu(this)) { + return; + } + + if (RefPtr parent = GetMenuParent()) { + parent->SetActiveMenuChild(this); + } + + // Open the menu asynchronously. + OwnerDoc()->Dispatch( + TaskCategory::Other, + NS_NewRunnableFunction( + "AsyncOpenMenu", [self = RefPtr{this}, aSelectFirstItem] { + if (self->GetMenuParent() && !self->IsMenuActive()) { + return; + } + if (nsXULPopupManager* pm = nsXULPopupManager::GetInstance()) { + pm->ShowMenu(self, aSelectFirstItem); + } + })); +} + +void XULButtonElement::CloseMenuPopup(bool aDeselectMenu) { + gMenuJustOpenedOrClosedTime = TimeStamp::Now(); + // Close the menu asynchronously + nsXULPopupManager* pm = nsXULPopupManager::GetInstance(); + if (!pm) { + return; + } + if (auto* popup = GetMenuPopupContent()) { + HidePopupOptions options{HidePopupOption::Async}; + if (aDeselectMenu) { + options += HidePopupOption::DeselectMenu; + } + pm->HidePopup(popup, options); + } +} + +int32_t XULButtonElement::MenuOpenCloseDelay() const { + if (IsOnMenuBar()) { + return 0; + } + return LookAndFeel::GetInt(LookAndFeel::IntID::SubmenuDelay, 300); // ms +} + +void XULButtonElement::ExecuteMenu(Modifiers aModifiers, int16_t aButton, + bool aIsTrusted) { + MOZ_ASSERT(IsMenu()); + + StopBlinking(); + + auto menuType = GetMenuType(); + if (NS_WARN_IF(!menuType)) { + return; + } + + // Because the command event is firing asynchronously, a flag is needed to + // indicate whether user input is being handled. This ensures that a popup + // window won't get blocked. + const bool userinput = dom::UserActivation::IsHandlingUserInput(); + + // Flip "checked" state if we're a checkbox menu, or an un-checked radio menu. + bool needToFlipChecked = false; + if (*menuType == MenuType::Checkbox || + (*menuType == MenuType::Radio && !GetXULBoolAttr(nsGkAtoms::checked))) { + needToFlipChecked = !AttrValueIs(kNameSpaceID_None, nsGkAtoms::autocheck, + nsGkAtoms::_false, eCaseMatters); + } + + mDelayedMenuCommandEvent = new nsXULMenuCommandEvent( + this, aIsTrusted, aModifiers, userinput, needToFlipChecked, aButton); + StartBlinking(); +} + +void XULButtonElement::StopBlinking() { + if (mMenuBlinkTimer) { + if (auto* parent = GetMenuParent()) { + parent->LockMenuUntilClosed(false); + } + mMenuBlinkTimer->Cancel(); + mMenuBlinkTimer = nullptr; + } + mDelayedMenuCommandEvent = nullptr; +} + +void XULButtonElement::PassMenuCommandEventToPopupManager() { + if (mDelayedMenuCommandEvent) { + if (RefPtr pm = nsXULPopupManager::GetInstance()) { + RefPtr event = std::move(mDelayedMenuCommandEvent); + nsCOMPtr content = this; + pm->ExecuteMenu(content, event); + } + } + mDelayedMenuCommandEvent = nullptr; +} + +static constexpr int32_t kBlinkDelay = 67; // milliseconds + +void XULButtonElement::StartBlinking() { + if (!LookAndFeel::GetInt(LookAndFeel::IntID::ChosenMenuItemsShouldBlink)) { + PassMenuCommandEventToPopupManager(); + return; + } + + // Blink off. + UnsetAttr(kNameSpaceID_None, nsGkAtoms::menuactive, true); + if (auto* parent = GetMenuParent()) { + // Make this menu ignore events from now on. + parent->LockMenuUntilClosed(true); + } + + // Set up a timer to blink back on. + NS_NewTimerWithFuncCallback( + getter_AddRefs(mMenuBlinkTimer), + [](nsITimer*, void* aClosure) MOZ_CAN_RUN_SCRIPT_BOUNDARY { + RefPtr self = static_cast(aClosure); + if (auto* parent = self->GetMenuParent()) { + if (parent->GetActiveMenuChild() == self) { + // Restore the highlighting if we're still the active item. + self->SetAttr(kNameSpaceID_None, nsGkAtoms::menuactive, u"true"_ns, + true); + } + } + // Reuse our timer to actually execute. + self->mMenuBlinkTimer->InitWithNamedFuncCallback( + [](nsITimer*, void* aClosure) MOZ_CAN_RUN_SCRIPT_BOUNDARY { + RefPtr self = static_cast(aClosure); + if (auto* parent = self->GetMenuParent()) { + parent->LockMenuUntilClosed(false); + } + self->PassMenuCommandEventToPopupManager(); + self->StopBlinking(); + }, + aClosure, kBlinkDelay, nsITimer::TYPE_ONE_SHOT, + "XULButtonElement::ContinueBlinking"); + }, + this, kBlinkDelay, nsITimer::TYPE_ONE_SHOT, + "XULButtonElement::StartBlinking", + OwnerDoc()->EventTargetFor(TaskCategory::Other)); +} + +void XULButtonElement::UnbindFromTree(bool aNullParent) { + StopBlinking(); + nsXULElement::UnbindFromTree(aNullParent); +} + +void XULButtonElement::ExecuteMenu(WidgetEvent& aEvent) { + MOZ_ASSERT(IsMenu()); + if (nsCOMPtr sound = do_GetService("@mozilla.org/sound;1")) { + sound->PlayEventSound(nsISound::EVENT_MENU_EXECUTE); + } + + Modifiers modifiers = 0; + if (WidgetInputEvent* inputEvent = aEvent.AsInputEvent()) { + modifiers = inputEvent->mModifiers; + } + + int16_t button = 0; + if (WidgetMouseEventBase* mouseEvent = aEvent.AsMouseEventBase()) { + button = mouseEvent->mButton; + } + + ExecuteMenu(modifiers, button, aEvent.IsTrusted()); +} + +void XULButtonElement::PostHandleEventForMenus( + EventChainPostVisitor& aVisitor) { + auto* event = aVisitor.mEvent; + + if (event->mOriginalTarget != this) { + return; + } + + if (auto* parent = GetMenuParent()) { + if (NS_WARN_IF(parent->IsLocked())) { + return; + } + } + + // If a menu just opened, ignore the mouseup event that might occur after a + // the mousedown event that opened it. However, if a different mousedown event + // occurs, just clear this flag. + if (!gMenuJustOpenedOrClosedTime.IsNull()) { + if (event->mMessage == eMouseDown) { + gMenuJustOpenedOrClosedTime = TimeStamp(); + } else if (event->mMessage == eMouseUp) { + return; + } + } + + if (event->mMessage == eKeyPress && !IsDisabled()) { + WidgetKeyboardEvent* keyEvent = event->AsKeyboardEvent(); + uint32_t keyCode = keyEvent->mKeyCode; +#ifdef XP_MACOSX + // On mac, open menulist on either up/down arrow or space (w/o Cmd pressed) + if (!IsMenuPopupOpen() && + ((keyEvent->mCharCode == ' ' && !keyEvent->IsMeta()) || + (keyCode == NS_VK_UP || keyCode == NS_VK_DOWN))) { + // When pressing space, don't open the menu if performing an incremental + // search. + if (keyEvent->mCharCode != ' ' || + !nsMenuPopupFrame::IsWithinIncrementalTime(keyEvent->mTimeStamp)) { + aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault; + OpenMenuPopup(false); + } + } +#else + // On other platforms, toggle menulist on unmodified F4 or Alt arrow + if ((keyCode == NS_VK_F4 && !keyEvent->IsAlt()) || + ((keyCode == NS_VK_UP || keyCode == NS_VK_DOWN) && keyEvent->IsAlt())) { + aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault; + ToggleMenuState(); + } +#endif + } else if (event->mMessage == eMouseDown && + event->AsMouseEvent()->mButton == MouseButton::ePrimary && +#ifdef XP_MACOSX + // On mac, ctrl-click will send a context menu event from the + // widget, so we don't want to bring up the menu. + !event->AsMouseEvent()->IsControl() && +#endif + !IsDisabled() && !IsMenuItem()) { + // The menu item was selected. Bring up the menu. + // We have children. + // Don't prevent the default action here, since that will also cancel + // potential drag starts. + if (!IsOnMenu()) { + ToggleMenuState(); + } else if (!IsMenuPopupOpen()) { + OpenMenuPopup(false); + } + } else if (event->mMessage == eMouseUp && IsMenuItem() && !IsDisabled() && + !event->mFlags.mMultipleActionsPrevented) { + // We accept left and middle clicks on all menu items to activate the item. + // On context menus we also accept right click to activate the item, because + // right-clicking on an item in a context menu cannot open another context + // menu. + bool isMacCtrlClick = false; +#ifdef XP_MACOSX + isMacCtrlClick = event->AsMouseEvent()->mButton == MouseButton::ePrimary && + event->AsMouseEvent()->IsControl(); +#endif + bool clickMightOpenContextMenu = + event->AsMouseEvent()->mButton == MouseButton::eSecondary || + isMacCtrlClick; + if (!clickMightOpenContextMenu || IsOnContextMenu()) { + aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault; + ExecuteMenu(*event); + } + } else if (event->mMessage == eContextMenu && IsOnContextMenu() && + !IsMenuItem() && !IsDisabled()) { + // Make sure we cancel default processing of the context menu event so + // that it doesn't bubble and get seen again by the popuplistener and show + // another context menu. + aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault; + } else if (event->mMessage == eMouseOut) { + KillMenuOpenTimer(); + if (RefPtr parent = GetMenuParent()) { + if (parent->GetActiveMenuChild() == this) { + // Deactivate the menu on mouse out in some cases... + const bool shouldDeactivate = [&] { + if (IsMenuPopupOpen()) { + // If we're open we never deselect. PopupClosed will do as needed. + return false; + } + if (auto* menubar = XULMenuBarElement::FromNode(*parent)) { + // De-select when exiting a menubar item, if the menubar wasn't + // activated by keyboard. + return !menubar->IsActiveByKeyboard(); + } + if (IsOnMenuList()) { + // Don't de-select if on a menu-list. That matches Chromium and our + // historical Windows behavior, see bug 1197913. + return false; + } + // De-select elsewhere. + return true; + }(); + + if (shouldDeactivate) { + parent->SetActiveMenuChild(nullptr); + } + } + } + } else if (event->mMessage == eMouseMove && (IsOnMenu() || IsOnMenuBar())) { + // Use a tolerance to address situations where a user might perform a + // "wiggly" click that is accompanied by near-simultaneous mousemove events. + const TimeDuration kTolerance = TimeDuration::FromMilliseconds(200); + if (!gMenuJustOpenedOrClosedTime.IsNull() && + gMenuJustOpenedOrClosedTime + kTolerance < TimeStamp::Now()) { + gMenuJustOpenedOrClosedTime = TimeStamp(); + return; + } + + if (IsDisabled() && IsOnMenuList()) { + return; + } + + RefPtr parent = GetMenuParent(); + MOZ_ASSERT(parent, "How did IsOnMenu{,Bar} return true then?"); + + const bool isOnOpenMenubar = + parent->IsMenuBar() && parent->GetActiveMenuChild() && + parent->GetActiveMenuChild()->IsMenuPopupOpen(); + + parent->SetActiveMenuChild(this); + + // We need to check if we really became the current menu item or not. + if (!IsMenuActive()) { + // We didn't (presumably because a context menu was active) + return; + } + if (IsDisabled() || IsMenuItem() || IsMenuPopupOpen() || mMenuOpenTimer) { + // Disabled, or already opening or what not. + return; + } + + if (parent->IsMenuBar() && !isOnOpenMenubar) { + // We should only open on hover in the menubar iff the menubar is open + // already. + return; + } + + // A timer is used so that it doesn't open if the user moves the mouse + // quickly past the menu. The MenuOpenCloseDelay ensures that only menus + // have this behaviour. + NS_NewTimerWithFuncCallback( + getter_AddRefs(mMenuOpenTimer), + [](nsITimer*, void* aClosure) MOZ_CAN_RUN_SCRIPT_BOUNDARY { + RefPtr self = static_cast(aClosure); + self->mMenuOpenTimer = nullptr; + if (self->IsMenuPopupOpen()) { + return; + } + // make sure we didn't open a context menu in the meantime + // (i.e. the user right-clicked while hovering over a submenu). + nsXULPopupManager* pm = nsXULPopupManager::GetInstance(); + if (!pm) { + return; + } + if (pm->HasContextMenu(nullptr) && !self->IsOnContextMenu()) { + return; + } + if (!self->IsMenuActive()) { + return; + } + self->OpenMenuPopup(false); + }, + this, MenuOpenCloseDelay(), nsITimer::TYPE_ONE_SHOT, + "XULButtonElement::OpenMenu", + OwnerDoc()->EventTargetFor(TaskCategory::Other)); + } +} + +nsresult XULButtonElement::PostHandleEvent(EventChainPostVisitor& aVisitor) { + if (aVisitor.mEventStatus == nsEventStatus_eConsumeNoDefault) { + return nsXULElement::PostHandleEvent(aVisitor); + } + + if (IsMenu()) { + PostHandleEventForMenus(aVisitor); + return nsXULElement::PostHandleEvent(aVisitor); + } + + auto* event = aVisitor.mEvent; + switch (event->mMessage) { + case eBlur: { + Blurred(); + break; + } + case eKeyDown: { + WidgetKeyboardEvent* keyEvent = event->AsKeyboardEvent(); + if (!keyEvent) { + break; + } + if (keyEvent->ShouldWorkAsSpaceKey() && aVisitor.mPresContext) { + EventStateManager* esm = aVisitor.mPresContext->EventStateManager(); + // :hover:active state + esm->SetContentState(this, ElementState::HOVER); + esm->SetContentState(this, ElementState::ACTIVE); + mIsHandlingKeyEvent = true; + } + break; + } + +// On mac, Return fires the default button, not the focused one. +#ifndef XP_MACOSX + case eKeyPress: { + WidgetKeyboardEvent* keyEvent = event->AsKeyboardEvent(); + if (!keyEvent) { + break; + } + if (NS_VK_RETURN == keyEvent->mKeyCode) { + if (RefPtr button = AsXULButton()) { + if (MouseClicked(*keyEvent)) { + aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault; + } + } + } + break; + } +#endif + + case eKeyUp: { + WidgetKeyboardEvent* keyEvent = event->AsKeyboardEvent(); + if (!keyEvent) { + break; + } + if (keyEvent->ShouldWorkAsSpaceKey()) { + mIsHandlingKeyEvent = false; + ElementState buttonState = State(); + if (buttonState.HasAllStates(ElementState::ACTIVE | + ElementState::HOVER) && + aVisitor.mPresContext) { + // return to normal state + EventStateManager* esm = aVisitor.mPresContext->EventStateManager(); + esm->SetContentState(nullptr, ElementState::ACTIVE); + esm->SetContentState(nullptr, ElementState::HOVER); + if (MouseClicked(*keyEvent)) { + aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault; + } + } + } + break; + } + + case eMouseClick: { + WidgetMouseEvent* mouseEvent = event->AsMouseEvent(); + if (mouseEvent->IsLeftClickEvent()) { + if (MouseClicked(*mouseEvent)) { + aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault; + } + } + break; + } + + default: + break; + } + + return nsXULElement::PostHandleEvent(aVisitor); +} + +void XULButtonElement::Blurred() { + ElementState buttonState = State(); + if (mIsHandlingKeyEvent && + buttonState.HasAllStates(ElementState::ACTIVE | ElementState::HOVER)) { + // Return to normal state + if (nsPresContext* pc = OwnerDoc()->GetPresContext()) { + EventStateManager* esm = pc->EventStateManager(); + esm->SetContentState(nullptr, ElementState::ACTIVE); + esm->SetContentState(nullptr, ElementState::HOVER); + } + } + mIsHandlingKeyEvent = false; +} + +bool XULButtonElement::MouseClicked(WidgetGUIEvent& aEvent) { + // Don't execute if we're disabled. + if (IsDisabled() || !IsInComposedDoc()) { + return false; + } + + // Have the content handle the event, propagating it according to normal DOM + // rules. + RefPtr presShell = OwnerDoc()->GetPresShell(); + if (!presShell) { + return false; + } + + // Execute the oncommand event handler. + WidgetInputEvent* inputEvent = aEvent.AsInputEvent(); + WidgetMouseEventBase* mouseEvent = aEvent.AsMouseEventBase(); + WidgetKeyboardEvent* keyEvent = aEvent.AsKeyboardEvent(); + // TODO: Set aSourceEvent? + nsContentUtils::DispatchXULCommand( + this, aEvent.IsTrusted(), /* aSourceEvent = */ nullptr, presShell, + inputEvent->IsControl(), inputEvent->IsAlt(), inputEvent->IsShift(), + inputEvent->IsMeta(), + mouseEvent ? mouseEvent->mInputSource + : (keyEvent ? MouseEvent_Binding::MOZ_SOURCE_KEYBOARD + : MouseEvent_Binding::MOZ_SOURCE_UNKNOWN), + mouseEvent ? mouseEvent->mButton : 0); + return true; +} + +bool XULButtonElement::IsMenu() const { + if (mIsAlwaysMenu) { + return true; + } + return IsAnyOfXULElements(nsGkAtoms::button, nsGkAtoms::toolbarbutton) && + AttrValueIs(kNameSpaceID_None, nsGkAtoms::type, nsGkAtoms::menu, + eCaseMatters); +} + +void XULButtonElement::UncheckRadioSiblings() { + MOZ_ASSERT(!nsContentUtils::IsSafeToRunScript()); + MOZ_ASSERT(GetMenuType() == Some(MenuType::Radio)); + nsAutoString groupName; + GetAttr(nsGkAtoms::name, groupName); + + nsIContent* parent = GetParent(); + if (!parent) { + return; + } + + auto ShouldUncheck = [&](const nsIContent& aSibling) { + const auto* button = XULButtonElement::FromNode(aSibling); + if (!button || button->GetMenuType() != Some(MenuType::Radio)) { + return false; + } + if (const auto* attr = button->GetParsedAttr(nsGkAtoms::name)) { + if (!attr->Equals(groupName, eCaseMatters)) { + return false; + } + } else if (!groupName.IsEmpty()) { + return false; + } + // we're in the same group, only uncheck if we're checked (for some reason, + // some tests rely on that specifically). + return button->GetXULBoolAttr(nsGkAtoms::checked); + }; + + for (nsIContent* child = parent->GetFirstChild(); child; + child = child->GetNextSibling()) { + if (child == this || !ShouldUncheck(*child)) { + continue; + } + child->AsElement()->UnsetAttr(nsGkAtoms::checked, IgnoreErrors()); + } +} + +void XULButtonElement::AfterSetAttr(int32_t aNamespaceID, nsAtom* aName, + const nsAttrValue* aValue, + const nsAttrValue* aOldValue, + nsIPrincipal* aSubjectPrincipal, + bool aNotify) { + nsXULElement::AfterSetAttr(aNamespaceID, aName, aValue, aOldValue, + aSubjectPrincipal, aNotify); + if (IsAlwaysMenu() && aNamespaceID == kNameSpaceID_None) { + // We need to uncheck radio siblings when we're a checked radio and switch + // groups, or become checked. + const bool shouldUncheckSiblings = [&] { + if (aName == nsGkAtoms::type || aName == nsGkAtoms::name) { + return *GetMenuType() == MenuType::Radio && + GetXULBoolAttr(nsGkAtoms::checked); + } + if (aName == nsGkAtoms::checked && aValue && + aValue->Equals(nsGkAtoms::_true, eCaseMatters)) { + return *GetMenuType() == MenuType::Radio; + } + return false; + }(); + if (shouldUncheckSiblings) { + UncheckRadioSiblings(); + } + } +} + +auto XULButtonElement::GetMenuType() const -> Maybe { + if (!IsAlwaysMenu()) { + return Nothing(); + } + + static Element::AttrValuesArray values[] = {nsGkAtoms::checkbox, + nsGkAtoms::radio, nullptr}; + switch (FindAttrValueIn(kNameSpaceID_None, nsGkAtoms::type, values, + eCaseMatters)) { + case 0: + return Some(MenuType::Checkbox); + case 1: + return Some(MenuType::Radio); + default: + return Some(MenuType::Normal); + } +} + +XULMenuBarElement* XULButtonElement::GetMenuBar() const { + if (!IsMenu()) { + return nullptr; + } + return FirstAncestorOfType(); +} + +XULMenuParentElement* XULButtonElement::GetMenuParent() const { + if (IsXULElement(nsGkAtoms::menulist)) { + return nullptr; + } + return FirstAncestorOfType(); +} + +XULPopupElement* XULButtonElement::GetMenuPopupContent() const { + if (!IsMenu()) { + return nullptr; + } + for (auto* child = GetFirstChild(); child; child = child->GetNextSibling()) { + if (auto* popup = XULPopupElement::FromNode(child)) { + return popup; + } + } + return nullptr; +} + +nsMenuPopupFrame* XULButtonElement::GetMenuPopupWithoutFlushing() const { + return const_cast(this)->GetMenuPopup(FlushType::None); +} + +nsMenuPopupFrame* XULButtonElement::GetMenuPopup(FlushType aFlushType) { + RefPtr popup = GetMenuPopupContent(); + if (!popup) { + return nullptr; + } + return do_QueryFrame(popup->GetPrimaryFrame(aFlushType)); +} + +bool XULButtonElement::OpenedWithKey() const { + auto* menubar = GetMenuBar(); + return menubar && menubar->IsActiveByKeyboard(); +} + +} // namespace mozilla::dom diff --git a/dom/xul/XULButtonElement.h b/dom/xul/XULButtonElement.h new file mode 100644 index 0000000000..fc880210a2 --- /dev/null +++ b/dom/xul/XULButtonElement.h @@ -0,0 +1,123 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef dom_xul_XULButtonElement_h__ +#define dom_xul_XULButtonElement_h__ + +#include "mozilla/Attributes.h" +#include "nsINode.h" +#include "nsXULElement.h" + +class nsMenuBarFrame; +class nsMenuPopupFrame; +class nsXULMenuCommandEvent; + +namespace mozilla::dom { + +class KeyboardEvent; +class XULPopupElement; +class XULMenuBarElement; +class XULMenuParentElement; + +class XULButtonElement : public nsXULElement { + public: + explicit XULButtonElement( + already_AddRefed&& aNodeInfo); + + ~XULButtonElement() override; + + MOZ_CAN_RUN_SCRIPT_BOUNDARY bool MouseClicked(WidgetGUIEvent&); + MOZ_CAN_RUN_SCRIPT nsresult PostHandleEvent(EventChainPostVisitor&) override; + MOZ_CAN_RUN_SCRIPT void PostHandleEventForMenus(EventChainPostVisitor&); + MOZ_CAN_RUN_SCRIPT void HandleEnterKeyPress(WidgetEvent&); + + void PopupOpened(); + MOZ_CAN_RUN_SCRIPT void PopupClosed(bool aDeselectMenu); + + XULPopupElement* GetContainingPopupElement() const; + nsMenuPopupFrame* GetContainingPopupWithoutFlushing() const; + MOZ_CAN_RUN_SCRIPT void ToggleMenuState(); + bool IsMenuPopupOpen(); + + bool IsMenuItem() const { return NodeInfo()->Equals(nsGkAtoms::menuitem); } + bool IsMenuList() const { return NodeInfo()->Equals(nsGkAtoms::menulist); } + bool IsMenuActive() const; + MOZ_CAN_RUN_SCRIPT void OpenMenuPopup(bool aSelectFirstItem); + void CloseMenuPopup(bool aDeselectMenu); + + bool IsOnMenu() const; + bool IsOnMenuList() const; + bool IsOnMenuBar() const; + bool IsOnContextMenu() const; + + XULMenuParentElement* GetMenuParent() const; + + void UnbindFromTree(bool aNullParent) override; + + MOZ_CAN_RUN_SCRIPT bool HandleKeyPress(KeyboardEvent& keyEvent); + bool OpenedWithKey() const; + // Called to execute our command handler. + MOZ_CAN_RUN_SCRIPT void ExecuteMenu(WidgetEvent&); + MOZ_CAN_RUN_SCRIPT void ExecuteMenu(Modifiers, int16_t aButton, + bool aIsTrusted); + + // Whether we are a menu/menulist/menuitem element. + bool IsAlwaysMenu() const { return mIsAlwaysMenu; } + // Whether we should behave like a menu. This is the above plus buttons with + // type=menu attribute. + bool IsMenu() const; + + nsChangeHint GetAttributeChangeHint(const nsAtom* aAttribute, + int32_t aModType) const override; + void AfterSetAttr(int32_t aNamespaceID, nsAtom* aName, + const nsAttrValue* aValue, const nsAttrValue* aOldValue, + nsIPrincipal* aSubjectPrincipal, bool aNotify) override; + + NS_IMPL_FROMNODE_HELPER(XULButtonElement, + IsAnyOfXULElements(nsGkAtoms::checkbox, + nsGkAtoms::radio, nsGkAtoms::thumb, + nsGkAtoms::button, nsGkAtoms::menu, + nsGkAtoms::menulist, + nsGkAtoms::menuitem, + nsGkAtoms::toolbarbutton, + nsGkAtoms::toolbarpaletteitem, + nsGkAtoms::scrollbarbutton)) + + nsMenuPopupFrame* GetMenuPopup(FlushType aFlushType); + nsMenuPopupFrame* GetMenuPopupWithoutFlushing() const; + XULPopupElement* GetMenuPopupContent() const; + int32_t MenuOpenCloseDelay() const; + + bool IsDisabled() const { return GetXULBoolAttr(nsGkAtoms::disabled); } + + private: + XULMenuBarElement* GetMenuBar() const; + void Blurred(); + enum class MenuType { + Checkbox, + Radio, + Normal, + }; + Maybe GetMenuType() const; + + void UncheckRadioSiblings(); + void StopBlinking(); + MOZ_CAN_RUN_SCRIPT void StartBlinking(); + void KillMenuOpenTimer(); + MOZ_CAN_RUN_SCRIPT void PassMenuCommandEventToPopupManager(); + + bool mIsHandlingKeyEvent = false; + + // Whether this is a XULMenuElement. + const bool mIsAlwaysMenu; + RefPtr mDelayedMenuCommandEvent; + nsCOMPtr mMenuOpenTimer; + nsCOMPtr mMenuBlinkTimer; +}; + +} // namespace mozilla::dom + +#endif diff --git a/dom/xul/XULFrameElement.cpp b/dom/xul/XULFrameElement.cpp new file mode 100644 index 0000000000..efbeb15829 --- /dev/null +++ b/dom/xul/XULFrameElement.cpp @@ -0,0 +1,200 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 "nsCOMPtr.h" +#include "nsIBrowser.h" +#include "nsIContent.h" +#include "nsIOpenWindowInfo.h" +#include "nsFrameLoader.h" +#include "mozilla/AsyncEventDispatcher.h" +#include "mozilla/dom/HTMLIFrameElement.h" +#include "mozilla/dom/WindowProxyHolder.h" +#include "mozilla/dom/XULFrameElement.h" +#include "mozilla/dom/XULFrameElementBinding.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_CLASS(XULFrameElement) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(XULFrameElement, nsXULElement) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mFrameLoader) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOpenWindowInfo) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(XULFrameElement, nsXULElement) + if (tmp->mFrameLoader) { + tmp->mFrameLoader->Destroy(); + } + NS_IMPL_CYCLE_COLLECTION_UNLINK(mOpenWindowInfo) +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED(XULFrameElement, nsXULElement, + nsFrameLoaderOwner) + +JSObject* XULFrameElement::WrapNode(JSContext* aCx, + JS::Handle aGivenProto) { + return XULFrameElement_Binding::Wrap(aCx, this, aGivenProto); +} + +nsDocShell* XULFrameElement::GetDocShell() { + RefPtr frameLoader = GetFrameLoader(); + return frameLoader ? frameLoader->GetDocShell(IgnoreErrors()) : nullptr; +} + +already_AddRefed XULFrameElement::GetWebNavigation() { + nsCOMPtr docShell = GetDocShell(); + nsCOMPtr webnav = do_QueryInterface(docShell); + return webnav.forget(); +} + +Nullable XULFrameElement::GetContentWindow() { + RefPtr docShell = GetDocShell(); + if (docShell) { + return docShell->GetWindowProxy(); + } + + return nullptr; +} + +Document* XULFrameElement::GetContentDocument() { + nsCOMPtr docShell = GetDocShell(); + if (docShell) { + nsCOMPtr win = docShell->GetWindow(); + if (win) { + return win->GetDoc(); + } + } + return nullptr; +} + +uint64_t XULFrameElement::BrowserId() { + if (mFrameLoader) { + if (auto* bc = mFrameLoader->GetExtantBrowsingContext()) { + return bc->GetBrowserId(); + } + } + return 0; +} + +nsIOpenWindowInfo* XULFrameElement::GetOpenWindowInfo() const { + return mOpenWindowInfo; +} + +void XULFrameElement::SetOpenWindowInfo(nsIOpenWindowInfo* aInfo) { + mOpenWindowInfo = aInfo; +} + +void XULFrameElement::LoadSrc() { + if (!IsInUncomposedDoc() || !OwnerDoc()->GetRootElement()) { + return; + } + RefPtr frameLoader = GetFrameLoader(); + if (!frameLoader) { + // We may have had a nsIOpenWindowInfo set on us by browser chrome, due to + // being used as the target for a `window.open` call. Fetch that information + // if it's available, and clear it out so we don't read it again. + nsCOMPtr openWindowInfo = mOpenWindowInfo.forget(); + + // false as the networkCreated parameter so that xul:iframe/browser/editor + // session history handling works like dynamic html:iframes. Usually xul + // elements are used in chrome, which doesn't have session history at all. + mFrameLoader = nsFrameLoader::Create(this, false, openWindowInfo); + if (NS_WARN_IF(!mFrameLoader)) { + return; + } + + AsyncEventDispatcher::RunDOMEventWhenSafe( + *this, u"XULFrameLoaderCreated"_ns, CanBubble::eYes); + } + + mFrameLoader->LoadFrame(false); +} + +void XULFrameElement::SwapFrameLoaders(HTMLIFrameElement& aOtherLoaderOwner, + ErrorResult& rv) { + aOtherLoaderOwner.SwapFrameLoaders(this, rv); +} + +void XULFrameElement::SwapFrameLoaders(XULFrameElement& aOtherLoaderOwner, + ErrorResult& rv) { + if (&aOtherLoaderOwner == this) { + // nothing to do + return; + } + + aOtherLoaderOwner.SwapFrameLoaders(this, rv); +} + +void XULFrameElement::SwapFrameLoaders(nsFrameLoaderOwner* aOtherLoaderOwner, + mozilla::ErrorResult& rv) { + if (RefPtr doc = GetComposedDoc()) { + // SwapWithOtherLoader relies on frames being up-to-date. + doc->FlushPendingNotifications(FlushType::Frames); + } + + RefPtr loader = GetFrameLoader(); + RefPtr otherLoader = aOtherLoaderOwner->GetFrameLoader(); + if (!loader || !otherLoader) { + rv.Throw(NS_ERROR_NOT_IMPLEMENTED); + return; + } + + rv = loader->SwapWithOtherLoader(otherLoader, this, aOtherLoaderOwner); +} + +nsresult XULFrameElement::BindToTree(BindContext& aContext, nsINode& aParent) { + nsresult rv = nsXULElement::BindToTree(aContext, aParent); + NS_ENSURE_SUCCESS(rv, rv); + + if (IsInUncomposedDoc()) { + NS_ASSERTION(!nsContentUtils::IsSafeToRunScript(), + "Missing a script blocker!"); + // We're in a document now. Kick off the frame load. + LoadSrc(); + } + + return NS_OK; +} + +void XULFrameElement::UnbindFromTree(bool aNullParent) { + if (RefPtr frameLoader = GetFrameLoader()) { + frameLoader->Destroy(); + } + mFrameLoader = nullptr; + + nsXULElement::UnbindFromTree(aNullParent); +} + +void XULFrameElement::DestroyContent() { + RefPtr frameLoader = GetFrameLoader(); + if (frameLoader) { + frameLoader->Destroy(); + } + mFrameLoader = nullptr; + + nsXULElement::DestroyContent(); +} + +void XULFrameElement::AfterSetAttr(int32_t aNamespaceID, nsAtom* aName, + const nsAttrValue* aValue, + const nsAttrValue* aOldValue, + nsIPrincipal* aSubjectPrincipal, + bool aNotify) { + if (aNamespaceID == kNameSpaceID_None) { + if (aName == nsGkAtoms::src && aValue) { + LoadSrc(); + } else if (aName == nsGkAtoms::disablefullscreen && mFrameLoader) { + if (auto* bc = mFrameLoader->GetExtantBrowsingContext()) { + MOZ_ALWAYS_SUCCEEDS(bc->SetFullscreenAllowedByOwner(!aValue)); + } + } + } + + return nsXULElement::AfterSetAttr(aNamespaceID, aName, aValue, aOldValue, + aSubjectPrincipal, aNotify); +} + +} // namespace mozilla::dom diff --git a/dom/xul/XULFrameElement.h b/dom/xul/XULFrameElement.h new file mode 100644 index 0000000000..13a65f42e8 --- /dev/null +++ b/dom/xul/XULFrameElement.h @@ -0,0 +1,84 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef XULFrameElement_h__ +#define XULFrameElement_h__ + +#include "mozilla/Attributes.h" +#include "mozilla/dom/Nullable.h" +#include "mozilla/dom/WindowProxyHolder.h" +#include "js/TypeDecls.h" +#include "nsCycleCollectionParticipant.h" +#include "nsIOpenWindowInfo.h" +#include "nsWrapperCache.h" +#include "nsString.h" +#include "nsXULElement.h" +#include "nsFrameLoaderOwner.h" + +class nsIWebNavigation; +class nsFrameLoader; + +namespace mozilla { +class ErrorResult; + +namespace dom { + +class BrowsingContext; + +class XULFrameElement final : public nsXULElement, public nsFrameLoaderOwner { + public: + explicit XULFrameElement(already_AddRefed&& aNodeInfo) + : nsXULElement(std::move(aNodeInfo)) {} + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(XULFrameElement, nsXULElement) + + // XULFrameElement.webidl + nsDocShell* GetDocShell(); + already_AddRefed GetWebNavigation(); + Nullable GetContentWindow(); + Document* GetContentDocument(); + uint64_t BrowserId(); + nsIOpenWindowInfo* GetOpenWindowInfo() const; + void SetOpenWindowInfo(nsIOpenWindowInfo* aInfo); + + void SwapFrameLoaders(mozilla::dom::HTMLIFrameElement& aOtherLoaderOwner, + mozilla::ErrorResult& rv); + void SwapFrameLoaders(XULFrameElement& aOtherLoaderOwner, + mozilla::ErrorResult& rv); + void SwapFrameLoaders(nsFrameLoaderOwner* aOtherLoaderOwner, + mozilla::ErrorResult& rv); + + // nsIContent + nsresult BindToTree(BindContext&, nsINode& aParent) override; + void UnbindFromTree(bool aNullParent) override; + void DestroyContent() override; + + void AfterSetAttr(int32_t aNamespaceID, nsAtom* aName, + const nsAttrValue* aValue, const nsAttrValue* aOldValue, + nsIPrincipal* aSubjectPrincipal, bool aNotify) override; + + NS_IMPL_FROMNODE_HELPER(XULFrameElement, + IsAnyOfXULElements(nsGkAtoms::iframe, + nsGkAtoms::browser, + nsGkAtoms::editor)) + + protected: + virtual ~XULFrameElement() = default; + + JSObject* WrapNode(JSContext* aCx, + JS::Handle aGivenProto) override; + + void LoadSrc(); + + private: + nsCOMPtr mOpenWindowInfo; +}; + +} // namespace dom +} // namespace mozilla + +#endif // XULFrameElement_h diff --git a/dom/xul/XULMenuBarElement.cpp b/dom/xul/XULMenuBarElement.cpp new file mode 100644 index 0000000000..ffb7adc784 --- /dev/null +++ b/dom/xul/XULMenuBarElement.cpp @@ -0,0 +1,102 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 "XULMenuBarElement.h" +#include "MenuBarListener.h" +#include "XULButtonElement.h" +#include "nsXULPopupManager.h" +#include "mozilla/Assertions.h" +#include "mozilla/dom/BindContext.h" +#include "mozilla/AsyncEventDispatcher.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_CLASS(XULMenuBarElement) +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(XULMenuBarElement, + XULMenuParentElement) + if (tmp->mListener) { + tmp->mListener->Detach(); + tmp->mListener = nullptr; + } +NS_IMPL_CYCLE_COLLECTION_UNLINK_END +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(XULMenuBarElement, + XULMenuParentElement) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mListener) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END +NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(XULMenuBarElement, + XULMenuParentElement) + +XULMenuBarElement::XULMenuBarElement( + already_AddRefed&& aNodeInfo) + : XULMenuParentElement(std::move(aNodeInfo)) {} + +XULMenuBarElement::~XULMenuBarElement() { MOZ_DIAGNOSTIC_ASSERT(!mListener); } + +void XULMenuBarElement::SetActive(bool aActiveFlag) { + // If the activity is not changed, there is nothing to do. + if (mIsActive == aActiveFlag) { + return; + } + + // We can't activate a menubar outside of the document. + if (!IsInComposedDoc()) { + MOZ_ASSERT(!mIsActive, "How?"); + return; + } + + if (!aActiveFlag) { + // If there is a request to deactivate the menu bar, check to see whether + // there is a menu popup open for the menu bar. In this case, don't + // deactivate the menu bar. + if (auto* activeChild = GetActiveMenuChild()) { + if (activeChild->IsMenuPopupOpen()) { + return; + } + } + } + + mIsActive = aActiveFlag; + if (nsXULPopupManager* pm = nsXULPopupManager::GetInstance()) { + pm->SetActiveMenuBar(this, aActiveFlag); + } + if (!aActiveFlag) { + mActiveByKeyboard = false; + SetActiveMenuChild(nullptr); + } + + RefPtr dispatcher = new AsyncEventDispatcher( + this, aActiveFlag ? u"DOMMenuBarActive"_ns : u"DOMMenuBarInactive"_ns, + CanBubble::eYes, ChromeOnlyDispatch::eNo); + DebugOnly rv = dispatcher->PostDOMEvent(); + NS_ASSERTION(NS_SUCCEEDED(rv), "AsyncEventDispatcher failed to dispatch"); +} + +nsresult XULMenuBarElement::BindToTree(BindContext& aContext, + nsINode& aParent) { + MOZ_TRY(XULMenuParentElement::BindToTree(aContext, aParent)); + MOZ_DIAGNOSTIC_ASSERT(!mListener); + if (aContext.InComposedDoc()) { + mListener = new MenuBarListener(*this); + } + return NS_OK; +} + +void XULMenuBarElement::UnbindFromTree(bool aNullParent) { + if (mListener) { + mListener->Detach(); + mListener = nullptr; + } + if (NS_WARN_IF(mIsActive)) { + // Clean up silently when getting removed from the document while active. + mIsActive = false; + if (nsXULPopupManager* pm = nsXULPopupManager::GetInstance()) { + pm->SetActiveMenuBar(this, false); + } + } + return XULMenuParentElement::UnbindFromTree(aNullParent); +} + +} // namespace mozilla::dom diff --git a/dom/xul/XULMenuBarElement.h b/dom/xul/XULMenuBarElement.h new file mode 100644 index 0000000000..117682642d --- /dev/null +++ b/dom/xul/XULMenuBarElement.h @@ -0,0 +1,59 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef XULMenuBarElement_h__ +#define XULMenuBarElement_h__ + +#include "mozilla/Attributes.h" +#include "mozilla/dom/NameSpaceConstants.h" +#include "nsINode.h" +#include "nsISupports.h" +#include "XULMenuParentElement.h" + +namespace mozilla::dom { + +class KeyboardEvent; +class XULButtonElement; +class MenuBarListener; + +nsXULElement* NS_NewXULMenuBarElement( + already_AddRefed&& aNodeInfo); + +class XULMenuBarElement final : public XULMenuParentElement { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(XULMenuBarElement, + XULMenuParentElement) + NS_IMPL_FROMNODE_WITH_TAG(XULMenuBarElement, kNameSpaceID_XUL, menubar) + + explicit XULMenuBarElement(already_AddRefed&&); + + MOZ_CAN_RUN_SCRIPT void SetActive(bool); + bool IsActive() const { return mIsActive; } + + void SetActiveByKeyboard() { mActiveByKeyboard = true; } + bool IsActiveByKeyboard() const { return mActiveByKeyboard; } + + nsresult BindToTree(BindContext&, nsINode& aParent) override; + void UnbindFromTree(bool aNullParent) override; + + protected: + ~XULMenuBarElement() override; + + // Whether or not the menu bar is active (a menu item is highlighted or + // shown). + bool mIsActive = false; + + // Whether the menubar was made active via the keyboard. + bool mActiveByKeyboard = false; + + // The event listener that listens to document key presses and so on. + RefPtr mListener; +}; + +} // namespace mozilla::dom + +#endif // XULMenuBarElement_h diff --git a/dom/xul/XULMenuElement.cpp b/dom/xul/XULMenuElement.cpp new file mode 100644 index 0000000000..928e850012 --- /dev/null +++ b/dom/xul/XULMenuElement.cpp @@ -0,0 +1,82 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/XULMenuElement.h" +#include "mozilla/StaticAnalysisFunctions.h" +#include "mozilla/dom/XULButtonElement.h" +#include "mozilla/dom/XULMenuElementBinding.h" +#include "mozilla/dom/XULPopupElement.h" +#include "mozilla/dom/KeyboardEvent.h" +#include "mozilla/dom/KeyboardEventBinding.h" +#include "nsXULPopupManager.h" +#include "nsMenuPopupFrame.h" + +namespace mozilla::dom { + +JSObject* XULMenuElement::WrapNode(JSContext* aCx, + JS::Handle aGivenProto) { + return XULMenuElement_Binding::Wrap(aCx, this, aGivenProto); +} + +Element* XULMenuElement::GetActiveMenuChild() { + RefPtr popup = GetMenuPopupContent(); + return popup ? popup->GetActiveMenuChild() : nullptr; +} + +void XULMenuElement::SetActiveMenuChild(Element* aChild) { + RefPtr popup = GetMenuPopupContent(); + if (NS_WARN_IF(!popup)) { + return; + } + + if (!aChild) { + popup->SetActiveMenuChild(nullptr); + return; + } + auto* button = XULButtonElement::FromNode(aChild); + if (NS_WARN_IF(!button) || NS_WARN_IF(!button->IsMenu())) { + return; + } + // KnownLive because it's aChild. + popup->SetActiveMenuChild(MOZ_KnownLive(button)); +} + +bool XULButtonElement::HandleKeyPress(KeyboardEvent& keyEvent) { + RefPtr pm = nsXULPopupManager::GetInstance(); + if (!pm) { + return false; + } + + // if event has already been handled, bail + if (keyEvent.DefaultPrevented()) { + return false; + } + + if (keyEvent.IsMenuAccessKeyPressed()) { + return false; + } + + nsMenuPopupFrame* popupFrame = GetMenuPopup(FlushType::Frames); + if (NS_WARN_IF(!popupFrame)) { + return false; + } + + uint32_t keyCode = keyEvent.KeyCode(); + switch (keyCode) { + case KeyboardEvent_Binding::DOM_VK_UP: + case KeyboardEvent_Binding::DOM_VK_DOWN: + case KeyboardEvent_Binding::DOM_VK_HOME: + case KeyboardEvent_Binding::DOM_VK_END: { + nsNavigationDirection theDirection; + theDirection = NS_DIRECTION_FROM_KEY_CODE(popupFrame, keyCode); + return pm->HandleKeyboardNavigationInPopup(popupFrame, theDirection); + } + default: + return pm->HandleShortcutNavigation(keyEvent, popupFrame); + } +} + +} // namespace mozilla::dom diff --git a/dom/xul/XULMenuElement.h b/dom/xul/XULMenuElement.h new file mode 100644 index 0000000000..5def6839fc --- /dev/null +++ b/dom/xul/XULMenuElement.h @@ -0,0 +1,37 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_dom_XULMenuElement_h +#define mozilla_dom_XULMenuElement_h + +#include "XULButtonElement.h" +#include "nsINode.h" +#include "nsXULElement.h" + +namespace mozilla::dom { + +class KeyboardEvent; + +class XULMenuElement final : public XULButtonElement { + public: + explicit XULMenuElement(already_AddRefed&& aNodeInfo) + : XULButtonElement(std::move(aNodeInfo)) {} + + MOZ_CAN_RUN_SCRIPT void SetActiveMenuChild(Element*); + Element* GetActiveMenuChild(); + + NS_IMPL_FROMNODE_HELPER(XULMenuElement, + IsAnyOfXULElements(nsGkAtoms::menu, + nsGkAtoms::menulist)); + + private: + virtual ~XULMenuElement() = default; + JSObject* WrapNode(JSContext* aCx, JS::Handle aGivenProto) final; +}; + +} // namespace mozilla::dom + +#endif // XULMenuElement_h diff --git a/dom/xul/XULMenuParentElement.cpp b/dom/xul/XULMenuParentElement.cpp new file mode 100644 index 0000000000..f2e4d1e572 --- /dev/null +++ b/dom/xul/XULMenuParentElement.cpp @@ -0,0 +1,398 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 "XULMenuParentElement.h" +#include "XULButtonElement.h" +#include "XULMenuBarElement.h" +#include "XULPopupElement.h" +#include "mozilla/LookAndFeel.h" +#include "mozilla/StaticAnalysisFunctions.h" +#include "mozilla/TextEvents.h" +#include "mozilla/dom/DocumentInlines.h" +#include "mozilla/dom/KeyboardEvent.h" +#include "mozilla/EventDispatcher.h" +#include "nsDebug.h" +#include "nsMenuPopupFrame.h" +#include "nsString.h" +#include "nsStringFwd.h" +#include "nsUTF8Utils.h" +#include "nsXULElement.h" +#include "nsXULPopupManager.h" + +namespace mozilla::dom { + +NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(XULMenuParentElement, + nsXULElement) +NS_IMPL_CYCLE_COLLECTION_INHERITED(XULMenuParentElement, nsXULElement, + mActiveItem) + +XULMenuParentElement::XULMenuParentElement( + already_AddRefed&& aNodeInfo) + : nsXULElement(std::move(aNodeInfo)) {} + +XULMenuParentElement::~XULMenuParentElement() = default; + +class MenuActivateEvent final : public Runnable { + public: + MenuActivateEvent(Element* aMenu, bool aIsActivate) + : Runnable("MenuActivateEvent"), mMenu(aMenu), mIsActivate(aIsActivate) {} + + // TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230, bug 1535398) + MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHOD Run() override { + nsAutoString domEventToFire; + if (mIsActivate) { + // Highlight the menu. + mMenu->SetAttr(kNameSpaceID_None, nsGkAtoms::menuactive, u"true"_ns, + true); + // The menuactivated event is used by accessibility to track the user's + // movements through menus + domEventToFire.AssignLiteral("DOMMenuItemActive"); + } else { + // Unhighlight the menu. + mMenu->UnsetAttr(kNameSpaceID_None, nsGkAtoms::menuactive, true); + domEventToFire.AssignLiteral("DOMMenuItemInactive"); + } + + RefPtr pc = mMenu->OwnerDoc()->GetPresContext(); + RefPtr event = NS_NewDOMEvent(mMenu, pc, nullptr); + event->InitEvent(domEventToFire, true, true); + + event->SetTrusted(true); + + EventDispatcher::DispatchDOMEvent(mMenu, nullptr, event, pc, nullptr); + return NS_OK; + } + + private: + const RefPtr mMenu; + bool mIsActivate; +}; + +static void ActivateOrDeactivate(XULButtonElement& aButton, bool aActivate) { + if (!aButton.IsMenu()) { + return; + } + + if (nsXULPopupManager* pm = nsXULPopupManager::GetInstance()) { + if (aActivate) { + // Cancel the close timer if selecting a menu within the popup to be + // closed. + pm->CancelMenuTimer(aButton.GetContainingPopupWithoutFlushing()); + } else if (auto* popup = aButton.GetMenuPopupWithoutFlushing()) { + if (popup->IsOpen()) { + // Set up the close timer if deselecting an open sub-menu. + pm->HidePopupAfterDelay(popup, aButton.MenuOpenCloseDelay()); + } + } + } + + nsCOMPtr event = new MenuActivateEvent(&aButton, aActivate); + aButton.OwnerDoc()->Dispatch(TaskCategory::Other, event.forget()); +} + +XULButtonElement* XULMenuParentElement::GetContainingMenu() const { + if (IsMenuBar()) { + return nullptr; + } + auto* button = XULButtonElement::FromNodeOrNull(GetParent()); + if (!button || !button->IsMenu()) { + return nullptr; + } + return button; +} + +void XULMenuParentElement::LockMenuUntilClosed(bool aLock) { + if (IsMenuBar()) { + return; + } + mLocked = aLock; + // Lock/Unlock the parent menu too. + if (XULButtonElement* menu = GetContainingMenu()) { + if (XULMenuParentElement* parent = menu->GetMenuParent()) { + parent->LockMenuUntilClosed(aLock); + } + } +} + +void XULMenuParentElement::SetActiveMenuChild(XULButtonElement* aChild, + ByKey aByKey) { + if (aChild == mActiveItem) { + return; + } + + if (mActiveItem) { + ActivateOrDeactivate(*mActiveItem, false); + } + mActiveItem = nullptr; + + if (auto* menuBar = XULMenuBarElement::FromNode(*this)) { + // KnownLive because `this` is known-live by definition. + MOZ_KnownLive(menuBar)->SetActive(!!aChild); + } + + if (!aChild) { + return; + } + + // When a menu opens a submenu, the mouse will often be moved onto a sibling + // before moving onto an item within the submenu, causing the parent to become + // deselected. We need to ensure that the parent menu is reselected when an + // item in the submenu is selected. + if (RefPtr menu = GetContainingMenu()) { + if (RefPtr parent = menu->GetMenuParent()) { + parent->SetActiveMenuChild(menu, aByKey); + } + } + + mActiveItem = aChild; + ActivateOrDeactivate(*mActiveItem, true); + + if (IsInMenuList()) { + if (nsMenuPopupFrame* f = do_QueryFrame(GetPrimaryFrame())) { + f->EnsureActiveMenuListItemIsVisible(); +#ifdef XP_WIN + // On Windows, a menulist should update its value whenever navigation was + // done by the keyboard. + // + // NOTE(emilio): This is a rather odd per-platform behavior difference, + // but other browsers also do this. + if (aByKey == ByKey::Yes && f->IsOpen()) { + // Fire a command event as the new item, but we don't want to close the + // menu, blink it, or update any other state of the menuitem. The + // command event will cause the item to be selected. + RefPtr presShell = OwnerDoc()->GetPresShell(); + nsContentUtils::DispatchXULCommand(aChild, /* aTrusted = */ true, + nullptr, presShell, false, false, + false, false); + } +#endif + } + } +} + +static bool IsValidMenuItem(const XULMenuParentElement& aMenuParent, + const nsIContent& aContent) { + const auto* button = XULButtonElement::FromNode(aContent); + if (!button || !button->IsMenu()) { + return false; + } + if (!button->GetPrimaryFrame()) { + // Hidden buttons are not focusable/activatable. + return false; + } + if (!button->IsDisabled()) { + return true; + } + // In the menubar or a menulist disabled items are always skipped. + const bool skipDisabled = + LookAndFeel::GetInt(LookAndFeel::IntID::SkipNavigatingDisabledMenuItem) || + aMenuParent.IsMenuBar() || aMenuParent.IsInMenuList(); + return !skipDisabled; +} + +enum class StartKind { Parent, Item }; + +template +static XULButtonElement* DoGetNextMenuItem( + const XULMenuParentElement& aMenuParent, const nsIContent& aStart, + StartKind aStartKind) { + nsIContent* start = + aStartKind == StartKind::Item + ? (aForward ? aStart.GetNextSibling() : aStart.GetPreviousSibling()) + : (aForward ? aStart.GetFirstChild() : aStart.GetLastChild()); + for (nsIContent* node = start; node; + node = aForward ? node->GetNextSibling() : node->GetPreviousSibling()) { + if (IsValidMenuItem(aMenuParent, *node)) { + return static_cast(node); + } + if (node->IsXULElement(nsGkAtoms::menugroup)) { + if (XULButtonElement* child = DoGetNextMenuItem( + aMenuParent, *node, StartKind::Parent)) { + return child; + } + } + } + if (aStartKind == StartKind::Item && aStart.GetParent() && + aStart.GetParent()->IsXULElement(nsGkAtoms::menugroup)) { + // We haven't found anything in aStart's sibling list, but if we're in a + // group we need to keep looking. + return DoGetNextMenuItem(aMenuParent, *aStart.GetParent(), + StartKind::Item); + } + return nullptr; +} + +XULButtonElement* XULMenuParentElement::GetFirstMenuItem() const { + return DoGetNextMenuItem(*this, *this, StartKind::Parent); +} + +XULButtonElement* XULMenuParentElement::GetLastMenuItem() const { + return DoGetNextMenuItem(*this, *this, StartKind::Parent); +} + +XULButtonElement* XULMenuParentElement::GetNextMenuItemFrom( + const XULButtonElement& aStartingItem) const { + return DoGetNextMenuItem(*this, aStartingItem, StartKind::Item); +} + +XULButtonElement* XULMenuParentElement::GetPrevMenuItemFrom( + const XULButtonElement& aStartingItem) const { + return DoGetNextMenuItem(*this, aStartingItem, StartKind::Item); +} + +XULButtonElement* XULMenuParentElement::GetNextMenuItem(Wrap aWrap) const { + if (mActiveItem) { + if (auto* next = GetNextMenuItemFrom(*mActiveItem)) { + return next; + } + if (aWrap == Wrap::No) { + return nullptr; + } + } + return GetFirstMenuItem(); +} + +XULButtonElement* XULMenuParentElement::GetPrevMenuItem(Wrap aWrap) const { + if (mActiveItem) { + if (auto* prev = GetPrevMenuItemFrom(*mActiveItem)) { + return prev; + } + if (aWrap == Wrap::No) { + return nullptr; + } + } + return GetLastMenuItem(); +} + +void XULMenuParentElement::SelectFirstItem() { + if (RefPtr firstItem = GetFirstMenuItem()) { + SetActiveMenuChild(firstItem); + } +} + +XULButtonElement* XULMenuParentElement::FindMenuWithShortcut( + KeyboardEvent& aKeyEvent) const { + using AccessKeyArray = AutoTArray; + AccessKeyArray accessKeys; + WidgetKeyboardEvent* nativeKeyEvent = + aKeyEvent.WidgetEventPtr()->AsKeyboardEvent(); + if (nativeKeyEvent) { + nativeKeyEvent->GetAccessKeyCandidates(accessKeys); + } + const uint32_t charCode = aKeyEvent.CharCode(); + if (accessKeys.IsEmpty() && charCode) { + accessKeys.AppendElement(charCode); + } + if (accessKeys.IsEmpty()) { + return nullptr; // no character was pressed so just return + } + XULButtonElement* foundMenu = nullptr; + size_t foundIndex = AccessKeyArray::NoIndex; + for (auto* item = GetFirstMenuItem(); item; + item = GetNextMenuItemFrom(*item)) { + nsAutoString shortcutKey; + item->GetAttr(nsGkAtoms::accesskey, shortcutKey); + if (shortcutKey.IsEmpty()) { + continue; + } + + ToLowerCase(shortcutKey); + const char16_t* start = shortcutKey.BeginReading(); + const char16_t* end = shortcutKey.EndReading(); + uint32_t ch = UTF16CharEnumerator::NextChar(&start, end); + size_t index = accessKeys.IndexOf(ch); + if (index == AccessKeyArray::NoIndex) { + continue; + } + if (foundIndex == AccessKeyArray::NoIndex || index < foundIndex) { + foundMenu = item; + foundIndex = index; + } + } + return foundMenu; +} + +XULButtonElement* XULMenuParentElement::FindMenuWithShortcut( + const nsAString& aString, bool& aDoAction) const { + aDoAction = false; + uint32_t accessKeyMatchCount = 0; + uint32_t matchCount = 0; + + XULButtonElement* foundAccessKeyMenu = nullptr; + XULButtonElement* foundMenuBeforeCurrent = nullptr; + XULButtonElement* foundMenuAfterCurrent = nullptr; + + bool foundActive = false; + for (auto* item = GetFirstMenuItem(); item; + item = GetNextMenuItemFrom(*item)) { + nsAutoString textKey; + // Get the shortcut attribute. + item->GetAttr(nsGkAtoms::accesskey, textKey); + const bool isAccessKey = !textKey.IsEmpty(); + if (textKey.IsEmpty()) { // No shortcut, try first letter + item->GetAttr(nsGkAtoms::label, textKey); + if (textKey.IsEmpty()) { // No label, try another attribute (value) + item->GetAttr(nsGkAtoms::value, textKey); + } + } + + const bool isActive = item == GetActiveMenuChild(); + foundActive |= isActive; + + if (!StringBeginsWith( + nsContentUtils::TrimWhitespace< + nsContentUtils::IsHTMLWhitespaceOrNBSP>(textKey, false), + aString, nsCaseInsensitiveStringComparator)) { + continue; + } + // There is one match + matchCount++; + if (isAccessKey) { + // There is one shortcut-key match + accessKeyMatchCount++; + // Record the matched item. If there is only one matched shortcut + // item, do it + foundAccessKeyMenu = item; + } + // Get the active status + if (isActive && aString.Length() > 1 && !foundMenuBeforeCurrent) { + // If there is more than one char typed and the current item matches, the + // current item has highest priority, otherwise the item next to current + // has highest priority. + return item; + } + if (!foundActive || isActive) { + // It's a first candidate item located before/on the current item + if (!foundMenuBeforeCurrent) { + foundMenuBeforeCurrent = item; + } + } else { + if (!foundMenuAfterCurrent) { + foundMenuAfterCurrent = item; + } + } + } + + aDoAction = !IsInMenuList() && (matchCount == 1 || accessKeyMatchCount == 1); + + if (accessKeyMatchCount == 1) { + // We have one matched accesskey item + return foundAccessKeyMenu; + } + // If we have matched an item after the current, use it. + if (foundMenuAfterCurrent) { + return foundMenuAfterCurrent; + } + // If we haven't, use the item before the current, if any. + return foundMenuBeforeCurrent; +} + +void XULMenuParentElement::HandleEnterKeyPress(WidgetEvent& aEvent) { + if (RefPtr child = GetActiveMenuChild()) { + child->HandleEnterKeyPress(aEvent); + } +} + +} // namespace mozilla::dom diff --git a/dom/xul/XULMenuParentElement.h b/dom/xul/XULMenuParentElement.h new file mode 100644 index 0000000000..c722a23d79 --- /dev/null +++ b/dom/xul/XULMenuParentElement.h @@ -0,0 +1,80 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef XULMenuParentElement_h__ +#define XULMenuParentElement_h__ + +#include "mozilla/Attributes.h" +#include "nsISupports.h" +#include "nsXULElement.h" + +namespace mozilla::dom { + +class KeyboardEvent; +class XULButtonElement; + +nsXULElement* NS_NewXULMenuParentElement( + already_AddRefed&& aNodeInfo); + +class XULMenuParentElement : public nsXULElement { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(XULMenuParentElement, nsXULElement) + + explicit XULMenuParentElement( + already_AddRefed&& aNodeInfo); + + bool IsMenuBar() const { return NodeInfo()->Equals(nsGkAtoms::menubar); } + bool IsMenu() const { return !IsMenuBar(); } + + bool IsLocked() const { return mLocked; } + + void LockMenuUntilClosed(bool aLock); + + bool IsInMenuList() const { + return GetParent() && GetParent()->IsXULElement(nsGkAtoms::menulist); + } + + XULButtonElement* FindMenuWithShortcut(KeyboardEvent&) const; + XULButtonElement* FindMenuWithShortcut(const nsAString& aString, + bool& aDoAction) const; + MOZ_CAN_RUN_SCRIPT void HandleEnterKeyPress(WidgetEvent&); + + NS_IMPL_FROMNODE_HELPER(XULMenuParentElement, + IsAnyOfXULElements(nsGkAtoms::menupopup, + nsGkAtoms::popup, nsGkAtoms::panel, + nsGkAtoms::tooltip, + nsGkAtoms::menubar)); + + XULButtonElement* GetActiveMenuChild() const { return mActiveItem.get(); } + enum class ByKey : bool { No, Yes }; + MOZ_CAN_RUN_SCRIPT void SetActiveMenuChild(XULButtonElement*, + ByKey = ByKey::No); + + XULButtonElement* GetFirstMenuItem() const; + XULButtonElement* GetLastMenuItem() const; + + XULButtonElement* GetNextMenuItemFrom(const XULButtonElement&) const; + XULButtonElement* GetPrevMenuItemFrom(const XULButtonElement&) const; + + enum class Wrap : bool { No, Yes }; + XULButtonElement* GetNextMenuItem(Wrap = Wrap::Yes) const; + XULButtonElement* GetPrevMenuItem(Wrap = Wrap::Yes) const; + + XULButtonElement* GetContainingMenu() const; + + MOZ_CAN_RUN_SCRIPT void SelectFirstItem(); + + protected: + RefPtr mActiveItem; + bool mLocked = false; + + ~XULMenuParentElement() override; +}; + +} // namespace mozilla::dom + +#endif // XULMenuParentElement_h diff --git a/dom/xul/XULPersist.cpp b/dom/xul/XULPersist.cpp new file mode 100644 index 0000000000..5048bd3ddd --- /dev/null +++ b/dom/xul/XULPersist.cpp @@ -0,0 +1,247 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 https://mozilla.org/MPL/2.0/. */ + +#include "XULPersist.h" + +#include "nsIXULStore.h" +#include "nsIStringEnumerator.h" +#include "nsServiceManagerUtils.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/Element.h" +#include "nsContentUtils.h" +#include "nsIAppWindow.h" + +namespace mozilla::dom { + +static bool IsRootElement(Element* aElement) { + return aElement->OwnerDoc()->GetRootElement() == aElement; +} + +// FIXME: This is a hack to differentiate "attribute is missing" from "attribute +// is present but empty". Use a newline to avoid virtually all collisions. +// Ideally the XUL store would be able to store this more reasonably. +constexpr auto kMissingAttributeToken = u"-moz-missing\n"_ns; + +static bool ShouldPersistAttribute(Element* aElement, nsAtom* aAttribute) { + if (IsRootElement(aElement)) { + // This is not an element of the top document, its owner is + // not an AppWindow. Persist it. + if (aElement->OwnerDoc()->GetInProcessParentDocument()) { + return true; + } + // The following attributes of xul:window should be handled in + // AppWindow::SavePersistentAttributes instead of here. + if (aAttribute == nsGkAtoms::screenX || aAttribute == nsGkAtoms::screenY || + aAttribute == nsGkAtoms::width || aAttribute == nsGkAtoms::height || + aAttribute == nsGkAtoms::sizemode) { + return false; + } + } + return true; +} + +NS_IMPL_ISUPPORTS(XULPersist, nsIDocumentObserver) + +XULPersist::XULPersist(Document* aDocument) + : nsStubDocumentObserver(), mDocument(aDocument) {} + +XULPersist::~XULPersist() = default; + +void XULPersist::Init() { + ApplyPersistentAttributes(); + mDocument->AddObserver(this); +} + +void XULPersist::DropDocumentReference() { + mDocument->RemoveObserver(this); + mDocument = nullptr; +} + +void XULPersist::AttributeChanged(dom::Element* aElement, int32_t aNameSpaceID, + nsAtom* aAttribute, int32_t aModType, + const nsAttrValue* aOldValue) { + NS_ASSERTION(aElement->OwnerDoc() == mDocument, "unexpected doc"); + + if (aNameSpaceID != kNameSpaceID_None) { + return; + } + + // See if there is anything we need to persist in the localstore. + nsAutoString persist; + // Persistence of attributes of xul:window is handled in AppWindow. + if (aElement->GetAttr(nsGkAtoms::persist, persist) && + ShouldPersistAttribute(aElement, aAttribute) && + // XXXldb This should check that it's a token, not just a substring. + persist.Find(nsDependentAtomString(aAttribute)) >= 0) { + // Might not need this, but be safe for now. + nsCOMPtr kungFuDeathGrip(this); + nsContentUtils::AddScriptRunner(NewRunnableMethod( + "dom::XULPersist::Persist", this, &XULPersist::Persist, aElement, + aAttribute)); + } +} + +void XULPersist::Persist(Element* aElement, nsAtom* aAttribute) { + if (!mDocument) { + return; + } + // For non-chrome documents, persistance is simply broken + if (!mDocument->NodePrincipal()->IsSystemPrincipal()) { + return; + } + + if (!mLocalStore) { + mLocalStore = do_GetService("@mozilla.org/xul/xulstore;1"); + if (NS_WARN_IF(!mLocalStore)) { + return; + } + } + + nsAutoString id; + + aElement->GetAttr(nsGkAtoms::id, id); + nsAtomString attrstr(aAttribute); + + nsAutoCString utf8uri; + nsresult rv = mDocument->GetDocumentURI()->GetSpec(utf8uri); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + // Persisting attributes to top level windows is handled by AppWindow. + if (IsRootElement(aElement)) { + if (nsCOMPtr win = + mDocument->GetAppWindowIfToplevelChrome()) { + return; + } + } + + NS_ConvertUTF8toUTF16 uri(utf8uri); + nsAutoString valuestr; + if (!aElement->GetAttr(aAttribute, valuestr)) { + valuestr = kMissingAttributeToken; + } + + mLocalStore->SetValue(uri, id, attrstr, valuestr); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "value set"); +} + +nsresult XULPersist::ApplyPersistentAttributes() { + if (!mDocument) { + return NS_ERROR_NOT_AVAILABLE; + } + // For non-chrome documents, persistance is simply broken + if (!mDocument->NodePrincipal()->IsSystemPrincipal()) { + return NS_ERROR_NOT_AVAILABLE; + } + + // Add all of the 'persisted' attributes into the content + // model. + if (!mLocalStore) { + mLocalStore = do_GetService("@mozilla.org/xul/xulstore;1"); + if (NS_WARN_IF(!mLocalStore)) { + return NS_ERROR_NOT_INITIALIZED; + } + } + + nsCOMArray elements; + + nsAutoCString utf8uri; + nsresult rv = mDocument->GetDocumentURI()->GetSpec(utf8uri); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + NS_ConvertUTF8toUTF16 uri(utf8uri); + + // Get a list of element IDs for which persisted values are available + nsCOMPtr ids; + rv = mLocalStore->GetIDsEnumerator(uri, getter_AddRefs(ids)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + bool hasmore; + while (NS_SUCCEEDED(ids->HasMore(&hasmore)) && hasmore) { + nsAutoString id; + ids->GetNext(id); + + // We want to hold strong refs to the elements while applying + // persistent attributes, just in case. + const nsTArray* allElements = mDocument->GetAllElementsForId(id); + if (!allElements) { + continue; + } + elements.Clear(); + elements.SetCapacity(allElements->Length()); + for (Element* element : *allElements) { + elements.AppendObject(element); + } + + rv = ApplyPersistentAttributesToElements(id, uri, elements); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + return NS_OK; +} + +nsresult XULPersist::ApplyPersistentAttributesToElements( + const nsAString& aID, const nsAString& aDocURI, + nsCOMArray& aElements) { + nsresult rv = NS_OK; + // Get a list of attributes for which persisted values are available + nsCOMPtr attrs; + rv = mLocalStore->GetAttributeEnumerator(aDocURI, aID, getter_AddRefs(attrs)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + bool hasmore; + while (NS_SUCCEEDED(attrs->HasMore(&hasmore)) && hasmore) { + nsAutoString attrstr; + attrs->GetNext(attrstr); + + nsAutoString value; + rv = mLocalStore->GetValue(aDocURI, aID, attrstr, value); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + RefPtr attr = NS_Atomize(attrstr); + if (NS_WARN_IF(!attr)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + uint32_t cnt = aElements.Length(); + for (int32_t i = int32_t(cnt) - 1; i >= 0; --i) { + Element* element = aElements.SafeElementAt(i); + if (!element) { + continue; + } + + // Applying persistent attributes to top level windows is handled + // by AppWindow. + if (IsRootElement(element)) { + if (nsCOMPtr win = + mDocument->GetAppWindowIfToplevelChrome()) { + continue; + } + } + + if (value == kMissingAttributeToken) { + Unused << element->UnsetAttr(kNameSpaceID_None, attr, true); + } else { + Unused << element->SetAttr(kNameSpaceID_None, attr, value, true); + } + } + } + + return NS_OK; +} + +} // namespace mozilla::dom diff --git a/dom/xul/XULPersist.h b/dom/xul/XULPersist.h new file mode 100644 index 0000000000..3c838c3ecf --- /dev/null +++ b/dom/xul/XULPersist.h @@ -0,0 +1,50 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_dom_XULPersist_h +#define mozilla_dom_XULPersist_h + +#include "nsStubDocumentObserver.h" + +#ifndef MOZ_NEW_XULSTORE +# include "nsCOMPtr.h" +class nsIXULStore; +#endif + +template +class nsCOMArray; + +namespace mozilla::dom { + +class XULPersist final : public nsStubDocumentObserver { + public: + NS_DECL_ISUPPORTS + + explicit XULPersist(Document* aDocument); + void Init(); + void DropDocumentReference(); + + NS_DECL_NSIMUTATIONOBSERVER_ATTRIBUTECHANGED + + protected: + void Persist(mozilla::dom::Element* aElement, nsAtom* aAttribute); + + private: + ~XULPersist(); + nsresult ApplyPersistentAttributes(); + nsresult ApplyPersistentAttributesToElements(const nsAString& aID, + const nsAString& aDocURI, + nsCOMArray& aElements); + + nsCOMPtr mLocalStore; + + // A weak pointer to our document. Nulled out by DropDocumentReference. + Document* MOZ_NON_OWNING_REF mDocument; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_XULPersist_h diff --git a/dom/xul/XULPopupElement.cpp b/dom/xul/XULPopupElement.cpp new file mode 100644 index 0000000000..d60173900e --- /dev/null +++ b/dom/xul/XULPopupElement.cpp @@ -0,0 +1,339 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 "XULMenuParentElement.h" +#include "nsCOMPtr.h" +#include "nsICSSDeclaration.h" +#include "nsIContent.h" +#include "nsNameSpaceManager.h" +#include "nsGkAtoms.h" +#include "nsMenuPopupFrame.h" +#include "nsStringFwd.h" +#include "nsView.h" +#include "mozilla/AppUnits.h" +#include "mozilla/AsyncEventDispatcher.h" +#include "mozilla/dom/DOMRect.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/Event.h" +#include "mozilla/dom/XULPopupElement.h" +#include "mozilla/dom/XULButtonElement.h" +#include "mozilla/dom/XULMenuElement.h" +#include "mozilla/dom/XULPopupElementBinding.h" +#ifdef MOZ_WAYLAND +# include "mozilla/WidgetUtilsGtk.h" +#endif + +namespace mozilla::dom { + +nsXULElement* NS_NewXULPopupElement( + already_AddRefed&& aNodeInfo) { + RefPtr nodeInfo(aNodeInfo); + auto* nim = nodeInfo->NodeInfoManager(); + return new (nim) XULPopupElement(nodeInfo.forget()); +} + +JSObject* XULPopupElement::WrapNode(JSContext* aCx, + JS::Handle aGivenProto) { + return XULPopupElement_Binding::Wrap(aCx, this, aGivenProto); +} + +nsMenuPopupFrame* XULPopupElement::GetFrame(FlushType aFlushType) { + nsIFrame* f = GetPrimaryFrame(aFlushType); + MOZ_ASSERT(!f || f->IsMenuPopupFrame()); + return static_cast(f); +} + +void XULPopupElement::OpenPopup(Element* aAnchorElement, + const StringOrOpenPopupOptions& aOptions, + int32_t aXPos, int32_t aYPos, + bool aIsContextMenu, bool aAttributesOverride, + Event* aTriggerEvent) { + nsAutoString position; + if (aOptions.IsOpenPopupOptions()) { + const OpenPopupOptions& options = aOptions.GetAsOpenPopupOptions(); + position = options.mPosition; + aXPos = options.mX; + aYPos = options.mY; + aIsContextMenu = options.mIsContextMenu; + aAttributesOverride = options.mAttributesOverride; + aTriggerEvent = options.mTriggerEvent; + } else { + position = aOptions.GetAsString(); + } + + nsXULPopupManager* pm = nsXULPopupManager::GetInstance(); + if (pm) { + // As a special case for popups that are menus when no anchor or position + // are specified, open the popup with ShowMenu instead of ShowPopup so that + // the popup is aligned with the menu. + if (!aAnchorElement && position.IsEmpty() && GetPrimaryFrame()) { + if (auto* menu = GetContainingMenu()) { + pm->ShowMenu(menu, false); + return; + } + } + + pm->ShowPopup(this, aAnchorElement, position, aXPos, aYPos, aIsContextMenu, + aAttributesOverride, false, aTriggerEvent); + } +} + +void XULPopupElement::OpenPopupAtScreen(int32_t aXPos, int32_t aYPos, + bool aIsContextMenu, + Event* aTriggerEvent) { + nsXULPopupManager* pm = nsXULPopupManager::GetInstance(); + if (pm) { + pm->ShowPopupAtScreen(this, aXPos, aYPos, aIsContextMenu, aTriggerEvent); + } +} + +void XULPopupElement::OpenPopupAtScreenRect(const nsAString& aPosition, + int32_t aXPos, int32_t aYPos, + int32_t aWidth, int32_t aHeight, + bool aIsContextMenu, + bool aAttributesOverride, + Event* aTriggerEvent) { + nsXULPopupManager* pm = nsXULPopupManager::GetInstance(); + if (pm) { + pm->ShowPopupAtScreenRect( + this, aPosition, nsIntRect(aXPos, aYPos, aWidth, aHeight), + aIsContextMenu, aAttributesOverride, aTriggerEvent); + } +} + +void XULPopupElement::HidePopup(bool aCancel) { + nsXULPopupManager* pm = nsXULPopupManager::GetInstance(); + if (!pm) { + return; + } + HidePopupOptions options{HidePopupOption::DeselectMenu}; + if (aCancel) { + options += HidePopupOption::IsRollup; + } + pm->HidePopup(this, options); +} + +static Modifiers ConvertModifiers(const ActivateMenuItemOptions& aModifiers) { + Modifiers modifiers = 0; + if (aModifiers.mCtrlKey) { + modifiers |= MODIFIER_CONTROL; + } + if (aModifiers.mAltKey) { + modifiers |= MODIFIER_ALT; + } + if (aModifiers.mShiftKey) { + modifiers |= MODIFIER_SHIFT; + } + if (aModifiers.mMetaKey) { + modifiers |= MODIFIER_META; + } + return modifiers; +} + +void XULPopupElement::PopupOpened(bool aSelectFirstItem) { + if (aSelectFirstItem) { + SelectFirstItem(); + } + if (RefPtr button = GetContainingMenu()) { + if (RefPtr parent = button->GetMenuParent()) { + parent->SetActiveMenuChild(button); + } + } +} + +void XULPopupElement::PopupClosed(bool aDeselectMenu) { + LockMenuUntilClosed(false); + SetActiveMenuChild(nullptr); + auto dispatcher = MakeRefPtr( + this, u"DOMMenuInactive"_ns, CanBubble::eYes, ChromeOnlyDispatch::eNo); + dispatcher->PostDOMEvent(); + if (RefPtr button = GetContainingMenu()) { + button->PopupClosed(aDeselectMenu); + } +} + +void XULPopupElement::ActivateItem(Element& aItemElement, + const ActivateMenuItemOptions& aOptions, + ErrorResult& aRv) { + if (!Contains(&aItemElement)) { + return aRv.ThrowInvalidStateError("Menu item is not inside this menu."); + } + + Modifiers modifiers = ConvertModifiers(aOptions); + + // First, check if a native menu is open, and activate the item in it. + if (nsXULPopupManager* pm = nsXULPopupManager::GetInstance()) { + if (pm->ActivateNativeMenuItem(&aItemElement, modifiers, aOptions.mButton, + aRv)) { + return; + } + } + + auto* item = XULButtonElement::FromNode(aItemElement); + if (!item || !item->IsMenu()) { + return aRv.ThrowInvalidStateError("Not a menu item"); + } + + if (!item->GetPrimaryFrame(FlushType::Frames)) { + return aRv.ThrowInvalidStateError("Menu item is hidden"); + } + + auto* popup = item->GetContainingPopupElement(); + if (!popup) { + return aRv.ThrowInvalidStateError("No popup"); + } + + nsMenuPopupFrame* frame = popup->GetFrame(FlushType::None); + if (!frame || !frame->IsOpen()) { + return aRv.ThrowInvalidStateError("Popup is not open"); + } + + // This is a chrome-only API, so we're trusted. + const bool trusted = true; + // KnownLive because item is aItemElement. + MOZ_KnownLive(item)->ExecuteMenu(modifiers, aOptions.mButton, trusted); +} + +void XULPopupElement::MoveTo(int32_t aLeft, int32_t aTop) { + if (nsMenuPopupFrame* menuPopupFrame = do_QueryFrame(GetPrimaryFrame())) { + menuPopupFrame->MoveTo(CSSIntPoint(aLeft, aTop), true); + } +} + +void XULPopupElement::MoveToAnchor(Element* aAnchorElement, + const nsAString& aPosition, int32_t aXPos, + int32_t aYPos, bool aAttributesOverride) { + nsMenuPopupFrame* menuPopupFrame = GetFrame(FlushType::None); + if (menuPopupFrame && menuPopupFrame->IsVisibleOrShowing()) { + menuPopupFrame->MoveToAnchor(aAnchorElement, aPosition, aXPos, aYPos, + aAttributesOverride); + } +} + +void XULPopupElement::SizeTo(int32_t aWidth, int32_t aHeight) { + nsAutoCString width; + nsAutoCString height; + width.AppendInt(aWidth); + width.AppendLiteral("px"); + height.AppendInt(aHeight); + height.AppendLiteral("px"); + + nsCOMPtr style = Style(); + style->SetProperty("width"_ns, width, ""_ns, IgnoreErrors()); + style->SetProperty("height"_ns, height, ""_ns, IgnoreErrors()); + + // If the popup is open, force a reposition of the popup after resizing it + // with notifications set to true so that the popuppositioned event is fired. + nsMenuPopupFrame* menuPopupFrame = do_QueryFrame(GetPrimaryFrame()); + if (menuPopupFrame && menuPopupFrame->PopupState() == ePopupShown) { + menuPopupFrame->SetPopupPosition(false); + } +} + +void XULPopupElement::GetState(nsString& aState) { + // set this here in case there's no frame for the popup + aState.AssignLiteral("closed"); + + if (nsXULPopupManager* pm = nsXULPopupManager::GetInstance()) { + switch (pm->GetPopupState(this)) { + case ePopupShown: + aState.AssignLiteral("open"); + break; + case ePopupShowing: + case ePopupPositioning: + case ePopupOpening: + case ePopupVisible: + aState.AssignLiteral("showing"); + break; + case ePopupHiding: + case ePopupInvisible: + aState.AssignLiteral("hiding"); + break; + case ePopupClosed: + break; + default: + MOZ_ASSERT_UNREACHABLE("Bad popup state"); + break; + } + } +} + +nsINode* XULPopupElement::GetTriggerNode() const { + nsMenuPopupFrame* menuPopupFrame = do_QueryFrame(GetPrimaryFrame()); + return nsMenuPopupFrame::GetTriggerContent(menuPopupFrame); +} + +// FIXME(emilio): should probably be renamed to GetAnchorElement? +Element* XULPopupElement::GetAnchorNode() const { + nsMenuPopupFrame* menuPopupFrame = do_QueryFrame(GetPrimaryFrame()); + if (!menuPopupFrame) { + return nullptr; + } + return Element::FromNodeOrNull(menuPopupFrame->GetAnchor()); +} + +already_AddRefed XULPopupElement::GetOuterScreenRect() { + RefPtr rect = new DOMRect(ToSupports(OwnerDoc())); + + // Return an empty rectangle if the popup is not open. + nsMenuPopupFrame* menuPopupFrame = + do_QueryFrame(GetPrimaryFrame(FlushType::Frames)); + if (!menuPopupFrame || !menuPopupFrame->IsOpen()) { + return rect.forget(); + } + + Maybe screenRect; + + if (menuPopupFrame->IsNativeMenu()) { + // For native menus we can't query the true size. Use the anchor rect + // instead, which at least has the position at which we were intending to + // open the menu. + screenRect = Some(CSSRect(menuPopupFrame->GetScreenAnchorRect())); + } else { + // For non-native menus, query the bounds from the widget. + if (nsView* view = menuPopupFrame->GetView()) { + if (nsIWidget* widget = view->GetWidget()) { + screenRect = Some(widget->GetScreenBounds() / + menuPopupFrame->PresContext()->CSSToDevPixelScale()); + } + } + } + + if (screenRect) { + rect->SetRect(screenRect->X(), screenRect->Y(), screenRect->Width(), + screenRect->Height()); + } + return rect.forget(); +} + +void XULPopupElement::SetConstraintRect(dom::DOMRectReadOnly& aRect) { + nsMenuPopupFrame* menuPopupFrame = + do_QueryFrame(GetPrimaryFrame(FlushType::Frames)); + if (menuPopupFrame) { + menuPopupFrame->SetOverrideConstraintRect(CSSIntRect::Truncate( + aRect.Left(), aRect.Top(), aRect.Width(), aRect.Height())); + } +} + +bool XULPopupElement::IsWaylandDragSource() const { +#ifdef MOZ_WAYLAND + nsMenuPopupFrame* f = do_QueryFrame(GetPrimaryFrame()); + return f && f->IsDragSource(); +#else + return false; +#endif +} + +bool XULPopupElement::IsWaylandPopup() const { +#ifdef MOZ_WAYLAND + return widget::GdkIsWaylandDisplay() || widget::IsXWaylandProtocol(); +#else + return false; +#endif +} + +} // namespace mozilla::dom diff --git a/dom/xul/XULPopupElement.h b/dom/xul/XULPopupElement.h new file mode 100644 index 0000000000..9bf8fe8af5 --- /dev/null +++ b/dom/xul/XULPopupElement.h @@ -0,0 +1,118 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef XULPopupElement_h__ +#define XULPopupElement_h__ + +#include "XULMenuParentElement.h" +#include "mozilla/Attributes.h" +#include "nsCycleCollectionParticipant.h" +#include "nsINode.h" +#include "nsWrapperCache.h" +#include "nsString.h" +#include "nsXULElement.h" + +class nsMenuPopupFrame; +struct JSContext; + +namespace mozilla { +class ErrorResult; + +namespace dom { + +class DOMRect; +class Element; +class Event; +class StringOrOpenPopupOptions; +struct ActivateMenuItemOptions; + +nsXULElement* NS_NewXULPopupElement( + already_AddRefed&& aNodeInfo); + +class XULPopupElement : public XULMenuParentElement { + private: + nsMenuPopupFrame* GetFrame(FlushType); + + public: + explicit XULPopupElement(already_AddRefed&& aNodeInfo) + : XULMenuParentElement(std::move(aNodeInfo)) {} + + void GetLabel(DOMString& aValue) const { + GetXULAttr(nsGkAtoms::label, aValue); + } + void SetLabel(const nsAString& aValue, ErrorResult& rv) { + SetXULAttr(nsGkAtoms::label, aValue, rv); + } + + bool IsMenu() const { + return IsAnyOfXULElements(nsGkAtoms::popup, nsGkAtoms::menupopup); + } + + void GetPosition(DOMString& aValue) const { + GetXULAttr(nsGkAtoms::position, aValue); + } + void SetPosition(const nsAString& aValue, ErrorResult& rv) { + SetXULAttr(nsGkAtoms::position, aValue, rv); + } + + void OpenPopup(Element* aAnchorElement, + const StringOrOpenPopupOptions& aOptions, int32_t aXPos, + int32_t aYPos, bool aIsContextMenu, bool aAttributesOverride, + Event* aTriggerEvent); + + void OpenPopupAtScreen(int32_t aXPos, int32_t aYPos, bool aIsContextMenu, + Event* aTriggerEvent); + + void OpenPopupAtScreenRect(const nsAString& aPosition, int32_t aXPos, + int32_t aYPos, int32_t aWidth, int32_t aHeight, + bool aIsContextMenu, bool aAttributesOverride, + Event* aTriggerEvent); + + void HidePopup(bool aCancel); + + MOZ_CAN_RUN_SCRIPT void ActivateItem(Element& aItemElement, + const ActivateMenuItemOptions& aOptions, + ErrorResult& aRv); + + void GetState(nsString& aState); + + MOZ_CAN_RUN_SCRIPT void PopupOpened(bool aSelectFirstItem); + MOZ_CAN_RUN_SCRIPT void PopupClosed(bool aDeselectMenu); + + nsINode* GetTriggerNode() const; + + Element* GetAnchorNode() const; + + already_AddRefed GetOuterScreenRect(); + + void MoveTo(int32_t aLeft, int32_t aTop); + + void MoveToAnchor(Element* aAnchorElement, const nsAString& aPosition, + int32_t aXPos, int32_t aYPos, bool aAttributesOverride); + + void SizeTo(int32_t aWidth, int32_t aHeight); + + void SetConstraintRect(DOMRectReadOnly& aRect); + + bool IsWaylandDragSource() const; + bool IsWaylandPopup() const; + + NS_IMPL_FROMNODE_HELPER(XULPopupElement, + IsAnyOfXULElements(nsGkAtoms::menupopup, + nsGkAtoms::popup, nsGkAtoms::panel, + nsGkAtoms::tooltip)); + + protected: + virtual ~XULPopupElement() = default; + + JSObject* WrapNode(JSContext* aCx, + JS::Handle aGivenProto) override; +}; + +} // namespace dom +} // namespace mozilla + +#endif // XULPopupElement_h diff --git a/dom/xul/XULResizerElement.cpp b/dom/xul/XULResizerElement.cpp new file mode 100644 index 0000000000..a0dbb485d5 --- /dev/null +++ b/dom/xul/XULResizerElement.cpp @@ -0,0 +1,354 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/XULResizerElement.h" +#include "mozilla/dom/XULResizerElementBinding.h" + +#include "mozilla/EventDispatcher.h" +#include "mozilla/PresShell.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/DocumentInlines.h" +#include "mozilla/MouseEvents.h" +#include "nsContentUtils.h" +#include "nsICSSDeclaration.h" +#include "nsIFrame.h" +#include "nsLayoutUtils.h" +#include "nsPresContext.h" +#include "nsStyledElement.h" + +namespace mozilla::dom { + +nsXULElement* NS_NewXULResizerElement( + already_AddRefed&& aNodeInfo) { + RefPtr nodeInfo(aNodeInfo); + auto* nim = nodeInfo->NodeInfoManager(); + return new (nim) XULResizerElement(nodeInfo.forget()); +} + +static bool GetEventPoint(const WidgetGUIEvent* aEvent, + LayoutDeviceIntPoint& aPoint) { + NS_ENSURE_TRUE(aEvent, false); + + const WidgetTouchEvent* touchEvent = aEvent->AsTouchEvent(); + if (touchEvent) { + // return false if there is more than one touch on the page, or if + // we can't find a touch point + if (touchEvent->mTouches.Length() != 1) { + return false; + } + + const dom::Touch* touch = touchEvent->mTouches.SafeElementAt(0); + if (!touch) { + return false; + } + aPoint = touch->mRefPoint; + } else { + aPoint = aEvent->mRefPoint; + } + return true; +} + +JSObject* XULResizerElement::WrapNode(JSContext* aCx, + JS::Handle aGivenProto) { + return XULResizerElement_Binding::Wrap(aCx, this, aGivenProto); +} + +XULResizerElement::Direction XULResizerElement::GetDirection() { + static const mozilla::dom::Element::AttrValuesArray strings[] = { + // clang-format off + nsGkAtoms::topleft, nsGkAtoms::top, nsGkAtoms::topright, + nsGkAtoms::left, nsGkAtoms::right, + nsGkAtoms::bottomleft, nsGkAtoms::bottom, nsGkAtoms::bottomright, + nsGkAtoms::bottomstart, nsGkAtoms::bottomend, + nullptr + // clang-format on + }; + + static const Direction directions[] = { + // clang-format off + {-1, -1}, {0, -1}, {1, -1}, + {-1, 0}, {1, 0}, + {-1, 1}, {0, 1}, {1, 1}, + {-1, 1}, {1, 1} + // clang-format on + }; + + const auto* frame = GetPrimaryFrame(); + if (!frame) { + return directions[0]; // default: topleft + } + + int32_t index = + FindAttrValueIn(kNameSpaceID_None, nsGkAtoms::dir, strings, eCaseMatters); + if (index < 0) { + return directions[0]; // default: topleft + } + + if (index >= 8) { + // Directions 8 and higher are RTL-aware directions and should reverse the + // horizontal component if RTL. + auto wm = frame->GetWritingMode(); + if (wm.IsPhysicalRTL()) { + Direction direction = directions[index]; + direction.mHorizontal *= -1; + return direction; + } + } + + return directions[index]; +} + +nsresult XULResizerElement::PostHandleEvent(EventChainPostVisitor& aVisitor) { + if (aVisitor.mEventStatus != nsEventStatus_eConsumeNoDefault) { + PostHandleEventInternal(aVisitor); + } + return nsXULElement::PostHandleEvent(aVisitor); +} + +Maybe XULResizerElement::GetCurrentSize() const { + nsIContent* contentToResize = GetContentToResize(); + if (!contentToResize) { + return Nothing(); + } + nsIFrame* frame = contentToResize->GetPrimaryFrame(); + if (!frame) { + return Nothing(); + } + return Some(frame->StylePosition()->mBoxSizing == StyleBoxSizing::Content + ? frame->GetContentRect().Size() + : frame->GetRect().Size()); +} + +void XULResizerElement::PostHandleEventInternal( + EventChainPostVisitor& aVisitor) { + bool doDefault = true; + const WidgetEvent& event = *aVisitor.mEvent; + switch (event.mMessage) { + case eTouchStart: + case eMouseDown: { + if (event.mClass == eTouchEventClass || + (event.mClass == eMouseEventClass && + event.AsMouseEvent()->mButton == MouseButton::ePrimary)) { + auto size = GetCurrentSize(); + if (!size) { + break; // don't do anything if there's nothing to resize + } + // cache the content rectangle for the frame to resize + mMouseDownSize = *size; + + // remember current mouse coordinates + auto* guiEvent = event.AsGUIEvent(); + if (!GetEventPoint(guiEvent, mMouseDownPoint)) { + break; + } + mTrackingMouseMove = true; + PresShell::SetCapturingContent(this, CaptureFlags::IgnoreAllowedState); + doDefault = false; + } + } break; + + case eTouchMove: + case eMouseMove: { + if (mTrackingMouseMove) { + nsCOMPtr contentToResize = GetContentToResize(); + if (!contentToResize) { + break; // don't do anything if there's nothing to resize + } + nsIFrame* frame = contentToResize->GetPrimaryFrame(); + if (!frame) { + break; + } + + // both MouseMove and direction are negative when pointing to the + // top and left, and positive when pointing to the bottom and right + + // retrieve the offset of the mousemove event relative to the mousedown. + // The difference is how much the resize needs to be + LayoutDeviceIntPoint refPoint; + auto* guiEvent = event.AsGUIEvent(); + if (!GetEventPoint(guiEvent, refPoint)) { + break; + } + + const nsPoint oldPos = nsLayoutUtils::GetEventCoordinatesRelativeTo( + guiEvent->mWidget, mMouseDownPoint, RelativeTo{frame}); + const nsPoint newPos = nsLayoutUtils::GetEventCoordinatesRelativeTo( + guiEvent->mWidget, refPoint, RelativeTo{frame}); + + nsPoint mouseMove(newPos - oldPos); + + // Determine which direction to resize by checking the dir attribute. + // For windows and menus, ensure that it can be resized in that + // direction. + Direction direction = GetDirection(); + + const CSSIntSize newSize = [&] { + nsSize newAuSize = mMouseDownSize; + // Check if there are any size constraints on this window. + newAuSize.width += direction.mHorizontal * mouseMove.x; + newAuSize.height += direction.mVertical * mouseMove.y; + if (newAuSize.width < AppUnitsPerCSSPixel() && mouseMove.x != 0) { + newAuSize.width = AppUnitsPerCSSPixel(); + } + if (newAuSize.height < AppUnitsPerCSSPixel() && mouseMove.y != 0) { + newAuSize.height = AppUnitsPerCSSPixel(); + } + + // When changing the size in a direction, don't allow the new size to + // be less that the resizer's size. This ensures that content isn't + // resized too small as to make the resizer invisible. + if (auto* resizerFrame = GetPrimaryFrame()) { + nsRect resizerRect = resizerFrame->GetRect(); + if (newAuSize.width < resizerRect.width && mouseMove.x != 0) { + newAuSize.width = resizerRect.width; + } + if (newAuSize.height < resizerRect.height && mouseMove.y != 0) { + newAuSize.height = resizerRect.height; + } + } + + // Convert the rectangle into css pixels. + return CSSIntSize::FromAppUnitsRounded(newAuSize); + }(); + + // Only resize in a given direction if the new size doesn't match the + // current size. + if (auto currentSize = GetCurrentSize()) { + auto newAuSize = CSSIntSize::ToAppUnits(newSize); + if (newAuSize.width == currentSize->width) { + direction.mHorizontal = 0; + } + if (newAuSize.height == currentSize->height) { + direction.mVertical = 0; + } + } + + SizeInfo sizeInfo, originalSizeInfo; + sizeInfo.width.AppendInt(newSize.width); + sizeInfo.height.AppendInt(newSize.height); + ResizeContent(contentToResize, direction, sizeInfo, &originalSizeInfo); + MaybePersistOriginalSize(contentToResize, originalSizeInfo); + + doDefault = false; + } + } break; + + case eMouseClick: { + auto* mouseEvent = event.AsMouseEvent(); + if (mouseEvent->IsLeftClickEvent()) { + // Execute the oncommand event handler. + nsContentUtils::DispatchXULCommand( + this, false, nullptr, nullptr, mouseEvent->IsControl(), + mouseEvent->IsAlt(), mouseEvent->IsShift(), mouseEvent->IsMeta(), + mouseEvent->mInputSource, mouseEvent->mButton); + } + } break; + + case eTouchEnd: + case eMouseUp: { + if (event.mClass == eTouchEventClass || + (event.mClass == eMouseEventClass && + event.AsMouseEvent()->mButton == MouseButton::ePrimary)) { + mTrackingMouseMove = false; + PresShell::ReleaseCapturingContent(); + doDefault = false; + } + } break; + + case eMouseDoubleClick: { + if (event.AsMouseEvent()->mButton == MouseButton::ePrimary) { + if (nsIContent* contentToResize = GetContentToResize()) { + RestoreOriginalSize(contentToResize); + } + } + } break; + + default: + break; + } + + if (!doDefault) { + aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault; + } +} + +nsIContent* XULResizerElement::GetContentToResize() const { + if (!IsInComposedDoc()) { + return nullptr; + } + // Return the parent, but skip over native anonymous content + nsIContent* parent = GetParent(); + return parent ? parent->FindFirstNonChromeOnlyAccessContent() : nullptr; +} + +/* static */ +void XULResizerElement::ResizeContent(nsIContent* aContent, + const Direction& aDirection, + const SizeInfo& aSizeInfo, + SizeInfo* aOriginalSizeInfo) { + RefPtr inlineStyleContent = nsStyledElement::FromNode(aContent); + if (!inlineStyleContent) { + return; + } + nsCOMPtr decl = inlineStyleContent->Style(); + if (aOriginalSizeInfo) { + decl->GetPropertyValue("width"_ns, aOriginalSizeInfo->width); + decl->GetPropertyValue("height"_ns, aOriginalSizeInfo->height); + } + + // only set the property if the element could have changed in that + // direction + if (aDirection.mHorizontal) { + nsAutoCString widthstr(aSizeInfo.width); + if (!widthstr.IsEmpty() && !StringEndsWith(widthstr, "px"_ns)) { + widthstr.AppendLiteral("px"); + } + decl->SetProperty("width"_ns, widthstr, ""_ns, IgnoreErrors()); + } + + if (aDirection.mVertical) { + nsAutoCString heightstr(aSizeInfo.height); + if (!heightstr.IsEmpty() && !StringEndsWith(heightstr, "px"_ns)) { + heightstr.AppendLiteral("px"); + } + decl->SetProperty("height"_ns, heightstr, ""_ns, IgnoreErrors()); + } +} + +/* static */ +void XULResizerElement::MaybePersistOriginalSize(nsIContent* aContent, + const SizeInfo& aSizeInfo) { + nsresult rv; + aContent->GetProperty(nsGkAtoms::_moz_original_size, &rv); + if (rv != NS_PROPTABLE_PROP_NOT_THERE) { + return; + } + + UniquePtr sizeInfo(new SizeInfo(aSizeInfo)); + rv = aContent->SetProperty( + nsGkAtoms::_moz_original_size, sizeInfo.get(), + nsINode::DeleteProperty); + if (NS_SUCCEEDED(rv)) { + Unused << sizeInfo.release(); + } +} + +/* static */ +void XULResizerElement::RestoreOriginalSize(nsIContent* aContent) { + nsresult rv; + SizeInfo* sizeInfo = static_cast( + aContent->GetProperty(nsGkAtoms::_moz_original_size, &rv)); + if (NS_FAILED(rv)) { + return; + } + + NS_ASSERTION(sizeInfo, "We set a null sizeInfo!?"); + Direction direction = {1, 1}; + ResizeContent(aContent, direction, *sizeInfo, nullptr); + aContent->RemoveProperty(nsGkAtoms::_moz_original_size); +} + +} // namespace mozilla::dom diff --git a/dom/xul/XULResizerElement.h b/dom/xul/XULResizerElement.h new file mode 100644 index 0000000000..f7c9e4d75d --- /dev/null +++ b/dom/xul/XULResizerElement.h @@ -0,0 +1,62 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_dom_XULResizerElement_h +#define mozilla_dom_XULResizerElement_h + +#include "nsXULElement.h" +#include "Units.h" + +namespace mozilla::dom { + +nsXULElement* NS_NewXULResizerElement( + already_AddRefed&& aNodeInfo); + +class XULResizerElement final : public nsXULElement { + public: + explicit XULResizerElement(already_AddRefed&& aNodeInfo) + : nsXULElement(std::move(aNodeInfo)) {} + + MOZ_CAN_RUN_SCRIPT + nsresult PostHandleEvent(EventChainPostVisitor&) override; + + private: + virtual ~XULResizerElement() = default; + JSObject* WrapNode(JSContext* aCx, JS::Handle aGivenProto) final; + + MOZ_CAN_RUN_SCRIPT + void PostHandleEventInternal(EventChainPostVisitor&); + + struct Direction { + int8_t mHorizontal; + int8_t mVertical; + }; + Direction GetDirection(); + + nsIContent* GetContentToResize() const; + // The current size of the content to resize (if available). + Maybe GetCurrentSize() const; + + struct SizeInfo { + nsCString width, height; + }; + static void SizeInfoDtorFunc(void* aObject, nsAtom* aPropertyName, + void* aPropertyValue, void* aData); + static void ResizeContent(nsIContent* aContent, const Direction& aDirection, + const SizeInfo& aSizeInfo, + SizeInfo* aOriginalSizeInfo); + static void MaybePersistOriginalSize(nsIContent* aContent, + const SizeInfo& aSizeInfo); + static void RestoreOriginalSize(nsIContent* aContent); + + nsSize mMouseDownSize; + LayoutDeviceIntPoint mMouseDownPoint; + bool mTrackingMouseMove = false; +}; + +} // namespace mozilla::dom + +#endif // XULResizerElement_h diff --git a/dom/xul/XULTextElement.cpp b/dom/xul/XULTextElement.cpp new file mode 100644 index 0000000000..04edfabc09 --- /dev/null +++ b/dom/xul/XULTextElement.cpp @@ -0,0 +1,49 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/Element.h" +#include "mozilla/dom/ToJSValue.h" +#include "nsCOMPtr.h" +#include "nsChangeHint.h" +#include "nsIContent.h" +#include "nsPresContext.h" +#include "nsIScrollableFrame.h" +#include "mozilla/dom/MutationEventBinding.h" +#include "mozilla/dom/XULTextElement.h" +#include "mozilla/dom/XULTextElementBinding.h" + +namespace mozilla::dom { + +nsChangeHint XULTextElement::GetAttributeChangeHint(const nsAtom* aAttribute, + int32_t aModType) const { + const bool reframe = [&] { + if (aAttribute == nsGkAtoms::value) { + // If we have an accesskey we need to recompute our -moz-label-content. + // Otherwise this is handled by either the attribute text node, or + // nsTextBoxFrame for crop="center". + return aModType == MutationEvent_Binding::ADDITION || + aModType == MutationEvent_Binding::REMOVAL || + HasAttr(nsGkAtoms::accesskey); + } + if (aAttribute == nsGkAtoms::crop || aAttribute == nsGkAtoms::accesskey) { + // value attr + crop="center" still uses nsTextBoxFrame. accesskey + // requires reframing as per the above comment. + return HasAttr(nsGkAtoms::value); + } + return false; + }(); + if (reframe) { + return nsChangeHint_ReconstructFrame; + } + return nsXULElement::GetAttributeChangeHint(aAttribute, aModType); +} + +JSObject* XULTextElement::WrapNode(JSContext* aCx, + JS::Handle aGivenProto) { + return XULTextElement_Binding::Wrap(aCx, this, aGivenProto); +} + +} // namespace mozilla::dom diff --git a/dom/xul/XULTextElement.h b/dom/xul/XULTextElement.h new file mode 100644 index 0000000000..0c6f086b5e --- /dev/null +++ b/dom/xul/XULTextElement.h @@ -0,0 +1,50 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef XULTextElement_h__ +#define XULTextElement_h__ + +#include "nsXULElement.h" + +namespace mozilla::dom { + +class XULTextElement final : public nsXULElement { + public: + explicit XULTextElement(already_AddRefed&& aNodeInfo) + : nsXULElement(std::move(aNodeInfo)) {} + + bool Disabled() { return GetXULBoolAttr(nsGkAtoms::disabled); } + MOZ_CAN_RUN_SCRIPT void SetDisabled(bool aValue) { + SetXULBoolAttr(nsGkAtoms::disabled, aValue, mozilla::IgnoreErrors()); + } + void GetValue(DOMString& aValue) const { + GetXULAttr(nsGkAtoms::value, aValue); + } + MOZ_CAN_RUN_SCRIPT void SetValue(const nsAString& aValue) { + SetAttr(kNameSpaceID_None, nsGkAtoms::value, aValue, true); + } + void GetAccessKey(DOMString& aValue) const { + GetXULAttr(nsGkAtoms::accesskey, aValue); + } + MOZ_CAN_RUN_SCRIPT void SetAccessKey(const nsAString& aValue) { + SetAttr(kNameSpaceID_None, nsGkAtoms::accesskey, aValue, true); + } + + nsChangeHint GetAttributeChangeHint(const nsAtom* aAttribute, + int32_t aModType) const override; + + NS_IMPL_FROMNODE_HELPER(XULTextElement, + IsAnyOfXULElements(nsGkAtoms::label, + nsGkAtoms::description)); + + protected: + virtual ~XULTextElement() = default; + JSObject* WrapNode(JSContext* aCx, JS::Handle aGivenProto) final; +}; + +} // namespace mozilla::dom + +#endif // XULTextElement_h diff --git a/dom/xul/XULTooltipElement.cpp b/dom/xul/XULTooltipElement.cpp new file mode 100644 index 0000000000..cf9e46c1ca --- /dev/null +++ b/dom/xul/XULTooltipElement.cpp @@ -0,0 +1,104 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 "nsCOMPtr.h" +#include "mozilla/dom/Event.h" +#include "mozilla/dom/XULTooltipElement.h" +#include "mozilla/dom/NodeInfo.h" +#include "mozilla/EventDispatcher.h" +#include "nsContentCreatorFunctions.h" +#include "nsContentUtils.h" +#include "nsCTooltipTextProvider.h" +#include "nsITooltipTextProvider.h" +#include "nsServiceManagerUtils.h" +#include "nsThreadUtils.h" + +namespace mozilla::dom { + +nsXULElement* NS_NewXULTooltipElement( + already_AddRefed&& aNodeInfo) { + RefPtr nodeInfo(aNodeInfo); + auto* nim = nodeInfo->NodeInfoManager(); + RefPtr tooltip = + new (nim) XULTooltipElement(nodeInfo.forget()); + NS_ENSURE_SUCCESS(tooltip->Init(), nullptr); + return tooltip; +} + +nsresult XULTooltipElement::Init() { + // Create the default child label node that will contain the text of the + // tooltip. + RefPtr nodeInfo; + nodeInfo = mNodeInfo->NodeInfoManager()->GetNodeInfo( + nsGkAtoms::description, nullptr, kNameSpaceID_XUL, nsINode::ELEMENT_NODE); + nsCOMPtr description; + nsresult rv = NS_NewXULElement(getter_AddRefs(description), nodeInfo.forget(), + dom::NOT_FROM_PARSER); + NS_ENSURE_SUCCESS(rv, rv); + description->SetAttr(kNameSpaceID_None, nsGkAtoms::_class, + u"tooltip-label"_ns, false); + ErrorResult error; + AppendChild(*description, error); + + return error.StealNSResult(); +} + +void XULTooltipElement::AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName, + const nsAttrValue* aValue, + const nsAttrValue* aOldValue, + nsIPrincipal* aSubjectPrincipal, + bool aNotify) { + if (aNameSpaceID == kNameSpaceID_None && aName == nsGkAtoms::label) { + // When the label attribute of this node changes propagate the text down + // into child description element. + nsCOMPtr description = GetFirstChild(); + if (description && description->IsXULElement(nsGkAtoms::description)) { + nsAutoString value; + if (aValue) { + aValue->ToString(value); + } + nsContentUtils::AddScriptRunner(NS_NewRunnableFunction( + "XULTooltipElement::AfterSetAttr", [description, value]() { + Element* descriptionElement = description->AsElement(); + descriptionElement->SetTextContent(value, IgnoreErrors()); + })); + } + } + return nsXULElement::AfterSetAttr(aNameSpaceID, aName, aValue, aOldValue, + aSubjectPrincipal, aNotify); +} + +nsresult XULTooltipElement::PostHandleEvent(EventChainPostVisitor& aVisitor) { + if (aVisitor.mEvent->mMessage == eXULPopupShowing && + aVisitor.mEvent->IsTrusted() && !aVisitor.mEvent->DefaultPrevented() && + AttrValueIs(kNameSpaceID_None, nsGkAtoms::page, nsGkAtoms::_true, + eCaseMatters) && + !AttrValueIs(kNameSpaceID_None, nsGkAtoms::titletip, nsGkAtoms::_true, + eCaseMatters)) { + // When the tooltip node has the "page" attribute set to "true" the + // tooltip text provider is used to find the tooltip text from page where + // mouse is hovering over. + nsCOMPtr textProvider = + do_GetService(NS_DEFAULTTOOLTIPTEXTPROVIDER_CONTRACTID); + nsString text; + nsString direction; + bool shouldChange = false; + if (textProvider) { + textProvider->GetNodeText(GetTriggerNode(), getter_Copies(text), + getter_Copies(direction), &shouldChange); + } + if (shouldChange) { + SetAttr(kNameSpaceID_None, nsGkAtoms::label, text, true); + SetAttr(kNameSpaceID_None, nsGkAtoms::direction, direction, true); + } else { + aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault; + aVisitor.mEvent->PreventDefault(); + } + } + return NS_OK; +} + +} // namespace mozilla::dom diff --git a/dom/xul/XULTooltipElement.h b/dom/xul/XULTooltipElement.h new file mode 100644 index 0000000000..1f39ad7f67 --- /dev/null +++ b/dom/xul/XULTooltipElement.h @@ -0,0 +1,35 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef XULTooltipElement_h__ +#define XULTooltipElement_h__ + +#include "XULPopupElement.h" + +namespace mozilla::dom { + +nsXULElement* NS_NewXULTooltipElement( + already_AddRefed&& aNodeInfo); + +class XULTooltipElement final : public XULPopupElement { + public: + explicit XULTooltipElement( + already_AddRefed&& aNodeInfo) + : XULPopupElement(std::move(aNodeInfo)) {} + nsresult Init(); + + void AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName, + const nsAttrValue* aValue, const nsAttrValue* aOldValue, + nsIPrincipal* aSubjectPrincipal, bool aNotify) override; + nsresult PostHandleEvent(EventChainPostVisitor& aVisitor) override; + + protected: + virtual ~XULTooltipElement() = default; +}; + +} // namespace mozilla::dom + +#endif // XULPopupElement_h diff --git a/dom/xul/XULTreeElement.cpp b/dom/xul/XULTreeElement.cpp new file mode 100644 index 0000000000..e190b22556 --- /dev/null +++ b/dom/xul/XULTreeElement.cpp @@ -0,0 +1,411 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 "nsCOMPtr.h" +#include "nsTreeContentView.h" +#include "nsITreeSelection.h" +#include "ChildIterator.h" +#include "nsError.h" +#include "nsTreeBodyFrame.h" +#include "mozilla/dom/DOMRect.h" +#include "mozilla/dom/BindingUtils.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/ToJSValue.h" +#include "mozilla/dom/XULTreeElement.h" +#include "mozilla/dom/XULTreeElementBinding.h" + +namespace mozilla::dom { + +NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(XULTreeElement, nsXULElement) +NS_IMPL_CYCLE_COLLECTION_INHERITED(XULTreeElement, nsXULElement, mView) + +JSObject* XULTreeElement::WrapNode(JSContext* aCx, + JS::Handle aGivenProto) { + return XULTreeElement_Binding::Wrap(aCx, this, aGivenProto); +} + +void XULTreeElement::UnbindFromTree(bool aNullParent) { + // Drop the view's ref to us. + if (mView) { + nsCOMPtr sel; + mView->GetSelection(getter_AddRefs(sel)); + if (sel) { + sel->SetTree(nullptr); + } + mView->SetTree(nullptr); // Break the circular ref between the view and us. + } + mView = nullptr; + + nsXULElement::UnbindFromTree(aNullParent); +} + +void XULTreeElement::DestroyContent() { + // Drop the view's ref to us. + if (mView) { + nsCOMPtr sel; + mView->GetSelection(getter_AddRefs(sel)); + if (sel) { + sel->SetTree(nullptr); + } + mView->SetTree(nullptr); // Break the circular ref between the view and us. + } + mView = nullptr; + + nsXULElement::DestroyContent(); +} + +static nsIContent* FindBodyElement(nsIContent* aParent) { + mozilla::dom::FlattenedChildIterator iter(aParent); + for (nsIContent* content = iter.GetNextChild(); content; + content = iter.GetNextChild()) { + mozilla::dom::NodeInfo* ni = content->NodeInfo(); + if (ni->Equals(nsGkAtoms::treechildren, kNameSpaceID_XUL)) { + return content; + } else if (ni->Equals(nsGkAtoms::tree, kNameSpaceID_XUL)) { + // There are nesting tree elements. Only the innermost should + // find the treechilren. + return nullptr; + } else if (content->IsElement() && + !ni->Equals(nsGkAtoms::_template, kNameSpaceID_XUL)) { + nsIContent* result = FindBodyElement(content); + if (result) return result; + } + } + + return nullptr; +} + +nsTreeBodyFrame* XULTreeElement::GetTreeBodyFrame(FlushType aFlushType) { + MOZ_ASSERT(aFlushType == FlushType::Frames || + aFlushType == FlushType::Layout || aFlushType == FlushType::None); + nsCOMPtr kungFuDeathGrip = this; // keep a reference + + // Make sure our frames are up to date, and layout as needed. We + // have to do this before checking for our cached mTreeBody, since + // it might go away on style flush, and in any case if aFlushLayout + // is true we need to make sure to flush no matter what. + if (aFlushType != FlushType::None) { + if (RefPtr doc = GetComposedDoc()) { + doc->FlushPendingNotifications(aFlushType); + } + } + + if (mTreeBody) { + // Have one cached already. + return mTreeBody; + } + + if (nsCOMPtr tree = FindBodyElement(this)) { + mTreeBody = do_QueryFrame(tree->GetPrimaryFrame()); + } + + return mTreeBody; +} + +already_AddRefed XULTreeElement::GetView(FlushType aFlushType) { + if (!mTreeBody) { + if (!GetTreeBodyFrame(aFlushType)) { + return nullptr; + } + + if (mView) { + nsCOMPtr view; + // Our new frame needs to initialise itself + mTreeBody->GetView(getter_AddRefs(view)); + return view.forget(); + } + } + if (!mView) { + // No tree builder, create a tree content view. + if (NS_FAILED(NS_NewTreeContentView(getter_AddRefs(mView)))) { + return nullptr; + } + + // Initialise the frame and view + mTreeBody->SetView(mView); + } + + return do_AddRef(mView); +} + +void XULTreeElement::SetView(nsITreeView* aView, CallerType aCallerType, + ErrorResult& aRv) { + if (aCallerType != CallerType::System) { + // Don't trust views coming from random places. + aRv.Throw(NS_ERROR_DOM_SECURITY_ERR); + return; + } + + mView = aView; + nsTreeBodyFrame* body = GetTreeBodyFrame(); + if (body) { + body->SetView(aView); + } +} + +bool XULTreeElement::Focused() { + nsTreeBodyFrame* body = GetTreeBodyFrame(); + if (body) { + return body->GetFocused(); + } + return false; +} + +void XULTreeElement::SetFocused(bool aFocused) { + nsTreeBodyFrame* body = GetTreeBodyFrame(); + if (body) { + body->SetFocused(aFocused); + } +} + +already_AddRefed XULTreeElement::GetTreeBody() { + nsTreeBodyFrame* body = GetTreeBodyFrame(); + if (body) { + nsCOMPtr element; + body->GetTreeBody(getter_AddRefs(element)); + return element.forget(); + } + + return nullptr; +} + +already_AddRefed XULTreeElement::GetColumns( + FlushType aFlushType) { + if (nsTreeBodyFrame* body = GetTreeBodyFrame(aFlushType)) { + return body->Columns(); + } + return nullptr; +} + +int32_t XULTreeElement::RowHeight() { + nsTreeBodyFrame* body = GetTreeBodyFrame(); + if (body) { + return body->RowHeight(); + } + return 0; +} + +int32_t XULTreeElement::RowWidth() { + nsTreeBodyFrame* body = GetTreeBodyFrame(); + if (body) { + return body->RowWidth(); + } + return 0; +} + +int32_t XULTreeElement::GetFirstVisibleRow() { + nsTreeBodyFrame* body = GetTreeBodyFrame(); + if (body) { + return body->FirstVisibleRow(); + } + return 0; +} + +int32_t XULTreeElement::GetLastVisibleRow() { + nsTreeBodyFrame* body = GetTreeBodyFrame(); + if (body) { + return body->LastVisibleRow(); + } + return 0; +} + +int32_t XULTreeElement::HorizontalPosition() { + nsTreeBodyFrame* body = GetTreeBodyFrame(); + if (body) { + return body->GetHorizontalPosition(); + } + return 0; +} + +int32_t XULTreeElement::GetPageLength() { + nsTreeBodyFrame* body = GetTreeBodyFrame(); + if (body) { + return body->PageLength(); + } + return 0; +} + +void XULTreeElement::EnsureRowIsVisible(int32_t aRow) { + nsTreeBodyFrame* body = GetTreeBodyFrame(); + if (body) { + body->EnsureRowIsVisible(aRow); + } +} + +void XULTreeElement::EnsureCellIsVisible(int32_t aRow, nsTreeColumn* aCol, + ErrorResult& aRv) { + nsTreeBodyFrame* body = GetTreeBodyFrame(); + if (body) { + nsresult rv = body->EnsureCellIsVisible(aRow, aCol); + if (NS_FAILED(rv)) { + aRv.Throw(rv); + } + } +} + +void XULTreeElement::ScrollToRow(int32_t aRow) { + nsTreeBodyFrame* body = GetTreeBodyFrame(FlushType::Layout); + if (!body) { + return; + } + + body->ScrollToRow(aRow); +} + +void XULTreeElement::ScrollByLines(int32_t aNumLines) { + nsTreeBodyFrame* body = GetTreeBodyFrame(); + if (!body) { + return; + } + body->ScrollByLines(aNumLines); +} + +void XULTreeElement::ScrollByPages(int32_t aNumPages) { + nsTreeBodyFrame* body = GetTreeBodyFrame(); + if (body) { + body->ScrollByPages(aNumPages); + } +} + +void XULTreeElement::Invalidate() { + nsTreeBodyFrame* body = GetTreeBodyFrame(); + if (body) { + body->Invalidate(); + } +} + +void XULTreeElement::InvalidateColumn(nsTreeColumn* aCol) { + nsTreeBodyFrame* body = GetTreeBodyFrame(); + if (body) { + body->InvalidateColumn(aCol); + } +} + +void XULTreeElement::InvalidateRow(int32_t aIndex) { + nsTreeBodyFrame* body = GetTreeBodyFrame(); + if (body) { + body->InvalidateRow(aIndex); + } +} + +void XULTreeElement::InvalidateCell(int32_t aRow, nsTreeColumn* aCol) { + nsTreeBodyFrame* body = GetTreeBodyFrame(); + if (body) { + body->InvalidateCell(aRow, aCol); + } +} + +void XULTreeElement::InvalidateRange(int32_t aStart, int32_t aEnd) { + nsTreeBodyFrame* body = GetTreeBodyFrame(); + if (body) { + body->InvalidateRange(aStart, aEnd); + } +} + +int32_t XULTreeElement::GetRowAt(int32_t x, int32_t y) { + nsTreeBodyFrame* body = GetTreeBodyFrame(); + if (!body) { + return 0; + } + return body->GetRowAt(x, y); +} + +void XULTreeElement::GetCellAt(int32_t aX, int32_t aY, TreeCellInfo& aRetVal, + ErrorResult& aRv) { + aRetVal.mRow = 0; + aRetVal.mCol = nullptr; + + nsTreeBodyFrame* body = GetTreeBodyFrame(); + if (body) { + nsAutoCString element; + body->GetCellAt(aX, aY, &aRetVal.mRow, getter_AddRefs(aRetVal.mCol), + element); + CopyUTF8toUTF16(element, aRetVal.mChildElt); + } +} + +nsIntRect XULTreeElement::GetCoordsForCellItem(int32_t aRow, nsTreeColumn* aCol, + const nsAString& aElement, + nsresult& rv) { + rv = NS_OK; + nsIntRect rect; + + nsTreeBodyFrame* body = GetTreeBodyFrame(); + NS_ConvertUTF16toUTF8 element(aElement); + if (body) { + rv = body->GetCoordsForCellItem(aRow, aCol, element, &rect.x, &rect.y, + &rect.width, &rect.height); + } + + return rect; +} + +already_AddRefed XULTreeElement::GetCoordsForCellItem( + int32_t aRow, nsTreeColumn& aCol, const nsAString& aElement, + ErrorResult& aRv) { + nsresult rv; + nsIntRect rect = GetCoordsForCellItem(aRow, &aCol, aElement, rv); + aRv = rv; + + RefPtr domRect = new DOMRect(ToSupports(OwnerDoc()), rect.x, rect.y, + rect.width, rect.height); + return domRect.forget(); +} + +bool XULTreeElement::IsCellCropped(int32_t aRow, nsTreeColumn* aCol, + ErrorResult& aRv) { + bool cropped = false; + + nsTreeBodyFrame* body = GetTreeBodyFrame(); + if (body) { + aRv = body->IsCellCropped(aRow, aCol, &cropped); + } + + return cropped; +} + +void XULTreeElement::RowCountChanged(int32_t aIndex, int32_t aDelta) { + nsTreeBodyFrame* body = GetTreeBodyFrame(); + if (body) { + body->RowCountChanged(aIndex, aDelta); + } +} + +void XULTreeElement::BeginUpdateBatch() { + nsTreeBodyFrame* body = GetTreeBodyFrame(); + if (body) { + body->BeginUpdateBatch(); + } +} + +void XULTreeElement::EndUpdateBatch() { + nsTreeBodyFrame* body = GetTreeBodyFrame(); + if (body) { + body->EndUpdateBatch(); + } +} + +void XULTreeElement::ClearStyleAndImageCaches() { + nsTreeBodyFrame* body = GetTreeBodyFrame(); + if (body) { + body->ClearStyleAndImageCaches(); + } +} + +void XULTreeElement::RemoveImageCacheEntry(int32_t aRowIndex, + nsTreeColumn& aCol, + ErrorResult& aRv) { + if (NS_WARN_IF(aRowIndex < 0)) { + aRv.Throw(NS_ERROR_INVALID_ARG); + return; + } + nsTreeBodyFrame* body = GetTreeBodyFrame(); + if (body) { + body->RemoveImageCacheEntry(aRowIndex, &aCol); + } +} + +} // namespace mozilla::dom diff --git a/dom/xul/XULTreeElement.h b/dom/xul/XULTreeElement.h new file mode 100644 index 0000000000..381ae88f41 --- /dev/null +++ b/dom/xul/XULTreeElement.h @@ -0,0 +1,129 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef XULTreeElement_h__ +#define XULTreeElement_h__ + +#include "mozilla/Attributes.h" +#include "nsCycleCollectionParticipant.h" +#include "nsWrapperCache.h" +#include "nsString.h" +#include "nsXULElement.h" +#include "nsITreeView.h" + +class nsTreeBodyFrame; +class nsTreeColumn; +class nsTreeColumns; + +namespace mozilla { +class ErrorResult; + +namespace dom { + +struct TreeCellInfo; +class DOMRect; +enum class CallerType : uint32_t; + +class XULTreeElement final : public nsXULElement { + public: + explicit XULTreeElement(already_AddRefed&& aNodeInfo) + : nsXULElement(std::move(aNodeInfo)), + mCachedFirstVisibleRow(0), + mTreeBody(nullptr) {} + + NS_IMPL_FROMNODE_WITH_TAG(XULTreeElement, kNameSpaceID_XUL, tree) + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(XULTreeElement, nsXULElement) + + nsTreeBodyFrame* GetTreeBodyFrame(FlushType = FlushType::Frames); + nsTreeBodyFrame* GetCachedTreeBodyFrame() { return mTreeBody; } + + already_AddRefed GetColumns(FlushType = FlushType::Frames); + + already_AddRefed GetView(CallerType /* unused */) { + return GetView(); + } + already_AddRefed GetView(FlushType = FlushType::Frames); + + void SetView(nsITreeView* arg, CallerType aCallerType, ErrorResult& aRv); + + bool Focused(); + + already_AddRefed GetTreeBody(); + + int32_t RowHeight(); + + int32_t RowWidth(); + + int32_t HorizontalPosition(); + + void EnsureCellIsVisible(int32_t row, nsTreeColumn* col, ErrorResult& aRv); + + void ScrollToRow(int32_t aRow); + + void ScrollByLines(int32_t aNumLines); + + void ScrollByPages(int32_t aNumPages); + + int32_t GetFirstVisibleRow(); + + int32_t GetLastVisibleRow(); + + int32_t GetPageLength(); + + int32_t GetRowAt(int32_t x, int32_t y); + + void GetCellAt(int32_t x, int32_t y, TreeCellInfo& aRetVal, ErrorResult& aRv); + + nsIntRect GetCoordsForCellItem(int32_t aRow, nsTreeColumn* aCol, + const nsAString& aElement, nsresult& rv); + already_AddRefed GetCoordsForCellItem(int32_t row, nsTreeColumn& col, + const nsAString& element, + ErrorResult& aRv); + + bool IsCellCropped(int32_t row, nsTreeColumn* col, ErrorResult& aRv); + + void RemoveImageCacheEntry(int32_t row, nsTreeColumn& col, ErrorResult& aRv); + + void SetFocused(bool aFocused); + void EnsureRowIsVisible(int32_t index); + void Invalidate(void); + void InvalidateColumn(nsTreeColumn* col); + void InvalidateRow(int32_t index); + void InvalidateCell(int32_t row, nsTreeColumn* col); + void InvalidateRange(int32_t startIndex, int32_t endIndex); + void RowCountChanged(int32_t index, int32_t count); + void BeginUpdateBatch(void); + void EndUpdateBatch(void); + void ClearStyleAndImageCaches(void); + + virtual void UnbindFromTree(bool aNullParent) override; + virtual void DestroyContent() override; + + void BodyDestroyed(int32_t aFirstVisibleRow) { + mTreeBody = nullptr; + mCachedFirstVisibleRow = aFirstVisibleRow; + } + + int32_t GetCachedTopVisibleRow() { return mCachedFirstVisibleRow; } + + protected: + int32_t mCachedFirstVisibleRow; + + nsTreeBodyFrame* mTreeBody; + nsCOMPtr mView; + + virtual ~XULTreeElement() = default; + + JSObject* WrapNode(JSContext* aCx, + JS::Handle aGivenProto) override; +}; + +} // namespace dom +} // namespace mozilla + +#endif diff --git a/dom/xul/crashtests/107518-1.xml b/dom/xul/crashtests/107518-1.xml new file mode 100644 index 0000000000..58e652624c --- /dev/null +++ b/dom/xul/crashtests/107518-1.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + diff --git a/dom/xul/crashtests/253479-1.xhtml b/dom/xul/crashtests/253479-1.xhtml new file mode 100644 index 0000000000..5860d2200d --- /dev/null +++ b/dom/xul/crashtests/253479-1.xhtml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/dom/xul/crashtests/253479-2.xhtml b/dom/xul/crashtests/253479-2.xhtml new file mode 100644 index 0000000000..43a8c17b84 --- /dev/null +++ b/dom/xul/crashtests/253479-2.xhtml @@ -0,0 +1,4 @@ + + + + diff --git a/dom/xul/crashtests/326204-1.xhtml b/dom/xul/crashtests/326204-1.xhtml new file mode 100644 index 0000000000..e69de29bb2 diff --git a/dom/xul/crashtests/326644-1-inner.xhtml b/dom/xul/crashtests/326644-1-inner.xhtml new file mode 100644 index 0000000000..63ed9b7212 --- /dev/null +++ b/dom/xul/crashtests/326644-1-inner.xhtml @@ -0,0 +1,34 @@ + + += 19 || aObj[i] == window.location) + continue; + setTimeout(doe,500, aObj[i], ++aNested, timers); + timers++; +} +} +catch(e){} +try { + //if (temp == 68 && aNested == 21 && aCurrentTimer >= 116) { + // alert(i + '-'+ aObj[i]); + // return; + // } + aObj[i]= i; + temp+=1; +} +catch (e) { + +} +} +} +var s=document.getElementsByTagName('window')[0]; +setTimeout(doe,100, s, 0); +]]> + diff --git a/dom/xul/crashtests/326644-1.html b/dom/xul/crashtests/326644-1.html new file mode 100644 index 0000000000..6f1d88a5eb --- /dev/null +++ b/dom/xul/crashtests/326644-1.html @@ -0,0 +1,9 @@ + + + + + + + diff --git a/dom/xul/crashtests/326875-1.xhtml b/dom/xul/crashtests/326875-1.xhtml new file mode 100644 index 0000000000..9dca9a0d53 --- /dev/null +++ b/dom/xul/crashtests/326875-1.xhtml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + diff --git a/dom/xul/crashtests/329982-1.xhtml b/dom/xul/crashtests/329982-1.xhtml new file mode 100644 index 0000000000..43f374a57e --- /dev/null +++ b/dom/xul/crashtests/329982-1.xhtml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/dom/xul/crashtests/336096-1.xhtml b/dom/xul/crashtests/336096-1.xhtml new file mode 100644 index 0000000000..e15691d88d --- /dev/null +++ b/dom/xul/crashtests/336096-1.xhtml @@ -0,0 +1,41 @@ + + + + + + + + + +
+
+ + + + + + +
+ + + diff --git a/dom/xul/crashtests/344215-1.xhtml b/dom/xul/crashtests/344215-1.xhtml new file mode 100644 index 0000000000..6443c22d6c --- /dev/null +++ b/dom/xul/crashtests/344215-1.xhtml @@ -0,0 +1,7 @@ + + + + +You should not see any assertions in a debug build. + + \ No newline at end of file diff --git a/dom/xul/crashtests/354611-1.html b/dom/xul/crashtests/354611-1.html new file mode 100644 index 0000000000..fe25de3660 --- /dev/null +++ b/dom/xul/crashtests/354611-1.html @@ -0,0 +1,20 @@ + + + + + + + + + \ No newline at end of file diff --git a/dom/xul/crashtests/360078-1.xhtml b/dom/xul/crashtests/360078-1.xhtml new file mode 100644 index 0000000000..a29087014d --- /dev/null +++ b/dom/xul/crashtests/360078-1.xhtml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + diff --git a/dom/xul/crashtests/363791-1.xhtml b/dom/xul/crashtests/363791-1.xhtml new file mode 100644 index 0000000000..0786960f23 --- /dev/null +++ b/dom/xul/crashtests/363791-1.xhtml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dom/xul/crashtests/384877-1-inner.xhtml b/dom/xul/crashtests/384877-1-inner.xhtml new file mode 100644 index 0000000000..bd75aa87a7 --- /dev/null +++ b/dom/xul/crashtests/384877-1-inner.xhtml @@ -0,0 +1,12 @@ + + + diff --git a/dom/xul/crashtests/384877-1.html b/dom/xul/crashtests/384877-1.html new file mode 100644 index 0000000000..b4ecd1b5f8 --- /dev/null +++ b/dom/xul/crashtests/384877-1.html @@ -0,0 +1,9 @@ + + + + + + + diff --git a/dom/xul/crashtests/386914-1-inner.xhtml b/dom/xul/crashtests/386914-1-inner.xhtml new file mode 100644 index 0000000000..909889d8b5 --- /dev/null +++ b/dom/xul/crashtests/386914-1-inner.xhtml @@ -0,0 +1,10 @@ + + + + + +document.getElementById('b').addEventListener('DOMAttrModified', function(e) {document.removeChild(document.documentElement);}, true); +setTimeout(function() {document.getElementById('a').setAttribute('tabindex', '1') ;}, 100); + + + \ No newline at end of file diff --git a/dom/xul/crashtests/386914-1.html b/dom/xul/crashtests/386914-1.html new file mode 100644 index 0000000000..2bb1224ab7 --- /dev/null +++ b/dom/xul/crashtests/386914-1.html @@ -0,0 +1,9 @@ + + + + + + + diff --git a/dom/xul/crashtests/425821-1.xhtml b/dom/xul/crashtests/425821-1.xhtml new file mode 100644 index 0000000000..9764e64aad --- /dev/null +++ b/dom/xul/crashtests/425821-1.xhtml @@ -0,0 +1,15 @@ + + + diff --git a/dom/xul/crashtests/428951-1.xhtml b/dom/xul/crashtests/428951-1.xhtml new file mode 100644 index 0000000000..41dd353f3d --- /dev/null +++ b/dom/xul/crashtests/428951-1.xhtml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + diff --git a/dom/xul/crashtests/431906-1-inner.xhtml b/dom/xul/crashtests/431906-1-inner.xhtml new file mode 100644 index 0000000000..367f621b2f --- /dev/null +++ b/dom/xul/crashtests/431906-1-inner.xhtml @@ -0,0 +1,19 @@ + + + + + + + + + \ No newline at end of file diff --git a/dom/xul/crashtests/431906-1.html b/dom/xul/crashtests/431906-1.html new file mode 100644 index 0000000000..a8570b1bc0 --- /dev/null +++ b/dom/xul/crashtests/431906-1.html @@ -0,0 +1,9 @@ + + + + + + + diff --git a/dom/xul/crashtests/461917-1.xhtml b/dom/xul/crashtests/461917-1.xhtml new file mode 100644 index 0000000000..15792f6f0f --- /dev/null +++ b/dom/xul/crashtests/461917-1.xhtml @@ -0,0 +1,6 @@ + + + + + + diff --git a/dom/xul/crashtests/crashtests.list b/dom/xul/crashtests/crashtests.list new file mode 100644 index 0000000000..970d57b457 --- /dev/null +++ b/dom/xul/crashtests/crashtests.list @@ -0,0 +1,17 @@ +load 107518-1.xml +load chrome://reftest/content/crashtests/dom/xul/crashtests/253479-1.xhtml +load chrome://reftest/content/crashtests/dom/xul/crashtests/253479-2.xhtml +load chrome://reftest/content/crashtests/dom/xul/crashtests/326204-1.xhtml +load 326644-1.html +load chrome://reftest/content/crashtests/dom/xul/crashtests/326875-1.xhtml +load 329982-1.xhtml +load 336096-1.xhtml +load chrome://reftest/content/crashtests/dom/xul/crashtests/344215-1.xhtml +load 354611-1.html +skip-if(Android) load chrome://reftest/content/crashtests/dom/xul/crashtests/363791-1.xhtml +load 384877-1.html +load 386914-1.html +load chrome://reftest/content/crashtests/dom/xul/crashtests/425821-1.xhtml +load chrome://reftest/content/crashtests/dom/xul/crashtests/428951-1.xhtml +load 431906-1.html +load 461917-1.xhtml diff --git a/dom/xul/moz.build b/dom/xul/moz.build new file mode 100644 index 0000000000..6057c71aa5 --- /dev/null +++ b/dom/xul/moz.build @@ -0,0 +1,94 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +with Files("**"): + BUG_COMPONENT = ("Core", "XUL") + +if CONFIG["MOZ_BUILD_APP"] == "browser": + DEFINES["MOZ_BREAK_XUL_OVERLAYS"] = True + +MOCHITEST_CHROME_MANIFESTS += ["test/chrome.ini"] +MOCHITEST_MANIFESTS += ["test/mochitest.ini"] + +EXPORTS += [ + "nsXULCommandDispatcher.h", + "nsXULElement.h", + "nsXULPrototypeDocument.h", + "nsXULSortService.h", +] + +EXPORTS.mozilla.dom += [ + "XULBroadcastManager.h", + "XULButtonElement.h", + "XULFrameElement.h", + "XULMenuBarElement.h", + "XULMenuElement.h", + "XULMenuParentElement.h", + "XULPersist.h", + "XULPopupElement.h", + "XULResizerElement.h", + "XULTextElement.h", + "XULTooltipElement.h", + "XULTreeElement.h", +] + +UNIFIED_SOURCES += [ + "MenuBarListener.cpp", + "nsXULCommandDispatcher.cpp", + "nsXULContentSink.cpp", + "nsXULContentUtils.cpp", + "nsXULElement.cpp", + "nsXULPopupListener.cpp", + "nsXULPrototypeCache.cpp", + "nsXULPrototypeDocument.cpp", + "nsXULSortService.cpp", + "XULBroadcastManager.cpp", + "XULButtonElement.cpp", + "XULFrameElement.cpp", + "XULMenuBarElement.cpp", + "XULMenuElement.cpp", + "XULMenuParentElement.cpp", + "XULPersist.cpp", + "XULPopupElement.cpp", + "XULResizerElement.cpp", + "XULTextElement.cpp", + "XULTooltipElement.cpp", + "XULTreeElement.cpp", +] + +XPIDL_SOURCES += [ + "nsIBrowserController.idl", + "nsIController.idl", + "nsIControllers.idl", +] + +XPIDL_MODULE = "xul" + +EXPORTS.mozilla.dom += [ + "ChromeObserver.h", +] + + +UNIFIED_SOURCES += [ + "ChromeObserver.cpp", + "nsXULControllers.cpp", +] + +LOCAL_INCLUDES += [ + "/docshell/base", + "/dom/base", + "/dom/html", + "/dom/xml", + "/layout/base", + "/layout/generic", + "/layout/style", + "/layout/xul", + "/layout/xul/tree", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" diff --git a/dom/xul/nsIBrowserController.idl b/dom/xul/nsIBrowserController.idl new file mode 100644 index 0000000000..5a7b77529b --- /dev/null +++ b/dom/xul/nsIBrowserController.idl @@ -0,0 +1,20 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* 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 "nsISupports.idl" + +/** + * This interface is used to accompany the nsIController for a + * element. It is used to update the commands in the + * parent process when the set of child command have changed. + */ +[scriptable, uuid(5bb3d56b-e733-4a2c-8a53-058123df65e2)] +interface nsIBrowserController : nsISupports +{ + // Update the commands for a given action in the parent process. + void enableDisableCommands(in AString action, + in Array enabledCommands, + in Array disabledCommands); +}; diff --git a/dom/xul/nsIController.idl b/dom/xul/nsIController.idl new file mode 100644 index 0000000000..c3249c5749 --- /dev/null +++ b/dom/xul/nsIController.idl @@ -0,0 +1,39 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* 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 "nsISupports.idl" + +[scriptable, uuid(D5B61B82-1DA4-11d3-BF87-00105A1B0627)] +interface nsIController : nsISupports { + boolean isCommandEnabled(in string command); + boolean supportsCommand(in string command); + + [can_run_script] + void doCommand(in string command); + + void onEvent(in string eventName); +}; + + +/* + + Enhanced controller interface that allows for passing of parameters + to commands. + +*/ + +interface nsICommandParams; + +[scriptable, uuid(EEC0B435-7F53-44FE-B00A-CF3EED65C01A)] +interface nsICommandController : nsISupports +{ + + void getCommandStateWithParams( in string command, in nsICommandParams aCommandParams); + + [can_run_script] + void doCommandWithParams(in string command, in nsICommandParams aCommandParams); + + Array getSupportedCommands(); +}; diff --git a/dom/xul/nsIControllers.idl b/dom/xul/nsIControllers.idl new file mode 100644 index 0000000000..2f272faec1 --- /dev/null +++ b/dom/xul/nsIControllers.idl @@ -0,0 +1,34 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* 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 "nsISupports.idl" + +interface nsIController; +interface nsIDOMXULCommandDispatcher; + +[scriptable, uuid(f36e3ec1-9197-4ad8-8d4c-d3b1927fd6df)] +interface nsIControllers : nsISupports +{ + nsIController getControllerForCommand(in string command); + + void insertControllerAt(in unsigned long index, in nsIController controller); + nsIController removeControllerAt(in unsigned long index); + nsIController getControllerAt(in unsigned long index); + + void appendController(in nsIController controller); + void removeController(in nsIController controller); + + /* + Return an ID for this controller which is unique to this + nsIControllers. + */ + unsigned long getControllerId(in nsIController controller); + /* + Get the controller specified by the given ID. + */ + nsIController getControllerById(in unsigned long controllerID); + + unsigned long getControllerCount(); +}; diff --git a/dom/xul/nsXULCommandDispatcher.cpp b/dom/xul/nsXULCommandDispatcher.cpp new file mode 100644 index 0000000000..0b07086914 --- /dev/null +++ b/dom/xul/nsXULCommandDispatcher.cpp @@ -0,0 +1,435 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 sw=2 et tw=80: */ +/* 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/. */ + +/* + + This file provides the implementation for the XUL Command Dispatcher. + + */ + +#include "nsIContent.h" +#include "nsFocusManager.h" +#include "nsIControllers.h" +#include "mozilla/dom/Document.h" +#include "nsPresContext.h" +#include "nsIScriptGlobalObject.h" +#include "nsPIDOMWindow.h" +#include "nsPIWindowRoot.h" +#include "nsXULCommandDispatcher.h" +#include "mozilla/Logging.h" +#include "nsContentUtils.h" +#include "nsReadableUtils.h" +#include "nsCRT.h" +#include "nsError.h" +#include "mozilla/BasicEvents.h" +#include "mozilla/EventDispatcher.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/ElementBinding.h" + +using namespace mozilla; +using mozilla::dom::Document; +using mozilla::dom::Element; + +#ifdef DEBUG +static LazyLogModule gCommandLog("nsXULCommandDispatcher"); +#endif + +//////////////////////////////////////////////////////////////////////// + +nsXULCommandDispatcher::nsXULCommandDispatcher(Document* aDocument) + : mDocument(aDocument), mUpdaters(nullptr), mLocked(false) {} + +nsXULCommandDispatcher::~nsXULCommandDispatcher() { Disconnect(); } + +// QueryInterface implementation for nsXULCommandDispatcher + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsXULCommandDispatcher) + NS_INTERFACE_MAP_ENTRY(nsIDOMXULCommandDispatcher) + NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIDOMXULCommandDispatcher) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(nsXULCommandDispatcher) +NS_IMPL_CYCLE_COLLECTING_RELEASE(nsXULCommandDispatcher) + +NS_IMPL_CYCLE_COLLECTION_CLASS(nsXULCommandDispatcher) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(nsXULCommandDispatcher) + tmp->Disconnect(); + NS_IMPL_CYCLE_COLLECTION_UNLINK_WEAK_REFERENCE +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(nsXULCommandDispatcher) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocument) + Updater* updater = tmp->mUpdaters; + while (updater) { + cb.NoteXPCOMChild(updater->mElement); + updater = updater->mNext; + } +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +void nsXULCommandDispatcher::Disconnect() { + while (mUpdaters) { + Updater* doomed = mUpdaters; + mUpdaters = mUpdaters->mNext; + delete doomed; + } + mDocument = nullptr; +} + +already_AddRefed nsXULCommandDispatcher::GetWindowRoot() { + if (mDocument) { + if (nsCOMPtr window = mDocument->GetWindow()) { + return window->GetTopWindowRoot(); + } + } + + return nullptr; +} + +Element* nsXULCommandDispatcher::GetRootFocusedContentAndWindow( + nsPIDOMWindowOuter** aWindow) { + *aWindow = nullptr; + + if (!mDocument) { + return nullptr; + } + + if (nsCOMPtr win = mDocument->GetWindow()) { + if (nsCOMPtr rootWindow = win->GetPrivateRoot()) { + return nsFocusManager::GetFocusedDescendant( + rootWindow, nsFocusManager::eIncludeAllDescendants, aWindow); + } + } + + return nullptr; +} + +NS_IMETHODIMP +nsXULCommandDispatcher::GetFocusedElement(Element** aElement) { + *aElement = nullptr; + + nsCOMPtr focusedWindow; + RefPtr focusedContent = + GetRootFocusedContentAndWindow(getter_AddRefs(focusedWindow)); + if (focusedContent) { + // Make sure the caller can access the focused element. + if (!nsContentUtils::SubjectPrincipalOrSystemIfNativeCaller()->Subsumes( + focusedContent->NodePrincipal())) { + // XXX This might want to return null, but we use that return value + // to mean "there is no focused element," so to be clear, throw an + // exception. + return NS_ERROR_DOM_SECURITY_ERR; + } + } + + focusedContent.forget(aElement); + return NS_OK; +} + +NS_IMETHODIMP +nsXULCommandDispatcher::GetFocusedWindow(mozIDOMWindowProxy** aWindow) { + *aWindow = nullptr; + + nsCOMPtr window; + GetRootFocusedContentAndWindow(getter_AddRefs(window)); + if (!window) return NS_OK; + + // Make sure the caller can access this window. The caller can access this + // window iff it can access the document. + nsCOMPtr doc = window->GetDoc(); + + // Note: If there is no document, then this window has been cleared and + // there's nothing left to protect, so let the window pass through. + if (doc && !nsContentUtils::CanCallerAccess(doc)) + return NS_ERROR_DOM_SECURITY_ERR; + + window.forget(aWindow); + return NS_OK; +} + +NS_IMETHODIMP +nsXULCommandDispatcher::SetFocusedElement(Element* aElement) { + RefPtr fm = nsFocusManager::GetFocusManager(); + NS_ENSURE_TRUE(fm, NS_ERROR_FAILURE); + + if (aElement) { + return fm->SetFocus(aElement, 0); + } + + // if aElement is null, clear the focus in the currently focused child window + nsCOMPtr focusedWindow; + GetRootFocusedContentAndWindow(getter_AddRefs(focusedWindow)); + return fm->ClearFocus(focusedWindow); +} + +NS_IMETHODIMP +nsXULCommandDispatcher::SetFocusedWindow(mozIDOMWindowProxy* aWindow) { + NS_ENSURE_TRUE(aWindow, NS_OK); // do nothing if set to null + + nsCOMPtr window = nsPIDOMWindowOuter::From(aWindow); + NS_ENSURE_TRUE(window, NS_ERROR_FAILURE); + + RefPtr fm = nsFocusManager::GetFocusManager(); + NS_ENSURE_TRUE(fm, NS_ERROR_FAILURE); + + // get the containing frame for the window, and set it as focused. This will + // end up focusing whatever is currently focused inside the frame. Since + // setting the command dispatcher's focused window doesn't raise the window, + // setting it to a top-level window doesn't need to do anything. + RefPtr frameElement = window->GetFrameElementInternal(); + if (frameElement) { + return fm->SetFocus(frameElement, 0); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsXULCommandDispatcher::AdvanceFocus() { + return AdvanceFocusIntoSubtree(nullptr); +} + +NS_IMETHODIMP +nsXULCommandDispatcher::AdvanceFocusIntoSubtree(Element* aElt) { + return MoveFocusIntoSubtree(aElt, /* aForward = */ true); +} + +NS_IMETHODIMP +nsXULCommandDispatcher::RewindFocus() { + return MoveFocusIntoSubtree(nullptr, /* aForward = */ false); +} + +nsresult nsXULCommandDispatcher::MoveFocusIntoSubtree(Element* aElt, + bool aForward) { + nsCOMPtr win; + GetRootFocusedContentAndWindow(getter_AddRefs(win)); + + RefPtr result; + nsFocusManager* fm = nsFocusManager::GetFocusManager(); + if (!fm) { + return NS_OK; + } + auto flags = nsFocusManager::ProgrammaticFocusFlags(dom::FocusOptions()) | + nsIFocusManager::FLAG_BYMOVEFOCUS; + auto type = aForward ? nsIFocusManager::MOVEFOCUS_FORWARD + : nsIFocusManager::MOVEFOCUS_BACKWARD; + return fm->MoveFocus(win, aElt, type, flags, getter_AddRefs(result)); +} + +NS_IMETHODIMP +nsXULCommandDispatcher::AddCommandUpdater(Element* aElement, + const nsAString& aEvents, + const nsAString& aTargets) { + MOZ_ASSERT(aElement != nullptr, "null ptr"); + if (!aElement) return NS_ERROR_NULL_POINTER; + + NS_ENSURE_TRUE(mDocument, NS_ERROR_UNEXPECTED); + + nsresult rv = nsContentUtils::CheckSameOrigin(mDocument, aElement); + + if (NS_FAILED(rv)) { + return rv; + } + + Updater* updater = mUpdaters; + Updater** link = &mUpdaters; + + while (updater) { + if (updater->mElement == aElement) { +#ifdef DEBUG + if (MOZ_LOG_TEST(gCommandLog, LogLevel::Debug)) { + nsAutoCString eventsC, targetsC, aeventsC, atargetsC; + LossyCopyUTF16toASCII(updater->mEvents, eventsC); + LossyCopyUTF16toASCII(updater->mTargets, targetsC); + CopyUTF16toUTF8(aEvents, aeventsC); + CopyUTF16toUTF8(aTargets, atargetsC); + MOZ_LOG(gCommandLog, LogLevel::Debug, + ("xulcmd[%p] replace %p(events=%s targets=%s) with (events=%s " + "targets=%s)", + this, aElement, eventsC.get(), targetsC.get(), aeventsC.get(), + atargetsC.get())); + } +#endif + + // If the updater was already in the list, then replace + // (?) the 'events' and 'targets' filters with the new + // specification. + updater->mEvents = aEvents; + updater->mTargets = aTargets; + return NS_OK; + } + + link = &(updater->mNext); + updater = updater->mNext; + } +#ifdef DEBUG + if (MOZ_LOG_TEST(gCommandLog, LogLevel::Debug)) { + nsAutoCString aeventsC, atargetsC; + CopyUTF16toUTF8(aEvents, aeventsC); + CopyUTF16toUTF8(aTargets, atargetsC); + + MOZ_LOG(gCommandLog, LogLevel::Debug, + ("xulcmd[%p] add %p(events=%s targets=%s)", this, aElement, + aeventsC.get(), atargetsC.get())); + } +#endif + + // If we get here, this is a new updater. Append it to the list. + *link = new Updater(aElement, aEvents, aTargets); + return NS_OK; +} + +NS_IMETHODIMP +nsXULCommandDispatcher::RemoveCommandUpdater(Element* aElement) { + MOZ_ASSERT(aElement != nullptr, "null ptr"); + if (!aElement) return NS_ERROR_NULL_POINTER; + + Updater* updater = mUpdaters; + Updater** link = &mUpdaters; + + while (updater) { + if (updater->mElement == aElement) { +#ifdef DEBUG + if (MOZ_LOG_TEST(gCommandLog, LogLevel::Debug)) { + nsAutoCString eventsC, targetsC; + LossyCopyUTF16toASCII(updater->mEvents, eventsC); + LossyCopyUTF16toASCII(updater->mTargets, targetsC); + MOZ_LOG(gCommandLog, LogLevel::Debug, + ("xulcmd[%p] remove %p(events=%s targets=%s)", this, aElement, + eventsC.get(), targetsC.get())); + } +#endif + + *link = updater->mNext; + delete updater; + return NS_OK; + } + + link = &(updater->mNext); + updater = updater->mNext; + } + + // Hmm. Not found. Oh well. + return NS_OK; +} + +NS_IMETHODIMP +nsXULCommandDispatcher::UpdateCommands(const nsAString& aEventName) { + if (mLocked) { + if (!mPendingUpdates.Contains(aEventName)) { + mPendingUpdates.AppendElement(aEventName); + } + + return NS_OK; + } + + nsAutoString id; + RefPtr element; + GetFocusedElement(getter_AddRefs(element)); + if (element) { + element->GetAttr(nsGkAtoms::id, id); + } + + nsCOMArray updaters; + + for (Updater* updater = mUpdaters; updater != nullptr; + updater = updater->mNext) { + // Skip any nodes that don't match our 'events' or 'targets' + // filters. + if (!Matches(updater->mEvents, aEventName)) continue; + + if (!Matches(updater->mTargets, id)) continue; + + nsIContent* content = updater->mElement; + NS_ASSERTION(content != nullptr, "mElement is null"); + if (!content) return NS_ERROR_UNEXPECTED; + + updaters.AppendObject(content); + } + + for (nsIContent* content : updaters) { +#ifdef DEBUG + if (MOZ_LOG_TEST(gCommandLog, LogLevel::Debug)) { + nsAutoCString aeventnameC; + CopyUTF16toUTF8(aEventName, aeventnameC); + MOZ_LOG( + gCommandLog, LogLevel::Debug, + ("xulcmd[%p] update %p event=%s", this, content, aeventnameC.get())); + } +#endif + + WidgetEvent event(true, eXULCommandUpdate); + EventDispatcher::Dispatch(MOZ_KnownLive(content), nullptr, &event); + } + return NS_OK; +} + +bool nsXULCommandDispatcher::Matches(const nsString& aList, + const nsAString& aElement) { + if (aList.EqualsLiteral("*")) return true; // match _everything_! + + int32_t indx = aList.Find(PromiseFlatString(aElement)); + if (indx == -1) return false; // not in the list at all + + // okay, now make sure it's not a substring snafu; e.g., 'ur' + // found inside of 'blur'. + if (indx > 0) { + char16_t ch = aList[indx - 1]; + if (!nsCRT::IsAsciiSpace(ch) && ch != char16_t(',')) return false; + } + + if (indx + aElement.Length() < aList.Length()) { + char16_t ch = aList[indx + aElement.Length()]; + if (!nsCRT::IsAsciiSpace(ch) && ch != char16_t(',')) return false; + } + + return true; +} + +NS_IMETHODIMP +nsXULCommandDispatcher::GetControllers(nsIControllers** aResult) { + nsCOMPtr root = GetWindowRoot(); + NS_ENSURE_TRUE(root, NS_ERROR_FAILURE); + + return root->GetControllers(false /* for any window */, aResult); +} + +NS_IMETHODIMP +nsXULCommandDispatcher::GetControllerForCommand(const char* aCommand, + nsIController** _retval) { + nsCOMPtr root = GetWindowRoot(); + NS_ENSURE_TRUE(root, NS_ERROR_FAILURE); + + return root->GetControllerForCommand(aCommand, false /* for any window */, + _retval); +} + +NS_IMETHODIMP +nsXULCommandDispatcher::Lock() { + // Since locking is used only as a performance optimization, we don't worry + // about nested lock calls. If that does happen, it just means we will unlock + // and process updates earlier. + mLocked = true; + return NS_OK; +} + +// TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230) +MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHODIMP nsXULCommandDispatcher::Unlock() { + if (mLocked) { + mLocked = false; + + // Handle any pending updates one at a time. In the unlikely case where a + // lock is added during the update, break out. + while (!mLocked && mPendingUpdates.Length() > 0) { + nsString name = mPendingUpdates.ElementAt(0); + mPendingUpdates.RemoveElementAt(0); + UpdateCommands(name); + } + } + + return NS_OK; +} diff --git a/dom/xul/nsXULCommandDispatcher.h b/dom/xul/nsXULCommandDispatcher.h new file mode 100644 index 0000000000..1f988c752e --- /dev/null +++ b/dom/xul/nsXULCommandDispatcher.h @@ -0,0 +1,82 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +/* + + This is the focus manager for XUL documents. + +*/ + +#ifndef nsXULCommandDispatcher_h__ +#define nsXULCommandDispatcher_h__ + +#include "nsCOMPtr.h" +#include "nsIDOMXULCommandDispatcher.h" +#include "nsWeakReference.h" +#include "nsString.h" +#include "nsCycleCollectionParticipant.h" +#include "nsTArray.h" +#include "mozilla/RefPtr.h" + +class nsPIDOMWindowOuter; +class nsPIWindowRoot; + +namespace mozilla::dom { +class Document; +class Element; +} // namespace mozilla::dom + +class nsXULCommandDispatcher : public nsIDOMXULCommandDispatcher, + public nsSupportsWeakReference { + using Document = mozilla::dom::Document; + using Element = mozilla::dom::Element; + + public: + explicit nsXULCommandDispatcher(Document* aDocument); + + // nsISupports + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(nsXULCommandDispatcher, + nsIDOMXULCommandDispatcher) + + // nsIDOMXULCommandDispatcher interface + NS_DECL_NSIDOMXULCOMMANDDISPATCHER + + void Disconnect(); + + protected: + virtual ~nsXULCommandDispatcher(); + + already_AddRefed GetWindowRoot(); + + Element* GetRootFocusedContentAndWindow(nsPIDOMWindowOuter** aWindow); + nsresult MoveFocusIntoSubtree(Element*, bool aForward); + + RefPtr mDocument; + + class Updater { + public: + Updater(Element* aElement, const nsAString& aEvents, + const nsAString& aTargets) + : mElement(aElement), + mEvents(aEvents), + mTargets(aTargets), + mNext(nullptr) {} + + RefPtr mElement; + nsString mEvents; + nsString mTargets; + Updater* mNext; + }; + + Updater* mUpdaters; + + bool Matches(const nsString& aList, const nsAString& aElement); + + bool mLocked; + nsTArray mPendingUpdates; +}; + +#endif // nsXULCommandDispatcher_h__ diff --git a/dom/xul/nsXULContentSink.cpp b/dom/xul/nsXULContentSink.cpp new file mode 100644 index 0000000000..a4c15c63cd --- /dev/null +++ b/dom/xul/nsXULContentSink.cpp @@ -0,0 +1,869 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +/* + * An implementation for a Gecko-style content sink that knows how + * to build a content model (the "prototype" document) from XUL. + * + * For more information on XUL, + * see http://developer.mozilla.org/en/docs/XUL + */ + +#include "nsXULContentSink.h" + +#include "jsfriendapi.h" + +#include "nsCOMPtr.h" +#include "nsHTMLStyleSheet.h" +#include "nsIContentSink.h" +#include "mozilla/dom/Document.h" +#include "nsIFormControl.h" +#include "mozilla/dom/NodeInfo.h" +#include "nsIScriptContext.h" +#include "nsIScriptGlobalObject.h" +#include "nsNameSpaceManager.h" +#include "nsParserBase.h" +#include "nsViewManager.h" +#include "nsIScriptSecurityManager.h" +#include "nsLayoutCID.h" +#include "nsNetUtil.h" +#include "nsString.h" +#include "nsReadableUtils.h" +#include "nsXULElement.h" +#include "mozilla/Logging.h" +#include "nsCRT.h" + +#include "nsXULPrototypeDocument.h" // XXXbe temporary +#include "mozilla/css/Loader.h" + +#include "nsUnicharUtils.h" +#include "nsGkAtoms.h" +#include "nsContentUtils.h" +#include "nsAttrName.h" +#include "nsXMLContentSink.h" +#include "nsIScriptError.h" +#include "nsContentTypeParser.h" + +static mozilla::LazyLogModule gContentSinkLog("nsXULContentSink"); + +using namespace mozilla; +using namespace mozilla::dom; +//---------------------------------------------------------------------- + +XULContentSinkImpl::ContextStack::ContextStack() : mTop(nullptr), mDepth(0) {} + +XULContentSinkImpl::ContextStack::~ContextStack() { + while (mTop) { + Entry* doomed = mTop; + mTop = mTop->mNext; + delete doomed; + } +} + +void XULContentSinkImpl::ContextStack::Push(RefPtr&& aNode, + State aState) { + mTop = new Entry(std::move(aNode), aState, mTop); + ++mDepth; +} + +nsresult XULContentSinkImpl::ContextStack::Pop(State* aState) { + if (mDepth == 0) return NS_ERROR_UNEXPECTED; + + Entry* entry = mTop; + mTop = mTop->mNext; + --mDepth; + + *aState = entry->mState; + delete entry; + + return NS_OK; +} + +nsresult XULContentSinkImpl::ContextStack::GetTopNode( + RefPtr& aNode) { + if (mDepth == 0) return NS_ERROR_UNEXPECTED; + + aNode = mTop->mNode; + return NS_OK; +} + +nsresult XULContentSinkImpl::ContextStack::GetTopChildren( + nsPrototypeArray** aChildren) { + if (mDepth == 0) return NS_ERROR_UNEXPECTED; + + *aChildren = &(mTop->mChildren); + return NS_OK; +} + +void XULContentSinkImpl::ContextStack::Clear() { + Entry* cur = mTop; + while (cur) { + // Release the root element (and its descendants). + Entry* next = cur->mNext; + delete cur; + cur = next; + } + + mTop = nullptr; + mDepth = 0; +} + +void XULContentSinkImpl::ContextStack::Traverse( + nsCycleCollectionTraversalCallback& aCb) { + nsCycleCollectionTraversalCallback& cb = aCb; + for (ContextStack::Entry* tmp = mTop; tmp; tmp = tmp->mNext) { + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mNode) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mChildren) + } +} + +//---------------------------------------------------------------------- + +XULContentSinkImpl::XULContentSinkImpl() + : mText(nullptr), + mTextLength(0), + mTextSize(0), + mConstrainSize(true), + mState(eInProlog) {} + +XULContentSinkImpl::~XULContentSinkImpl() { + // The context stack _should_ be empty, unless something has gone wrong. + NS_ASSERTION(mContextStack.Depth() == 0, "Context stack not empty?"); + mContextStack.Clear(); + + free(mText); +} + +//---------------------------------------------------------------------- +// nsISupports interface + +NS_IMPL_CYCLE_COLLECTION_CLASS(XULContentSinkImpl) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(XULContentSinkImpl) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mNodeInfoManager) + tmp->mContextStack.Clear(); + NS_IMPL_CYCLE_COLLECTION_UNLINK(mPrototype) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mParser) +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(XULContentSinkImpl) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mNodeInfoManager) + tmp->mContextStack.Traverse(cb); + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPrototype) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mParser) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(XULContentSinkImpl) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIXMLContentSink) + NS_INTERFACE_MAP_ENTRY(nsIXMLContentSink) + NS_INTERFACE_MAP_ENTRY(nsIExpatSink) + NS_INTERFACE_MAP_ENTRY(nsIContentSink) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(XULContentSinkImpl) +NS_IMPL_CYCLE_COLLECTING_RELEASE(XULContentSinkImpl) + +//---------------------------------------------------------------------- +// nsIContentSink interface + +NS_IMETHODIMP +XULContentSinkImpl::DidBuildModel(bool aTerminated) { + nsCOMPtr doc = do_QueryReferent(mDocument); + if (doc) { + mPrototype->NotifyLoadDone(); + mDocument = nullptr; + } + + // Drop our reference to the parser to get rid of a circular + // reference. + mParser = nullptr; + return NS_OK; +} + +NS_IMETHODIMP +XULContentSinkImpl::WillInterrupt(void) { + // XXX Notify the docshell, if necessary + return NS_OK; +} + +void XULContentSinkImpl::WillResume() { + // XXX Notify the docshell, if necessary +} + +NS_IMETHODIMP +XULContentSinkImpl::SetParser(nsParserBase* aParser) { + mParser = aParser; + return NS_OK; +} + +void XULContentSinkImpl::SetDocumentCharset( + NotNull aEncoding) { + nsCOMPtr doc = do_QueryReferent(mDocument); + if (doc) { + doc->SetDocumentCharacterSet(aEncoding); + } +} + +nsISupports* XULContentSinkImpl::GetTarget() { + nsCOMPtr doc = do_QueryReferent(mDocument); + return ToSupports(doc); +} + +//---------------------------------------------------------------------- + +nsresult XULContentSinkImpl::Init(Document* aDocument, + nsXULPrototypeDocument* aPrototype) { + MOZ_ASSERT(aDocument != nullptr, "null ptr"); + if (!aDocument) return NS_ERROR_NULL_POINTER; + + mDocument = do_GetWeakReference(aDocument); + mPrototype = aPrototype; + + mDocumentURL = mPrototype->GetURI(); + mNodeInfoManager = aPrototype->GetNodeInfoManager(); + if (!mNodeInfoManager) return NS_ERROR_UNEXPECTED; + + mState = eInProlog; + return NS_OK; +} + +//---------------------------------------------------------------------- +// +// Text buffering +// + +bool XULContentSinkImpl::IsDataInBuffer(char16_t* buffer, int32_t length) { + for (int32_t i = 0; i < length; ++i) { + if (buffer[i] == ' ' || buffer[i] == '\t' || buffer[i] == '\n' || + buffer[i] == '\r') + continue; + + return true; + } + return false; +} + +nsresult XULContentSinkImpl::FlushText(bool aCreateTextNode) { + nsresult rv; + + do { + // Don't do anything if there's no text to create a node from, or + // if they've told us not to create a text node + if (!mTextLength) break; + + if (!aCreateTextNode) break; + + RefPtr node; + rv = mContextStack.GetTopNode(node); + if (NS_FAILED(rv)) return rv; + + bool stripWhitespace = false; + if (node->mType == nsXULPrototypeNode::eType_Element) { + mozilla::dom::NodeInfo* nodeInfo = + static_cast(node.get())->mNodeInfo; + + if (nodeInfo->NamespaceEquals(kNameSpaceID_XUL)) + stripWhitespace = !nodeInfo->Equals(nsGkAtoms::label) && + !nodeInfo->Equals(nsGkAtoms::description); + } + + // Don't bother if there's nothing but whitespace. + if (stripWhitespace && !IsDataInBuffer(mText, mTextLength)) break; + + // Don't bother if we're not in XUL document body + if (mState != eInDocumentElement || mContextStack.Depth() == 0) break; + + RefPtr text = new nsXULPrototypeText(); + text->mValue.Assign(mText, mTextLength); + if (stripWhitespace) text->mValue.Trim(" \t\n\r"); + + // hook it up + nsPrototypeArray* children = nullptr; + rv = mContextStack.GetTopChildren(&children); + if (NS_FAILED(rv)) return rv; + + children->AppendElement(text.forget()); + } while (0); + + // Reset our text buffer + mTextLength = 0; + return NS_OK; +} + +//---------------------------------------------------------------------- + +nsresult XULContentSinkImpl::NormalizeAttributeString( + const char16_t* aExpatName, nsAttrName& aName) { + int32_t nameSpaceID; + RefPtr prefix, localName; + nsContentUtils::SplitExpatName(aExpatName, getter_AddRefs(prefix), + getter_AddRefs(localName), &nameSpaceID); + + if (nameSpaceID == kNameSpaceID_None) { + aName.SetTo(localName); + + return NS_OK; + } + + RefPtr ni; + ni = mNodeInfoManager->GetNodeInfo(localName, prefix, nameSpaceID, + nsINode::ATTRIBUTE_NODE); + aName.SetTo(ni); + + return NS_OK; +} + +/**** BEGIN NEW APIs ****/ + +NS_IMETHODIMP +XULContentSinkImpl::HandleStartElement(const char16_t* aName, + const char16_t** aAtts, + uint32_t aAttsCount, + uint32_t aLineNumber, + uint32_t aColumnNumber) { + // XXX Hopefully the parser will flag this before we get here. If + // we're in the epilog, there should be no new elements + MOZ_ASSERT(mState != eInEpilog, "tag in XUL doc epilog"); + MOZ_ASSERT(aAttsCount % 2 == 0, "incorrect aAttsCount"); + + // Adjust aAttsCount so it's the actual number of attributes + aAttsCount /= 2; + + if (mState == eInEpilog) return NS_ERROR_UNEXPECTED; + + if (mState != eInScript) { + FlushText(); + } + + int32_t nameSpaceID; + RefPtr prefix, localName; + nsContentUtils::SplitExpatName(aName, getter_AddRefs(prefix), + getter_AddRefs(localName), &nameSpaceID); + + RefPtr nodeInfo; + nodeInfo = mNodeInfoManager->GetNodeInfo(localName, prefix, nameSpaceID, + nsINode::ELEMENT_NODE); + + nsresult rv = NS_OK; + switch (mState) { + case eInProlog: + // We're the root document element + rv = OpenRoot(aAtts, aAttsCount, nodeInfo); + break; + + case eInDocumentElement: + rv = OpenTag(aAtts, aAttsCount, aLineNumber, nodeInfo); + break; + + case eInEpilog: + case eInScript: + MOZ_LOG( + gContentSinkLog, LogLevel::Warning, + ("xul: warning: unexpected tags in epilog at line %d", aLineNumber)); + rv = NS_ERROR_UNEXPECTED; // XXX + break; + } + + return rv; +} + +NS_IMETHODIMP +XULContentSinkImpl::HandleEndElement(const char16_t* aName) { + // Never EVER return anything but NS_OK or + // NS_ERROR_HTMLPARSER_BLOCK from this method. Doing so will blow + // the parser's little mind all over the planet. + nsresult rv; + + RefPtr node; + rv = mContextStack.GetTopNode(node); + + if (NS_FAILED(rv)) { + return NS_OK; + } + + switch (node->mType) { + case nsXULPrototypeNode::eType_Element: { + // Flush any text _now_, so that we'll get text nodes created + // before popping the stack. + FlushText(); + + // Pop the context stack and do prototype hookup. + nsPrototypeArray* children = nullptr; + rv = mContextStack.GetTopChildren(&children); + if (NS_FAILED(rv)) return rv; + + nsXULPrototypeElement* element = + static_cast(node.get()); + + int32_t count = children->Length(); + if (count) { + element->mChildren.SetCapacity(count); + + for (int32_t i = 0; i < count; ++i) + element->mChildren.AppendElement(children->ElementAt(i)); + } + } break; + + case nsXULPrototypeNode::eType_Script: { + nsXULPrototypeScript* script = + static_cast(node.get()); + + // If given a src= attribute, we must ignore script tag content. + if (!script->mSrcURI && !script->HasStencil()) { + nsCOMPtr doc = do_QueryReferent(mDocument); + + script->mOutOfLine = false; + if (doc) { + script->Compile(mText, mTextLength, JS::SourceOwnership::Borrowed, + mDocumentURL, script->mLineNo, doc); + } + } + + FlushText(false); + } break; + + default: + NS_ERROR("didn't expect that"); + break; + } + + rv = mContextStack.Pop(&mState); + NS_ASSERTION(NS_SUCCEEDED(rv), "context stack corrupted"); + if (NS_FAILED(rv)) return rv; + + if (mContextStack.Depth() == 0) { + // The root element should -always- be an element, because + // it'll have been created via XULContentSinkImpl::OpenRoot(). + NS_ASSERTION(node->mType == nsXULPrototypeNode::eType_Element, + "root is not an element"); + if (node->mType != nsXULPrototypeNode::eType_Element) + return NS_ERROR_UNEXPECTED; + + // Now that we're done parsing, set the prototype document's + // root element. This transfers ownership of the prototype + // element tree to the prototype document. + nsXULPrototypeElement* element = + static_cast(node.get()); + + mPrototype->SetRootElement(element); + mState = eInEpilog; + } + + return NS_OK; +} + +NS_IMETHODIMP +XULContentSinkImpl::HandleComment(const char16_t* aName) { + FlushText(); + return NS_OK; +} + +NS_IMETHODIMP +XULContentSinkImpl::HandleCDataSection(const char16_t* aData, + uint32_t aLength) { + FlushText(); + return AddText(aData, aLength); +} + +NS_IMETHODIMP +XULContentSinkImpl::HandleDoctypeDecl(const nsAString& aSubset, + const nsAString& aName, + const nsAString& aSystemId, + const nsAString& aPublicId, + nsISupports* aCatalogData) { + return NS_OK; +} + +NS_IMETHODIMP +XULContentSinkImpl::HandleCharacterData(const char16_t* aData, + uint32_t aLength) { + if (aData && mState != eInProlog && mState != eInEpilog) { + return AddText(aData, aLength); + } + return NS_OK; +} + +NS_IMETHODIMP +XULContentSinkImpl::HandleProcessingInstruction(const char16_t* aTarget, + const char16_t* aData) { + FlushText(); + + const nsDependentString target(aTarget); + const nsDependentString data(aData); + + // Note: the created nsXULPrototypePI has mRefCnt == 1 + RefPtr pi = new nsXULPrototypePI(); + pi->mTarget = target; + pi->mData = data; + + if (mState == eInProlog) { + // Note: passing in already addrefed pi + return mPrototype->AddProcessingInstruction(pi); + } + + nsresult rv; + nsPrototypeArray* children = nullptr; + rv = mContextStack.GetTopChildren(&children); + if (NS_FAILED(rv)) { + return rv; + } + + // XXX(Bug 1631371) Check if this should use a fallible operation as it + // pretended earlier. + children->AppendElement(pi); + + return NS_OK; +} + +NS_IMETHODIMP +XULContentSinkImpl::HandleXMLDeclaration(const char16_t* aVersion, + const char16_t* aEncoding, + int32_t aStandalone) { + return NS_OK; +} + +NS_IMETHODIMP +XULContentSinkImpl::ReportError(const char16_t* aErrorText, + const char16_t* aSourceText, + nsIScriptError* aError, bool* _retval) { + MOZ_ASSERT(aError && aSourceText && aErrorText, "Check arguments!!!"); + + // The expat driver should report the error. + *_retval = true; + + nsresult rv = NS_OK; + + // make sure to empty the context stack so that + // could become the root element. + mContextStack.Clear(); + + mState = eInProlog; + + // Clear any buffered-up text we have. It's enough to set the length to 0. + // The buffer itself is allocated when we're created and deleted in our + // destructor, so don't mess with it. + mTextLength = 0; + + // return leaving the document empty if we're asked to not add a + // root node + nsCOMPtr idoc = do_QueryReferent(mDocument); + if (idoc && idoc->SuppressParserErrorElement()) { + return NS_OK; + }; + + const char16_t* noAtts[] = {0, 0}; + + constexpr auto errorNs = + u"http://www.mozilla.org/newlayout/xml/parsererror.xml"_ns; + + nsAutoString parsererror(errorNs); + parsererror.Append((char16_t)0xFFFF); + parsererror.AppendLiteral("parsererror"); + + rv = HandleStartElement(parsererror.get(), noAtts, 0, 0, 0); + NS_ENSURE_SUCCESS(rv, rv); + + rv = HandleCharacterData(aErrorText, NS_strlen(aErrorText)); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoString sourcetext(errorNs); + sourcetext.Append((char16_t)0xFFFF); + sourcetext.AppendLiteral("sourcetext"); + + rv = HandleStartElement(sourcetext.get(), noAtts, 0, 0, 0); + NS_ENSURE_SUCCESS(rv, rv); + + rv = HandleCharacterData(aSourceText, NS_strlen(aSourceText)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = HandleEndElement(sourcetext.get()); + NS_ENSURE_SUCCESS(rv, rv); + + rv = HandleEndElement(parsererror.get()); + NS_ENSURE_SUCCESS(rv, rv); + + return rv; +} + +nsresult XULContentSinkImpl::OpenRoot(const char16_t** aAttributes, + const uint32_t aAttrLen, + mozilla::dom::NodeInfo* aNodeInfo) { + NS_ASSERTION(mState == eInProlog, "how'd we get here?"); + if (mState != eInProlog) return NS_ERROR_UNEXPECTED; + + if (aNodeInfo->Equals(nsGkAtoms::script, kNameSpaceID_XHTML) || + aNodeInfo->Equals(nsGkAtoms::script, kNameSpaceID_XUL)) { + MOZ_LOG(gContentSinkLog, LogLevel::Error, + ("xul: script tag not allowed as root content element")); + + return NS_ERROR_UNEXPECTED; + } + + // Create the element + RefPtr element = new nsXULPrototypeElement(aNodeInfo); + + // Add the attributes + nsresult rv = AddAttributes(aAttributes, aAttrLen, element); + if (NS_FAILED(rv)) return rv; + + // Push the element onto the context stack, so that child + // containers will hook up to us as their parent. + mContextStack.Push(std::move(element), mState); + + mState = eInDocumentElement; + return NS_OK; +} + +nsresult XULContentSinkImpl::OpenTag(const char16_t** aAttributes, + const uint32_t aAttrLen, + const uint32_t aLineNumber, + mozilla::dom::NodeInfo* aNodeInfo) { + // Create the element + RefPtr element = new nsXULPrototypeElement(aNodeInfo); + + // Link this element to its parent. + nsPrototypeArray* children = nullptr; + nsresult rv = mContextStack.GetTopChildren(&children); + if (NS_FAILED(rv)) { + return rv; + } + + // Add the attributes + rv = AddAttributes(aAttributes, aAttrLen, element); + if (NS_FAILED(rv)) return rv; + + children->AppendElement(element); + + if (aNodeInfo->Equals(nsGkAtoms::script, kNameSpaceID_XHTML) || + aNodeInfo->Equals(nsGkAtoms::script, kNameSpaceID_XUL)) { + // Do scripty things now + rv = OpenScript(aAttributes, aLineNumber); + NS_ENSURE_SUCCESS(rv, rv); + + NS_ASSERTION(mState == eInScript || mState == eInDocumentElement, + "Unexpected state"); + if (mState == eInScript) { + // OpenScript has pushed the nsPrototypeScriptElement onto the + // stack, so we're done. + return NS_OK; + } + } + + // Push the element onto the context stack, so that child + // containers will hook up to us as their parent. + mContextStack.Push(std::move(element), mState); + + mState = eInDocumentElement; + return NS_OK; +} + +nsresult XULContentSinkImpl::OpenScript(const char16_t** aAttributes, + const uint32_t aLineNumber) { + bool isJavaScript = true; + nsresult rv; + + // Look for SRC attribute and look for a LANGUAGE attribute + nsAutoString src; + while (*aAttributes) { + const nsDependentString key(aAttributes[0]); + if (key.EqualsLiteral("src")) { + src.Assign(aAttributes[1]); + } else if (key.EqualsLiteral("type")) { + nsDependentString str(aAttributes[1]); + nsContentTypeParser parser(str); + nsAutoString mimeType; + rv = parser.GetType(mimeType); + if (NS_FAILED(rv)) { + if (rv == NS_ERROR_INVALID_ARG) { + // Fail immediately rather than checking if later things + // are okay. + return NS_OK; + } + // We do want the warning here + NS_ENSURE_SUCCESS(rv, rv); + } + + // NOTE(emilio): Module scripts don't pass this test, aren't cached yet. + // If they become cached, then we need to tweak + // PrototypeDocumentContentSink and remove the special cases there. + if (nsContentUtils::IsJavascriptMIMEType(mimeType)) { + isJavaScript = true; + + // Get the version string, and ensure that JavaScript supports it. + nsAutoString versionName; + rv = parser.GetParameter("version", versionName); + + if (NS_SUCCEEDED(rv)) { + nsContentUtils::ReportToConsoleNonLocalized( + u"Versioned JavaScripts are no longer supported. " + "Please remove the version parameter."_ns, + nsIScriptError::errorFlag, "XUL Document"_ns, nullptr, + mDocumentURL, u""_ns, aLineNumber); + isJavaScript = false; + } else if (rv != NS_ERROR_INVALID_ARG) { + return rv; + } + } else { + isJavaScript = false; + } + } else if (key.EqualsLiteral("language")) { + // Language is deprecated, and the impl in ScriptLoader ignores the + // various version strings anyway. So we make no attempt to support + // languages other than JS for language= + nsAutoString lang(aAttributes[1]); + if (nsContentUtils::IsJavaScriptLanguage(lang)) { + isJavaScript = true; + } + } + aAttributes += 2; + } + + // Don't process scripts that aren't JavaScript. + if (!isJavaScript) { + return NS_OK; + } + + nsCOMPtr doc(do_QueryReferent(mDocument)); + nsCOMPtr globalObject; + if (doc) globalObject = do_QueryInterface(doc->GetWindow()); + RefPtr script = new nsXULPrototypeScript(aLineNumber); + + // If there is a SRC attribute... + if (!src.IsEmpty()) { + // Use the SRC attribute value to load the URL + rv = NS_NewURI(getter_AddRefs(script->mSrcURI), src, nullptr, mDocumentURL); + + // Check if this document is allowed to load a script from this source + // NOTE: if we ever allow scripts added via the DOM to run, we need to + // add a CheckLoadURI call for that as well. + if (NS_SUCCEEDED(rv)) { + if (!mSecMan) + mSecMan = do_GetService(NS_SCRIPTSECURITYMANAGER_CONTRACTID, &rv); + if (NS_SUCCEEDED(rv)) { + nsCOMPtr doc = do_QueryReferent(mDocument, &rv); + + if (NS_SUCCEEDED(rv)) { + rv = mSecMan->CheckLoadURIWithPrincipal( + doc->NodePrincipal(), script->mSrcURI, + nsIScriptSecurityManager::ALLOW_CHROME, doc->InnerWindowID()); + } + } + } + + if (NS_FAILED(rv)) { + return rv; + } + + // Attempt to deserialize an out-of-line script from the FastLoad + // file right away. Otherwise we'll end up reloading the script and + // corrupting the FastLoad file trying to serialize it, in the case + // where it's already there. + script->DeserializeOutOfLine(nullptr, mPrototype); + } + + nsPrototypeArray* children = nullptr; + rv = mContextStack.GetTopChildren(&children); + if (NS_FAILED(rv)) { + return rv; + } + + children->AppendElement(script); + + mConstrainSize = false; + + mContextStack.Push(script, mState); + mState = eInScript; + + return NS_OK; +} + +nsresult XULContentSinkImpl::AddAttributes(const char16_t** aAttributes, + const uint32_t aAttrLen, + nsXULPrototypeElement* aElement) { + // Add tag attributes to the element + nsresult rv; + + // Create storage for the attributes + nsXULPrototypeAttribute* attrs = nullptr; + if (aAttrLen > 0) { + attrs = aElement->mAttributes.AppendElements(aAttrLen); + } + + // Copy the attributes into the prototype + uint32_t i; + for (i = 0; i < aAttrLen; ++i) { + rv = NormalizeAttributeString(aAttributes[i * 2], attrs[i].mName); + NS_ENSURE_SUCCESS(rv, rv); + + rv = aElement->SetAttrAt(i, nsDependentString(aAttributes[i * 2 + 1]), + mDocumentURL); + NS_ENSURE_SUCCESS(rv, rv); + + if (MOZ_LOG_TEST(gContentSinkLog, LogLevel::Debug)) { + nsAutoString extraWhiteSpace; + int32_t cnt = mContextStack.Depth(); + while (--cnt >= 0) extraWhiteSpace.AppendLiteral(" "); + nsAutoString qnameC, valueC; + qnameC.Assign(aAttributes[0]); + valueC.Assign(aAttributes[1]); + MOZ_LOG(gContentSinkLog, LogLevel::Debug, + ("xul: %.5d. %s %s=%s", + -1, // XXX pass in line number + NS_ConvertUTF16toUTF8(extraWhiteSpace).get(), + NS_ConvertUTF16toUTF8(qnameC).get(), + NS_ConvertUTF16toUTF8(valueC).get())); + } + } + + return NS_OK; +} + +nsresult XULContentSinkImpl::AddText(const char16_t* aText, int32_t aLength) { + // Create buffer when we first need it + if (0 == mTextSize) { + mText = (char16_t*)malloc(sizeof(char16_t) * 4096); + if (nullptr == mText) { + return NS_ERROR_OUT_OF_MEMORY; + } + mTextSize = 4096; + } + + // Copy data from string into our buffer; flush buffer when it fills up + int32_t offset = 0; + while (0 != aLength) { + int32_t amount = mTextSize - mTextLength; + if (amount > aLength) { + amount = aLength; + } + if (0 == amount) { + if (mConstrainSize) { + nsresult rv = FlushText(); + if (NS_OK != rv) { + return rv; + } + } else { + CheckedInt32 size = mTextSize; + size += aLength; + if (!size.isValid()) { + return NS_ERROR_OUT_OF_MEMORY; + } + mTextSize = size.value(); + + mText = (char16_t*)realloc(mText, sizeof(char16_t) * mTextSize); + if (nullptr == mText) { + return NS_ERROR_OUT_OF_MEMORY; + } + } + } + memcpy(&mText[mTextLength], aText + offset, sizeof(char16_t) * amount); + + mTextLength += amount; + offset += amount; + aLength -= amount; + } + + return NS_OK; +} diff --git a/dom/xul/nsXULContentSink.h b/dom/xul/nsXULContentSink.h new file mode 100644 index 0000000000..0538c013b4 --- /dev/null +++ b/dom/xul/nsXULContentSink.h @@ -0,0 +1,143 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef nsXULContentSink_h__ +#define nsXULContentSink_h__ + +#include "mozilla/Attributes.h" +#include "nsIExpatSink.h" +#include "nsIWeakReferenceUtils.h" +#include "nsIXMLContentSink.h" +#include "nsNodeInfoManager.h" +#include "nsXULElement.h" +#include "nsIDTD.h" + +class nsIScriptSecurityManager; +class nsAttrName; +class nsXULPrototypeDocument; +class nsXULPrototypeElement; +class nsXULPrototypeNode; + +class XULContentSinkImpl final : public nsIXMLContentSink, public nsIExpatSink { + public: + XULContentSinkImpl(); + + // nsISupports + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_NSIEXPATSINK + + NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(XULContentSinkImpl, + nsIXMLContentSink) + + // nsIContentSink + NS_IMETHOD WillParse(void) override { return NS_OK; } + NS_IMETHOD DidBuildModel(bool aTerminated) override; + NS_IMETHOD WillInterrupt(void) override; + void WillResume() override; + NS_IMETHOD SetParser(nsParserBase* aParser) override; + virtual void FlushPendingNotifications(mozilla::FlushType aType) override {} + virtual void SetDocumentCharset(NotNull aEncoding) override; + virtual nsISupports* GetTarget() override; + + /** + * Initialize the content sink, giving it a document with which to communicate + * with the outside world, and an nsXULPrototypeDocument to build. + */ + nsresult Init(mozilla::dom::Document* aDocument, + nsXULPrototypeDocument* aPrototype); + + protected: + virtual ~XULContentSinkImpl(); + + // pseudo-constants + char16_t* mText; + int32_t mTextLength; + int32_t mTextSize; + bool mConstrainSize; + + nsresult AddAttributes(const char16_t** aAttributes, const uint32_t aAttrLen, + nsXULPrototypeElement* aElement); + + nsresult OpenRoot(const char16_t** aAttributes, const uint32_t aAttrLen, + mozilla::dom::NodeInfo* aNodeInfo); + + nsresult OpenTag(const char16_t** aAttributes, const uint32_t aAttrLen, + const uint32_t aLineNumber, + mozilla::dom::NodeInfo* aNodeInfo); + + // If OpenScript returns NS_OK and after it returns our state is eInScript, + // that means that we created a prototype script and stuck it on + // mContextStack. If NS_OK is returned but the state is still + // eInDocumentElement then we didn't create a prototype script (e.g. the + // script had an unknown type), and the caller should create a prototype + // element. + nsresult OpenScript(const char16_t** aAttributes, const uint32_t aLineNumber); + + static bool IsDataInBuffer(char16_t* aBuffer, int32_t aLength); + + // Text management + nsresult FlushText(bool aCreateTextNode = true); + nsresult AddText(const char16_t* aText, int32_t aLength); + + RefPtr mNodeInfoManager; + + nsresult NormalizeAttributeString(const char16_t* aExpatName, + nsAttrName& aName); + + public: + enum State { eInProlog, eInDocumentElement, eInScript, eInEpilog }; + + protected: + State mState; + + // content stack management + class ContextStack { + protected: + struct Entry { + RefPtr mNode; + // a LOT of nodes have children; preallocate for 8 + nsPrototypeArray mChildren; + State mState; + Entry* mNext; + Entry(RefPtr&& aNode, State aState, Entry* aNext) + : mNode(std::move(aNode)), + mChildren(8), + mState(aState), + mNext(aNext) {} + }; + + Entry* mTop; + int32_t mDepth; + + public: + ContextStack(); + ~ContextStack(); + + int32_t Depth() { return mDepth; } + + void Push(RefPtr&& aNode, State aState); + nsresult Pop(State* aState); + + nsresult GetTopNode(RefPtr& aNode); + nsresult GetTopChildren(nsPrototypeArray** aChildren); + + void Clear(); + + void Traverse(nsCycleCollectionTraversalCallback& aCallback); + }; + + friend class ContextStack; + ContextStack mContextStack; + + nsWeakPtr mDocument; // [OWNER] + nsCOMPtr mDocumentURL; // [OWNER] + + RefPtr mPrototype; // [OWNER] + + RefPtr mParser; + nsCOMPtr mSecMan; +}; + +#endif /* nsXULContentSink_h__ */ diff --git a/dom/xul/nsXULContentUtils.cpp b/dom/xul/nsXULContentUtils.cpp new file mode 100644 index 0000000000..109aa1975a --- /dev/null +++ b/dom/xul/nsXULContentUtils.cpp @@ -0,0 +1,89 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * 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/. */ + +/* + + A package of routines shared by the XUL content code. + + */ + +#include "mozilla/ArrayUtils.h" +#include "mozilla/intl/LocaleService.h" +#include "mozilla/intl/Collator.h" + +#include "nsCOMPtr.h" +#include "nsComponentManagerUtils.h" +#include "nsIContent.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/Element.h" +#include "nsXULContentUtils.h" +#include "nsLayoutCID.h" +#include "nsString.h" +#include "nsGkAtoms.h" + +using namespace mozilla; + +//------------------------------------------------------------------------ + +const mozilla::intl::Collator* nsXULContentUtils::gCollator; + +//------------------------------------------------------------------------ +// Constructors n' stuff +// + +nsresult nsXULContentUtils::Finish() { + if (gCollator) { + delete gCollator; + gCollator = nullptr; + } + + return NS_OK; +} + +const mozilla::intl::Collator* nsXULContentUtils::GetCollator() { + if (!gCollator) { + // Lazily initialize the Collator. + auto result = mozilla::intl::LocaleService::TryCreateComponent< + mozilla::intl::Collator>(); + if (result.isErr()) { + NS_ERROR("couldn't create a mozilla::intl::Collator"); + return nullptr; + } + + auto collator = result.unwrap(); + + // Sort in a case-insensitive way, where "base" letters are considered + // equal, e.g: a = á, a = A, a ≠ b. + mozilla::intl::Collator::Options options{}; + options.sensitivity = mozilla::intl::Collator::Sensitivity::Base; + auto optResult = collator->SetOptions(options); + if (optResult.isErr()) { + NS_ERROR("couldn't set options for mozilla::intl::Collator"); + return nullptr; + } + gCollator = collator.release(); + } + + return gCollator; +} + +//------------------------------------------------------------------------ +// + +nsresult nsXULContentUtils::FindChildByTag(nsIContent* aElement, + int32_t aNameSpaceID, nsAtom* aTag, + mozilla::dom::Element** aResult) { + for (nsIContent* child = aElement->GetFirstChild(); child; + child = child->GetNextSibling()) { + if (child->IsElement() && child->NodeInfo()->Equals(aTag, aNameSpaceID)) { + NS_ADDREF(*aResult = child->AsElement()); + return NS_OK; + } + } + + *aResult = nullptr; + return NS_RDF_NO_VALUE; // not found +} diff --git a/dom/xul/nsXULContentUtils.h b/dom/xul/nsXULContentUtils.h new file mode 100644 index 0000000000..c996bcec2b --- /dev/null +++ b/dom/xul/nsXULContentUtils.h @@ -0,0 +1,45 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +/* + + A package of routines shared by the XUL content code. + + */ + +#ifndef nsXULContentUtils_h__ +#define nsXULContentUtils_h__ + +#include "nsISupports.h" + +class nsAtom; +class nsIContent; + +namespace mozilla::dom { +class Element; +} +namespace mozilla::intl { +class Collator; +} + +class nsXULContentUtils { + protected: + const static mozilla::intl::Collator* gCollator; + + static bool gDisableXULCache; + + static int DisableXULCacheChangedCallback(const char* aPrefName, + void* aClosure); + + public: + static nsresult Finish(); + + static nsresult FindChildByTag(nsIContent* aElement, int32_t aNameSpaceID, + nsAtom* aTag, mozilla::dom::Element** aResult); + + static const mozilla::intl::Collator* GetCollator(); +}; + +#endif // nsXULContentUtils_h__ diff --git a/dom/xul/nsXULControllers.cpp b/dom/xul/nsXULControllers.cpp new file mode 100644 index 0000000000..3e96fd6e41 --- /dev/null +++ b/dom/xul/nsXULControllers.cpp @@ -0,0 +1,214 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +/* + + This file provides the implementation for the XUL "controllers" + object. + +*/ + +#include "nsString.h" + +#include "nsIControllers.h" +#include "nsXULControllers.h" +#include "nsIController.h" +#include "mozilla/RefPtr.h" + +//---------------------------------------------------------------------- + +nsXULControllerData::nsXULControllerData(uint32_t inControllerID, + nsIController* inController) + : mControllerID(inControllerID), mController(inController) {} + +nsresult nsXULControllerData::GetController(nsIController** outController) { + NS_IF_ADDREF(*outController = mController); + return NS_OK; +} + +nsXULControllers::nsXULControllers() : mCurControllerID(0) {} + +nsXULControllers::~nsXULControllers(void) { DeleteControllers(); } + +void nsXULControllers::DeleteControllers() { + uint32_t count = mControllers.Length(); + for (uint32_t i = 0; i < count; i++) { + nsXULControllerData* controllerData = mControllers.ElementAt(i); + delete controllerData; // releases the nsIController + } + + mControllers.Clear(); +} + +NS_IMPL_CYCLE_COLLECTION_CLASS(nsXULControllers) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(nsXULControllers) + tmp->DeleteControllers(); +NS_IMPL_CYCLE_COLLECTION_UNLINK_END +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(nsXULControllers) + { + uint32_t i, count = tmp->mControllers.Length(); + for (i = 0; i < count; ++i) { + nsXULControllerData* controllerData = tmp->mControllers[i]; + if (controllerData) { + cb.NoteXPCOMChild(controllerData->mController); + } + } + } +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsXULControllers) + NS_INTERFACE_MAP_ENTRY(nsIControllers) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIControllers) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(nsXULControllers) +NS_IMPL_CYCLE_COLLECTING_RELEASE(nsXULControllers) + +NS_IMETHODIMP +nsXULControllers::GetControllerForCommand(const char* aCommand, + nsIController** _retval) { + NS_ENSURE_ARG_POINTER(_retval); + *_retval = nullptr; + + uint32_t count = mControllers.Length(); + for (uint32_t i = 0; i < count; i++) { + nsXULControllerData* controllerData = mControllers.ElementAt(i); + if (controllerData) { + nsCOMPtr controller; + controllerData->GetController(getter_AddRefs(controller)); + if (controller) { + bool supportsCommand; + controller->SupportsCommand(aCommand, &supportsCommand); + if (supportsCommand) { + controller.forget(_retval); + return NS_OK; + } + } + } + } + + return NS_OK; +} + +NS_IMETHODIMP +nsXULControllers::InsertControllerAt(uint32_t aIndex, + nsIController* controller) { + nsXULControllerData* controllerData = + new nsXULControllerData(++mCurControllerID, controller); +#ifdef DEBUG + nsXULControllerData** inserted = +#endif + mControllers.InsertElementAt(aIndex, controllerData); + NS_ASSERTION(inserted != nullptr, "Insertion of controller failed"); + return NS_OK; +} + +NS_IMETHODIMP +nsXULControllers::RemoveControllerAt(uint32_t aIndex, nsIController** _retval) { + NS_ENSURE_ARG_POINTER(_retval); + *_retval = nullptr; + + nsXULControllerData* controllerData = mControllers.SafeElementAt(aIndex); + if (!controllerData) return NS_ERROR_FAILURE; + + mControllers.RemoveElementAt(aIndex); + + controllerData->GetController(_retval); + delete controllerData; + + return NS_OK; +} + +NS_IMETHODIMP +nsXULControllers::GetControllerAt(uint32_t aIndex, nsIController** _retval) { + NS_ENSURE_ARG_POINTER(_retval); + *_retval = nullptr; + + nsXULControllerData* controllerData = mControllers.SafeElementAt(aIndex); + if (!controllerData) return NS_ERROR_FAILURE; + + return controllerData->GetController(_retval); // does the addref +} + +NS_IMETHODIMP +nsXULControllers::AppendController(nsIController* controller) { + // This assigns controller IDs starting at 1 so we can use 0 to test if an ID + // was obtained + nsXULControllerData* controllerData = + new nsXULControllerData(++mCurControllerID, controller); + +#ifdef DEBUG + nsXULControllerData** appended = +#endif + mControllers.AppendElement(controllerData); + NS_ASSERTION(appended != nullptr, "Appending controller failed"); + return NS_OK; +} + +NS_IMETHODIMP +nsXULControllers::RemoveController(nsIController* controller) { + // first get the identity pointer + nsCOMPtr controllerSup(do_QueryInterface(controller)); + // then find it + uint32_t count = mControllers.Length(); + for (uint32_t i = 0; i < count; i++) { + nsXULControllerData* controllerData = mControllers.ElementAt(i); + if (controllerData) { + nsCOMPtr thisController; + controllerData->GetController(getter_AddRefs(thisController)); + nsCOMPtr thisControllerSup( + do_QueryInterface(thisController)); // get identity + if (thisControllerSup == controllerSup) { + mControllers.RemoveElementAt(i); + delete controllerData; + return NS_OK; + } + } + } + return NS_ERROR_FAILURE; // right thing to return if no controller found? +} + +NS_IMETHODIMP +nsXULControllers::GetControllerId(nsIController* controller, + uint32_t* _retval) { + NS_ENSURE_ARG_POINTER(_retval); + + uint32_t count = mControllers.Length(); + for (uint32_t i = 0; i < count; i++) { + nsXULControllerData* controllerData = mControllers.ElementAt(i); + if (controllerData) { + nsCOMPtr thisController; + controllerData->GetController(getter_AddRefs(thisController)); + if (thisController.get() == controller) { + *_retval = controllerData->GetControllerID(); + return NS_OK; + } + } + } + return NS_ERROR_FAILURE; // none found +} + +NS_IMETHODIMP +nsXULControllers::GetControllerById(uint32_t controllerID, + nsIController** _retval) { + NS_ENSURE_ARG_POINTER(_retval); + + uint32_t count = mControllers.Length(); + for (uint32_t i = 0; i < count; i++) { + nsXULControllerData* controllerData = mControllers.ElementAt(i); + if (controllerData && controllerData->GetControllerID() == controllerID) { + return controllerData->GetController(_retval); + } + } + return NS_ERROR_FAILURE; // none found +} + +NS_IMETHODIMP +nsXULControllers::GetControllerCount(uint32_t* _retval) { + NS_ENSURE_ARG_POINTER(_retval); + *_retval = mControllers.Length(); + return NS_OK; +} diff --git a/dom/xul/nsXULControllers.h b/dom/xul/nsXULControllers.h new file mode 100644 index 0000000000..495cf64e71 --- /dev/null +++ b/dom/xul/nsXULControllers.h @@ -0,0 +1,51 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +/* + + The XUL "controllers" object. + +*/ + +#ifndef nsXULControllers_h__ +#define nsXULControllers_h__ + +#include "nsCOMPtr.h" +#include "nsTArray.h" +#include "nsIControllers.h" +#include "nsCycleCollectionParticipant.h" + +/* non-XPCOM class for holding controllers and their IDs */ +class nsXULControllerData final { + public: + nsXULControllerData(uint32_t inControllerID, nsIController* inController); + ~nsXULControllerData() = default; + + uint32_t GetControllerID() { return mControllerID; } + + nsresult GetController(nsIController** outController); + + uint32_t mControllerID; + nsCOMPtr mController; +}; + +class nsXULControllers final : public nsIControllers { + public: + nsXULControllers(); + + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(nsXULControllers, nsIControllers) + NS_DECL_NSICONTROLLERS + + protected: + virtual ~nsXULControllers(void); + + void DeleteControllers(); + + nsTArray mControllers; + uint32_t mCurControllerID; +}; + +#endif // nsXULControllers_h__ diff --git a/dom/xul/nsXULElement.cpp b/dom/xul/nsXULElement.cpp new file mode 100644 index 0000000000..66d6d0a20c --- /dev/null +++ b/dom/xul/nsXULElement.cpp @@ -0,0 +1,2008 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 "nsXULElement.h" + +#include +#include +#include "AttrArray.h" +#include "MainThreadUtils.h" +#include "ReferrerInfo.h" +#include "Units.h" +#include "XULButtonElement.h" +#include "XULFrameElement.h" +#include "XULMenuElement.h" +#include "XULMenuBarElement.h" +#include "XULPopupElement.h" +#include "XULResizerElement.h" +#include "XULTextElement.h" +#include "XULTooltipElement.h" +#include "XULTreeElement.h" +#include "js/CompilationAndEvaluation.h" +#include "js/CompileOptions.h" +#include "js/experimental/JSStencil.h" +#include "js/OffThreadScriptCompilation.h" +#include "js/SourceText.h" +#include "js/Transcoding.h" +#include "js/Utility.h" +#include "jsapi.h" +#include "mozilla/Assertions.h" +#include "mozilla/ArrayIterator.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/DeclarationBlock.h" +#include "mozilla/EventDispatcher.h" +#include "mozilla/EventListenerManager.h" +#include "mozilla/EventStateManager.h" +#include "mozilla/FlushType.h" +#include "mozilla/GlobalKeyListener.h" +#include "mozilla/HoldDropJSObjects.h" +#include "mozilla/MacroForEach.h" +#include "mozilla/Maybe.h" +#include "mozilla/MouseEvents.h" +#include "mozilla/OwningNonNull.h" +#include "mozilla/PresShell.h" +#include "mozilla/RefPtr.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/StaticAnalysisFunctions.h" +#include "mozilla/StaticPtr.h" +#include "mozilla/URLExtraData.h" +#include "mozilla/dom/BindContext.h" +#include "mozilla/dom/BorrowedAttrInfo.h" +#include "mozilla/dom/CSSRuleBinding.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/DocumentInlines.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/Event.h" +#include "mozilla/dom/EventTarget.h" +#include "mozilla/dom/FragmentOrElement.h" +#include "mozilla/dom/FromParser.h" +#include "mozilla/dom/MouseEventBinding.h" +#include "mozilla/dom/MutationEventBinding.h" +#include "mozilla/dom/NodeInfo.h" +#include "mozilla/dom/ReferrerPolicyBinding.h" +#include "mozilla/dom/ScriptSettings.h" +#include "mozilla/dom/XULBroadcastManager.h" +#include "mozilla/dom/XULCommandEvent.h" +#include "mozilla/dom/XULElementBinding.h" +#include "mozilla/dom/nsCSPUtils.h" +#include "mozilla/fallible.h" +#include "nsAtom.h" +#include "nsAttrValueInlines.h" +#include "nsCaseTreatment.h" +#include "nsChangeHint.h" +#include "nsCOMPtr.h" +#include "nsCompatibility.h" +#include "nsContentCreatorFunctions.h" +#include "nsContentUtils.h" +#include "nsCycleCollectionNoteChild.h" +#include "nsCycleCollectionTraversalCallback.h" +#include "nsDebug.h" +#include "nsError.h" +#include "nsFocusManager.h" +#include "nsGkAtoms.h" +#include "nsIContent.h" +#include "nsIContentSecurityPolicy.h" +#include "nsIControllers.h" +#include "nsID.h" +#include "nsIDOMEventListener.h" +#include "nsIDOMXULControlElement.h" +#include "nsIDOMXULSelectCntrlItemEl.h" +#include "nsIDocShell.h" +#include "nsIFocusManager.h" +#include "nsIFrame.h" +#include "nsIObjectInputStream.h" +#include "nsIObjectOutputStream.h" +#include "nsIRunnable.h" +#include "nsIScriptContext.h" +#include "nsISupportsUtils.h" +#include "nsIURI.h" +#include "nsIXPConnect.h" +#include "nsMenuPopupFrame.h" +#include "nsNodeInfoManager.h" +#include "nsPIDOMWindow.h" +#include "nsPIDOMWindowInlines.h" +#include "nsPresContext.h" +#include "nsQueryFrame.h" +#include "nsString.h" +#include "nsStyledElement.h" +#include "nsThreadUtils.h" +#include "nsXULControllers.h" +#include "nsXULPopupListener.h" +#include "nsXULPopupManager.h" +#include "nsXULPrototypeCache.h" +#include "nsXULTooltipListener.h" +#include "xpcpublic.h" + +using namespace mozilla; +using namespace mozilla::dom; + +#ifdef XUL_PROTOTYPE_ATTRIBUTE_METERING +uint32_t nsXULPrototypeAttribute::gNumElements; +uint32_t nsXULPrototypeAttribute::gNumAttributes; +uint32_t nsXULPrototypeAttribute::gNumCacheTests; +uint32_t nsXULPrototypeAttribute::gNumCacheHits; +uint32_t nsXULPrototypeAttribute::gNumCacheSets; +uint32_t nsXULPrototypeAttribute::gNumCacheFills; +#endif + +#define NS_DISPATCH_XUL_COMMAND (1 << 0) + +//---------------------------------------------------------------------- +// nsXULElement +// + +nsXULElement::nsXULElement(already_AddRefed&& aNodeInfo) + : nsStyledElement(std::move(aNodeInfo)) { + XUL_PROTOTYPE_ATTRIBUTE_METER(gNumElements); +} + +nsXULElement::~nsXULElement() = default; + +/* static */ +nsXULElement* NS_NewBasicXULElement( + already_AddRefed&& aNodeInfo) { + RefPtr nodeInfo(std::move(aNodeInfo)); + auto* nim = nodeInfo->NodeInfoManager(); + return new (nim) nsXULElement(nodeInfo.forget()); +} + +/* static */ +nsXULElement* nsXULElement::Construct( + already_AddRefed&& aNodeInfo) { + // NOTE: If you add elements here, you probably also want to change + // mozilla::dom::binding_detail::HTMLConstructor in BindingUtils.cpp to take + // them into account, otherwise you'll start getting "Illegal constructor" + // exceptions in chrome code. + RefPtr nodeInfo = aNodeInfo; + if (nodeInfo->Equals(nsGkAtoms::resizer)) { + return NS_NewXULResizerElement(nodeInfo.forget()); + } + + if (nodeInfo->Equals(nsGkAtoms::label) || + nodeInfo->Equals(nsGkAtoms::description)) { + auto* nim = nodeInfo->NodeInfoManager(); + return new (nim) XULTextElement(nodeInfo.forget()); + } + + if (nodeInfo->Equals(nsGkAtoms::menupopup) || + nodeInfo->Equals(nsGkAtoms::popup) || + nodeInfo->Equals(nsGkAtoms::panel)) { + return NS_NewXULPopupElement(nodeInfo.forget()); + } + + if (nodeInfo->Equals(nsGkAtoms::tooltip)) { + return NS_NewXULTooltipElement(nodeInfo.forget()); + } + + if (nodeInfo->Equals(nsGkAtoms::iframe) || + nodeInfo->Equals(nsGkAtoms::browser) || + nodeInfo->Equals(nsGkAtoms::editor)) { + auto* nim = nodeInfo->NodeInfoManager(); + return new (nim) XULFrameElement(nodeInfo.forget()); + } + + if (nodeInfo->Equals(nsGkAtoms::menubar)) { + auto* nim = nodeInfo->NodeInfoManager(); + return new (nim) XULMenuBarElement(nodeInfo.forget()); + } + + if (nodeInfo->Equals(nsGkAtoms::menu) || + nodeInfo->Equals(nsGkAtoms::menulist)) { + auto* nim = nodeInfo->NodeInfoManager(); + return new (nim) XULMenuElement(nodeInfo.forget()); + } + + if (nodeInfo->Equals(nsGkAtoms::tree)) { + auto* nim = nodeInfo->NodeInfoManager(); + return new (nim) XULTreeElement(nodeInfo.forget()); + } + + if (nodeInfo->Equals(nsGkAtoms::checkbox) || + nodeInfo->Equals(nsGkAtoms::radio) || + nodeInfo->Equals(nsGkAtoms::thumb) || + nodeInfo->Equals(nsGkAtoms::button) || + nodeInfo->Equals(nsGkAtoms::menuitem) || + nodeInfo->Equals(nsGkAtoms::toolbarbutton) || + nodeInfo->Equals(nsGkAtoms::toolbarpaletteitem) || + nodeInfo->Equals(nsGkAtoms::scrollbarbutton)) { + auto* nim = nodeInfo->NodeInfoManager(); + return new (nim) XULButtonElement(nodeInfo.forget()); + } + + return NS_NewBasicXULElement(nodeInfo.forget()); +} + +/* static */ +already_AddRefed nsXULElement::CreateFromPrototype( + nsXULPrototypeElement* aPrototype, mozilla::dom::NodeInfo* aNodeInfo, + bool aIsScriptable, bool aIsRoot) { + RefPtr ni = aNodeInfo; + nsCOMPtr baseElement; + NS_NewXULElement(getter_AddRefs(baseElement), ni.forget(), + dom::FROM_PARSER_NETWORK, aPrototype->mIsAtom); + + if (baseElement) { + nsXULElement* element = FromNode(baseElement); + + if (aPrototype->mHasIdAttribute) { + element->SetHasID(); + } + if (aPrototype->mHasClassAttribute) { + element->SetMayHaveClass(); + } + if (aPrototype->mHasStyleAttribute) { + element->SetMayHaveStyle(); + } + + element->MakeHeavyweight(aPrototype); + if (aIsScriptable) { + // Check each attribute on the prototype to see if we need to do + // any additional processing and hookup that would otherwise be + // done 'automagically' by SetAttr(). + for (const auto& attribute : aPrototype->mAttributes) { + element->AddListenerForAttributeIfNeeded(attribute.mName); + } + } + + return baseElement.forget().downcast(); + } + + return nullptr; +} + +nsresult nsXULElement::CreateFromPrototype(nsXULPrototypeElement* aPrototype, + Document* aDocument, + bool aIsScriptable, bool aIsRoot, + Element** aResult) { + // Create an nsXULElement from a prototype + MOZ_ASSERT(aPrototype != nullptr, "null ptr"); + if (!aPrototype) return NS_ERROR_NULL_POINTER; + + MOZ_ASSERT(aResult != nullptr, "null ptr"); + if (!aResult) return NS_ERROR_NULL_POINTER; + + RefPtr nodeInfo; + if (aDocument) { + mozilla::dom::NodeInfo* ni = aPrototype->mNodeInfo; + nodeInfo = aDocument->NodeInfoManager()->GetNodeInfo( + ni->NameAtom(), ni->GetPrefixAtom(), ni->NamespaceID(), ELEMENT_NODE); + } else { + nodeInfo = aPrototype->mNodeInfo; + } + + RefPtr element = + CreateFromPrototype(aPrototype, nodeInfo, aIsScriptable, aIsRoot); + element.forget(aResult); + + return NS_OK; +} + +nsresult NS_NewXULElement(Element** aResult, + already_AddRefed&& aNodeInfo, + FromParser aFromParser, nsAtom* aIsAtom, + mozilla::dom::CustomElementDefinition* aDefinition) { + RefPtr nodeInfo = aNodeInfo; + + MOZ_ASSERT(nodeInfo, "need nodeinfo for non-proto Create"); + + NS_ASSERTION( + nodeInfo->NamespaceEquals(kNameSpaceID_XUL), + "Trying to create XUL elements that don't have the XUL namespace"); + + Document* doc = nodeInfo->GetDocument(); + if (doc && !doc->AllowXULXBL()) { + return NS_ERROR_NOT_AVAILABLE; + } + + return nsContentUtils::NewXULOrHTMLElement(aResult, nodeInfo, aFromParser, + aIsAtom, aDefinition); +} + +void NS_TrustedNewXULElement( + Element** aResult, already_AddRefed&& aNodeInfo) { + RefPtr ni = aNodeInfo; + MOZ_ASSERT(ni, "need nodeinfo for non-proto Create"); + + // Create an nsXULElement with the specified namespace and tag. + NS_ADDREF(*aResult = nsXULElement::Construct(ni.forget())); +} + +//---------------------------------------------------------------------- +// nsISupports interface + +NS_IMPL_CYCLE_COLLECTION_INHERITED(nsXULElement, nsStyledElement) + +NS_IMPL_ADDREF_INHERITED(nsXULElement, nsStyledElement) +NS_IMPL_RELEASE_INHERITED(nsXULElement, nsStyledElement) + +NS_INTERFACE_TABLE_HEAD_CYCLE_COLLECTION_INHERITED(nsXULElement) + NS_ELEMENT_INTERFACE_TABLE_TO_MAP_SEGUE +NS_INTERFACE_MAP_END_INHERITING(nsStyledElement) + +//---------------------------------------------------------------------- +// nsINode interface + +nsresult nsXULElement::Clone(mozilla::dom::NodeInfo* aNodeInfo, + nsINode** aResult) const { + *aResult = nullptr; + + RefPtr ni = aNodeInfo; + RefPtr element = Construct(ni.forget()); + + nsresult rv = const_cast(this)->CopyInnerTo( + element, ReparseAttributes::No); + NS_ENSURE_SUCCESS(rv, rv); + + // Note that we're _not_ copying mControllers. + + element.forget(aResult); + return rv; +} + +//---------------------------------------------------------------------- + +EventListenerManager* nsXULElement::GetEventListenerManagerForAttr( + nsAtom* aAttrName, bool* aDefer) { + // XXXbz sXBL/XBL2 issue: should we instead use GetComposedDoc() + // here, override BindToTree for those classes and munge event + // listeners there? + Document* doc = OwnerDoc(); + + nsPIDOMWindowInner* window; + Element* root = doc->GetRootElement(); + if ((!root || root == this) && (window = doc->GetInnerWindow())) { + nsCOMPtr piTarget = do_QueryInterface(window); + + *aDefer = false; + return piTarget->GetOrCreateListenerManager(); + } + + return nsStyledElement::GetEventListenerManagerForAttr(aAttrName, aDefer); +} + +// returns true if the element is not a list +static bool IsNonList(mozilla::dom::NodeInfo* aNodeInfo) { + return !aNodeInfo->Equals(nsGkAtoms::tree) && + !aNodeInfo->Equals(nsGkAtoms::richlistbox); +} + +bool nsXULElement::IsFocusableInternal(int32_t* aTabIndex, bool aWithMouse) { + /* + * Returns true if an element may be focused, and false otherwise. The inout + * argument aTabIndex will be set to the tab order index to be used; -1 for + * elements that should not be part of the tab order and a greater value to + * indicate its tab order. + * + * Confusingly, the supplied value for the aTabIndex argument may indicate + * whether the element may be focused as a result of the -moz-user-focus + * property, where -1 means no and 0 means yes. + * + * For controls, the element cannot be focused and is not part of the tab + * order if it is disabled. + * + * -moz-user-focus is overridden if a tabindex (even -1) is specified. + * + * Specifically, the behaviour for all XUL elements is as follows: + * *aTabIndex = -1 no tabindex Not focusable or tabbable + * *aTabIndex = -1 tabindex="-1" Focusable but not tabbable + * *aTabIndex = -1 tabindex=">=0" Focusable and tabbable + * *aTabIndex >= 0 no tabindex Focusable and tabbable + * *aTabIndex >= 0 tabindex="-1" Focusable but not tabbable + * *aTabIndex >= 0 tabindex=">=0" Focusable and tabbable + * + * If aTabIndex is null, then the tabindex is not computed, and + * true is returned for non-disabled controls and false otherwise. + */ + + // elements are not focusable by default + bool shouldFocus = false; + +#ifdef XP_MACOSX + // on Mac, mouse interactions only focus the element if it's a list, + // or if it's a remote target, since the remote target must handle + // the focus. + if (aWithMouse && IsNonList(mNodeInfo) && + !EventStateManager::IsTopLevelRemoteTarget(this)) { + return false; + } +#endif + + nsCOMPtr xulControl = AsXULControl(); + if (xulControl) { + // a disabled element cannot be focused and is not part of the tab order + bool disabled; + xulControl->GetDisabled(&disabled); + if (disabled) { + if (aTabIndex) *aTabIndex = -1; + return false; + } + shouldFocus = true; + } + + if (aTabIndex) { + Maybe attrVal = GetTabIndexAttrValue(); + if (attrVal.isSome()) { + // The tabindex attribute was specified, so the element becomes + // focusable. + shouldFocus = true; + *aTabIndex = attrVal.value(); + } else { + // otherwise, if there is no tabindex attribute, just use the value of + // *aTabIndex to indicate focusability. Reset any supplied tabindex to 0. + shouldFocus = *aTabIndex >= 0; + if (shouldFocus) { + *aTabIndex = 0; + } + } + + if (xulControl && shouldFocus && sTabFocusModelAppliesToXUL && + !(sTabFocusModel & eTabFocus_formElementsMask)) { + // By default, the tab focus model doesn't apply to xul element on any + // system but OS X. on OS X we're following it for UI elements (XUL) as + // sTabFocusModel is based on "Full Keyboard Access" system setting (see + // mac/nsILookAndFeel). both textboxes and list elements (i.e. trees and + // list) should always be focusable (textboxes are handled as html:input) + // For compatibility, we only do this for controls, otherwise elements + // like cannot take this focus. + if (IsNonList(mNodeInfo)) { + *aTabIndex = -1; + } + } + } + + return shouldFocus; +} + +bool nsXULElement::HasMenu() { + if (auto* button = XULButtonElement::FromNode(this)) { + return button->IsMenu(); + } + return false; +} + +void nsXULElement::OpenMenu(bool aOpenFlag) { + // Flush frames first. It's not clear why this is needed, see bug 1704670. + if (Document* doc = GetComposedDoc()) { + doc->FlushPendingNotifications(FlushType::Frames); + } + + nsXULPopupManager* pm = nsXULPopupManager::GetInstance(); + if (!pm) { + return; + } + + if (aOpenFlag) { + // Nothing will happen if this element isn't a menu. + pm->ShowMenu(this, false); + } else { + // Nothing will happen if this element isn't a menu. + pm->HideMenu(this); + } +} + +Result nsXULElement::PerformAccesskey(bool aKeyCausesActivation, + bool aIsTrustedEvent) { + if (IsXULElement(nsGkAtoms::label)) { + nsAutoString control; + GetAttr(kNameSpaceID_None, nsGkAtoms::control, control); + if (control.IsEmpty()) { + return Err(NS_ERROR_UNEXPECTED); + } + + // XXXsmaug Should we use ShadowRoot::GetElementById in case + // element is in Shadow DOM? + RefPtr document = GetUncomposedDoc(); + if (!document) { + return Err(NS_ERROR_UNEXPECTED); + } + + RefPtr element = document->GetElementById(control); + if (!element) { + return Err(NS_ERROR_UNEXPECTED); + } + + // XXXedgar, This is mainly for HTMLElement which doesn't do visible + // check in PerformAccesskey. We probably should always do visible + // check on HTMLElement even if the PerformAccesskey is not redirected from + // label XULelement per spec. + nsIFrame* frame = element->GetPrimaryFrame(); + if (!frame || !frame->IsVisibleConsideringAncestors()) { + return Err(NS_ERROR_UNEXPECTED); + } + + return element->PerformAccesskey(aKeyCausesActivation, aIsTrustedEvent); + } + + nsIFrame* frame = GetPrimaryFrame(); + if (!frame || !frame->IsVisibleConsideringAncestors()) { + return Err(NS_ERROR_UNEXPECTED); + } + + bool focused = false; + // Define behavior for each type of XUL element. + if (!IsXULElement(nsGkAtoms::toolbarbutton)) { + if (RefPtr fm = nsFocusManager::GetFocusManager()) { + RefPtr elementToFocus = this; + // for radio buttons, focus the radiogroup instead + if (IsXULElement(nsGkAtoms::radio)) { + if (nsCOMPtr controlItem = + AsXULSelectControlItem()) { + bool disabled; + controlItem->GetDisabled(&disabled); + if (!disabled) { + controlItem->GetControl(getter_AddRefs(elementToFocus)); + } + } + } + + if (elementToFocus) { + fm->SetFocus(elementToFocus, nsIFocusManager::FLAG_BYKEY); + + // Return true if the element became focused. + nsPIDOMWindowOuter* window = OwnerDoc()->GetWindow(); + focused = (window && window->GetFocusedElement() == elementToFocus); + } + } + } + + if (aKeyCausesActivation && !IsXULElement(nsGkAtoms::menulist)) { + ClickWithInputSource(MouseEvent_Binding::MOZ_SOURCE_KEYBOARD, + aIsTrustedEvent); + return focused; + } + + // If the accesskey won't cause the activation and the focus isn't changed, + // either. Return error so EventStateManager would try to find next element + // to handle the accesskey. + return focused ? Result{focused} : Err(NS_ERROR_ABORT); +} + +//---------------------------------------------------------------------- + +void nsXULElement::AddListenerForAttributeIfNeeded(nsAtom* aLocalName) { + // If appropriate, add a popup listener and/or compile the event + // handler. Called when we change the element's document, create a + // new element, change an attribute's value, etc. + // Eventlistenener-attributes are always in the null namespace. + if (aLocalName == nsGkAtoms::menu || aLocalName == nsGkAtoms::contextmenu || + // XXXdwh popup and context are deprecated + aLocalName == nsGkAtoms::popup || aLocalName == nsGkAtoms::context) { + AddPopupListener(aLocalName); + } + if (nsContentUtils::IsEventAttributeName(aLocalName, EventNameType_XUL)) { + nsAutoString value; + GetAttr(kNameSpaceID_None, aLocalName, value); + SetEventHandler(aLocalName, value, true); + } +} + +void nsXULElement::AddListenerForAttributeIfNeeded(const nsAttrName& aName) { + if (aName.IsAtom()) { + AddListenerForAttributeIfNeeded(aName.Atom()); + } +} + +//---------------------------------------------------------------------- +// +// nsIContent interface +// +void nsXULElement::UpdateEditableState(bool aNotify) { + // Don't call through to Element here because the things + // it does don't work for cases when we're an editable control. + nsIContent* parent = GetParent(); + + SetEditableFlag(parent && parent->HasFlag(NODE_IS_EDITABLE)); + UpdateState(aNotify); +} + +class XULInContentErrorReporter : public Runnable { + public: + explicit XULInContentErrorReporter(Document& aDocument) + : mozilla::Runnable("XULInContentErrorReporter"), mDocument(aDocument) {} + + NS_IMETHOD Run() override { + mDocument->WarnOnceAbout(DeprecatedOperations::eImportXULIntoContent, + false); + return NS_OK; + } + + private: + OwningNonNull mDocument; +}; + +static bool NeedTooltipSupport(const nsXULElement& aXULElement) { + if (aXULElement.NodeInfo()->Equals(nsGkAtoms::treechildren)) { + // treechildren always get tooltip support, since cropped tree cells show + // their full text in a tooltip. + return true; + } + + return aXULElement.GetBoolAttr(nsGkAtoms::tooltip) || + aXULElement.GetBoolAttr(nsGkAtoms::tooltiptext); +} + +nsresult nsXULElement::BindToTree(BindContext& aContext, nsINode& aParent) { + nsresult rv = nsStyledElement::BindToTree(aContext, aParent); + NS_ENSURE_SUCCESS(rv, rv); + + if (!IsInComposedDoc()) { + return rv; + } + + Document& doc = aContext.OwnerDoc(); + if (!IsInNativeAnonymousSubtree() && !doc.AllowXULXBL() && + !doc.HasWarnedAbout(DeprecatedOperations::eImportXULIntoContent)) { + nsContentUtils::AddScriptRunner(new XULInContentErrorReporter(doc)); + } + +#ifdef DEBUG + if (!doc.AllowXULXBL() && !doc.IsUnstyledDocument()) { + // To save CPU cycles and memory, we don't load xul.css for other elements + // except scrollbars. + // + // This assertion makes sure no other XUL element is used in a non-XUL + // document. + nsAtom* tag = NodeInfo()->NameAtom(); + MOZ_ASSERT(tag == nsGkAtoms::scrollbar || + tag == nsGkAtoms::scrollbarbutton || + tag == nsGkAtoms::scrollcorner || tag == nsGkAtoms::slider || + tag == nsGkAtoms::thumb || tag == nsGkAtoms::resizer, + "Unexpected XUL element in non-XUL doc"); + } +#endif + + // Within Bug 1492063 and its dependencies we started to apply a + // CSP to system privileged about pages. Since some about: pages + // are implemented in *.xul files we added this workaround to + // apply a CSP to them. To do so, we check the introduced custom + // attribute 'csp' on the root element. + if (doc.GetRootElement() == this) { + nsAutoString cspPolicyStr; + GetAttr(kNameSpaceID_None, nsGkAtoms::csp, cspPolicyStr); + +#ifdef DEBUG + { + nsCOMPtr docCSP = doc.GetCsp(); + uint32_t policyCount = 0; + if (docCSP) { + docCSP->GetPolicyCount(&policyCount); + } + MOZ_ASSERT(policyCount == 0, "how come we already have a policy?"); + } +#endif + + CSP_ApplyMetaCSPToDoc(doc, cspPolicyStr); + } + + if (NodeInfo()->Equals(nsGkAtoms::keyset, kNameSpaceID_XUL)) { + // Create our XUL key listener and hook it up. + XULKeySetGlobalKeyListener::AttachKeyHandler(this); + } + + RegUnRegAccessKey(true); + + if (NeedTooltipSupport(*this)) { + AddTooltipSupport(); + } + + if (XULBroadcastManager::MayNeedListener(*this)) { + if (!doc.HasXULBroadcastManager()) { + doc.InitializeXULBroadcastManager(); + } + XULBroadcastManager* broadcastManager = doc.GetXULBroadcastManager(); + broadcastManager->AddListener(this); + } + return rv; +} + +void nsXULElement::UnbindFromTree(bool aNullParent) { + if (NodeInfo()->Equals(nsGkAtoms::keyset, kNameSpaceID_XUL)) { + XULKeySetGlobalKeyListener::DetachKeyHandler(this); + } + + RegUnRegAccessKey(false); + + if (NeedTooltipSupport(*this)) { + RemoveTooltipSupport(); + } + + Document* doc = GetComposedDoc(); + if (doc && doc->HasXULBroadcastManager() && + XULBroadcastManager::MayNeedListener(*this)) { + RefPtr broadcastManager = + doc->GetXULBroadcastManager(); + broadcastManager->RemoveListener(this); + } + + // mControllers can own objects that are implemented + // in JavaScript (such as some implementations of + // nsIControllers. These objects prevent their global + // object's script object from being garbage collected, + // which means JS continues to hold an owning reference + // to the nsGlobalWindow, which owns the document, + // which owns this content. That's a cycle, so we break + // it here. (It might be better to break this by releasing + // mDocument in nsGlobalWindow::SetDocShell, but I'm not + // sure whether that would fix all possible cycles through + // mControllers.) + nsExtendedDOMSlots* slots = GetExistingExtendedDOMSlots(); + if (slots) { + slots->mControllers = nullptr; + } + + nsStyledElement::UnbindFromTree(aNullParent); +} + +void nsXULElement::DoneAddingChildren(bool aHaveNotified) { + if (IsXULElement(nsGkAtoms::linkset)) { + Document* doc = GetComposedDoc(); + if (doc) { + doc->OnL10nResourceContainerParsed(); + } + } +} + +void nsXULElement::RegUnRegAccessKey(bool aDoReg) { + // Don't try to register for unsupported elements + if (!SupportsAccessKey()) { + return; + } + + nsStyledElement::RegUnRegAccessKey(aDoReg); +} + +bool nsXULElement::SupportsAccessKey() const { + if (NodeInfo()->Equals(nsGkAtoms::label) && HasAttr(nsGkAtoms::control)) { + return true; + } + + // XXX(ntim): check if description[value] or description[accesskey] are + // actually used, remove `value` from {Before/After}SetAttr if not the case + if (NodeInfo()->Equals(nsGkAtoms::description) && HasAttr(nsGkAtoms::value) && + HasAttr(nsGkAtoms::control)) { + return true; + } + + return IsAnyOfXULElements(nsGkAtoms::button, nsGkAtoms::toolbarbutton, + nsGkAtoms::checkbox, nsGkAtoms::tab, + nsGkAtoms::radio); +} + +void nsXULElement::BeforeSetAttr(int32_t aNamespaceID, nsAtom* aName, + const nsAttrValue* aValue, bool aNotify) { + if (aNamespaceID == kNameSpaceID_None) { + if (aName == nsGkAtoms::accesskey || aName == nsGkAtoms::control || + aName == nsGkAtoms::value) { + RegUnRegAccessKey(false); + } else if ((aName == nsGkAtoms::command || aName == nsGkAtoms::observes) && + IsInUncomposedDoc()) { + // XXX sXBL/XBL2 issue! Owner or current document? + // XXX Why does this not also remove broadcast listeners if the + // "element" attribute was changed on an ? + nsAutoString oldValue; + GetAttr(kNameSpaceID_None, nsGkAtoms::observes, oldValue); + if (oldValue.IsEmpty()) { + GetAttr(kNameSpaceID_None, nsGkAtoms::command, oldValue); + } + Document* doc = GetUncomposedDoc(); + if (!oldValue.IsEmpty() && doc->HasXULBroadcastManager()) { + RefPtr broadcastManager = + doc->GetXULBroadcastManager(); + broadcastManager->RemoveListener(this); + } +#ifdef DEBUG + } else if (aName == nsGkAtoms::usercontextid) { + const nsAttrValue* oldValue = GetParsedAttr(aName); + if (oldValue && (!aValue || !aValue->Equals(*oldValue))) { + MOZ_ASSERT(false, + "Changing usercontextid doesn't really work properly."); + } +#endif + } + } + + return nsStyledElement::BeforeSetAttr(aNamespaceID, aName, aValue, aNotify); +} + +void nsXULElement::AfterSetAttr(int32_t aNamespaceID, nsAtom* aName, + const nsAttrValue* aValue, + const nsAttrValue* aOldValue, + nsIPrincipal* aSubjectPrincipal, bool aNotify) { + if (aNamespaceID == kNameSpaceID_None) { + if (aValue) { + AddListenerForAttributeIfNeeded(aName); + } + + if (aName == nsGkAtoms::accesskey || aName == nsGkAtoms::control || + aName == nsGkAtoms::value) { + RegUnRegAccessKey(true); + } else if (aName == nsGkAtoms::tooltip || aName == nsGkAtoms::tooltiptext) { + if (!!aValue != !!aOldValue && IsInComposedDoc() && + !NodeInfo()->Equals(nsGkAtoms::treechildren)) { + if (aValue) { + AddTooltipSupport(); + } else { + RemoveTooltipSupport(); + } + } + } + Document* doc = GetComposedDoc(); + if (doc && doc->HasXULBroadcastManager()) { + RefPtr broadcastManager = + doc->GetXULBroadcastManager(); + broadcastManager->AttributeChanged(this, aNamespaceID, aName); + } + if (doc && XULBroadcastManager::MayNeedListener(*this)) { + if (!doc->HasXULBroadcastManager()) { + doc->InitializeXULBroadcastManager(); + } + XULBroadcastManager* broadcastManager = doc->GetXULBroadcastManager(); + broadcastManager->AddListener(this); + } + + // XXX need to check if they're changing an event handler: if + // so, then we need to unhook the old one. Or something. + } + + return nsStyledElement::AfterSetAttr(aNamespaceID, aName, aValue, aOldValue, + aSubjectPrincipal, aNotify); +} + +void nsXULElement::AddTooltipSupport() { + nsXULTooltipListener* listener = nsXULTooltipListener::GetInstance(); + if (!listener) { + return; + } + + listener->AddTooltipSupport(this); +} + +void nsXULElement::RemoveTooltipSupport() { + nsXULTooltipListener* listener = nsXULTooltipListener::GetInstance(); + if (!listener) { + return; + } + + listener->RemoveTooltipSupport(this); +} + +bool nsXULElement::ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) { + if (aNamespaceID == kNameSpaceID_None && aAttribute == nsGkAtoms::tabindex) { + return aResult.ParseIntValue(aValue); + } + + // Parse into a nsAttrValue + if (!nsStyledElement::ParseAttribute(aNamespaceID, aAttribute, aValue, + aMaybeScriptedPrincipal, aResult)) { + // Fall back to parsing as atom for short values + aResult.ParseStringOrAtom(aValue); + } + + return true; +} + +void nsXULElement::DestroyContent() { + nsExtendedDOMSlots* slots = GetExistingExtendedDOMSlots(); + if (slots) { + slots->mControllers = nullptr; + } + + nsStyledElement::DestroyContent(); +} + +#ifdef MOZ_DOM_LIST +void nsXULElement::List(FILE* out, int32_t aIndent) const { + nsCString prefix("XUL"); + if (HasSlots()) { + prefix.Append('*'); + } + prefix.Append(' '); + + nsStyledElement::List(out, aIndent, prefix); +} +#endif + +bool nsXULElement::IsEventStoppedFromAnonymousScrollbar(EventMessage aMessage) { + return (IsRootOfNativeAnonymousSubtree() && + IsAnyOfXULElements(nsGkAtoms::scrollbar, nsGkAtoms::scrollcorner) && + (aMessage == eMouseClick || aMessage == eMouseDoubleClick || + aMessage == eXULCommand || aMessage == eContextMenu || + aMessage == eDragStart || aMessage == eMouseAuxClick)); +} + +nsresult nsXULElement::DispatchXULCommand(const EventChainVisitor& aVisitor, + nsAutoString& aCommand) { + // XXX sXBL/XBL2 issue! Owner or current document? + nsCOMPtr doc = GetUncomposedDoc(); + NS_ENSURE_STATE(doc); + RefPtr commandElt = doc->GetElementById(aCommand); + if (commandElt) { + // Create a new command event to dispatch to the element + // pointed to by the command attribute. The new event's + // sourceEvent will be the original command event that we're + // handling. + RefPtr event = aVisitor.mDOMEvent; + uint16_t inputSource = MouseEvent_Binding::MOZ_SOURCE_UNKNOWN; + int16_t button = 0; + while (event) { + NS_ENSURE_STATE(event->GetOriginalTarget() != commandElt); + RefPtr commandEvent = event->AsXULCommandEvent(); + if (commandEvent) { + event = commandEvent->GetSourceEvent(); + inputSource = commandEvent->InputSource(); + button = commandEvent->Button(); + } else { + event = nullptr; + } + } + WidgetInputEvent* orig = aVisitor.mEvent->AsInputEvent(); + nsContentUtils::DispatchXULCommand( + commandElt, orig->IsTrusted(), MOZ_KnownLive(aVisitor.mDOMEvent), + nullptr, orig->IsControl(), orig->IsAlt(), orig->IsShift(), + orig->IsMeta(), inputSource, button); + } else { + NS_WARNING("A XUL element is attached to a command that doesn't exist!\n"); + } + return NS_OK; +} + +void nsXULElement::GetEventTargetParent(EventChainPreVisitor& aVisitor) { + aVisitor.mForceContentDispatch = true; // FIXME! Bug 329119 + if (IsEventStoppedFromAnonymousScrollbar(aVisitor.mEvent->mMessage)) { + // Don't propagate these events from native anonymous scrollbar. + aVisitor.mCanHandle = true; + aVisitor.SetParentTarget(nullptr, false); + return; + } + if (aVisitor.mEvent->mMessage == eXULCommand && + aVisitor.mEvent->mClass == eInputEventClass && + aVisitor.mEvent->mOriginalTarget == static_cast(this) && + !IsXULElement(nsGkAtoms::command)) { + // Check that we really have an xul command event. That will be handled + // in a special way. + // See if we have a command elt. If so, we execute on the command + // instead of on our content element. + if (aVisitor.mDOMEvent && aVisitor.mDOMEvent->AsXULCommandEvent() && + HasNonEmptyAttr(nsGkAtoms::command)) { + // Stop building the event target chain for the original event. + // We don't want it to propagate to any DOM nodes. + aVisitor.mCanHandle = false; + aVisitor.mAutomaticChromeDispatch = false; + // Dispatch XUL command in PreHandleEvent to prevent it breaks event + // target chain creation + aVisitor.mWantsPreHandleEvent = true; + aVisitor.mItemFlags |= NS_DISPATCH_XUL_COMMAND; + return; + } + } + + nsStyledElement::GetEventTargetParent(aVisitor); +} + +nsresult nsXULElement::PreHandleEvent(EventChainVisitor& aVisitor) { + if (aVisitor.mItemFlags & NS_DISPATCH_XUL_COMMAND) { + nsAutoString command; + GetAttr(kNameSpaceID_None, nsGkAtoms::command, command); + MOZ_ASSERT(!command.IsEmpty()); + return DispatchXULCommand(aVisitor, command); + } + return nsStyledElement::PreHandleEvent(aVisitor); +} + +//---------------------------------------------------------------------- +// Implementation methods + +NS_IMETHODIMP_(bool) +nsXULElement::IsAttributeMapped(const nsAtom* aAttribute) const { + return false; +} + +nsIControllers* nsXULElement::GetControllers(ErrorResult& rv) { + if (!Controllers()) { + nsExtendedDOMSlots* slots = ExtendedDOMSlots(); + + slots->mControllers = new nsXULControllers(); + } + + return Controllers(); +} + +void nsXULElement::Click(CallerType aCallerType) { + ClickWithInputSource(MouseEvent_Binding::MOZ_SOURCE_UNKNOWN, + aCallerType == CallerType::System); +} + +void nsXULElement::ClickWithInputSource(uint16_t aInputSource, + bool aIsTrustedEvent) { + if (BoolAttrIsTrue(nsGkAtoms::disabled)) return; + + nsCOMPtr doc = GetComposedDoc(); // Strong just in case + if (doc) { + RefPtr context = doc->GetPresContext(); + if (context) { + // strong ref to PresContext so events don't destroy it + + WidgetMouseEvent eventDown(aIsTrustedEvent, eMouseDown, nullptr, + WidgetMouseEvent::eReal); + WidgetMouseEvent eventUp(aIsTrustedEvent, eMouseUp, nullptr, + WidgetMouseEvent::eReal); + // This helps to avoid commands being dispatched from + // XULButtonElement::PostHandleEventForMenu. + eventUp.mFlags.mMultipleActionsPrevented = true; + WidgetMouseEvent eventClick(aIsTrustedEvent, eMouseClick, nullptr, + WidgetMouseEvent::eReal); + eventDown.mInputSource = eventUp.mInputSource = eventClick.mInputSource = + aInputSource; + + // send mouse down + nsEventStatus status = nsEventStatus_eIgnore; + EventDispatcher::Dispatch(static_cast(this), context, + &eventDown, nullptr, &status); + + // send mouse up + status = nsEventStatus_eIgnore; // reset status + EventDispatcher::Dispatch(static_cast(this), context, + &eventUp, nullptr, &status); + + // send mouse click + status = nsEventStatus_eIgnore; // reset status + EventDispatcher::Dispatch(static_cast(this), context, + &eventClick, nullptr, &status); + + // If the click has been prevented, lets skip the command call + // this is how a physical click works + if (status == nsEventStatus_eConsumeNoDefault) { + return; + } + } + } + + // oncommand is fired when an element is clicked... + DoCommand(); +} + +void nsXULElement::DoCommand() { + nsCOMPtr doc = GetComposedDoc(); // strong just in case + if (doc) { + RefPtr self = this; + nsContentUtils::DispatchXULCommand(self, true); + } +} + +nsresult nsXULElement::AddPopupListener(nsAtom* aName) { + // Add a popup listener to the element + bool isContext = + (aName == nsGkAtoms::context || aName == nsGkAtoms::contextmenu); + uint32_t listenerFlag = isContext ? XUL_ELEMENT_HAS_CONTENTMENU_LISTENER + : XUL_ELEMENT_HAS_POPUP_LISTENER; + + if (HasFlag(listenerFlag)) { + return NS_OK; + } + + nsCOMPtr listener = + new nsXULPopupListener(this, isContext); + + // Add the popup as a listener on this element. + EventListenerManager* manager = GetOrCreateListenerManager(); + SetFlags(listenerFlag); + + if (isContext) { + manager->AddEventListenerByType(listener, u"contextmenu"_ns, + TrustedEventsAtSystemGroupBubble()); + } else { + manager->AddEventListenerByType(listener, u"mousedown"_ns, + TrustedEventsAtSystemGroupBubble()); + } + return NS_OK; +} + +//---------------------------------------------------------------------- + +nsresult nsXULElement::MakeHeavyweight(nsXULPrototypeElement* aPrototype) { + if (!aPrototype) { + return NS_OK; + } + + size_t i; + nsresult rv; + for (i = 0; i < aPrototype->mAttributes.Length(); ++i) { + nsXULPrototypeAttribute* protoattr = &aPrototype->mAttributes[i]; + nsAttrValue attrValue; + + // Style rules need to be cloned. + if (protoattr->mValue.Type() == nsAttrValue::eCSSDeclaration) { + DeclarationBlock* decl = protoattr->mValue.GetCSSDeclarationValue(); + RefPtr declClone = decl->Clone(); + + nsString stringValue; + protoattr->mValue.ToString(stringValue); + + attrValue.SetTo(declClone.forget(), &stringValue); + } else { + attrValue.SetTo(protoattr->mValue); + } + + bool oldValueSet; + // XXX we might wanna have a SetAndTakeAttr that takes an nsAttrName + if (protoattr->mName.IsAtom()) { + rv = mAttrs.SetAndSwapAttr(protoattr->mName.Atom(), attrValue, + &oldValueSet); + } else { + rv = mAttrs.SetAndSwapAttr(protoattr->mName.NodeInfo(), attrValue, + &oldValueSet); + } + NS_ENSURE_SUCCESS(rv, rv); + } + return NS_OK; +} + +bool nsXULElement::BoolAttrIsTrue(nsAtom* aName) const { + const nsAttrValue* attr = GetAttrInfo(kNameSpaceID_None, aName).mValue; + + return attr && attr->Type() == nsAttrValue::eAtom && + attr->GetAtomValue() == nsGkAtoms::_true; +} + +bool nsXULElement::IsEventAttributeNameInternal(nsAtom* aName) { + return nsContentUtils::IsEventAttributeName(aName, EventNameType_XUL); +} + +JSObject* nsXULElement::WrapNode(JSContext* aCx, + JS::Handle aGivenProto) { + return dom::XULElement_Binding::Wrap(aCx, this, aGivenProto); +} + +bool nsXULElement::IsInteractiveHTMLContent() const { + return IsXULElement(nsGkAtoms::menupopup) || + Element::IsInteractiveHTMLContent(); +} + +NS_IMPL_CYCLE_COLLECTION_CLASS(nsXULPrototypeNode) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(nsXULPrototypeNode) + if (tmp->mType == nsXULPrototypeNode::eType_Element) { + static_cast(tmp)->Unlink(); + } +NS_IMPL_CYCLE_COLLECTION_UNLINK_END +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(nsXULPrototypeNode) + if (tmp->mType == nsXULPrototypeNode::eType_Element) { + nsXULPrototypeElement* elem = static_cast(tmp); + NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(cb, "mNodeInfo"); + cb.NoteNativeChild(elem->mNodeInfo, + NS_CYCLE_COLLECTION_PARTICIPANT(NodeInfo)); + size_t i; + for (i = 0; i < elem->mAttributes.Length(); ++i) { + const nsAttrName& name = elem->mAttributes[i].mName; + if (!name.IsAtom()) { + NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(cb, + "mAttributes[i].mName.NodeInfo()"); + cb.NoteNativeChild(name.NodeInfo(), + NS_CYCLE_COLLECTION_PARTICIPANT(NodeInfo)); + } + } + ImplCycleCollectionTraverse(cb, elem->mChildren, "mChildren"); + } +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(nsXULPrototypeNode) +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +//---------------------------------------------------------------------- +// +// nsXULPrototypeAttribute +// + +nsXULPrototypeAttribute::~nsXULPrototypeAttribute() { + MOZ_COUNT_DTOR(nsXULPrototypeAttribute); +} + +//---------------------------------------------------------------------- +// +// nsXULPrototypeElement +// + +nsresult nsXULPrototypeElement::Serialize( + nsIObjectOutputStream* aStream, nsXULPrototypeDocument* aProtoDoc, + const nsTArray>* aNodeInfos) { + nsresult rv; + + // Write basic prototype data + rv = aStream->Write32(mType); + + // Write Node Info + int32_t index = aNodeInfos->IndexOf(mNodeInfo); + NS_ASSERTION(index >= 0, "unknown mozilla::dom::NodeInfo index"); + nsresult tmp = aStream->Write32(index); + if (NS_FAILED(tmp)) { + rv = tmp; + } + + // Write Attributes + tmp = aStream->Write32(mAttributes.Length()); + if (NS_FAILED(tmp)) { + rv = tmp; + } + + nsAutoString attributeValue; + size_t i; + for (i = 0; i < mAttributes.Length(); ++i) { + RefPtr ni; + if (mAttributes[i].mName.IsAtom()) { + ni = mNodeInfo->NodeInfoManager()->GetNodeInfo( + mAttributes[i].mName.Atom(), nullptr, kNameSpaceID_None, + nsINode::ATTRIBUTE_NODE); + NS_ASSERTION(ni, "the nodeinfo should already exist"); + } else { + ni = mAttributes[i].mName.NodeInfo(); + } + + index = aNodeInfos->IndexOf(ni); + NS_ASSERTION(index >= 0, "unknown mozilla::dom::NodeInfo index"); + tmp = aStream->Write32(index); + if (NS_FAILED(tmp)) { + rv = tmp; + } + + mAttributes[i].mValue.ToString(attributeValue); + tmp = aStream->WriteWStringZ(attributeValue.get()); + if (NS_FAILED(tmp)) { + rv = tmp; + } + } + + // Now write children + tmp = aStream->Write32(uint32_t(mChildren.Length())); + if (NS_FAILED(tmp)) { + rv = tmp; + } + for (i = 0; i < mChildren.Length(); i++) { + nsXULPrototypeNode* child = mChildren[i].get(); + switch (child->mType) { + case eType_Element: + case eType_Text: + case eType_PI: + tmp = child->Serialize(aStream, aProtoDoc, aNodeInfos); + if (NS_FAILED(tmp)) { + rv = tmp; + } + break; + case eType_Script: + tmp = aStream->Write32(child->mType); + if (NS_FAILED(tmp)) { + rv = tmp; + } + nsXULPrototypeScript* script = + static_cast(child); + + tmp = aStream->Write8(script->mOutOfLine); + if (NS_FAILED(tmp)) { + rv = tmp; + } + if (!script->mOutOfLine) { + tmp = script->Serialize(aStream, aProtoDoc, aNodeInfos); + if (NS_FAILED(tmp)) { + rv = tmp; + } + } else { + tmp = aStream->WriteCompoundObject(script->mSrcURI, + NS_GET_IID(nsIURI), true); + if (NS_FAILED(tmp)) { + rv = tmp; + } + + if (script->HasStencil()) { + // This may return NS_OK without muxing script->mSrcURI's + // data into the cache file, in the case where that + // muxed document is already there (written by a prior + // session, or by an earlier cache episode during this + // session). + tmp = script->SerializeOutOfLine(aStream, aProtoDoc); + if (NS_FAILED(tmp)) { + rv = tmp; + } + } + } + break; + } + } + + return rv; +} + +nsresult nsXULPrototypeElement::Deserialize( + nsIObjectInputStream* aStream, nsXULPrototypeDocument* aProtoDoc, + nsIURI* aDocumentURI, + const nsTArray>* aNodeInfos) { + MOZ_ASSERT(aNodeInfos, "missing nodeinfo array"); + + // Read Node Info + uint32_t number = 0; + nsresult rv = aStream->Read32(&number); + if (NS_WARN_IF(NS_FAILED(rv))) return rv; + mNodeInfo = aNodeInfos->SafeElementAt(number, nullptr); + if (!mNodeInfo) { + return NS_ERROR_UNEXPECTED; + } + + // Read Attributes + rv = aStream->Read32(&number); + if (NS_WARN_IF(NS_FAILED(rv))) return rv; + int32_t attributes = int32_t(number); + + if (attributes > 0) { + mAttributes.AppendElements(attributes); + + nsAutoString attributeValue; + for (size_t i = 0; i < mAttributes.Length(); ++i) { + rv = aStream->Read32(&number); + if (NS_WARN_IF(NS_FAILED(rv))) return rv; + mozilla::dom::NodeInfo* ni = aNodeInfos->SafeElementAt(number, nullptr); + if (!ni) { + return NS_ERROR_UNEXPECTED; + } + + mAttributes[i].mName.SetTo(ni); + + rv = aStream->ReadString(attributeValue); + if (NS_WARN_IF(NS_FAILED(rv))) return rv; + rv = SetAttrAt(i, attributeValue, aDocumentURI); + if (NS_WARN_IF(NS_FAILED(rv))) return rv; + } + } + + rv = aStream->Read32(&number); + if (NS_WARN_IF(NS_FAILED(rv))) return rv; + uint32_t numChildren = int32_t(number); + + if (numChildren > 0) { + if (!mChildren.SetCapacity(numChildren, fallible)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + for (uint32_t i = 0; i < numChildren; i++) { + rv = aStream->Read32(&number); + if (NS_WARN_IF(NS_FAILED(rv))) return rv; + Type childType = (Type)number; + + RefPtr child; + + switch (childType) { + case eType_Element: + child = new nsXULPrototypeElement(); + rv = child->Deserialize(aStream, aProtoDoc, aDocumentURI, aNodeInfos); + if (NS_WARN_IF(NS_FAILED(rv))) return rv; + break; + case eType_Text: + child = new nsXULPrototypeText(); + rv = child->Deserialize(aStream, aProtoDoc, aDocumentURI, aNodeInfos); + if (NS_WARN_IF(NS_FAILED(rv))) return rv; + break; + case eType_PI: + child = new nsXULPrototypePI(); + rv = child->Deserialize(aStream, aProtoDoc, aDocumentURI, aNodeInfos); + if (NS_WARN_IF(NS_FAILED(rv))) return rv; + break; + case eType_Script: { + // language version/options obtained during deserialization. + RefPtr script = new nsXULPrototypeScript(0); + + rv = aStream->ReadBoolean(&script->mOutOfLine); + if (NS_WARN_IF(NS_FAILED(rv))) return rv; + if (!script->mOutOfLine) { + rv = script->Deserialize(aStream, aProtoDoc, aDocumentURI, + aNodeInfos); + if (NS_WARN_IF(NS_FAILED(rv))) return rv; + } else { + nsCOMPtr supports; + rv = aStream->ReadObject(true, getter_AddRefs(supports)); + if (NS_WARN_IF(NS_FAILED(rv))) return rv; + script->mSrcURI = do_QueryInterface(supports); + + rv = script->DeserializeOutOfLine(aStream, aProtoDoc); + if (NS_WARN_IF(NS_FAILED(rv))) return rv; + } + + child = std::move(script); + break; + } + default: + MOZ_ASSERT(false, "Unexpected child type!"); + return NS_ERROR_UNEXPECTED; + } + + MOZ_ASSERT(child, "Don't append null to mChildren"); + MOZ_ASSERT(child->mType == childType); + mChildren.AppendElement(child); + + // Oh dear. Something failed during the deserialization. + // We don't know what. But likely consequences of failed + // deserializations included calls to |AbortCaching| which + // shuts down the cache and closes our streams. + // If that happens, next time through this loop, we die a messy + // death. So, let's just fail now, and propagate that failure + // upward so that the ChromeProtocolHandler knows it can't use + // a cached chrome channel for this. + if (NS_WARN_IF(NS_FAILED(rv))) return rv; + } + } + + return rv; +} + +nsresult nsXULPrototypeElement::SetAttrAt(uint32_t aPos, + const nsAString& aValue, + nsIURI* aDocumentURI) { + MOZ_ASSERT(aPos < mAttributes.Length(), "out-of-bounds"); + + // WARNING!! + // This code is largely duplicated in nsXULElement::SetAttr. + // Any changes should be made to both functions. + + if (!mNodeInfo->NamespaceEquals(kNameSpaceID_XUL)) { + if (mNodeInfo->NamespaceEquals(kNameSpaceID_XHTML) && + mAttributes[aPos].mName.Equals(nsGkAtoms::is)) { + // We still care about the is attribute set on HTML elements. + mAttributes[aPos].mValue.ParseAtom(aValue); + mIsAtom = mAttributes[aPos].mValue.GetAtomValue(); + + return NS_OK; + } + + mAttributes[aPos].mValue.ParseStringOrAtom(aValue); + + return NS_OK; + } + + if (mAttributes[aPos].mName.Equals(nsGkAtoms::id) && !aValue.IsEmpty()) { + mHasIdAttribute = true; + // Store id as atom. + // id="" means that the element has no id. Not that it has + // emptystring as id. + mAttributes[aPos].mValue.ParseAtom(aValue); + + return NS_OK; + } else if (mAttributes[aPos].mName.Equals(nsGkAtoms::is)) { + // Store is as atom. + mAttributes[aPos].mValue.ParseAtom(aValue); + mIsAtom = mAttributes[aPos].mValue.GetAtomValue(); + + return NS_OK; + } else if (mAttributes[aPos].mName.Equals(nsGkAtoms::_class)) { + mHasClassAttribute = true; + // Compute the element's class list + mAttributes[aPos].mValue.ParseAtomArray(aValue); + + return NS_OK; + } else if (mAttributes[aPos].mName.Equals(nsGkAtoms::style)) { + mHasStyleAttribute = true; + // Parse the element's 'style' attribute + + // This is basically duplicating what nsINode::NodePrincipal() does + nsIPrincipal* principal = mNodeInfo->NodeInfoManager()->DocumentPrincipal(); + // XXX Get correct Base URI (need GetBaseURI on *prototype* element) + // TODO: If we implement Content Security Policy for chrome documents + // as has been discussed, the CSP should be checked here to see if + // inline styles are allowed to be applied. + // XXX No specific specs talk about xul and referrer policy, pass Unset + auto referrerInfo = + MakeRefPtr(aDocumentURI, ReferrerPolicy::_empty); + auto data = MakeRefPtr(aDocumentURI, referrerInfo, principal); + RefPtr declaration = DeclarationBlock::FromCssText( + aValue, data, eCompatibility_FullStandards, nullptr, + StyleCssRuleType::Style); + if (declaration) { + mAttributes[aPos].mValue.SetTo(declaration.forget(), &aValue); + + return NS_OK; + } + // Don't abort if parsing failed, it could just be malformed css. + } else if (mAttributes[aPos].mName.Equals(nsGkAtoms::tabindex)) { + mAttributes[aPos].mValue.ParseIntValue(aValue); + + return NS_OK; + } + + mAttributes[aPos].mValue.ParseStringOrAtom(aValue); + + return NS_OK; +} + +void nsXULPrototypeElement::Unlink() { + mAttributes.Clear(); + mChildren.Clear(); +} + +//---------------------------------------------------------------------- +// +// nsXULPrototypeScript +// + +nsXULPrototypeScript::nsXULPrototypeScript(uint32_t aLineNo) + : nsXULPrototypeNode(eType_Script), + mLineNo(aLineNo), + mSrcLoading(false), + mOutOfLine(true), + mSrcLoadWaiters(nullptr), + mStencil(nullptr) {} + +static nsresult WriteStencil(nsIObjectOutputStream* aStream, JSContext* aCx, + JS::Stencil* aStencil) { + JS::TranscodeBuffer buffer; + JS::TranscodeResult code; + code = JS::EncodeStencil(aCx, aStencil, buffer); + + if (code != JS::TranscodeResult::Ok) { + if (code == JS::TranscodeResult::Throw) { + JS_ClearPendingException(aCx); + return NS_ERROR_OUT_OF_MEMORY; + } + + MOZ_ASSERT(IsTranscodeFailureResult(code)); + return NS_ERROR_FAILURE; + } + + size_t size = buffer.length(); + if (size > UINT32_MAX) { + return NS_ERROR_FAILURE; + } + nsresult rv = aStream->Write32(size); + if (NS_SUCCEEDED(rv)) { + // Ideally we could just pass "buffer" here. See bug 1566574. + rv = aStream->WriteBytes(Span(buffer.begin(), size)); + } + + return rv; +} + +static nsresult ReadStencil(nsIObjectInputStream* aStream, JSContext* aCx, + const JS::DecodeOptions& aOptions, + JS::Stencil** aStencilOut) { + // We don't serialize mutedError-ness of scripts, which is fine as long as + // we only serialize system and XUL-y things. We can detect this by checking + // where the caller wants us to deserialize. + // + // CompilationScope() could theoretically GC, so get that out of the way + // before comparing to the cx global. + JSObject* loaderGlobal = xpc::CompilationScope(); + MOZ_RELEASE_ASSERT(nsContentUtils::IsSystemCaller(aCx) || + JS::CurrentGlobalOrNull(aCx) == loaderGlobal); + + uint32_t size; + nsresult rv = aStream->Read32(&size); + if (NS_FAILED(rv)) { + return rv; + } + + char* data; + rv = aStream->ReadBytes(size, &data); + if (NS_FAILED(rv)) { + return rv; + } + + // The decoded stencil shouldn't borrow from the XDR buffer. + MOZ_ASSERT(!aOptions.borrowBuffer); + auto cleanupData = MakeScopeExit([&]() { free(data); }); + + JS::TranscodeRange range(reinterpret_cast(data), size); + + { + JS::TranscodeResult code; + RefPtr stencil; + code = JS::DecodeStencil(aCx, aOptions, range, getter_AddRefs(stencil)); + if (code != JS::TranscodeResult::Ok) { + if (code == JS::TranscodeResult::Throw) { + JS_ClearPendingException(aCx); + return NS_ERROR_OUT_OF_MEMORY; + } + + MOZ_ASSERT(IsTranscodeFailureResult(code)); + return NS_ERROR_FAILURE; + } + + stencil.forget(aStencilOut); + } + + return rv; +} + +void nsXULPrototypeScript::FillCompileOptions(JS::CompileOptions& options) { + // If the script was inline, tell the JS parser to save source for + // Function.prototype.toSource(). If it's out of line, we retrieve the + // source from the files on demand. + options.setSourceIsLazy(mOutOfLine); +} + +nsresult nsXULPrototypeScript::Serialize( + nsIObjectOutputStream* aStream, nsXULPrototypeDocument* aProtoDoc, + const nsTArray>* aNodeInfos) { + NS_ENSURE_TRUE(aProtoDoc, NS_ERROR_UNEXPECTED); + + AutoJSAPI jsapi; + if (!jsapi.Init(xpc::CompilationScope())) { + return NS_ERROR_UNEXPECTED; + } + + NS_ASSERTION(!mSrcLoading || mSrcLoadWaiters != nullptr || !mStencil, + "script source still loading when serializing?!"); + if (!mStencil) return NS_ERROR_FAILURE; + + // Write basic prototype data + nsresult rv; + rv = aStream->Write32(mLineNo); + if (NS_FAILED(rv)) return rv; + + JSContext* cx = jsapi.cx(); + MOZ_ASSERT(xpc::CompilationScope() == JS::CurrentGlobalOrNull(cx)); + + return WriteStencil(aStream, cx, mStencil); +} + +nsresult nsXULPrototypeScript::SerializeOutOfLine( + nsIObjectOutputStream* aStream, nsXULPrototypeDocument* aProtoDoc) { + if (!mSrcURI->SchemeIs("chrome")) + // Don't cache scripts that don't come from chrome uris. + return NS_ERROR_NOT_IMPLEMENTED; + + nsXULPrototypeCache* cache = nsXULPrototypeCache::GetInstance(); + if (!cache) return NS_ERROR_OUT_OF_MEMORY; + + NS_ASSERTION(cache->IsEnabled(), + "writing to the cache file, but the XUL cache is off?"); + bool exists; + cache->HasScript(mSrcURI, &exists); + + /* return will be NS_OK from GetAsciiSpec. + * that makes no sense. + * nor does returning NS_OK from HasMuxedDocument. + * XXX return something meaningful. + */ + if (exists) return NS_OK; + + nsCOMPtr oos; + nsresult rv = cache->GetScriptOutputStream(mSrcURI, getter_AddRefs(oos)); + NS_ENSURE_SUCCESS(rv, rv); + + nsresult tmp = Serialize(oos, aProtoDoc, nullptr); + if (NS_FAILED(tmp)) { + rv = tmp; + } + tmp = cache->FinishScriptOutputStream(mSrcURI); + if (NS_FAILED(tmp)) { + rv = tmp; + } + + if (NS_FAILED(rv)) cache->AbortCaching(); + return rv; +} + +nsresult nsXULPrototypeScript::Deserialize( + nsIObjectInputStream* aStream, nsXULPrototypeDocument* aProtoDoc, + nsIURI* aDocumentURI, + const nsTArray>* aNodeInfos) { + nsresult rv; + NS_ASSERTION(!mSrcLoading || mSrcLoadWaiters != nullptr || !mStencil, + "prototype script not well-initialized when deserializing?!"); + + // Read basic prototype data + rv = aStream->Read32(&mLineNo); + if (NS_FAILED(rv)) return rv; + + AutoJSAPI jsapi; + if (!jsapi.Init(xpc::CompilationScope())) { + return NS_ERROR_UNEXPECTED; + } + JSContext* cx = jsapi.cx(); + + JS::DecodeOptions options; + RefPtr newStencil; + rv = ReadStencil(aStream, cx, options, getter_AddRefs(newStencil)); + NS_ENSURE_SUCCESS(rv, rv); + Set(newStencil); + return NS_OK; +} + +nsresult nsXULPrototypeScript::DeserializeOutOfLine( + nsIObjectInputStream* aInput, nsXULPrototypeDocument* aProtoDoc) { + // Keep track of failure via rv, so we can + // AbortCaching if things look bad. + nsresult rv = NS_OK; + nsXULPrototypeCache* cache = nsXULPrototypeCache::GetInstance(); + + nsCOMPtr objectInput = aInput; + if (cache) { + bool useXULCache = true; + if (mSrcURI) { + // NB: we must check the XUL script cache early, to avoid + // multiple deserialization attempts for a given script. + // Note that PrototypeDocumentContentSink::LoadScript + // checks the XUL script cache too, in order to handle the + // serialization case. + // + // We need do this only for + + + + + + + + + + + + Text for tab ONE + (test case for bug 398289) + + + + + + + + + Text for tab TWO + (When the document is reloaded, this content gets replaced by the one from the first tab.) + + + + + + + + + + diff --git a/dom/xul/test/chrome.ini b/dom/xul/test/chrome.ini new file mode 100644 index 0000000000..a1313d269e --- /dev/null +++ b/dom/xul/test/chrome.ini @@ -0,0 +1,26 @@ +[DEFAULT] +support-files = + 398289-resource.xhtml + window_bug583948.xhtml + window_bug757137.xhtml + +[test_bug199692.xhtml] +[test_bug311681.xhtml] +[test_bug391002.xhtml] +[test_bug398289.html] +[test_bug403868.xhtml] +[test_bug418216.xhtml] +[test_bug445177.xhtml] +[test_bug468176.xhtml] +[test_bug583948.xhtml] +[test_bug757137.xhtml] +[test_bug775972.xhtml] +[test_bug1070049_throw_from_script.xhtml] +[test_html_template.xhtml] +[test_import_xul_to_content.xhtml] +[test_bug1290965.xhtml] +[test_bug749367.xhtml] +[test_xul_tabindex_focus.xhtml] +[test_bug1686822.xhtml] + support-files = window_bug1686822.xhtml +[test_accesskey.xhtml] diff --git a/dom/xul/test/file_bug236853.rdf b/dom/xul/test/file_bug236853.rdf new file mode 100644 index 0000000000..8d17bf6912 --- /dev/null +++ b/dom/xul/test/file_bug236853.rdf @@ -0,0 +1,14 @@ + + + + + + + + + + + + + diff --git a/dom/xul/test/mochitest.ini b/dom/xul/test/mochitest.ini new file mode 100644 index 0000000000..9ec3128ab6 --- /dev/null +++ b/dom/xul/test/mochitest.ini @@ -0,0 +1,3 @@ +[DEFAULT] + +[test_disable_scroll_frame_plain.html] diff --git a/dom/xul/test/test_accesskey.xhtml b/dom/xul/test/test_accesskey.xhtml new file mode 100644 index 0000000000..c71fcf78a0 --- /dev/null +++ b/dom/xul/test/test_accesskey.xhtml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + diff --git a/dom/xul/test/test_bug1070049_throw_from_script.xhtml b/dom/xul/test/test_bug1070049_throw_from_script.xhtml new file mode 100644 index 0000000000..b9c635efd6 --- /dev/null +++ b/dom/xul/test/test_bug1070049_throw_from_script.xhtml @@ -0,0 +1,40 @@ + + + + + + + + + + Mozilla Bug 1070049 + + diff --git a/dom/xul/test/test_bug1290965.xhtml b/dom/xul/test/test_bug1290965.xhtml new file mode 100644 index 0000000000..ce1f7a8522 --- /dev/null +++ b/dom/xul/test/test_bug1290965.xhtml @@ -0,0 +1,38 @@ + + + + + diff --git a/dom/xul/test/test_bug1686822.xhtml b/dom/xul/test/test_bug1686822.xhtml new file mode 100644 index 0000000000..cdad585d00 --- /dev/null +++ b/dom/xul/test/test_bug1686822.xhtml @@ -0,0 +1,35 @@ + + + + + + + +Mozilla Bug 199692 + + + + + + + + + + + +
+  
+
+ +
diff --git a/dom/xul/test/test_bug311681.xhtml b/dom/xul/test/test_bug311681.xhtml new file mode 100644 index 0000000000..8caef2ca5b --- /dev/null +++ b/dom/xul/test/test_bug311681.xhtml @@ -0,0 +1,107 @@ + + + + + + + + + +Mozilla Bug 311681 + +

+ +
+
+ + +
diff --git a/dom/xul/test/test_bug391002.xhtml b/dom/xul/test/test_bug391002.xhtml new file mode 100644 index 0000000000..da8c8b4d35 --- /dev/null +++ b/dom/xul/test/test_bug391002.xhtml @@ -0,0 +1,41 @@ + + + + + + + + diff --git a/dom/xul/test/test_bug398289.html b/dom/xul/test/test_bug398289.html new file mode 100644 index 0000000000..c4873fe2b2 --- /dev/null +++ b/dom/xul/test/test_bug398289.html @@ -0,0 +1,41 @@ + + + + Test for bug 398289 + + + + + + + + + + diff --git a/dom/xul/test/test_bug403868.xhtml b/dom/xul/test/test_bug403868.xhtml new file mode 100644 index 0000000000..c2427bb644 --- /dev/null +++ b/dom/xul/test/test_bug403868.xhtml @@ -0,0 +1,83 @@ + + + + + + + diff --git a/dom/xul/test/test_bug418216.xhtml b/dom/xul/test/test_bug418216.xhtml new file mode 100644 index 0000000000..56e487faf2 --- /dev/null +++ b/dom/xul/test/test_bug418216.xhtml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + diff --git a/dom/xul/test/test_bug445177.xhtml b/dom/xul/test/test_bug445177.xhtml new file mode 100644 index 0000000000..d188f19241 --- /dev/null +++ b/dom/xul/test/test_bug445177.xhtml @@ -0,0 +1,66 @@ + + + + + + + + + +Mozilla Bug 445177 + + + + + +
+  
+
+ +
diff --git a/dom/xul/test/test_bug468176.xhtml b/dom/xul/test/test_bug468176.xhtml new file mode 100644 index 0000000000..f21c1bcde3 --- /dev/null +++ b/dom/xul/test/test_bug468176.xhtml @@ -0,0 +1,84 @@ + + + + + + + + + +Mozilla Bug 468176 + + + + + + + +
+  
+
+ +
diff --git a/dom/xul/test/test_bug583948.xhtml b/dom/xul/test/test_bug583948.xhtml new file mode 100644 index 0000000000..2cab13a4ac --- /dev/null +++ b/dom/xul/test/test_bug583948.xhtml @@ -0,0 +1,41 @@ + + + + + + + + + diff --git a/dom/xul/test/test_bug749367.xhtml b/dom/xul/test/test_bug749367.xhtml new file mode 100644 index 0000000000..3dcea24f1c --- /dev/null +++ b/dom/xul/test/test_bug749367.xhtml @@ -0,0 +1,39 @@ + + + + + + + + + + diff --git a/dom/xul/test/test_bug757137.xhtml b/dom/xul/test/test_bug757137.xhtml new file mode 100644 index 0000000000..a08309bd49 --- /dev/null +++ b/dom/xul/test/test_bug757137.xhtml @@ -0,0 +1,37 @@ + + + + + + + + + diff --git a/dom/xul/test/test_bug775972.xhtml b/dom/xul/test/test_bug775972.xhtml new file mode 100644 index 0000000000..571bf6a6c3 --- /dev/null +++ b/dom/xul/test/test_bug775972.xhtml @@ -0,0 +1,35 @@ + + + + + + + + + + Mozilla Bug 775972 + + + + + + diff --git a/dom/xul/test/test_disable_scroll_frame_plain.html b/dom/xul/test/test_disable_scroll_frame_plain.html new file mode 100644 index 0000000000..8104f55721 --- /dev/null +++ b/dom/xul/test/test_disable_scroll_frame_plain.html @@ -0,0 +1,19 @@ + + + + + disable scroll frame exposed + + + + +

+ +

+
+ + + diff --git a/dom/xul/test/test_html_template.xhtml b/dom/xul/test/test_html_template.xhtml new file mode 100644 index 0000000000..7e6029486e --- /dev/null +++ b/dom/xul/test/test_html_template.xhtml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/dom/xul/test/test_import_xul_to_content.xhtml b/dom/xul/test/test_import_xul_to_content.xhtml new file mode 100644 index 0000000000..32db06e564 --- /dev/null +++ b/dom/xul/test/test_import_xul_to_content.xhtml @@ -0,0 +1,68 @@ + + + + + + + + + Mozilla Bug 1027299 + + + + + + + diff --git a/dom/xul/test/test_xul_tabindex_focus.xhtml b/dom/xul/test/test_xul_tabindex_focus.xhtml new file mode 100644 index 0000000000..1363d9b1e1 --- /dev/null +++ b/dom/xul/test/test_xul_tabindex_focus.xhtml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + diff --git a/dom/xul/test/window_bug1686822.xhtml b/dom/xul/test/window_bug1686822.xhtml new file mode 100644 index 0000000000..1a2cf25636 --- /dev/null +++ b/dom/xul/test/window_bug1686822.xhtml @@ -0,0 +1,7 @@ + + + + + diff --git a/dom/xul/test/window_bug583948.xhtml b/dom/xul/test/window_bug583948.xhtml new file mode 100644 index 0000000000..d0f6a26926 --- /dev/null +++ b/dom/xul/test/window_bug583948.xhtml @@ -0,0 +1,8 @@ + + + + + +