From 222377e8ac81e43b10edb6ef1f601f872d46259f Mon Sep 17 00:00:00 2001 From: Alastor Wu Date: Tue, 25 Apr 2017 12:19:28 +0800 Subject: [PATCH 01/68] Bug 1358061 - remove moz-audiochannel codes for media element and web audio. r=baku MozReview-Commit-ID: BwZ6vHUPnSB --HG-- extra : rebase_source : c92556cf92ff96b8014281396b28e1e232aa8576 --- dom/base/nsGkAtomList.h | 5 - dom/html/HTMLMediaElement.cpp | 91 ----------- dom/html/HTMLMediaElement.h | 13 -- dom/html/test/file_mozaudiochannel.html | 91 ----------- dom/html/test/mochitest.ini | 2 - dom/html/test/test_mozaudiochannel.html | 31 ---- .../html/nsIDOMHTMLMediaElement.idl | 34 ---- dom/media/webaudio/AudioContext.cpp | 15 -- dom/media/webaudio/AudioContext.h | 7 - dom/media/webaudio/AudioDestinationNode.cpp | 3 - dom/media/webaudio/test/mochitest.ini | 2 - .../webaudio/test/test_mozaudiochannel.html | 151 ------------------ dom/webidl/AudioContext.webidl | 21 +-- dom/webidl/HTMLMediaElement.webidl | 15 -- 14 files changed, 1 insertion(+), 480 deletions(-) delete mode 100644 dom/html/test/file_mozaudiochannel.html delete mode 100644 dom/html/test/test_mozaudiochannel.html delete mode 100644 dom/media/webaudio/test/test_mozaudiochannel.html diff --git a/dom/base/nsGkAtomList.h b/dom/base/nsGkAtomList.h index 58014713432c..1558cdc6e0be 100644 --- a/dom/base/nsGkAtomList.h +++ b/dom/base/nsGkAtomList.h @@ -627,7 +627,6 @@ GK_ATOM(mouseout, "mouseout") GK_ATOM(mouseover, "mouseover") GK_ATOM(mousethrough, "mousethrough") GK_ATOM(mouseup, "mouseup") -GK_ATOM(mozaudiochannel, "mozaudiochannel") GK_ATOM(mozfullscreenchange, "mozfullscreenchange") GK_ATOM(mozfullscreenerror, "mozfullscreenerror") GK_ATOM(mozpointerlockchange, "mozpointerlockchange") @@ -1959,10 +1958,6 @@ GK_ATOM(onuserproximity, "onuserproximity") // light sensor support GK_ATOM(ondevicelight, "ondevicelight") -// Audio channel events -GK_ATOM(onmozinterruptbegin, "onmozinterruptbegin") -GK_ATOM(onmozinterruptend, "onmozinterruptend") - // MediaDevices device change event GK_ATOM(ondevicechange, "ondevicechange") diff --git a/dom/html/HTMLMediaElement.cpp b/dom/html/HTMLMediaElement.cpp index cd8e05bbdd76..1fa3f427b850 100644 --- a/dom/html/HTMLMediaElement.cpp +++ b/dom/html/HTMLMediaElement.cpp @@ -1444,23 +1444,6 @@ NS_IMPL_BOOL_ATTR(HTMLMediaElement, Loop, loop) NS_IMPL_BOOL_ATTR(HTMLMediaElement, DefaultMuted, muted) NS_IMPL_ENUM_ATTR_DEFAULT_VALUE(HTMLMediaElement, Preload, preload, nullptr) -NS_IMETHODIMP -HTMLMediaElement::GetMozAudioChannelType(nsAString& aValue) -{ - nsString defaultValue; - AudioChannelService::GetDefaultAudioChannelString(defaultValue); - - NS_ConvertUTF16toUTF8 str(defaultValue); - GetEnumAttr(nsGkAtoms::mozaudiochannel, str.get(), aValue); - return NS_OK; -} - -NS_IMETHODIMP -HTMLMediaElement::SetMozAudioChannelType(const nsAString& aValue) -{ - return SetAttrHelper(nsGkAtoms::mozaudiochannel, aValue); -} - NS_IMETHODIMP_(bool) HTMLMediaElement::IsVideo() { @@ -4142,77 +4125,12 @@ bool HTMLMediaElement::ParseAttribute(int32_t aNamespaceID, if (aAttribute == nsGkAtoms::preload) { return aResult.ParseEnumValue(aValue, kPreloadTable, false); } - - // Remove the b2g-specific audio channel setting in bug1299390. - if (aAttribute == nsGkAtoms::mozaudiochannel) { - const nsAttrValue::EnumTable* table = - AudioChannelService::GetAudioChannelTable(); - MOZ_ASSERT(table); - - bool parsed = aResult.ParseEnumValue(aValue, table, false, &table[0]); - if (!parsed) { - return false; - } - - AudioChannel audioChannel = static_cast(aResult.GetEnumValue()); - - if (audioChannel == mAudioChannel || - !CheckAudioChannelPermissions(aValue)) { - return true; - } - - // We cannot change the AudioChannel of a decoder. - if (mDecoder) { - return true; - } - - mAudioChannel = audioChannel; - - if (mSrcStream) { - RefPtr stream = GetSrcMediaStream(); - if (stream) { - stream->SetAudioChannelType(mAudioChannel); - } - } - - return true; - } } return nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue, aResult); } -bool HTMLMediaElement::CheckAudioChannelPermissions(const nsAString& aString) -{ - // Only normal channel doesn't need permission. - if (aString.EqualsASCII("normal")) { - return true; - } - - // Maybe this audio channel is equal to the default value from the pref. - nsString audioChannel; - AudioChannelService::GetDefaultAudioChannelString(audioChannel); - if (audioChannel.Equals(aString)) { - return true; - } - - nsCOMPtr permissionManager = - services::GetPermissionManager(); - if (!permissionManager) { - return false; - } - - uint32_t perm = nsIPermissionManager::UNKNOWN_ACTION; - permissionManager->TestExactPermissionFromPrincipal(NodePrincipal(), - nsCString(NS_LITERAL_CSTRING("audio-channel-") + NS_ConvertUTF16toUTF8(aString)).get(), &perm); - if (perm != nsIPermissionManager::ALLOW_ACTION) { - return false; - } - - return true; -} - void HTMLMediaElement::DoneCreatingElement() { if (HasAttr(kNameSpaceID_None, nsGkAtoms::muted)) { @@ -7063,15 +6981,6 @@ HTMLMediaElement::GetOrCreateTextTrackManager() return mTextTrackManager; } -void -HTMLMediaElement::SetMozAudioChannelType(AudioChannel aValue, ErrorResult& aRv) -{ - nsString channel; - channel.AssignASCII(AudioChannelValues::strings[uint32_t(aValue)].value, - AudioChannelValues::strings[uint32_t(aValue)].length); - SetHTMLAttr(nsGkAtoms::mozaudiochannel, channel, aRv); -} - MediaDecoderOwner::NextFrameStatus HTMLMediaElement::NextFrameStatus() { diff --git a/dom/html/HTMLMediaElement.h b/dom/html/HTMLMediaElement.h index c5f9f382e9de..539b6ab311aa 100644 --- a/dom/html/HTMLMediaElement.h +++ b/dom/html/HTMLMediaElement.h @@ -693,13 +693,6 @@ public: double MozFragmentEnd(); - AudioChannel MozAudioChannelType() const - { - return mAudioChannel; - } - - void SetMozAudioChannelType(AudioChannel aValue, ErrorResult& aRv); - AudioTrackList* AudioTracks(); VideoTrackList* VideoTracks(); @@ -751,9 +744,6 @@ public: // that will soon be gone. bool IsBeingDestroyed(); - IMPL_EVENT_HANDLER(mozinterruptbegin) - IMPL_EVENT_HANDLER(mozinterruptend) - // These are used for testing only float ComputedVolume() const; bool ComputedMuted() const; @@ -1250,9 +1240,6 @@ protected: void ReportTelemetry(); - // Check the permissions for audiochannel. - bool CheckAudioChannelPermissions(const nsAString& aType); - // Seeks to aTime seconds. aSeekType can be Exact to seek to exactly the // seek target, or PrevSyncPoint if a quicker but less precise seek is // desired, and we'll seek to the sync point (keyframe and/or start of the diff --git a/dom/html/test/file_mozaudiochannel.html b/dom/html/test/file_mozaudiochannel.html deleted file mode 100644 index 588ae338ba9b..000000000000 --- a/dom/html/test/file_mozaudiochannel.html +++ /dev/null @@ -1,91 +0,0 @@ - - - - - - - - diff --git a/dom/html/test/mochitest.ini b/dom/html/test/mochitest.ini index b0870b14e2db..78cb54779b12 100644 --- a/dom/html/test/mochitest.ini +++ b/dom/html/test/mochitest.ini @@ -183,7 +183,6 @@ support-files = reflect.js file_ignoreuserfocus.html simpleFileOpener.js - file_mozaudiochannel.html file_bug1166138_1x.png file_bug1166138_2x.png file_bug1166138_def.png @@ -506,7 +505,6 @@ skip-if = toolkit == 'android' # bug 939642 [test_map_attributes_reflection.html] [test_meta_attributes_reflection.html] [test_mod_attributes_reflection.html] -[test_mozaudiochannel.html] [test_named_options.html] [test_nested_invalid_fieldsets.html] [test_object_attributes_reflection.html] diff --git a/dom/html/test/test_mozaudiochannel.html b/dom/html/test/test_mozaudiochannel.html deleted file mode 100644 index 722e181bf154..000000000000 --- a/dom/html/test/test_mozaudiochannel.html +++ /dev/null @@ -1,31 +0,0 @@ - - - - Test for mozaudiochannel - - - - - - -
-
-
- - diff --git a/dom/interfaces/html/nsIDOMHTMLMediaElement.idl b/dom/interfaces/html/nsIDOMHTMLMediaElement.idl index 7a03320794c9..0cadc614aeea 100644 --- a/dom/interfaces/html/nsIDOMHTMLMediaElement.idl +++ b/dom/interfaces/html/nsIDOMHTMLMediaElement.idl @@ -87,40 +87,6 @@ interface nsIDOMHTMLMediaElement : nsISupports // it is equal to the media duration. readonly attribute double mozFragmentEnd; - // Mozilla extension: an audio channel type for media elements. - // An exception is thrown if the app tries to change the audio channel type - // without the permission (manifest file for B2G apps). - // The supported values are: - // * normal (default value) - // Automatically paused if "notification" or higher priority channel - // is played - // Use case: normal applications - // * content - // Automatically paused if "notification" or higher priority channel - // is played. Also paused if another app starts using "content" - // channel. Using this channel never affects applications using - // the "normal" channel. - // Use case: video/audio players - // * notification - // Automatically paused if "alarm" or higher priority channel is played. - // Use case: New email, incoming SMS - // * alarm - // Automatically paused if "telephony" or higher priority channel is - // played. - // User case: Alarm clock, calendar alarms - // * telephony - // Automatically paused if "ringer" or higher priority - // channel is played. - // Use case: dialer, voip - // * ringer - // Automatically paused if "publicnotification" or higher priority - // channel is played. - // Use case: dialer, voip - // * publicnotification - // Always plays in speaker, even when headphones are plugged in. - // Use case: Camera shutter sound. - attribute DOMString mozAudioChannelType; - // In addition the media element has this new events: // * onmozinterruptbegin - called when the media element is interrupted // because of the audiochannel manager. diff --git a/dom/media/webaudio/AudioContext.cpp b/dom/media/webaudio/AudioContext.cpp index 01f886400ea7..31a825f257f1 100644 --- a/dom/media/webaudio/AudioContext.cpp +++ b/dom/media/webaudio/AudioContext.cpp @@ -1066,21 +1066,6 @@ AudioContext::Unmute() const } } -AudioChannel -AudioContext::MozAudioChannelType() const -{ - return mDestination->MozAudioChannelType(); -} - -AudioChannel -AudioContext::TestAudioChannelInAudioNodeStream() -{ - MediaStream* stream = mDestination->Stream(); - MOZ_ASSERT(stream); - - return stream->AudioChannelType(); -} - size_t AudioContext::SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const { diff --git a/dom/media/webaudio/AudioContext.h b/dom/media/webaudio/AudioContext.h index 7cfd71f2b371..64d2e3577905 100644 --- a/dom/media/webaudio/AudioContext.h +++ b/dom/media/webaudio/AudioContext.h @@ -312,10 +312,6 @@ public: JSObject* GetGlobalJSObject() const; - AudioChannel MozAudioChannelType() const; - - AudioChannel TestAudioChannelInAudioNodeStream(); - void RegisterNode(AudioNode* aNode); void UnregisterNode(AudioNode* aNode); @@ -323,9 +319,6 @@ public: BasicWaveFormCache* GetBasicWaveFormCache(); - IMPL_EVENT_HANDLER(mozinterruptbegin) - IMPL_EVENT_HANDLER(mozinterruptend) - bool CheckClosed(ErrorResult& aRv); void Dispatch(already_AddRefed&& aRunnable); diff --git a/dom/media/webaudio/AudioDestinationNode.cpp b/dom/media/webaudio/AudioDestinationNode.cpp index 98618506994a..abe357e3264e 100644 --- a/dom/media/webaudio/AudioDestinationNode.cpp +++ b/dom/media/webaudio/AudioDestinationNode.cpp @@ -545,9 +545,6 @@ AudioDestinationNode::WindowSuspendChanged(nsSuspendedTypes aSuspend) "this = %p, aSuspend = %s\n", this, SuspendTypeToStr(aSuspend))); mAudioChannelSuspended = suspended; - Context()->DispatchTrustedEvent(!suspended ? - NS_LITERAL_STRING("mozinterruptend") : - NS_LITERAL_STRING("mozinterruptbegin")); DisabledTrackMode disabledMode = suspended ? DisabledTrackMode::SILENCE_BLACK : DisabledTrackMode::ENABLED; diff --git a/dom/media/webaudio/test/mochitest.ini b/dom/media/webaudio/test/mochitest.ini index 4345d79cfead..dce2d7333df3 100644 --- a/dom/media/webaudio/test/mochitest.ini +++ b/dom/media/webaudio/test/mochitest.ini @@ -171,8 +171,6 @@ tags=capturestream tags=capturestream [test_mixingRules.html] skip-if = toolkit == 'android' # bug 1091965 -[test_mozaudiochannel.html] -# Android: bug 1061675; OSX 10.6: bug 1097721 skip-if = (toolkit == 'android') || (os == 'mac' && os_version == '10.6') [test_nodeToParamConnection.html] [test_nodeCreationDocumentGone.html] diff --git a/dom/media/webaudio/test/test_mozaudiochannel.html b/dom/media/webaudio/test/test_mozaudiochannel.html deleted file mode 100644 index 6ba14347bf70..000000000000 --- a/dom/media/webaudio/test/test_mozaudiochannel.html +++ /dev/null @@ -1,151 +0,0 @@ - - - - Test for mozaudiochannel - - - - - -

-
-
-
- - diff --git a/dom/webidl/AudioContext.webidl b/dom/webidl/AudioContext.webidl index cf4bd2acf000..72e3aabb87da 100644 --- a/dom/webidl/AudioContext.webidl +++ b/dom/webidl/AudioContext.webidl @@ -33,23 +33,4 @@ interface AudioContext : BaseAudioContext { [NewObject, Throws] MediaStreamAudioDestinationNode createMediaStreamDestination(); -}; - -// Mozilla extensions -partial interface AudioContext { - // Read AudioChannel.webidl for more information about this attribute. - [Pref="media.useAudioChannelAPI"] - readonly attribute AudioChannel mozAudioChannelType; - - // These 2 events are dispatched when the AudioContext object is muted by - // the AudioChannelService. It's call 'interrupt' because when this event is - // dispatched on a HTMLMediaElement, the audio stream is paused. - [Pref="media.useAudioChannelAPI"] - attribute EventHandler onmozinterruptbegin; - - [Pref="media.useAudioChannelAPI"] - attribute EventHandler onmozinterruptend; - - // This method is for test only. - [ChromeOnly] AudioChannel testAudioChannelInAudioNodeStream(); -}; +}; \ No newline at end of file diff --git a/dom/webidl/HTMLMediaElement.webidl b/dom/webidl/HTMLMediaElement.webidl index 056a8bb1608e..4de2d228953b 100644 --- a/dom/webidl/HTMLMediaElement.webidl +++ b/dom/webidl/HTMLMediaElement.webidl @@ -138,21 +138,6 @@ partial interface HTMLMediaElement { // the media element has a fragment URI for the currentSrc, otherwise // it is equal to the media duration. readonly attribute double mozFragmentEnd; - - // Mozilla extension: an audio channel type for media elements. - // Read AudioChannel.webidl for more information about this attribute. - [SetterThrows, Pref="media.useAudioChannelAPI"] - attribute AudioChannel mozAudioChannelType; - - // In addition the media element has this new events: - // * onmozinterruptbegin - called when the media element is interrupted - // because of the audiochannel manager. - // * onmozinterruptend - called when the interruption is concluded - [Pref="media.useAudioChannelAPI"] - attribute EventHandler onmozinterruptbegin; - - [Pref="media.useAudioChannelAPI"] - attribute EventHandler onmozinterruptend; }; // Encrypted Media Extensions From da6126aa490c49668b15f00779bdd6ba9c59e75a Mon Sep 17 00:00:00 2001 From: Alastor Wu Date: Tue, 25 Apr 2017 12:38:47 +0800 Subject: [PATCH 02/68] Bug 1339449 - Re-enable video-replay-after-audio-end.html crashtest. r=jya MozReview-Commit-ID: Eknt8O0HEfv --HG-- extra : rebase_source : 83894debf8784cd0db5048d67320c29497324704 --- dom/media/test/crashtests/crashtests.list | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dom/media/test/crashtests/crashtests.list b/dom/media/test/crashtests/crashtests.list index 25df24406d7e..d545b01acf21 100644 --- a/dom/media/test/crashtests/crashtests.list +++ b/dom/media/test/crashtests/crashtests.list @@ -98,7 +98,7 @@ HTTP load media-element-source-seek-1.html load offline-buffer-source-ended-1.html load oscillator-ended-1.html load oscillator-ended-2.html -skip-if(Android) load video-replay-after-audio-end.html # bug 1339449 +load video-replay-after-audio-end.html # This needs to run at the end to avoid leaking busted state into other tests. load 691096-1.html load 1236639.html From 4dbe9d9e6da96fe98c5d39b1b9eefe86eda688a0 Mon Sep 17 00:00:00 2001 From: maliu Date: Mon, 24 Apr 2017 01:16:11 +0800 Subject: [PATCH 03/68] Bug 1358089 - [RTL] Separate xml drawable into v17 folder, r=ahunt MozReview-Commit-ID: LaOwxXwhsHA --HG-- extra : rebase_source : c34b558f97c52cd989668aad25d6b7f743db73f9 --- .../base/java/org/mozilla/gecko/toolbar/BrowserToolbar.java | 1 + .../java/org/mozilla/gecko/toolbar/ToolbarEditLayout.java | 4 ++++ .../url_bar_translating_edge.xml | 2 +- .../base/resources/drawable/url_bar_translating_edge.xml | 2 +- 4 files changed, 7 insertions(+), 2 deletions(-) rename mobile/android/base/resources/{drawable-ldrtl-v17 => drawable-v17}/url_bar_translating_edge.xml (93%) diff --git a/mobile/android/base/java/org/mozilla/gecko/toolbar/BrowserToolbar.java b/mobile/android/base/java/org/mozilla/gecko/toolbar/BrowserToolbar.java index 3b4e0e0bc0ec..b7de5d2eb053 100644 --- a/mobile/android/base/java/org/mozilla/gecko/toolbar/BrowserToolbar.java +++ b/mobile/android/base/java/org/mozilla/gecko/toolbar/BrowserToolbar.java @@ -367,6 +367,7 @@ public abstract class BrowserToolbar extends ThemedRelativeLayout public void refresh() { progressBar.setImageDrawable(getResources().getDrawable(R.drawable.progress)); urlDisplayLayout.dismissSiteIdentityPopup(); + urlEditLayout.refresh(); } public boolean onBackPressed() { diff --git a/mobile/android/base/java/org/mozilla/gecko/toolbar/ToolbarEditLayout.java b/mobile/android/base/java/org/mozilla/gecko/toolbar/ToolbarEditLayout.java index c9731a4014d6..a37ff2a3188b 100644 --- a/mobile/android/base/java/org/mozilla/gecko/toolbar/ToolbarEditLayout.java +++ b/mobile/android/base/java/org/mozilla/gecko/toolbar/ToolbarEditLayout.java @@ -203,6 +203,10 @@ public class ToolbarEditLayout extends ThemedLinearLayout { } } + public void refresh() { + mEditText.setHint(getResources().getString(R.string.url_bar_default_text)); + } + void setToolbarPrefs(final ToolbarPrefs prefs) { mEditText.setToolbarPrefs(prefs); } diff --git a/mobile/android/base/resources/drawable-ldrtl-v17/url_bar_translating_edge.xml b/mobile/android/base/resources/drawable-v17/url_bar_translating_edge.xml similarity index 93% rename from mobile/android/base/resources/drawable-ldrtl-v17/url_bar_translating_edge.xml rename to mobile/android/base/resources/drawable-v17/url_bar_translating_edge.xml index 0a304c5e5a51..3cfa9daf3f3b 100644 --- a/mobile/android/base/resources/drawable-ldrtl-v17/url_bar_translating_edge.xml +++ b/mobile/android/base/resources/drawable-v17/url_bar_translating_edge.xml @@ -6,4 +6,4 @@ \ No newline at end of file + android:gravity="end"/> \ No newline at end of file diff --git a/mobile/android/base/resources/drawable/url_bar_translating_edge.xml b/mobile/android/base/resources/drawable/url_bar_translating_edge.xml index 334d49929a85..379499284a09 100644 --- a/mobile/android/base/resources/drawable/url_bar_translating_edge.xml +++ b/mobile/android/base/resources/drawable/url_bar_translating_edge.xml @@ -6,4 +6,4 @@ \ No newline at end of file + android:gravity="right"/> \ No newline at end of file From 684adde04005be2b234abc3468742a9543699307 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emilio=20Cobos=20=C3=81lvarez?= Date: Tue, 25 Apr 2017 20:31:00 -0500 Subject: [PATCH 04/68] servo: Merge #16608 - style: Move all the fixup code into a StyleAdjuster struct (from emilio:refactor-fixup); r=jryans This will allow reusing it from text styles, which we need for some corner cases, like text-align: -moz-center and similar stuff. Source-Repo: https://github.com/servo/servo Source-Revision: 09f0ff74811405eb95200ee78d936b4378de65fb --HG-- extra : subtree_source : https%3A//hg.mozilla.org/projects/converted-servo-linear extra : subtree_revision : 8a14ec6fc96a1c13823feaffc5d3723722f1806b --- servo/components/style/lib.rs | 1 + .../components/style/properties/gecko.mako.rs | 10 +- .../style/properties/longhand/box.mako.rs | 48 ++++ .../style/properties/properties.mako.rs | 255 ++++-------------- servo/components/style/style_adjuster.rs | 250 +++++++++++++++++ 5 files changed, 356 insertions(+), 208 deletions(-) create mode 100644 servo/components/style/style_adjuster.rs diff --git a/servo/components/style/lib.rs b/servo/components/style/lib.rs index ec77062a53c0..74f5cc5d4b68 100644 --- a/servo/components/style/lib.rs +++ b/servo/components/style/lib.rs @@ -116,6 +116,7 @@ pub mod stylist; pub mod sequential; pub mod sink; pub mod str; +pub mod style_adjuster; pub mod stylesheet_set; pub mod stylesheets; pub mod supports; diff --git a/servo/components/style/properties/gecko.mako.rs b/servo/components/style/properties/gecko.mako.rs index 1dace6b6b32d..5190d6c3baf7 100644 --- a/servo/components/style/properties/gecko.mako.rs +++ b/servo/components/style/properties/gecko.mako.rs @@ -179,6 +179,12 @@ impl ComputedValues { !self.get_box().gecko.mBinding.mPtr.mRawPtr.is_null() } + #[allow(non_snake_case)] + pub fn in_top_layer(&self) -> bool { + matches!(self.get_box().clone__moz_top_layer(), + longhands::_moz_top_layer::SpecifiedValue::top) + } + // FIXME(bholley): Implement this properly. #[inline] pub fn is_multicol(&self) -> bool { false } @@ -1814,7 +1820,9 @@ fn static_assert() { /// Set the display value from the style adjustment code. This is pretty /// much like set_display, but without touching the mOriginalDisplay field, /// which we want to keep. - pub fn set_adjusted_display(&mut self, v: longhands::display::computed_value::T) { + pub fn set_adjusted_display(&mut self, + v: longhands::display::computed_value::T, + _is_item_or_root: bool) { use properties::longhands::display::computed_value::T as Keyword; let result = match v { % for value in display_keyword.values_for('gecko'): diff --git a/servo/components/style/properties/longhand/box.mako.rs b/servo/components/style/properties/longhand/box.mako.rs index 580cd41c8c96..6af83510664b 100644 --- a/servo/components/style/properties/longhand/box.mako.rs +++ b/servo/components/style/properties/longhand/box.mako.rs @@ -36,6 +36,54 @@ pub mod computed_value { pub use super::SpecifiedValue as T; + + impl T { + /// Returns whether this "display" value is the display of a flex or + /// grid container. + /// + /// This is used to implement various style fixups. + pub fn is_item_container(&self) -> bool { + matches!(*self, + T::flex + | T::inline_flex + % if product == "gecko": + | T::grid + | T::inline_grid + % endif + ) + } + + /// Convert this display into an equivalent block display. + /// + /// Also used for style adjustments. + pub fn equivalent_block_display(&self, _is_root_element: bool) -> Self { + match *self { + // Values that have a corresponding block-outside version. + T::inline_table => T::table, + T::inline_flex => T::flex, + + % if product == "gecko": + T::inline_grid => T::grid, + T::_webkit_inline_box => T::_webkit_box, + % endif + + // Special handling for contents and list-item on the root + // element for Gecko. + % if product == "gecko": + T::contents | T::list_item if _is_root_element => T::block, + % endif + + // These are not changed by blockification. + T::none | T::block | T::flex | T::list_item | T::table => *self, + % if product == "gecko": + T::contents | T::flow_root | T::grid | T::_webkit_box => *self, + % endif + + // Everything else becomes block. + _ => T::block, + } + } + } } #[allow(non_camel_case_types)] diff --git a/servo/components/style/properties/properties.mako.rs b/servo/components/style/properties/properties.mako.rs index 8502d4f29af4..0b34c5193e27 100644 --- a/servo/components/style/properties/properties.mako.rs +++ b/servo/components/style/properties/properties.mako.rs @@ -37,6 +37,7 @@ use stylesheets::{CssRuleType, Origin, UrlExtraData}; use values::{HasViewportPercentage, computed}; use cascade_info::CascadeInfo; use rule_tree::StrongRuleNode; +use style_adjuster::StyleAdjuster; #[cfg(feature = "servo")] use values::specified::BorderStyle; pub use self::declaration_block::*; @@ -1568,6 +1569,18 @@ pub mod style_structs { pub fn has_line_through(&self) -> bool { self.text_decoration_line.contains(longhands::text_decoration_line::LINE_THROUGH) } + % elif style_struct.name == "Box": + /// Sets the display property, but without touching + /// __servo_display_for_hypothetical_box, except when the + /// adjustment comes from root or item display fixups. + pub fn set_adjusted_display(&mut self, + dpy: longhands::display::computed_value::T, + is_item_or_root: bool) { + self.set_display(dpy); + if is_item_or_root { + self.set__servo_display_for_hypothetical_box(dpy); + } + } % endif } @@ -1725,6 +1738,11 @@ impl ComputedValues { /// Servo for obvious reasons. pub fn has_moz_binding(&self) -> bool { false } + /// Whether this style has a top-layer style. That's implemented in Gecko + /// via the -moz-top-layer property, but servo doesn't have any concept of a + /// top layer (yet, it's needed for fullscreen). + pub fn in_top_layer(&self) -> bool { false } + /// Returns whether this style's display value is equal to contents. /// /// Since this isn't supported in Servo, this is always false for Servo. @@ -1943,6 +1961,21 @@ impl ComputedValues { } } +impl ComputedValues { + /// Returns whether this computed style represents a floated object. + pub fn floated(&self) -> bool { + self.get_box().clone_float() != longhands::float::computed_value::T::none + } + + /// Returns whether this computed style represents an out of flow-positioned + /// object. + pub fn out_of_flow_positioned(&self) -> bool { + use properties::longhands::position::computed_value::T as position; + matches!(self.get_box().clone_position(), + position::absolute | position::fixed) + } +} + /// Return a WritingMode bitflags from the relevant CSS properties. pub fn get_writing_mode(inheritedbox_style: &style_structs::InheritedBox) -> WritingMode { @@ -2339,207 +2372,9 @@ pub fn apply_declarations<'a, F, I>(device: &Device, let mut style = context.style; - let mut positioned = matches!(style.get_box().clone_position(), - longhands::position::SpecifiedValue::absolute | - longhands::position::SpecifiedValue::fixed); - - // https://fullscreen.spec.whatwg.org/#new-stacking-layer - // Any position value other than 'absolute' and 'fixed' are - // computed to 'absolute' if the element is in a top layer. - % if product == "gecko": - if !positioned && - matches!(style.get_box().clone__moz_top_layer(), - longhands::_moz_top_layer::SpecifiedValue::top) { - positioned = true; - style.mutate_box().set_position(longhands::position::computed_value::T::absolute); - } - % endif - - let positioned = positioned; // To ensure it's not mutated further. - - let floated = style.get_box().clone_float() != longhands::float::computed_value::T::none; - let is_item = matches!(context.layout_parent_style.get_box().clone_display(), - % if product == "gecko": - computed_values::display::T::grid | - computed_values::display::T::inline_grid | - % endif - computed_values::display::T::flex | - computed_values::display::T::inline_flex); - - let (blockify_root, blockify_item) = - if flags.contains(SKIP_ROOT_AND_ITEM_BASED_DISPLAY_FIXUP) { - (false, false) - } else { - (is_root_element, is_item) - }; - - if positioned || floated || blockify_root || blockify_item { - use computed_values::display::T; - - let specified_display = style.get_box().clone_display(); - let computed_display = match specified_display { - // Values that have a corresponding block-outside version. - T::inline_table => Some(T::table), - % if product == "gecko": - T::inline_flex => Some(T::flex), - T::inline_grid => Some(T::grid), - T::_webkit_inline_box => Some(T::_webkit_box), - % endif - - // Special handling for contents and list-item on the root element for Gecko. - % if product == "gecko": - T::contents | T::list_item if blockify_root => Some(T::block), - % endif - - // Values that are not changed by blockification. - T::none | T::block | T::flex | T::list_item | T::table => None, - % if product == "gecko": - T::contents | T::flow_root | T::grid | T::_webkit_box => None, - % endif - - // Everything becomes block. - _ => Some(T::block), - }; - if let Some(computed_display) = computed_display { - let box_ = style.mutate_box(); - % if product == "servo": - box_.set_display(computed_display); - box_.set__servo_display_for_hypothetical_box(if blockify_root || blockify_item { - computed_display - } else { - specified_display - }); - % else: - box_.set_adjusted_display(computed_display); - % endif - } - } - - { - use computed_values::display::T as display; - // CSS writing modes spec (https://drafts.csswg.org/css-writing-modes-3/#block-flow): - // - // If a box has a different writing-mode value than its containing block: - // - If the box has a specified display of inline, its display computes to inline-block. [CSS21] - // - // www-style mail regarding above spec: https://lists.w3.org/Archives/Public/www-style/2017Mar/0045.html - // See https://github.com/servo/servo/issues/15754 - let our_writing_mode = style.get_inheritedbox().clone_writing_mode(); - let parent_writing_mode = context.layout_parent_style.get_inheritedbox().clone_writing_mode(); - if our_writing_mode != parent_writing_mode && - style.get_box().clone_display() == display::inline { - style.mutate_box().set_display(display::inline_block); - } - } - - { - use computed_values::overflow_x::T as overflow; - - let original_overflow_x = style.get_box().clone_overflow_x(); - let original_overflow_y = style.get_box().clone_overflow_y(); - let mut overflow_x = original_overflow_x; - let mut overflow_y = original_overflow_y; - - // CSS3 overflow-x and overflow-y require some fixup as well in some - // cases. overflow: clip and overflow: visible are meaningful only when - // used in both dimensions. - if overflow_x != overflow_y { - // If 'visible' is specified but doesn't match the other dimension, - // it turns into 'auto'. - if overflow_x == overflow::visible { - overflow_x = overflow::auto; - } - if overflow_y == overflow::visible { - overflow_y = overflow::auto; - } - - % if product == "gecko": - // overflow: clip is deprecated, so convert to hidden if it's - // specified in only one dimension. - if overflow_x == overflow::_moz_hidden_unscrollable { - overflow_x = overflow::hidden; - } - if overflow_y == overflow::_moz_hidden_unscrollable { - overflow_y = overflow::hidden; - } - % endif - } - - % if product == "gecko": - use properties::longhands::contain; - // When 'contain: paint', update overflow from 'visible' to 'clip'. - if style.get_box().clone_contain().contains(contain::PAINT) { - if overflow_x == overflow::visible { - overflow_x = overflow::_moz_hidden_unscrollable; - } - if overflow_y == overflow::visible { - overflow_y = overflow::_moz_hidden_unscrollable; - } - } - % endif - - if overflow_x != original_overflow_x || - overflow_y != original_overflow_y { - let mut box_style = style.mutate_box(); - box_style.set_overflow_x(overflow_x); - box_style.set_overflow_y(overflow_y); - } - } - - % if product == "gecko": - { - use computed_values::display::T as display; - use properties::longhands::contain; - // An element with contain:paint or contain:layout needs to "be a - // formatting context" - let contain = style.get_box().clone_contain(); - if contain.contains(contain::PAINT) && - style.get_box().clone_display() == display::inline { - style.mutate_box().set_adjusted_display(display::inline_block); - } - } - % endif - - // CSS 2.1 section 9.7: - // - // If 'position' has the value 'absolute' or 'fixed', [...] the computed - // value of 'float' is 'none'. - // - if positioned && floated { - style.mutate_box().set_float(longhands::float::computed_value::T::none); - } - - // This implements an out-of-date spec. The new spec moves the handling of - // this to layout, which Gecko implements but Servo doesn't. - // - // See https://github.com/servo/servo/issues/15229 - % if product == "servo" and "align-items" in data.longhands_by_name: - { - use computed_values::align_self::T as align_self; - use computed_values::align_items::T as align_items; - if style.get_position().clone_align_self() == computed_values::align_self::T::auto && !positioned { - let self_align = - match context.layout_parent_style.get_position().clone_align_items() { - align_items::stretch => align_self::stretch, - align_items::baseline => align_self::baseline, - align_items::flex_start => align_self::flex_start, - align_items::flex_end => align_self::flex_end, - align_items::center => align_self::center, - }; - style.mutate_position().set_align_self(self_align); - } - } - % endif - - // The initial value of border-*-width may be changed at computed value time. - % for side in ["top", "right", "bottom", "left"]: - // Like calling to_computed_value, which wouldn't type check. - if style.get_border().clone_border_${side}_style().none_or_hidden() && - style.get_border().border_${side}_has_nonzero_width() { - style.mutate_border().set_border_${side}_width(Au(0)); - } - % endfor - + StyleAdjuster::new(&mut style, is_root_element) + .adjust(context.layout_parent_style, + flags.contains(SKIP_ROOT_AND_ITEM_BASED_DISPLAY_FIXUP)); % if product == "gecko": // FIXME(emilio): This is effectively creating a new nsStyleBackground @@ -2549,12 +2384,6 @@ pub fn apply_declarations<'a, F, I>(device: &Device, style.mutate_svg().fill_arrays(); % endif - // The initial value of outline width may be changed at computed value time. - if style.get_outline().clone_outline_style().none_or_hidden() && - style.get_outline().outline_has_nonzero_width() { - style.mutate_outline().set_outline_width(Au(0)); - } - if is_root_element { let s = style.get_font().clone_font_size(); style.root_font_size = s; @@ -2572,6 +2401,18 @@ pub fn apply_declarations<'a, F, I>(device: &Device, style } + +/// See StyleAdjuster::adjust_for_border_width. +pub fn adjust_border_width(style: &mut ComputedValues) { + % for side in ["top", "right", "bottom", "left"]: + // Like calling to_computed_value, which wouldn't type check. + if style.get_border().clone_border_${side}_style().none_or_hidden() && + style.get_border().border_${side}_has_nonzero_width() { + style.mutate_border().set_border_${side}_width(Au(0)); + } + % endfor +} + /// Adjusts borders as appropriate to account for a fragment's status as the /// first or last fragment within the range of an element. /// diff --git a/servo/components/style/style_adjuster.rs b/servo/components/style/style_adjuster.rs new file mode 100644 index 000000000000..1cc2fa7470f9 --- /dev/null +++ b/servo/components/style/style_adjuster.rs @@ -0,0 +1,250 @@ +/* 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 struct to encapsulate all the style fixups a computed style needs in order +//! for it to adhere to the CSS spec. + +use app_units::Au; +use properties::{self, ComputedValues}; +use properties::longhands::display::computed_value::T as display; +use properties::longhands::float::computed_value::T as float; +use properties::longhands::overflow_x::computed_value::T as overflow; +use properties::longhands::position::computed_value::T as position; + + +/// An unsized struct that implements all the adjustment methods. +pub struct StyleAdjuster<'a> { + style: &'a mut ComputedValues, + is_root_element: bool, +} + +impl<'a> StyleAdjuster<'a> { + /// Trivially constructs a new StyleAdjuster. + pub fn new(style: &'a mut ComputedValues, is_root_element: bool) -> Self { + StyleAdjuster { + style: style, + is_root_element: is_root_element, + } + } + + /// https://fullscreen.spec.whatwg.org/#new-stacking-layer + /// + /// Any position value other than 'absolute' and 'fixed' are + /// computed to 'absolute' if the element is in a top layer. + /// + fn adjust_for_top_layer(&mut self) { + if !self.style.out_of_flow_positioned() && self.style.in_top_layer() { + self.style.mutate_box().set_position(position::absolute); + } + } + + /// CSS 2.1 section 9.7: + /// + /// If 'position' has the value 'absolute' or 'fixed', [...] the computed + /// value of 'float' is 'none'. + /// + fn adjust_for_position(&mut self) { + if self.style.out_of_flow_positioned() && self.style.floated() { + self.style.mutate_box().set_float(float::none); + } + } + + fn blockify_if_necessary(&mut self, + layout_parent_style: &ComputedValues, + skip_root_and_element_display_fixup: bool) { + let mut blockify = false; + macro_rules! blockify_if { + ($if_what:expr) => { + if !blockify { + blockify = $if_what; + } + } + } + + if !skip_root_and_element_display_fixup { + blockify_if!(self.is_root_element); + blockify_if!(layout_parent_style.get_box().clone_display().is_item_container()); + } + + let is_item_or_root = blockify; + + blockify_if!(self.style.floated()); + blockify_if!(self.style.out_of_flow_positioned()); + + if !blockify { + return; + } + + let display = self.style.get_box().clone_display(); + let blockified_display = + display.equivalent_block_display(self.is_root_element); + if display != blockified_display { + self.style.mutate_box().set_adjusted_display(blockified_display, + is_item_or_root); + } + } + + /// https://drafts.csswg.org/css-writing-modes-3/#block-flow: + /// + /// If a box has a different writing-mode value than its containing + /// block: + /// + /// - If the box has a specified display of inline, its display + /// computes to inline-block. [CSS21] + /// + /// This matches the adjustment that Gecko does, not exactly following + /// the spec. See also: + /// + /// https://lists.w3.org/Archives/Public/www-style/2017Mar/0045.html + /// https://github.com/servo/servo/issues/15754 + fn adjust_for_writing_mode(&mut self, + layout_parent_style: &ComputedValues) { + let our_writing_mode = self.style.get_inheritedbox().clone_writing_mode(); + let parent_writing_mode = layout_parent_style.get_inheritedbox().clone_writing_mode(); + + if our_writing_mode != parent_writing_mode && + self.style.get_box().clone_display() == display::inline { + self.style.mutate_box().set_display(display::inline_block); + } + } + + #[cfg(feature = "gecko")] + fn adjust_for_contain(&mut self) { + use properties::longhands::contain; + + // An element with contain: paint needs to be a formatting context, and + // also implies overflow: clip. + // + // TODO(emilio): This mimics Gecko, but spec links are missing! + let contain = self.style.get_box().clone_contain(); + if !contain.contains(contain::PAINT) { + return; + } + + if self.style.get_box().clone_display() == display::inline { + self.style.mutate_box().set_adjusted_display(display::inline_block, + false); + } + + + // When 'contain: paint', update overflow from 'visible' to 'clip'. + if self.style.get_box().clone_contain().contains(contain::PAINT) { + if self.style.get_box().clone_overflow_x() == overflow::visible { + let mut box_style = self.style.mutate_box(); + box_style.set_overflow_x(overflow::_moz_hidden_unscrollable); + box_style.set_overflow_y(overflow::_moz_hidden_unscrollable); + } + } + } + + /// This implements an out-of-date spec. The new spec moves the handling of + /// this to layout, which Gecko implements but Servo doesn't. + /// + /// See https://github.com/servo/servo/issues/15229 + #[cfg(feature = "servo")] + fn adjust_for_alignment(&mut self, layout_parent_style: &ComputedValues) { + use computed_values::align_items::T as align_items; + use computed_values::align_self::T as align_self; + + if self.style.get_position().clone_align_self() == align_self::auto && + !self.style.out_of_flow_positioned() { + let self_align = + match layout_parent_style.get_position().clone_align_items() { + align_items::stretch => align_self::stretch, + align_items::baseline => align_self::baseline, + align_items::flex_start => align_self::flex_start, + align_items::flex_end => align_self::flex_end, + align_items::center => align_self::center, + }; + self.style.mutate_position().set_align_self(self_align); + } + } + + /// The initial value of border-*-width may be changed at computed value + /// time. + /// + /// This is moved to properties.rs for convenience. + fn adjust_for_border_width(&mut self) { + properties::adjust_border_width(self.style); + } + + /// The initial value of outline-width may be changed at computed value time. + fn adjust_for_outline(&mut self) { + if self.style.get_outline().clone_outline_style().none_or_hidden() && + self.style.get_outline().outline_has_nonzero_width() { + self.style.mutate_outline().set_outline_width(Au(0)); + } + } + + /// CSS3 overflow-x and overflow-y require some fixup as well in some + /// cases. + /// + /// overflow: clip and overflow: visible are meaningful only when used in + /// both dimensions. + fn adjust_for_overflow(&mut self) { + let original_overflow_x = self.style.get_box().clone_overflow_x(); + let original_overflow_y = self.style.get_box().clone_overflow_y(); + + let mut overflow_x = original_overflow_x; + let mut overflow_y = original_overflow_y; + + if overflow_x == overflow_y { + return; + } + + // If 'visible' is specified but doesn't match the other dimension, + // it turns into 'auto'. + if overflow_x == overflow::visible { + overflow_x = overflow::auto; + } + + if overflow_y == overflow::visible { + overflow_y = overflow::auto; + } + + #[cfg(feature = "gecko")] + { + // overflow: clip is deprecated, so convert to hidden if it's + // specified in only one dimension. + if overflow_x == overflow::_moz_hidden_unscrollable { + overflow_x = overflow::hidden; + } + if overflow_y == overflow::_moz_hidden_unscrollable { + overflow_y = overflow::hidden; + } + } + + if overflow_x != original_overflow_x || + overflow_y != original_overflow_y { + let mut box_style = self.style.mutate_box(); + box_style.set_overflow_x(overflow_x); + box_style.set_overflow_y(overflow_y); + } + } + + /// Adjusts the style to account for display fixups. + pub fn adjust(mut self, + layout_parent_style: &ComputedValues, + skip_root_and_element_display_fixup: bool) { + self.adjust_for_top_layer(); + self.blockify_if_necessary(layout_parent_style, + skip_root_and_element_display_fixup); + self.adjust_for_writing_mode(layout_parent_style); + self.adjust_for_position(); + + self.adjust_for_overflow(); + #[cfg(feature = "gecko")] + { + self.adjust_for_contain(); + } + + #[cfg(feature = "servo")] + { + self.adjust_for_alignment(layout_parent_style); + } + + self.adjust_for_border_width(); + self.adjust_for_outline(); + } +} From 8d69ee37e909c1237e8fe37e6380d87d31706352 Mon Sep 17 00:00:00 2001 From: Manish Goregaokar Date: Tue, 25 Apr 2017 21:31:14 -0500 Subject: [PATCH 05/68] servo: Merge #16611 - stylo: Add support for -moz-control-character-visibility (from Manishearth:stylo-controlchar); r=heycam Source-Repo: https://github.com/servo/servo Source-Revision: 5daf92dd5a33599fab399ce7432fb62a6aa4e554 --HG-- extra : subtree_source : https%3A//hg.mozilla.org/projects/converted-servo-linear extra : subtree_revision : fcbf0b1bcaa0da576e0a1b4a08c33fbad65afcdf --- .../style/properties/longhand/inherited_text.mako.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/servo/components/style/properties/longhand/inherited_text.mako.rs b/servo/components/style/properties/longhand/inherited_text.mako.rs index e40791293286..57385c982179 100644 --- a/servo/components/style/properties/longhand/inherited_text.mako.rs +++ b/servo/components/style/properties/longhand/inherited_text.mako.rs @@ -1265,3 +1265,11 @@ ${helpers.single_keyword("text-rendering", "auto optimizespeed optimizelegibility geometricprecision", animation_value_type="none", spec="https://www.w3.org/TR/SVG11/painting.html#TextRenderingProperty")} + +${helpers.single_keyword("-moz-control-character-visibility", + "hidden visible", + gecko_constant_prefix="NS_STYLE_CONTROL_CHARACTER_VISIBILITY", + gecko_ffi_name="mControlCharacterVisibility", + animation_value_type="none", + products="gecko", + spec="Nonstandard")} From 6b3d58414b76448ae92f39c921fb4fa048a1c33f Mon Sep 17 00:00:00 2001 From: Phil Ringnalda Date: Tue, 25 Apr 2017 20:55:30 -0700 Subject: [PATCH 06/68] Backed out changeset 6e7efb9115fd (bug 1358061) for build bustage CLOSED TREE MozReview-Commit-ID: F9FSciVgynw --- dom/base/nsGkAtomList.h | 5 + dom/html/HTMLMediaElement.cpp | 91 +++++++++++ dom/html/HTMLMediaElement.h | 13 ++ dom/html/test/file_mozaudiochannel.html | 91 +++++++++++ dom/html/test/mochitest.ini | 2 + dom/html/test/test_mozaudiochannel.html | 31 ++++ .../html/nsIDOMHTMLMediaElement.idl | 34 ++++ dom/media/webaudio/AudioContext.cpp | 15 ++ dom/media/webaudio/AudioContext.h | 7 + dom/media/webaudio/AudioDestinationNode.cpp | 3 + dom/media/webaudio/test/mochitest.ini | 2 + .../webaudio/test/test_mozaudiochannel.html | 151 ++++++++++++++++++ dom/webidl/AudioContext.webidl | 21 ++- dom/webidl/HTMLMediaElement.webidl | 15 ++ 14 files changed, 480 insertions(+), 1 deletion(-) create mode 100644 dom/html/test/file_mozaudiochannel.html create mode 100644 dom/html/test/test_mozaudiochannel.html create mode 100644 dom/media/webaudio/test/test_mozaudiochannel.html diff --git a/dom/base/nsGkAtomList.h b/dom/base/nsGkAtomList.h index 1558cdc6e0be..58014713432c 100644 --- a/dom/base/nsGkAtomList.h +++ b/dom/base/nsGkAtomList.h @@ -627,6 +627,7 @@ GK_ATOM(mouseout, "mouseout") GK_ATOM(mouseover, "mouseover") GK_ATOM(mousethrough, "mousethrough") GK_ATOM(mouseup, "mouseup") +GK_ATOM(mozaudiochannel, "mozaudiochannel") GK_ATOM(mozfullscreenchange, "mozfullscreenchange") GK_ATOM(mozfullscreenerror, "mozfullscreenerror") GK_ATOM(mozpointerlockchange, "mozpointerlockchange") @@ -1958,6 +1959,10 @@ GK_ATOM(onuserproximity, "onuserproximity") // light sensor support GK_ATOM(ondevicelight, "ondevicelight") +// Audio channel events +GK_ATOM(onmozinterruptbegin, "onmozinterruptbegin") +GK_ATOM(onmozinterruptend, "onmozinterruptend") + // MediaDevices device change event GK_ATOM(ondevicechange, "ondevicechange") diff --git a/dom/html/HTMLMediaElement.cpp b/dom/html/HTMLMediaElement.cpp index 1fa3f427b850..cd8e05bbdd76 100644 --- a/dom/html/HTMLMediaElement.cpp +++ b/dom/html/HTMLMediaElement.cpp @@ -1444,6 +1444,23 @@ NS_IMPL_BOOL_ATTR(HTMLMediaElement, Loop, loop) NS_IMPL_BOOL_ATTR(HTMLMediaElement, DefaultMuted, muted) NS_IMPL_ENUM_ATTR_DEFAULT_VALUE(HTMLMediaElement, Preload, preload, nullptr) +NS_IMETHODIMP +HTMLMediaElement::GetMozAudioChannelType(nsAString& aValue) +{ + nsString defaultValue; + AudioChannelService::GetDefaultAudioChannelString(defaultValue); + + NS_ConvertUTF16toUTF8 str(defaultValue); + GetEnumAttr(nsGkAtoms::mozaudiochannel, str.get(), aValue); + return NS_OK; +} + +NS_IMETHODIMP +HTMLMediaElement::SetMozAudioChannelType(const nsAString& aValue) +{ + return SetAttrHelper(nsGkAtoms::mozaudiochannel, aValue); +} + NS_IMETHODIMP_(bool) HTMLMediaElement::IsVideo() { @@ -4125,12 +4142,77 @@ bool HTMLMediaElement::ParseAttribute(int32_t aNamespaceID, if (aAttribute == nsGkAtoms::preload) { return aResult.ParseEnumValue(aValue, kPreloadTable, false); } + + // Remove the b2g-specific audio channel setting in bug1299390. + if (aAttribute == nsGkAtoms::mozaudiochannel) { + const nsAttrValue::EnumTable* table = + AudioChannelService::GetAudioChannelTable(); + MOZ_ASSERT(table); + + bool parsed = aResult.ParseEnumValue(aValue, table, false, &table[0]); + if (!parsed) { + return false; + } + + AudioChannel audioChannel = static_cast(aResult.GetEnumValue()); + + if (audioChannel == mAudioChannel || + !CheckAudioChannelPermissions(aValue)) { + return true; + } + + // We cannot change the AudioChannel of a decoder. + if (mDecoder) { + return true; + } + + mAudioChannel = audioChannel; + + if (mSrcStream) { + RefPtr stream = GetSrcMediaStream(); + if (stream) { + stream->SetAudioChannelType(mAudioChannel); + } + } + + return true; + } } return nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue, aResult); } +bool HTMLMediaElement::CheckAudioChannelPermissions(const nsAString& aString) +{ + // Only normal channel doesn't need permission. + if (aString.EqualsASCII("normal")) { + return true; + } + + // Maybe this audio channel is equal to the default value from the pref. + nsString audioChannel; + AudioChannelService::GetDefaultAudioChannelString(audioChannel); + if (audioChannel.Equals(aString)) { + return true; + } + + nsCOMPtr permissionManager = + services::GetPermissionManager(); + if (!permissionManager) { + return false; + } + + uint32_t perm = nsIPermissionManager::UNKNOWN_ACTION; + permissionManager->TestExactPermissionFromPrincipal(NodePrincipal(), + nsCString(NS_LITERAL_CSTRING("audio-channel-") + NS_ConvertUTF16toUTF8(aString)).get(), &perm); + if (perm != nsIPermissionManager::ALLOW_ACTION) { + return false; + } + + return true; +} + void HTMLMediaElement::DoneCreatingElement() { if (HasAttr(kNameSpaceID_None, nsGkAtoms::muted)) { @@ -6981,6 +7063,15 @@ HTMLMediaElement::GetOrCreateTextTrackManager() return mTextTrackManager; } +void +HTMLMediaElement::SetMozAudioChannelType(AudioChannel aValue, ErrorResult& aRv) +{ + nsString channel; + channel.AssignASCII(AudioChannelValues::strings[uint32_t(aValue)].value, + AudioChannelValues::strings[uint32_t(aValue)].length); + SetHTMLAttr(nsGkAtoms::mozaudiochannel, channel, aRv); +} + MediaDecoderOwner::NextFrameStatus HTMLMediaElement::NextFrameStatus() { diff --git a/dom/html/HTMLMediaElement.h b/dom/html/HTMLMediaElement.h index 539b6ab311aa..c5f9f382e9de 100644 --- a/dom/html/HTMLMediaElement.h +++ b/dom/html/HTMLMediaElement.h @@ -693,6 +693,13 @@ public: double MozFragmentEnd(); + AudioChannel MozAudioChannelType() const + { + return mAudioChannel; + } + + void SetMozAudioChannelType(AudioChannel aValue, ErrorResult& aRv); + AudioTrackList* AudioTracks(); VideoTrackList* VideoTracks(); @@ -744,6 +751,9 @@ public: // that will soon be gone. bool IsBeingDestroyed(); + IMPL_EVENT_HANDLER(mozinterruptbegin) + IMPL_EVENT_HANDLER(mozinterruptend) + // These are used for testing only float ComputedVolume() const; bool ComputedMuted() const; @@ -1240,6 +1250,9 @@ protected: void ReportTelemetry(); + // Check the permissions for audiochannel. + bool CheckAudioChannelPermissions(const nsAString& aType); + // Seeks to aTime seconds. aSeekType can be Exact to seek to exactly the // seek target, or PrevSyncPoint if a quicker but less precise seek is // desired, and we'll seek to the sync point (keyframe and/or start of the diff --git a/dom/html/test/file_mozaudiochannel.html b/dom/html/test/file_mozaudiochannel.html new file mode 100644 index 000000000000..588ae338ba9b --- /dev/null +++ b/dom/html/test/file_mozaudiochannel.html @@ -0,0 +1,91 @@ + + + + + + + + diff --git a/dom/html/test/mochitest.ini b/dom/html/test/mochitest.ini index 78cb54779b12..b0870b14e2db 100644 --- a/dom/html/test/mochitest.ini +++ b/dom/html/test/mochitest.ini @@ -183,6 +183,7 @@ support-files = reflect.js file_ignoreuserfocus.html simpleFileOpener.js + file_mozaudiochannel.html file_bug1166138_1x.png file_bug1166138_2x.png file_bug1166138_def.png @@ -505,6 +506,7 @@ skip-if = toolkit == 'android' # bug 939642 [test_map_attributes_reflection.html] [test_meta_attributes_reflection.html] [test_mod_attributes_reflection.html] +[test_mozaudiochannel.html] [test_named_options.html] [test_nested_invalid_fieldsets.html] [test_object_attributes_reflection.html] diff --git a/dom/html/test/test_mozaudiochannel.html b/dom/html/test/test_mozaudiochannel.html new file mode 100644 index 000000000000..722e181bf154 --- /dev/null +++ b/dom/html/test/test_mozaudiochannel.html @@ -0,0 +1,31 @@ + + + + Test for mozaudiochannel + + + + + + +
+
+
+ + diff --git a/dom/interfaces/html/nsIDOMHTMLMediaElement.idl b/dom/interfaces/html/nsIDOMHTMLMediaElement.idl index 0cadc614aeea..7a03320794c9 100644 --- a/dom/interfaces/html/nsIDOMHTMLMediaElement.idl +++ b/dom/interfaces/html/nsIDOMHTMLMediaElement.idl @@ -87,6 +87,40 @@ interface nsIDOMHTMLMediaElement : nsISupports // it is equal to the media duration. readonly attribute double mozFragmentEnd; + // Mozilla extension: an audio channel type for media elements. + // An exception is thrown if the app tries to change the audio channel type + // without the permission (manifest file for B2G apps). + // The supported values are: + // * normal (default value) + // Automatically paused if "notification" or higher priority channel + // is played + // Use case: normal applications + // * content + // Automatically paused if "notification" or higher priority channel + // is played. Also paused if another app starts using "content" + // channel. Using this channel never affects applications using + // the "normal" channel. + // Use case: video/audio players + // * notification + // Automatically paused if "alarm" or higher priority channel is played. + // Use case: New email, incoming SMS + // * alarm + // Automatically paused if "telephony" or higher priority channel is + // played. + // User case: Alarm clock, calendar alarms + // * telephony + // Automatically paused if "ringer" or higher priority + // channel is played. + // Use case: dialer, voip + // * ringer + // Automatically paused if "publicnotification" or higher priority + // channel is played. + // Use case: dialer, voip + // * publicnotification + // Always plays in speaker, even when headphones are plugged in. + // Use case: Camera shutter sound. + attribute DOMString mozAudioChannelType; + // In addition the media element has this new events: // * onmozinterruptbegin - called when the media element is interrupted // because of the audiochannel manager. diff --git a/dom/media/webaudio/AudioContext.cpp b/dom/media/webaudio/AudioContext.cpp index 31a825f257f1..01f886400ea7 100644 --- a/dom/media/webaudio/AudioContext.cpp +++ b/dom/media/webaudio/AudioContext.cpp @@ -1066,6 +1066,21 @@ AudioContext::Unmute() const } } +AudioChannel +AudioContext::MozAudioChannelType() const +{ + return mDestination->MozAudioChannelType(); +} + +AudioChannel +AudioContext::TestAudioChannelInAudioNodeStream() +{ + MediaStream* stream = mDestination->Stream(); + MOZ_ASSERT(stream); + + return stream->AudioChannelType(); +} + size_t AudioContext::SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const { diff --git a/dom/media/webaudio/AudioContext.h b/dom/media/webaudio/AudioContext.h index 64d2e3577905..7cfd71f2b371 100644 --- a/dom/media/webaudio/AudioContext.h +++ b/dom/media/webaudio/AudioContext.h @@ -312,6 +312,10 @@ public: JSObject* GetGlobalJSObject() const; + AudioChannel MozAudioChannelType() const; + + AudioChannel TestAudioChannelInAudioNodeStream(); + void RegisterNode(AudioNode* aNode); void UnregisterNode(AudioNode* aNode); @@ -319,6 +323,9 @@ public: BasicWaveFormCache* GetBasicWaveFormCache(); + IMPL_EVENT_HANDLER(mozinterruptbegin) + IMPL_EVENT_HANDLER(mozinterruptend) + bool CheckClosed(ErrorResult& aRv); void Dispatch(already_AddRefed&& aRunnable); diff --git a/dom/media/webaudio/AudioDestinationNode.cpp b/dom/media/webaudio/AudioDestinationNode.cpp index abe357e3264e..98618506994a 100644 --- a/dom/media/webaudio/AudioDestinationNode.cpp +++ b/dom/media/webaudio/AudioDestinationNode.cpp @@ -545,6 +545,9 @@ AudioDestinationNode::WindowSuspendChanged(nsSuspendedTypes aSuspend) "this = %p, aSuspend = %s\n", this, SuspendTypeToStr(aSuspend))); mAudioChannelSuspended = suspended; + Context()->DispatchTrustedEvent(!suspended ? + NS_LITERAL_STRING("mozinterruptend") : + NS_LITERAL_STRING("mozinterruptbegin")); DisabledTrackMode disabledMode = suspended ? DisabledTrackMode::SILENCE_BLACK : DisabledTrackMode::ENABLED; diff --git a/dom/media/webaudio/test/mochitest.ini b/dom/media/webaudio/test/mochitest.ini index dce2d7333df3..4345d79cfead 100644 --- a/dom/media/webaudio/test/mochitest.ini +++ b/dom/media/webaudio/test/mochitest.ini @@ -171,6 +171,8 @@ tags=capturestream tags=capturestream [test_mixingRules.html] skip-if = toolkit == 'android' # bug 1091965 +[test_mozaudiochannel.html] +# Android: bug 1061675; OSX 10.6: bug 1097721 skip-if = (toolkit == 'android') || (os == 'mac' && os_version == '10.6') [test_nodeToParamConnection.html] [test_nodeCreationDocumentGone.html] diff --git a/dom/media/webaudio/test/test_mozaudiochannel.html b/dom/media/webaudio/test/test_mozaudiochannel.html new file mode 100644 index 000000000000..6ba14347bf70 --- /dev/null +++ b/dom/media/webaudio/test/test_mozaudiochannel.html @@ -0,0 +1,151 @@ + + + + Test for mozaudiochannel + + + + + +

+
+
+
+ + diff --git a/dom/webidl/AudioContext.webidl b/dom/webidl/AudioContext.webidl index 72e3aabb87da..cf4bd2acf000 100644 --- a/dom/webidl/AudioContext.webidl +++ b/dom/webidl/AudioContext.webidl @@ -33,4 +33,23 @@ interface AudioContext : BaseAudioContext { [NewObject, Throws] MediaStreamAudioDestinationNode createMediaStreamDestination(); -}; \ No newline at end of file +}; + +// Mozilla extensions +partial interface AudioContext { + // Read AudioChannel.webidl for more information about this attribute. + [Pref="media.useAudioChannelAPI"] + readonly attribute AudioChannel mozAudioChannelType; + + // These 2 events are dispatched when the AudioContext object is muted by + // the AudioChannelService. It's call 'interrupt' because when this event is + // dispatched on a HTMLMediaElement, the audio stream is paused. + [Pref="media.useAudioChannelAPI"] + attribute EventHandler onmozinterruptbegin; + + [Pref="media.useAudioChannelAPI"] + attribute EventHandler onmozinterruptend; + + // This method is for test only. + [ChromeOnly] AudioChannel testAudioChannelInAudioNodeStream(); +}; diff --git a/dom/webidl/HTMLMediaElement.webidl b/dom/webidl/HTMLMediaElement.webidl index 4de2d228953b..056a8bb1608e 100644 --- a/dom/webidl/HTMLMediaElement.webidl +++ b/dom/webidl/HTMLMediaElement.webidl @@ -138,6 +138,21 @@ partial interface HTMLMediaElement { // the media element has a fragment URI for the currentSrc, otherwise // it is equal to the media duration. readonly attribute double mozFragmentEnd; + + // Mozilla extension: an audio channel type for media elements. + // Read AudioChannel.webidl for more information about this attribute. + [SetterThrows, Pref="media.useAudioChannelAPI"] + attribute AudioChannel mozAudioChannelType; + + // In addition the media element has this new events: + // * onmozinterruptbegin - called when the media element is interrupted + // because of the audiochannel manager. + // * onmozinterruptend - called when the interruption is concluded + [Pref="media.useAudioChannelAPI"] + attribute EventHandler onmozinterruptbegin; + + [Pref="media.useAudioChannelAPI"] + attribute EventHandler onmozinterruptend; }; // Encrypted Media Extensions From c03792963c21299e42a70a9c102e4d4684402ac3 Mon Sep 17 00:00:00 2001 From: Alastor Wu Date: Wed, 26 Apr 2017 12:02:32 +0800 Subject: [PATCH 07/68] Bug 1358061 - remove moz-audiochannel codes for media element and web audio. r=baku MozReview-Commit-ID: KPuhxCVezOZ --HG-- extra : rebase_source : 74a9c9a9746d0add26f279c6dd5ddc30681be901 --- dom/base/nsGkAtomList.h | 5 - dom/html/HTMLMediaElement.cpp | 91 ----------- dom/html/HTMLMediaElement.h | 13 -- dom/html/test/file_mozaudiochannel.html | 91 ----------- dom/html/test/mochitest.ini | 2 - dom/html/test/test_mozaudiochannel.html | 31 ---- .../html/nsIDOMHTMLMediaElement.idl | 34 ---- dom/media/webaudio/AudioContext.cpp | 15 -- dom/media/webaudio/AudioContext.h | 7 - dom/media/webaudio/AudioDestinationNode.cpp | 3 - dom/media/webaudio/test/mochitest.ini | 3 - .../webaudio/test/test_mozaudiochannel.html | 151 ------------------ dom/webidl/AudioContext.webidl | 21 +-- dom/webidl/HTMLMediaElement.webidl | 15 -- 14 files changed, 1 insertion(+), 481 deletions(-) delete mode 100644 dom/html/test/file_mozaudiochannel.html delete mode 100644 dom/html/test/test_mozaudiochannel.html delete mode 100644 dom/media/webaudio/test/test_mozaudiochannel.html diff --git a/dom/base/nsGkAtomList.h b/dom/base/nsGkAtomList.h index 58014713432c..1558cdc6e0be 100644 --- a/dom/base/nsGkAtomList.h +++ b/dom/base/nsGkAtomList.h @@ -627,7 +627,6 @@ GK_ATOM(mouseout, "mouseout") GK_ATOM(mouseover, "mouseover") GK_ATOM(mousethrough, "mousethrough") GK_ATOM(mouseup, "mouseup") -GK_ATOM(mozaudiochannel, "mozaudiochannel") GK_ATOM(mozfullscreenchange, "mozfullscreenchange") GK_ATOM(mozfullscreenerror, "mozfullscreenerror") GK_ATOM(mozpointerlockchange, "mozpointerlockchange") @@ -1959,10 +1958,6 @@ GK_ATOM(onuserproximity, "onuserproximity") // light sensor support GK_ATOM(ondevicelight, "ondevicelight") -// Audio channel events -GK_ATOM(onmozinterruptbegin, "onmozinterruptbegin") -GK_ATOM(onmozinterruptend, "onmozinterruptend") - // MediaDevices device change event GK_ATOM(ondevicechange, "ondevicechange") diff --git a/dom/html/HTMLMediaElement.cpp b/dom/html/HTMLMediaElement.cpp index cd8e05bbdd76..1fa3f427b850 100644 --- a/dom/html/HTMLMediaElement.cpp +++ b/dom/html/HTMLMediaElement.cpp @@ -1444,23 +1444,6 @@ NS_IMPL_BOOL_ATTR(HTMLMediaElement, Loop, loop) NS_IMPL_BOOL_ATTR(HTMLMediaElement, DefaultMuted, muted) NS_IMPL_ENUM_ATTR_DEFAULT_VALUE(HTMLMediaElement, Preload, preload, nullptr) -NS_IMETHODIMP -HTMLMediaElement::GetMozAudioChannelType(nsAString& aValue) -{ - nsString defaultValue; - AudioChannelService::GetDefaultAudioChannelString(defaultValue); - - NS_ConvertUTF16toUTF8 str(defaultValue); - GetEnumAttr(nsGkAtoms::mozaudiochannel, str.get(), aValue); - return NS_OK; -} - -NS_IMETHODIMP -HTMLMediaElement::SetMozAudioChannelType(const nsAString& aValue) -{ - return SetAttrHelper(nsGkAtoms::mozaudiochannel, aValue); -} - NS_IMETHODIMP_(bool) HTMLMediaElement::IsVideo() { @@ -4142,77 +4125,12 @@ bool HTMLMediaElement::ParseAttribute(int32_t aNamespaceID, if (aAttribute == nsGkAtoms::preload) { return aResult.ParseEnumValue(aValue, kPreloadTable, false); } - - // Remove the b2g-specific audio channel setting in bug1299390. - if (aAttribute == nsGkAtoms::mozaudiochannel) { - const nsAttrValue::EnumTable* table = - AudioChannelService::GetAudioChannelTable(); - MOZ_ASSERT(table); - - bool parsed = aResult.ParseEnumValue(aValue, table, false, &table[0]); - if (!parsed) { - return false; - } - - AudioChannel audioChannel = static_cast(aResult.GetEnumValue()); - - if (audioChannel == mAudioChannel || - !CheckAudioChannelPermissions(aValue)) { - return true; - } - - // We cannot change the AudioChannel of a decoder. - if (mDecoder) { - return true; - } - - mAudioChannel = audioChannel; - - if (mSrcStream) { - RefPtr stream = GetSrcMediaStream(); - if (stream) { - stream->SetAudioChannelType(mAudioChannel); - } - } - - return true; - } } return nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue, aResult); } -bool HTMLMediaElement::CheckAudioChannelPermissions(const nsAString& aString) -{ - // Only normal channel doesn't need permission. - if (aString.EqualsASCII("normal")) { - return true; - } - - // Maybe this audio channel is equal to the default value from the pref. - nsString audioChannel; - AudioChannelService::GetDefaultAudioChannelString(audioChannel); - if (audioChannel.Equals(aString)) { - return true; - } - - nsCOMPtr permissionManager = - services::GetPermissionManager(); - if (!permissionManager) { - return false; - } - - uint32_t perm = nsIPermissionManager::UNKNOWN_ACTION; - permissionManager->TestExactPermissionFromPrincipal(NodePrincipal(), - nsCString(NS_LITERAL_CSTRING("audio-channel-") + NS_ConvertUTF16toUTF8(aString)).get(), &perm); - if (perm != nsIPermissionManager::ALLOW_ACTION) { - return false; - } - - return true; -} - void HTMLMediaElement::DoneCreatingElement() { if (HasAttr(kNameSpaceID_None, nsGkAtoms::muted)) { @@ -7063,15 +6981,6 @@ HTMLMediaElement::GetOrCreateTextTrackManager() return mTextTrackManager; } -void -HTMLMediaElement::SetMozAudioChannelType(AudioChannel aValue, ErrorResult& aRv) -{ - nsString channel; - channel.AssignASCII(AudioChannelValues::strings[uint32_t(aValue)].value, - AudioChannelValues::strings[uint32_t(aValue)].length); - SetHTMLAttr(nsGkAtoms::mozaudiochannel, channel, aRv); -} - MediaDecoderOwner::NextFrameStatus HTMLMediaElement::NextFrameStatus() { diff --git a/dom/html/HTMLMediaElement.h b/dom/html/HTMLMediaElement.h index c5f9f382e9de..539b6ab311aa 100644 --- a/dom/html/HTMLMediaElement.h +++ b/dom/html/HTMLMediaElement.h @@ -693,13 +693,6 @@ public: double MozFragmentEnd(); - AudioChannel MozAudioChannelType() const - { - return mAudioChannel; - } - - void SetMozAudioChannelType(AudioChannel aValue, ErrorResult& aRv); - AudioTrackList* AudioTracks(); VideoTrackList* VideoTracks(); @@ -751,9 +744,6 @@ public: // that will soon be gone. bool IsBeingDestroyed(); - IMPL_EVENT_HANDLER(mozinterruptbegin) - IMPL_EVENT_HANDLER(mozinterruptend) - // These are used for testing only float ComputedVolume() const; bool ComputedMuted() const; @@ -1250,9 +1240,6 @@ protected: void ReportTelemetry(); - // Check the permissions for audiochannel. - bool CheckAudioChannelPermissions(const nsAString& aType); - // Seeks to aTime seconds. aSeekType can be Exact to seek to exactly the // seek target, or PrevSyncPoint if a quicker but less precise seek is // desired, and we'll seek to the sync point (keyframe and/or start of the diff --git a/dom/html/test/file_mozaudiochannel.html b/dom/html/test/file_mozaudiochannel.html deleted file mode 100644 index 588ae338ba9b..000000000000 --- a/dom/html/test/file_mozaudiochannel.html +++ /dev/null @@ -1,91 +0,0 @@ - - - - - - - - diff --git a/dom/html/test/mochitest.ini b/dom/html/test/mochitest.ini index b0870b14e2db..78cb54779b12 100644 --- a/dom/html/test/mochitest.ini +++ b/dom/html/test/mochitest.ini @@ -183,7 +183,6 @@ support-files = reflect.js file_ignoreuserfocus.html simpleFileOpener.js - file_mozaudiochannel.html file_bug1166138_1x.png file_bug1166138_2x.png file_bug1166138_def.png @@ -506,7 +505,6 @@ skip-if = toolkit == 'android' # bug 939642 [test_map_attributes_reflection.html] [test_meta_attributes_reflection.html] [test_mod_attributes_reflection.html] -[test_mozaudiochannel.html] [test_named_options.html] [test_nested_invalid_fieldsets.html] [test_object_attributes_reflection.html] diff --git a/dom/html/test/test_mozaudiochannel.html b/dom/html/test/test_mozaudiochannel.html deleted file mode 100644 index 722e181bf154..000000000000 --- a/dom/html/test/test_mozaudiochannel.html +++ /dev/null @@ -1,31 +0,0 @@ - - - - Test for mozaudiochannel - - - - - - -
-
-
- - diff --git a/dom/interfaces/html/nsIDOMHTMLMediaElement.idl b/dom/interfaces/html/nsIDOMHTMLMediaElement.idl index 7a03320794c9..0cadc614aeea 100644 --- a/dom/interfaces/html/nsIDOMHTMLMediaElement.idl +++ b/dom/interfaces/html/nsIDOMHTMLMediaElement.idl @@ -87,40 +87,6 @@ interface nsIDOMHTMLMediaElement : nsISupports // it is equal to the media duration. readonly attribute double mozFragmentEnd; - // Mozilla extension: an audio channel type for media elements. - // An exception is thrown if the app tries to change the audio channel type - // without the permission (manifest file for B2G apps). - // The supported values are: - // * normal (default value) - // Automatically paused if "notification" or higher priority channel - // is played - // Use case: normal applications - // * content - // Automatically paused if "notification" or higher priority channel - // is played. Also paused if another app starts using "content" - // channel. Using this channel never affects applications using - // the "normal" channel. - // Use case: video/audio players - // * notification - // Automatically paused if "alarm" or higher priority channel is played. - // Use case: New email, incoming SMS - // * alarm - // Automatically paused if "telephony" or higher priority channel is - // played. - // User case: Alarm clock, calendar alarms - // * telephony - // Automatically paused if "ringer" or higher priority - // channel is played. - // Use case: dialer, voip - // * ringer - // Automatically paused if "publicnotification" or higher priority - // channel is played. - // Use case: dialer, voip - // * publicnotification - // Always plays in speaker, even when headphones are plugged in. - // Use case: Camera shutter sound. - attribute DOMString mozAudioChannelType; - // In addition the media element has this new events: // * onmozinterruptbegin - called when the media element is interrupted // because of the audiochannel manager. diff --git a/dom/media/webaudio/AudioContext.cpp b/dom/media/webaudio/AudioContext.cpp index 01f886400ea7..31a825f257f1 100644 --- a/dom/media/webaudio/AudioContext.cpp +++ b/dom/media/webaudio/AudioContext.cpp @@ -1066,21 +1066,6 @@ AudioContext::Unmute() const } } -AudioChannel -AudioContext::MozAudioChannelType() const -{ - return mDestination->MozAudioChannelType(); -} - -AudioChannel -AudioContext::TestAudioChannelInAudioNodeStream() -{ - MediaStream* stream = mDestination->Stream(); - MOZ_ASSERT(stream); - - return stream->AudioChannelType(); -} - size_t AudioContext::SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const { diff --git a/dom/media/webaudio/AudioContext.h b/dom/media/webaudio/AudioContext.h index 7cfd71f2b371..64d2e3577905 100644 --- a/dom/media/webaudio/AudioContext.h +++ b/dom/media/webaudio/AudioContext.h @@ -312,10 +312,6 @@ public: JSObject* GetGlobalJSObject() const; - AudioChannel MozAudioChannelType() const; - - AudioChannel TestAudioChannelInAudioNodeStream(); - void RegisterNode(AudioNode* aNode); void UnregisterNode(AudioNode* aNode); @@ -323,9 +319,6 @@ public: BasicWaveFormCache* GetBasicWaveFormCache(); - IMPL_EVENT_HANDLER(mozinterruptbegin) - IMPL_EVENT_HANDLER(mozinterruptend) - bool CheckClosed(ErrorResult& aRv); void Dispatch(already_AddRefed&& aRunnable); diff --git a/dom/media/webaudio/AudioDestinationNode.cpp b/dom/media/webaudio/AudioDestinationNode.cpp index 98618506994a..abe357e3264e 100644 --- a/dom/media/webaudio/AudioDestinationNode.cpp +++ b/dom/media/webaudio/AudioDestinationNode.cpp @@ -545,9 +545,6 @@ AudioDestinationNode::WindowSuspendChanged(nsSuspendedTypes aSuspend) "this = %p, aSuspend = %s\n", this, SuspendTypeToStr(aSuspend))); mAudioChannelSuspended = suspended; - Context()->DispatchTrustedEvent(!suspended ? - NS_LITERAL_STRING("mozinterruptend") : - NS_LITERAL_STRING("mozinterruptbegin")); DisabledTrackMode disabledMode = suspended ? DisabledTrackMode::SILENCE_BLACK : DisabledTrackMode::ENABLED; diff --git a/dom/media/webaudio/test/mochitest.ini b/dom/media/webaudio/test/mochitest.ini index 4345d79cfead..56a07ed0801a 100644 --- a/dom/media/webaudio/test/mochitest.ini +++ b/dom/media/webaudio/test/mochitest.ini @@ -171,9 +171,6 @@ tags=capturestream tags=capturestream [test_mixingRules.html] skip-if = toolkit == 'android' # bug 1091965 -[test_mozaudiochannel.html] -# Android: bug 1061675; OSX 10.6: bug 1097721 -skip-if = (toolkit == 'android') || (os == 'mac' && os_version == '10.6') [test_nodeToParamConnection.html] [test_nodeCreationDocumentGone.html] [test_OfflineAudioContext.html] diff --git a/dom/media/webaudio/test/test_mozaudiochannel.html b/dom/media/webaudio/test/test_mozaudiochannel.html deleted file mode 100644 index 6ba14347bf70..000000000000 --- a/dom/media/webaudio/test/test_mozaudiochannel.html +++ /dev/null @@ -1,151 +0,0 @@ - - - - Test for mozaudiochannel - - - - - -

-
-
-
- - diff --git a/dom/webidl/AudioContext.webidl b/dom/webidl/AudioContext.webidl index cf4bd2acf000..72e3aabb87da 100644 --- a/dom/webidl/AudioContext.webidl +++ b/dom/webidl/AudioContext.webidl @@ -33,23 +33,4 @@ interface AudioContext : BaseAudioContext { [NewObject, Throws] MediaStreamAudioDestinationNode createMediaStreamDestination(); -}; - -// Mozilla extensions -partial interface AudioContext { - // Read AudioChannel.webidl for more information about this attribute. - [Pref="media.useAudioChannelAPI"] - readonly attribute AudioChannel mozAudioChannelType; - - // These 2 events are dispatched when the AudioContext object is muted by - // the AudioChannelService. It's call 'interrupt' because when this event is - // dispatched on a HTMLMediaElement, the audio stream is paused. - [Pref="media.useAudioChannelAPI"] - attribute EventHandler onmozinterruptbegin; - - [Pref="media.useAudioChannelAPI"] - attribute EventHandler onmozinterruptend; - - // This method is for test only. - [ChromeOnly] AudioChannel testAudioChannelInAudioNodeStream(); -}; +}; \ No newline at end of file diff --git a/dom/webidl/HTMLMediaElement.webidl b/dom/webidl/HTMLMediaElement.webidl index 056a8bb1608e..4de2d228953b 100644 --- a/dom/webidl/HTMLMediaElement.webidl +++ b/dom/webidl/HTMLMediaElement.webidl @@ -138,21 +138,6 @@ partial interface HTMLMediaElement { // the media element has a fragment URI for the currentSrc, otherwise // it is equal to the media duration. readonly attribute double mozFragmentEnd; - - // Mozilla extension: an audio channel type for media elements. - // Read AudioChannel.webidl for more information about this attribute. - [SetterThrows, Pref="media.useAudioChannelAPI"] - attribute AudioChannel mozAudioChannelType; - - // In addition the media element has this new events: - // * onmozinterruptbegin - called when the media element is interrupted - // because of the audiochannel manager. - // * onmozinterruptend - called when the interruption is concluded - [Pref="media.useAudioChannelAPI"] - attribute EventHandler onmozinterruptbegin; - - [Pref="media.useAudioChannelAPI"] - attribute EventHandler onmozinterruptend; }; // Encrypted Media Extensions From ff2192eef40c9893810e4cd7c4dccf1c1d892fc1 Mon Sep 17 00:00:00 2001 From: Alex Chronopoulos Date: Tue, 25 Apr 2017 18:25:01 +0300 Subject: [PATCH 08/68] Bug 1359451 - Update cubeb from upstream to 17503c4. r=padenot MozReview-Commit-ID: C3NXIvjwjgD --HG-- extra : rebase_source : 06cc2449c9892ba431662e9cd72633a91efcba2e --- media/libcubeb/README_MOZILLA | 2 +- media/libcubeb/gtest/test_record.cpp | 2 +- media/libcubeb/src/cubeb_audiounit.cpp | 30 +++++++++++++++++--------- media/libcubeb/src/cubeb_mixer.cpp | 3 ++- media/libcubeb/src/cubeb_mixer.h | 5 ++++- media/libcubeb/src/cubeb_pulse.c | 17 +++++++++------ media/libcubeb/src/cubeb_wasapi.cpp | 5 ----- 7 files changed, 39 insertions(+), 25 deletions(-) diff --git a/media/libcubeb/README_MOZILLA b/media/libcubeb/README_MOZILLA index ecddedb61668..8ce6b16571fe 100644 --- a/media/libcubeb/README_MOZILLA +++ b/media/libcubeb/README_MOZILLA @@ -5,4 +5,4 @@ Makefile.in build files for the Mozilla build system. The cubeb git repository is: git://github.com/kinetiknz/cubeb.git -The git commit ID used was 6e52314f24bba463d6ca97951f7d9fcc677d76a4 (2017-04-18 17:28:50 +0200) +The git commit ID used was 17503c41318ec7a18ba3d728fb803a66ec60cf84 (2017-04-25 15:26:11 +0200) diff --git a/media/libcubeb/gtest/test_record.cpp b/media/libcubeb/gtest/test_record.cpp index 340012c1eff5..1744832b255c 100644 --- a/media/libcubeb/gtest/test_record.cpp +++ b/media/libcubeb/gtest/test_record.cpp @@ -92,7 +92,7 @@ TEST(cubeb, record) params.format = STREAM_FORMAT; params.rate = SAMPLE_FREQUENCY; params.channels = 1; - params.layout = CUBEB_LAYOUT_MONO; + params.layout = CUBEB_LAYOUT_UNDEFINED; r = cubeb_stream_init(ctx, &stream, "Cubeb record (mono)", NULL, ¶ms, NULL, nullptr, 4096, data_cb_record, state_cb_record, &stream_state); diff --git a/media/libcubeb/src/cubeb_audiounit.cpp b/media/libcubeb/src/cubeb_audiounit.cpp index bfd43ade8fc3..2d8107308da7 100644 --- a/media/libcubeb/src/cubeb_audiounit.cpp +++ b/media/libcubeb/src/cubeb_audiounit.cpp @@ -226,6 +226,7 @@ channel_label_to_cubeb_channel(UInt32 label) case kAudioChannelLabel_RearSurroundLeft: return CHANNEL_RLS; case kAudioChannelLabel_RearSurroundRight: return CHANNEL_RRS; case kAudioChannelLabel_CenterSurround: return CHANNEL_RCENTER; + case kAudioChannelLabel_Unknown: return CHANNEL_UNMAPPED; default: return CHANNEL_INVALID; } } @@ -244,6 +245,7 @@ cubeb_channel_to_channel_label(cubeb_channel channel) case CHANNEL_RLS: return kAudioChannelLabel_RearSurroundLeft; case CHANNEL_RRS: return kAudioChannelLabel_RearSurroundRight; case CHANNEL_RCENTER: return kAudioChannelLabel_CenterSurround; + case CHANNEL_UNMAPPED: return kAudioChannelLabel_Unknown; default: return kAudioChannelLabel_Unknown; } } @@ -643,10 +645,10 @@ static int audiounit_stream_get_volume(cubeb_stream * stm, float * volume); static int audiounit_stream_set_volume(cubeb_stream * stm, float volume); static int -audiounit_reinit_stream(cubeb_stream * stm, bool is_started) +audiounit_reinit_stream(cubeb_stream * stm) { auto_lock context_lock(stm->context->mutex); - if (is_started) { + if (!stm->shutdown) { audiounit_stream_stop_internal(stm); } @@ -671,7 +673,7 @@ audiounit_reinit_stream(cubeb_stream * stm, bool is_started) stm->frames_read = 0; // If the stream was running, start it again. - if (is_started) { + if (!stm->shutdown) { audiounit_stream_start_internal(stm); } } @@ -685,8 +687,6 @@ audiounit_property_listener_callback(AudioObjectID /* id */, UInt32 address_coun { cubeb_stream * stm = (cubeb_stream*) user; stm->switching_device = true; - // Note if the stream was running or not - bool was_running = !stm->shutdown; LOG("(%p) Audio device changed, %u events.", stm, (unsigned int) address_count); for (UInt32 i = 0; i < address_count; i++) { @@ -715,8 +715,12 @@ audiounit_property_listener_callback(AudioObjectID /* id */, UInt32 address_coun stm->input_device = 0; } break; - case kAudioDevicePropertyDataSource: - LOG("Event[%u] - mSelector == kAudioHardwarePropertyDataSource", (unsigned int) i); + case kAudioDevicePropertyDataSource: { + LOG("Event[%u] - mSelector == kAudioHardwarePropertyDataSource", (unsigned int) i); + if (has_output(stm)) { + stm->output_device = 0; + } + } break; default: LOG("Event[%u] - mSelector == Unexpected Event id %d, return", (unsigned int) i, addresses[i].mSelector); @@ -743,7 +747,7 @@ audiounit_property_listener_callback(AudioObjectID /* id */, UInt32 address_coun // Use a new thread, through the queue, to avoid deadlock when calling // Get/SetProperties method from inside notify callback dispatch_async(stm->context->serial_queue, ^() { - if (audiounit_reinit_stream(stm, was_running) != CUBEB_OK) { + if (audiounit_reinit_stream(stm) != CUBEB_OK) { stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_STOPPED); LOG("(%p) Could not reopen the stream after switching.", stm); } @@ -1089,6 +1093,12 @@ audiounit_convert_channel_layout(AudioChannelLayout * layout) return CUBEB_LAYOUT_UNDEFINED; } + // This devices has more channels that we can support, bail out. + if (layout->mNumberChannelDescriptions >= CHANNEL_MAX) { + LOG("Audio device has more than %d channels, bailing out.", CHANNEL_MAX); + return CUBEB_LAYOUT_UNDEFINED; + } + cubeb_channel_map cm; cm.channels = layout->mNumberChannelDescriptions; for (UInt32 i = 0; i < layout->mNumberChannelDescriptions; ++i) { @@ -2579,10 +2589,10 @@ audiounit_stream_start_internal(cubeb_stream * stm) static int audiounit_stream_start(cubeb_stream * stm) { + auto_lock context_lock(stm->context->mutex); stm->shutdown = false; stm->draining = false; - auto_lock context_lock(stm->context->mutex); audiounit_stream_start_internal(stm); stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_STARTED); @@ -2608,9 +2618,9 @@ audiounit_stream_stop_internal(cubeb_stream * stm) static int audiounit_stream_stop(cubeb_stream * stm) { + auto_lock context_lock(stm->context->mutex); stm->shutdown = true; - auto_lock context_lock(stm->context->mutex); audiounit_stream_stop_internal(stm); stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_STOPPED); diff --git a/media/libcubeb/src/cubeb_mixer.cpp b/media/libcubeb/src/cubeb_mixer.cpp index 32f7e66d133a..f9ac3e7335a8 100644 --- a/media/libcubeb/src/cubeb_mixer.cpp +++ b/media/libcubeb/src/cubeb_mixer.cpp @@ -31,7 +31,8 @@ cubeb_channel_layout cubeb_channel_map_to_layout(cubeb_channel_map const * chann { uint32_t channel_mask = 0; for (uint8_t i = 0 ; i < channel_map->channels ; ++i) { - if (channel_map->map[i] == CHANNEL_INVALID) { + if (channel_map->map[i] == CHANNEL_INVALID || + channel_map->map[i] == CHANNEL_UNMAPPED) { return CUBEB_LAYOUT_UNDEFINED; } channel_mask |= 1 << channel_map->map[i]; diff --git a/media/libcubeb/src/cubeb_mixer.h b/media/libcubeb/src/cubeb_mixer.h index e9eab59d7432..9b6d8c7d62b9 100644 --- a/media/libcubeb/src/cubeb_mixer.h +++ b/media/libcubeb/src/cubeb_mixer.h @@ -27,7 +27,8 @@ typedef enum { CHANNEL_RCENTER, CHANNEL_RRS, CHANNEL_LFE, - CHANNEL_MAX // Max number of supported channels. + CHANNEL_UNMAPPED, + CHANNEL_MAX = 256 // Max number of supported channels. } cubeb_channel; static cubeb_channel const CHANNEL_INDEX_TO_ORDER[CUBEB_LAYOUT_MAX][CHANNEL_MAX] = { @@ -50,6 +51,8 @@ static cubeb_channel const CHANNEL_INDEX_TO_ORDER[CUBEB_LAYOUT_MAX][CHANNEL_MAX] { CHANNEL_LEFT, CHANNEL_RIGHT, CHANNEL_CENTER, CHANNEL_LFE, CHANNEL_LS, CHANNEL_RS }, // 3F2_LFE { CHANNEL_LEFT, CHANNEL_RIGHT, CHANNEL_CENTER, CHANNEL_LFE, CHANNEL_RCENTER, CHANNEL_LS, CHANNEL_RS }, // 3F3R_LFE { CHANNEL_LEFT, CHANNEL_RIGHT, CHANNEL_CENTER, CHANNEL_LFE, CHANNEL_RLS, CHANNEL_RRS, CHANNEL_LS, CHANNEL_RS } // 3F4_LFE + // When more channels are present, the stream is considered unmapped to a + // particular speaker set. }; typedef struct { diff --git a/media/libcubeb/src/cubeb_pulse.c b/media/libcubeb/src/cubeb_pulse.c index b358c10456fa..e41d5e7f9ed4 100644 --- a/media/libcubeb/src/cubeb_pulse.c +++ b/media/libcubeb/src/cubeb_pulse.c @@ -748,8 +748,10 @@ create_pa_stream(cubeb_stream * stm, cubeb_stream_params * stream_params, char const * stream_name) { - assert(stm && stream_params && stream_params->layout != CUBEB_LAYOUT_UNDEFINED && - CUBEB_CHANNEL_LAYOUT_MAPS[stream_params->layout].channels == stream_params->channels); + assert(stm && stream_params); + assert(&stm->input_stream == pa_stm || (&stm->output_stream == pa_stm && + stream_params->layout != CUBEB_LAYOUT_UNDEFINED && + CUBEB_CHANNEL_LAYOUT_MAPS[stream_params->layout].channels == stream_params->channels)); *pa_stm = NULL; pa_sample_spec ss; ss.format = to_pulse_format(stream_params->format); @@ -758,10 +760,13 @@ create_pa_stream(cubeb_stream * stm, ss.rate = stream_params->rate; ss.channels = stream_params->channels; - pa_channel_map cm; - layout_to_channel_map(stream_params->layout, &cm); - - *pa_stm = WRAP(pa_stream_new)(stm->context->context, stream_name, &ss, &cm); + if (stream_params->layout == CUBEB_LAYOUT_UNDEFINED) { + *pa_stm = WRAP(pa_stream_new)(stm->context->context, stream_name, &ss, NULL); + } else { + pa_channel_map cm; + layout_to_channel_map(stream_params->layout, &cm); + *pa_stm = WRAP(pa_stream_new)(stm->context->context, stream_name, &ss, &cm); + } return (*pa_stm == NULL) ? CUBEB_ERROR : CUBEB_OK; } diff --git a/media/libcubeb/src/cubeb_wasapi.cpp b/media/libcubeb/src/cubeb_wasapi.cpp index 954f8346e3ba..4ba6582ad0c9 100644 --- a/media/libcubeb/src/cubeb_wasapi.cpp +++ b/media/libcubeb/src/cubeb_wasapi.cpp @@ -1556,11 +1556,6 @@ int setup_wasapi_stream_one_side(cubeb_stream * stm, * use the default setting retrieved from the stream format of the audio * engine's internal processing by GetMixFormat. */ if (mix_format->nChannels > 2) { - /* Currently, we only support mono and stereo for capture stream. */ - if (direction == eCapture) { - XASSERT(false && "Multichannel recording is not supported."); - } - handle_channel_layout(stm, mix_format, stream_params); } From 00e9caf1965c0dc0445a188ee37233b6697edc46 Mon Sep 17 00:00:00 2001 From: Cameron McCormack Date: Wed, 26 Apr 2017 13:49:29 +0800 Subject: [PATCH 09/68] Bug 1341102 - Update stylo text expectations. r=me --- layout/reftests/text/reftest-stylo.list | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/layout/reftests/text/reftest-stylo.list b/layout/reftests/text/reftest-stylo.list index ef952179f0dd..9ff4ff1388ce 100644 --- a/layout/reftests/text/reftest-stylo.list +++ b/layout/reftests/text/reftest-stylo.list @@ -328,8 +328,8 @@ fails == control-chars-01b.html control-chars-01b.html fails == control-chars-01c.html control-chars-01c.html fails == control-chars-01d.html control-chars-01d.html == control-chars-02.html control-chars-02.html -fails == control-chars-03a.html control-chars-03a.html -fails == control-chars-03b.html control-chars-03b.html +== control-chars-03a.html control-chars-03a.html +== control-chars-03b.html control-chars-03b.html pref(layout.css.control-characters.visible,true) == control-chars-04a.html control-chars-04a.html pref(layout.css.control-characters.visible,true) == control-chars-04b.html control-chars-04b.html pref(layout.css.control-characters.visible,true) == control-chars-04c.html control-chars-04c.html From 03a364a2855f607bd2f91da4f4839d45e26ae545 Mon Sep 17 00:00:00 2001 From: Mark Banner Date: Tue, 25 Apr 2017 21:54:13 +0100 Subject: [PATCH 10/68] Bug 1359604 - Enable the ESLint recommended rules for taskcluster/docker/index-task/. r=dustin MozReview-Commit-ID: EOX2SvofSS5 --HG-- extra : rebase_source : 279286c25cb6813895f31990aa22592c0b5c9103 --- taskcluster/docker/index-task/.eslintrc.js | 15 +++++++++++++++ taskcluster/docker/index-task/insert-indexes.js | 16 ++++++++-------- 2 files changed, 23 insertions(+), 8 deletions(-) create mode 100644 taskcluster/docker/index-task/.eslintrc.js diff --git a/taskcluster/docker/index-task/.eslintrc.js b/taskcluster/docker/index-task/.eslintrc.js new file mode 100644 index 000000000000..26e10cb2ef2d --- /dev/null +++ b/taskcluster/docker/index-task/.eslintrc.js @@ -0,0 +1,15 @@ +"use strict"; + +module.exports = { + "extends": [ + "plugin:mozilla/recommended" + ], + + "plugins": [ + "mozilla" + ], + + "env": { + "node": true + } +}; diff --git a/taskcluster/docker/index-task/insert-indexes.js b/taskcluster/docker/index-task/insert-indexes.js index 72b7601de05d..95ca141ec7b7 100644 --- a/taskcluster/docker/index-task/insert-indexes.js +++ b/taskcluster/docker/index-task/insert-indexes.js @@ -1,10 +1,10 @@ -let taskcluster = require('taskcluster-client'); +let taskcluster = require("taskcluster-client"); // Create instance of index client let index = new taskcluster.Index({ delayFactor: 750, // Good solid delay for background process retries: 8, // A few extra retries for robustness - baseUrl: 'taskcluster/index/v1', + baseUrl: "taskcluster/index/v1", }); // Create queue instance for fetching taskId @@ -19,14 +19,14 @@ let namespaces = process.argv.slice(2); // Validate input if (!taskId) { - console.log('Expected target task as environment variable: TARGET_TASKID'); + console.log("Expected target task as environment variable: TARGET_TASKID"); process.exit(1); } // Fetch task definition to get expiration and then insert into index queue.task(taskId).then(task => task.expires).then(expires => { return Promise.all(namespaces.map(namespace => { - console.log('Inserting %s into index under: %s', taskId, namespace); + console.log("Inserting %s into index under: %s", taskId, namespace); return index.insertTask(namespace, { taskId, rank: 0, @@ -35,13 +35,13 @@ queue.task(taskId).then(task => task.expires).then(expires => { }); })); }).then(() => { - console.log('indexing successfully completed.'); + console.log("indexing successfully completed."); process.exit(0); }).catch(err => { - console.log('Error:\n%s', err); + console.log("Error:\n%s", err); if (err.stack) { - console.log('Stack:\n%s', err.stack); + console.log("Stack:\n%s", err.stack); } - console.log('Properties:\n%j', err); + console.log("Properties:\n%j", err); throw err; }).catch(() => process.exit(1)); From d8a015d07f446edc10273b3911b8321be2ffd859 Mon Sep 17 00:00:00 2001 From: Jan Henning Date: Fri, 21 Apr 2017 22:53:19 +0200 Subject: [PATCH 11/68] Bug 1357579 - Correctly copy the sparse Boolean array when clearing Site Settings. r=ahunt The checked items are stored in a *sparse* Boolean array, which we want to transform into an array (list) of the checked indices for transmission to Gecko. The current approach doesn't do this correctly, as it iterates over all (sparse and non-sparse) items, but uses SparseBooleanArray.size() (which only counts non-sparse items) as its iteration limit. This means that we only copy the checked state of the first n items, where n is the total count of checked items. For correctly iterating over the array to retrieve all indices that are true, we'd either have to use the largest available key (if we'd want to iterate over everything, including the sparse indices), or else use the approach chosen in this patch, namely using valueAt/keyAt in order to iterate over the internal array that's storing the values for all non-sparse indices. MozReview-Commit-ID: FRGI4Rr0uCb --HG-- extra : rebase_source : d9bb3a08af3a7ee2e730beb4777df30d019c922c --- mobile/android/base/java/org/mozilla/gecko/GeckoApp.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java b/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java index 1d161bb6a569..bd24041ef280 100644 --- a/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java +++ b/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java @@ -886,11 +886,11 @@ public abstract class GeckoApp ListView listView = ((AlertDialog) dialog).getListView(); SparseBooleanArray checkedItemPositions = listView.getCheckedItemPositions(); - // An array of the indices of the permissions we want to clear + // An array of the indices of the permissions we want to clear. final ArrayList permissionsToClear = new ArrayList<>(); for (int i = 0; i < checkedItemPositions.size(); i++) { - if (checkedItemPositions.get(i)) { - permissionsToClear.add(i); + if (checkedItemPositions.valueAt(i)) { + permissionsToClear.add(checkedItemPositions.keyAt(i)); } } From bfb79c0f40121c5b0a5113b4495271fba1a98699 Mon Sep 17 00:00:00 2001 From: Simon Sapin Date: Wed, 26 Apr 2017 00:59:58 -0500 Subject: [PATCH 12/68] servo: Merge #16455 - Bug 1354970 - Add @counter-style rules (from servo:counter-style); r=upsuper Source-Repo: https://github.com/servo/servo Source-Revision: 2eff661ebb2737979b80367ada3ac99d73a06fc4 --HG-- extra : subtree_source : https%3A//hg.mozilla.org/projects/converted-servo-linear extra : subtree_revision : c2342be5a8c1db8e4497da1858211ce72d49f716 --- servo/components/atoms/build.rs | 15 +- servo/components/atoms/static_atoms.txt | 2 + servo/components/layout/generated_content.rs | 6 +- .../components/script/dom/csskeyframesrule.rs | 22 +- servo/components/script/dom/cssrule.rs | 1 + servo/components/style/animation.rs | 17 +- servo/components/style/counter_style/mod.rs | 632 ++++++++++++++++++ .../style/counter_style/predefined.rs | 61 ++ .../style/counter_style/update_predefined.py | 32 + servo/components/style/gecko/rules.rs | 123 +++- .../gecko_bindings/sugar/ns_css_value.rs | 5 + .../style/gecko_string_cache/mod.rs | 26 + servo/components/style/lib.rs | 1 + servo/components/style/matching.rs | 2 +- .../components/style/properties/gecko.mako.rs | 37 +- .../style/properties/longhand/box.mako.rs | 49 +- .../properties/longhand/counters.mako.rs | 43 +- .../style/properties/properties.mako.rs | 3 +- servo/components/style/stylesheets.rs | 58 +- servo/components/style/stylist.rs | 4 +- servo/components/style/values/mod.rs | 93 ++- servo/tests/unit/style/lib.rs | 2 +- servo/tests/unit/style/parsing/animation.rs | 11 +- .../unit/style/properties/serialization.rs | 5 +- servo/tests/unit/style/stylesheets.rs | 3 +- 25 files changed, 1128 insertions(+), 125 deletions(-) create mode 100644 servo/components/style/counter_style/mod.rs create mode 100644 servo/components/style/counter_style/predefined.rs create mode 100755 servo/components/style/counter_style/update_predefined.py diff --git a/servo/components/atoms/build.rs b/servo/components/atoms/build.rs index 4de629d288c9..c8edc9bcd5ab 100644 --- a/servo/components/atoms/build.rs +++ b/servo/components/atoms/build.rs @@ -12,7 +12,20 @@ use std::path::Path; fn main() { let static_atoms = Path::new(&env::var("CARGO_MANIFEST_DIR").unwrap()).join("static_atoms.txt"); let static_atoms = BufReader::new(File::open(&static_atoms).unwrap()); - string_cache_codegen::AtomType::new("Atom", "atom!") + let mut atom_type = string_cache_codegen::AtomType::new("Atom", "atom!"); + + macro_rules! predefined { + ($($name: expr,)+) => { + { + $( + atom_type.atom($name); + )+ + } + } + } + include!("../style/counter_style/predefined.rs"); + + atom_type .atoms(static_atoms.lines().map(Result::unwrap)) .write_to_file(&Path::new(&env::var("OUT_DIR").unwrap()).join("atom.rs")) .unwrap(); diff --git a/servo/components/atoms/static_atoms.txt b/servo/components/atoms/static_atoms.txt index df9cd426984d..a41aae29289f 100644 --- a/servo/components/atoms/static_atoms.txt +++ b/servo/components/atoms/static_atoms.txt @@ -8,6 +8,8 @@ left center right +none + hidden submit button diff --git a/servo/components/layout/generated_content.rs b/servo/components/layout/generated_content.rs index 887562e89fbd..a23fd165ecf4 100644 --- a/servo/components/layout/generated_content.rs +++ b/servo/components/layout/generated_content.rs @@ -273,6 +273,7 @@ impl<'a,'b> ResolveGeneratedContentFragmentMutator<'a,'b> { self.traversal.list_item.truncate_to_level(self.level); for &(ref counter_name, value) in &fragment.style().get_counters().counter_reset.0 { + let counter_name = &*counter_name.0; if let Some(ref mut counter) = self.traversal.counters.get_mut(counter_name) { counter.reset(self.level, value); continue @@ -280,10 +281,11 @@ impl<'a,'b> ResolveGeneratedContentFragmentMutator<'a,'b> { let mut counter = Counter::new(); counter.reset(self.level, value); - self.traversal.counters.insert((*counter_name).clone(), counter); + self.traversal.counters.insert(counter_name.to_owned(), counter); } for &(ref counter_name, value) in &fragment.style().get_counters().counter_increment.0 { + let counter_name = &*counter_name.0; if let Some(ref mut counter) = self.traversal.counters.get_mut(counter_name) { counter.increment(self.level, value); continue @@ -291,7 +293,7 @@ impl<'a,'b> ResolveGeneratedContentFragmentMutator<'a,'b> { let mut counter = Counter::new(); counter.increment(self.level, value); - self.traversal.counters.insert((*counter_name).clone(), counter); + self.traversal.counters.insert(counter_name.to_owned(), counter); } self.incremented = true diff --git a/servo/components/script/dom/csskeyframesrule.rs b/servo/components/script/dom/csskeyframesrule.rs index 281ccd972a7e..a3715e914edb 100644 --- a/servo/components/script/dom/csskeyframesrule.rs +++ b/servo/components/script/dom/csskeyframesrule.rs @@ -5,7 +5,7 @@ use cssparser::Parser; use dom::bindings::codegen::Bindings::CSSKeyframesRuleBinding; use dom::bindings::codegen::Bindings::CSSKeyframesRuleBinding::CSSKeyframesRuleMethods; -use dom::bindings::error::{Error, ErrorResult}; +use dom::bindings::error::ErrorResult; use dom::bindings::inheritance::Castable; use dom::bindings::js::{MutNullableJS, Root}; use dom::bindings::reflector::{DomObject, reflect_dom_object}; @@ -16,11 +16,11 @@ use dom::cssrulelist::{CSSRuleList, RulesSource}; use dom::cssstylesheet::CSSStyleSheet; use dom::window::Window; use dom_struct::dom_struct; -use servo_atoms::Atom; use std::sync::Arc; use style::keyframes::{Keyframe, KeyframeSelector}; use style::shared_lock::{Locked, ToCssWithGuard}; use style::stylesheets::KeyframesRule; +use style::values::KeyframesName; #[dom_struct] pub struct CSSKeyframesRule { @@ -107,23 +107,17 @@ impl CSSKeyframesRuleMethods for CSSKeyframesRule { // https://drafts.csswg.org/css-animations/#dom-csskeyframesrule-name fn Name(&self) -> DOMString { let guard = self.cssrule.shared_lock().read(); - DOMString::from(&*self.keyframesrule.read_with(&guard).name) + DOMString::from(&**self.keyframesrule.read_with(&guard).name.as_atom()) } // https://drafts.csswg.org/css-animations/#dom-csskeyframesrule-name fn SetName(&self, value: DOMString) -> ErrorResult { - // https://github.com/w3c/csswg-drafts/issues/801 - // Setting this property to a CSS-wide keyword or `none` will - // throw a Syntax Error. - match_ignore_ascii_case! { &value, - "initial" => return Err(Error::Syntax), - "inherit" => return Err(Error::Syntax), - "unset" => return Err(Error::Syntax), - "none" => return Err(Error::Syntax), - _ => () - } + // Spec deviation: https://github.com/w3c/csswg-drafts/issues/801 + // Setting this property to a CSS-wide keyword or `none` does not throw, + // it stores a value that serializes as a quoted string. + let name = KeyframesName::from_ident(value.into()); let mut guard = self.cssrule.shared_lock().write(); - self.keyframesrule.write_with(&mut guard).name = Atom::from(value); + self.keyframesrule.write_with(&mut guard).name = name; Ok(()) } } diff --git a/servo/components/script/dom/cssrule.rs b/servo/components/script/dom/cssrule.rs index 1a24d5839bf7..6bfe057d6d9c 100644 --- a/servo/components/script/dom/cssrule.rs +++ b/servo/components/script/dom/cssrule.rs @@ -78,6 +78,7 @@ impl CSSRule { StyleCssRule::Import(s) => Root::upcast(CSSImportRule::new(window, parent_stylesheet, s)), StyleCssRule::Style(s) => Root::upcast(CSSStyleRule::new(window, parent_stylesheet, s)), StyleCssRule::FontFace(s) => Root::upcast(CSSFontFaceRule::new(window, parent_stylesheet, s)), + StyleCssRule::CounterStyle(_) => unimplemented!(), StyleCssRule::Keyframes(s) => Root::upcast(CSSKeyframesRule::new(window, parent_stylesheet, s)), StyleCssRule::Media(s) => Root::upcast(CSSMediaRule::new(window, parent_stylesheet, s)), StyleCssRule::Namespace(s) => Root::upcast(CSSNamespaceRule::new(window, parent_stylesheet, s)), diff --git a/servo/components/style/animation.rs b/servo/components/style/animation.rs index 8db5ea2235c4..2229d4855911 100644 --- a/servo/components/style/animation.rs +++ b/servo/components/style/animation.rs @@ -464,13 +464,19 @@ pub fn maybe_start_animations(context: &SharedStyleContext, let box_style = new_style.get_box(); for (i, name) in box_style.animation_name_iter().enumerate() { + let name = if let Some(atom) = name.as_atom() { + atom + } else { + continue + }; + debug!("maybe_start_animations: name={}", name); let total_duration = box_style.animation_duration_mod(i).seconds(); if total_duration == 0. { continue } - if let Some(ref anim) = context.stylist.animations().get(&name.0) { + if let Some(ref anim) = context.stylist.animations().get(name) { debug!("maybe_start_animations: animation {} found", name); // If this animation doesn't have any keyframe, we can just continue @@ -506,7 +512,7 @@ pub fn maybe_start_animations(context: &SharedStyleContext, new_animations_sender - .send(Animation::Keyframes(node, name.0.clone(), KeyframesAnimationState { + .send(Animation::Keyframes(node, name.clone(), KeyframesAnimationState { started_at: animation_start, duration: duration as f64, delay: delay as f64, @@ -584,9 +590,10 @@ pub fn update_style_for_animation(context: &SharedStyleContext, debug_assert!(!animation.steps.is_empty()); - let maybe_index = style.get_box() - .animation_name_iter() - .position(|animation_name| *name == animation_name.0); + let maybe_index = style + .get_box() + .animation_name_iter() + .position(|animation_name| Some(name) == animation_name.as_atom()); let index = match maybe_index { Some(index) => index, diff --git a/servo/components/style/counter_style/mod.rs b/servo/components/style/counter_style/mod.rs new file mode 100644 index 000000000000..ca27a650334a --- /dev/null +++ b/servo/components/style/counter_style/mod.rs @@ -0,0 +1,632 @@ +/* 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 [`@counter-style`][counter-style] at-rule. +//! +//! [counter-style]: https://drafts.csswg.org/css-counter-styles/ + +use Atom; +use cssparser::{AtRuleParser, DeclarationListParser, DeclarationParser, Parser, Token}; +use cssparser::{serialize_string, serialize_identifier}; +#[cfg(feature = "gecko")] use gecko::rules::CounterStyleDescriptors; +#[cfg(feature = "gecko")] use gecko_bindings::structs::nsCSSCounterDesc; +use parser::{ParserContext, log_css_error, Parse}; +use shared_lock::{SharedRwLockReadGuard, ToCssWithGuard}; +use std::ascii::AsciiExt; +use std::borrow::Cow; +use std::fmt; +use std::ops::Range; +use style_traits::{ToCss, OneOrMoreCommaSeparated}; +use values::CustomIdent; + +/// Parse the prelude of an @counter-style rule +pub fn parse_counter_style_name(input: &mut Parser) -> Result { + macro_rules! predefined { + ($($name: expr,)+) => { + { + ascii_case_insensitive_phf_map! { + // FIXME: use static atoms https://github.com/rust-lang/rust/issues/33156 + predefined -> &'static str = { + $( + $name => $name, + )+ + } + } + + let ident = input.expect_ident()?; + if let Some(&lower_cased) = predefined(&ident) { + Ok(CustomIdent(Atom::from(lower_cased))) + } else { + // https://github.com/w3c/csswg-drafts/issues/1295 excludes "none" + CustomIdent::from_ident(ident, &["none"]) + } + } + } + } + include!("predefined.rs") +} + +/// Parse the body (inside `{}`) of an @counter-style rule +pub fn parse_counter_style_body(name: CustomIdent, context: &ParserContext, input: &mut Parser) + -> Result { + let start = input.position(); + let mut rule = CounterStyleRule::empty(name); + { + let parser = CounterStyleRuleParser { + context: context, + rule: &mut rule, + }; + let mut iter = DeclarationListParser::new(input, parser); + while let Some(declaration) = iter.next() { + if let Err(range) = declaration { + let pos = range.start; + let message = format!("Unsupported @counter-style descriptor declaration: '{}'", + iter.input.slice(range)); + log_css_error(iter.input, pos, &*message, context); + } + } + } + let error = match *rule.system() { + ref system @ System::Cyclic | + ref system @ System::Fixed { .. } | + ref system @ System::Symbolic | + ref system @ System::Alphabetic | + ref system @ System::Numeric + if rule.symbols.is_none() => { + let system = system.to_css_string(); + Some(format!("Invalid @counter-style rule: 'system: {}' without 'symbols'", system)) + } + ref system @ System::Alphabetic | + ref system @ System::Numeric + if rule.symbols().unwrap().0.len() < 2 => { + let system = system.to_css_string(); + Some(format!("Invalid @counter-style rule: 'system: {}' less than two 'symbols'", + system)) + } + System::Additive if rule.additive_symbols.is_none() => { + let s = "Invalid @counter-style rule: 'system: additive' without 'additive-symbols'"; + Some(s.to_owned()) + } + System::Extends(_) if rule.symbols.is_some() => { + let s = "Invalid @counter-style rule: 'system: extends …' with 'symbols'"; + Some(s.to_owned()) + } + System::Extends(_) if rule.additive_symbols.is_some() => { + let s = "Invalid @counter-style rule: 'system: extends …' with 'additive-symbols'"; + Some(s.to_owned()) + } + _ => None + }; + if let Some(message) = error { + log_css_error(input, start, &message, context); + Err(()) + } else { + Ok(rule) + } +} + +struct CounterStyleRuleParser<'a, 'b: 'a> { + context: &'a ParserContext<'b>, + rule: &'a mut CounterStyleRule, +} + +/// Default methods reject all at rules. +impl<'a, 'b> AtRuleParser for CounterStyleRuleParser<'a, 'b> { + type Prelude = (); + type AtRule = (); +} + +macro_rules! accessor { + (#[$doc: meta] $name: tt $ident: ident: $ty: ty = !) => { + #[$doc] + pub fn $ident(&self) -> Option<&$ty> { + self.$ident.as_ref() + } + }; + + (#[$doc: meta] $name: tt $ident: ident: $ty: ty = $initial: expr) => { + #[$doc] + pub fn $ident(&self) -> Cow<$ty> { + if let Some(ref value) = self.$ident { + Cow::Borrowed(value) + } else { + Cow::Owned($initial) + } + } + } +} + +macro_rules! counter_style_descriptors { + ( + $( #[$doc: meta] $name: tt $ident: ident / $gecko_ident: ident: $ty: ty = $initial: tt )+ + ) => { + /// An @counter-style rule + #[derive(Debug)] + pub struct CounterStyleRule { + name: CustomIdent, + $( + #[$doc] + $ident: Option<$ty>, + )+ + } + + impl CounterStyleRule { + fn empty(name: CustomIdent) -> Self { + CounterStyleRule { + name: name, + $( + $ident: None, + )+ + } + } + + $( + accessor!(#[$doc] $name $ident: $ty = $initial); + )+ + + /// Convert to Gecko types + #[cfg(feature = "gecko")] + pub fn set_descriptors(&self, descriptors: &mut CounterStyleDescriptors) { + $( + if let Some(ref value) = self.$ident { + descriptors[nsCSSCounterDesc::$gecko_ident as usize].set_from(value) + } + )* + } + } + + impl<'a, 'b> DeclarationParser for CounterStyleRuleParser<'a, 'b> { + type Declaration = (); + + fn parse_value(&mut self, name: &str, input: &mut Parser) -> Result<(), ()> { + match_ignore_ascii_case! { name, + $( + $name => { + // DeclarationParser also calls parse_entirely + // so we’d normally not need to, + // but in this case we do because we set the value as a side effect + // rather than returning it. + let value = input.parse_entirely(|i| Parse::parse(self.context, i))?; + self.rule.$ident = Some(value) + } + )* + _ => return Err(()) + } + Ok(()) + } + } + + impl ToCssWithGuard for CounterStyleRule { + fn to_css(&self, _guard: &SharedRwLockReadGuard, dest: &mut W) -> fmt::Result + where W: fmt::Write { + dest.write_str("@counter-style ")?; + self.name.to_css(dest)?; + dest.write_str(" {\n")?; + $( + if let Some(ref value) = self.$ident { + dest.write_str(concat!(" ", $name, ": "))?; + ToCss::to_css(value, dest)?; + dest.write_str(";\n")?; + } + )+ + dest.write_str("}") + } + } + } +} + +counter_style_descriptors! { + /// https://drafts.csswg.org/css-counter-styles/#counter-style-system + "system" system / eCSSCounterDesc_System: System = { + System::Symbolic + } + + /// https://drafts.csswg.org/css-counter-styles/#counter-style-negative + "negative" negative / eCSSCounterDesc_Negative: Negative = { + Negative(Symbol::String("-".to_owned()), None) + } + + /// https://drafts.csswg.org/css-counter-styles/#counter-style-prefix + "prefix" prefix / eCSSCounterDesc_Prefix: Symbol = { + Symbol::String("".to_owned()) + } + + /// https://drafts.csswg.org/css-counter-styles/#counter-style-suffix + "suffix" suffix / eCSSCounterDesc_Suffix: Symbol = { + Symbol::String(". ".to_owned()) + } + + /// https://drafts.csswg.org/css-counter-styles/#counter-style-range + "range" range / eCSSCounterDesc_Range: Ranges = { + Ranges(Vec::new()) // Empty Vec represents 'auto' + } + + /// https://drafts.csswg.org/css-counter-styles/#counter-style-pad + "pad" pad / eCSSCounterDesc_Pad: Pad = { + Pad(0, Symbol::String("".to_owned())) + } + + /// https://drafts.csswg.org/css-counter-styles/#counter-style-fallback + "fallback" fallback / eCSSCounterDesc_Fallback: Fallback = { + // FIXME https://bugzilla.mozilla.org/show_bug.cgi?id=1359323 use atom!() + Fallback(CustomIdent(Atom::from("decimal"))) + } + + /// https://drafts.csswg.org/css-counter-styles/#descdef-counter-style-symbols + "symbols" symbols / eCSSCounterDesc_Symbols: Symbols = ! + + /// https://drafts.csswg.org/css-counter-styles/#descdef-counter-style-additive-symbols + "additive-symbols" additive_symbols / eCSSCounterDesc_AdditiveSymbols: AdditiveSymbols = ! + + /// https://drafts.csswg.org/css-counter-styles/#counter-style-speak-as + "speak-as" speak_as / eCSSCounterDesc_SpeakAs: SpeakAs = { + SpeakAs::Auto + } +} + +/// https://drafts.csswg.org/css-counter-styles/#counter-style-system +#[derive(Debug, Clone)] +pub enum System { + /// 'cyclic' + Cyclic, + /// 'numeric' + Numeric, + /// 'alphabetic' + Alphabetic, + /// 'symbolic' + Symbolic, + /// 'additive' + Additive, + /// 'fixed ?' + Fixed { + /// '?' + first_symbol_value: Option + }, + /// 'extends ' + Extends(CustomIdent), +} + +impl Parse for System { + fn parse(_context: &ParserContext, input: &mut Parser) -> Result { + match_ignore_ascii_case! { &input.expect_ident()?, + "cyclic" => Ok(System::Cyclic), + "numeric" => Ok(System::Numeric), + "alphabetic" => Ok(System::Alphabetic), + "symbolic" => Ok(System::Symbolic), + "additive" => Ok(System::Additive), + "fixed" => { + let first_symbol_value = input.try(|i| i.expect_integer()).ok(); + Ok(System::Fixed { first_symbol_value: first_symbol_value }) + } + "extends" => { + let other = parse_counter_style_name(input)?; + Ok(System::Extends(other)) + } + _ => Err(()) + } + } +} + +impl ToCss for System { + fn to_css(&self, dest: &mut W) -> fmt::Result where W: fmt::Write { + match *self { + System::Cyclic => dest.write_str("cyclic"), + System::Numeric => dest.write_str("numeric"), + System::Alphabetic => dest.write_str("alphabetic"), + System::Symbolic => dest.write_str("symbolic"), + System::Additive => dest.write_str("additive"), + System::Fixed { first_symbol_value } => { + if let Some(value) = first_symbol_value { + write!(dest, "fixed {}", value) + } else { + dest.write_str("fixed") + } + } + System::Extends(ref other) => { + dest.write_str("extends ")?; + other.to_css(dest) + } + } + } +} + +/// https://drafts.csswg.org/css-counter-styles/#typedef-symbol +#[derive(Debug, Clone)] +pub enum Symbol { + /// + String(String), + /// + Ident(String), + // Not implemented: + // /// + // Image(Image), +} + +impl Parse for Symbol { + fn parse(_context: &ParserContext, input: &mut Parser) -> Result { + match input.next() { + Ok(Token::QuotedString(s)) => Ok(Symbol::String(s.into_owned())), + Ok(Token::Ident(s)) => Ok(Symbol::Ident(s.into_owned())), + _ => Err(()) + } + } +} + +impl ToCss for Symbol { + fn to_css(&self, dest: &mut W) -> fmt::Result where W: fmt::Write { + match *self { + Symbol::String(ref s) => serialize_string(s, dest), + Symbol::Ident(ref s) => serialize_identifier(s, dest), + } + } +} + +/// https://drafts.csswg.org/css-counter-styles/#counter-style-negative +#[derive(Debug, Clone)] +pub struct Negative(pub Symbol, pub Option); + +impl Parse for Negative { + fn parse(context: &ParserContext, input: &mut Parser) -> Result { + Ok(Negative( + Symbol::parse(context, input)?, + input.try(|input| Symbol::parse(context, input)).ok(), + )) + } +} + +impl ToCss for Negative { + fn to_css(&self, dest: &mut W) -> fmt::Result where W: fmt::Write { + self.0.to_css(dest)?; + if let Some(ref symbol) = self.1 { + dest.write_char(' ')?; + symbol.to_css(dest)? + } + Ok(()) + } +} + +/// https://drafts.csswg.org/css-counter-styles/#counter-style-range +/// +/// Empty Vec represents 'auto' +#[derive(Debug, Clone)] +pub struct Ranges(pub Vec>>); + +impl Parse for Ranges { + fn parse(_context: &ParserContext, input: &mut Parser) -> Result { + if input.try(|input| input.expect_ident_matching("auto")).is_ok() { + Ok(Ranges(Vec::new())) + } else { + input.parse_comma_separated(|input| { + let opt_start = parse_bound(input)?; + let opt_end = parse_bound(input)?; + if let (Some(start), Some(end)) = (opt_start, opt_end) { + if start > end { + return Err(()) + } + } + Ok(opt_start..opt_end) + }).map(Ranges) + } + } +} + +fn parse_bound(input: &mut Parser) -> Result, ()> { + match input.next() { + Ok(Token::Number(ref v)) if v.int_value.is_some() => Ok(Some(v.int_value.unwrap())), + Ok(Token::Ident(ref ident)) if ident.eq_ignore_ascii_case("infinite") => Ok(None), + _ => Err(()) + } +} + +impl ToCss for Ranges { + fn to_css(&self, dest: &mut W) -> fmt::Result where W: fmt::Write { + let mut iter = self.0.iter(); + if let Some(first) = iter.next() { + range_to_css(first, dest)?; + for item in iter { + dest.write_str(", ")?; + range_to_css(item, dest)?; + } + Ok(()) + } else { + dest.write_str("auto") + } + } +} + +fn range_to_css(range: &Range>, dest: &mut W) -> fmt::Result +where W: fmt::Write { + bound_to_css(range.start, dest)?; + dest.write_char(' ')?; + bound_to_css(range.end, dest) +} + +fn bound_to_css(range: Option, dest: &mut W) -> fmt::Result where W: fmt::Write { + if let Some(finite) = range { + write!(dest, "{}", finite) + } else { + dest.write_str("infinite") + } +} + +/// https://drafts.csswg.org/css-counter-styles/#counter-style-pad +#[derive(Debug, Clone)] +pub struct Pad(pub u32, pub Symbol); + +impl Parse for Pad { + fn parse(context: &ParserContext, input: &mut Parser) -> Result { + let pad_with = input.try(|input| Symbol::parse(context, input)); + let min_length = input.expect_integer()?; + if min_length < 0 { + return Err(()) + } + let pad_with = pad_with.or_else(|()| Symbol::parse(context, input))?; + Ok(Pad(min_length as u32, pad_with)) + } +} + +impl ToCss for Pad { + fn to_css(&self, dest: &mut W) -> fmt::Result where W: fmt::Write { + write!(dest, "{} ", self.0)?; + self.1.to_css(dest) + } +} + +/// https://drafts.csswg.org/css-counter-styles/#counter-style-fallback +#[derive(Debug, Clone)] +pub struct Fallback(pub CustomIdent); + +impl Parse for Fallback { + fn parse(_context: &ParserContext, input: &mut Parser) -> Result { + parse_counter_style_name(input).map(Fallback) + } +} + +impl ToCss for Fallback { + fn to_css(&self, dest: &mut W) -> fmt::Result where W: fmt::Write { + self.0.to_css(dest) + } +} + +/// https://drafts.csswg.org/css-counter-styles/#descdef-counter-style-symbols +#[derive(Debug, Clone)] +pub struct Symbols(pub Vec); + +impl Parse for Symbols { + fn parse(context: &ParserContext, input: &mut Parser) -> Result { + let mut symbols = Vec::new(); + loop { + if let Ok(s) = input.try(|input| Symbol::parse(context, input)) { + symbols.push(s) + } else { + if symbols.is_empty() { + return Err(()) + } else { + return Ok(Symbols(symbols)) + } + } + } + } +} + +impl ToCss for Symbols { + fn to_css(&self, dest: &mut W) -> fmt::Result where W: fmt::Write { + let mut iter = self.0.iter(); + let first = iter.next().expect("expected at least one symbol"); + first.to_css(dest)?; + for item in iter { + dest.write_char(' ')?; + item.to_css(dest)?; + } + Ok(()) + } +} + +/// https://drafts.csswg.org/css-counter-styles/#descdef-counter-style-additive-symbols +#[derive(Debug, Clone)] +pub struct AdditiveSymbols(pub Vec); + +impl Parse for AdditiveSymbols { + fn parse(context: &ParserContext, input: &mut Parser) -> Result { + let tuples = Vec::::parse(context, input)?; + // FIXME maybe? https://github.com/w3c/csswg-drafts/issues/1220 + if tuples.windows(2).any(|window| window[0].value <= window[1].value) { + return Err(()) + } + Ok(AdditiveSymbols(tuples)) + } +} + +impl ToCss for AdditiveSymbols { + fn to_css(&self, dest: &mut W) -> fmt::Result where W: fmt::Write { + self.0.to_css(dest) + } +} + +/// && +#[derive(Debug, Clone)] +pub struct AdditiveTuple { + value: u32, + symbol: Symbol, +} + +impl OneOrMoreCommaSeparated for AdditiveTuple {} + +impl Parse for AdditiveTuple { + fn parse(context: &ParserContext, input: &mut Parser) -> Result { + let symbol = input.try(|input| Symbol::parse(context, input)); + let value = input.expect_integer()?; + if value < 0 { + return Err(()) + } + let symbol = symbol.or_else(|()| Symbol::parse(context, input))?; + Ok(AdditiveTuple { + value: value as u32, + symbol: symbol, + }) + } +} + +impl ToCss for AdditiveTuple { + fn to_css(&self, dest: &mut W) -> fmt::Result where W: fmt::Write { + write!(dest, "{} ", self.value)?; + self.symbol.to_css(dest) + } +} + +/// https://drafts.csswg.org/css-counter-styles/#counter-style-speak-as +#[derive(Debug, Clone)] +pub enum SpeakAs { + /// auto + Auto, + /// bullets + Bullets, + /// numbers + Numbers, + /// words + Words, + // /// spell-out, not supported, see bug 1024178 + // SpellOut, + /// + Other(CustomIdent), +} + +impl Parse for SpeakAs { + fn parse(_context: &ParserContext, input: &mut Parser) -> Result { + let mut is_spell_out = false; + let result = input.try(|input| { + match_ignore_ascii_case! { &input.expect_ident()?, + "auto" => Ok(SpeakAs::Auto), + "bullets" => Ok(SpeakAs::Bullets), + "numbers" => Ok(SpeakAs::Numbers), + "words" => Ok(SpeakAs::Words), + "spell-out" => { + is_spell_out = true; + Err(()) + } + _ => Err(()) + } + }); + if is_spell_out { + // spell-out is not supported, but don’t parse it as a . + // See bug 1024178. + return Err(()) + } + result.or_else(|()| { + Ok(SpeakAs::Other(parse_counter_style_name(input)?)) + }) + } +} + +impl ToCss for SpeakAs { + fn to_css(&self, dest: &mut W) -> fmt::Result where W: fmt::Write { + match *self { + SpeakAs::Auto => dest.write_str("auto"), + SpeakAs::Bullets => dest.write_str("bullets"), + SpeakAs::Numbers => dest.write_str("numbers"), + SpeakAs::Words => dest.write_str("words"), + SpeakAs::Other(ref other) => other.to_css(dest), + } + } +} diff --git a/servo/components/style/counter_style/predefined.rs b/servo/components/style/counter_style/predefined.rs new file mode 100644 index 000000000000..7b27514cd33a --- /dev/null +++ b/servo/components/style/counter_style/predefined.rs @@ -0,0 +1,61 @@ +/* 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/. */ + +predefined! { + "decimal", + "decimal-leading-zero", + "arabic-indic", + "armenian", + "upper-armenian", + "lower-armenian", + "bengali", + "cambodian", + "khmer", + "cjk-decimal", + "devanagari", + "georgian", + "gujarati", + "gurmukhi", + "hebrew", + "kannada", + "lao", + "malayalam", + "mongolian", + "myanmar", + "oriya", + "persian", + "lower-roman", + "upper-roman", + "tamil", + "telugu", + "thai", + "tibetan", + "lower-alpha", + "lower-latin", + "upper-alpha", + "upper-latin", + "cjk-earthly-branch", + "cjk-heavenly-stem", + "lower-greek", + "hiragana", + "hiragana-iroha", + "katakana", + "katakana-iroha", + "disc", + "circle", + "square", + "disclosure-open", + "disclosure-closed", + "japanese-informal", + "japanese-formal", + "korean-hangul-formal", + "korean-hanja-informal", + "korean-hanja-formal", + "simp-chinese-informal", + "simp-chinese-formal", + "trad-chinese-informal", + "trad-chinese-formal", + "cjk-ideographic", + "ethiopic-numeric", +} diff --git a/servo/components/style/counter_style/update_predefined.py b/servo/components/style/counter_style/update_predefined.py new file mode 100755 index 000000000000..3e5be33e4486 --- /dev/null +++ b/servo/components/style/counter_style/update_predefined.py @@ -0,0 +1,32 @@ +#!/usr/bin/env 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/. */ + +import os.path +import re +import urllib + + +def main(filename): + names = [ + re.search('>([^>]+)(| nscssvalue.set_enum(structs::NS_STYLE_COUNTER_SYSTEM_CYCLIC as i32), + Numeric => nscssvalue.set_enum(structs::NS_STYLE_COUNTER_SYSTEM_NUMERIC as i32), + Alphabetic => nscssvalue.set_enum(structs::NS_STYLE_COUNTER_SYSTEM_ALPHABETIC as i32), + Symbolic => nscssvalue.set_enum(structs::NS_STYLE_COUNTER_SYSTEM_SYMBOLIC as i32), + Additive => nscssvalue.set_enum(structs::NS_STYLE_COUNTER_SYSTEM_ADDITIVE as i32), + Fixed { first_symbol_value } => { + let mut a = nsCSSValue::null(); + let mut b = nsCSSValue::null(); + a.set_enum(structs::NS_STYLE_COUNTER_SYSTEM_FIXED as i32); + b.set_integer(first_symbol_value.unwrap_or(1)); + //nscssvalue.set_pair(a, b); // FIXME: add bindings for nsCSSValue::SetPairValue + } + Extends(ref other) => { + let mut a = nsCSSValue::null(); + let mut b = nsCSSValue::null(); + a.set_enum(structs::NS_STYLE_COUNTER_SYSTEM_EXTENDS as i32); + b.set_string_from_atom(&other.0); + //nscssvalue.set_pair(a, b); // FIXME: add bindings for nsCSSValue::SetPairValue + } + } + } +} + +impl ToNsCssValue for counter_style::Negative { + fn convert(&self, nscssvalue: &mut nsCSSValue) { + if let Some(ref second) = self.1 { + let mut a = nsCSSValue::null(); + let mut b = nsCSSValue::null(); + a.set_from(&self.0); + b.set_from(second); + //nscssvalue.set_pair(a, b); // FIXME: add bindings for nsCSSValue::SetPairValue + } else { + nscssvalue.set_from(&self.0) + } + } +} + +impl ToNsCssValue for counter_style::Symbol { + fn convert(&self, nscssvalue: &mut nsCSSValue) { + match *self { + counter_style::Symbol::String(ref s) => nscssvalue.set_string(s), + counter_style::Symbol::Ident(ref s) => nscssvalue.set_ident(s), + } + } +} + +impl ToNsCssValue for counter_style::Ranges { + fn convert(&self, _nscssvalue: &mut nsCSSValue) { + if self.0.is_empty() { + //nscssvalue.set_auto(); // FIXME: add bindings for nsCSSValue::SetAutoValue + } else { + for range in &self.0 { + fn set_bound(bound: Option, nscssvalue: &mut nsCSSValue) { + if let Some(finite) = bound { + nscssvalue.set_integer(finite) + } else { + nscssvalue.set_enum(structs::NS_STYLE_COUNTER_RANGE_INFINITE as i32) + } + } + let mut start = nsCSSValue::null(); + let mut end = nsCSSValue::null(); + set_bound(range.start, &mut start); + set_bound(range.end, &mut end); + // FIXME: add bindings for nsCSSValuePairList + } + } + } +} + +impl ToNsCssValue for counter_style::Pad { + fn convert(&self, _nscssvalue: &mut nsCSSValue) { + let mut min_length = nsCSSValue::null(); + let mut pad_with = nsCSSValue::null(); + min_length.set_integer(self.0 as i32); + pad_with.set_from(&self.1); + // FIXME: add bindings for nsCSSValue::SetPairValue + //nscssvalue.set_pair(min_length, pad_with); + } +} + +impl ToNsCssValue for counter_style::Fallback { + fn convert(&self, nscssvalue: &mut nsCSSValue) { + nscssvalue.set_ident_from_atom(&self.0 .0) + } +} + +impl ToNsCssValue for counter_style::Symbols { + fn convert(&self, _nscssvalue: &mut nsCSSValue) { + debug_assert!(!self.0.is_empty()); + // FIXME: add bindings for nsCSSValueList + } +} + +impl ToNsCssValue for counter_style::AdditiveSymbols { + fn convert(&self, _nscssvalue: &mut nsCSSValue) { + debug_assert!(!self.0.is_empty()); + // FIXME: add bindings for nsCSSValuePairList + } +} + +impl ToNsCssValue for counter_style::SpeakAs { + fn convert(&self, nscssvalue: &mut nsCSSValue) { + use counter_style::SpeakAs::*; + match *self { + Auto => {} //nscssvalue.set_auto(), // FIXME: add bindings for nsCSSValue::SetAutoValue + Bullets => nscssvalue.set_enum(structs::NS_STYLE_COUNTER_SPEAKAS_BULLETS as i32), + Numbers => nscssvalue.set_enum(structs::NS_STYLE_COUNTER_SPEAKAS_NUMBERS as i32), + Words => nscssvalue.set_enum(structs::NS_STYLE_COUNTER_SPEAKAS_WORDS as i32), + Other(ref other) => nscssvalue.set_ident_from_atom(&other.0), + } + } +} diff --git a/servo/components/style/gecko_bindings/sugar/ns_css_value.rs b/servo/components/style/gecko_bindings/sugar/ns_css_value.rs index d22f1148549e..8d89db94c9cc 100644 --- a/servo/components/style/gecko_bindings/sugar/ns_css_value.rs +++ b/servo/components/style/gecko_bindings/sugar/ns_css_value.rs @@ -124,6 +124,11 @@ impl nsCSSValue { self.set_string_from_atom_internal(s, nsCSSUnit::eCSSUnit_String) } + /// Set to a ident value from the given atom + pub fn set_ident_from_atom(&mut self, s: &Atom) { + self.set_string_from_atom_internal(s, nsCSSUnit::eCSSUnit_Ident) + } + /// Set to an identifier value pub fn set_ident(&mut self, s: &str) { self.set_string_internal(s, nsCSSUnit::eCSSUnit_Ident) diff --git a/servo/components/style/gecko_string_cache/mod.rs b/servo/components/style/gecko_string_cache/mod.rs index 82589b645ea0..63ba6d7977ce 100644 --- a/servo/components/style/gecko_string_cache/mod.rs +++ b/servo/components/style/gecko_string_cache/mod.rs @@ -13,6 +13,7 @@ use gecko_bindings::bindings::Gecko_ReleaseAtom; use gecko_bindings::structs::nsIAtom; use nsstring::nsAString; use precomputed_hash::PrecomputedHash; +use std::ascii::AsciiExt; use std::borrow::{Cow, Borrow}; use std::char::{self, DecodeUtf16}; use std::fmt::{self, Write}; @@ -163,6 +164,12 @@ impl WeakAtom { } } + /// Returns whether this atom is the empty string. + #[inline] + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + /// Returns the atom as a mutable pointer. #[inline] pub fn as_ptr(&self) -> *mut nsIAtom { @@ -218,6 +225,25 @@ impl Atom { Atom(WeakAtom::new(ptr)) } } + + /// Return whether two atoms are ASCII-case-insensitive matches + pub fn eq_ignore_ascii_case(&self, other: &Self) -> bool { + let a = self.as_slice(); + let b = other.as_slice(); + a.len() == b.len() && a.iter().zip(b).all(|(&a16, &b16)| { + if a16 <= 0x7F && b16 <= 0x7F { + (a16 as u8).eq_ignore_ascii_case(&(b16 as u8)) + } else { + a16 == b16 + } + }) + } + + /// Return whether this atom is an ASCII-case-insensitive match for the given string + pub fn eq_str_ignore_ascii_case(&self, other: &str) -> bool { + self.chars().map(|r| r.map(|c: char| c.to_ascii_lowercase())) + .eq(other.chars().map(|c: char| Ok(c.to_ascii_lowercase()))) + } } impl Hash for Atom { diff --git a/servo/components/style/lib.rs b/servo/components/style/lib.rs index 74f5cc5d4b68..7479279418dd 100644 --- a/servo/components/style/lib.rs +++ b/servo/components/style/lib.rs @@ -89,6 +89,7 @@ pub mod bloom; pub mod cache; pub mod cascade_info; pub mod context; +pub mod counter_style; pub mod custom_properties; pub mod data; pub mod dom; diff --git a/servo/components/style/matching.rs b/servo/components/style/matching.rs index e1c1ba47223c..201f6c508785 100644 --- a/servo/components/style/matching.rs +++ b/servo/components/style/matching.rs @@ -560,7 +560,7 @@ trait PrivateMatchMethods: TElement { pseudo: Option<&PseudoElement>) -> bool { let ref new_box_style = new_values.get_box(); let has_new_animation_style = new_box_style.animation_name_count() >= 1 && - new_box_style.animation_name_at(0).0.len() != 0; + new_box_style.animation_name_at(0).0.is_some(); let has_animations = self.has_css_animations(pseudo); old_values.as_ref().map_or(has_new_animation_style, |ref old| { diff --git a/servo/components/style/properties/gecko.mako.rs b/servo/components/style/properties/gecko.mako.rs index 5190d6c3baf7..57ebd1114ff9 100644 --- a/servo/components/style/properties/gecko.mako.rs +++ b/servo/components/style/properties/gecko.mako.rs @@ -62,7 +62,7 @@ use std::ptr; use std::sync::Arc; use std::cmp; use values::computed::ToComputedValue; -use values::{Either, Auto}; +use values::{Either, Auto, KeyframesName}; use computed_values::border_style; pub mod style_structs { @@ -1175,11 +1175,10 @@ fn static_assert() { % for value in GRID_LINES: pub fn set_${value.name}(&mut self, v: longhands::${value.name}::computed_value::T) { - use nsstring::nsCString; use gecko_bindings::structs::{nsStyleGridLine_kMinLine, nsStyleGridLine_kMaxLine}; let ident = v.ident.unwrap_or(String::new()); - self.gecko.${value.gecko}.mLineName.assign_utf8(&nsCString::from(&*ident)); + self.gecko.${value.gecko}.mLineName.assign_utf8(&ident); self.gecko.${value.gecko}.mHasSpan = v.is_span; self.gecko.${value.gecko}.mInteger = v.integer.map(|i| { // clamping the integer between a range @@ -2197,23 +2196,28 @@ fn static_assert() { } pub fn set_animation_name(&mut self, v: longhands::animation_name::computed_value::T) { - use nsstring::nsCString; - debug_assert!(!v.0.is_empty()); unsafe { self.gecko.mAnimations.ensure_len(v.0.len()) }; self.gecko.mAnimationNameCount = v.0.len() as u32; for (servo, gecko) in v.0.into_iter().zip(self.gecko.mAnimations.iter_mut()) { // TODO This is inefficient. We should fix this in bug 1329169. - gecko.mName.assign_utf8(&nsCString::from(servo.0.to_string())); + gecko.mName.assign(match servo.0 { + Some(ref name) => name.as_atom().as_slice(), + None => &[], // Empty string for 'none' + }); } } pub fn animation_name_at(&self, index: usize) -> longhands::animation_name::computed_value::SingleComputedValue { - use Atom; use properties::longhands::animation_name::single_value::SpecifiedValue as AnimationName; // XXX: Is there any effective ways? - AnimationName(Atom::from(String::from_utf16_lossy(&self.gecko.mAnimations[index].mName[..]))) + let atom = &self.gecko.mAnimations[index].mName; + if atom.is_empty() { + AnimationName(None) + } else { + AnimationName(Some(KeyframesName::from_ident(atom.to_string()))) + } } pub fn copy_animation_name_from(&mut self, other: &Self) { unsafe { self.gecko.mAnimations.ensure_len(other.gecko.mAnimations.len()) }; @@ -2886,15 +2890,14 @@ fn static_assert() { pub fn set_quotes(&mut self, other: longhands::quotes::computed_value::T) { use gecko_bindings::bindings::Gecko_NewStyleQuoteValues; use gecko_bindings::sugar::refptr::UniqueRefPtr; - use nsstring::nsCString; let mut refptr = unsafe { UniqueRefPtr::from_addrefed(Gecko_NewStyleQuoteValues(other.0.len() as u32)) }; for (servo, gecko) in other.0.into_iter().zip(refptr.mQuotePairs.iter_mut()) { - gecko.first.assign_utf8(&nsCString::from(&*servo.0)); - gecko.second.assign_utf8(&nsCString::from(&*servo.1)); + gecko.first.assign_utf8(&servo.0); + gecko.second.assign_utf8(&servo.1); } unsafe { self.gecko.mQuotes.set_move(refptr.get()) } @@ -3381,7 +3384,6 @@ fn static_assert() { <%call expr="impl_simple_copy('text_emphasis_position', 'mTextEmphasisPosition')"> pub fn set_text_emphasis_style(&mut self, v: longhands::text_emphasis_style::computed_value::T) { - use nsstring::nsCString; use properties::longhands::text_emphasis_style::computed_value::T; use properties::longhands::text_emphasis_style::ShapeKeyword; @@ -3408,7 +3410,7 @@ fn static_assert() { (structs::NS_STYLE_TEXT_EMPHASIS_STYLE_STRING, &**s) }, }; - self.gecko.mTextEmphasisStyleString.assign_utf8(&nsCString::from(s)); + self.gecko.mTextEmphasisStyleString.assign_utf8(s); self.gecko.mTextEmphasisStyle = te as u8; } @@ -3489,12 +3491,11 @@ fn static_assert() { use properties::longhands::text_overflow::{SpecifiedValue, Side}; fn set(side: &mut nsStyleTextOverflowSide, value: &Side) { - use nsstring::nsCString; let ty = match *value { Side::Clip => structs::NS_STYLE_TEXT_OVERFLOW_CLIP, Side::Ellipsis => structs::NS_STYLE_TEXT_OVERFLOW_ELLIPSIS, Side::String(ref s) => { - side.mString.assign_utf8(&nsCString::from(&**s)); + side.mString.assign_utf8(s); structs::NS_STYLE_TEXT_OVERFLOW_STRING } }; @@ -4011,9 +4012,9 @@ clip-path unsafe { bindings::Gecko_ClearAndResizeCounter${counter_property}s(&mut self.gecko, v.0.len() as u32); - for (i, item) in v.0.into_iter().enumerate() { - self.gecko.m${counter_property}s[i].mCounter.assign_utf8(&item.0); - self.gecko.m${counter_property}s[i].mValue = item.1; + for (i, (name, value)) in v.0.into_iter().enumerate() { + self.gecko.m${counter_property}s[i].mCounter.assign(name.0.as_slice()); + self.gecko.m${counter_property}s[i].mValue = value; } } } diff --git a/servo/components/style/properties/longhand/box.mako.rs b/servo/components/style/properties/longhand/box.mako.rs index 6af83510664b..2806a04e8031 100644 --- a/servo/components/style/properties/longhand/box.mako.rs +++ b/servo/components/style/properties/longhand/box.mako.rs @@ -796,7 +796,7 @@ ${helpers.single_keyword("overflow-x", "visible hidden scroll auto", use std::ops::Deref; use style_traits::ToCss; use values::computed::ComputedValueAsSpecified; - use values::HasViewportPercentage; + use values::{HasViewportPercentage, KeyframesName}; pub mod computed_value { pub use super::SpecifiedValue as T; @@ -804,7 +804,14 @@ ${helpers.single_keyword("overflow-x", "visible hidden scroll auto", #[derive(Clone, Debug, Hash, Eq, PartialEq)] #[cfg_attr(feature = "servo", derive(HeapSizeOf))] - pub struct SpecifiedValue(pub Atom); + pub struct SpecifiedValue(pub Option); + + impl SpecifiedValue { + /// As an Atom + pub fn as_atom(&self) -> Option< &Atom> { + self.0.as_ref().map(|n| n.as_atom()) + } + } #[inline] pub fn get_initial_value() -> computed_value::T { @@ -813,48 +820,32 @@ ${helpers.single_keyword("overflow-x", "visible hidden scroll auto", #[inline] pub fn get_initial_specified_value() -> SpecifiedValue { - SpecifiedValue(atom!("")) + SpecifiedValue(None) } impl fmt::Display for SpecifiedValue { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - self.0.fmt(f) + self.to_css(f) } } impl ToCss for SpecifiedValue { fn to_css(&self, dest: &mut W) -> fmt::Result where W: fmt::Write { - if self.0 == atom!("") { - dest.write_str("none") + if let Some(ref name) = self.0 { + name.to_css(dest) } else { - dest.write_str(&*self.0.to_string()) + dest.write_str("none") } } } impl Parse for SpecifiedValue { - fn parse(_context: &ParserContext, input: &mut ::cssparser::Parser) -> Result { - use cssparser::Token; - use properties::CSSWideKeyword; - use std::ascii::AsciiExt; - - let atom = match input.next() { - Ok(Token::Ident(ref value)) => { - if CSSWideKeyword::from_ident(value).is_some() { - // We allow any ident for the animation-name except one - // of the CSS-wide keywords. - return Err(()); - } else if value.eq_ignore_ascii_case("none") { - // FIXME We may want to support `@keyframes ""` at some point. - atom!("") - } else { - Atom::from(&**value) - } - } - Ok(Token::QuotedString(value)) => Atom::from(&*value), - _ => return Err(()), - }; - Ok(SpecifiedValue(atom)) + fn parse(context: &ParserContext, input: &mut ::cssparser::Parser) -> Result { + if let Ok(name) = input.try(|input| KeyframesName::parse(context, input)) { + Ok(SpecifiedValue(Some(name))) + } else { + input.expect_ident_matching("none").map(|()| SpecifiedValue(None)) + } } } no_viewport_percentage!(SpecifiedValue); diff --git a/servo/components/style/properties/longhand/counters.mako.rs b/servo/components/style/properties/longhand/counters.mako.rs index c8dc8ebb4ce6..d5e68d72970c 100644 --- a/servo/components/style/properties/longhand/counters.mako.rs +++ b/servo/components/style/properties/longhand/counters.mako.rs @@ -240,21 +240,22 @@ use std::fmt; use style_traits::ToCss; use super::content; - use values::HasViewportPercentage; + use values::{HasViewportPercentage, CustomIdent}; use cssparser::{Token, serialize_identifier}; use std::borrow::{Cow, ToOwned}; #[derive(Debug, Clone, PartialEq)] - pub struct SpecifiedValue(pub Vec<(String, specified::Integer)>); + pub struct SpecifiedValue(pub Vec<(CustomIdent, specified::Integer)>); pub mod computed_value { use std::fmt; use style_traits::ToCss; + use values::CustomIdent; #[derive(Debug, Clone, PartialEq)] #[cfg_attr(feature = "servo", derive(HeapSizeOf))] - pub struct T(pub Vec<(String, i32)>); + pub struct T(pub Vec<(CustomIdent, i32)>); impl ToCss for T { fn to_css(&self, dest: &mut W) -> fmt::Result @@ -266,14 +267,14 @@ } let mut first = true; - for pair in &self.0 { + for &(ref name, value) in &self.0 { if !first { - try!(dest.write_str(" ")); + dest.write_str(" ")?; } first = false; - try!(serialize_identifier(&pair.0, dest)); - try!(dest.write_str(" ")); - try!(pair.1.to_css(dest)); + name.to_css(dest)?; + dest.write_str(" ")?; + value.to_css(dest)?; } Ok(()) } @@ -284,14 +285,14 @@ type ComputedValue = computed_value::T; fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { - computed_value::T(self.0.iter().map(|entry| { - (entry.0.clone(), entry.1.to_computed_value(context)) + computed_value::T(self.0.iter().map(|&(ref name, ref value)| { + (name.clone(), value.to_computed_value(context)) }).collect::>()) } fn from_computed_value(computed: &Self::ComputedValue) -> Self { - SpecifiedValue(computed.0.iter().map(|entry| { - (entry.0.clone(), specified::Integer::from_computed_value(&entry.1)) + SpecifiedValue(computed.0.iter().map(|&(ref name, ref value)| { + (name.clone(), specified::Integer::from_computed_value(&value)) }).collect::>()) } } @@ -311,14 +312,14 @@ return dest.write_str("none"); } let mut first = true; - for pair in &self.0 { + for &(ref name, ref value) in &self.0 { if !first { - try!(dest.write_str(" ")); + dest.write_str(" ")?; } first = false; - try!(serialize_identifier(&pair.0, dest)); - try!(dest.write_str(" ")); - try!(pair.1.to_css(dest)); + name.to_css(dest)?; + dest.write_str(" ")?; + value.to_css(dest)?; } Ok(()) @@ -339,13 +340,7 @@ let mut counters = Vec::new(); loop { let counter_name = match input.next() { - Ok(Token::Ident(ident)) => { - if CSSWideKeyword::from_ident(&ident).is_some() || ident.eq_ignore_ascii_case("none") { - // Don't accept CSS-wide keywords or none as the counter name. - return Err(()); - } - (*ident).to_owned() - } + Ok(Token::Ident(ident)) => CustomIdent::from_ident(ident, &["none"])?, Ok(_) => return Err(()), Err(_) => break, }; diff --git a/servo/components/style/properties/properties.mako.rs b/servo/components/style/properties/properties.mako.rs index 0b34c5193e27..69932549fe29 100644 --- a/servo/components/style/properties/properties.mako.rs +++ b/servo/components/style/properties/properties.mako.rs @@ -444,6 +444,7 @@ impl CSSWideKeyword { /// to a CSSWideKeyword. pub fn from_ident<'i>(ident: &Cow<'i, str>) -> Option { match_ignore_ascii_case! { ident, + // If modifying this set of keyword, also update values::CustomIdent::from_ident "initial" => Some(CSSWideKeyword::Initial), "inherit" => Some(CSSWideKeyword::Inherit), "unset" => Some(CSSWideKeyword::Unset), @@ -1616,7 +1617,7 @@ pub mod style_structs { /// Returns whether there is any animation specified with /// animation-name other than `none`. pub fn specifies_animations(&self) -> bool { - self.animation_name_iter().any(|name| name.0 != atom!("")) + self.animation_name_iter().any(|name| name.0.is_some()) } /// Returns whether there are any transitions specified. diff --git a/servo/components/style/stylesheets.rs b/servo/components/style/stylesheets.rs index b5ac86188d61..9986328cb009 100644 --- a/servo/components/style/stylesheets.rs +++ b/servo/components/style/stylesheets.rs @@ -7,8 +7,9 @@ #![deny(missing_docs)] use {Atom, Prefix, Namespace}; +use counter_style::{CounterStyleRule, parse_counter_style_name, parse_counter_style_body}; use cssparser::{AtRuleParser, Parser, QualifiedRuleParser}; -use cssparser::{AtRuleType, RuleListParser, Token, parse_one_rule}; +use cssparser::{AtRuleType, RuleListParser, parse_one_rule}; use cssparser::ToCss as ParserToCss; use error_reporting::{ParseErrorReporter, NullReporter}; #[cfg(feature = "servo")] @@ -40,6 +41,7 @@ use str::starts_with_ignore_ascii_case; use style_traits::ToCss; use stylist::FnvHashMap; use supports::SupportsCondition; +use values::{CustomIdent, KeyframesName}; use values::specified::url::SpecifiedUrl; use viewport::ViewportRule; @@ -289,6 +291,7 @@ pub enum CssRule { Style(Arc>), Media(Arc>), FontFace(Arc>), + CounterStyle(Arc>), Viewport(Arc>), Keyframes(Arc>), Supports(Arc>), @@ -331,15 +334,16 @@ impl CssRule { #[allow(missing_docs)] pub fn rule_type(&self) -> CssRuleType { match *self { - CssRule::Style(_) => CssRuleType::Style, - CssRule::Import(_) => CssRuleType::Import, - CssRule::Media(_) => CssRuleType::Media, - CssRule::FontFace(_) => CssRuleType::FontFace, + CssRule::Style(_) => CssRuleType::Style, + CssRule::Import(_) => CssRuleType::Import, + CssRule::Media(_) => CssRuleType::Media, + CssRule::FontFace(_) => CssRuleType::FontFace, + CssRule::CounterStyle(_) => CssRuleType::CounterStyle, CssRule::Keyframes(_) => CssRuleType::Keyframes, CssRule::Namespace(_) => CssRuleType::Namespace, - CssRule::Viewport(_) => CssRuleType::Viewport, - CssRule::Supports(_) => CssRuleType::Supports, - CssRule::Page(_) => CssRuleType::Page, + CssRule::Viewport(_) => CssRuleType::Viewport, + CssRule::Supports(_) => CssRuleType::Supports, + CssRule::Page(_) => CssRuleType::Page, } } @@ -372,6 +376,7 @@ impl CssRule { CssRule::Namespace(_) | CssRule::Style(_) | CssRule::FontFace(_) | + CssRule::CounterStyle(_) | CssRule::Viewport(_) | CssRule::Keyframes(_) | CssRule::Page(_) => { @@ -445,6 +450,7 @@ impl ToCssWithGuard for CssRule { CssRule::Import(ref lock) => lock.read_with(guard).to_css(guard, dest), CssRule::Style(ref lock) => lock.read_with(guard).to_css(guard, dest), CssRule::FontFace(ref lock) => lock.read_with(guard).to_css(guard, dest), + CssRule::CounterStyle(ref lock) => lock.read_with(guard).to_css(guard, dest), CssRule::Viewport(ref lock) => lock.read_with(guard).to_css(guard, dest), CssRule::Keyframes(ref lock) => lock.read_with(guard).to_css(guard, dest), CssRule::Media(ref lock) => lock.read_with(guard).to_css(guard, dest), @@ -513,7 +519,7 @@ impl ToCssWithGuard for ImportRule { #[derive(Debug)] pub struct KeyframesRule { /// The name of the current animation. - pub name: Atom, + pub name: KeyframesName, /// The keyframes specified for this CSS rule. pub keyframes: Vec>>, /// Vendor prefix type the @keyframes has. @@ -525,7 +531,7 @@ impl ToCssWithGuard for KeyframesRule { fn to_css(&self, guard: &SharedRwLockReadGuard, dest: &mut W) -> fmt::Result where W: fmt::Write { try!(dest.write_str("@keyframes ")); - try!(dest.write_str(&*self.name.to_string())); + try!(self.name.to_css(dest)); try!(dest.write_str(" { ")); let iter = self.keyframes.iter(); let mut first = true; @@ -834,6 +840,7 @@ rule_filter! { effective_style_rules(Style => StyleRule), effective_media_rules(Media => MediaRule), effective_font_face_rules(FontFace => FontFaceRule), + effective_counter_style_rules(CounterStyle => CounterStyleRule), effective_viewport_rules(Viewport => ViewportRule), effective_keyframes_rules(Keyframes => KeyframesRule), effective_supports_rules(Supports => SupportsRule), @@ -915,6 +922,8 @@ pub enum VendorPrefix { enum AtRulePrelude { /// A @font-face rule prelude. FontFace, + /// A @counter-style rule prelude, with its counter style name. + CounterStyle(CustomIdent), /// A @media rule prelude, with its media queries. Media(Arc>), /// An @supports rule, with its conditional @@ -922,7 +931,7 @@ enum AtRulePrelude { /// A @viewport rule prelude. Viewport, /// A @keyframes rule, with its animation name and vendor prefix if exists. - Keyframes(Atom, Option), + Keyframes(KeyframesName, Option), /// A @page rule prelude. Page, } @@ -1102,6 +1111,20 @@ impl<'a, 'b> AtRuleParser for NestedRuleParser<'a, 'b> { "font-face" => { Ok(AtRuleType::WithBlock(AtRulePrelude::FontFace)) }, + "counter-style" => { + if !cfg!(feature = "gecko") { + // Support for this rule is not fully implemented in Servo yet. + return Err(()) + } + let name = parse_counter_style_name(input)?; + // ASCII-case-insensitive matches for "decimal" are already lower-cased + // by `parse_counter_style_name`, so we can use == here. + // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1359323 use atom!("decimal") + if name.0 == Atom::from("decimal") { + return Err(()) + } + Ok(AtRuleType::WithBlock(AtRulePrelude::CounterStyle(name))) + }, "viewport" => { if is_viewport_enabled() { Ok(AtRuleType::WithBlock(AtRulePrelude::Viewport)) @@ -1122,13 +1145,9 @@ impl<'a, 'b> AtRuleParser for NestedRuleParser<'a, 'b> { // Servo should not support @-moz-keyframes. return Err(()) } - let name = match input.next() { - Ok(Token::Ident(ref value)) if value != "none" => Atom::from(&**value), - Ok(Token::QuotedString(value)) => Atom::from(&*value), - _ => return Err(()) - }; + let name = KeyframesName::parse(self.context, input)?; - Ok(AtRuleType::WithBlock(AtRulePrelude::Keyframes(Atom::from(name), prefix))) + Ok(AtRuleType::WithBlock(AtRulePrelude::Keyframes(name, prefix))) }, "page" => { if cfg!(feature = "gecko") { @@ -1148,6 +1167,11 @@ impl<'a, 'b> AtRuleParser for NestedRuleParser<'a, 'b> { Ok(CssRule::FontFace(Arc::new(self.shared_lock.wrap( parse_font_face_block(&context, input).into())))) } + AtRulePrelude::CounterStyle(name) => { + let context = ParserContext::new_with_rule_type(self.context, Some(CssRuleType::CounterStyle)); + Ok(CssRule::CounterStyle(Arc::new(self.shared_lock.wrap( + parse_counter_style_body(name, &context, input)?)))) + } AtRulePrelude::Media(media_queries) => { Ok(CssRule::Media(Arc::new(self.shared_lock.wrap(MediaRule { media_queries: media_queries, diff --git a/servo/components/style/stylist.rs b/servo/components/style/stylist.rs index 38dbe24b4c17..8ae9998c4cb4 100644 --- a/servo/components/style/stylist.rs +++ b/servo/components/style/stylist.rs @@ -349,13 +349,13 @@ impl Stylist { // Don't let a prefixed keyframes animation override a non-prefixed one. let needs_insertion = keyframes_rule.vendor_prefix.is_none() || - self.animations.get(&keyframes_rule.name).map_or(true, |rule| + self.animations.get(keyframes_rule.name.as_atom()).map_or(true, |rule| rule.vendor_prefix.is_some()); if needs_insertion { let animation = KeyframesAnimation::from_keyframes( &keyframes_rule.keyframes, keyframes_rule.vendor_prefix.clone(), guard); debug!("Found valid keyframe animation: {:?}", animation); - self.animations.insert(keyframes_rule.name.clone(), animation); + self.animations.insert(keyframes_rule.name.as_atom().clone(), animation); } } CssRule::FontFace(ref rule) => { diff --git a/servo/components/style/values/mod.rs b/servo/components/style/values/mod.rs index 2b371b74da96..430eac274198 100644 --- a/servo/components/style/values/mod.rs +++ b/servo/components/style/values/mod.rs @@ -8,9 +8,13 @@ #![deny(missing_docs)] -pub use cssparser::{RGBA, Parser}; +use Atom; +pub use cssparser::{RGBA, Token, Parser, serialize_identifier, serialize_string}; use parser::{Parse, ParserContext}; +use std::ascii::AsciiExt; +use std::borrow::Cow; use std::fmt::{self, Debug}; +use std::hash; use style_traits::ToCss; macro_rules! define_numbered_css_keyword_enum { @@ -211,6 +215,93 @@ impl ToComputedValue for Either { } } +/// https://drafts.csswg.org/css-values-4/#custom-idents +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "servo", derive(HeapSizeOf))] +pub struct CustomIdent(pub Atom); + +impl CustomIdent { + /// Parse an already-tokenizer identifier + pub fn from_ident(ident: Cow, excluding: &[&str]) -> Result { + match_ignore_ascii_case! { &ident, + "initial" | "inherit" | "unset" | "default" => return Err(()), + _ => {} + }; + if excluding.iter().any(|s| ident.eq_ignore_ascii_case(s)) { + Err(()) + } else { + Ok(CustomIdent(ident.into())) + } + } +} + +impl ToCss for CustomIdent { + fn to_css(&self, dest: &mut W) -> fmt::Result where W: fmt::Write { + serialize_identifier(&self.0.to_string(), dest) + } +} + +/// https://drafts.csswg.org/css-animations/#typedef-keyframes-name +#[derive(Debug, Clone)] +#[cfg_attr(feature = "servo", derive(HeapSizeOf))] +pub enum KeyframesName { + /// + Ident(CustomIdent), + /// + QuotedString(Atom), +} + +impl KeyframesName { + /// https://drafts.csswg.org/css-animations/#dom-csskeyframesrule-name + pub fn from_ident(value: String) -> Self { + match CustomIdent::from_ident((&*value).into(), &["none"]) { + Ok(ident) => KeyframesName::Ident(ident), + Err(()) => KeyframesName::QuotedString(value.into()), + } + } + + /// The name as an Atom + pub fn as_atom(&self) -> &Atom { + match *self { + KeyframesName::Ident(ref ident) => &ident.0, + KeyframesName::QuotedString(ref atom) => atom, + } + } +} + +impl Eq for KeyframesName {} + +impl PartialEq for KeyframesName { + fn eq(&self, other: &Self) -> bool { + self.as_atom() == other.as_atom() + } +} + +impl hash::Hash for KeyframesName { + fn hash(&self, state: &mut H) where H: hash::Hasher { + self.as_atom().hash(state) + } +} + +impl Parse for KeyframesName { + fn parse(_context: &ParserContext, input: &mut Parser) -> Result { + match input.next() { + Ok(Token::Ident(s)) => Ok(KeyframesName::Ident(CustomIdent::from_ident(s, &["none"])?)), + Ok(Token::QuotedString(s)) => Ok(KeyframesName::QuotedString(s.into())), + _ => Err(()) + } + } +} + +impl ToCss for KeyframesName { + fn to_css(&self, dest: &mut W) -> fmt::Result where W: fmt::Write { + match *self { + KeyframesName::Ident(ref ident) => ident.to_css(dest), + KeyframesName::QuotedString(ref atom) => serialize_string(&atom.to_string(), dest), + } + } +} + // A type for possible values for min- and max- flavors of width, // height, block-size, and inline-size. define_css_keyword_enum!(ExtremumLength: diff --git a/servo/tests/unit/style/lib.rs b/servo/tests/unit/style/lib.rs index f6a39d1a656a..baddd4da618c 100644 --- a/servo/tests/unit/style/lib.rs +++ b/servo/tests/unit/style/lib.rs @@ -13,7 +13,7 @@ extern crate parking_lot; extern crate rayon; extern crate rustc_serialize; extern crate selectors; -#[macro_use] extern crate servo_atoms; +extern crate servo_atoms; extern crate servo_config; extern crate servo_url; extern crate style; diff --git a/servo/tests/unit/style/parsing/animation.rs b/servo/tests/unit/style/parsing/animation.rs index 3db81be64844..37cb3c5037b8 100644 --- a/servo/tests/unit/style/parsing/animation.rs +++ b/servo/tests/unit/style/parsing/animation.rs @@ -7,6 +7,7 @@ use servo_atoms::Atom; use style::parser::Parse; use style::properties::longhands::animation_iteration_count::single_value::computed_value::T as AnimationIterationCount; use style::properties::longhands::animation_name; +use style::values::{KeyframesName, CustomIdent}; use style_traits::ToCss; #[test] @@ -14,13 +15,13 @@ fn test_animation_name() { use self::animation_name::single_value::SpecifiedValue as SingleValue; let other_name = Atom::from("other-name"); assert_eq!(parse_longhand!(animation_name, "none"), - animation_name::SpecifiedValue(vec![SingleValue(atom!(""))])); + animation_name::SpecifiedValue(vec![SingleValue(None)])); assert_eq!(parse_longhand!(animation_name, "other-name, none, 'other-name', \"other-name\""), animation_name::SpecifiedValue( - vec![SingleValue(other_name.clone()), - SingleValue(atom!("")), - SingleValue(other_name.clone()), - SingleValue(other_name.clone())])); + vec![SingleValue(Some(KeyframesName::Ident(CustomIdent(other_name.clone())))), + SingleValue(None), + SingleValue(Some(KeyframesName::QuotedString(other_name.clone()))), + SingleValue(Some(KeyframesName::QuotedString(other_name.clone())))])); } #[test] diff --git a/servo/tests/unit/style/properties/serialization.rs b/servo/tests/unit/style/properties/serialization.rs index aeb07f874849..fea8d3f416eb 100644 --- a/servo/tests/unit/style/properties/serialization.rs +++ b/servo/tests/unit/style/properties/serialization.rs @@ -8,6 +8,7 @@ use style::properties::{PropertyDeclaration, Importance, PropertyId}; use style::properties::longhands::outline_color::computed_value::T as ComputedColor; use style::properties::parse_property_declaration_list; use style::values::{RGBA, Auto}; +use style::values::CustomIdent; use style::values::specified::{BorderStyle, BorderWidth, CSSColor, Length, NoCalcLength}; use style::values::specified::{LengthOrPercentage, LengthOrPercentageOrAuto, LengthOrPercentageOrAutoOrContent}; use style::values::specified::url::SpecifiedUrl; @@ -1259,8 +1260,8 @@ mod shorthand_serialization { fn counter_increment_with_properties_should_serialize_correctly() { let mut properties = Vec::new(); - properties.push(("counter1".to_owned(), Integer::new(1))); - properties.push(("counter2".to_owned(), Integer::new(-4))); + properties.push((CustomIdent("counter1".into()), Integer::new(1))); + properties.push((CustomIdent("counter2".into()), Integer::new(-4))); let counter_increment = CounterIncrement(properties); let counter_increment_css = "counter1 1 counter2 -4"; diff --git a/servo/tests/unit/style/stylesheets.rs b/servo/tests/unit/style/stylesheets.rs index 8005139e6767..dd1762ee686b 100644 --- a/servo/tests/unit/style/stylesheets.rs +++ b/servo/tests/unit/style/stylesheets.rs @@ -23,6 +23,7 @@ use style::properties::longhands::animation_play_state; use style::shared_lock::SharedRwLock; use style::stylesheets::{Origin, Namespaces}; use style::stylesheets::{Stylesheet, NamespaceRule, CssRule, CssRules, StyleRule, KeyframesRule}; +use style::values::{KeyframesName, CustomIdent}; use style::values::specified::{LengthOrPercentageOrAuto, Percentage}; pub fn block_from(iterable: I) -> PropertyDeclarationBlock @@ -221,7 +222,7 @@ fn test_parse_stylesheet() { ]))), }))), CssRule::Keyframes(Arc::new(stylesheet.shared_lock.wrap(KeyframesRule { - name: "foo".into(), + name: KeyframesName::Ident(CustomIdent("foo".into())), keyframes: vec![ Arc::new(stylesheet.shared_lock.wrap(Keyframe { selector: KeyframeSelector::new_for_unit_testing( From c351b765ffc5c56364f047e531497183d80deb5a Mon Sep 17 00:00:00 2001 From: JW Wang Date: Fri, 21 Apr 2017 14:29:25 +0800 Subject: [PATCH 13/68] Bug 1358399. P1 - move "elem.crossOrigin = test.crossOrigin || false" from SetupEME() to LoadTest() to improve cohesion. r=gerald The attributes are used by MaybeCrossOriginURI() which is called by LoadTest() indirectly. MozReview-Commit-ID: LH2STpONuCE --HG-- extra : rebase_source : 5762de80943d30064df0d4a69ebe7d36a12f308b extra : intermediate-source : 73e455a974c9bc3609b72d3ffbbcbc6f1077f62b extra : source : 7802185d9bcaec4f7377de94e4876d995a8ab019 --- dom/media/test/eme.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dom/media/test/eme.js b/dom/media/test/eme.js index e3438d0bf4da..5067f4028810 100644 --- a/dom/media/test/eme.js +++ b/dom/media/test/eme.js @@ -239,6 +239,7 @@ function LoadTest(test, elem, token, loadParams) var ms = new MediaSource(); elem.src = URL.createObjectURL(ms); + elem.crossOrigin = test.crossOrigin || false; return new Promise(function (resolve, reject) { ms.addEventListener("sourceopen", function () { @@ -282,7 +283,6 @@ function EMEPromiseAll(v, token, promises) { function SetupEME(test, token, params) { var v = document.createElement("video"); - v.crossOrigin = test.crossOrigin || false; v.sessions = []; v.closeSessions = function() { From 4054b9864bd5850258fa1c33c6dd8b427719c9d0 Mon Sep 17 00:00:00 2001 From: JW Wang Date: Fri, 21 Apr 2017 17:05:18 +0800 Subject: [PATCH 14/68] Bug 1358399. P2 - split SetupEME() into small functions which will be useful in next patches. r=gerald MozReview-Commit-ID: 7IZUYZaSlIr --HG-- extra : rebase_source : 57a4556b4ce0bb6e5f2fd04b812de4613bd4c31a extra : intermediate-source : 5a336b709abeeaace6167b52d7d8c67c0c27218b extra : source : f4e04d207b81793218d231cbec4aa8d4a0101318 --- dom/media/test/eme.js | 131 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) diff --git a/dom/media/test/eme.js b/dom/media/test/eme.js index 5067f4028810..ee3dd9ca3322 100644 --- a/dom/media/test/eme.js +++ b/dom/media/test/eme.js @@ -280,6 +280,137 @@ function EMEPromiseAll(v, token, promises) { }); } +/* + * Create a new MediaKeys object. + * Return a promise which will be resolved with a new MediaKeys object, + * or will be rejected with a string that describes the failure. + */ +function CreateMediaKeys(v, test, token) { + let p = new EMEPromise; + + function streamType(type) { + var x = test.tracks.find(o => o.name == type); + return x ? x.type : undefined; + } + + function onencrypted(ev) { + var options = { initDataTypes: [ev.initDataType] }; + if (streamType("video")) { + options.videoCapabilities = [{contentType: streamType("video")}]; + } + if (streamType("audio")) { + options.audioCapabilities = [{contentType: streamType("audio")}]; + } + navigator.requestMediaKeySystemAccess(CLEARKEY_KEYSYSTEM, [options]) + .then(keySystemAccess => { + keySystemAccess.createMediaKeys().then( + p.resolve, + () => p.reject(`${token} Failed to create MediaKeys object.`) + ); + }, () => p.reject(`${token} Failed to request key system access.`)); + } + + v.addEventListener("encrypted", onencrypted, {once: true}); + return p.promise; +} + +/* + * Create a new MediaKeys object and provide it to the media element. + * Return a promise which will be resolved if succeeded, or will be rejected + * with a string that describes the failure. + */ +function CreateAndSetMediaKeys(v, test, token) { + let p = new EMEPromise; + + CreateMediaKeys(v, test, token).then(mediaKeys => { + v.setMediaKeys(mediaKeys).then( + p.resolve, + () => p.reject(`${token} Failed to set MediaKeys on I'm a normal link +Back in browser history +Forward in browser history I go to an anchor I open a window with javascript Click me diff --git a/testing/marionette/interaction.js b/testing/marionette/interaction.js index dbad95631dc8..a691576b496a 100644 --- a/testing/marionette/interaction.js +++ b/testing/marionette/interaction.js @@ -178,8 +178,8 @@ function* webdriverClickElement (el, a11y) { yield interaction.flushEventLoop(win); // step 10 - // TODO(ato): if the click causes navigation, - // run post-navigation checks + // if the click causes navigation, the post-navigation checks are + // handled by the load listener in listener.js } function* chromeClick (el, a11y) { @@ -291,20 +291,19 @@ interaction.selectOption = function (el) { * |win| has closed or been unloaded before the queue can be flushed. */ interaction.flushEventLoop = function* (win) { - let unloadEv; + return new Promise(resolve => { + let handleEvent = event => { + win.removeEventListener("beforeunload", this); + resolve(); + }; - return new Promise((resolve, reject) => { if (win.closed) { - reject(); + resolve(); return; } - unloadEv = reject; - win.addEventListener("unload", unloadEv, {once: true}); - - win.requestAnimationFrame(resolve); - }).then(() => { - win.removeEventListener("unload", unloadEv); + win.addEventListener("beforeunload", handleEvent); + win.requestAnimationFrame(handleEvent); }); }; diff --git a/testing/marionette/listener.js b/testing/marionette/listener.js index f85604944e48..a4e907b2edb9 100644 --- a/testing/marionette/listener.js +++ b/testing/marionette/listener.js @@ -117,8 +117,10 @@ var sandboxName = "default"; */ var loadListener = { command_id: null, + seenUnload: null, timeout: null, - timer: null, + timerPageLoad: null, + timerPageUnload: null, /** * Start listening for page unload/load events. @@ -136,48 +138,81 @@ var loadListener = { this.command_id = command_id; this.timeout = timeout; + this.seenUnload = false; + + this.timerPageLoad = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + this.timerPageUnload = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + // In case of a remoteness change, only wait the remaining time timeout = startTime + timeout - new Date().getTime(); if (timeout <= 0) { - this.notify(); + this.notify(this.timerPageLoad); return; } - this.timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); - this.timer.initWithCallback(this, timeout, Ci.nsITimer.TYPE_ONE_SHOT); - if (waitForUnloaded) { addEventListener("hashchange", this, false); addEventListener("pagehide", this, false); + + // The event can only be received if the listener gets added to the + // currently selected frame. + curContainer.frame.addEventListener("beforeunload", this, false); + + Services.obs.addObserver(this, "outer-window-destroyed"); } else { addEventListener("DOMContentLoaded", loadListener, false); addEventListener("pageshow", loadListener, false); } + + this.timerPageLoad.initWithCallback(this, timeout, Ci.nsITimer.TYPE_ONE_SHOT); }, /** * Stop listening for page unload/load events. */ stop: function () { - if (this.timer) { - this.timer.cancel(); - this.timer = null; + if (this.timerPageLoad) { + this.timerPageLoad.cancel(); + } + + if (this.timerPageUnload) { + this.timerPageUnload.cancel(); } removeEventListener("hashchange", this); removeEventListener("pagehide", this); removeEventListener("DOMContentLoaded", this); removeEventListener("pageshow", this); + + // If the original content window, where the navigation was triggered, + // doesn't exist anymore, exceptions can be silently ignored. + try { + curContainer.frame.removeEventListener("beforeunload", this); + } catch (e if e.name == "TypeError") {} + + // In the case when the observer was added before a remoteness change, + // it will no longer be available. Exceptions can be silently ignored. + try { + Services.obs.removeObserver(this, "outer-window-destroyed"); + } catch (e) {} }, /** * Callback for registered DOM events. */ handleEvent: function (event) { + logger.debug(`Received DOM event "${event.type}" for "${event.originalTarget.baseURI}"`); + switch (event.type) { + case "beforeunload": + this.seenUnload = true; + break; + case "pagehide": if (event.originalTarget === curContainer.frame.document) { + this.seenUnload = true; + removeEventListener("hashchange", this); removeEventListener("pagehide", this); @@ -223,9 +258,43 @@ var loadListener = { * Callback for navigation timeout timer. */ notify: function (timer) { - this.stop(); - sendError(new TimeoutError("Timeout loading page after " + this.timeout + "ms"), - this.command_id); + switch (timer) { + // If the page unload timer is raised, ensure to properly stop the load + // listener, and return from the currently active command. + case this.timerPageUnload: + if (!this.seenUnload) { + logger.debug("Canceled page load listener because no navigation " + + "has been detected"); + this.stop(); + sendOk(this.command_id); + } + break; + + case this.timerPageLoad: + this.stop(); + sendError(new TimeoutError(`Timeout loading page after ${this.timeout}ms`), + this.command_id); + break; + } + }, + + observe: function (subject, topic, data) { + const winID = subject.QueryInterface(Components.interfaces.nsISupportsPRUint64).data; + const curWinID = curContainer.frame.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils).outerWindowID; + + logger.debug(`Received observer notification "${topic}" for "${winID}"`); + + switch (topic) { + // In the case when the currently selected frame is about to close, + // there will be no further load events. Stop listening immediately. + case "outer-window-destroyed": + if (curWinID === winID) { + this.stop(); + sendOk(this.command_id); + } + break; + } }, /** @@ -251,23 +320,15 @@ var loadListener = { * @param {number} command_id * ID of the currently handled message between the driver and listener. * @param {number} pageTimeout - * Timeout in milliseconds the method has to wait for the page being finished loading. + * Timeout in milliseconds the method has to wait for the page finished loading. + * @param {boolean=} loadEventExpected + * Optional flag, which indicates that navigate has to wait for the page + * finished loading. * @param {string=} url * Optional URL, which is used to check if a page load is expected. */ - navigate: function (trigger, command_id, timeout, url = undefined) { - let loadEventExpected = true; - - if (typeof url == "string") { - try { - let requestedURL = new URL(url).toString(); - loadEventExpected = navigate.isLoadEventExpected(requestedURL); - } catch (e) { - sendError(new InvalidArgumentError("Malformed URL: " + e.message), command_id); - return; - } - } - + navigate: function (trigger, command_id, timeout, loadEventExpected = true, + useUnloadTimer = false) { if (loadEventExpected) { let startTime = new Date().getTime(); this.start(command_id, timeout, startTime, true); @@ -277,8 +338,14 @@ var loadListener = { yield trigger(); }).then(val => { - if (!loadEventExpected) { + if (!loadEventExpected) { sendOk(command_id); + return; + } + + // If requested setup a timer to detect a possible page load + if (useUnloadTimer) { + this.timerPageUnload.initWithCallback(this, 200, Ci.nsITimer.TYPE_ONE_SHOT); } }).catch(err => { @@ -286,7 +353,6 @@ var loadListener = { this.stop(); } - // Check why we do not raise an error if err is of type Event sendError(err, command_id); return; }); @@ -402,7 +468,6 @@ function removeMessageListenerId(messageName, handler) { var getTitleFn = dispatch(getTitle); var getPageSourceFn = dispatch(getPageSource); var getActiveElementFn = dispatch(getActiveElement); -var clickElementFn = dispatch(clickElement); var getElementAttributeFn = dispatch(getElementAttribute); var getElementPropertyFn = dispatch(getElementProperty); var getElementTextFn = dispatch(getElementText); @@ -457,7 +522,7 @@ function startListeners() { addMessageListenerId("Marionette:findElementContent", findElementContentFn); addMessageListenerId("Marionette:findElementsContent", findElementsContentFn); addMessageListenerId("Marionette:getActiveElement", getActiveElementFn); - addMessageListenerId("Marionette:clickElement", clickElementFn); + addMessageListenerId("Marionette:clickElement", clickElement); addMessageListenerId("Marionette:getElementAttribute", getElementAttributeFn); addMessageListenerId("Marionette:getElementProperty", getElementPropertyFn); addMessageListenerId("Marionette:getElementText", getElementTextFn); @@ -562,7 +627,7 @@ function deleteSession(msg) { removeMessageListenerId("Marionette:findElementContent", findElementContentFn); removeMessageListenerId("Marionette:findElementsContent", findElementsContentFn); removeMessageListenerId("Marionette:getActiveElement", getActiveElementFn); - removeMessageListenerId("Marionette:clickElement", clickElementFn); + removeMessageListenerId("Marionette:clickElement", clickElement); removeMessageListenerId("Marionette:getElementAttribute", getElementAttributeFn); removeMessageListenerId("Marionette:getElementProperty", getElementPropertyFn); removeMessageListenerId("Marionette:getElementText", getElementTextFn); @@ -1101,15 +1166,26 @@ function waitForPageLoaded(msg) { */ function get(msg) { let {command_id, pageTimeout, url} = msg.json; + let loadEventExpected = true; try { + if (typeof url == "string") { + try { + let requestedURL = new URL(url).toString(); + loadEventExpected = navigate.isLoadEventExpected(requestedURL); + } catch (e) { + sendError(new InvalidArgumentError("Malformed URL: " + e.message), command_id); + return; + } + } + // We need to move to the top frame before navigating sendSyncMessage("Marionette:switchedToFrame", {frameValue: null}); curContainer.frame = content; loadListener.navigate(() => { curContainer.frame.location = url; - }, command_id, pageTimeout, url); + }, command_id, pageTimeout, loadEventExpected); } catch (e) { sendError(e, command_id); @@ -1255,15 +1331,36 @@ function getActiveElement() { /** * Send click event to element. * + * @param {number} command_id + * ID of the currently handled message between the driver and listener. * @param {WebElement} id * Reference to the web element to click. + * @param {number} pageTimeout + * Timeout in milliseconds the method has to wait for the page being finished loading. */ -function clickElement(id) { - let el = seenEls.get(id, curContainer); - return interaction.clickElement( - el, - capabilities.get("moz:accessibilityChecks"), - capabilities.get("specificationLevel") >= 1); +function clickElement(msg) { + let {command_id, id, pageTimeout} = msg.json; + + try { + let loadEventExpected = true; + + let target = getElementAttribute(id, "target"); + + if (target === "_blank") { + loadEventExpected = false; + } + + loadListener.navigate(() => { + return interaction.clickElement( + seenEls.get(id, curContainer), + capabilities.get("moz:accessibilityChecks"), + capabilities.get("specificationLevel") >= 1 + ); + }, command_id, pageTimeout, loadEventExpected, true); + + } catch (e) { + sendError(e, command_id); + } } function getElementAttribute(id, name) { From 055f41ca82a992fc1b516d11a32b3e8afedf3121 Mon Sep 17 00:00:00 2001 From: Sebastian Hengst Date: Wed, 26 Apr 2017 12:42:08 +0200 Subject: [PATCH 34/68] Backed out changeset 44800134e7db (bug 1358013) for failing browser-chrome's browser_misused_characters_in_strings.js. r=backout --- .../locales/en-US/netmonitor.properties | 23 ------------ .../netmonitor/src/components/status-bar.js | 36 ++++++++----------- 2 files changed, 15 insertions(+), 44 deletions(-) diff --git a/devtools/client/locales/en-US/netmonitor.properties b/devtools/client/locales/en-US/netmonitor.properties index e74d80f1a748..bba6779aee85 100644 --- a/devtools/client/locales/en-US/netmonitor.properties +++ b/devtools/client/locales/en-US/netmonitor.properties @@ -143,18 +143,6 @@ networkMenu.sortedAsc=Sorted ascending # in the network table toolbar, for any column that is sorted descending. networkMenu.sortedDesc=Sorted descending -# LOCALIZATION NOTE (networkMenu.summary.tooltip.perf): A tooltip explaining -# what the perf button does -networkMenu.summary.tooltip.perf=Start performance analysis - -# LOCALIZATION NOTE (networkMenu.summary.tooltip.domContentLoaded): A tooltip explaining -# what the DOMContentLoaded label displays -networkMenu.summary.tooltip.domContentLoaded=Time when 'DOMContentLoad' event occurred - -# LOCALIZATION NOTE (networkMenu.summary.tooltip.load): A tooltip explaining -# what the load label displays -networkMenu.summary.tooltip.load=Time when 'load' event occurred - # LOCALIZATION NOTE (networkMenu.summary.requestsCount): This label is displayed # in the network table footer providing the number of requests # See: http://developer.mozilla.org/en/docs/Localization_and_Plurals @@ -164,25 +152,14 @@ networkMenu.summary.requestsCount=One request;%S requests # in the network table footer when there are no requests networkMenu.summary.requestsCountEmpty=No requests -# LOCALIZATION NOTE (networkMenu.summary.tooltip.requestsCount): A tooltip explaining -# what the requestsCount label displays -networkMenu.summary.tooltip.requestsCount=Number of requests - # LOCALIZATION NOTE (networkMenu.summary.transferred): This label is displayed # in the network table footer providing the transferred size. networkMenu.summary.transferred=%S / %S transferred -# LOCALIZATION NOTE (networkMenu.summary.tooltip.transferred): A tooltip explaining -# what the transferred label displays -networkMenu.summary.tooltip.transferred=Size/transferred size of all requests - # LOCALIZATION NOTE (networkMenu.summary.finish): This label is displayed # in the network table footer providing the transfer time. networkMenu.summary.finish=Finish: %S -# LOCALIZATION NOTE (networkMenu.summary.tooltip.finish): A tooltip explaining -# what the finish label displays -networkMenu.summary.tooltip.finish=Total time needed to load all requests # LOCALIZATION NOTE (networkMenu.sizeB): This is the label displayed # in the network menu specifying the size of a request (in bytes). diff --git a/devtools/client/netmonitor/src/components/status-bar.js b/devtools/client/netmonitor/src/components/status-bar.js index bad6869a2b12..b128de9aa91d 100644 --- a/devtools/client/netmonitor/src/components/status-bar.js +++ b/devtools/client/netmonitor/src/components/status-bar.js @@ -43,35 +43,29 @@ function StatusBar({ summary, openStatistics, timingMarkers }) { div({ className: "devtools-toolbar devtools-status-bottom" }, button({ className: "devtools-button requests-list-network-summary-button", - title: L10N.getStr("networkMenu.summary.tooltip.perf"), onClick: openStatistics, }, div({ className: "summary-info-icon" }), ), - div({ - className: "status-bar-label requests-list-network-summary-count", - title: L10N.getStr("networkMenu.summary.tooltip.requestsCount"), - }, countText), + div({ className: "status-bar-label requests-list-network-summary-count" }, + countText + ), count !== 0 && - div({ - className: "status-bar-label requests-list-network-summary-transfer", - title: L10N.getStr("networkMenu.summary.tooltip.transferred"), - }, transferText), + div({ className: "status-bar-label requests-list-network-summary-transfer" }, + transferText + ), count !== 0 && - div({ - className: "status-bar-label requests-list-network-summary-finish", - title: L10N.getStr("networkMenu.summary.tooltip.finish"), - }, finishText), + div({ className: "status-bar-label requests-list-network-summary-finish" }, + finishText + ), DOMContentLoaded > -1 && - div({ - className: "status-bar-label dom-content-loaded", - title: L10N.getStr("networkMenu.summary.tooltip.domContentLoaded"), - }, `DOMContentLoaded: ${getFormattedTime(DOMContentLoaded)}`), + div({ className: "status-bar-label dom-content-loaded" }, + `DOMContentLoaded: ${getFormattedTime(DOMContentLoaded)}` + ), load > -1 && - div({ - className: "status-bar-label load", - title: L10N.getStr("networkMenu.summary.tooltip.load"), - }, `load: ${getFormattedTime(load)}`), + div({ className: "status-bar-label load" }, + `load: ${getFormattedTime(load)}` + ), ) ); } From 99eb2c1bc7f108dc24ee5783ac8babdb8a7fdd09 Mon Sep 17 00:00:00 2001 From: Aryeh Gregor Date: Tue, 25 Apr 2017 15:49:27 +0300 Subject: [PATCH 35/68] Bug 1359387 - Support Selection.empty()/setPosition() r=masayuki,smaug Blink, WebKit, and Edge already support these, and they're in the spec. Tests submitted to wpt upstream. MozReview-Commit-ID: 5NFBeClNN7y --HG-- extra : rebase_source : ea073639904e1ae9449990827ad32626aa6267d9 --- dom/webidl/Selection.webidl | 6 ++++-- .../meta/selection/interfaces.html.ini | 15 --------------- 2 files changed, 4 insertions(+), 17 deletions(-) diff --git a/dom/webidl/Selection.webidl b/dom/webidl/Selection.webidl index f85f4c39b76d..4acd78555323 100644 --- a/dom/webidl/Selection.webidl +++ b/dom/webidl/Selection.webidl @@ -26,10 +26,12 @@ interface Selection { void removeRange(Range range); [Throws] void removeAllRanges(); - //void empty(); + [Throws, BinaryName="RemoveAllRanges"] + void empty(); [Throws, BinaryName="collapseJS"] void collapse(Node? node, optional unsigned long offset = 0); - //void setPosition(Node? node, optional unsigned long offset = 0); + [Throws, BinaryName="collapseJS"] + void setPosition(Node? node, optional unsigned long offset = 0); [Throws, BinaryName="collapseToStartJS"] void collapseToStart(); [Throws, BinaryName="collapseToEndJS"] diff --git a/testing/web-platform/meta/selection/interfaces.html.ini b/testing/web-platform/meta/selection/interfaces.html.ini index 9fc6a2b564ee..6d8dce9b3580 100644 --- a/testing/web-platform/meta/selection/interfaces.html.ini +++ b/testing/web-platform/meta/selection/interfaces.html.ini @@ -3,21 +3,6 @@ [Selection interface: attribute type] expected: FAIL - [Selection interface: operation empty()] - expected: FAIL - - [Selection interface: operation setPosition(Node,unsigned long)] - expected: FAIL - [Selection interface: getSelection() must inherit property "type" with the proper type (6)] expected: FAIL - [Selection interface: getSelection() must inherit property "empty" with the proper type (11)] - expected: FAIL - - [Selection interface: getSelection() must inherit property "setPosition" with the proper type (13)] - expected: FAIL - - [Selection interface: calling setPosition(Node,unsigned long) on getSelection() with too few arguments must throw TypeError] - expected: FAIL - From d5097e1bdadd0668b7d37592a0c7394fcfad6dbb Mon Sep 17 00:00:00 2001 From: Brian Birtles Date: Wed, 26 Apr 2017 14:31:23 +0900 Subject: [PATCH 36/68] Bug 1359658 - Crashtest for clearing animation-only dirty descendants bit on display:none descendants; r=hiro I have verified that this crashtest fails without the corresponding Servo-side code changes. MozReview-Commit-ID: 9obKluN3fgv --HG-- extra : rebase_source : 71d82eb24ccb9fea1674d08be9b317997df7fe3f --- dom/animation/test/crashtests/1359658-1.html | 33 +++++++++++++++++++ dom/animation/test/crashtests/crashtests.list | 1 + 2 files changed, 34 insertions(+) create mode 100644 dom/animation/test/crashtests/1359658-1.html diff --git a/dom/animation/test/crashtests/1359658-1.html b/dom/animation/test/crashtests/1359658-1.html new file mode 100644 index 000000000000..501c03e4d34a --- /dev/null +++ b/dom/animation/test/crashtests/1359658-1.html @@ -0,0 +1,33 @@ + + + + + Bug 1359658: Animation-only dirty descendants bit should be cleared + for display:none content + + +
+ + + +
+ + + diff --git a/dom/animation/test/crashtests/crashtests.list b/dom/animation/test/crashtests/crashtests.list index 8bae3b35504b..d13f6862e1d1 100644 --- a/dom/animation/test/crashtests/crashtests.list +++ b/dom/animation/test/crashtests/crashtests.list @@ -26,3 +26,4 @@ pref(dom.animations-api.core.enabled,true) load 1333539-1.html pref(dom.animations-api.core.enabled,true) load 1333539-2.html pref(dom.animations-api.core.enabled,true) load 1333418-1.html pref(dom.animations-api.core.enabled,true) load 1343589-1.html +pref(dom.animations-api.core.enabled,true) load 1359658-1.html From f246a81c6dfc18b3c72a23d335480e9caaad8f98 Mon Sep 17 00:00:00 2001 From: Andreas Pehrson Date: Tue, 28 Mar 2017 16:03:21 +0200 Subject: [PATCH 37/68] Bug 1320994 - Re-enable tests. r=florian MozReview-Commit-ID: KGe95JhBoVD --HG-- extra : rebase_source : 23dfba0783dcba9b89955724e62c7c4344262c82 --- browser/base/content/test/webrtc/browser.ini | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/browser/base/content/test/webrtc/browser.ini b/browser/base/content/test/webrtc/browser.ini index c4053f8a69d3..bbb9187e8bca 100644 --- a/browser/base/content/test/webrtc/browser.ini +++ b/browser/base/content/test/webrtc/browser.ini @@ -12,10 +12,9 @@ skip-if = (os == "linux" && debug) # linux: bug 976544 [browser_devices_get_user_media_multi_process.js] skip-if = e10s && (asan || debug) # bug 1347625 [browser_devices_get_user_media_screen.js] -skip-if = (os == "linux") || (os == "win" && !debug) # bug 1320994 for linux opt, bug 1338038 for windows and linux debug [browser_devices_get_user_media_tear_off_tab.js] [browser_devices_get_user_media_unprompted_access.js] [browser_devices_get_user_media_unprompted_access_in_frame.js] [browser_devices_get_user_media_unprompted_access_tear_off_tab.js] -skip-if = (os == "linux") || (os == "win" && bits == 64) # linux: bug 1331616, win8: bug 1334752 +skip-if = (os == "win" && bits == 64) # win8: bug 1334752 [browser_webrtc_hooks.js] From edff555cf51b6fcad522500dd72c128f35a98f89 Mon Sep 17 00:00:00 2001 From: Andreas Pehrson Date: Wed, 26 Apr 2017 11:13:40 +0200 Subject: [PATCH 38/68] Bug 1320994 - Refactor MediaManager's window management. r=jib This in large does the following: * Changes the model in MediaManager to align with that of chrome code, namely: - One GetUserMediaWindowListener *per window*, containing N SourceListeners for N gUM requests, and - GetUserMediaWindowListener replaces GetUserMediaStreamListener - So if two SourceListeners stop we can still send only one event * Breaks a special event specific to B2G chrome MozReview-Commit-ID: 3wbPFmc9yWj --HG-- extra : rebase_source : 4ad324f6bb1be637da584f323a3e039c5b4f664d --- dom/media/MediaManager.cpp | 1849 ++++++++++++++++++++---------------- dom/media/MediaManager.h | 60 +- 2 files changed, 1045 insertions(+), 864 deletions(-) diff --git a/dom/media/MediaManager.cpp b/dom/media/MediaManager.cpp index ddf2de3bb087..c3c424edafb0 100644 --- a/dom/media/MediaManager.cpp +++ b/dom/media/MediaManager.cpp @@ -157,6 +157,8 @@ using media::Refcountable; static Atomic sInShutdown; +typedef media::Pledge PledgeVoid; + static bool HostIsHttps(nsIURI &docURI) { @@ -168,251 +170,117 @@ HostIsHttps(nsIURI &docURI) return isHttps; } -/** - * This class is an implementation of MediaStreamListener. This is used - * to Start() and Stop() the underlying MediaEngineSource when MediaStreams - * are assigned and deassigned in content. - */ -class GetUserMediaCallbackMediaStreamListener : public MediaStreamListener -{ - friend MediaManager; +class SourceListener : public MediaStreamListener { public: - // Create in an inactive state - GetUserMediaCallbackMediaStreamListener(base::Thread *aThread, - uint64_t aWindowID, - const PrincipalHandle& aPrincipalHandle) - : mMediaThread(aThread) - , mMainThreadCheck(nullptr) - , mWindowID(aWindowID) - , mPrincipalHandle(aPrincipalHandle) - , mStopped(false) - , mFinished(false) - , mRemoved(false) - , mAudioStopped(false) - , mAudioStopPending(false) - , mVideoStopped(false) - , mVideoStopPending(false) - , mChromeNotificationTaskPosted(false) - {} + SourceListener(); - ~GetUserMediaCallbackMediaStreamListener() - { - Unused << mMediaThread; - // It's OK to release mStream on any thread; they have thread-safe - // refcounts. - } + /** + * Registers this source listener as belonging to the given window listener. + */ + void Register(GetUserMediaWindowListener* aListener); - void Activate(already_AddRefed aStream, + /** + * Marks this listener as active and adds itself as a listener to aStream. + */ + void Activate(SourceMediaStream* aStream, AudioDevice* aAudioDevice, - VideoDevice* aVideoDevice) - { - MOZ_ASSERT(NS_IsMainThread()); - mMainThreadCheck = PR_GetCurrentThread(); - mStream = aStream; - mAudioDevice = aAudioDevice; - mVideoDevice = aVideoDevice; + VideoDevice* aVideoDevice); - mStream->AddListener(this); - } + /** + * Stops all live tracks, finishes the associated MediaStream and cleans up. + */ + void Stop(); - MediaStream *Stream() // Can be used to test if Activate was called + /** + * Removes this SourceListener from its associated MediaStream and marks it + * removed. Also removes the weak reference to the associated window listener. + */ + void Remove(); + + /** + * Posts a task to stop the device associated with aTrackID and notifies the + * associated window listener that a track was stopped. + * Should this track be the last live one to be stopped, we'll also clean up. + */ + void StopTrack(TrackID aTrackID); + + /** + * Stops all screen/app/window/audioCapture sharing, but not camera or + * microphone. + */ + void StopSharing(); + + MediaStream* Stream() const { return mStream; } - SourceMediaStream *GetSourceStream() + + SourceMediaStream* GetSourceStream(); + + AudioDevice* GetAudioDevice() const { - NS_ASSERTION(mStream,"Getting stream from never-activated GUMCMSListener"); - if (!mStream) { - return nullptr; - } - return mStream->AsSourceStream(); + return mAudioDevice; } - void StopSharing(); + VideoDevice* GetVideoDevice() const + { + return mVideoDevice; + } - void StopTrack(TrackID aID); + void GetSettings(dom::MediaTrackSettings& aOutSettings, TrackID aTrackID); - void NotifyChromeOfTrackStops(); + void NotifyPull(MediaStreamGraph* aGraph, + StreamTime aDesiredTime) override; - typedef media::Pledge PledgeVoid; + void NotifyEvent(MediaStreamGraph* aGraph, + MediaStreamGraphEvent aEvent) override; + + void NotifyFinished(); + + /** + * this can be in response to our own RemoveListener() (via ::Remove()), or + * because the DOM GC'd the DOMLocalMediaStream/etc we're attached to. + */ + void NotifyRemoved(); + + void NotifyDirectListeners(MediaStreamGraph* aGraph, bool aHasListeners); + + bool Activated() const + { + return mActivated; + } + + bool Stopped() const + { + return mStopped; + } + + bool CapturingVideo() const; + + bool CapturingAudio() const; + + bool CapturingScreen() const; + + bool CapturingWindow() const; + + bool CapturingApplication() const; + + bool CapturingBrowser() const; already_AddRefed ApplyConstraintsToTrack(nsPIDOMWindowInner* aWindow, - TrackID aID, + TrackID aTrackID, const dom::MediaTrackConstraints& aConstraints, dom::CallerType aCallerType); - // mVideo/AudioDevice are set by Activate(), so we assume they're capturing - // if set and represent a real capture device. - bool CapturingVideo() - { - MOZ_ASSERT(NS_IsMainThread()); - return mVideoDevice && !mStopped && - !mVideoDevice->GetSource()->IsAvailable() && - mVideoDevice->GetMediaSource() == dom::MediaSourceEnum::Camera && - (!mVideoDevice->GetSource()->IsFake() || - Preferences::GetBool("media.navigator.permission.fake")); - } - bool CapturingAudio() - { - MOZ_ASSERT(NS_IsMainThread()); - return mAudioDevice && !mStopped && - !mAudioDevice->GetSource()->IsAvailable() && - (!mAudioDevice->GetSource()->IsFake() || - Preferences::GetBool("media.navigator.permission.fake")); - } - bool CapturingScreen() - { - MOZ_ASSERT(NS_IsMainThread()); - return mVideoDevice && !mStopped && - !mVideoDevice->GetSource()->IsAvailable() && - mVideoDevice->GetMediaSource() == dom::MediaSourceEnum::Screen; - } - bool CapturingWindow() - { - MOZ_ASSERT(NS_IsMainThread()); - return mVideoDevice && !mStopped && - !mVideoDevice->GetSource()->IsAvailable() && - mVideoDevice->GetMediaSource() == dom::MediaSourceEnum::Window; - } - bool CapturingApplication() - { - MOZ_ASSERT(NS_IsMainThread()); - return mVideoDevice && !mStopped && - !mVideoDevice->GetSource()->IsAvailable() && - mVideoDevice->GetMediaSource() == dom::MediaSourceEnum::Application; - } - bool CapturingBrowser() - { - MOZ_ASSERT(NS_IsMainThread()); - return mVideoDevice && !mStopped && - mVideoDevice->GetSource()->IsAvailable() && - mVideoDevice->GetMediaSource() == dom::MediaSourceEnum::Browser; - } - - void GetSettings(dom::MediaTrackSettings& aOutSettings, TrackID aTrackID) - { - switch (aTrackID) { - case kVideoTrack: - if (mVideoDevice) { - mVideoDevice->GetSource()->GetSettings(aOutSettings); - } - break; - - case kAudioTrack: - if (mAudioDevice) { - mAudioDevice->GetSource()->GetSettings(aOutSettings); - } - break; - } - } - - // implement in .cpp to avoid circular dependency with MediaOperationTask - // Can be invoked from EITHER MainThread or MSG thread - void Stop(); - - void - AudioConfig(bool aEchoOn, uint32_t aEcho, - bool aAgcOn, uint32_t aAGC, - bool aNoiseOn, uint32_t aNoise, - int32_t aPlayoutDelay); - - void - Remove() - { - MOZ_ASSERT(NS_IsMainThread()); - // allow calling even if inactive (!mStream) for easier cleanup - // Caller holds strong reference to us, so no death grip required - if (mStream && !mRemoved) { - MM_LOG(("Listener removed on purpose, mFinished = %d", (int) mFinished)); - mRemoved = true; // RemoveListener is async, avoid races - // If it's destroyed, don't call - listener will be removed and we'll be notified! - if (!mStream->IsDestroyed()) { - mStream->RemoveListener(this); - } - } - } - - // Proxy NotifyPull() to sources - void - NotifyPull(MediaStreamGraph* aGraph, StreamTime aDesiredTime) override - { - // Currently audio sources ignore NotifyPull, but they could - // watch it especially for fake audio. - if (mAudioDevice) { - mAudioDevice->GetSource()->NotifyPull(aGraph, mStream, kAudioTrack, - aDesiredTime, mPrincipalHandle); - } - if (mVideoDevice) { - mVideoDevice->GetSource()->NotifyPull(aGraph, mStream, kVideoTrack, - aDesiredTime, mPrincipalHandle); - } - } - - void - NotifyEvent(MediaStreamGraph* aGraph, - MediaStreamGraphEvent aEvent) override - { - nsresult rv; - nsCOMPtr thread; - - switch (aEvent) { - case MediaStreamGraphEvent::EVENT_FINISHED: - rv = NS_GetMainThread(getter_AddRefs(thread)); - if (NS_WARN_IF(NS_FAILED(rv))) { - NS_ASSERTION(false, "Mainthread not available; running on current thread"); - // Ensure this really *was* MainThread (NS_GetCurrentThread won't work) - MOZ_RELEASE_ASSERT(mMainThreadCheck == PR_GetCurrentThread()); - NotifyFinished(); - return; - } - thread->Dispatch(NewRunnableMethod(this, &GetUserMediaCallbackMediaStreamListener::NotifyFinished), - NS_DISPATCH_NORMAL); - break; - case MediaStreamGraphEvent::EVENT_REMOVED: - rv = NS_GetMainThread(getter_AddRefs(thread)); - if (NS_WARN_IF(NS_FAILED(rv))) { - NS_ASSERTION(false, "Mainthread not available; running on current thread"); - // Ensure this really *was* MainThread (NS_GetCurrentThread won't work) - MOZ_RELEASE_ASSERT(mMainThreadCheck == PR_GetCurrentThread()); - NotifyRemoved(); - return; - } - thread->Dispatch(NewRunnableMethod(this, &GetUserMediaCallbackMediaStreamListener::NotifyRemoved), - NS_DISPATCH_NORMAL); - break; - case MediaStreamGraphEvent::EVENT_HAS_DIRECT_LISTENERS: - NotifyDirectListeners(aGraph, true); - break; - case MediaStreamGraphEvent::EVENT_HAS_NO_DIRECT_LISTENERS: - NotifyDirectListeners(aGraph, false); - break; - default: - break; - } - } - - void - NotifyFinished(); - - void - NotifyRemoved(); - - void - NotifyDirectListeners(MediaStreamGraph* aGraph, bool aHasListeners); - - PrincipalHandle GetPrincipalHandle() const { return mPrincipalHandle; } + PrincipalHandle GetPrincipalHandle() const; private: - // Set at construction - base::Thread* mMediaThread; - // never ever indirect off this; just for assertions - PRThread* mMainThreadCheck; + // true after this listener has been Activate()d in a WindowListener. + // MainThread only. + bool mActivated; - uint64_t mWindowID; - const PrincipalHandle mPrincipalHandle; - - // true after this listener has sent MEDIA_STOP. MainThread only. + // true after this listener has had all devices stopped. MainThread only. bool mStopped; // true after the stream this listener is listening to has finished in the @@ -423,25 +291,20 @@ private: // MainThread only. bool mRemoved; - // true if we have sent MEDIA_STOP or MEDIA_STOP_TRACK for mAudioDevice. - // MainThread only. + // true if we have stopped mAudioDevice. MainThread only. bool mAudioStopped; - // true if we have scheduled MEDIA_STOP or MEDIA_STOP_TRACK for mAudioDevice. - // MainThread only. - bool mAudioStopPending; - - // true if we have sent MEDIA_STOP or MEDIA_STOP_TRACK for mVideoDevice. - // MainThread only. + // true if we have stopped mVideoDevice. MainThread only. bool mVideoStopped; - // true if we have scheduled MEDIA_STOP or MEDIA_STOP_TRACK for mVideoDevice. - // MainThread only. - bool mVideoStopPending; + // never ever indirect off this; just for assertions + PRThread* mMainThreadCheck; - // true if we have scheduled a task to notify chrome in the next stable state. - // The task will reset this to false. MainThread only. - bool mChromeNotificationTaskPosted; + // Set in Register() on main thread, then read from any thread. + PrincipalHandle mPrincipalHandle; + + // Weak pointer to the window listener that owns us. MainThread only. + GetUserMediaWindowListener* mWindowListener; // Set at Activate on MainThread @@ -452,155 +315,332 @@ private: RefPtr mStream; // threadsafe refcnt }; -// Generic class for running long media operations like Start off the main -// thread, and then (because nsDOMMediaStreams aren't threadsafe), -// ProxyReleases mStream since it's cycle collected. -class MediaOperationTask : public Runnable +/** + * This class represents a WindowID and handles all MediaStreamListeners + * (here subclassed as SourceListeners) used to feed GetUserMedia source + * streams. It proxies feedback from them into messages for browser chrome. + * The SourceListeners are used to Start() and Stop() the underlying + * MediaEngineSource when MediaStreams are assigned and deassigned in content. + */ +class GetUserMediaWindowListener { + friend MediaManager; public: - // so we can send Stop without AddRef()ing from the MSG thread - MediaOperationTask(MediaOperation aType, - GetUserMediaCallbackMediaStreamListener* aListener, - DOMMediaStream* aStream, - OnTracksAvailableCallback* aOnTracksAvailableCallback, - AudioDevice* aAudioDevice, - VideoDevice* aVideoDevice, - bool aBool, + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(GetUserMediaWindowListener) + + // Create in an inactive state + GetUserMediaWindowListener(base::Thread *aThread, uint64_t aWindowID, - already_AddRefed aError, - const dom::MediaTrackConstraints& aConstraints = dom::MediaTrackConstraints()) - : mType(aType) - , mStream(aStream) - , mOnTracksAvailableCallback(aOnTracksAvailableCallback) - , mAudioDevice(aAudioDevice) - , mVideoDevice(aVideoDevice) - , mListener(aListener) - , mBool(aBool) + const PrincipalHandle& aPrincipalHandle) + : mMediaThread(aThread) , mWindowID(aWindowID) - , mOnFailure(aError) - , mConstraints(aConstraints) + , mPrincipalHandle(aPrincipalHandle) + , mChromeNotificationTaskPosted(false) {} - ~MediaOperationTask() + /** + * Registers an inactive gUM source listener for this WindowListener. + */ + void Register(SourceListener* aListener) { - // MediaStreams can be released on any thread. - } - - void - ReturnCallbackError(nsresult rv, const char* errorLog); - - NS_IMETHOD - Run() override - { - SourceMediaStream *source = mListener->GetSourceStream(); - // No locking between these is required as all the callbacks for the - // same MediaStream will occur on the same thread. - if (!source) // means the stream was never Activated() - return NS_OK; - - switch (mType) { - case MEDIA_START: - { - NS_ASSERTION(!NS_IsMainThread(), "Never call on main thread"); - nsresult rv; - - if (mAudioDevice) { - rv = mAudioDevice->GetSource()->Start(source, kAudioTrack, - mListener->GetPrincipalHandle()); - if (NS_FAILED(rv)) { - ReturnCallbackError(rv, "Starting audio failed"); - return NS_OK; - } - } - if (mVideoDevice) { - rv = mVideoDevice->GetSource()->Start(source, kVideoTrack, - mListener->GetPrincipalHandle()); - if (NS_FAILED(rv)) { - ReturnCallbackError(rv, "Starting video failed"); - return NS_OK; - } - } - // Start() queued the tracks to be added synchronously to avoid races - source->FinishAddTracks(); - - source->SetPullEnabled(true); - source->AdvanceKnownTracksTime(STREAM_TIME_MAX); - - MM_LOG(("started all sources")); - // Forward mOnTracksAvailableCallback to GetUserMediaNotificationEvent, - // because mOnTracksAvailableCallback needs to be added to mStream - // on the main thread. - nsIRunnable *event = - new GetUserMediaNotificationEvent(GetUserMediaNotificationEvent::STARTING, - mStream.forget(), - mOnTracksAvailableCallback.forget(), - mAudioDevice != nullptr, - mVideoDevice != nullptr, - mWindowID, mOnFailure.forget()); - // event must always be released on mainthread due to the JS callbacks - // in the TracksAvailableCallback - NS_DispatchToMainThread(event); - } - break; - - case MEDIA_STOP: - case MEDIA_STOP_TRACK: - { - NS_ASSERTION(!NS_IsMainThread(), "Never call on main thread"); - if (mAudioDevice) { - mAudioDevice->GetSource()->Stop(source, kAudioTrack); - mAudioDevice->Deallocate(); - } - if (mVideoDevice) { - mVideoDevice->GetSource()->Stop(source, kVideoTrack); - mVideoDevice->Deallocate(); - } - if (mType == MEDIA_STOP) { - source->EndAllTrackAndFinish(); - } - - nsIRunnable *event = - new GetUserMediaNotificationEvent(mListener, - mType == MEDIA_STOP ? - GetUserMediaNotificationEvent::STOPPING : - GetUserMediaNotificationEvent::STOPPED_TRACK, - mAudioDevice != nullptr, - mVideoDevice != nullptr, - mWindowID); - // event must always be released on mainthread due to the JS callbacks - // in the TracksAvailableCallback - NS_DispatchToMainThread(event); - } - break; - - case MEDIA_DIRECT_LISTENERS: - { - NS_ASSERTION(!NS_IsMainThread(), "Never call on main thread"); - if (mVideoDevice) { - mVideoDevice->GetSource()->SetDirectListeners(mBool); - } - } - break; - - default: - MOZ_ASSERT(false,"invalid MediaManager operation"); - break; + MOZ_ASSERT(NS_IsMainThread()); + if (!aListener || aListener->Activated()) { + MOZ_ASSERT(false, "Invalid listener"); + return; + } + if (mInactiveListeners.Contains(aListener)) { + MOZ_ASSERT(false, "Already registered"); + return; + } + if (mActiveListeners.Contains(aListener)) { + MOZ_ASSERT(false, "Already activated"); + return; } - return NS_OK; + aListener->Register(this); + mInactiveListeners.AppendElement(aListener); } + /** + * Activates an already registered and inactive gUM source listener for this + * WindowListener. + */ + void Activate(SourceListener* aListener, + SourceMediaStream* aStream, + AudioDevice* aAudioDevice, + VideoDevice* aVideoDevice) + { + MOZ_ASSERT(NS_IsMainThread()); + + if (!aListener || aListener->Activated()) { + MOZ_ASSERT(false, "Cannot activate already activated source listener"); + return; + } + + if (!mInactiveListeners.RemoveElement(aListener)) { + MOZ_ASSERT(false, "Cannot activate non-registered source listener"); + return; + } + + RefPtr listener = aListener; + listener->Activate(aStream, aAudioDevice, aVideoDevice); + mActiveListeners.AppendElement(listener.forget()); + } + + // Can be invoked from EITHER MainThread or MSG thread + void Stop() + { + MOZ_ASSERT(NS_IsMainThread(), "Only call on main thread"); + + for (auto& source : mActiveListeners) { + source->Stop(); + } + + // Once all tracks have stopped, that will trigger the chrome notification + } + + /** + * Removes all SourceListeners from this window listener. + * Removes this window listener from the list of active windows, so callers + * need to make sure to hold a strong reference. + */ + void RemoveAll() + { + MOZ_ASSERT(NS_IsMainThread()); + + // Shallow copy since SourceListener::Remove() will modify the arrays. + nsTArray> listeners(mInactiveListeners.Length() + + mActiveListeners.Length()); + listeners.AppendElements(mInactiveListeners); + listeners.AppendElements(mActiveListeners); + for (auto& l : listeners) { + Remove(l); + } + + MOZ_ASSERT(mInactiveListeners.Length() == 0); + MOZ_ASSERT(mActiveListeners.Length() == 0); + + RefPtr mgr = MediaManager::GetIfExists(); + if (!mgr) { + MOZ_ASSERT(false, "MediaManager should stay until everything is removed"); + return; + } + GetUserMediaWindowListener* windowListener = + mgr->GetWindowListener(mWindowID); + + if (!windowListener) { + nsCOMPtr obs = services::GetObserverService(); + auto* globalWindow = nsGlobalWindow::GetInnerWindowWithId(mWindowID); + if (globalWindow) { + RefPtr req = + new GetUserMediaRequest(globalWindow->AsInner(), + NullString(), NullString()); + obs->NotifyObservers(req, "recording-device-stopped", nullptr); + } + return; + } + + MOZ_ASSERT(windowListener == this, + "There should only be one window listener per window ID"); + + LOG(("GUMWindowListener %p removing windowID %" PRIu64, this, mWindowID)); + mgr->RemoveWindowID(mWindowID); + } + + bool Remove(SourceListener* aListener) + { + MOZ_ASSERT(NS_IsMainThread()); + + if (!mInactiveListeners.RemoveElement(aListener) && + !mActiveListeners.RemoveElement(aListener)) { + return false; + } + + MOZ_ASSERT(!mInactiveListeners.Contains(aListener), + "A SourceListener should only be once in one of " + "mInactiveListeners and mActiveListeners"); + MOZ_ASSERT(!mActiveListeners.Contains(aListener), + "A SourceListener should only be once in one of " + "mInactiveListeners and mActiveListeners"); + + LOG(("GUMWindowListener %p removing SourceListener %p.", this, aListener)); + aListener->Remove(); + + if (VideoDevice* removedDevice = aListener->GetVideoDevice()) { + bool revokeVideoPermission = true; + nsString removedRawId; + nsString removedSourceType; + removedDevice->GetRawId(removedRawId); + removedDevice->GetMediaSource(removedSourceType); + for (const auto& l : mActiveListeners) { + if (VideoDevice* device = l->GetVideoDevice()) { + nsString rawId; + device->GetRawId(rawId); + if (removedRawId.Equals(rawId)) { + revokeVideoPermission = false; + break; + } + } + } + + if (revokeVideoPermission) { + nsCOMPtr obs = services::GetObserverService(); + auto* globalWindow = nsGlobalWindow::GetInnerWindowWithId(mWindowID); + nsPIDOMWindowInner* window = globalWindow ? globalWindow->AsInner() + : nullptr; + RefPtr req = + new GetUserMediaRequest(window, removedRawId, removedSourceType); + obs->NotifyObservers(req, "recording-device-stopped", nullptr); + } + } + + if (AudioDevice* removedDevice = aListener->GetAudioDevice()) { + bool revokeAudioPermission = true; + nsString removedRawId; + nsString removedSourceType; + removedDevice->GetRawId(removedRawId); + removedDevice->GetMediaSource(removedSourceType); + for (const auto& l : mActiveListeners) { + if (AudioDevice* device = l->GetAudioDevice()) { + nsString rawId; + device->GetRawId(rawId); + if (removedRawId.Equals(rawId)) { + revokeAudioPermission = false; + break; + } + } + } + + if (revokeAudioPermission) { + nsCOMPtr obs = services::GetObserverService(); + auto* globalWindow = nsGlobalWindow::GetInnerWindowWithId(mWindowID); + nsPIDOMWindowInner* window = globalWindow ? globalWindow->AsInner() + : nullptr; + RefPtr req = + new GetUserMediaRequest(window, removedRawId, removedSourceType); + obs->NotifyObservers(req, "recording-device-stopped", nullptr); + } + } + + if (mInactiveListeners.Length() == 0 && + mActiveListeners.Length() == 0) { + LOG(("GUMWindowListener %p Removed the last SourceListener. " + "Cleaning up.", this)); + RemoveAll(); + } + + return true; + } + + void StopSharing(); + + /** + * Called by one of our SourceListeners when one of its tracks has stopped. + * Schedules an event for the next stable state to update chrome. + */ + void NotifySourceTrackStopped(); + + /** + * Called in stable state to send a notification to update chrome. + */ + void NotifyChromeOfTrackStops(); + + bool CapturingVideo() const + { + MOZ_ASSERT(NS_IsMainThread()); + for (auto& l : mActiveListeners) { + if (l->CapturingVideo()) { + return true; + } + } + return false; + } + bool CapturingAudio() const + { + MOZ_ASSERT(NS_IsMainThread()); + for (auto& l : mActiveListeners) { + if (l->CapturingAudio()) { + return true; + } + } + return false; + } + bool CapturingScreen() const + { + MOZ_ASSERT(NS_IsMainThread()); + for (auto& l : mActiveListeners) { + if (l->CapturingScreen()) { + return true; + } + } + return false; + } + bool CapturingWindow() const + { + MOZ_ASSERT(NS_IsMainThread()); + for (auto& l : mActiveListeners) { + if (l->CapturingWindow()) { + return true; + } + } + return false; + } + bool CapturingApplication() const + { + MOZ_ASSERT(NS_IsMainThread()); + for (auto& l : mActiveListeners) { + if (l->CapturingApplication()) { + return true; + } + } + return false; + } + bool CapturingBrowser() const + { + MOZ_ASSERT(NS_IsMainThread()); + for (auto& l : mActiveListeners) { + if (l->CapturingBrowser()) { + return true; + } + } + return false; + } + + uint64_t WindowID() const + { + return mWindowID; + } + + PrincipalHandle GetPrincipalHandle() const { return mPrincipalHandle; } + private: - MediaOperation mType; - RefPtr mStream; - nsAutoPtr mOnTracksAvailableCallback; - RefPtr mAudioDevice; // threadsafe - RefPtr mVideoDevice; // threadsafe - RefPtr mListener; // threadsafe - bool mBool; + ~GetUserMediaWindowListener() + { + for (auto& l : mInactiveListeners) { + l->NotifyRemoved(); + } + mInactiveListeners.Clear(); + for (auto& l : mActiveListeners) { + l->NotifyRemoved(); + } + mActiveListeners.Clear(); + Unused << mMediaThread; + // It's OK to release mStream on any thread; they have thread-safe + // refcounts. + } + + // Set at construction + base::Thread* mMediaThread; + uint64_t mWindowID; - nsCOMPtr mOnFailure; - dom::MediaTrackConstraints mConstraints; + const PrincipalHandle mPrincipalHandle; + + // true if we have scheduled a task to notify chrome in the next stable state. + // The task will reset this to false. MainThread only. + bool mChromeNotificationTaskPosted; + + nsTArray> mInactiveListeners; + nsTArray> mActiveListeners; }; /** @@ -613,8 +653,8 @@ class ErrorCallbackRunnable : public Runnable { public: ErrorCallbackRunnable( - nsCOMPtr& aOnSuccess, - nsCOMPtr& aOnFailure, + nsCOMPtr&& aOnSuccess, + nsCOMPtr&& aOnFailure, MediaMgrError& aError, uint64_t aWindowID) : mError(&aError) @@ -659,29 +699,6 @@ private: RefPtr mManager; // get ref to this when creating the runnable }; -// Handle removing GetUserMediaCallbackMediaStreamListener from main thread -class GetUserMediaListenerRemove: public Runnable -{ -public: - GetUserMediaListenerRemove(uint64_t aWindowID, - GetUserMediaCallbackMediaStreamListener *aListener) - : mWindowID(aWindowID) - , mListener(aListener) {} - - NS_IMETHOD - Run() override - { - MOZ_ASSERT(NS_IsMainThread()); - RefPtr manager(MediaManager::GetInstance()); - manager->RemoveFromWindowList(mWindowID, mListener); - return NS_OK; - } - -protected: - uint64_t mWindowID; - RefPtr mListener; -}; - /** * nsIMediaDevice implementation. */ @@ -894,25 +911,6 @@ nsresult MediaDevice::Deallocate() { return GetSource()->Deallocate(mAllocationHandle); } -void -MediaOperationTask::ReturnCallbackError(nsresult rv, const char* errorLog) -{ - MM_LOG(("%s , rv=%" PRIu32, errorLog, static_cast(rv))); - NS_DispatchToMainThread(do_AddRef(new ReleaseMediaOperationResource(mStream.forget(), - mOnTracksAvailableCallback.forget()))); - nsString log; - - log.AssignASCII(errorLog); - nsCOMPtr onSuccess; - RefPtr error = new MediaMgrError( - NS_LITERAL_STRING("InternalError"), log); - NS_DispatchToMainThread(do_AddRef( - new ErrorCallbackRunnable(onSuccess, - mOnFailure, - *error, - mWindowID))); -} - static bool IsOn(const OwningBooleanOrMediaTrackConstraints &aUnion) { return !aUnion.IsBoolean() || aUnion.GetAsBoolean(); @@ -987,7 +985,8 @@ public: nsCOMPtr& aOnSuccess, nsCOMPtr& aOnFailure, uint64_t aWindowID, - GetUserMediaCallbackMediaStreamListener* aListener, + GetUserMediaWindowListener* aWindowListener, + SourceListener* aSourceListener, const ipc::PrincipalInfo& aPrincipalInfo, const MediaStreamConstraints& aConstraints, AudioDevice* aAudioDevice, @@ -997,7 +996,8 @@ public: , mAudioDevice(aAudioDevice) , mVideoDevice(aVideoDevice) , mWindowID(aWindowID) - , mListener(aListener) + , mWindowListener(aWindowListener) + , mSourceListener(aSourceListener) , mPrincipalInfo(aPrincipalInfo) , mPeerIdentity(aPeerIdentity) , mManager(MediaManager::GetInstance()) @@ -1012,7 +1012,7 @@ public: { public: TracksAvailableCallback(MediaManager* aManager, - nsIDOMGetUserMediaSuccessCallback* aSuccess, + already_AddRefed aSuccess, uint64_t aWindowID, DOMMediaStream* aStream) : mWindowID(aWindowID), mOnSuccess(aSuccess), mManager(aManager), @@ -1056,8 +1056,9 @@ public: // We're on main-thread, and the windowlist can only // be invalidated from the main-thread (see OnNavigation) - StreamListeners* listeners = mManager->GetWindowListeners(mWindowID); - if (!listeners || !window || !window->GetExtantDoc()) { + GetUserMediaWindowListener* listener = + mManager->GetWindowListener(mWindowID); + if (!listener || !window || !window->GetExtantDoc()) { // This window is no longer live. mListener has already been removed return NS_OK; } @@ -1094,7 +1095,7 @@ public: public: LocalTrackSource(nsIPrincipal* aPrincipal, const nsString& aLabel, - GetUserMediaCallbackMediaStreamListener* aListener, + SourceListener* aListener, const MediaSourceEnum aSource, const TrackID aTrackID, const PeerIdentity* aPeerIdentity) @@ -1111,6 +1112,7 @@ public: return mPeerIdentity; } + already_AddRefed ApplyConstraints(nsPIDOMWindowInner* aWindow, const MediaTrackConstraints& aConstraints, @@ -1146,7 +1148,7 @@ public: protected: ~LocalTrackSource() {} - RefPtr mListener; + RefPtr mListener; const MediaSourceEnum mSource; const TrackID mTrackID; const RefPtr mPeerIdentity; @@ -1165,6 +1167,7 @@ public: domStream = DOMLocalMediaStream::CreateSourceStreamAsInput(window, msg, new FakeTrackSourceGetter(principal)); + stream = domStream->GetInputStream()->AsSourceStream(); if (mAudioDevice) { nsString audioDeviceName; @@ -1172,8 +1175,8 @@ public: const MediaSourceEnum source = mAudioDevice->GetSource()->GetMediaSource(); RefPtr audioSource = - new LocalTrackSource(principal, audioDeviceName, mListener, source, - kAudioTrack, mPeerIdentity); + new LocalTrackSource(principal, audioDeviceName, mSourceListener, + source, kAudioTrack, mPeerIdentity); MOZ_ASSERT(IsOn(mConstraints.mAudio)); RefPtr track = domStream->CreateDOMTrack(kAudioTrack, MediaSegment::AUDIO, audioSource, @@ -1186,18 +1189,17 @@ public: const MediaSourceEnum source = mVideoDevice->GetSource()->GetMediaSource(); RefPtr videoSource = - new LocalTrackSource(principal, videoDeviceName, mListener, source, - kVideoTrack, mPeerIdentity); + new LocalTrackSource(principal, videoDeviceName, mSourceListener, + source, kVideoTrack, mPeerIdentity); MOZ_ASSERT(IsOn(mConstraints.mVideo)); RefPtr track = domStream->CreateDOMTrack(kVideoTrack, MediaSegment::VIDEO, videoSource, GetInvariant(mConstraints.mVideo)); domStream->AddTrackInternal(track); } - stream = domStream->GetInputStream()->AsSourceStream(); } - if (!domStream || sInShutdown) { + if (!domStream || !stream || sInShutdown) { nsCOMPtr onFailure = mOnFailure.forget(); LOG(("Returning error for getUserMedia() - no stream")); @@ -1211,29 +1213,86 @@ public: return NS_OK; } - // The listener was added at the beginning in an inactive state. - // Activate our listener. We'll call Start() on the source when get a callback - // that the MediaStream has started consuming. The listener is freed - // when the page is invalidated (on navigation or close). - MOZ_ASSERT(stream); - mListener->Activate(stream.forget(), mAudioDevice, mVideoDevice); + // Activate our source listener. We'll call Start() on the source when we + // get a callback that the MediaStream has started consuming. The listener + // is freed when the page is invalidated (on navigation or close). + mWindowListener->Activate(mSourceListener, stream, mAudioDevice, mVideoDevice); // Note: includes JS callbacks; must be released on MainThread - TracksAvailableCallback* tracksAvailableCallback = - new TracksAvailableCallback(mManager, mOnSuccess, mWindowID, domStream); + auto callback = MakeRefPtr>>( + new TracksAvailableCallback(mManager, mOnSuccess.forget(), mWindowID, domStream)); // Dispatch to the media thread to ask it to start the sources, // because that can take a while. - // Pass ownership of domStream to the MediaOperationTask - // to ensure it's kept alive until the MediaOperationTask runs (at least). - RefPtr mediaOperation = - new MediaOperationTask(MEDIA_START, mListener, domStream, - tracksAvailableCallback, - mAudioDevice, mVideoDevice, - false, mWindowID, mOnFailure.forget()); - MediaManager::PostTask(mediaOperation.forget()); - // We won't need mOnFailure now. - mOnFailure = nullptr; + // Pass ownership of domStream through the lambda to + // GetUserMediaNotificationEvent to ensure it's kept alive until the + // GetUserMediaNotificationEvent runs or is discarded. + RefPtr self = this; + MediaManager::PostTask(NewTaskFrom([self, domStream, callback]() mutable { + MOZ_ASSERT(MediaManager::IsInMediaThread()); + SourceMediaStream* source = self->mSourceListener->GetSourceStream(); + + RefPtr error = nullptr; + if (self->mAudioDevice) { + nsresult rv = + self->mAudioDevice->GetSource()->Start(source, kAudioTrack, + self->mSourceListener->GetPrincipalHandle()); + if (NS_FAILED(rv)) { + nsString log; + log.AssignASCII("Starting audio failed"); + error = new MediaMgrError(NS_LITERAL_STRING("InternalError"), log); + } + } + + if (!error && self->mVideoDevice) { + nsresult rv = + self->mVideoDevice->GetSource()->Start(source, kVideoTrack, + self->mSourceListener->GetPrincipalHandle()); + if (NS_FAILED(rv)) { + nsString log; + log.AssignASCII("Starting video failed"); + error = new MediaMgrError(NS_LITERAL_STRING("InternalError"), log); + } + } + + if (error) { + // The DOM stream and track callback must be released on main thread. + NS_DispatchToMainThread(do_AddRef(new ReleaseMediaOperationResource( + domStream.forget(), callback.forget()))); + + // Dispatch the error callback on main thread. + nsCOMPtr onSuccess; + NS_DispatchToMainThread(do_AddRef( + new ErrorCallbackRunnable( + Move(onSuccess), Move(self->mOnFailure), *error, self->mWindowID))); + + // This should be empty now + MOZ_ASSERT(!self->mOnFailure); + return NS_OK; + } + + // Start() queued the tracks to be added synchronously to avoid races + source->FinishAddTracks(); + + source->SetPullEnabled(true); + source->AdvanceKnownTracksTime(STREAM_TIME_MAX); + + MM_LOG(("started all sources")); + + // Forward onTracksAvailableCallback to GetUserMediaNotificationEvent, + // because onTracksAvailableCallback needs to be added to domStream + // on the main thread. + // The event runnable must always be released on mainthread due to the JS + // callbacks in the TracksAvailableCallback. + NS_DispatchToMainThread(do_AddRef( + new GetUserMediaNotificationEvent( + GetUserMediaNotificationEvent::STARTING, + domStream.forget(), + callback.forget(), + self->mWindowID, + self->mOnFailure.forget()))); + return NS_OK; + })); if (!IsPincipalInfoPrivate(mPrincipalInfo)) { // Call GetPrincipalKey again, this time w/persist = true, to promote @@ -1251,7 +1310,8 @@ private: RefPtr mAudioDevice; RefPtr mVideoDevice; uint64_t mWindowID; - RefPtr mListener; + RefPtr mWindowListener; + RefPtr mSourceListener; ipc::PrincipalInfo mPrincipalInfo; RefPtr mPeerIdentity; RefPtr mManager; // get ref to this when creating the runnable @@ -1384,8 +1444,8 @@ public: const MediaStreamConstraints& aConstraints, already_AddRefed aOnSuccess, already_AddRefed aOnFailure, - uint64_t aWindowID, GetUserMediaCallbackMediaStreamListener *aListener, - MediaEnginePrefs &aPrefs, + uint64_t aWindowID, GetUserMediaWindowListener *aWindowListener, + SourceListener *aSourceListener, MediaEnginePrefs &aPrefs, const ipc::PrincipalInfo& aPrincipalInfo, bool aIsChrome, MediaManager::SourceSet* aSourceSet) @@ -1393,7 +1453,8 @@ public: , mOnSuccess(aOnSuccess) , mOnFailure(aOnFailure) , mWindowID(aWindowID) - , mListener(aListener) + , mWindowListener(aWindowListener) + , mSourceListener(aSourceListener) , mPrefs(aPrefs) , mPrincipalInfo(aPrincipalInfo) , mIsChrome(aIsChrome) @@ -1410,15 +1471,16 @@ public: const nsAString& aMessage = EmptyString(), const nsAString& aConstraint = EmptyString()) { RefPtr error = new MediaMgrError(aName, aMessage, aConstraint); - auto runnable = MakeRefPtr>( - mOnSuccess, mOnFailure, *error, mWindowID); + auto errorRunnable = MakeRefPtr>( + Move(mOnSuccess), Move(mOnFailure), *error, mWindowID); // These should be empty now MOZ_ASSERT(!mOnSuccess); MOZ_ASSERT(!mOnFailure); - NS_DispatchToMainThread(runnable.forget()); + NS_DispatchToMainThread(errorRunnable.forget()); // Do after ErrorCallbackRunnable Run()s, as it checks active window list - NS_DispatchToMainThread(do_AddRef(new GetUserMediaListenerRemove(mWindowID, mListener))); + NS_DispatchToMainThread(NewRunnableMethod>( + mWindowListener, &GetUserMediaWindowListener::Remove, mSourceListener)); } NS_IMETHOD @@ -1486,8 +1548,9 @@ public: NS_DispatchToMainThread(do_AddRef( new GetUserMediaStreamRunnable(mOnSuccess, mOnFailure, mWindowID, - mListener, mPrincipalInfo, - mConstraints, mAudioDevice, mVideoDevice, + mWindowListener, mSourceListener, + mPrincipalInfo, mConstraints, + mAudioDevice, mVideoDevice, peerIdentity))); MOZ_ASSERT(!mOnSuccess); MOZ_ASSERT(!mOnFailure); @@ -1515,8 +1578,7 @@ public: onFailure->OnError(error); } // Should happen *after* error runs for consistency, but may not matter - RefPtr manager(MediaManager::GetInstance()); - manager->RemoveFromWindowList(mWindowID, mListener); + mWindowListener->Remove(mSourceListener); } else { // This will re-check the window being alive on main-thread // and remove the listener on MainThread as well @@ -1564,7 +1626,8 @@ private: nsCOMPtr mOnSuccess; nsCOMPtr mOnFailure; uint64_t mWindowID; - RefPtr mListener; + RefPtr mWindowListener; + RefPtr mSourceListener; RefPtr mAudioDevice; RefPtr mVideoDevice; MediaEnginePrefs mPrefs; @@ -1895,9 +1958,7 @@ MediaManager::PostTask(already_AddRefed task) /* static */ nsresult MediaManager::NotifyRecordingStatusChange(nsPIDOMWindowInner* aWindow, - const nsString& aMsg, - const bool& aIsAudio, - const bool& aIsVideo) + const nsString& aMsg) { NS_ENSURE_ARG(aWindow); @@ -1908,8 +1969,6 @@ MediaManager::NotifyRecordingStatusChange(nsPIDOMWindowInner* aWindow, } RefPtr props = new nsHashPropertyBag(); - props->SetPropertyAsBool(NS_LITERAL_STRING("isAudio"), aIsAudio); - props->SetPropertyAsBool(NS_LITERAL_STRING("isVideo"), aIsVideo); nsCString pageURL; nsCOMPtr docURI = aWindow->GetDocumentURI(); @@ -1926,16 +1985,6 @@ MediaManager::NotifyRecordingStatusChange(nsPIDOMWindowInner* aWindow, "recording-device-events", aMsg.get()); - // Forward recording events to parent process. - // The events are gathered in chrome process and used for recording indicator - if (!XRE_IsParentProcess()) { - Unused << - dom::ContentChild::GetSingleton()->SendRecordingDeviceEvents(aMsg, - requestURL, - aIsAudio, - aIsVideo); - } - return NS_OK; } @@ -2232,15 +2281,20 @@ if (privileged) { audioType = MediaSourceEnum::Microphone; } - StreamListeners* listeners = AddWindowID(windowID); + // Create a window listener if it doesn't already exist. + RefPtr windowListener = + GetWindowListener(windowID); + if (windowListener) { + PrincipalHandle existingPrincipalHandle = windowListener->GetPrincipalHandle(); + MOZ_ASSERT(PrincipalHandleMatches(existingPrincipalHandle, principal)); + } else { + windowListener = new GetUserMediaWindowListener(mMediaThread, windowID, + MakePrincipalHandle(principal)); + AddWindowID(windowID, windowListener); + } - // Create a disabled listener to act as a placeholder - RefPtr listener = - new GetUserMediaCallbackMediaStreamListener(mMediaThread, windowID, - MakePrincipalHandle(principal)); - - // No need for locking because we always do this in the main thread. - listeners->AppendElement(listener); + RefPtr sourceListener = new SourceListener(); + windowListener->Register(sourceListener); if (!privileged) { // Check if this site has had persistent permissions denied. @@ -2269,7 +2323,7 @@ if (privileged) { RefPtr error = new MediaStreamError(aWindow, NS_LITERAL_STRING("NotAllowedError")); onFailure->OnError(error); - RemoveFromWindowList(windowID, listener); + windowListener->Remove(sourceListener); return NS_OK; } } @@ -2292,8 +2346,8 @@ if (privileged) { RefPtr p = EnumerateDevicesImpl(windowID, videoType, audioType, fake); RefPtr self = this; - p->Then([self, onSuccess, onFailure, windowID, c, listener, askPermission, - prefs, isHTTPS, callID, principalInfo, + p->Then([self, onSuccess, onFailure, windowID, c, windowListener, + sourceListener, askPermission, prefs, isHTTPS, callID, principalInfo, isChrome](SourceSet*& aDevices) mutable { // grab result auto devices = MakeRefPtr>>(aDevices); @@ -2307,8 +2361,9 @@ if (privileged) { RefPtr p2 = self->SelectSettings(c, isChrome, devices); p2->Then([self, onSuccess, onFailure, windowID, c, - listener, askPermission, prefs, isHTTPS, callID, principalInfo, - isChrome, devices](const char*& badConstraint) mutable { + windowListener, sourceListener, askPermission, prefs, isHTTPS, + callID, principalInfo, isChrome, devices + ](const char*& badConstraint) mutable { // Ensure that the captured 'this' pointer and our windowID are still good. auto* globalWindow = nsGlobalWindow::GetInnerWindowWithId(windowID); @@ -2346,11 +2401,15 @@ if (privileged) { } } - // Pass callbacks and MediaStreamListener along to GetUserMediaTask. - RefPtr task (new GetUserMediaTask(c, onSuccess.forget(), + // Pass callbacks and listeners along to GetUserMediaTask. + RefPtr task (new GetUserMediaTask(c, + onSuccess.forget(), onFailure.forget(), - windowID, listener, - prefs, principalInfo, + windowID, + windowListener, + sourceListener, + prefs, + principalInfo, isChrome, devices->release())); // Store the task w/callbacks. @@ -2550,17 +2609,24 @@ MediaManager::EnumerateDevices(nsPIDOMWindowInner* aWindow, nsCOMPtr onFailure(aOnFailure); uint64_t windowId = aWindow->WindowID(); - StreamListeners* listeners = AddWindowID(windowId); - nsIPrincipal* principal = aWindow->GetExtantDoc()->NodePrincipal(); - // Create a disabled listener to act as a placeholder - RefPtr listener = - new GetUserMediaCallbackMediaStreamListener(mMediaThread, windowId, - MakePrincipalHandle(principal)); + RefPtr windowListener = + GetWindowListener(windowId); + if (windowListener) { + PrincipalHandle existingPrincipalHandle = + windowListener->GetPrincipalHandle(); + MOZ_ASSERT(PrincipalHandleMatches(existingPrincipalHandle, principal)); + } else { + windowListener = new GetUserMediaWindowListener(mMediaThread, windowId, + MakePrincipalHandle(principal)); + AddWindowID(windowId, windowListener); + } - // No need for locking because we always do this in the main thread. - listeners->AppendElement(listener); + // Create an inactive SourceListener to act as a placeholder, so the + // window listener doesn't clean itself up until we're done. + RefPtr sourceListener = new SourceListener(); + windowListener->Register(sourceListener); bool fake = Preferences::GetBool("media.navigator.streams.fake"); @@ -2568,15 +2634,15 @@ MediaManager::EnumerateDevices(nsPIDOMWindowInner* aWindow, MediaSourceEnum::Camera, MediaSourceEnum::Microphone, fake); - p->Then([onSuccess, windowId, listener](SourceSet*& aDevices) mutable { + p->Then([onSuccess, windowListener, sourceListener](SourceSet*& aDevices) mutable { UniquePtr devices(aDevices); // grab result - RefPtr mgr = MediaManager_GetInstance(); - mgr->RemoveFromWindowList(windowId, listener); + DebugOnly rv = windowListener->Remove(sourceListener); + MOZ_ASSERT(rv); nsCOMPtr array = MediaManager_ToJSArray(*devices); onSuccess->OnSuccess(array); - }, [onFailure, windowId, listener](MediaStreamError*& reason) mutable { - RefPtr mgr = MediaManager_GetInstance(); - mgr->RemoveFromWindowList(windowId, listener); + }, [onFailure, windowListener, sourceListener](MediaStreamError*& reason) mutable { + DebugOnly rv = windowListener->Remove(sourceListener); + MOZ_ASSERT(rv); onFailure->OnError(reason); }); return NS_OK; @@ -2642,24 +2708,21 @@ MediaManager::GetBackend(uint64_t aWindowId) static void StopSharingCallback(MediaManager *aThis, uint64_t aWindowID, - StreamListeners *aListeners, + GetUserMediaWindowListener *aListener, void *aData) { MOZ_ASSERT(NS_IsMainThread()); - if (aListeners) { - auto length = aListeners->Length(); - for (size_t i = 0; i < length; ++i) { - GetUserMediaCallbackMediaStreamListener *listener = aListeners->ElementAt(i); - if (listener->Stream()) { // aka HasBeenActivate()ed - listener->Stop(); - } - listener->Remove(); - listener->StopSharing(); - } - aListeners->Clear(); - aThis->RemoveWindowID(aWindowID); + // Grab a strong ref since RemoveAll() might destroy the listener mid-way + // when clearing the mActiveWindows reference. + RefPtr listener(aListener); + if (!listener) { + return; } + + listener->Stop(); + listener->RemoveAll(); + MOZ_ASSERT(!aThis->GetWindowListener(aWindowID)); } @@ -2688,6 +2751,7 @@ MediaManager::OnNavigation(uint64_t aWindowID) } else { RemoveWindowID(aWindowID); } + MOZ_ASSERT(!GetWindowListener(aWindowID)); RemoveMediaDevicesCallback(aWindowID); } @@ -2711,20 +2775,20 @@ MediaManager::RemoveMediaDevicesCallback(uint64_t aWindowID) } } -StreamListeners* -MediaManager::AddWindowID(uint64_t aWindowId) +void +MediaManager::AddWindowID(uint64_t aWindowId, + GetUserMediaWindowListener* aListener) { MOZ_ASSERT(NS_IsMainThread()); // Store the WindowID in a hash table and mark as active. The entry is removed // when this window is closed or navigated away from. // This is safe since we're on main-thread, and the windowlist can only // be invalidated from the main-thread (see OnNavigation) - StreamListeners* listeners = GetActiveWindows()->Get(aWindowId); - if (!listeners) { - listeners = new StreamListeners; - GetActiveWindows()->Put(aWindowId, listeners); + if (IsWindowStillActive(aWindowId)) { + MOZ_ASSERT(false, "Window already added"); + return; } - return listeners; + GetActiveWindows()->Put(aWindowId, aListener); } void @@ -2758,108 +2822,6 @@ MediaManager::RemoveWindowID(uint64_t aWindowId) aWindowId, outerID)); } -void -MediaManager::RemoveFromWindowList(uint64_t aWindowID, - GetUserMediaCallbackMediaStreamListener *aListener) -{ - MOZ_ASSERT(NS_IsMainThread()); - - nsString videoRawId; - nsString audioRawId; - nsString videoSourceType; - nsString audioSourceType; - bool hasVideoDevice = aListener->mVideoDevice; - bool hasAudioDevice = aListener->mAudioDevice; - - if (hasVideoDevice) { - aListener->mVideoDevice->GetRawId(videoRawId); - aListener->mVideoDevice->GetMediaSource(videoSourceType); - } - if (hasAudioDevice) { - aListener->mAudioDevice->GetRawId(audioRawId); - aListener->mAudioDevice->GetMediaSource(audioSourceType); - } - - // This is defined as safe on an inactive GUMCMSListener - aListener->Remove(); // really queues the remove - - StreamListeners* listeners = GetWindowListeners(aWindowID); - if (!listeners) { - nsCOMPtr obs = services::GetObserverService(); - auto* globalWindow = nsGlobalWindow::GetInnerWindowWithId(aWindowID); - RefPtr window = globalWindow ? globalWindow->AsInner() - : nullptr; - if (window != nullptr) { - RefPtr req = - new GetUserMediaRequest(window, NullString(), NullString()); - obs->NotifyObservers(req, "recording-device-stopped", nullptr); - } - return; - } - listeners->RemoveElement(aListener); - - uint32_t length = listeners->Length(); - - if (hasVideoDevice) { - bool revokeVideoPermission = true; - - for (uint32_t i = 0; i < length; ++i) { - RefPtr listener = - listeners->ElementAt(i); - if (hasVideoDevice && listener->mVideoDevice) { - nsString rawId; - listener->mVideoDevice->GetRawId(rawId); - if (videoRawId.Equals(rawId)) { - revokeVideoPermission = false; - break; - } - } - } - - if (revokeVideoPermission) { - nsCOMPtr obs = services::GetObserverService(); - auto* globalWindow = nsGlobalWindow::GetInnerWindowWithId(aWindowID); - RefPtr window = globalWindow ? globalWindow->AsInner() - : nullptr; - RefPtr req = - new GetUserMediaRequest(window, videoRawId, videoSourceType); - obs->NotifyObservers(req, "recording-device-stopped", nullptr); - } - } - - if (hasAudioDevice) { - bool revokeAudioPermission = true; - - for (uint32_t i = 0; i < length; ++i) { - RefPtr listener = - listeners->ElementAt(i); - if (hasAudioDevice && listener->mAudioDevice) { - nsString rawId; - listener->mAudioDevice->GetRawId(rawId); - if (audioRawId.Equals(rawId)) { - revokeAudioPermission = false; - break; - } - } - } - - if (revokeAudioPermission) { - nsCOMPtr obs = services::GetObserverService(); - auto* globalWindow = nsGlobalWindow::GetInnerWindowWithId(aWindowID); - RefPtr window = globalWindow ? globalWindow->AsInner() - : nullptr; - RefPtr req = - new GetUserMediaRequest(window, audioRawId, audioSourceType); - obs->NotifyObservers(req, "recording-device-stopped", nullptr); - } - } - - if (length == 0) { - RemoveWindowID(aWindowID); - // listeners has been deleted here - } -} - void MediaManager::GetPref(nsIPrefBranch *aBranch, const char *aPref, const char *aData, int32_t *aVal) @@ -3161,7 +3123,10 @@ MediaManager::GetActiveMediaCaptureWindows(nsIArray** aArray) for (auto iter = mActiveWindows.Iter(); !iter.Done(); iter.Next()) { const uint64_t& id = iter.Key(); - StreamListeners* listeners = iter.UserData(); + RefPtr winListener = iter.UserData(); + if (!winListener) { + continue; + } nsPIDOMWindowInner* window = nsGlobalWindow::GetInnerWindowWithId(id)->AsInner(); @@ -3170,24 +3135,10 @@ MediaManager::GetActiveMediaCaptureWindows(nsIArray** aArray) if (!window) { continue; } - // mActiveWindows contains both windows that have requested device - // access and windows that are currently capturing media. We want - // to return only the latter. See bug 975177. - bool capturing = false; - if (listeners) { - uint32_t length = listeners->Length(); - for (uint32_t i = 0; i < length; ++i) { - RefPtr listener = - listeners->ElementAt(i); - if (listener->CapturingVideo() || listener->CapturingAudio() || - listener->CapturingScreen() || listener->CapturingWindow() || - listener->CapturingApplication()) { - capturing = true; - break; - } - } - } - if (capturing) { + + if (winListener->CapturingVideo() || winListener->CapturingAudio() || + winListener->CapturingScreen() || winListener->CapturingWindow() || + winListener->CapturingApplication()) { array->AppendElement(window, /*weak =*/ false); } } @@ -3209,39 +3160,35 @@ struct CaptureWindowStateData { static void CaptureWindowStateCallback(MediaManager *aThis, uint64_t aWindowID, - StreamListeners *aListeners, + GetUserMediaWindowListener *aListener, void *aData) { struct CaptureWindowStateData *data = (struct CaptureWindowStateData *) aData; - if (aListeners) { - auto length = aListeners->Length(); - for (size_t i = 0; i < length; ++i) { - GetUserMediaCallbackMediaStreamListener *listener = aListeners->ElementAt(i); + if (!aListener) { + return; + } - if (listener->CapturingVideo()) { - *data->mVideo = true; - } - if (listener->CapturingAudio()) { - *data->mAudio = true; - } - if (listener->CapturingScreen()) { - *data->mScreenShare = true; - } - if (listener->CapturingWindow()) { - *data->mWindowShare = true; - } - if (listener->CapturingApplication()) { - *data->mAppShare = true; - } - if (listener->CapturingBrowser()) { - *data->mBrowserShare = true; - } - } + if (aListener->CapturingVideo()) { + *data->mVideo = true; + } + if (aListener->CapturingAudio()) { + *data->mAudio = true; + } + if (aListener->CapturingScreen()) { + *data->mScreenShare = true; + } + if (aListener->CapturingWindow()) { + *data->mWindowShare = true; + } + if (aListener->CapturingApplication()) { + *data->mAppShare = true; + } + if (aListener->CapturingBrowser()) { + *data->mBrowserShare = true; } } - NS_IMETHODIMP MediaManager::MediaCaptureWindowState(nsIDOMWindow* aWindow, bool* aVideo, bool* aAudio, bool *aScreenShare, @@ -3290,15 +3237,14 @@ MediaManager::SanitizeDeviceIds(int64_t aSinceWhen) static void StopScreensharingCallback(MediaManager *aThis, uint64_t aWindowID, - StreamListeners *aListeners, + GetUserMediaWindowListener *aListener, void *aData) { - if (aListeners) { - auto length = aListeners->Length(); - for (size_t i = 0; i < length; ++i) { - aListeners->ElementAt(i)->StopSharing(); - } + if (!aListener) { + return; } + + aListener->StopSharing(); } void @@ -3323,10 +3269,12 @@ MediaManager::IterateWindowListeners(nsPIDOMWindowInner* aWindow, // Iterate the docshell tree to find all the child windows, and for each // invoke the callback if (aWindow) { - uint64_t windowID = aWindow->WindowID(); - StreamListeners* listeners = GetActiveWindows()->Get(windowID); - // pass listeners so it can modify/delete the list - (*aCallback)(this, windowID, listeners, aData); + { + uint64_t windowID = aWindow->WindowID(); + GetUserMediaWindowListener* listener = GetWindowListener(windowID); + (*aCallback)(this, windowID, listener, aData); + // NB: `listener` might have been destroyed. + } // iterate any children of *this* window (iframes, etc) nsCOMPtr docShell = aWindow->GetDocShell(); @@ -3414,33 +3362,173 @@ MediaManager::IsActivelyCapturingOrHasAPermission(uint64_t aWindowId) video == nsIPermissionManager::ALLOW_ACTION; } +SourceListener::SourceListener() + : mActivated(false) + , mStopped(false) + , mFinished(false) + , mRemoved(false) + , mAudioStopped(false) + , mVideoStopped(false) + , mMainThreadCheck(nullptr) + , mPrincipalHandle(PRINCIPAL_HANDLE_NONE) + , mWindowListener(nullptr) +{} + void -GetUserMediaCallbackMediaStreamListener::Stop() +SourceListener::Register(GetUserMediaWindowListener* aListener) +{ + if (mWindowListener) { + MOZ_ASSERT(false, "Already registered"); + return; + } + if (mActivated) { + MOZ_ASSERT(false, "Already activated"); + return; + } + if (!aListener) { + MOZ_ASSERT(false, "No listener"); + return; + } + mPrincipalHandle = aListener->GetPrincipalHandle(); + mWindowListener = aListener; +} + +void +SourceListener::Activate(SourceMediaStream* aStream, + AudioDevice* aAudioDevice, + VideoDevice* aVideoDevice) { MOZ_ASSERT(NS_IsMainThread(), "Only call on main thread"); + + if (mActivated) { + MOZ_ASSERT(false, "Already activated"); + return; + } + + mActivated = true; + mMainThreadCheck = PR_GetCurrentThread(); + mStream = aStream; + mAudioDevice = aAudioDevice; + mVideoDevice = aVideoDevice; + mStream->AddListener(this); +} + +void +SourceListener::Stop() +{ + MOZ_ASSERT(NS_IsMainThread(), "Only call on main thread"); + if (mStopped) { return; } - // We can't take a chance on blocking here, so proxy this to another - // thread. - // Pass a ref to us (which is threadsafe) so it can query us for the - // source stream info. - RefPtr mediaOperation = - new MediaOperationTask(MEDIA_STOP, - this, nullptr, nullptr, - !mAudioStopped ? mAudioDevice.get() : nullptr, - !mVideoStopped ? mVideoDevice.get() : nullptr, - false, mWindowID, nullptr); - MediaManager::PostTask(mediaOperation.forget()); - mStopped = mAudioStopped = mVideoStopped = true; + // StopSharing() has some special logic, at least for audio capture. + // It must be called when all tracks have stopped, before setting mStopped. + StopSharing(); + + mStopped = true; + + if (mAudioDevice && !mAudioStopped) { + StopTrack(kAudioTrack); + } + if (mVideoDevice && !mVideoStopped) { + StopTrack(kVideoTrack); + } + RefPtr source = GetSourceStream(); + MediaManager::PostTask(NewTaskFrom([source]() { + MOZ_ASSERT(MediaManager::IsInMediaThread()); + source->EndAllTrackAndFinish(); + })); } -// Doesn't kill audio void -GetUserMediaCallbackMediaStreamListener::StopSharing() +SourceListener::Remove() { MOZ_ASSERT(NS_IsMainThread()); + if (!mStream || mRemoved) { + return; + } + + MM_LOG(("SourceListener removed on purpose, mFinished = %d", (int) mFinished)); + mRemoved = true; // RemoveListener is async, avoid races + mWindowListener = nullptr; + + // If it's destroyed, don't call - listener will be removed and we'll be notified! + if (!mStream->IsDestroyed()) { + mStream->RemoveListener(this); + } +} + +void +SourceListener::StopTrack(TrackID aTrackID) +{ + MOZ_ASSERT(NS_IsMainThread(), "Only call on main thread"); + + RefPtr device; + RefPtr source; + + switch (aTrackID) { + case kAudioTrack: { + if (!mAudioDevice) { + NS_ASSERTION(false, "Can't stop audio. No device."); + return; + } + if (mAudioStopped) { + // Audio already stopped + return; + } + device = mAudioDevice; + source = GetSourceStream(); + mAudioStopped = true; + break; + } + case kVideoTrack: { + if (!mVideoDevice) { + NS_ASSERTION(false, "Can't stop video. No device."); + return; + } + if (mVideoStopped) { + // Video already stopped + return; + } + device = mVideoDevice; + source = GetSourceStream(); + mVideoStopped = true; + break; + } + default: { + MOZ_ASSERT(false, "Unknown track id"); + return; + } + } + + MediaManager::PostTask(NewTaskFrom([device, source, aTrackID]() { + device->GetSource()->Stop(source, aTrackID); + device->Deallocate(); + })); + + if ((!mAudioDevice || mAudioStopped) && + (!mVideoDevice || mVideoStopped)) { + Stop(); + } + + if (!mWindowListener) { + MOZ_ASSERT(false, "Should still have window listener"); + return; + } + mWindowListener->NotifySourceTrackStopped(); +} + +void +SourceListener::StopSharing() +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_RELEASE_ASSERT(mWindowListener); + + if (mStopped) { + return; + } + if (mVideoDevice && (mVideoDevice->GetMediaSource() == MediaSourceEnum::Screen || mVideoDevice->GetMediaSource() == MediaSourceEnum::Application || @@ -3449,27 +3537,218 @@ GetUserMediaCallbackMediaStreamListener::StopSharing() // just the video track if we have both. // StopTrack figures this out for us. StopTrack(kVideoTrack); - } else if (mAudioDevice && - mAudioDevice->GetMediaSource() == MediaSourceEnum::AudioCapture) { - nsCOMPtr window = nsGlobalWindow::GetInnerWindowWithId(mWindowID)->AsInner(); - MOZ_ASSERT(window); + } + if (mAudioDevice && + mAudioDevice->GetMediaSource() == MediaSourceEnum::AudioCapture) { + uint64_t windowID = mWindowListener->WindowID(); + nsCOMPtr window = nsGlobalWindow::GetInnerWindowWithId(windowID)->AsInner(); + MOZ_RELEASE_ASSERT(window); window->SetAudioCapture(false); MediaStreamGraph* graph = MediaStreamGraph::GetInstance(MediaStreamGraph::AUDIO_THREAD_DRIVER, dom::AudioChannel::Normal); - graph->UnregisterCaptureStreamForWindow(mWindowID); + graph->UnregisterCaptureStreamForWindow(windowID); mStream->Destroy(); } } -// ApplyConstraints for track +SourceMediaStream* +SourceListener::GetSourceStream() +{ + NS_ASSERTION(mStream,"Getting stream from never-activated SourceListener"); + if (!mStream) { + return nullptr; + } + return mStream->AsSourceStream(); +} -auto -GetUserMediaCallbackMediaStreamListener::ApplyConstraintsToTrack( - nsPIDOMWindowInner* aWindow, - TrackID aTrackID, - const MediaTrackConstraints& aConstraints, - dom::CallerType aCallerType) -> already_AddRefed +void +SourceListener::GetSettings(dom::MediaTrackSettings& aOutSettings, TrackID aTrackID) +{ + switch (aTrackID) { + case kVideoTrack: { + if (mVideoDevice) { + mVideoDevice->GetSource()->GetSettings(aOutSettings); + } + break; + } + case kAudioTrack: { + if (mAudioDevice) { + mAudioDevice->GetSource()->GetSettings(aOutSettings); + } + break; + } + default: { + MOZ_ASSERT(false, "Unknown track id"); + } + } +} + +// Proxy NotifyPull() to sources +void +SourceListener::NotifyPull(MediaStreamGraph* aGraph, + StreamTime aDesiredTime) +{ + // Currently audio sources ignore NotifyPull, but they could + // watch it especially for fake audio. + if (mAudioDevice) { + mAudioDevice->GetSource()->NotifyPull(aGraph, mStream, kAudioTrack, + aDesiredTime, mPrincipalHandle); + } + if (mVideoDevice) { + mVideoDevice->GetSource()->NotifyPull(aGraph, mStream, kVideoTrack, + aDesiredTime, mPrincipalHandle); + } +} + +void +SourceListener::NotifyEvent(MediaStreamGraph* aGraph, + MediaStreamGraphEvent aEvent) +{ + nsresult rv; + nsCOMPtr thread; + + switch (aEvent) { + case MediaStreamGraphEvent::EVENT_FINISHED: + rv = NS_GetMainThread(getter_AddRefs(thread)); + if (NS_WARN_IF(NS_FAILED(rv))) { + NS_ASSERTION(false, "Mainthread not available; running on current thread"); + // Ensure this really *was* MainThread (NS_GetCurrentThread won't work) + MOZ_RELEASE_ASSERT(mMainThreadCheck == PR_GetCurrentThread()); + NotifyFinished(); + return; + } + thread->Dispatch(NewRunnableMethod(this, &SourceListener::NotifyFinished), + NS_DISPATCH_NORMAL); + break; + case MediaStreamGraphEvent::EVENT_REMOVED: + rv = NS_GetMainThread(getter_AddRefs(thread)); + if (NS_WARN_IF(NS_FAILED(rv))) { + NS_ASSERTION(false, "Mainthread not available; running on current thread"); + // Ensure this really *was* MainThread (NS_GetCurrentThread won't work) + MOZ_RELEASE_ASSERT(mMainThreadCheck == PR_GetCurrentThread()); + NotifyRemoved(); + return; + } + thread->Dispatch(NewRunnableMethod(this, &SourceListener::NotifyRemoved), + NS_DISPATCH_NORMAL); + break; + case MediaStreamGraphEvent::EVENT_HAS_DIRECT_LISTENERS: + NotifyDirectListeners(aGraph, true); + break; + case MediaStreamGraphEvent::EVENT_HAS_NO_DIRECT_LISTENERS: + NotifyDirectListeners(aGraph, false); + break; + default: + break; + } +} + +void +SourceListener::NotifyFinished() +{ + MOZ_ASSERT(NS_IsMainThread()); + mFinished = true; + if (!mWindowListener) { + // Removed explicitly before finished. + return; + } + + Stop(); // we know it's been activated + mWindowListener->Remove(this); +} + +void +SourceListener::NotifyRemoved() +{ + MOZ_ASSERT(NS_IsMainThread()); + MM_LOG(("SourceListener removed, mFinished = %d", (int) mFinished)); + mRemoved = true; + + if (!mFinished) { + NotifyFinished(); + } + + mWindowListener = nullptr; +} + +void +SourceListener::NotifyDirectListeners(MediaStreamGraph* aGraph, + bool aHasListeners) +{ + if (!mVideoDevice) { + return; + } + + auto& videoDevice = mVideoDevice; + MediaManager::PostTask(NewTaskFrom([videoDevice, aHasListeners]() { + videoDevice->GetSource()->SetDirectListeners(aHasListeners); + return NS_OK; + })); +} + +bool +SourceListener::CapturingVideo() const +{ + MOZ_ASSERT(NS_IsMainThread()); + return mActivated && mVideoDevice && !mVideoStopped && + !mVideoDevice->GetSource()->IsAvailable() && + mVideoDevice->GetMediaSource() == dom::MediaSourceEnum::Camera && + (!mVideoDevice->GetSource()->IsFake() || + Preferences::GetBool("media.navigator.permission.fake")); +} + +bool +SourceListener::CapturingAudio() const +{ + MOZ_ASSERT(NS_IsMainThread()); + return mActivated && mAudioDevice && !mAudioStopped && + !mAudioDevice->GetSource()->IsAvailable() && + (!mAudioDevice->GetSource()->IsFake() || + Preferences::GetBool("media.navigator.permission.fake")); +} + +bool +SourceListener::CapturingScreen() const +{ + MOZ_ASSERT(NS_IsMainThread()); + return mActivated && mVideoDevice && !mVideoStopped && + !mVideoDevice->GetSource()->IsAvailable() && + mVideoDevice->GetMediaSource() == dom::MediaSourceEnum::Screen; +} + +bool +SourceListener::CapturingWindow() const +{ + MOZ_ASSERT(NS_IsMainThread()); + return mActivated && mVideoDevice && !mVideoStopped && + !mVideoDevice->GetSource()->IsAvailable() && + mVideoDevice->GetMediaSource() == dom::MediaSourceEnum::Window; +} + +bool +SourceListener::CapturingApplication() const +{ + MOZ_ASSERT(NS_IsMainThread()); + return mActivated && mVideoDevice && !mVideoStopped && + !mVideoDevice->GetSource()->IsAvailable() && + mVideoDevice->GetMediaSource() == dom::MediaSourceEnum::Application; +} + +bool +SourceListener::CapturingBrowser() const +{ + MOZ_ASSERT(NS_IsMainThread()); + return mActivated && mVideoDevice && !mVideoStopped && + !mVideoDevice->GetSource()->IsAvailable() && + mVideoDevice->GetMediaSource() == dom::MediaSourceEnum::Browser; +} + +already_AddRefed +SourceListener::ApplyConstraintsToTrack(nsPIDOMWindowInner* aWindow, + TrackID aTrackID, + const dom::MediaTrackConstraints& aConstraints, + dom::CallerType aCallerType) { MOZ_ASSERT(NS_IsMainThread()); RefPtr p = new PledgeVoid(); @@ -3557,157 +3836,66 @@ GetUserMediaCallbackMediaStreamListener::ApplyConstraintsToTrack( return p.forget(); } -// Stop backend for track +PrincipalHandle +SourceListener::GetPrincipalHandle() const +{ + return mPrincipalHandle; +} + +// Doesn't kill audio +void +GetUserMediaWindowListener::StopSharing() +{ + MOZ_ASSERT(NS_IsMainThread(), "Only call on main thread"); + + for (auto& source : mActiveListeners) { + source->StopSharing(); + } +} void -GetUserMediaCallbackMediaStreamListener::StopTrack(TrackID aTrackID) +GetUserMediaWindowListener::NotifySourceTrackStopped() { MOZ_ASSERT(NS_IsMainThread()); - MOZ_ASSERT(aTrackID == kAudioTrack || aTrackID == kVideoTrack); - - // XXX to support multiple tracks of a type in a stream, this should key off - // the TrackID and not just hard coded values. - - bool stopAudio = aTrackID == kAudioTrack; - bool stopVideo = aTrackID == kVideoTrack; - - if (mStopped || - (stopAudio && (mAudioStopped || !mAudioDevice)) || - (stopVideo && (mVideoStopped || !mVideoDevice))) - { - LOG(("Can't stop gUM track %d (%s), exists=%d, stopped=%d", - aTrackID, - stopAudio ? "audio" : "video", - stopAudio ? !!mAudioDevice : !!mVideoDevice, - stopAudio ? mAudioStopped : mVideoStopped)); - return; - } - - if ((stopAudio || mAudioStopped || !mAudioDevice) && - (stopVideo || mVideoStopped || !mVideoDevice)) { - Stop(); - return; - } // We wait until stable state before notifying chrome so chrome only does one // update if more tracks are stopped in this event loop. - mAudioStopPending |= stopAudio; - mVideoStopPending |= stopVideo; - if (mChromeNotificationTaskPosted) { return; } nsCOMPtr runnable = - NewRunnableMethod(this, &GetUserMediaCallbackMediaStreamListener::NotifyChromeOfTrackStops); + NewRunnableMethod(this, &GetUserMediaWindowListener::NotifyChromeOfTrackStops); nsContentUtils::RunInStableState(runnable.forget()); mChromeNotificationTaskPosted = true; } void -GetUserMediaCallbackMediaStreamListener::NotifyChromeOfTrackStops() +GetUserMediaWindowListener::NotifyChromeOfTrackStops() { MOZ_ASSERT(mChromeNotificationTaskPosted); mChromeNotificationTaskPosted = false; - // We make sure these are always reset. - bool stopAudio = mAudioStopPending; - bool stopVideo = mVideoStopPending; - mAudioStopPending = false; - mVideoStopPending = false; - - if (mStopped) { - // The entire capture was stopped while we were waiting for stable state. - return; - } - - MOZ_ASSERT(stopAudio || stopVideo); - MOZ_ASSERT(!stopAudio || !mAudioStopped, - "If there's a pending stop for audio, audio must not have been stopped"); - MOZ_ASSERT(!stopAudio || mAudioDevice, - "If there's a pending stop for audio, there must be an audio device"); - MOZ_ASSERT(!stopVideo || !mVideoStopped, - "If there's a pending stop for video, video must not have been stopped"); - MOZ_ASSERT(!stopVideo || mVideoDevice, - "If there's a pending stop for video, there must be a video device"); - - if ((stopAudio || mAudioStopped || !mAudioDevice) && - (stopVideo || mVideoStopped || !mVideoDevice)) { - // All tracks stopped. - Stop(); - return; - } - - mAudioStopped |= stopAudio; - mVideoStopped |= stopVideo; - - RefPtr mediaOperation = - new MediaOperationTask(MEDIA_STOP_TRACK, - this, nullptr, nullptr, - stopAudio ? mAudioDevice.get() : nullptr, - stopVideo ? mVideoDevice.get() : nullptr, - false , mWindowID, nullptr); - MediaManager::PostTask(mediaOperation.forget()); -} - -void -GetUserMediaCallbackMediaStreamListener::NotifyFinished() -{ - MOZ_ASSERT(NS_IsMainThread()); - mFinished = true; - Stop(); // we know it's been activated - - RefPtr manager(MediaManager::GetIfExists()); - if (manager) { - manager->RemoveFromWindowList(mWindowID, this); - } else { - NS_WARNING("Late NotifyFinished after MediaManager shutdown"); - } -} - -// Called from the MediaStreamGraph thread -void -GetUserMediaCallbackMediaStreamListener::NotifyDirectListeners(MediaStreamGraph* aGraph, - bool aHasListeners) -{ - RefPtr mediaOperation = - new MediaOperationTask(MEDIA_DIRECT_LISTENERS, - this, nullptr, nullptr, - mAudioDevice, mVideoDevice, - aHasListeners, mWindowID, nullptr); - MediaManager::PostTask(mediaOperation.forget()); -} - -// this can be in response to our own RemoveListener() (via ::Remove()), or -// because the DOM GC'd the DOMLocalMediaStream/etc we're attached to. -void -GetUserMediaCallbackMediaStreamListener::NotifyRemoved() -{ - MOZ_ASSERT(NS_IsMainThread()); - MM_LOG(("Listener removed by DOM Destroy(), mFinished = %d", (int) mFinished)); - mRemoved = true; - - if (!mFinished) { - NotifyFinished(); - } + NS_DispatchToMainThread(do_AddRef(new GetUserMediaNotificationEvent( + GetUserMediaNotificationEvent::STOPPING, mWindowID))); } GetUserMediaNotificationEvent::GetUserMediaNotificationEvent( - GetUserMediaCallbackMediaStreamListener* aListener, GetUserMediaStatus aStatus, - bool aIsAudio, bool aIsVideo, uint64_t aWindowID) -: mListener(aListener) , mStatus(aStatus) , mIsAudio(aIsAudio) -, mIsVideo(aIsVideo), mWindowID(aWindowID) {} + uint64_t aWindowID) +: mStatus(aStatus), mWindowID(aWindowID) {} GetUserMediaNotificationEvent::GetUserMediaNotificationEvent( GetUserMediaStatus aStatus, already_AddRefed aStream, - OnTracksAvailableCallback* aOnTracksAvailableCallback, - bool aIsAudio, bool aIsVideo, uint64_t aWindowID, + already_AddRefed>> aOnTracksAvailableCallback, + uint64_t aWindowID, already_AddRefed aError) -: mStream(aStream), mOnTracksAvailableCallback(aOnTracksAvailableCallback), - mStatus(aStatus), mIsAudio(aIsAudio), mIsVideo(aIsVideo), mWindowID(aWindowID), +: mStream(aStream), + mOnTracksAvailableCallback(aOnTracksAvailableCallback), + mStatus(aStatus), + mWindowID(aWindowID), mOnFailure(aError) {} GetUserMediaNotificationEvent::~GetUserMediaNotificationEvent() { @@ -3727,10 +3915,9 @@ GetUserMediaNotificationEvent::Run() switch (mStatus) { case STARTING: msg = NS_LITERAL_STRING("starting"); - stream->OnTracksAvailable(mOnTracksAvailableCallback.forget()); + stream->OnTracksAvailable(mOnTracksAvailableCallback->release()); break; case STOPPING: - case STOPPED_TRACK: msg = NS_LITERAL_STRING("shutdown"); break; } @@ -3738,7 +3925,7 @@ GetUserMediaNotificationEvent::Run() RefPtr window = nsGlobalWindow::GetInnerWindowWithId(mWindowID); NS_ENSURE_TRUE(window, NS_ERROR_FAILURE); - return MediaManager::NotifyRecordingStatusChange(window->AsInner(), msg, mIsAudio, mIsVideo); + return MediaManager::NotifyRecordingStatusChange(window->AsInner(), msg); } } // namespace mozilla diff --git a/dom/media/MediaManager.h b/dom/media/MediaManager.h index a171deb88274..765b546e8dee 100644 --- a/dom/media/MediaManager.h +++ b/dom/media/MediaManager.h @@ -55,9 +55,10 @@ namespace ipc { class PrincipalInfo; } -class MediaManager; -class GetUserMediaCallbackMediaStreamListener; class GetUserMediaTask; +class GetUserMediaWindowListener; +class MediaManager; +class SourceListener; extern LogModule* GetMediaManagerLog(); #define MM_LOG(msg) MOZ_LOG(GetMediaManagerLog(), mozilla::LogLevel::Debug, msg) @@ -136,34 +137,29 @@ class GetUserMediaNotificationEvent: public Runnable enum GetUserMediaStatus { STARTING, STOPPING, - STOPPED_TRACK, }; - GetUserMediaNotificationEvent(GetUserMediaCallbackMediaStreamListener* aListener, - GetUserMediaStatus aStatus, - bool aIsAudio, bool aIsVideo, uint64_t aWindowID); + GetUserMediaNotificationEvent(GetUserMediaStatus aStatus, + uint64_t aWindowID); GetUserMediaNotificationEvent(GetUserMediaStatus aStatus, already_AddRefed aStream, - OnTracksAvailableCallback* aOnTracksAvailableCallback, - bool aIsAudio, bool aIsVideo, uint64_t aWindowID, + already_AddRefed>> aOnTracksAvailableCallback, + uint64_t aWindowID, already_AddRefed aError); virtual ~GetUserMediaNotificationEvent(); NS_IMETHOD Run() override; protected: - RefPtr mListener; // threadsafe + RefPtr mListener; // threadsafe RefPtr mStream; - nsAutoPtr mOnTracksAvailableCallback; + RefPtr>> mOnTracksAvailableCallback; GetUserMediaStatus mStatus; - bool mIsAudio; - bool mIsVideo; uint64_t mWindowID; RefPtr mOnFailure; }; typedef enum { - MEDIA_START, MEDIA_STOP, MEDIA_STOP_TRACK, MEDIA_DIRECT_LISTENERS, @@ -172,30 +168,30 @@ typedef enum { class ReleaseMediaOperationResource : public Runnable { public: - ReleaseMediaOperationResource(already_AddRefed aStream, - OnTracksAvailableCallback* aOnTracksAvailableCallback): + ReleaseMediaOperationResource( + already_AddRefed aStream, + already_AddRefed>> aOnTracksAvailableCallback): mStream(aStream), mOnTracksAvailableCallback(aOnTracksAvailableCallback) {} NS_IMETHOD Run() override {return NS_OK;} private: RefPtr mStream; - nsAutoPtr mOnTracksAvailableCallback; + RefPtr>> mOnTracksAvailableCallback; }; -typedef nsTArray > StreamListeners; -typedef nsClassHashtable WindowTable; +typedef nsRefPtrHashtable WindowTable; // we could add MediaManager if needed typedef void (*WindowListenerCallback)(MediaManager *aThis, uint64_t aWindowID, - StreamListeners *aListeners, + GetUserMediaWindowListener *aListener, void *aData); class MediaManager final : public nsIMediaManagerService, public nsIObserver ,public DeviceChangeCallback { - friend GetUserMediaCallbackMediaStreamListener; + friend SourceListener; public: static already_AddRefed GetInstance(); @@ -216,9 +212,7 @@ public: } static nsresult NotifyRecordingStatusChange(nsPIDOMWindowInner* aWindow, - const nsString& aMsg, - const bool& aIsAudio, - const bool& aIsVideo); + const nsString& aMsg); NS_DECL_THREADSAFE_ISUPPORTS NS_DECL_NSIOBSERVER @@ -226,17 +220,23 @@ public: media::Parent* GetNonE10sParent(); MediaEngine* GetBackend(uint64_t aWindowId = 0); - StreamListeners *GetWindowListeners(uint64_t aWindowId) { + + WindowTable *GetActiveWindows() { MOZ_ASSERT(NS_IsMainThread()); - return mActiveWindows.Get(aWindowId); + return &mActiveWindows; } + GetUserMediaWindowListener *GetWindowListener(uint64_t aWindowId) { + MOZ_ASSERT(NS_IsMainThread()); + return mActiveWindows.GetWeak(aWindowId); + } + void AddWindowID(uint64_t aWindowId, GetUserMediaWindowListener *aListener); void RemoveWindowID(uint64_t aWindowId); bool IsWindowStillActive(uint64_t aWindowId) { - return !!GetWindowListeners(aWindowId); + return !!GetWindowListener(aWindowId); } // Note: also calls aListener->Remove(), even if inactive void RemoveFromWindowList(uint64_t aWindowID, - GetUserMediaCallbackMediaStreamListener *aListener); + GetUserMediaWindowListener *aListener); nsresult GetUserMedia( nsPIDOMWindowInner* aWindow, @@ -293,12 +293,6 @@ private: bool aIsChrome, RefPtr>>& aSources); - StreamListeners* AddWindowID(uint64_t aWindowId); - WindowTable *GetActiveWindows() { - MOZ_ASSERT(NS_IsMainThread()); - return &mActiveWindows; - } - void GetPref(nsIPrefBranch *aBranch, const char *aPref, const char *aData, int32_t *aVal); void GetPrefBool(nsIPrefBranch *aBranch, const char *aPref, From 936d3605983b5446072f05ea25eacf759d002af4 Mon Sep 17 00:00:00 2001 From: Andreas Pehrson Date: Fri, 7 Apr 2017 15:30:43 +0200 Subject: [PATCH 39/68] Bug 1320994 - Remove chrome-test hacks that accomodated the misaligned MediaManager model. r=florian MozReview-Commit-ID: BS693gGyVhm --HG-- extra : rebase_source : ae85538f9150f8c272ca90ff43a9348b69f7bc22 --- .../browser_devices_get_user_media_screen.js | 4 +-- ...evices_get_user_media_unprompted_access.js | 6 ++--- ...t_user_media_unprompted_access_in_frame.js | 4 +-- ...er_media_unprompted_access_tear_off_tab.js | 1 - browser/base/content/test/webrtc/head.js | 26 +++++-------------- 5 files changed, 14 insertions(+), 27 deletions(-) diff --git a/browser/base/content/test/webrtc/browser_devices_get_user_media_screen.js b/browser/base/content/test/webrtc/browser_devices_get_user_media_screen.js index 796fd350a7f5..3d82772617b2 100644 --- a/browser/base/content/test/webrtc/browser_devices_get_user_media_screen.js +++ b/browser/base/content/test/webrtc/browser_devices_get_user_media_screen.js @@ -410,7 +410,7 @@ var gTests = [ yield check({video: true, audio: true}); info("Stop the camera, everything should stop."); - yield stopSharing("camera", false, true); + yield stopSharing("camera"); info("Now, share only the screen..."); indicator = promiseIndicatorWindow(); @@ -423,7 +423,7 @@ var gTests = [ yield check({video: true, audio: true, screen: "Screen"}); info("Stop the camera, this should stop everything."); - yield stopSharing("camera", false, true); + yield stopSharing("camera"); } }, diff --git a/browser/base/content/test/webrtc/browser_devices_get_user_media_unprompted_access.js b/browser/base/content/test/webrtc/browser_devices_get_user_media_unprompted_access.js index 929bcc9680fa..41c2f02286c1 100644 --- a/browser/base/content/test/webrtc/browser_devices_get_user_media_unprompted_access.js +++ b/browser/base/content/test/webrtc/browser_devices_get_user_media_unprompted_access.js @@ -62,7 +62,7 @@ var gTests = [ SitePermissions.remove(null, "microphone", gBrowser.selectedBrowser); // After closing all streams, gUM(audio+camera) causes a prompt. - yield closeStream(false, 0, 2); + yield closeStream(); promise = promisePopupNotificationShown("webRTC-shareDevices"); yield promiseRequestDevice(true, true); yield promise; @@ -169,7 +169,7 @@ var gTests = [ yield checkSharingUI({audio: false, video: true}); // close all streams - yield closeStream(false, 0, 2); + yield closeStream(); } }, @@ -241,7 +241,7 @@ var gTests = [ yield checkSharingUI({audio: true, video: false}); // close all streams - yield closeStream(false, 0, 2); + yield closeStream(); } } diff --git a/browser/base/content/test/webrtc/browser_devices_get_user_media_unprompted_access_in_frame.js b/browser/base/content/test/webrtc/browser_devices_get_user_media_unprompted_access_in_frame.js index bed6c6647e76..cb274b3d4720 100644 --- a/browser/base/content/test/webrtc/browser_devices_get_user_media_unprompted_access_in_frame.js +++ b/browser/base/content/test/webrtc/browser_devices_get_user_media_unprompted_access_in_frame.js @@ -57,7 +57,7 @@ var gTests = [ yield expectObserverCalled("recording-device-events"); // close the stream - yield closeStream(false, "frame1", 2); + yield closeStream(false, "frame1"); } }, @@ -197,7 +197,7 @@ var gTests = [ yield expectObserverCalled("recording-window-ended"); // close the stream - yield closeStream(false); + yield closeStream(); SitePermissions.remove(null, "screen", gBrowser.selectedBrowser); SitePermissions.remove(null, "camera", gBrowser.selectedBrowser); SitePermissions.remove(null, "microphone", gBrowser.selectedBrowser); diff --git a/browser/base/content/test/webrtc/browser_devices_get_user_media_unprompted_access_tear_off_tab.js b/browser/base/content/test/webrtc/browser_devices_get_user_media_unprompted_access_tear_off_tab.js index 1ae2b4eb38fb..acb41ee6769f 100644 --- a/browser/base/content/test/webrtc/browser_devices_get_user_media_unprompted_access_tear_off_tab.js +++ b/browser/base/content/test/webrtc/browser_devices_get_user_media_unprompted_access_tear_off_tab.js @@ -41,7 +41,6 @@ var gTests = [ yield Promise.all(promises); promises = [promiseObserverCalled("recording-device-events"), - promiseObserverCalled("recording-device-events"), promiseObserverCalled("recording-window-ended")]; yield BrowserTestUtils.closeWindow(win); yield Promise.all(promises); diff --git a/browser/base/content/test/webrtc/head.js b/browser/base/content/test/webrtc/head.js index 4c50be2822de..9ef1c6e00c67 100644 --- a/browser/base/content/test/webrtc/head.js +++ b/browser/base/content/test/webrtc/head.js @@ -215,7 +215,7 @@ function expectObserverCalled(aTopic) { }); } -function expectNoObserverCalled(aIgnoreDeviceEvents = false) { +function expectNoObserverCalled() { return new Promise(resolve => { let mm = _mm(); mm.addMessageListener("Test:ExpectNoObserverCalled:Reply", @@ -225,15 +225,7 @@ function expectNoObserverCalled(aIgnoreDeviceEvents = false) { if (!data[topic]) continue; - // If we are stopping tracks that were created from 2 different - // getUserMedia calls, the "recording-device-events" notification is - // fired twice on Windows and Mac, and intermittently twice on Linux. - if (topic == "recording-device-events" && aIgnoreDeviceEvents) { - todo(false, "Got " + data[topic] + " unexpected " + topic + - " notifications, see bug 1320994"); - } else { - is(data[topic], 0, topic + " notification unexpected"); - } + is(data[topic], 0, topic + " notification unexpected"); } resolve(); }); @@ -354,8 +346,7 @@ function getMediaCaptureState() { }); } -function* stopSharing(aType = "camera", aShouldKeepSharing = false, - aExpectDoubleRecordingEvent = false) { +function* stopSharing(aType = "camera", aShouldKeepSharing = false) { let promiseRecordingEvent = promiseObserverCalled("recording-device-events"); gIdentityHandler._identityBox.click(); let permissions = document.getElementById("identity-popup-permission-list"); @@ -372,7 +363,7 @@ function* stopSharing(aType = "camera", aShouldKeepSharing = false, if (!aShouldKeepSharing) yield expectObserverCalled("recording-window-ended"); - yield expectNoObserverCalled(aExpectDoubleRecordingEvent); + yield expectNoObserverCalled(); if (!aShouldKeepSharing) yield* checkNotSharing(); @@ -391,16 +382,13 @@ function promiseRequestDevice(aRequestAudio, aRequestVideo, aFrameId, aType, }); } -function* closeStream(aAlreadyClosed, aFrameId, aStreamCount = 1) { +function* closeStream(aAlreadyClosed, aFrameId) { yield expectNoObserverCalled(); let promises; if (!aAlreadyClosed) { - promises = []; - for (let i = 0; i < aStreamCount; i++) { - promises.push(promiseObserverCalled("recording-device-events")); - } - promises.push(promiseObserverCalled("recording-window-ended")); + promises = [promiseObserverCalled("recording-device-events"), + promiseObserverCalled("recording-window-ended")]; } info("closing the stream"); From 3294058c0ae14dfc374cb4ada5036617d12aad72 Mon Sep 17 00:00:00 2001 From: Andreas Pehrson Date: Wed, 29 Mar 2017 12:06:49 +0200 Subject: [PATCH 40/68] Bug 1320994 - Unify MediaManager logging macros. r=jib MozReview-Commit-ID: 14b6cXgOqP8 --HG-- extra : rebase_source : cb702280e9b3fcfcd09a11feeb9f8dfbdf115580 --- dom/media/MediaManager.cpp | 6 +++--- dom/media/MediaManager.h | 3 --- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/dom/media/MediaManager.cpp b/dom/media/MediaManager.cpp index c3c424edafb0..659f4502ed57 100644 --- a/dom/media/MediaManager.cpp +++ b/dom/media/MediaManager.cpp @@ -1277,7 +1277,7 @@ public: source->SetPullEnabled(true); source->AdvanceKnownTracksTime(STREAM_TIME_MAX); - MM_LOG(("started all sources")); + LOG(("started all sources")); // Forward onTracksAvailableCallback to GetUserMediaNotificationEvent, // because onTracksAvailableCallback needs to be added to domStream @@ -3449,7 +3449,7 @@ SourceListener::Remove() return; } - MM_LOG(("SourceListener removed on purpose, mFinished = %d", (int) mFinished)); + LOG(("SourceListener removed on purpose, mFinished = %d", (int) mFinished)); mRemoved = true; // RemoveListener is async, avoid races mWindowListener = nullptr; @@ -3662,7 +3662,7 @@ void SourceListener::NotifyRemoved() { MOZ_ASSERT(NS_IsMainThread()); - MM_LOG(("SourceListener removed, mFinished = %d", (int) mFinished)); + LOG(("SourceListener removed, mFinished = %d", (int) mFinished)); mRemoved = true; if (!mFinished) { diff --git a/dom/media/MediaManager.h b/dom/media/MediaManager.h index 765b546e8dee..11eafd8be238 100644 --- a/dom/media/MediaManager.h +++ b/dom/media/MediaManager.h @@ -60,9 +60,6 @@ class GetUserMediaWindowListener; class MediaManager; class SourceListener; -extern LogModule* GetMediaManagerLog(); -#define MM_LOG(msg) MOZ_LOG(GetMediaManagerLog(), mozilla::LogLevel::Debug, msg) - class MediaDevice : public nsIMediaDevice { public: From 0dcc290a85d031440d07d00b37f9f0129bcfde55 Mon Sep 17 00:00:00 2001 From: Andreas Pehrson Date: Wed, 29 Mar 2017 12:07:45 +0200 Subject: [PATCH 41/68] Bug 1320994 - Improve SourceListener logging. r=jib MozReview-Commit-ID: 1xuLwpMHMQB --HG-- extra : rebase_source : 0e964f4b41ff53c678fa015001156f3af27563db --- dom/media/MediaManager.cpp | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/dom/media/MediaManager.cpp b/dom/media/MediaManager.cpp index 659f4502ed57..b4129452c7fc 100644 --- a/dom/media/MediaManager.cpp +++ b/dom/media/MediaManager.cpp @@ -3377,6 +3377,8 @@ SourceListener::SourceListener() void SourceListener::Register(GetUserMediaWindowListener* aListener) { + LOG(("SourceListener %p registering with window listener %p", this, aListener)); + if (mWindowListener) { MOZ_ASSERT(false, "Already registered"); return; @@ -3400,6 +3402,8 @@ SourceListener::Activate(SourceMediaStream* aStream, { MOZ_ASSERT(NS_IsMainThread(), "Only call on main thread"); + LOG(("SourceListener %p activating audio=%p video=%p", this, aAudioDevice, aVideoDevice)); + if (mActivated) { MOZ_ASSERT(false, "Already activated"); return; @@ -3422,6 +3426,8 @@ SourceListener::Stop() return; } + LOG(("SourceListener %p stopping", this)); + // StopSharing() has some special logic, at least for audio capture. // It must be called when all tracks have stopped, before setting mStopped. StopSharing(); @@ -3449,7 +3455,7 @@ SourceListener::Remove() return; } - LOG(("SourceListener removed on purpose, mFinished = %d", (int) mFinished)); + LOG(("SourceListener %p removed on purpose, mFinished = %d", this, (int) mFinished)); mRemoved = true; // RemoveListener is async, avoid races mWindowListener = nullptr; @@ -3469,6 +3475,7 @@ SourceListener::StopTrack(TrackID aTrackID) switch (aTrackID) { case kAudioTrack: { + LOG(("SourceListener %p stopping audio track %d", this, aTrackID)); if (!mAudioDevice) { NS_ASSERTION(false, "Can't stop audio. No device."); return; @@ -3483,6 +3490,7 @@ SourceListener::StopTrack(TrackID aTrackID) break; } case kVideoTrack: { + LOG(("SourceListener %p stopping video track %d", this, aTrackID)); if (!mVideoDevice) { NS_ASSERTION(false, "Can't stop video. No device."); return; @@ -3509,6 +3517,7 @@ SourceListener::StopTrack(TrackID aTrackID) if ((!mAudioDevice || mAudioStopped) && (!mVideoDevice || mVideoStopped)) { + LOG(("SourceListener %p this was the last track stopped", this)); Stop(); } @@ -3529,6 +3538,8 @@ SourceListener::StopSharing() return; } + LOG(("SourceListener %p StopSharing", this)); + if (mVideoDevice && (mVideoDevice->GetMediaSource() == MediaSourceEnum::Screen || mVideoDevice->GetMediaSource() == MediaSourceEnum::Application || @@ -3654,6 +3665,8 @@ SourceListener::NotifyFinished() return; } + LOG(("SourceListener %p NotifyFinished", this)); + Stop(); // we know it's been activated mWindowListener->Remove(this); } From 2bb5df4de715f58af68f4a6decd215d12f952aaf Mon Sep 17 00:00:00 2001 From: Sebastian Hengst Date: Wed, 26 Apr 2017 14:12:13 +0200 Subject: [PATCH 42/68] Backed out changeset e084deb550c2 (bug 1335778) --- testing/marionette/driver.js | 20 +- .../tests/unit/test_click.py | 57 ++---- .../tests/unit/test_modal_dialogs.py | 2 +- .../marionette_harness/www/clicks.html | 2 - testing/marionette/interaction.js | 21 ++- testing/marionette/listener.js | 171 ++++-------------- 6 files changed, 61 insertions(+), 212 deletions(-) diff --git a/testing/marionette/driver.js b/testing/marionette/driver.js index bb256ae1811a..8f28e0faa845 100644 --- a/testing/marionette/driver.js +++ b/testing/marionette/driver.js @@ -2026,25 +2026,7 @@ GeckoDriver.prototype.clickElement = function* (cmd, resp) { // listen for it and then just send an error back. The person making the // call should be aware something isnt right and handle accordingly this.addFrameCloseListener("click"); - - let click = this.listener.clickElement({id: id, pageTimeout: this.timeouts.pageLoad}); - - // If a remoteness update interrupts our page load, this will never return - // We need to re-issue this request to correctly poll for readyState and - // send errors. - this.curBrowser.pendingCommands.push(() => { - let parameters = { - // TODO(ato): Bug 1242595 - command_id: this.listener.activeMessageId, - pageTimeout: this.timeouts.pageLoad, - startTime: new Date().getTime(), - }; - this.mm.broadcastAsyncMessage( - "Marionette:waitForPageLoaded" + this.curBrowser.curFrameId, - parameters); - }); - - yield click; + yield this.listener.clickElement(id); break; } }; diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_click.py b/testing/marionette/harness/marionette_harness/tests/unit/test_click.py index 3871cfed34ec..7779ee9a3f36 100644 --- a/testing/marionette/harness/marionette_harness/tests/unit/test_click.py +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_click.py @@ -8,7 +8,7 @@ from marionette_driver.by import By from marionette_driver import errors from marionette_driver.wait import Wait -from marionette_harness import MarionetteTestCase, run_if_e10s +from marionette_harness import MarionetteTestCase def inline(doc): @@ -97,7 +97,11 @@ class TestLegacyClick(MarionetteTestCase): test_html = self.marionette.absolute_url("clicks.html") self.marionette.navigate(test_html) self.marionette.find_element(By.LINK_TEXT, "333333").click() - self.marionette.find_element(By.ID, "username") + # Bug 1335778 - Missing implicit wait for page being loaded + Wait(self.marionette, timeout=self.marionette.timeout.page_load, + ignored_exceptions=errors.NoSuchElementException).until( + lambda m: m.find_element(By.ID, "username"), + message="Username field hasn't been found") self.assertEqual(self.marionette.title, "XHTML Test Page") def test_clicking_an_element_that_is_not_displayed_raises(self): @@ -111,7 +115,11 @@ class TestLegacyClick(MarionetteTestCase): test_html = self.marionette.absolute_url("clicks.html") self.marionette.navigate(test_html) self.marionette.find_element(By.ID, "overflowLink").click() - self.marionette.find_element(By.ID, "username") + # Bug 1335778 - Missing implicit wait for page being loaded + Wait(self.marionette, timeout=self.marionette.timeout.page_load, + ignored_exceptions=errors.NoSuchElementException).until( + lambda m: m.find_element(By.ID, "username"), + message="Username field hasn't been found") self.assertEqual(self.marionette.title, "XHTML Test Page") def test_scroll_into_view_near_end(self): @@ -269,46 +277,3 @@ class TestClick(TestLegacyClick): "does not have pointer events enabled"): button.click() self.assertFalse(self.marionette.execute_script("return window.clicked", sandbox=None)) - - -class TestClickNavigation(MarionetteTestCase): - - def setUp(self): - super(TestClickNavigation, self).setUp() - - def test_click_link_page_load(self): - test_html = self.marionette.absolute_url("clicks.html") - self.marionette.navigate(test_html) - self.marionette.find_element(By.LINK_TEXT, "333333").click() - self.assertNotEqual(self.marionette.get_url(), test_html) - self.assertEqual(self.marionette.title, "XHTML Test Page") - - def test_click_link_anchor(self): - test_html = self.marionette.absolute_url("clicks.html") - self.marionette.navigate(test_html) - self.marionette.find_element(By.ID, "anchor").click() - self.assertEqual(self.marionette.get_url(), "{}#".format(test_html)) - - def test_click_no_link(self): - test_html = self.marionette.absolute_url("clicks.html") - self.marionette.navigate(test_html) - self.marionette.find_element(By.ID, "showbutton").click() - self.assertEqual(self.marionette.get_url(), test_html) - - @run_if_e10s("Requires e10s mode enabled") - def test_click_remoteness_change(self): - test_html = self.marionette.absolute_url("clicks.html") - - self.marionette.navigate("about:robots") - self.marionette.navigate(test_html) - self.marionette.find_element(By.ID, "anchor") - self.marionette.navigate("about:robots") - with self.assertRaises(errors.NoSuchElementException): - self.marionette.find_element(By.ID, "anchor") - - self.marionette.go_back() - self.marionette.find_element(By.ID, "anchor") - self.marionette.find_element(By.ID, "history-back").click() - with self.assertRaises(errors.NoSuchElementException): - self.marionette.find_element(By.ID, "anchor") - diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_modal_dialogs.py b/testing/marionette/harness/marionette_harness/tests/unit/test_modal_dialogs.py index f9a93ec611ae..52020e00f016 100644 --- a/testing/marionette/harness/marionette_harness/tests/unit/test_modal_dialogs.py +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_modal_dialogs.py @@ -8,7 +8,7 @@ from marionette_driver import errors from marionette_driver.marionette import Alert from marionette_driver.wait import Wait -from marionette_harness import MarionetteTestCase, WindowManagerMixin +from marionette_harness import MarionetteTestCase, skip_if_e10s, WindowManagerMixin class BaseAlertTestCase(WindowManagerMixin, MarionetteTestCase): diff --git a/testing/marionette/harness/marionette_harness/www/clicks.html b/testing/marionette/harness/marionette_harness/www/clicks.html index bb2ffc931e21..e48cb1b2f7d6 100644 --- a/testing/marionette/harness/marionette_harness/www/clicks.html +++ b/testing/marionette/harness/marionette_harness/www/clicks.html @@ -17,8 +17,6 @@ I'm a normal link -Back in browser history -Forward in browser history I go to an anchor I open a window with javascript Click me diff --git a/testing/marionette/interaction.js b/testing/marionette/interaction.js index a691576b496a..dbad95631dc8 100644 --- a/testing/marionette/interaction.js +++ b/testing/marionette/interaction.js @@ -178,8 +178,8 @@ function* webdriverClickElement (el, a11y) { yield interaction.flushEventLoop(win); // step 10 - // if the click causes navigation, the post-navigation checks are - // handled by the load listener in listener.js + // TODO(ato): if the click causes navigation, + // run post-navigation checks } function* chromeClick (el, a11y) { @@ -291,19 +291,20 @@ interaction.selectOption = function (el) { * |win| has closed or been unloaded before the queue can be flushed. */ interaction.flushEventLoop = function* (win) { - return new Promise(resolve => { - let handleEvent = event => { - win.removeEventListener("beforeunload", this); - resolve(); - }; + let unloadEv; + return new Promise((resolve, reject) => { if (win.closed) { - resolve(); + reject(); return; } - win.addEventListener("beforeunload", handleEvent); - win.requestAnimationFrame(handleEvent); + unloadEv = reject; + win.addEventListener("unload", unloadEv, {once: true}); + + win.requestAnimationFrame(resolve); + }).then(() => { + win.removeEventListener("unload", unloadEv); }); }; diff --git a/testing/marionette/listener.js b/testing/marionette/listener.js index a4e907b2edb9..f85604944e48 100644 --- a/testing/marionette/listener.js +++ b/testing/marionette/listener.js @@ -117,10 +117,8 @@ var sandboxName = "default"; */ var loadListener = { command_id: null, - seenUnload: null, timeout: null, - timerPageLoad: null, - timerPageUnload: null, + timer: null, /** * Start listening for page unload/load events. @@ -138,81 +136,48 @@ var loadListener = { this.command_id = command_id; this.timeout = timeout; - this.seenUnload = false; - - this.timerPageLoad = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); - this.timerPageUnload = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); - // In case of a remoteness change, only wait the remaining time timeout = startTime + timeout - new Date().getTime(); if (timeout <= 0) { - this.notify(this.timerPageLoad); + this.notify(); return; } + this.timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + this.timer.initWithCallback(this, timeout, Ci.nsITimer.TYPE_ONE_SHOT); + if (waitForUnloaded) { addEventListener("hashchange", this, false); addEventListener("pagehide", this, false); - - // The event can only be received if the listener gets added to the - // currently selected frame. - curContainer.frame.addEventListener("beforeunload", this, false); - - Services.obs.addObserver(this, "outer-window-destroyed"); } else { addEventListener("DOMContentLoaded", loadListener, false); addEventListener("pageshow", loadListener, false); } - - this.timerPageLoad.initWithCallback(this, timeout, Ci.nsITimer.TYPE_ONE_SHOT); }, /** * Stop listening for page unload/load events. */ stop: function () { - if (this.timerPageLoad) { - this.timerPageLoad.cancel(); - } - - if (this.timerPageUnload) { - this.timerPageUnload.cancel(); + if (this.timer) { + this.timer.cancel(); + this.timer = null; } removeEventListener("hashchange", this); removeEventListener("pagehide", this); removeEventListener("DOMContentLoaded", this); removeEventListener("pageshow", this); - - // If the original content window, where the navigation was triggered, - // doesn't exist anymore, exceptions can be silently ignored. - try { - curContainer.frame.removeEventListener("beforeunload", this); - } catch (e if e.name == "TypeError") {} - - // In the case when the observer was added before a remoteness change, - // it will no longer be available. Exceptions can be silently ignored. - try { - Services.obs.removeObserver(this, "outer-window-destroyed"); - } catch (e) {} }, /** * Callback for registered DOM events. */ handleEvent: function (event) { - logger.debug(`Received DOM event "${event.type}" for "${event.originalTarget.baseURI}"`); - switch (event.type) { - case "beforeunload": - this.seenUnload = true; - break; - case "pagehide": if (event.originalTarget === curContainer.frame.document) { - this.seenUnload = true; - removeEventListener("hashchange", this); removeEventListener("pagehide", this); @@ -258,43 +223,9 @@ var loadListener = { * Callback for navigation timeout timer. */ notify: function (timer) { - switch (timer) { - // If the page unload timer is raised, ensure to properly stop the load - // listener, and return from the currently active command. - case this.timerPageUnload: - if (!this.seenUnload) { - logger.debug("Canceled page load listener because no navigation " + - "has been detected"); - this.stop(); - sendOk(this.command_id); - } - break; - - case this.timerPageLoad: - this.stop(); - sendError(new TimeoutError(`Timeout loading page after ${this.timeout}ms`), - this.command_id); - break; - } - }, - - observe: function (subject, topic, data) { - const winID = subject.QueryInterface(Components.interfaces.nsISupportsPRUint64).data; - const curWinID = curContainer.frame.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIDOMWindowUtils).outerWindowID; - - logger.debug(`Received observer notification "${topic}" for "${winID}"`); - - switch (topic) { - // In the case when the currently selected frame is about to close, - // there will be no further load events. Stop listening immediately. - case "outer-window-destroyed": - if (curWinID === winID) { - this.stop(); - sendOk(this.command_id); - } - break; - } + this.stop(); + sendError(new TimeoutError("Timeout loading page after " + this.timeout + "ms"), + this.command_id); }, /** @@ -320,15 +251,23 @@ var loadListener = { * @param {number} command_id * ID of the currently handled message between the driver and listener. * @param {number} pageTimeout - * Timeout in milliseconds the method has to wait for the page finished loading. - * @param {boolean=} loadEventExpected - * Optional flag, which indicates that navigate has to wait for the page - * finished loading. + * Timeout in milliseconds the method has to wait for the page being finished loading. * @param {string=} url * Optional URL, which is used to check if a page load is expected. */ - navigate: function (trigger, command_id, timeout, loadEventExpected = true, - useUnloadTimer = false) { + navigate: function (trigger, command_id, timeout, url = undefined) { + let loadEventExpected = true; + + if (typeof url == "string") { + try { + let requestedURL = new URL(url).toString(); + loadEventExpected = navigate.isLoadEventExpected(requestedURL); + } catch (e) { + sendError(new InvalidArgumentError("Malformed URL: " + e.message), command_id); + return; + } + } + if (loadEventExpected) { let startTime = new Date().getTime(); this.start(command_id, timeout, startTime, true); @@ -338,14 +277,8 @@ var loadListener = { yield trigger(); }).then(val => { - if (!loadEventExpected) { + if (!loadEventExpected) { sendOk(command_id); - return; - } - - // If requested setup a timer to detect a possible page load - if (useUnloadTimer) { - this.timerPageUnload.initWithCallback(this, 200, Ci.nsITimer.TYPE_ONE_SHOT); } }).catch(err => { @@ -353,6 +286,7 @@ var loadListener = { this.stop(); } + // Check why we do not raise an error if err is of type Event sendError(err, command_id); return; }); @@ -468,6 +402,7 @@ function removeMessageListenerId(messageName, handler) { var getTitleFn = dispatch(getTitle); var getPageSourceFn = dispatch(getPageSource); var getActiveElementFn = dispatch(getActiveElement); +var clickElementFn = dispatch(clickElement); var getElementAttributeFn = dispatch(getElementAttribute); var getElementPropertyFn = dispatch(getElementProperty); var getElementTextFn = dispatch(getElementText); @@ -522,7 +457,7 @@ function startListeners() { addMessageListenerId("Marionette:findElementContent", findElementContentFn); addMessageListenerId("Marionette:findElementsContent", findElementsContentFn); addMessageListenerId("Marionette:getActiveElement", getActiveElementFn); - addMessageListenerId("Marionette:clickElement", clickElement); + addMessageListenerId("Marionette:clickElement", clickElementFn); addMessageListenerId("Marionette:getElementAttribute", getElementAttributeFn); addMessageListenerId("Marionette:getElementProperty", getElementPropertyFn); addMessageListenerId("Marionette:getElementText", getElementTextFn); @@ -627,7 +562,7 @@ function deleteSession(msg) { removeMessageListenerId("Marionette:findElementContent", findElementContentFn); removeMessageListenerId("Marionette:findElementsContent", findElementsContentFn); removeMessageListenerId("Marionette:getActiveElement", getActiveElementFn); - removeMessageListenerId("Marionette:clickElement", clickElement); + removeMessageListenerId("Marionette:clickElement", clickElementFn); removeMessageListenerId("Marionette:getElementAttribute", getElementAttributeFn); removeMessageListenerId("Marionette:getElementProperty", getElementPropertyFn); removeMessageListenerId("Marionette:getElementText", getElementTextFn); @@ -1166,26 +1101,15 @@ function waitForPageLoaded(msg) { */ function get(msg) { let {command_id, pageTimeout, url} = msg.json; - let loadEventExpected = true; try { - if (typeof url == "string") { - try { - let requestedURL = new URL(url).toString(); - loadEventExpected = navigate.isLoadEventExpected(requestedURL); - } catch (e) { - sendError(new InvalidArgumentError("Malformed URL: " + e.message), command_id); - return; - } - } - // We need to move to the top frame before navigating sendSyncMessage("Marionette:switchedToFrame", {frameValue: null}); curContainer.frame = content; loadListener.navigate(() => { curContainer.frame.location = url; - }, command_id, pageTimeout, loadEventExpected); + }, command_id, pageTimeout, url); } catch (e) { sendError(e, command_id); @@ -1331,36 +1255,15 @@ function getActiveElement() { /** * Send click event to element. * - * @param {number} command_id - * ID of the currently handled message between the driver and listener. * @param {WebElement} id * Reference to the web element to click. - * @param {number} pageTimeout - * Timeout in milliseconds the method has to wait for the page being finished loading. */ -function clickElement(msg) { - let {command_id, id, pageTimeout} = msg.json; - - try { - let loadEventExpected = true; - - let target = getElementAttribute(id, "target"); - - if (target === "_blank") { - loadEventExpected = false; - } - - loadListener.navigate(() => { - return interaction.clickElement( - seenEls.get(id, curContainer), - capabilities.get("moz:accessibilityChecks"), - capabilities.get("specificationLevel") >= 1 - ); - }, command_id, pageTimeout, loadEventExpected, true); - - } catch (e) { - sendError(e, command_id); - } +function clickElement(id) { + let el = seenEls.get(id, curContainer); + return interaction.clickElement( + el, + capabilities.get("moz:accessibilityChecks"), + capabilities.get("specificationLevel") >= 1); } function getElementAttribute(id, name) { From daa08aca84284f26d284a29df477d62fe7b35bb5 Mon Sep 17 00:00:00 2001 From: Sebastian Hengst Date: Wed, 26 Apr 2017 14:12:17 +0200 Subject: [PATCH 43/68] Backed out changeset 0e6dee0ccecf (bug 1335778) --- .../marionette_harness/tests/unit/test_timeouts.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_timeouts.py b/testing/marionette/harness/marionette_harness/tests/unit/test_timeouts.py index 6961be880065..9268e7175d30 100644 --- a/testing/marionette/harness/marionette_harness/tests/unit/test_timeouts.py +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_timeouts.py @@ -24,9 +24,8 @@ class TestTimeouts(MarionetteTestCase): def test_page_timeout_fail(self): self.marionette.timeout.page_load = 0 - test_html = self.marionette.absolute_url("slow") - with self.assertRaises(MarionetteException): - self.marionette.navigate(test_html) + test_html = self.marionette.absolute_url("test.html") + self.assertRaises(MarionetteException, self.marionette.navigate, test_html) def test_page_timeout_pass(self): self.marionette.timeout.page_load = 60 @@ -37,11 +36,9 @@ class TestTimeouts(MarionetteTestCase): test_html = self.marionette.absolute_url("test.html") self.marionette.navigate(test_html) self.marionette.timeout.implicit = 1 - with self.assertRaises(NoSuchElementException): - self.marionette.find_element(By.ID, "I'm not on the page") + self.assertRaises(NoSuchElementException, self.marionette.find_element, By.ID, "I'm not on the page") self.marionette.timeout.implicit = 0 - with self.assertRaises(NoSuchElementException): - self.marionette.find_element(By.ID, "I'm not on the page") + self.assertRaises(NoSuchElementException, self.marionette.find_element, By.ID, "I'm not on the page") @skip_if_mobile("Bug 1317121 - android emulator is too slow") def test_search_timeout_found_settimeout(self): @@ -85,8 +82,7 @@ class TestTimeouts(MarionetteTestCase): test_html = self.marionette.absolute_url("test.html") self.marionette.navigate(test_html) self.marionette.timeout.script = 1 - with self.assertRaises(ScriptTimeoutException): - self.marionette.execute_async_script("var x = 1;") + self.assertRaises(ScriptTimeoutException, self.marionette.execute_async_script, "var x = 1;") def test_no_timeout_settimeout(self): test_html = self.marionette.absolute_url("test.html") From 58e94402a04dee672c5dc36ecdc199a5f75ea54c Mon Sep 17 00:00:00 2001 From: Sebastian Hengst Date: Wed, 26 Apr 2017 14:12:21 +0200 Subject: [PATCH 44/68] Backed out changeset 5589cde521f8 (bug 1335778) --- testing/marionette/listener.js | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/testing/marionette/listener.js b/testing/marionette/listener.js index f85604944e48..5cc7855def94 100644 --- a/testing/marionette/listener.js +++ b/testing/marionette/listener.js @@ -273,23 +273,19 @@ var loadListener = { this.start(command_id, timeout, startTime, true); } - return Task.spawn(function* () { - yield trigger(); - - }).then(val => { - if (!loadEventExpected) { - sendOk(command_id); - } - - }).catch(err => { + try { + trigger(); + } catch (e) { if (loadEventExpected) { this.stop(); } - - // Check why we do not raise an error if err is of type Event - sendError(err, command_id); + sendError(new UnknownCommandError(e.message), command_id); return; - }); + } + + if (!loadEventExpected) { + sendOk(command_id); + } }, } From 3a9a395200eff3ff41f209bd1ef8264cee2067ee Mon Sep 17 00:00:00 2001 From: Sebastian Hengst Date: Wed, 26 Apr 2017 14:13:09 +0200 Subject: [PATCH 45/68] Backed out changeset 4a9cc95b8a9f (bug 1335778) for failing firefox-ui-functional-local's test_notifications.py TestNotifications.test_addon_install_failed_notification. r=backout --- testing/marionette/listener.js | 1 - 1 file changed, 1 deletion(-) diff --git a/testing/marionette/listener.js b/testing/marionette/listener.js index 5cc7855def94..1d0899879896 100644 --- a/testing/marionette/listener.js +++ b/testing/marionette/listener.js @@ -1606,7 +1606,6 @@ function switchToFrame(msg) { curContainer.frame.focus(); } checkTimer.initWithCallback(checkLoad, 100, Ci.nsITimer.TYPE_ONE_SHOT); - return; } sendResponse(rv, command_id); From 2ef33d779d08cbaba7633ad93aa48a6ab0905e5b Mon Sep 17 00:00:00 2001 From: Hiroyuki Ikezoe Date: Wed, 26 Apr 2017 06:28:26 -0500 Subject: [PATCH 46/68] servo: Merge #16615 - Fix overflow in ::nth-child() (from hiikezoe:fix-overflow); r=SimonSapin This is a PR of https://bugzilla.mozilla.org/show_bug.cgi?id=1358754 --- - [X] `./mach build -d` does not report any errors - [X] `./mach test-tidy` does not report any errors - [X] We have a test case in mozilla-central. Source-Repo: https://github.com/servo/servo Source-Revision: 4800d2a47c93ada76d77fdb22fad634805713692 --HG-- extra : subtree_source : https%3A//hg.mozilla.org/projects/converted-servo-linear extra : subtree_revision : 312bbf2c3e99ce2f17dc876e73ec82a026832081 --- servo/components/selectors/matching.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/servo/components/selectors/matching.rs b/servo/components/selectors/matching.rs index 24e3345d2940..ada150890618 100644 --- a/servo/components/selectors/matching.rs +++ b/servo/components/selectors/matching.rs @@ -407,7 +407,7 @@ fn matches_generic_nth_child(element: &E, HAS_SLOW_SELECTOR_LATER_SIBLINGS }); - let mut index = 1; + let mut index: i32 = 1; let mut next_sibling = if is_from_end { element.next_sibling_element() } else { @@ -435,11 +435,13 @@ fn matches_generic_nth_child(element: &E, }; } - if a == 0 { - b == index - } else { - (index - b) / a >= 0 && - (index - b) % a == 0 + // Is there a non-negative integer n such that An+B=index? + match index.checked_sub(b) { + None => false, + Some(an) => match an.checked_div(a) { + Some(n) => n >= 0 && a * n == an, + None /* a == 0 */ => an == 0, + }, } } From dea9b3f094aa7f6c92cfc07f63af5198b2840a62 Mon Sep 17 00:00:00 2001 From: Ayodeji Oyewole Date: Sat, 1 Apr 2017 15:59:36 -0400 Subject: [PATCH 47/68] Bug 1351010 - Fix return statements in optimize.py; r=dustin MozReview-Commit-ID: ctgm1fw0Fo *** Bug 1351010 - Completely fixed; r?dustin MozReview-Commit-ID: HKoWcINVSnV --HG-- extra : rebase_source : 8ee85a34a6dfb6bc8fe40c4768d2713fffe4a67c --- taskcluster/taskgraph/optimize.py | 32 +++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/taskcluster/taskgraph/optimize.py b/taskcluster/taskgraph/optimize.py index b84ab1017ca5..f8a6558a8fd2 100644 --- a/taskcluster/taskgraph/optimize.py +++ b/taskcluster/taskgraph/optimize.py @@ -76,11 +76,11 @@ def optimize_task(task, params): for opt in task.optimizations: opt_type, args = opt[0], opt[1:] opt_fn = _optimizations[opt_type] - optimized, task_id = opt_fn(task, params, *args) - if optimized or task_id: - return optimized, task_id + opt_result = opt_fn(task, params, *args) + if opt_result: + return opt_result - return False, None + return False def annotate_task_graph(target_task_graph, params, do_not_optimize, @@ -118,7 +118,15 @@ def annotate_task_graph(target_task_graph, params, do_not_optimize, replacement_task_id = existing_tasks[label] # otherwise, examine the task itself (which may be an expensive operation) else: - optimized, replacement_task_id = optimize_task(task, params) + opt_result = optimize_task(task, params) + + # use opt_result to determine values for optimized, replacement_task_id + optimized = True + replacement_task_id = None + if opt_result is False: + optimized = False + elif opt_result is not True: + replacement_task_id = opt_result task.optimized = optimized task.task_id = replacement_task_id @@ -195,11 +203,11 @@ def opt_index_search(task, params, index_path): index_path, use_proxy=bool(os.environ.get('TASK_ID'))) - return True, task_id + return task_id or True except requests.exceptions.HTTPError: pass - return False, None + return False @optimization('seta') @@ -221,20 +229,20 @@ def opt_seta(task, params): params.get('pushdate'), bbb_task): # Always optimize away low-value tasks - return True, None + return True else: - return False, None + return False @optimization('files-changed') def opt_files_changed(task, params, file_patterns): # pushlog_id == -1 - this is the case when run from a cron.yml job if params.get('pushlog_id') == -1: - return True, None + return True changed = files_changed.check(params, file_patterns) if not changed: logger.debug('no files found matching a pattern in `when.files-changed` for ' + task.label) - return True, None - return False, None + return True + return False From 5bc9092ee81327a06f9415dfbc9a5ef831027421 Mon Sep 17 00:00:00 2001 From: Aryeh Gregor Date: Wed, 26 Apr 2017 15:13:44 +0300 Subject: [PATCH 48/68] Bug 1359780 - Always remove duplicates/whitespace in DOMTokenList methods r=masayuki Previously, replace() and toggle() would not always remove duplicates and whitespace from the DOM attribute in the case where they were no-ops (replacing a nonexistent token, force-toggling a token to its current state). Now they do. This matches the behavior of add() and remove(), and also replace() in one case (replacing an existing token with itself). This follows a spec change: https://github.com/whatwg/dom/pull/444 MozReview-Commit-ID: 7lDEQxOGxPV --HG-- extra : rebase_source : 842ff24c46681649affcedcba2623128fc6f5a7b --- dom/base/nsDOMTokenList.cpp | 35 +++++++++----- .../mozilla/tests/dom/classList.html | 48 +++++-------------- .../tests/dom/nodes/Element-classlist.html | 48 +++++-------------- .../nodes/MutationObserver-attributes.html | 10 ++-- 4 files changed, 51 insertions(+), 90 deletions(-) diff --git a/dom/base/nsDOMTokenList.cpp b/dom/base/nsDOMTokenList.cpp index 649e2184364e..fb6b665a3725 100644 --- a/dom/base/nsDOMTokenList.cpp +++ b/dom/base/nsDOMTokenList.cpp @@ -300,16 +300,29 @@ nsDOMTokenList::Toggle(const nsAString& aToken, AutoTArray tokens; (*tokens.AppendElement()).Rebind(aToken.Data(), aToken.Length()); - if (isPresent) { - if (!forceOn) { - RemoveInternal(attr, tokens); - isPresent = false; - } - } else { - if (!forceOff) { - AddInternal(attr, tokens); - isPresent = true; + if (isPresent && !forceOn) { + RemoveInternal(attr, tokens); + return false; + } + + if (!isPresent && !forceOff) { + AddInternal(attr, tokens); + return true; + } + + if (attr) { + // Remove duplicates and whitespace from attr + RemoveDuplicates(attr); + + nsAutoString resultStr; + for (uint32_t i = 0; i < attr->GetAtomCount(); i++) { + if (!resultStr.IsEmpty()) { + resultStr.AppendLiteral(" "); + } + resultStr.Append(nsDependentAtomString(attr->AtomAt(i))); } + + mElement->SetAttr(kNameSpaceID_None, mAttrAtom, resultStr, true); } return isPresent; @@ -375,9 +388,7 @@ nsDOMTokenList::ReplaceInternal(const nsAttrValue* aAttr, resultStr.Append(nsDependentAtomString(aAttr->AtomAt(i))); } - if (sawIt) { - mElement->SetAttr(kNameSpaceID_None, mAttrAtom, resultStr, true); - } + mElement->SetAttr(kNameSpaceID_None, mAttrAtom, resultStr, true); } bool diff --git a/testing/web-platform/mozilla/tests/dom/classList.html b/testing/web-platform/mozilla/tests/dom/classList.html index ecefb0096ec6..b19dbdd8adcd 100644 --- a/testing/web-platform/mozilla/tests/dom/classList.html +++ b/testing/web-platform/mozilla/tests/dom/classList.html @@ -254,12 +254,10 @@ function testClassList(e, desc) { function checkAdd(before, argument, after, expectedException) { checkModification(e, "add", argument, undefined, before, after, expectedException, desc); - // Also check force toggle - // XXX https://github.com/whatwg/dom/issues/443 - //if (!Array.isArray(argument)) { - // checkModification(e, "toggle", [argument, true], true, before, after, - // expectedException); - //} + if (!Array.isArray(argument)) { + checkModification(e, "toggle", [argument, true], true, before, after, + expectedException, desc); + } } checkAdd(null, "", null, "SyntaxError"); @@ -309,12 +307,10 @@ function testClassList(e, desc) { function checkRemove(before, argument, after, expectedException) { checkModification(e, "remove", argument, undefined, before, after, expectedException, desc); - // Also check force toggle - // XXX https://github.com/whatwg/dom/issues/443 - //if (!Array.isArray(argument)) { - // checkModification(e, "toggle", [argument, false], false, before, after, - // expectedException); - //} + if (!Array.isArray(argument)) { + checkModification(e, "toggle", [argument, false], false, before, after, + expectedException, desc); + } } checkRemove(null, "", null, "SyntaxError"); @@ -407,24 +403,6 @@ function testClassList(e, desc) { checkToggle("", undefined, true, "undefined"); - // tests for the force argument handling - // XXX Remove these if https://github.com/whatwg/dom/issues/443 is fixed - - function checkForceToggle(before, argument, force, expectedRes, after, expectedException) { - checkModification(e, "toggle", [argument, force], expectedRes, before, - after, expectedException, desc); - } - - checkForceToggle("", "a", true, true, "a"); - checkForceToggle("a", "a", true, true, "a"); - checkForceToggle("a", "b", true, true, "a b"); - checkForceToggle("a b", "b", true, true, "a b"); - checkForceToggle("", "a", false, false, ""); - checkForceToggle("a", "a", false, false, ""); - checkForceToggle("a", "b", false, false, "a"); - checkForceToggle("a b", "b", false, false, "a"); - - // replace() method function checkReplace(before, token, newToken, after, expectedException) { checkModification(e, "replace", [token, newToken], undefined, before, @@ -464,19 +442,15 @@ function testClassList(e, desc) { checkReplace("a", "A", "b", "a"); checkReplace("a b", "b", "A", "a A"); checkReplace("a b c", "d", "e", "a b c"); - // https://github.com/whatwg/dom/issues/443 checkReplace("a a a b", "a", "a", "a b"); - checkReplace("a a a b", "c", "d", "a a a b"); + checkReplace("a a a b", "c", "d", "a b"); checkReplace(null, "a", "b", null); checkReplace("", "a", "b", ""); - checkReplace(" ", "a", "b", " "); + checkReplace(" ", "a", "b", ""); checkReplace(" a \f", "a", "b", "b"); checkReplace("a b c", "b", "d", "a d c"); - // https://github.com/whatwg/dom/issues/442 - // Implementations agree on the first one here, so I test it, but disagree on - // the second, so no test until the spec decides what to say. checkReplace("a b c", "c", "a", "a b"); - //checkReplace("c b a", "c", "a", ???); + checkReplace("c b a", "c", "a", "a b"); checkReplace("a b a", "a", "c", "c b"); checkReplace("a b a", "b", "c", "a c"); checkReplace(" a a b", "a", "c", "c b"); diff --git a/testing/web-platform/tests/dom/nodes/Element-classlist.html b/testing/web-platform/tests/dom/nodes/Element-classlist.html index 5beee0942064..d87db5e9f24f 100644 --- a/testing/web-platform/tests/dom/nodes/Element-classlist.html +++ b/testing/web-platform/tests/dom/nodes/Element-classlist.html @@ -202,12 +202,10 @@ function testClassList(e, desc) { function checkAdd(before, argument, after, expectedException) { checkModification(e, "add", argument, undefined, before, after, expectedException, desc); - // Also check force toggle - // XXX https://github.com/whatwg/dom/issues/443 - //if (!Array.isArray(argument)) { - // checkModification(e, "toggle", [argument, true], true, before, after, - // expectedException); - //} + if (!Array.isArray(argument)) { + checkModification(e, "toggle", [argument, true], true, before, after, + expectedException, desc); + } } checkAdd(null, "", null, "SyntaxError"); @@ -257,12 +255,10 @@ function testClassList(e, desc) { function checkRemove(before, argument, after, expectedException) { checkModification(e, "remove", argument, undefined, before, after, expectedException, desc); - // Also check force toggle - // XXX https://github.com/whatwg/dom/issues/443 - //if (!Array.isArray(argument)) { - // checkModification(e, "toggle", [argument, false], false, before, after, - // expectedException); - //} + if (!Array.isArray(argument)) { + checkModification(e, "toggle", [argument, false], false, before, after, + expectedException, desc); + } } checkRemove(null, "", null, "SyntaxError"); @@ -355,24 +351,6 @@ function testClassList(e, desc) { checkToggle("", undefined, true, "undefined"); - // tests for the force argument handling - // XXX Remove these if https://github.com/whatwg/dom/issues/443 is fixed - - function checkForceToggle(before, argument, force, expectedRes, after, expectedException) { - checkModification(e, "toggle", [argument, force], expectedRes, before, - after, expectedException, desc); - } - - checkForceToggle("", "a", true, true, "a"); - checkForceToggle("a", "a", true, true, "a"); - checkForceToggle("a", "b", true, true, "a b"); - checkForceToggle("a b", "b", true, true, "a b"); - checkForceToggle("", "a", false, false, ""); - checkForceToggle("a", "a", false, false, ""); - checkForceToggle("a", "b", false, false, "a"); - checkForceToggle("a b", "b", false, false, "a"); - - // replace() method function checkReplace(before, token, newToken, after, expectedException) { checkModification(e, "replace", [token, newToken], undefined, before, @@ -412,19 +390,15 @@ function testClassList(e, desc) { checkReplace("a", "A", "b", "a"); checkReplace("a b", "b", "A", "a A"); checkReplace("a b c", "d", "e", "a b c"); - // https://github.com/whatwg/dom/issues/443 checkReplace("a a a b", "a", "a", "a b"); - checkReplace("a a a b", "c", "d", "a a a b"); + checkReplace("a a a b", "c", "d", "a b"); checkReplace(null, "a", "b", null); checkReplace("", "a", "b", ""); - checkReplace(" ", "a", "b", " "); + checkReplace(" ", "a", "b", ""); checkReplace(" a \f", "a", "b", "b"); checkReplace("a b c", "b", "d", "a d c"); - // https://github.com/whatwg/dom/issues/442 - // Implementations agree on the first one here, so I test it, but disagree on - // the second, so no test until the spec decides what to say. checkReplace("a b c", "c", "a", "a b"); - //checkReplace("c b a", "c", "a", ???); + checkReplace("c b a", "c", "a", "a b"); checkReplace("a b a", "a", "c", "c b"); checkReplace("a b a", "b", "c", "a c"); checkReplace(" a a b", "a", "c", "c b"); diff --git a/testing/web-platform/tests/dom/nodes/MutationObserver-attributes.html b/testing/web-platform/tests/dom/nodes/MutationObserver-attributes.html index 6721b7eecd13..359f993fff47 100644 --- a/testing/web-platform/tests/dom/nodes/MutationObserver-attributes.html +++ b/testing/web-platform/tests/dom/nodes/MutationObserver-attributes.html @@ -221,16 +221,18 @@ runMutationTest(n42, var n43 = document.getElementById('n43'); runMutationTest(n43, {"attributes":true, "attributeOldValue": true}, - [{type: "attributes", oldValue: "n43", attributeName: "id"}], + [{type: "attributes", oldValue: "c01 c02", attributeName: "class"}, + {type: "attributes", oldValue: "n43", attributeName: "id"}], function() { n43.classList.toggle("c03", false); n43.id = "n430"; }, - "attributes Element.classList.toggle: forced missing token removal no mutation"); + "attributes Element.classList.toggle: forced missing token removal no-op"); var n44 = document.getElementById('n44'); runMutationTest(n44, {"attributes":true, "attributeOldValue": true}, - [{type: "attributes", oldValue: "n44", attributeName: "id"}], + [{type: "attributes", oldValue: "c01 c02", attributeName: "class"}, + {type: "attributes", oldValue: "n44", attributeName: "id"}], function() { n44.classList.toggle("c01", true); n44.id = "n440"; }, - "attributes Element.classList.toggle: forced existing token addition no mutation"); + "attributes Element.classList.toggle: forced existing token addition no-op"); var n45 = document.getElementById('n45'); runMutationTest(n45, From 9cb9e8157ac14c9b0232b29d675c9e4df675691f Mon Sep 17 00:00:00 2001 From: Glenn Watson Date: Wed, 26 Apr 2017 07:56:32 -0500 Subject: [PATCH 49/68] servo: Merge #16612 - Update WR (groove/ridge borders, mix-blend-mode opts) (from glennw:update-wr-groove-ridge-2); r=jdm Source-Repo: https://github.com/servo/servo Source-Revision: c1b347794cc9812bf608fdc267c4c0e4ef5ef968 --HG-- extra : subtree_source : https%3A//hg.mozilla.org/projects/converted-servo-linear extra : subtree_revision : 838486b2f323b419e11b991e511397037f61bb65 --- servo/Cargo.lock | 54 ++++++++++---------- servo/components/layout/webrender_helpers.rs | 1 - servo/components/webvr/webvr_thread.rs | 7 +-- 3 files changed, 31 insertions(+), 31 deletions(-) diff --git a/servo/Cargo.lock b/servo/Cargo.lock index 047647d867da..9553a2bb23b7 100644 --- a/servo/Cargo.lock +++ b/servo/Cargo.lock @@ -289,7 +289,7 @@ dependencies = [ "num-traits 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)", "offscreen_gl_context 0.8.6 (registry+https://github.com/rust-lang/crates.io-index)", "servo_config 0.0.1", - "webrender_traits 0.35.0 (git+https://github.com/servo/webrender)", + "webrender_traits 0.36.0 (git+https://github.com/servo/webrender)", ] [[package]] @@ -303,7 +303,7 @@ dependencies = [ "ipc-channel 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", "serde 0.9.15 (registry+https://github.com/rust-lang/crates.io-index)", "serde_derive 0.9.11 (registry+https://github.com/rust-lang/crates.io-index)", - "webrender_traits 0.35.0 (git+https://github.com/servo/webrender)", + "webrender_traits 0.36.0 (git+https://github.com/servo/webrender)", ] [[package]] @@ -424,8 +424,8 @@ dependencies = [ "servo_url 0.0.1", "style_traits 0.0.1", "time 0.1.36 (registry+https://github.com/rust-lang/crates.io-index)", - "webrender 0.35.0 (git+https://github.com/servo/webrender)", - "webrender_traits 0.35.0 (git+https://github.com/servo/webrender)", + "webrender 0.36.0 (git+https://github.com/servo/webrender)", + "webrender_traits 0.36.0 (git+https://github.com/servo/webrender)", ] [[package]] @@ -460,7 +460,7 @@ dependencies = [ "servo_remutex 0.0.1", "servo_url 0.0.1", "style_traits 0.0.1", - "webrender_traits 0.35.0 (git+https://github.com/servo/webrender)", + "webrender_traits 0.36.0 (git+https://github.com/servo/webrender)", "webvr_traits 0.0.1", ] @@ -707,7 +707,7 @@ dependencies = [ "servo_geometry 0.0.1", "servo_url 0.0.1", "style_traits 0.0.1", - "webrender_traits 0.35.0 (git+https://github.com/servo/webrender)", + "webrender_traits 0.36.0 (git+https://github.com/servo/webrender)", "x11 2.12.1 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -993,7 +993,7 @@ dependencies = [ "time 0.1.36 (registry+https://github.com/rust-lang/crates.io-index)", "truetype 0.26.0 (registry+https://github.com/rust-lang/crates.io-index)", "unicode-script 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", - "webrender_traits 0.35.0 (git+https://github.com/servo/webrender)", + "webrender_traits 0.36.0 (git+https://github.com/servo/webrender)", "xi-unicode 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1072,7 +1072,7 @@ dependencies = [ "servo_url 0.0.1", "style_traits 0.0.1", "user32-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", - "webrender_traits 0.35.0 (git+https://github.com/servo/webrender)", + "webrender_traits 0.36.0 (git+https://github.com/servo/webrender)", "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", "x11 2.12.1 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1390,7 +1390,7 @@ dependencies = [ "style_traits 0.0.1", "unicode-bidi 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", "unicode-script 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", - "webrender_traits 0.35.0 (git+https://github.com/servo/webrender)", + "webrender_traits 0.36.0 (git+https://github.com/servo/webrender)", ] [[package]] @@ -1430,7 +1430,7 @@ dependencies = [ "servo_geometry 0.0.1", "servo_url 0.0.1", "style 0.0.1", - "webrender_traits 0.35.0 (git+https://github.com/servo/webrender)", + "webrender_traits 0.36.0 (git+https://github.com/servo/webrender)", ] [[package]] @@ -1444,7 +1444,7 @@ dependencies = [ "profile_traits 0.0.1", "script_traits 0.0.1", "servo_url 0.0.1", - "webrender_traits 0.35.0 (git+https://github.com/servo/webrender)", + "webrender_traits 0.36.0 (git+https://github.com/servo/webrender)", ] [[package]] @@ -1521,8 +1521,8 @@ dependencies = [ "style 0.0.1", "style_traits 0.0.1", "webdriver_server 0.0.1", - "webrender 0.35.0 (git+https://github.com/servo/webrender)", - "webrender_traits 0.35.0 (git+https://github.com/servo/webrender)", + "webrender 0.36.0 (git+https://github.com/servo/webrender)", + "webrender_traits 0.36.0 (git+https://github.com/servo/webrender)", "webvr 0.0.1", "webvr_traits 0.0.1", ] @@ -1671,7 +1671,7 @@ dependencies = [ "heapsize_derive 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", "serde 0.9.15 (registry+https://github.com/rust-lang/crates.io-index)", "serde_derive 0.9.11 (registry+https://github.com/rust-lang/crates.io-index)", - "webrender_traits 0.35.0 (git+https://github.com/servo/webrender)", + "webrender_traits 0.36.0 (git+https://github.com/servo/webrender)", ] [[package]] @@ -1715,7 +1715,7 @@ dependencies = [ "unicase 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "url 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "uuid 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "webrender_traits 0.35.0 (git+https://github.com/servo/webrender)", + "webrender_traits 0.36.0 (git+https://github.com/servo/webrender)", ] [[package]] @@ -1774,7 +1774,7 @@ dependencies = [ "servo_url 0.0.1", "url 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "uuid 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "webrender_traits 0.35.0 (git+https://github.com/servo/webrender)", + "webrender_traits 0.36.0 (git+https://github.com/servo/webrender)", ] [[package]] @@ -2301,7 +2301,7 @@ dependencies = [ "unicode-segmentation 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "url 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "uuid 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "webrender_traits 0.35.0 (git+https://github.com/servo/webrender)", + "webrender_traits 0.36.0 (git+https://github.com/servo/webrender)", "webvr 0.0.1", "webvr_traits 0.0.1", "xml5ever 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -2331,7 +2331,7 @@ dependencies = [ "selectors 0.18.0", "servo_url 0.0.1", "style 0.0.1", - "webrender_traits 0.35.0 (git+https://github.com/servo/webrender)", + "webrender_traits 0.36.0 (git+https://github.com/servo/webrender)", ] [[package]] @@ -2376,7 +2376,7 @@ dependencies = [ "style_traits 0.0.1", "time 0.1.36 (registry+https://github.com/rust-lang/crates.io-index)", "url 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "webrender_traits 0.35.0 (git+https://github.com/servo/webrender)", + "webrender_traits 0.36.0 (git+https://github.com/servo/webrender)", "webvr_traits 0.0.1", ] @@ -3167,8 +3167,8 @@ dependencies = [ [[package]] name = "webrender" -version = "0.35.0" -source = "git+https://github.com/servo/webrender#861fb7b5570d69de37d8a1f364f42f38c87cb2d3" +version = "0.36.0" +source = "git+https://github.com/servo/webrender#6609c796d66c68920f8596ba7c8b50ffa1490069" dependencies = [ "app_units 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "bincode 1.0.0-alpha6 (registry+https://github.com/rust-lang/crates.io-index)", @@ -3190,13 +3190,13 @@ dependencies = [ "thread_profiler 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "threadpool 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", "time 0.1.36 (registry+https://github.com/rust-lang/crates.io-index)", - "webrender_traits 0.35.0 (git+https://github.com/servo/webrender)", + "webrender_traits 0.36.0 (git+https://github.com/servo/webrender)", ] [[package]] name = "webrender_traits" -version = "0.35.0" -source = "git+https://github.com/servo/webrender#861fb7b5570d69de37d8a1f364f42f38c87cb2d3" +version = "0.36.0" +source = "git+https://github.com/servo/webrender#6609c796d66c68920f8596ba7c8b50ffa1490069" dependencies = [ "app_units 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "byteorder 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -3222,7 +3222,7 @@ dependencies = [ "msg 0.0.1", "script_traits 0.0.1", "servo_config 0.0.1", - "webrender_traits 0.35.0 (git+https://github.com/servo/webrender)", + "webrender_traits 0.36.0 (git+https://github.com/servo/webrender)", "webvr_traits 0.0.1", ] @@ -3564,8 +3564,8 @@ dependencies = [ "checksum void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" "checksum walkdir 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)" = "bb08f9e670fab86099470b97cd2b252d6527f0b3cc1401acdb595ffc9dd288ff" "checksum webdriver 0.22.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d548aabf87411b1b4ba91fd07eacd8b238135c7131a452b8a9f6386209167e18" -"checksum webrender 0.35.0 (git+https://github.com/servo/webrender)" = "" -"checksum webrender_traits 0.35.0 (git+https://github.com/servo/webrender)" = "" +"checksum webrender 0.36.0 (git+https://github.com/servo/webrender)" = "" +"checksum webrender_traits 0.36.0 (git+https://github.com/servo/webrender)" = "" "checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" "checksum winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" "checksum ws 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "04614a58714f3fd4a8b1da4bcae9f031c532d35988c3d39627619248113f8be8" diff --git a/servo/components/layout/webrender_helpers.rs b/servo/components/layout/webrender_helpers.rs index 92012d71cb73..723e33287024 100644 --- a/servo/components/layout/webrender_helpers.rs +++ b/servo/components/layout/webrender_helpers.rs @@ -416,7 +416,6 @@ impl WebRenderDisplayItemConverter for DisplayItem { builder.push_stacking_context(stacking_context.scroll_policy, stacking_context.bounds.to_rectf(), - stacking_context.z_index, transform, webrender_traits::TransformStyle::Flat, perspective, diff --git a/servo/components/webvr/webvr_thread.rs b/servo/components/webvr/webvr_thread.rs index 6d16a23b83a7..3f268ab3cbb0 100644 --- a/servo/components/webvr/webvr_thread.rs +++ b/servo/components/webvr/webvr_thread.rs @@ -12,6 +12,7 @@ use std::collections::{HashMap, HashSet}; use std::sync::mpsc; use std::sync::mpsc::{Receiver, Sender}; use webrender_traits; +use webrender_traits::DeviceIntSize; use webvr_traits::{WebVRMsg, WebVRResult}; use webvr_traits::webvr::*; @@ -329,7 +330,7 @@ impl WebVRCompositorHandler { impl webrender_traits::VRCompositorHandler for WebVRCompositorHandler { #[allow(unsafe_code)] - fn handle(&mut self, cmd: webrender_traits::VRCompositorCommand, texture_id: Option) { + fn handle(&mut self, cmd: webrender_traits::VRCompositorCommand, texture: Option<(u32, DeviceIntSize)>) { match cmd { webrender_traits::VRCompositorCommand::Create(compositor_id) => { self.create_compositor(compositor_id); @@ -347,12 +348,12 @@ impl webrender_traits::VRCompositorHandler for WebVRCompositorHandler { } webrender_traits::VRCompositorCommand::SubmitFrame(compositor_id, left_bounds, right_bounds) => { if let Some(compositor) = self.compositors.get(&compositor_id) { - if let Some(texture_id) = texture_id { + if let Some((texture_id, size)) = texture { let layer = VRLayer { texture_id: texture_id, left_bounds: left_bounds, right_bounds: right_bounds, - texture_size: None + texture_size: Some((size.width as u32, size.height as u32)) }; unsafe { (*compositor.0).submit_frame(&layer); From 36ea32d206a0051c56c03474783b3edf8a13666a Mon Sep 17 00:00:00 2001 From: Mike Conley Date: Tue, 25 Apr 2017 18:03:12 -0400 Subject: [PATCH 50/68] Bug 1357098 - Fix off-by-one error in SessionStore when preparing deferred restore data. r=mikedeboer Bug 588482 made it possible to restore pinned tabs in windows before restoring the rest of the tabs. Part of this involves extracting the pinned tab data from the last session, and make it the default state to restore. The rest can be restored later. If one of the pinned tabs in a window were selected, the code that was preparing the default state for that window set the selection value by calculating the length of the tabs array (before adding the selected one), and adding 2. Presumably, 1 count is because of the tab we're about to add, and another count is due to how the "selected" value in SessionStore is 1-indexed (where "0" means, "don't change the selection"). This is an off-by-one error though, since the tabs array length already cancels out the need for the extra "1-index" difference. It is sufficient to just add 1 to the length of the tabs array to calculate the selected value. This appears to have been a bug for a while, but was covered up by the fact that the old tab selection code exited early if the 0-indexed value of the selected tab was not within the length of the tabs array. Bug 1351677 uncovered this bug by removing that second check. This patch fixes the off-by-one error, and also puts a check back in to ensure that the tab we're going to select is at a valid index. MozReview-Commit-ID: 9WbbU0vUJHG --HG-- extra : rebase_source : cd5f4432c23084b672a844da40879bf53e393596 --- browser/components/sessionstore/SessionStore.jsm | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/browser/components/sessionstore/SessionStore.jsm b/browser/components/sessionstore/SessionStore.jsm index 2c99723e8cc6..803c3851b452 100644 --- a/browser/components/sessionstore/SessionStore.jsm +++ b/browser/components/sessionstore/SessionStore.jsm @@ -3334,7 +3334,7 @@ var SessionStoreInternal = { } } - if (selectTab > 0) { + if (selectTab > 0 && selectTab <= tabs.length) { // The state we're restoring wants to select a particular tab. This // implies that we're overwriting tabs. let currentIndex = tabbrowser.tabContainer.selectedIndex; @@ -4404,8 +4404,8 @@ var SessionStoreInternal = { if (tIndex + 1 < window.selected) window.selected -= 1; else if (tIndex + 1 == window.selected) - pinnedWindowState.selected = pinnedWindowState.tabs.length + 2; - // + 2 because the tab isn't actually in the array yet + pinnedWindowState.selected = pinnedWindowState.tabs.length + 1; + // + 1 because the tab isn't actually in the array yet // Now add the pinned tab to our window pinnedWindowState.tabs = From 8cb6dfec68ec1d5f9868872f9630c4e885321cac Mon Sep 17 00:00:00 2001 From: Sebastian Hengst Date: Wed, 26 Apr 2017 16:19:19 +0200 Subject: [PATCH 51/68] Backed out changeset e5073b47f5c8 (bug 1351010) for failing taskcluster/taskgraph/test/test_optimize.py. r=backout --- taskcluster/taskgraph/optimize.py | 32 ++++++++++++------------------- 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/taskcluster/taskgraph/optimize.py b/taskcluster/taskgraph/optimize.py index f8a6558a8fd2..b84ab1017ca5 100644 --- a/taskcluster/taskgraph/optimize.py +++ b/taskcluster/taskgraph/optimize.py @@ -76,11 +76,11 @@ def optimize_task(task, params): for opt in task.optimizations: opt_type, args = opt[0], opt[1:] opt_fn = _optimizations[opt_type] - opt_result = opt_fn(task, params, *args) - if opt_result: - return opt_result + optimized, task_id = opt_fn(task, params, *args) + if optimized or task_id: + return optimized, task_id - return False + return False, None def annotate_task_graph(target_task_graph, params, do_not_optimize, @@ -118,15 +118,7 @@ def annotate_task_graph(target_task_graph, params, do_not_optimize, replacement_task_id = existing_tasks[label] # otherwise, examine the task itself (which may be an expensive operation) else: - opt_result = optimize_task(task, params) - - # use opt_result to determine values for optimized, replacement_task_id - optimized = True - replacement_task_id = None - if opt_result is False: - optimized = False - elif opt_result is not True: - replacement_task_id = opt_result + optimized, replacement_task_id = optimize_task(task, params) task.optimized = optimized task.task_id = replacement_task_id @@ -203,11 +195,11 @@ def opt_index_search(task, params, index_path): index_path, use_proxy=bool(os.environ.get('TASK_ID'))) - return task_id or True + return True, task_id except requests.exceptions.HTTPError: pass - return False + return False, None @optimization('seta') @@ -229,20 +221,20 @@ def opt_seta(task, params): params.get('pushdate'), bbb_task): # Always optimize away low-value tasks - return True + return True, None else: - return False + return False, None @optimization('files-changed') def opt_files_changed(task, params, file_patterns): # pushlog_id == -1 - this is the case when run from a cron.yml job if params.get('pushlog_id') == -1: - return True + return True, None changed = files_changed.check(params, file_patterns) if not changed: logger.debug('no files found matching a pattern in `when.files-changed` for ' + task.label) - return True - return False + return True, None + return False, None From 966c8d03f541d5806cab70b10c8b9cef7b50db67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emilio=20Cobos=20=C3=81lvarez?= Date: Wed, 26 Apr 2017 08:59:33 -0500 Subject: [PATCH 52/68] servo: Merge #16618 - script: Ensure we don't suppress reflows when stylesheets are dirty (from emilio:dirty-doc); r=nox I suspect this will prevent some intermittentness in #16617 Source-Repo: https://github.com/servo/servo Source-Revision: 4e70e10ed15d4e6a7b7d0ed1e48135cdb941692a --HG-- extra : subtree_source : https%3A//hg.mozilla.org/projects/converted-servo-linear extra : subtree_revision : d69976cdafd1b296b0910fb14a9ebd6ff6af84ae --- servo/components/script/dom/document.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/servo/components/script/dom/document.rs b/servo/components/script/dom/document.rs index f5ffd9739364..ab124ba1734d 100644 --- a/servo/components/script/dom/document.rs +++ b/servo/components/script/dom/document.rs @@ -492,6 +492,7 @@ impl Document { // FIXME: This should check the dirty bit on the document, // not the document element. Needs some layout changes to make // that workable. + self.stylesheets_changed_since_reflow.get() || match self.GetDocumentElement() { Some(root) => { root.upcast::().has_dirty_descendants() || From aec49bff0f3485af55d01907fcc32123b5172562 Mon Sep 17 00:00:00 2001 From: Aryeh Gregor Date: Wed, 26 Apr 2017 15:50:36 +0300 Subject: [PATCH 53/68] Bug 1359453 - Use standard exception for selection.removeRange() r=masayuki The test change is already upstream: https://github.com/w3c/web-platform-tests/pull/5686 The spec change has not officially been accepted yet as of this writing, possibly delayed for IPR review: https://github.com/w3c/selection-api/pull/87 MozReview-Commit-ID: 3auwvwP4X72 --HG-- extra : rebase_source : 0396cd7c454b234598d77427bc20c6ea0645dc2e --- layout/generic/nsSelection.cpp | 2 +- .../meta/selection/removeRange.html.ini | 86 ------------------- .../tests/selection/removeRange.html | 4 +- 3 files changed, 4 insertions(+), 88 deletions(-) delete mode 100644 testing/web-platform/meta/selection/removeRange.html.ini diff --git a/layout/generic/nsSelection.cpp b/layout/generic/nsSelection.cpp index 99c9c8e6ed3e..988cb4977b68 100644 --- a/layout/generic/nsSelection.cpp +++ b/layout/generic/nsSelection.cpp @@ -4076,7 +4076,7 @@ Selection::RemoveItem(nsRange* aItem) } } if (idx < 0) - return NS_ERROR_INVALID_ARG; + return NS_ERROR_DOM_NOT_FOUND_ERR; mRanges.RemoveElementAt(idx); aItem->SetSelection(nullptr); diff --git a/testing/web-platform/meta/selection/removeRange.html.ini b/testing/web-platform/meta/selection/removeRange.html.ini deleted file mode 100644 index 7285e6bd9244..000000000000 --- a/testing/web-platform/meta/selection/removeRange.html.ini +++ /dev/null @@ -1,86 +0,0 @@ -[removeRange.html] - type: testharness - [removeRange() with Range 0] - expected: FAIL - - [removeRange() with Range 1] - expected: FAIL - - [removeRange() with Range 2] - expected: FAIL - - [removeRange() with Range 3] - expected: FAIL - - [removeRange() with Range 4] - expected: FAIL - - [removeRange() with Range 5] - expected: FAIL - - [removeRange() with Range 6] - expected: FAIL - - [removeRange() with Range 7] - expected: FAIL - - [removeRange() with Range 14] - expected: FAIL - - [removeRange() with Range 15] - expected: FAIL - - [removeRange() with Range 16] - expected: FAIL - - [removeRange() with Range 17] - expected: FAIL - - [removeRange() with Range 18] - expected: FAIL - - [removeRange() with Range 22] - expected: FAIL - - [removeRange() with Range 23] - expected: FAIL - - [removeRange() with Range 26] - expected: FAIL - - [removeRange() with Range 27] - expected: FAIL - - [removeRange() with Range 28] - expected: FAIL - - [removeRange() with Range 29] - expected: FAIL - - [removeRange() with Range 30] - expected: FAIL - - [removeRange() with Range 31] - expected: FAIL - - [removeRange() with Range 32] - expected: FAIL - - [removeRange() with Range 34] - expected: FAIL - - [removeRange() with Range 35] - expected: FAIL - - [removeRange() with Range 36] - expected: FAIL - - [removeRange() with Range 37] - expected: FAIL - - [removeRange() with Range 38] - expected: FAIL - - [removeRange() with Range 39] - expected: FAIL - diff --git a/testing/web-platform/tests/selection/removeRange.html b/testing/web-platform/tests/selection/removeRange.html index 723f5105ca57..6ddccc66f225 100644 --- a/testing/web-platform/tests/selection/removeRange.html +++ b/testing/web-platform/tests/selection/removeRange.html @@ -30,7 +30,9 @@ testRanges.forEach(function(rangeData, index) { var equivalentRange = ownerDocument(endpoints[0]).createRange(); equivalentRange.setStart(endpoints[0], endpoints[1]); equivalentRange.setEnd(endpoints[2], endpoints[3]); - selection.removeRange(equivalentRange); + assert_throws("NotFoundError", + function() { selection.removeRange(equivalentRange) }, + "Removing a different range should throw"); assert_equals(selection.rangeCount, 1, 'Equivalent Range should not remove the registered Range.'); }, 'removeRange() with Range ' + index); From 1408fb1a9d642118855ac68acfd000693517f396 Mon Sep 17 00:00:00 2001 From: Andrew Swan Date: Tue, 25 Apr 2017 16:37:53 -0700 Subject: [PATCH 54/68] Bug 1359476 Don't disable non-extensions due to MPC=false r=rhelmer MozReview-Commit-ID: 9AHyWSbdwuk --HG-- extra : rebase_source : 23c31df7278bce6de92c17061060114cba1d9259 --- .../extensions/internal/XPIProvider.jsm | 3 ++- .../xpcshell/test_multiprocessCompatible.js | 25 +++++++++++++++++-- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/toolkit/mozapps/extensions/internal/XPIProvider.jsm b/toolkit/mozapps/extensions/internal/XPIProvider.jsm index 6c352563483b..772fc2dae105 100644 --- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm +++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm @@ -808,7 +808,8 @@ function isUsableAddon(aAddon) { return false; } - if (!ALLOW_NON_MPC && aAddon.multiprocessCompatible !== true) { + if (!ALLOW_NON_MPC && aAddon.type == "extension" && + aAddon.multiprocessCompatible !== true) { logger.warn(`disabling ${aAddon.id} since it is not multiprocess compatible`); return false; } diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_multiprocessCompatible.js b/toolkit/mozapps/extensions/test/xpcshell/test_multiprocessCompatible.js index f9addae55862..d77d27077668 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/test_multiprocessCompatible.js +++ b/toolkit/mozapps/extensions/test/xpcshell/test_multiprocessCompatible.js @@ -109,6 +109,7 @@ for (let bootstrap of [false, true]) { add_task(async function test_disable() { const ID_MPC = "mpc@tests.mozilla.org"; const ID_NON_MPC = "non-mpc@tests.mozilla.org"; + const ID_DICTIONARY = "dictionary@tests.mozilla.org"; let addonData = { name: "Test Add-on", @@ -129,6 +130,17 @@ add_task(async function test_disable() { id: ID_NON_MPC, multiprocessCompatible: false, }, addonData)); + let xpi3 = createTempXPIFile({ + id: ID_DICTIONARY, + name: "Test Dictionary", + version: "1.0", + type: "64", + targetApplications: [{ + id: "xpcshell@tests.mozilla.org", + minVersion: "1", + maxVersion: "1" + }] + }); async function testOnce(initialAllow) { if (initialAllow !== undefined) { @@ -137,9 +149,10 @@ add_task(async function test_disable() { let install1 = await AddonManager.getInstallForFile(xpi1); let install2 = await AddonManager.getInstallForFile(xpi2); - await promiseCompleteAllInstalls([install1, install2]); + let install3 = await AddonManager.getInstallForFile(xpi3); + await promiseCompleteAllInstalls([install1, install2, install3]); - let [addon1, addon2] = await AddonManager.getAddonsByIDs([ID_MPC, ID_NON_MPC]); + let [addon1, addon2, addon3] = await AddonManager.getAddonsByIDs([ID_MPC, ID_NON_MPC, ID_DICTIONARY]); do_check_neq(addon1, null); do_check_eq(addon1.multiprocessCompatible, true); do_check_eq(addon1.appDisabled, false); @@ -148,6 +161,9 @@ add_task(async function test_disable() { do_check_eq(addon2.multiprocessCompatible, false); do_check_eq(addon2.appDisabled, initialAllow === false); + do_check_neq(addon3, null); + do_check_eq(addon3.appDisabled, false); + // Flip the allow-non-mpc preference let newValue = (initialAllow === true) ? false : true; Services.prefs.setBoolPref(NON_MPC_PREF, newValue); @@ -158,14 +174,19 @@ add_task(async function test_disable() { // The non-mpc extension should become disabled if we don't allow non-mpc do_check_eq(addon2.appDisabled, !newValue); + // A non-extension (eg a dictionary) should not become disabled + do_check_eq(addon3.appDisabled, false); + // Flip the pref back and check appDisabled Services.prefs.setBoolPref(NON_MPC_PREF, !newValue); do_check_eq(addon1.appDisabled, false); do_check_eq(addon2.appDisabled, newValue); + do_check_eq(addon3.appDisabled, false); addon1.uninstall(); addon2.uninstall(); + addon3.uninstall(); } await testOnce(undefined); From 42086632ef77028d59a9152e56b2b51a40c28610 Mon Sep 17 00:00:00 2001 From: Boris Chiou Date: Wed, 26 Apr 2017 09:33:08 -0500 Subject: [PATCH 55/68] servo: Merge #16614 - stylo: Bug 1357357 - Make the parser of transition-property match the spec (from BorisChiou:stylo/transition/transition_property); r=emilio These are interdependent patches of Bug 1357357. We add one more arm, TransitionProperty::Unsupported, which stores the string of non-animatable, custom, or unrecognized property, so we can parse these kinds of properties and serialize them correctly. This is necessary because we need to start transitions even though some transition-properties are non-animatable, custom, or unrecognized. --- - [X] `./mach build -d` does not report any errors - [X] `./mach test-tidy` does not report any errors - [X] These changes fix [Bug 1357357](https://bugzilla.mozilla.org/show_bug.cgi?id=1357357). - [X] There are tests for these changes. Source-Repo: https://github.com/servo/servo Source-Revision: 8f1356de60d0f0b1571b0e84b929ba7d8054177f --HG-- extra : subtree_source : https%3A//hg.mozilla.org/projects/converted-servo-linear extra : subtree_revision : caa7fc297a7cf88a430802eecaa53b5854354cef --- servo/components/style/animation.rs | 18 ++++--- servo/components/style/build_gecko.rs | 1 + servo/components/style/dom.rs | 2 +- servo/components/style/gecko/wrapper.rs | 20 ++++---- .../style/gecko_bindings/bindings.rs | 6 +++ servo/components/style/keyframes.rs | 2 +- .../components/style/properties/gecko.mako.rs | 34 ++++++++++++- .../helpers/animated_properties.mako.rs | 49 +++++++++++++------ .../style/properties/properties.mako.rs | 4 +- .../style/properties/shorthand/box.mako.rs | 4 +- servo/ports/geckolib/glue.rs | 6 +-- .../unit/style/parsing/transition_property.rs | 40 ++++++++++++--- 12 files changed, 139 insertions(+), 47 deletions(-) diff --git a/servo/components/style/animation.rs b/servo/components/style/animation.rs index 2229d4855911..16c7dab364d2 100644 --- a/servo/components/style/animation.rs +++ b/servo/components/style/animation.rs @@ -272,9 +272,13 @@ impl PropertyAnimation { let timing_function = box_style.transition_timing_function_mod(transition_index); let duration = box_style.transition_duration_mod(transition_index); + if let TransitionProperty::Unsupported(_) = transition_property { + return result + } + if transition_property.is_shorthand() { return transition_property.longhands().iter().filter_map(|transition_property| { - PropertyAnimation::from_transition_property(*transition_property, + PropertyAnimation::from_transition_property(transition_property, timing_function, duration, old_style, @@ -284,7 +288,7 @@ impl PropertyAnimation { if transition_property != TransitionProperty::All { if let Some(property_animation) = - PropertyAnimation::from_transition_property(transition_property, + PropertyAnimation::from_transition_property(&transition_property, timing_function, duration, old_style, @@ -296,7 +300,7 @@ impl PropertyAnimation { TransitionProperty::each(|transition_property| { if let Some(property_animation) = - PropertyAnimation::from_transition_property(transition_property, + PropertyAnimation::from_transition_property(&transition_property, timing_function, duration, old_style, @@ -308,15 +312,15 @@ impl PropertyAnimation { result } - fn from_transition_property(transition_property: TransitionProperty, + fn from_transition_property(transition_property: &TransitionProperty, timing_function: TransitionTimingFunction, duration: Time, old_style: &ComputedValues, new_style: &ComputedValues) -> Option { debug_assert!(!transition_property.is_shorthand() && - transition_property != TransitionProperty::All); - let animated_property = AnimatedProperty::from_transition_property(&transition_property, + transition_property != &TransitionProperty::All); + let animated_property = AnimatedProperty::from_transition_property(transition_property, old_style, new_style); @@ -702,7 +706,7 @@ pub fn update_style_for_animation(context: &SharedStyleContext, for transition_property in &animation.properties_changed { debug!("update_style_for_animation: scanning prop {:?} for animation \"{}\"", transition_property, name); - match PropertyAnimation::from_transition_property(*transition_property, + match PropertyAnimation::from_transition_property(transition_property, timing_function, Time::from_seconds(relative_duration as f32), &from_style, diff --git a/servo/components/style/build_gecko.rs b/servo/components/style/build_gecko.rs index 00aac6d8526f..9d170e641058 100644 --- a/servo/components/style/build_gecko.rs +++ b/servo/components/style/build_gecko.rs @@ -659,6 +659,7 @@ mod bindings { "StyleBasicShape", "StyleBasicShapeType", "StyleShapeSource", + "StyleTransition", "nsCSSFontFaceRule", "nsCSSKeyword", "nsCSSPropertyID", diff --git a/servo/components/style/dom.rs b/servo/components/style/dom.rs index ffc9f67ef8d5..645db055be75 100644 --- a/servo/components/style/dom.rs +++ b/servo/components/style/dom.rs @@ -520,7 +520,7 @@ pub trait TElement : Eq + PartialEq + Debug + Hash + Sized + Copy + Clone + /// Returns true if we need to update transitions for the specified property on this element. #[cfg(feature = "gecko")] fn needs_transitions_update_per_property(&self, - property: TransitionProperty, + property: &TransitionProperty, combined_duration: f32, before_change_style: &Arc, after_change_style: &Arc, diff --git a/servo/components/style/gecko/wrapper.rs b/servo/components/style/gecko/wrapper.rs index 6c72aa0b3d58..948a87d4cf6f 100644 --- a/servo/components/style/gecko/wrapper.rs +++ b/servo/components/style/gecko/wrapper.rs @@ -811,7 +811,7 @@ impl<'le> TElement for GeckoElement<'le> { continue; } - let mut property_check_helper = |property: TransitionProperty| -> bool { + let mut property_check_helper = |property: &TransitionProperty| -> bool { if self.needs_transitions_update_per_property(property, combined_duration, before_change_style, @@ -821,7 +821,9 @@ impl<'le> TElement for GeckoElement<'le> { } if let Some(set) = transitions_to_keep.as_mut() { - set.insert(property); + // The TransitionProperty here must be animatable, so cloning it is cheap + // because it is an integer-like enum. + set.insert(property.clone()); } false }; @@ -835,12 +837,12 @@ impl<'le> TElement for GeckoElement<'le> { }); if is_shorthand { let shorthand: TransitionProperty = property.into(); - if shorthand.longhands().iter().any(|&p| property_check_helper(p)) { + if shorthand.longhands().iter().any(|p| property_check_helper(p)) { return true; } } else { if animated_properties::nscsspropertyid_is_animatable(property) && - property_check_helper(property.into()) { + property_check_helper(&property.into()) { return true; } } @@ -855,7 +857,7 @@ impl<'le> TElement for GeckoElement<'le> { } fn needs_transitions_update_per_property(&self, - property: TransitionProperty, + property: &TransitionProperty, combined_duration: f32, before_change_style: &Arc, after_change_style: &Arc, @@ -869,17 +871,17 @@ impl<'le> TElement for GeckoElement<'le> { return false; } - if existing_transitions.contains_key(&property) { + if existing_transitions.contains_key(property) { // If there is an existing transition, update only if the end value differs. // If the end value has not changed, we should leave the currently running // transition as-is since we don't want to interrupt its timing function. let after_value = - Arc::new(AnimationValue::from_computed_values(&property, after_change_style)); - return existing_transitions.get(&property).unwrap() != &after_value; + Arc::new(AnimationValue::from_computed_values(property, after_change_style)); + return existing_transitions.get(property).unwrap() != &after_value; } combined_duration > 0.0f32 && - AnimatedProperty::from_transition_property(&property, + AnimatedProperty::from_transition_property(property, before_change_style, after_change_style).does_animate() } diff --git a/servo/components/style/gecko_bindings/bindings.rs b/servo/components/style/gecko_bindings/bindings.rs index c663df920120..f4d1a515d4b6 100644 --- a/servo/components/style/gecko_bindings/bindings.rs +++ b/servo/components/style/gecko_bindings/bindings.rs @@ -39,6 +39,7 @@ use gecko_bindings::structs::SheetParsingMode; use gecko_bindings::structs::StyleBasicShape; use gecko_bindings::structs::StyleBasicShapeType; use gecko_bindings::structs::StyleShapeSource; +use gecko_bindings::structs::StyleTransition; use gecko_bindings::structs::nsCSSFontFaceRule; use gecko_bindings::structs::nsCSSKeyword; use gecko_bindings::structs::nsCSSPropertyID; @@ -692,6 +693,11 @@ extern "C" { aProperty: nsCSSPropertyID) -> RawServoAnimationValueBorrowedOrNull; } +extern "C" { + pub fn Gecko_StyleTransition_SetUnsupportedProperty(aTransition: + *mut StyleTransition, + aAtom: *mut nsIAtom); +} extern "C" { pub fn Gecko_Atomize(aString: *const ::std::os::raw::c_char, aLength: u32) -> *mut nsIAtom; diff --git a/servo/components/style/keyframes.rs b/servo/components/style/keyframes.rs index d7e3485fc7d8..fb8290fedf4c 100644 --- a/servo/components/style/keyframes.rs +++ b/servo/components/style/keyframes.rs @@ -259,8 +259,8 @@ fn get_animated_properties(keyframes: &[Arc>], guard: &SharedRw if let Some(property) = TransitionProperty::from_declaration(declaration) { if !seen.has_transition_property_bit(&property) { - ret.push(property); seen.set_transition_property_bit(&property); + ret.push(property); } } } diff --git a/servo/components/style/properties/gecko.mako.rs b/servo/components/style/properties/gecko.mako.rs index 57ebd1114ff9..fc40dcff52da 100644 --- a/servo/components/style/properties/gecko.mako.rs +++ b/servo/components/style/properties/gecko.mako.rs @@ -33,6 +33,7 @@ use gecko_bindings::bindings::Gecko_FontFamilyList_AppendNamed; use gecko_bindings::bindings::Gecko_FontFamilyList_Clear; use gecko_bindings::bindings::Gecko_SetCursorArrayLength; use gecko_bindings::bindings::Gecko_SetCursorImage; +use gecko_bindings::bindings::Gecko_StyleTransition_SetUnsupportedProperty; use gecko_bindings::bindings::Gecko_NewCSSShadowArray; use gecko_bindings::bindings::Gecko_nsStyleFont_SetLang; use gecko_bindings::bindings::Gecko_nsStyleFont_CopyLangFrom; @@ -53,6 +54,7 @@ use gecko::values::convert_rgba_to_nscolor; use gecko::values::GeckoStyleCoordConvertible; use gecko::values::round_border_to_device_pixels; use logical_geometry::WritingMode; +use properties::animated_properties::TransitionProperty; use properties::longhands; use properties::{Importance, LonghandId}; use properties::{PropertyDeclaration, PropertyDeclarationBlock, PropertyDeclarationId}; @@ -2149,7 +2151,12 @@ fn static_assert() { unsafe { self.gecko.mTransitions.ensure_len(v.0.len()) }; self.gecko.mTransitionPropertyCount = v.0.len() as u32; for (servo, gecko) in v.0.into_iter().zip(self.gecko.mTransitions.iter_mut()) { - gecko.mProperty = servo.into(); + match servo { + TransitionProperty::Unsupported(ref atom) => unsafe { + Gecko_StyleTransition_SetUnsupportedProperty(gecko, atom.as_ptr()) + }, + _ => gecko.mProperty = (&servo).into(), + } } } else { // In gecko |none| is represented by eCSSPropertyExtra_no_properties. @@ -2172,7 +2179,22 @@ fn static_assert() { pub fn transition_property_at(&self, index: usize) -> longhands::transition_property::computed_value::SingleComputedValue { - self.gecko.mTransitions[index].mProperty.into() + use gecko_bindings::structs::nsCSSPropertyID::eCSSPropertyExtra_no_properties; + use gecko_bindings::structs::nsCSSPropertyID::eCSSPropertyExtra_variable; + use gecko_bindings::structs::nsCSSPropertyID::eCSSProperty_UNKNOWN; + + let property = self.gecko.mTransitions[index].mProperty; + if property == eCSSProperty_UNKNOWN || property == eCSSPropertyExtra_variable { + let atom = self.gecko.mTransitions[index].mUnknownProperty.raw(); + debug_assert!(!atom.is_null()); + TransitionProperty::Unsupported(atom.into()) + } else if property == eCSSPropertyExtra_no_properties { + // Actually, we don't expect TransitionProperty::Unsupported also represents "none", + // but if the caller wants to convert it, it is fine. Please use it carefully. + TransitionProperty::Unsupported(atom!("none")) + } else { + property.into() + } } pub fn transition_nscsspropertyid_at(&self, index: usize) -> nsCSSPropertyID { @@ -2180,6 +2202,8 @@ fn static_assert() { } pub fn copy_transition_property_from(&mut self, other: &Self) { + use gecko_bindings::structs::nsCSSPropertyID::eCSSPropertyExtra_variable; + use gecko_bindings::structs::nsCSSPropertyID::eCSSProperty_UNKNOWN; unsafe { self.gecko.mTransitions.ensure_len(other.gecko.mTransitions.len()) }; let count = other.gecko.mTransitionPropertyCount; @@ -2187,6 +2211,12 @@ fn static_assert() { for (index, transition) in self.gecko.mTransitions.iter_mut().enumerate().take(count as usize) { transition.mProperty = other.gecko.mTransitions[index].mProperty; + if transition.mProperty == eCSSProperty_UNKNOWN || + transition.mProperty == eCSSPropertyExtra_variable { + let atom = other.gecko.mTransitions[index].mUnknownProperty.raw(); + debug_assert!(!atom.is_null()); + unsafe { Gecko_StyleTransition_SetUnsupportedProperty(transition, atom) }; + } } } ${impl_transition_count('property', 'Property')} diff --git a/servo/components/style/properties/helpers/animated_properties.mako.rs b/servo/components/style/properties/helpers/animated_properties.mako.rs index 8783fc4a44f7..cd384da294a0 100644 --- a/servo/components/style/properties/helpers/animated_properties.mako.rs +++ b/servo/components/style/properties/helpers/animated_properties.mako.rs @@ -7,9 +7,10 @@ <% from data import SYSTEM_FONT_LONGHANDS %> use app_units::Au; -use cssparser::{Color as CSSParserColor, Parser, RGBA}; +use cssparser::{Color as CSSParserColor, Parser, RGBA, serialize_identifier}; use euclid::{Point2D, Size2D}; #[cfg(feature = "gecko")] use gecko_bindings::structs::nsCSSPropertyID; +#[cfg(feature = "gecko")] use gecko_string_cache::Atom; use properties::{CSSWideKeyword, PropertyDeclaration}; use properties::longhands; use properties::longhands::background_size::computed_value::T as BackgroundSizeList; @@ -25,6 +26,7 @@ use properties::longhands::transform::computed_value::T as TransformList; use properties::longhands::vertical_align::computed_value::T as VerticalAlign; use properties::longhands::visibility::computed_value::T as Visibility; #[cfg(feature = "gecko")] use properties::{PropertyDeclarationId, LonghandId}; +#[cfg(feature = "servo")] use servo_atoms::Atom; use std::cmp; #[cfg(feature = "gecko")] use std::collections::HashMap; use std::fmt; @@ -45,7 +47,7 @@ use values::generics::position as generic_position; /// property. // NB: This needs to be here because it needs all the longhands generated // beforehand. -#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +#[derive(Clone, Debug, PartialEq, Eq, Hash)] #[cfg_attr(feature = "servo", derive(HeapSizeOf))] pub enum TransitionProperty { /// All, any animatable property changing should generate a transition. @@ -62,24 +64,27 @@ pub enum TransitionProperty { /// ${prop.name} ${prop.camel_case}, % endfor + /// Unrecognized property which could be any non-animatable, custom property, or + /// unknown property. + Unsupported(Atom) } impl TransitionProperty { /// Iterates over each longhand property. - pub fn each ()>(mut cb: F) { + pub fn each ()>(mut cb: F) { % for prop in data.longhands: % if prop.animatable: - cb(TransitionProperty::${prop.camel_case}); + cb(&TransitionProperty::${prop.camel_case}); % endif % endfor } /// Iterates over every property that is not TransitionProperty::All, stopping and returning /// true when the provided callback returns true for the first time. - pub fn any bool>(mut cb: F) -> bool { + pub fn any bool>(mut cb: F) -> bool { % for prop in data.longhands: % if prop.animatable: - if cb(TransitionProperty::${prop.camel_case}) { + if cb(&TransitionProperty::${prop.camel_case}) { return true; } % endif @@ -89,7 +94,8 @@ impl TransitionProperty { /// Parse a transition-property value. pub fn parse(input: &mut Parser) -> Result { - match_ignore_ascii_case! { &try!(input.expect_ident()), + let ident = try!(input.expect_ident()); + match_ignore_ascii_case! { &ident, "all" => Ok(TransitionProperty::All), % for prop in data.longhands: % if prop.animatable: @@ -99,7 +105,13 @@ impl TransitionProperty { % for prop in data.shorthands_except_all(): "${prop.name}" => Ok(TransitionProperty::${prop.camel_case}), % endfor - _ => Err(()) + "none" => Err(()), + _ => { + match CSSWideKeyword::from_ident(&ident) { + Some(_) => Err(()), + None => Ok(TransitionProperty::Unsupported((&*ident).into())) + } + } } } @@ -199,6 +211,11 @@ impl ToCss for TransitionProperty { % for prop in data.shorthands_except_all(): TransitionProperty::${prop.camel_case} => dest.write_str("${prop.name}"), % endfor + #[cfg(feature = "gecko")] + TransitionProperty::Unsupported(ref atom) => serialize_identifier(&atom.to_string(), + dest), + #[cfg(feature = "servo")] + TransitionProperty::Unsupported(ref atom) => serialize_identifier(atom, dest), } } } @@ -206,9 +223,9 @@ impl ToCss for TransitionProperty { /// Convert to nsCSSPropertyID. #[cfg(feature = "gecko")] #[allow(non_upper_case_globals)] -impl From for nsCSSPropertyID { - fn from(transition_property: TransitionProperty) -> nsCSSPropertyID { - match transition_property { +impl<'a> From< &'a TransitionProperty> for nsCSSPropertyID { + fn from(transition_property: &'a TransitionProperty) -> nsCSSPropertyID { + match *transition_property { % for prop in data.longhands: % if prop.animatable: TransitionProperty::${prop.camel_case} @@ -220,6 +237,7 @@ impl From for nsCSSPropertyID { => ${helpers.to_nscsspropertyid(prop.ident)}, % endfor TransitionProperty::All => nsCSSPropertyID::eCSSPropertyExtra_all_properties, + _ => panic!("Unconvertable Servo transition property: {:?}", transition_property), } } } @@ -234,6 +252,9 @@ impl From for TransitionProperty { % if prop.animatable: ${helpers.to_nscsspropertyid(prop.ident)} => TransitionProperty::${prop.camel_case}, + % else: + ${helpers.to_nscsspropertyid(prop.ident)} + => TransitionProperty::Unsupported(Atom::from("${prop.ident}")), % endif % endfor % for prop in data.shorthands_except_all(): @@ -241,7 +262,7 @@ impl From for TransitionProperty { => TransitionProperty::${prop.camel_case}, % endfor nsCSSPropertyID::eCSSPropertyExtra_all_properties => TransitionProperty::All, - _ => panic!("Unsupported Servo transition property: {:?}", property), + _ => panic!("Unconvertable nsCSSPropertyID: {:?}", property), } } } @@ -356,7 +377,7 @@ impl AnimatedProperty { } % endif % endfor - other => panic!("Can't use TransitionProperty::{:?} here", other), + ref other => panic!("Can't use TransitionProperty::{:?} here", other), } } } @@ -544,7 +565,7 @@ impl AnimationValue { } % endif % endfor - other => panic!("Can't use TransitionProperty::{:?} here.", other), + ref other => panic!("Can't use TransitionProperty::{:?} here.", other), } } } diff --git a/servo/components/style/properties/properties.mako.rs b/servo/components/style/properties/properties.mako.rs index 69932549fe29..fa6ed21a33ba 100644 --- a/servo/components/style/properties/properties.mako.rs +++ b/servo/components/style/properties/properties.mako.rs @@ -258,7 +258,7 @@ impl LonghandIdSet { TransitionProperty::${prop.camel_case} => self.insert(LonghandId::${prop.camel_case}), % endif % endfor - other => unreachable!("Tried to set TransitionProperty::{:?} in a PropertyBitfield", other), + ref other => unreachable!("Tried to set TransitionProperty::{:?} in a PropertyBitfield", other), } } @@ -271,7 +271,7 @@ impl LonghandIdSet { TransitionProperty::${prop.camel_case} => self.contains(LonghandId::${prop.camel_case}), % endif % endfor - other => unreachable!("Tried to get TransitionProperty::{:?} in a PropertyBitfield", other), + ref other => unreachable!("Tried to get TransitionProperty::{:?} in a PropertyBitfield", other), } } } diff --git a/servo/components/style/properties/shorthand/box.mako.rs b/servo/components/style/properties/shorthand/box.mako.rs index cf98bffa3479..0dd320735a85 100644 --- a/servo/components/style/properties/shorthand/box.mako.rs +++ b/servo/components/style/properties/shorthand/box.mako.rs @@ -98,10 +98,12 @@ macro_rules! try_parse_one { loop { parsed += 1; - try_parse_one!(input, property, transition_property); try_parse_one!(context, input, duration, transition_duration); try_parse_one!(context, input, timing_function, transition_timing_function); try_parse_one!(context, input, delay, transition_delay); + // Must check 'transition-property' after 'transition-timing-function' since + // 'transition-property' accepts any keyword. + try_parse_one!(input, property, transition_property); parsed -= 1; break diff --git a/servo/ports/geckolib/glue.rs b/servo/ports/geckolib/glue.rs index 0366a05aa83f..7e463db9d134 100644 --- a/servo/ports/geckolib/glue.rs +++ b/servo/ports/geckolib/glue.rs @@ -1985,7 +1985,7 @@ pub extern "C" fn Servo_GetComputedKeyframeValues(keyframes: RawGeckoKeyframeLis // This is safe since we immediately write to the uninitialized values. unsafe { animation_values.set_len((i + 1) as u32) }; seen.set_transition_property_bit(&anim.0); - animation_values[i].mProperty = anim.0.into(); + animation_values[i].mProperty = (&anim.0).into(); // We only make sure we have enough space for this variable, // but didn't construct a default value for StyleAnimationValue, // so we should zero it to avoid getting undefined behaviors. @@ -2056,7 +2056,7 @@ pub extern "C" fn Servo_StyleSet_FillKeyframesForName(raw_data: RawServoStyleSet let block = style.to_declaration_block(property.clone().into()); unsafe { (*keyframe).mPropertyValues.set_len((index + 1) as u32); - (*keyframe).mPropertyValues[index].mProperty = property.clone().into(); + (*keyframe).mPropertyValues[index].mProperty = property.into(); // FIXME. Do not set computed values once we handles missing keyframes // with additive composition. (*keyframe).mPropertyValues[index].mServoDeclarationBlock.set_arc_leaky( @@ -2087,7 +2087,7 @@ pub extern "C" fn Servo_StyleSet_FillKeyframesForName(raw_data: RawServoStyleSet unsafe { let property = TransitionProperty::from_declaration(declaration).unwrap(); (*keyframe).mPropertyValues.set_len((index + 1) as u32); - (*keyframe).mPropertyValues[index].mProperty = property.into(); + (*keyframe).mPropertyValues[index].mProperty = (&property).into(); (*keyframe).mPropertyValues[index].mServoDeclarationBlock.set_arc_leaky( Arc::new(global_style_data.shared_lock.wrap( PropertyDeclarationBlock::with_one( diff --git a/servo/tests/unit/style/parsing/transition_property.rs b/servo/tests/unit/style/parsing/transition_property.rs index 5283f173afc8..5929489651c3 100644 --- a/servo/tests/unit/style/parsing/transition_property.rs +++ b/servo/tests/unit/style/parsing/transition_property.rs @@ -3,6 +3,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ use parsing::parse; +use servo_atoms::Atom; use style::properties::animated_properties::TransitionProperty; use style::properties::longhands::transition_property; use style::properties::shorthands::transition; @@ -13,16 +14,21 @@ fn test_longhand_properties() { assert_roundtrip_with_context!(transition_property::parse, "margin-left"); assert_roundtrip_with_context!(transition_property::parse, "background-color"); assert_roundtrip_with_context!(transition_property::parse, "width"); + assert_roundtrip_with_context!(transition_property::parse, "transition-duration"); + assert_roundtrip_with_context!(transition_property::parse, "unsupported-property"); + assert_roundtrip_with_context!(transition_property::parse, "-other-unsupported-property"); + assert_roundtrip_with_context!(transition_property::parse, "--var"); - assert_eq!(parse_longhand!(transition_property, "margin-left, width"), + assert_eq!(parse_longhand!(transition_property, "margin-left, transition-delay, width, --var"), transition_property::SpecifiedValue( vec![TransitionProperty::MarginLeft, - TransitionProperty::Width])); + TransitionProperty::Unsupported(Atom::from("transition-delay")), + TransitionProperty::Width, + TransitionProperty::Unsupported(Atom::from("--var"))])); - // TODO: If one of the identifiers listed is not a recognized property name or is not an - // animatable property, the implementation must still start transitions on the animatable - // properties. Therefore, the parser shouldn't return Err for non-animatable property. - assert!(parse(transition_property::parse, "transition-duration").is_err()); + assert!(parse(transition_property::parse, ".width").is_err()); + assert!(parse(transition_property::parse, "1width").is_err()); + assert!(parse(transition_property::parse, "- ").is_err()); } #[test] @@ -44,11 +50,22 @@ fn test_keywords() { assert_eq!(parse_longhand!(transition_property, "all"), transition_property::SpecifiedValue(vec![TransitionProperty::All])); + assert_eq!(parse_longhand!(transition_property, "width, all"), + transition_property::SpecifiedValue(vec![TransitionProperty::Width, + TransitionProperty::All])); + + // Using CSS Wide Keyword or none in the list will get the syntax invalid. + // Note: Only "none" alone is valid. + assert!(parse(transition_property::parse, "none").is_ok()); assert_eq!(parse_longhand!(transition_property, "none"), transition_property::SpecifiedValue(vec![])); - assert!(parse(transition_property::parse, "inherit").is_err()); assert!(parse(transition_property::parse, "initial").is_err()); + assert!(parse(transition_property::parse, "unset").is_err()); + assert!(parse(transition_property::parse, "width, none").is_err()); + assert!(parse(transition_property::parse, "width, initial").is_err()); + assert!(parse(transition_property::parse, "width, inherit").is_err()); + assert!(parse(transition_property::parse, "width, unset").is_err()); } #[test] @@ -61,7 +78,16 @@ fn test_transition_shorthand() { assert_eq!(result.transition_property, parse_longhand!(transition_property, "margin, all")); + let result = parse(transition::parse_value, "2s width, 3s --var, 4s background").unwrap(); + assert_eq!(result.transition_property, + parse_longhand!(transition_property, "width, --var, background")); + let result = parse(transition::parse_value, "none").unwrap(); assert_eq!(result.transition_property, parse_longhand!(transition_property, "none")); + + assert!(parse(transition::parse_value, "2s width, none").is_err()); + assert!(parse(transition::parse_value, "2s width, 2s initial").is_err()); + assert!(parse(transition::parse_value, "2s width, 3s unset").is_err()); + assert!(parse(transition::parse_value, "2s width, 4s inherit").is_err()); } From 3a385c46dbdd2915a3ec7f979334a4df7bd85cd7 Mon Sep 17 00:00:00 2001 From: Brian Grinstead Date: Wed, 26 Apr 2017 08:10:04 -0700 Subject: [PATCH 56/68] Bug 1359597 - Set up new console with devtools-launchpad;r=Honza MozReview-Commit-ID: FxuwGLYef5v --HG-- extra : rebase_source : e526562574050eba6b77b464f559e2c95d6c8c3a --- devtools/client/shims/devtools.js | 3 + devtools/client/themes/new-webconsole.css | 596 ++++++++++++++++++ devtools/client/webconsole/.babelrc | 6 +- devtools/client/webconsole/bin/configure.js | 29 + devtools/client/webconsole/bin/dev-server.js | 19 + .../webconsole/configs/development.json | 35 + devtools/client/webconsole/local-dev/index.js | 95 +++ .../webconsole/local-dev/jsterm-stub.js | 17 + .../new-console-output/actions/index.js | 10 +- .../test/fixtures/WebConsoleUtils.js | 5 +- devtools/client/webconsole/new-webconsole.js | 269 ++++++++ devtools/client/webconsole/package.json | 42 +- .../webconsole/webconsole-connection-proxy.js | 21 +- devtools/client/webconsole/webpack.config.js | 128 ++++ 14 files changed, 1240 insertions(+), 35 deletions(-) create mode 100644 devtools/client/themes/new-webconsole.css create mode 100644 devtools/client/webconsole/bin/configure.js create mode 100644 devtools/client/webconsole/bin/dev-server.js create mode 100644 devtools/client/webconsole/configs/development.json create mode 100644 devtools/client/webconsole/local-dev/index.js create mode 100644 devtools/client/webconsole/local-dev/jsterm-stub.js create mode 100644 devtools/client/webconsole/new-webconsole.js create mode 100644 devtools/client/webconsole/webpack.config.js diff --git a/devtools/client/shims/devtools.js b/devtools/client/shims/devtools.js index 17eae9f8f8af..18c4fee4a47f 100644 --- a/devtools/client/shims/devtools.js +++ b/devtools/client/shims/devtools.js @@ -10,6 +10,9 @@ */ const DevTools = { chromeWindowType: "navigator:browser", + getToolbox: function () { + return {}; + } }; exports.gDevTools = DevTools; diff --git a/devtools/client/themes/new-webconsole.css b/devtools/client/themes/new-webconsole.css new file mode 100644 index 000000000000..a2d1a20e2e1a --- /dev/null +++ b/devtools/client/themes/new-webconsole.css @@ -0,0 +1,596 @@ + +@import "chrome://devtools/skin/widgets.css"; +@import "resource://devtools/client/themes/light-theme.css"; + +/* Webconsole specific theme variables */ +.theme-light, +.theme-firebug { + --error-color: #FF0000; + --error-background-color: #FFEBEB; + --warning-background-color: #FFFFC8; +} + +/* General output styles */ + +a { + -moz-user-focus: normal; + -moz-user-input: enabled; + cursor: pointer; + text-decoration: underline; +} + +/* Workaround for Bug 575675 - FindChildWithRules aRelevantLinkVisited + * assertion when loading HTML page with links in XUL iframe */ +*:visited { } + +.webconsole-filterbar-wrapper { + flex-grow: 0; +} + +.webconsole-filterbar-primary { + display: flex; +} + +.devtools-toolbar.webconsole-filterbar-secondary { + height: initial; +} + +.webconsole-filterbar-primary .devtools-plaininput { + flex: 1 1 100%; +} + +.webconsole-output.hideTimestamps > .message > .timestamp { + display: none; +} + +.message.startGroup .message-body > .objectBox-string, +.message.startGroupCollapsed .message-body > .objectBox-string { + color: var(--theme-body-color); + font-weight: bold; +} + +.webconsole-output-wrapper .message > .icon { + margin: 3px 0 0 0; + padding: 0 0 0 6px; +} + +.message.error > .icon::before { + background-position: -12px -36px; +} + +.message.warn > .icon::before { + background-position: -24px -36px; +} + +.message.info > .icon::before { + background-position: -36px -36px; +} + +.message.network .method { + margin-inline-end: 5px; +} + +.network .message-flex-body > .message-body { + display: flex; +} + +.webconsole-output-wrapper .message .indent { + display: inline-block; + border-inline-end: solid 1px var(--theme-splitter-color); +} + +.message.startGroup .indent, +.message.startGroupCollapsed .indent { + border-inline-end-color: transparent; + margin-inline-end: 5px; +} + +.message.startGroup .icon, +.message.startGroupCollapsed .icon { + display: none; +} + +/* console.table() */ +.new-consoletable { + width: 100%; + border-collapse: collapse; + --consoletable-border: 1px solid var(--table-splitter-color); +} + +.new-consoletable thead, +.new-consoletable tbody { + background-color: var(--theme-body-background); +} + +.new-consoletable th { + background-color: var(--theme-selection-background); + color: var(--theme-selection-color); + margin: 0; + padding: 5px 0 0; + font-weight: inherit; + border-inline-end: var(--consoletable-border); + border-bottom: var(--consoletable-border); +} + +.new-consoletable tr:nth-of-type(even) { + background-color: var(--table-zebra-background); +} + +.new-consoletable td { + padding: 3px 4px; + min-width: 100px; + -moz-user-focus: normal; + color: var(--theme-body-color); + border-inline-end: var(--consoletable-border); + height: 1.25em; + line-height: 1.25em; +} + + +/* Layout */ +.webconsole-output { + flex: 1; + direction: ltr; + overflow: auto; + -moz-user-select: text; + position: relative; +} + +:root, +body, +#app-wrapper { + height: 100%; + margin: 0; + padding: 0; +} + +body { + overflow: hidden; +} + +#app-wrapper { + display: flex; + flex-direction: column; +} + +:root, body { + margin: 0; + padding: 0; + height: 100%; +} + +#app-wrapper { + height: 100%; + display: flex; + flex-direction: column; +} +#left-wrapper { + flex: 1; + display: flex; + flex-direction: column; +} +#output-container { + flex: 1; + overflow: hidden; +} +.webconsole-output-wrapper { + display: flex; + flex-direction: column; + height: 100%; +} + +.message { + display: flex; + padding: 0 7px; + width: 100%; + box-sizing: border-box; +} + +.message > .prefix, +.message > .timestamp { + flex: none; + color: var(--theme-comment); + margin: 3px 6px 0 0; +} + +.message > .indent { + flex: none; +} + +.message > .icon { + flex: none; + margin: 3px 6px 0 0; + padding: 0 4px; + height: 1em; + align-self: flex-start; +} + +.theme-firebug .message > .icon { + margin: 0; + margin-inline-end: 6px; +} + +.theme-firebug .message[severity="error"], +.theme-light .message.error, +.theme-firebug .message.error { + color: var(--error-color); + background-color: var(--error-background-color); +} + +.theme-firebug .message[severity="warn"], +.theme-light .message.warn, +.theme-firebug .message.warn { + background-color: var(--warning-background-color); +} + +.message > .icon::before { + content: ""; + background-image: url(chrome://devtools/skin/images/webconsole.svg); + background-position: 12px 12px; + background-repeat: no-repeat; + background-size: 72px 60px; + width: 12px; + height: 12px; + display: inline-block; +} + +.theme-light .message > .icon::before { + background-image: url(chrome://devtools/skin/images/webconsole.svg#light-icons); +} + +.message > .message-body-wrapper { + flex: auto; + min-width: 0px; + margin: 3px; +} + +/* The red bubble that shows the number of times a message is repeated */ +.message-repeats { + -moz-user-select: none; + flex: none; + margin: 2px 6px; + padding: 0 6px; + height: 1.25em; + color: white; + background-color: red; + border-radius: 40px; + font: message-box; + font-size: 0.9em; + font-weight: 600; +} + +.message-repeats[value="1"] { + display: none; +} + +.message-location { + max-width: 40%; +} + +.stack-trace { + /* The markup contains extra whitespace to improve formatting of clipboard text. + Make sure this whitespace doesn't affect the HTML rendering */ + white-space: normal; +} + +.stack-trace .frame-link-source, +.message-location .frame-link-source { + /* Makes the file name truncated (and ellipsis shown) on the left side */ + direction: rtl; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.stack-trace .frame-link-source-inner, +.message-location .frame-link-source-inner { + /* Enforce LTR direction for the file name - fixes bug 1290056 */ + direction: ltr; + unicode-bidi: embed; +} + +.stack-trace .frame-link-function-display-name { + max-width: 50%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.message-flex-body { + display: flex; +} + +.message-body > * { + white-space: pre-wrap; + word-wrap: break-word; +} + +.message-flex-body > .message-body { + display: block; + flex: auto; +} +#output-container.hideTimestamps > .message { + padding-inline-start: 0; + margin-inline-start: 7px; + width: calc(100% - 7px); +} + +#output-container.hideTimestamps > .message > .timestamp { + display: none; +} + +#output-container.hideTimestamps > .message > .indent { + background-color: var(--theme-body-background); +} +.message:hover { + background-color: var(--theme-selection-background-semitransparent) !important; +} +.theme-light .message.error { + background-color: rgba(255, 150, 150, 0.3); +} + +.theme-dark .message.error { + background-color: rgba(235, 83, 104, 0.17); +} + +.console-string { + color: var(--theme-highlight-lightorange); +} +.theme-selected .console-string, +.theme-selected .cm-number, +.theme-selected .cm-variable, +.theme-selected .kind-ArrayLike { + color: #f5f7fa !important; /* Selection Text Color */ +} + + +.message.network.error > .icon::before { + background-position: -12px 0; +} +.message.network > .message-body { + display: flex; + flex-wrap: wrap; +} + + +.message.network .method { + flex: none; +} +.message.network:not(.navigation-marker) .url { + flex: 1 1 auto; + /* Make sure the URL is very small initially, let flex change width as needed. */ + width: 100px; + min-width: 5em; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.message.network .status { + flex: none; + margin-inline-start: 6px; +} +.message.network.mixed-content .url { + color: var(--theme-highlight-red); +} + +.message .learn-more-link { + color: var(--theme-highlight-blue); + margin: 0 6px; +} + +.message.network .xhr { + background-color: var(--theme-body-color-alt); + color: var(--theme-body-background); + border-radius: 3px; + font-weight: bold; + font-size: 10px; + padding: 2px; + line-height: 10px; + margin-inline-start: 3px; + margin-inline-end: 1ex; +} +.message.cssparser > .indent { + border-inline-end: solid #00b6f0 6px; +} +.message.cssparser.error > .icon::before { + background-position: -12px -12px; +} + +.message.cssparser.warn > .icon::before { + background-position: -24px -12px; +} +.message.exception > .indent { + border-inline-end: solid #fb9500 6px; +} + +.message.exception.error > .icon::before { + background-position: -12px -24px; +} +.message.exception.warn > .icon::before { + background-position: -24px -24px; +} +.message.console-api > .indent { + border-inline-end: solid #cbcbcb 6px; +} + +.message.server > .indent { + border-inline-end: solid #90B090 6px; +} + +/* Input and output styles */ +.message.command > .indent, +.message.result > .indent { + border-inline-end: solid #808080 6px; +} + +.message.command > .icon::before { + background-position: -48px -36px; +} + +.message.result > .icon::before { + background-position: -60px -36px; +} + + + + +/* JSTerm Styles */ +#jsterm-wrapper { + flex: 0; +} +.jsterm-input-container { + background-color: var(--theme-tab-toolbar-background); + border-top: 1px solid var(--theme-splitter-color); +} + +.theme-light .jsterm-input-container { + /* For light theme use a white background for the input - it looks better + than off-white */ + background-color: #fff; + border-top-color: #e0e0e0; +} + +.theme-firebug .jsterm-input-container { + border-top: 1px solid #ccc; +} + +.jsterm-input-node, +.jsterm-complete-node { + border: none; + padding: 0; + padding-inline-start: 20px; + margin: 0; + -moz-appearance: none; appearance: none; + background-color: transparent; +} + +.jsterm-input-node[focused="true"] { + background-image: var(--theme-command-line-image-focus); + box-shadow: none; +} + +.jsterm-complete-node { + color: var(--theme-comment); +} + +.jsterm-input-node { + /* Always allow scrolling on input - it auto expands in js by setting height, + but don't want it to get bigger than the window. 24px = toolbar height. */ + max-height: calc(90vh - 24px); + background-image: var(--theme-command-line-image); + background-repeat: no-repeat; + background-size: 16px 16px; + background-position: 4px 50%; + color: var(--theme-content-color1); +} + +:-moz-any(.jsterm-input-node, + .jsterm-complete-node) > .textbox-input-box > .textbox-textarea { + overflow-x: hidden; + /* Set padding for console input on textbox to make sure it is inlcuded in + scrollHeight that is used when resizing JSTerminal's input. Note: textbox + default style has important already */ + padding: 4px 0 !important; +} +#webconsole-notificationbox, +.jsterm-stack-node { + width: 100%; +} + +.message.security > .indent { + border-inline-end: solid red 6px; +} + +.message.security.error > .icon::before { + background-position: -12px -48px; +} + +.message.security.warn > .icon::before { + background-position: -24px -48px; +} + +.navigation-marker { + color: #aaa; + background: linear-gradient(#aaa, #aaa) no-repeat left 50%; + background-size: 100% 2px; + margin-top: 6px; + margin-bottom: 6px; + font-size: 0.9em; +} + +.navigation-marker .url { + padding-inline-end: 9px; + text-decoration: none; + background: var(--theme-body-background); +} + +.theme-light .navigation-marker .url { + background: #fff; +} + +.stacktrace { + display: none; + padding: 5px 10px; + margin: 5px 0 0 0; + overflow-y: auto; + border: 1px solid var(--theme-splitter-color); + border-radius: 3px; +} + +.theme-light .message.error .stacktrace { + background-color: rgba(255, 255, 255, 0.5); +} + +.theme-dark .message.error .stacktrace { + background-color: rgba(0, 0, 0, 0.5); +} + +.message.open .stacktrace { + display: block; +} + +.message .theme-twisty { + display: inline-block; + vertical-align: middle; + margin: 3px 0 0 0; + flex-shrink: 0; +} + +/*Do not mirror the twisty because container force to ltr */ +.message .theme-twisty:dir(rtl), +.message .theme-twisty:-moz-locale-dir(rtl) { + transform: none; +} + +.cm-s-mozilla a[class] { + font-style: italic; + text-decoration: none; +} + +.cm-s-mozilla a[class]:hover, +.cm-s-mozilla a[class]:focus { + text-decoration: underline; +} + +a.learn-more-link.webconsole-learn-more-link { + font-style: normal; +} + +/* Open DOMNode in inspector button */ +.open-inspector { + background: url(chrome://devtools/skin/images/vview-open-inspector.png) no-repeat 0 0; + padding-left: 16px; + margin-left: 5px; + cursor: pointer; +} + +.elementNode:hover .open-inspector, +.open-inspector:hover { + filter: url(images/filters.svg#checked-icon-state); +} + +.elementNode:hover .open-inspector:active, +.open-inspector:active { + filter: url(images/filters.svg#checked-icon-state) brightness(0.9); +} + diff --git a/devtools/client/webconsole/.babelrc b/devtools/client/webconsole/.babelrc index af0f0c3d3521..f30713e4c9ea 100644 --- a/devtools/client/webconsole/.babelrc +++ b/devtools/client/webconsole/.babelrc @@ -1,3 +1,7 @@ { - "presets": ["es2015"] + "env": { + "test": { + "presets": ["es2015"] + } + } } \ No newline at end of file diff --git a/devtools/client/webconsole/bin/configure.js b/devtools/client/webconsole/bin/configure.js new file mode 100644 index 000000000000..38922cee175f --- /dev/null +++ b/devtools/client/webconsole/bin/configure.js @@ -0,0 +1,29 @@ +/* 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/. */ + +/* eslint-env node */ + +"use strict"; + +const fs = require("fs"); +const path = require("path"); + +function getConfig() { + if (process.env.TARGET === "firefox-panel") { + return require("../configs/firefox-panel.json"); + } + + const developmentConfig = require("../configs/development.json"); + + let localConfig = {}; + if (fs.existsSync(path.resolve(__dirname, "../configs/local.json"))) { + localConfig = require("../configs/local.json"); + } + + return Object.assign({}, developmentConfig, localConfig); +} + +module.exports = { + getConfig, +}; diff --git a/devtools/client/webconsole/bin/dev-server.js b/devtools/client/webconsole/bin/dev-server.js new file mode 100644 index 000000000000..ae0a5c075de9 --- /dev/null +++ b/devtools/client/webconsole/bin/dev-server.js @@ -0,0 +1,19 @@ +/* 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/. */ + +/* eslint-env node */ + +"use strict"; + +const toolbox = require("devtools-launchpad/index"); +const feature = require("devtools-config"); +const { getConfig } = require("./configure"); + +const envConfig = getConfig(); + +feature.setConfig(envConfig); + +let webpackConfig = require("../webpack.config"); + +toolbox.startDevServer(envConfig, webpackConfig, __dirname); diff --git a/devtools/client/webconsole/configs/development.json b/devtools/client/webconsole/configs/development.json new file mode 100644 index 000000000000..11898a4a1c11 --- /dev/null +++ b/devtools/client/webconsole/configs/development.json @@ -0,0 +1,35 @@ +{ + "title": "Console", + "environment": "development", + "baseWorkerURL": "http://localhost:8000/public/build/", + "host": "", + "theme": "light", + "dir": "ltr", + "features": { + }, + "logging": { + "client": false, + "firefoxProxy": false, + "actions": false + }, + "chrome": { + "debug": false, + "host": "localhost", + "port": 9222 + }, + "node": { + "debug": false, + "host": "localhost", + "port": 9229 + }, + "firefox": { + "webSocketConnection": false, + "proxyHost": "localhost:9000", + "webSocketHost": "localhost:6080", + "mcPath": "./firefox" + }, + "development": { + "serverPort": 8000, + "examplesPort": 7999 + } +} diff --git a/devtools/client/webconsole/local-dev/index.js b/devtools/client/webconsole/local-dev/index.js new file mode 100644 index 000000000000..1a28fb5158b9 --- /dev/null +++ b/devtools/client/webconsole/local-dev/index.js @@ -0,0 +1,95 @@ +/* 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/. */ + + /* eslint-env browser */ + +"use strict"; + +const React = require("react"); +const ReactDOM = require("react-dom"); +const { EventEmitter } = require("devtools-modules"); +const { Services: { appinfo, pref } } = require("devtools-modules"); +const { bootstrap } = require("devtools-launchpad"); + +EventEmitter.decorate(window); + +require("../../themes/new-webconsole.css"); +require("../../shared/components/reps/reps.css"); + +pref("devtools.debugger.remote-timeout", 10000); +pref("devtools.hud.loglimit", 1000); +pref("devtools.webconsole.filter.error", true); +pref("devtools.webconsole.filter.warn", true); +pref("devtools.webconsole.filter.info", true); +pref("devtools.webconsole.filter.log", true); +pref("devtools.webconsole.filter.debug", true); +pref("devtools.webconsole.filter.css", false); +pref("devtools.webconsole.filter.net", false); +pref("devtools.webconsole.filter.netxhr", false); +pref("devtools.webconsole.ui.filterbar", false); +pref("devtools.webconsole.inputHistoryCount", 50); +pref("devtools.webconsole.persistlog", false); +pref("devtools.webconsole.timestampMessages", false); +pref("devtools.webconsole.autoMultiline", true); + +const NewConsoleOutputWrapper = require("../new-console-output/new-console-output-wrapper"); +const NewWebConsoleFrame = require("../new-webconsole").NewWebConsoleFrame; + +// Replicate the DOM that the root component lives within +const el = document.createElement("div"); +el.style.flex = "1"; +el.innerHTML = ` +
+
+
+`; +document.querySelector("#mount").appendChild(el); + +document.documentElement.classList.add("theme-light"); + +// Copied from netmonitor/index.js: +window.addEventListener("DOMContentLoaded", () => { + for (let link of document.head.querySelectorAll("link")) { + link.href = link.href.replace(/(resource|chrome)\:\/\//, "/"); + } + + if (appinfo.OS === "Darwin") { + document.documentElement.setAttribute("platform", "mac"); + } else if (appinfo.OS === "Linux") { + document.documentElement.setAttribute("platform", "linux"); + } else { + document.documentElement.setAttribute("platform", "win"); + } +}); + +let consoleFrame; +function onConnect(connection) { + // If we are on the main dashboard don't render the component + if (!connection || !connection.tabConnection || !connection.tabConnection.tabTarget) { + return; + } + + // Stub out properties that are received from hudservice + const owner = { + iframeWindow: window, + chromeWindow: window, + hudId: "hud_0", + target: connection.tabConnection.tabTarget, + _browserConsole: false, + NewConsoleOutputWrapper, + }; + consoleFrame = new NewWebConsoleFrame(owner); + consoleFrame.init().then(function () { + console.log("NewWebConsoleFrame initialized"); + }); +} + +// This is just a hack until the local dev environment includes jsterm +window.evaluateJS = function (input) { + consoleFrame.webConsoleClient.evaluateJSAsync(`${input}`, function (r) { + consoleFrame.newConsoleOutput.dispatchMessageAdd(r); + }, {}); +}; + +bootstrap(React, ReactDOM, el).then(onConnect); diff --git a/devtools/client/webconsole/local-dev/jsterm-stub.js b/devtools/client/webconsole/local-dev/jsterm-stub.js new file mode 100644 index 000000000000..e03e29b1e632 --- /dev/null +++ b/devtools/client/webconsole/local-dev/jsterm-stub.js @@ -0,0 +1,17 @@ +/* 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/. */ + +"use strict"; + +function JSTerm(webConsoleFrame) { + this.hud = webConsoleFrame; + this.hudId = this.hud.hudId; + this.historyLoaded = new Promise(r => { + r(); + }); + this.openVariablesView = () => { }; + this.init = () => { }; +} + +module.exports.JSTerm = JSTerm; diff --git a/devtools/client/webconsole/new-console-output/actions/index.js b/devtools/client/webconsole/new-console-output/actions/index.js index 5ce76a402a52..08e88e85c0d3 100644 --- a/devtools/client/webconsole/new-console-output/actions/index.js +++ b/devtools/client/webconsole/new-console-output/actions/index.js @@ -7,11 +7,11 @@ "use strict"; const actionModules = [ - "enhancers", - "filters", - "messages", - "ui", -].map(filename => require(`./${filename}`)); + require("./enhancers"), + require("./filters"), + require("./messages"), + require("./ui"), +]; const actions = Object.assign({}, ...actionModules); diff --git a/devtools/client/webconsole/new-console-output/test/fixtures/WebConsoleUtils.js b/devtools/client/webconsole/new-console-output/test/fixtures/WebConsoleUtils.js index 5ab1c0bb4de4..64ce5e5dd168 100644 --- a/devtools/client/webconsole/new-console-output/test/fixtures/WebConsoleUtils.js +++ b/devtools/client/webconsole/new-console-output/test/fixtures/WebConsoleUtils.js @@ -6,7 +6,10 @@ const L10n = require("devtools/client/webconsole/new-console-output/test/fixtures/L10n"); const Utils = { - L10n + L10n, + supportsString: function (s) { + return s; + } }; module.exports = { diff --git a/devtools/client/webconsole/new-webconsole.js b/devtools/client/webconsole/new-webconsole.js new file mode 100644 index 000000000000..82710bf799fb --- /dev/null +++ b/devtools/client/webconsole/new-webconsole.js @@ -0,0 +1,269 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ft=javascript ts=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/. */ + +"use strict"; + +const {Utils: WebConsoleUtils} = require("devtools/client/webconsole/utils"); +const EventEmitter = require("devtools/shared/event-emitter"); +const promise = require("promise"); +const defer = require("devtools/shared/defer"); +const Services = require("Services"); +const { gDevTools } = require("devtools/client/framework/devtools"); +const { JSTerm } = require("devtools/client/webconsole/jsterm"); +const { WebConsoleConnectionProxy } = require("devtools/client/webconsole/webconsole-connection-proxy"); + +const PREF_MESSAGE_TIMESTAMP = "devtools.webconsole.timestampMessages"; + +// XXX: This file is incomplete (see bug 1326937). +// It's used when loading the webconsole with devtools-launchpad, but will ultimately be +// the entry point for the new frontend + +/** + * A WebConsoleFrame instance is an interactive console initialized *per target* + * that displays console log data as well as provides an interactive terminal to + * manipulate the target's document content. + * + * The WebConsoleFrame is responsible for the actual Web Console UI + * implementation. + * + * @constructor + * @param object webConsoleOwner + * The WebConsole owner object. + */ +function NewWebConsoleFrame(webConsoleOwner) { + this.owner = webConsoleOwner; + this.hudId = this.owner.hudId; + this.isBrowserConsole = this.owner._browserConsole; + this.NEW_CONSOLE_OUTPUT_ENABLED = true; + this.window = this.owner.iframeWindow; + + this._onToolboxPrefChanged = this._onToolboxPrefChanged.bind(this); + + EventEmitter.decorate(this); +} +NewWebConsoleFrame.prototype = { + /** + * Getter for the debugger WebConsoleClient. + * @type object + */ + get webConsoleClient() { + return this.proxy ? this.proxy.webConsoleClient : null; + }, + + /** + * Initialize the WebConsoleFrame instance. + * @return object + * A promise object that resolves once the frame is ready to use. + */ + init() { + this._initUI(); + let connectionInited = this._initConnection(); + + // Don't reject if the history fails to load for some reason. + // This would be fine, the panel will just start with empty history. + let allReady = this.jsterm.historyLoaded.catch(() => {}).then(() => { + return connectionInited; + }); + + // This notification is only used in tests. Don't chain it onto + // the returned promise because the console panel needs to be attached + // to the toolbox before the web-console-created event is receieved. + let notifyObservers = () => { + let id = WebConsoleUtils.supportsString(this.hudId); + if (Services.obs) { + Services.obs.notifyObservers(id, "web-console-created"); + } + }; + allReady.then(notifyObservers, notifyObservers) + .then(this.newConsoleOutput.init); + + return allReady; + }, + + destroy() { + if (this._destroyer) { + return this._destroyer.promise; + } + + this._destroyer = defer(); + + Services.prefs.addObserver(PREF_MESSAGE_TIMESTAMP, this._onToolboxPrefChanged); + this.React = this.ReactDOM = this.FrameView = null; + + let onDestroy = () => { + this._destroyer.resolve(null); + }; + if (this.proxy) { + this.proxy.disconnect().then(onDestroy); + this.proxy = null; + } else { + onDestroy(); + } + + return this._destroyer.promise; + }, + + _onUpdateListeners() { + + }, + + logWarningAboutReplacedAPI() { + + }, + + /** + * Setter for saving of network request and response bodies. + * + * @param boolean value + * The new value you want to set. + */ + setSaveRequestAndResponseBodies: function (value) { + if (!this.webConsoleClient) { + // Don't continue if the webconsole disconnected. + return promise.resolve(null); + } + + let deferred = defer(); + let newValue = !!value; + let toSet = { + "NetworkMonitor.saveRequestAndResponseBodies": newValue, + }; + + // Make sure the web console client connection is established first. + this.webConsoleClient.setPreferences(toSet, response => { + if (!response.error) { + this._saveRequestAndResponseBodies = newValue; + deferred.resolve(response); + } else { + deferred.reject(response.error); + } + }); + + return deferred.promise; + }, + + /** + * Connect to the server using the remote debugging protocol. + * + * @private + * @return object + * A promise object that is resolved/reject based on the connection + * result. + */ + _initConnection: function () { + if (this._initDefer) { + return this._initDefer.promise; + } + + this._initDefer = defer(); + this.proxy = new WebConsoleConnectionProxy(this, this.owner.target); + + this.proxy.connect().then(() => { + // on success + this._initDefer.resolve(this); + }, (reason) => { + // on failure + // TODO Print a message to console + this._initDefer.reject(reason); + }); + + return this._initDefer.promise; + }, + + _initUI: function () { + this.document = this.window.document; + this.rootElement = this.document.documentElement; + + this.outputNode = this.document.getElementById("output-container"); + this.completeNode = this.document.querySelector(".jsterm-complete-node"); + this.inputNode = this.document.querySelector(".jsterm-input-node"); + + this.jsterm = new JSTerm(this); + this.jsterm.init(); + + let toolbox = gDevTools.getToolbox(this.owner.target); + + // @TODO Remove this once JSTerm is handled with React/Redux. + this.window.jsterm = this.jsterm; + // @TODO Once the toolbox has been converted to React, see if passing + // in JSTerm is still necessary. + + // Handle both launchpad and toolbox loading + let Wrapper = this.owner.NewConsoleOutputWrapper || this.window.NewConsoleOutput; + this.newConsoleOutput = new Wrapper( + this.outputNode, this.jsterm, toolbox, this.owner, this.document); + + // Toggle the timestamp on preference change + Services.prefs.addObserver(PREF_MESSAGE_TIMESTAMP, this._onToolboxPrefChanged); + this._onToolboxPrefChanged(); + }, + + /** + * Handler for page location changes. + * + * @param string uri + * New page location. + * @param string title + * New page title. + */ + onLocationChange: function (uri, title) { + this.contentLocation = uri; + if (this.owner.onLocationChange) { + this.owner.onLocationChange(uri, title); + } + }, + + /** + * Release an actor. + * + * @private + * @param string actor + * The actor ID you want to release. + */ + _releaseObject: function (actor) { + if (this.proxy) { + this.proxy.releaseActor(actor); + } + }, + + /** + * Called when the message timestamp pref changes. + */ + _onToolboxPrefChanged: function () { + let newValue = Services.prefs.getBoolPref(PREF_MESSAGE_TIMESTAMP); + this.newConsoleOutput.dispatchTimestampsToggle(newValue); + }, + + /** + * Handler for the tabNavigated notification. + * + * @param string event + * Event name. + * @param object packet + * Notification packet received from the server. + */ + handleTabNavigated: function (event, packet) { + if (event == "will-navigate") { + if (this.persistLog) { + // Add a _type to hit convertCachedPacket. + packet._type = true; + this.newConsoleOutput.dispatchMessageAdd(packet); + } else { + this.jsterm.clearOutput(); + } + } + + if (packet.url) { + this.onLocationChange(packet.url, packet.title); + } + + if (event == "navigate" && !packet.nativeConsoleAPI) { + this.logWarningAboutReplacedAPI(); + } + }, +}; + +exports.NewWebConsoleFrame = NewWebConsoleFrame; diff --git a/devtools/client/webconsole/package.json b/devtools/client/webconsole/package.json index 34939b61372e..c9635af8ce43 100644 --- a/devtools/client/webconsole/package.json +++ b/devtools/client/webconsole/package.json @@ -1,21 +1,35 @@ { "name": "webconsole", "version": "0.0.1", - "devDependencies": { - "amd-loader": "0.0.5", - "babel-preset-es2015": "^6.6.0", - "babel-register": "^6.7.2", - "cross-env": "^3.1.3", - "enzyme": "^2.4.1", - "expect": "^1.16.0", - "jsdom": "^9.4.1", - "jsdom-global": "^2.0.0", - "mocha": "^2.5.3", - "require-hacker": "^2.1.4", - "sinon": "^1.17.5" + "engines": { + "node": ">=6.9.0" }, "scripts": { - "postinstall": "cd ../ && npm install && cd webconsole", - "test": "cross-env NODE_PATH=../../../ mocha new-console-output/test/**/*.test.js --compilers js:babel-register -r jsdom-global/register -r ./new-console-output/test/require-helper.js" + "start": "node bin/dev-server", + "test": "cross-env NODE_ENV=test NODE_PATH=../../../ mocha new-console-output/test/**/*.test.js --compilers js:babel-register -r jsdom-global/register -r ./new-console-output/test/require-helper.js" + }, + "dependencies": { + "amd-loader": "0.0.5", + "babel-preset-es2015": "^6.6.0", + "babel-register": "^6.24.0", + "cross-env": "^3.1.3", + "devtools-config": "0.0.12", + "devtools-launchpad": "0.0.67", + "devtools-modules": "0.0.24", + "enzyme": "^2.4.1", + "expect": "^1.16.0", + "file-loader": "^0.10.1", + "immutable": "^3.8.1", + "jsdom": "^9.4.1", + "jsdom-global": "^2.0.0", + "json-loader": "^0.5.4", + "mocha": "^2.5.3", + "raw-loader": "^0.5.1", + "react": "=15.3.2", + "react-dom": "=15.3.2", + "react-redux": "=5.0.3", + "redux": "^3.6.0", + "require-hacker": "^2.1.4", + "sinon": "^1.17.5" } } diff --git a/devtools/client/webconsole/webconsole-connection-proxy.js b/devtools/client/webconsole/webconsole-connection-proxy.js index e19c09eba1df..a5c6af69d4b4 100644 --- a/devtools/client/webconsole/webconsole-connection-proxy.js +++ b/devtools/client/webconsole/webconsole-connection-proxy.js @@ -6,14 +6,8 @@ "use strict"; -const {Cc, Ci, Cu} = require("chrome"); - -const {Utils: WebConsoleUtils} = - require("devtools/client/webconsole/utils"); -const BrowserLoaderModule = {}; -Cu.import("resource://devtools/client/shared/browser-loader.js", BrowserLoaderModule); - -const promise = require("promise"); +const {Utils: WebConsoleUtils} = require("devtools/client/webconsole/utils"); +const defer = require("devtools/shared/defer"); const Services = require("Services"); const STRINGS_URI = "devtools/client/locales/webconsole.properties"; @@ -127,18 +121,17 @@ WebConsoleConnectionProxy.prototype = { return this._connectDefer.promise; } - this._connectDefer = promise.defer(); + this._connectDefer = defer(); let timeout = Services.prefs.getIntPref(PREF_CONNECTION_TIMEOUT); - this._connectTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); - this._connectTimer.initWithCallback(this._connectionTimeout, - timeout, Ci.nsITimer.TYPE_ONE_SHOT); + this._connectTimer = setTimeout(this._connectionTimeout, timeout); let connPromise = this._connectDefer.promise; connPromise.then(() => { - this._connectTimer.cancel(); + clearTimeout(this._connectTimer); this._connectTimer = null; }, () => { + clearTimeout(this._connectTimer); this._connectTimer = null; }); @@ -475,7 +468,7 @@ WebConsoleConnectionProxy.prototype = { return this._disconnecter.promise; } - this._disconnecter = promise.defer(); + this._disconnecter = defer(); if (!this.client) { this._disconnecter.resolve(null); diff --git a/devtools/client/webconsole/webpack.config.js b/devtools/client/webconsole/webpack.config.js new file mode 100644 index 000000000000..9992feabe712 --- /dev/null +++ b/devtools/client/webconsole/webpack.config.js @@ -0,0 +1,128 @@ +/* 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/. */ + +/* eslint-env node */ +/* eslint max-len: [0] */ + +"use strict"; + +const {toolboxConfig} = require("./node_modules/devtools-launchpad/index"); +const { NormalModuleReplacementPlugin } = require("webpack"); +const {getConfig} = require("./bin/configure"); + +const path = require("path"); +const projectPath = path.join(__dirname, "local-dev"); + +let webpackConfig = { + entry: { + console: [path.join(projectPath, "index.js")], + }, + + module: { + loaders: [ + { + test: /\.(png|svg)$/, + loader: "file-loader?name=[path][name].[ext]", + }, + ] + }, + + output: { + path: path.join(__dirname, "assets/build"), + filename: "[name].js", + publicPath: "/assets/build", + }, + + externals: [ + { + "promise": "var Promise", + } + ], +}; + +webpackConfig.resolve = { + alias: { + "Services": "devtools-modules/client/shared/shim/Services", + + "devtools/client/webconsole/jsterm": path.join(projectPath, "jsterm-stub"), + "devtools/client/webconsole/utils": path.join(__dirname, "new-console-output/test/fixtures/WebConsoleUtils"), + "devtools/client/webconsole/new-console-output": path.join(__dirname, "new-console-output"), + "devtools/client/webconsole/webconsole-connection-proxy": path.join(__dirname, "webconsole-connection-proxy"), + + "react": path.join(__dirname, "node_modules/react"), + "devtools/client/shared/vendor/immutable": "immutable", + "devtools/client/shared/vendor/react": "react", + "devtools/client/shared/vendor/react-dom": "react-dom", + "devtools/client/shared/vendor/react-redux": "react-redux", + "devtools/client/shared/vendor/redux": "redux", + + "devtools/client/locales": path.join(__dirname, "../../client/locales/en-US"), + "toolkit/locales": path.join(__dirname, "../../../toolkit/locales/en-US"), + "devtools/shared/locales": path.join(__dirname, "../../shared/locales/en-US"), + "devtools/shared/plural-form": path.join(__dirname, "../../shared/plural-form"), + "devtools/shared/l10n": path.join(__dirname, "../../shared/l10n"), + + "devtools/client/framework/devtools": path.join(__dirname, "../../client/shims/devtools"), + "devtools/client/framework/menu": "devtools-modules/client/framework/menu", + "devtools/client/framework/menu-item": path.join(__dirname, "../../client/framework/menu-item"), + + "devtools/client/shared/components/reps/reps": path.join(__dirname, "../../client/shared/components/reps/reps"), + "devtools/client/shared/redux/middleware/thunk": path.join(__dirname, "../../client/shared/redux/middleware/thunk"), + "devtools/client/shared/components/stack-trace": path.join(__dirname, "../../client/shared/components/stack-trace"), + "devtools/client/shared/source-utils": path.join(__dirname, "../../client/shared/source-utils"), + "devtools/client/shared/components/frame": path.join(__dirname, "../../client/shared/components/frame"), + + "devtools/shared/defer": path.join(__dirname, "../../shared/defer"), + "devtools/shared/event-emitter": "devtools-modules/shared/event-emitter", + "devtools/shared/client/main": path.join(__dirname, "new-console-output/test/fixtures/ObjectClient"), + "devtools/shared/platform/clipboard": path.join(__dirname, "../../shared/platform/content/clipboard"), + } +}; + +const mappings = [ + [ + /utils\/menu/, "devtools-launchpad/src/components/shared/menu" + ], + [ + /chrome:\/\/devtools\/skin/, + (result) => { + result.request = result.request + .replace("./chrome://devtools/skin", path.join(__dirname, "../themes")); + } + ], + [ + /chrome:\/\/devtools\/content/, + (result) => { + result.request = result.request + .replace("./chrome://devtools/content", path.join(__dirname, "..")); + } + ], + [ + /resource:\/\/devtools/, + (result) => { + result.request = result.request + .replace("./resource://devtools/client", path.join(__dirname, "..")); + } + ], +]; + +webpackConfig.plugins = mappings.map(([regex, res]) => + new NormalModuleReplacementPlugin(regex, res)); + +// Exclude to transpile all scripts in devtools/ but not for this folder +const basePath = path.join(__dirname, "../../").replace(/\\/g, "\\\\"); +const baseName = path.basename(__dirname); +webpackConfig.babelExcludes = new RegExp(`^${basePath}(.(?!${baseName}))*$`); + +let config = toolboxConfig(webpackConfig, getConfig()); + +// Remove loaders from devtools-launchpad's webpack.config.js +// * For svg-inline loader: +// Webconsole uses file loader to bundle image assets instead of svg-inline loader +// * For raw loader: +// devtools/shared/l10n has preloaded raw loader in require.context +config.module.loaders = config.module.loaders + .filter((loader) => !["svg-inline", "raw"].includes(loader.loader)); + +module.exports = config; From afec853085b5ebcccd47fa3f6cacf4e4792ea3ee Mon Sep 17 00:00:00 2001 From: Boris Chiou Date: Wed, 26 Apr 2017 17:41:45 +0800 Subject: [PATCH 57/68] Bug 1357357 - Part 1: Add one FFI to set unsupported transition property. r=emilio We need an FFI, Gecko_StyleTransition_SetUnsupportedProperty, to set the mProperty and mUnknownProperty of StyleTransition. In Servo, we put non-animatable, custom, and unrecognized property together, so this FFI should look up the nsCSSPropertyID of the input string, and then store the value and the unknown property string (if any) back to StyleTransition. MozReview-Commit-ID: 4iZNKXNSrzj --HG-- extra : rebase_source : 088daa620133418afdc6b1c97b6e90ceb7cf3b75 --- js/src/devtools/rootAnalysis/analyzeHeapWrites.js | 1 + layout/style/ServoBindings.cpp | 14 ++++++++++++++ layout/style/ServoBindings.h | 4 ++++ layout/style/nsStyleStruct.cpp | 12 ++++++++++-- layout/style/nsStyleStruct.h | 2 ++ 5 files changed, 31 insertions(+), 2 deletions(-) diff --git a/js/src/devtools/rootAnalysis/analyzeHeapWrites.js b/js/src/devtools/rootAnalysis/analyzeHeapWrites.js index babafd3046cd..d391bb5fc8ca 100644 --- a/js/src/devtools/rootAnalysis/analyzeHeapWrites.js +++ b/js/src/devtools/rootAnalysis/analyzeHeapWrites.js @@ -212,6 +212,7 @@ function treatAsSafeArgument(entry, varName, csuName) ["Gecko_DestroyShapeSource", "aShape", null], ["Gecko_StyleShapeSource_SetURLValue", "aShape", null], ["Gecko_nsFont_InitSystem", "aDest", null], + ["Gecko_StyleTransition_SetUnsupportedProperty", "aTransition", null], ]; for (var [entryMatch, varMatch, csuMatch] of whitelist) { assert(entryMatch || varMatch || csuMatch); diff --git a/layout/style/ServoBindings.cpp b/layout/style/ServoBindings.cpp index aef6eff20883..d1454e56ead6 100644 --- a/layout/style/ServoBindings.cpp +++ b/layout/style/ServoBindings.cpp @@ -627,6 +627,20 @@ Gecko_AnimationGetBaseStyle(void* aBaseStyles, nsCSSPropertyID aProperty) return base->GetWeak(aProperty); } +void +Gecko_StyleTransition_SetUnsupportedProperty(StyleTransition* aTransition, + nsIAtom* aAtom) +{ + nsCSSPropertyID id = + nsCSSProps::LookupProperty(nsDependentAtomString(aAtom), + CSSEnabledState::eForAllContent); + if (id == eCSSProperty_UNKNOWN || id == eCSSPropertyExtra_variable) { + aTransition->SetUnknownProperty(id, aAtom); + } else { + aTransition->SetProperty(id); + } +} + void Gecko_FillAllBackgroundLists(nsStyleImageLayers* aLayers, uint32_t aMaxLen) { diff --git a/layout/style/ServoBindings.h b/layout/style/ServoBindings.h index 18117c14fb95..48fa9bcdcef1 100644 --- a/layout/style/ServoBindings.h +++ b/layout/style/ServoBindings.h @@ -37,6 +37,7 @@ namespace mozilla { enum FontFamilyType : uint32_t; struct Keyframe; enum Side; + struct StyleTransition; namespace css { struct URLValue; }; @@ -230,6 +231,9 @@ double Gecko_GetPositionInSegment( RawServoAnimationValueBorrowedOrNull Gecko_AnimationGetBaseStyle( void* aBaseStyles, nsCSSPropertyID aProperty); +void Gecko_StyleTransition_SetUnsupportedProperty( + mozilla::StyleTransition* aTransition, + nsIAtom* aAtom); // Atoms. nsIAtom* Gecko_Atomize(const char* aString, uint32_t aLength); diff --git a/layout/style/nsStyleStruct.cpp b/layout/style/nsStyleStruct.cpp index 97176bd41fb6..23ef89e79504 100644 --- a/layout/style/nsStyleStruct.cpp +++ b/layout/style/nsStyleStruct.cpp @@ -3234,17 +3234,25 @@ StyleTransition::SetInitialValues() void StyleTransition::SetUnknownProperty(nsCSSPropertyID aProperty, - const nsAString& aPropertyString) + const nsAString& aPropertyString) { MOZ_ASSERT(nsCSSProps::LookupProperty(aPropertyString, CSSEnabledState::eForAllContent) == aProperty, "property and property string should match"); + nsCOMPtr temp = NS_Atomize(aPropertyString); + SetUnknownProperty(aProperty, temp); +} + +void +StyleTransition::SetUnknownProperty(nsCSSPropertyID aProperty, + nsIAtom* aPropertyString) +{ MOZ_ASSERT(aProperty == eCSSProperty_UNKNOWN || aProperty == eCSSPropertyExtra_variable, "should be either unknown or custom property"); mProperty = aProperty; - mUnknownProperty = NS_Atomize(aPropertyString); + mUnknownProperty = aPropertyString; } bool diff --git a/layout/style/nsStyleStruct.h b/layout/style/nsStyleStruct.h index c90ace6650f8..6d88fcda8846 100644 --- a/layout/style/nsStyleStruct.h +++ b/layout/style/nsStyleStruct.h @@ -2308,6 +2308,8 @@ struct StyleTransition } void SetUnknownProperty(nsCSSPropertyID aProperty, const nsAString& aPropertyString); + void SetUnknownProperty(nsCSSPropertyID aProperty, + nsIAtom* aPropertyString); void CopyPropertyFrom(const StyleTransition& aOther) { mProperty = aOther.mProperty; From 68278092df473caa656f07c9161271cfc3860e82 Mon Sep 17 00:00:00 2001 From: Boris Chiou Date: Mon, 24 Apr 2017 19:51:13 +0800 Subject: [PATCH 58/68] Bug 1357357 - Part 2: Update mochitest expectations and enable two mochitests. r=hiro MozReview-Commit-ID: 55YjkHCWn3K --HG-- extra : rebase_source : dc4f3249717d8a1b521ae8d38b4f061cfbefe45f --- layout/style/test/mochitest.ini | 2 -- layout/style/test/stylo-failures.md | 2 -- 2 files changed, 4 deletions(-) diff --git a/layout/style/test/mochitest.ini b/layout/style/test/mochitest.ini index ecfcb573a6c0..fb8f793bdaeb 100644 --- a/layout/style/test/mochitest.ini +++ b/layout/style/test/mochitest.ini @@ -275,11 +275,9 @@ skip-if = toolkit == 'android' #bug 775227 [test_transitions_and_restyles.html] [test_transitions_and_zoom.html] [test_transitions_cancel_near_end.html] -skip-if = stylo # timeout bug 1328499 [test_transitions_computed_values.html] [test_transitions_computed_value_combinations.html] [test_transitions_events.html] -skip-if = stylo # timeout bug 1328499 [test_transitions.html] skip-if = (android_version == '18' && debug) # bug 1159532 [test_transitions_bug537151.html] diff --git a/layout/style/test/stylo-failures.md b/layout/style/test/stylo-failures.md index 715401b32fb2..aaa101995c8e 100644 --- a/layout/style/test/stylo-failures.md +++ b/layout/style/test/stylo-failures.md @@ -75,8 +75,6 @@ to mochitest command. * Transition support: * test_compute_data_with_start_struct.html `transition` [2] * test_transitions.html: pseudo elements [10] - * test_transitions_computed_value_combinations.html [145] - * test_value_storage.html `transition` [218] * Events: * test_animations_event_order.html [2] * test_computed_style.html `gradient`: -moz- and -webkit-prefixed gradient values [35] From f9cb2ecf9bf77df2862f19f9967eea5ad5cc3a94 Mon Sep 17 00:00:00 2001 From: Michael Kaply Date: Mon, 24 Apr 2017 16:06:43 -0500 Subject: [PATCH 59/68] Bug 1359223 - Add geckoview to checkstyle and correct style issues. r=snorp MozReview-Commit-ID: AM0bdF8dZQW --HG-- extra : rebase_source : e6f21dafc2fa99cb1ddbac102fad8c3cbafb1516 --- mobile/android/app/build.gradle | 2 +- .../java/org/mozilla/gecko/gfx/LayerView.java | 4 ++-- .../java/org/mozilla/gecko/media/CodecProxy.java | 2 +- .../gecko/process/GeckoProcessManager.java | 16 ++++++++-------- .../java/org/mozilla/gecko/util/StringUtils.java | 2 +- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/mobile/android/app/build.gradle b/mobile/android/app/build.gradle index 3a2697f29039..d878576604c2 100644 --- a/mobile/android/app/build.gradle +++ b/mobile/android/app/build.gradle @@ -261,7 +261,7 @@ dependencies { task checkstyle(type: Checkstyle) { configFile file("checkstyle.xml") // TODO: should use sourceSets from project instead of hard-coded str. - source '../base/java/' + source = ['../base/java/','../geckoview/src/main/java/'] // TODO: This ignores our pre-processed resources. include '**/*.java' // TODO: classpath should probably be something. diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/LayerView.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/LayerView.java index a85489633c87..cf2a73cff525 100644 --- a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/LayerView.java +++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/LayerView.java @@ -203,7 +203,7 @@ public class LayerView extends FrameLayout { } /* package */ void handleToolbarAnimatorMessage(int message) { - switch(message) { + switch (message) { case STATIC_TOOLBAR_NEEDS_UPDATE: // Send updated toolbar image to compositor. Bitmap bm = mToolbarAnimator.getBitmapOfToolbarChrome(); @@ -245,7 +245,7 @@ public class LayerView extends FrameLayout { mToolbarAnimator.notifyCompositorControllerOpen(); break; default: - Log.e(LOGTAG,"Unhandled Toolbar Animator Message: " + message); + Log.e(LOGTAG, "Unhandled Toolbar Animator Message: " + message); break; } } diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/CodecProxy.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/CodecProxy.java index 20f895bbe417..04c9d2b25620 100644 --- a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/CodecProxy.java +++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/CodecProxy.java @@ -251,7 +251,7 @@ public final class CodecProxy { @WrapForJNI public boolean release() { mCallbacks.setCodecProxyReleased(); - synchronized(this) { + synchronized (this) { if (mRemote == null) { Log.w(LOGTAG, "codec already ended"); return true; diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/GeckoProcessManager.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/GeckoProcessManager.java index 00dd9b942f2a..d01349c97f2f 100644 --- a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/GeckoProcessManager.java +++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/GeckoProcessManager.java @@ -59,7 +59,7 @@ public final class GeckoProcessManager extends IProcessManager.Stub { void waitForChild() { ThreadUtils.assertNotOnUiThread(); - synchronized(this) { + synchronized (this) { if (mWait) { try { this.wait(5000); // 5 seconds @@ -87,7 +87,7 @@ public final class GeckoProcessManager extends IProcessManager.Stub { } catch (final RemoteException e) { Log.e(LOGTAG, "Failed to get child " + mType + " process PID. Process may have died.", e); } - synchronized(this) { + synchronized (this) { if (mWait) { mWait = false; this.notifyAll(); @@ -97,10 +97,10 @@ public final class GeckoProcessManager extends IProcessManager.Stub { @Override public void onServiceDisconnected(ComponentName name) { - synchronized(INSTANCE.mConnections) { + synchronized (INSTANCE.mConnections) { INSTANCE.mConnections.remove(mType); } - synchronized(this) { + synchronized (this) { if (mWait) { mWait = false; this.notifyAll(); @@ -110,11 +110,11 @@ public final class GeckoProcessManager extends IProcessManager.Stub { @Override public void binderDied() { - Log.e(LOGTAG,"Binder died. Attempt to unbind service: " + mType + " " + mPid); + Log.e(LOGTAG, "Binder died. Attempt to unbind service: " + mType + " " + mPid); try { GeckoAppShell.getApplicationContext().unbindService(this); } catch (final java.lang.IllegalArgumentException e) { - Log.e(LOGTAG,"Looks like connection was already unbound", e); + Log.e(LOGTAG, "Looks like connection was already unbound", e); } } } @@ -127,7 +127,7 @@ public final class GeckoProcessManager extends IProcessManager.Stub { public int start(String type, String[] args, int crashFd, int ipcFd) { ChildConnection connection = null; - synchronized(mConnections) { + synchronized (mConnections) { connection = mConnections.get(type); } if (connection != null) { @@ -166,7 +166,7 @@ public final class GeckoProcessManager extends IProcessManager.Stub { crashPfd.close(); } ipcPfd.close(); - synchronized(mConnections) { + synchronized (mConnections) { mConnections.put(type, connection); } return connection.mPid; diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/StringUtils.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/StringUtils.java index 56f782915135..ad8c5f80a101 100644 --- a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/StringUtils.java +++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/StringUtils.java @@ -111,7 +111,7 @@ public class StringUtils { } if (newURL.endsWith("/")) { - newURL = newURL.substring(0, newURL.length()-1); + newURL = newURL.substring(0, newURL.length() - 1); } return newURL; From 820e8c33db7126417103d5a9330a3fdd3fa7376d Mon Sep 17 00:00:00 2001 From: Michael Kaply Date: Mon, 17 Apr 2017 13:11:43 -0500 Subject: [PATCH 60/68] Bug 1357121 - Allow dashes and underscore in OTA distribution name. r=nalexander MozReview-Commit-ID: 9PchGiKPGLi --HG-- extra : rebase_source : 42360222fb3abc359d46180dc385c89a4c706cfb --- .../base/java/org/mozilla/gecko/distribution/Distribution.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobile/android/base/java/org/mozilla/gecko/distribution/Distribution.java b/mobile/android/base/java/org/mozilla/gecko/distribution/Distribution.java index 2b32872bc409..b765c05d7042 100644 --- a/mobile/android/base/java/org/mozilla/gecko/distribution/Distribution.java +++ b/mobile/android/base/java/org/mozilla/gecko/distribution/Distribution.java @@ -878,7 +878,7 @@ public class Distribution { // We restrict here to avoid injection attacks. After all, // we're downloading a distribution payload based on intent input. - if (!content.matches("^[a-zA-Z0-9]+$")) { + if (!content.matches("^[a-zA-Z0-9_-]+$")) { Log.e(LOGTAG, "Invalid referrer content: " + content); Telemetry.addToHistogram(HISTOGRAM_REFERRER_INVALID, 1); return null; From 23615603c1320e62730e7d8e7a6be3da918df34b Mon Sep 17 00:00:00 2001 From: nchevobbe Date: Wed, 26 Apr 2017 10:40:04 +0200 Subject: [PATCH 61/68] Bug 1359716 - Make test_render_perf.html more accurate; r=Honza Set the logLimit to the number of messages we add. Bump up the number of messages to add to 4000. Iterate 25 times instead of 10. Compute the median to limit the impact of low/high spikes. MozReview-Commit-ID: 1WoiYk5pF9v --HG-- extra : rebase_source : 5e2459d04640f56de373a3e0e3805c13f382b7ea --- .../test/chrome/test_render_perf.html | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/devtools/client/webconsole/new-console-output/test/chrome/test_render_perf.html b/devtools/client/webconsole/new-console-output/test/chrome/test_render_perf.html index 2d6768428144..a99e08600b8f 100644 --- a/devtools/client/webconsole/new-console-output/test/chrome/test_render_perf.html +++ b/devtools/client/webconsole/new-console-output/test/chrome/test_render_perf.html @@ -14,7 +14,7 @@