import changes from `dev' branch of rmottola/Arctic-Fox:

- Bug 1244883 - Add Nightly-/Aurora-only crash for AsyncTransactionWaiter timeouts - r=nical (132b2ceff9)
- Bug 1148978 - Trigger paints when moving plugin windows around on the browser main thread. r=mattwoodrow (c75ce5ec09)
- Dedent some functions. (bug 1254899 part 1, r=jrmuizel) (c84fb419c7)
- Refactor acceleration pref initialization. (bug 1254899 part 2, r=jrmuizel) (29a164c70e)
- Remove NS_NATIVE_GRAPHIC on Windows. (bug 1266536, r=jimm) (84011349d1)
- Bug 1267253 - Delete gfxWindowsPlatform::RenderMode and replace it with a check against the default backend. r=bas (93cb6e503e)
- Introduce gfxConfig, a manager for graphics feature settings. (bug 1254899 part 3, r=milan) (97498ca46a)
- Bug 1262187: Allow D3D9 if D3D11 failed, behind the pref, but on by default. r=bas (8163e28b26)
- Bug 1178376 - Optionally fade in new progressively painted tiles r=nical (777bf1799d)
- Bug 1178376 - Allow progressive painting when low-precision tiles are disabled r=BenWa (3b8d84e19c)
- Bug 1178376 - Put progressive paint status in tile updates r=nical (935d3b46ed)
- Bug 1251778: Attempt to avoid presenting when the window is still resizing. r=jrmuizel (32b194a6f7)
- Hoist mWidget into the Compositor base class. (bug 1264545 part 1, r=nical) (aca26ec343)
- Lift compositor-accessed methods from nsIWidget into CompositorWidgetProxy. (bug 1264545 part 2, r=jimm) (609a23157a)
- Rename FeatureStatus::Crashed to CrashedInHandler. (bug 1254899 part 4, r=milan) (0eae23a3fd)
- Move DeviceInitData from gfxWindowsPlatform to gfxPlatforn. (bug 1254899 part 5, r=milan) (e31540ba18)
- Merge gfxWindowsPlatform::mAcceleration into gfxConfig. (bug 1254899 part 6, r=milan) (9d45cc8b87)
- Add another feature state for blacklisting and environment decisions. (bug 1254899 part 7, r=milan) (e7eee53cfb)
- Give FeatureState a public interface. (bug 1254899 part 8, r=milan) (52d8e7f355)
- Replace gfxWindowsPlatform::mD3D11Status with gfxConfig. (bug 1254899 part 9, r=jrmuizel) (90dc658b53)
- Replace gfxWindowsPlatform::mD2D1Status with gfxConfig. (bug 1254899 part 10, r=milan) (eb9474f309)
- Fix assertion failure in gfxConfig. (bug 1269565, r=milan) (0eb738ce66)
- Add gfxConfig to nsIGfxInfo, for about:support access. (bug 1254899 part 11, r=jrmuizel) (e770240152)
- Change Compositor::GetWidget to return a CompositorWidgetProxy. (bug 1264545 part 3, r=jimm) (fdf1d96255)
- Bug 1251778 - Followup: Remove unreferenced local variable. r=bustage (27579f5542)
- Use CompositorWidgetProxy in place of nsIWidget in the compositor. (bug 1264545 part 4, r=jimm) (80def1c2eb)
- Use CompositorWidgetProxy in place of nsIWidget in CompositorBridgeParent. (bug 1264545 part 5, r=jimm,kats) (67d0e1ef7d)
- Move CompositorWidgetProxy inheritance out of nsIWidget. (bug 1264545 part 6, r=jimm) (61075722c5)
- Bug 1251894 - In CompositorD3D11::CreateTexture, copy as much as the render target allows. r=bas (bf5fc6baa2)
- Bug 1266444: It is OK for us not to have texture sharing. r=jrmuizel (0b1885f89d)
- Bug 1266396 - Make TextureClient more robust against racy shutdown situations. r=Bas (b1d7f54643)
- Fix test bustage due to platform line-endings. Bug 1222624 (10b8cf3592)
- More test bustage from bug 1222624 (763c4c0bb9)
- Backed out 4 changesets (bug 1222624) to fix bug 1249572 (7ba3d433d0)
- Bug 1268230 - RunTime.cpp and ScriptLoader do not have to use MainThreadStopSyncLoopRunnable, r=khuey (88499a3982)
- Bug 1037725 - Add warning message in the console when worker spawn over limit. r=khuey (8af94dbc1d)
- Bug 1145171 - Show the detailed version in about:support (usefull for beta, no impact for the rest) r=dolske (9b85c113e6)
- Reorganize and tidy up the graphics section of about:support. (bug 1263849, r=milan) (6b68a6f8d8)
- Bug 1047663 - Disabling the cache in a tab should also disable it for all workers in that tab;r=khuey (5411d81682)
- Bug 1253793 Update ScriptLoader assertion to handle cancelation case. r=khuey (18c78d5651)
- Bug 1245768 - Implement a test for the correct error management when worker imports 3rd party scripts, r=bz (c1d3f290a9)
- Bug 1249673. Muted errors should be turned into NetworkError DOMExceptions when returning from importScripts on workers, instead of becoming NS_ERROR_FAILURE. r=baku (0358282cbe)
- Bug 1265405 - Add a dictionary to specify how PeriodicWave should be normalized (or not); r=smaug …normalized (or not); r=smaugu (201213146c)
- Bug 1251082. Restore comments in PageTransitionEvent.webidl that got lost when nsIDOMPageTransitionEvent.idl was migrated to webidl. r=bz The mentioned migration happened in http://hg.mozilla.org/mozilla-central/rev/e6377ca32f3d from bug 1031051. (2dfa309056)
- Bug 1266178 Make ServiceWorkerClient not assert if the document doesn't have an outer window. r=ehsan (eafb169c91)
- Bug 1259164 - Set ServiceWorkerMessageEvent.origin correctly when calling ServiceWorkerClient.postMessage(); r=bkelly (caeb65d10e)
- Bug 1246319 P1 Dedupe service worker registrar entries. r=baku (b76deef941)
- Bug 1246319 P2 Verify entries are deduped from the ServiceWorkerRegistrar. r=baku (8a4e348d6e)
- Bug 1246319 P3 Fix service worker registry value update. r=bz (14abf6b6ce)
- Bug 1247970 - Remove principal spec from service worker registrar file. r=baku (3c30130700)
- Bug 1249438 P1 Move guts of RegisterServiceWorker() into a protected method that can be tested in gtest. r=baku (488243196d)
- Bug 1249438 P2 Modify existing gtest to use RegisterServiceWorkerInternal. r=baku (e86c66891d)
- Bug 1249438 P3 Add a gtest that registers duplicate service worker registrations. r=baku (35e269f9af)
- Bug 1226443 P6 Ignore update() called during top level service worker script evaluation. r=ehsan (dcb9d02553)
- Bug 1241725 - about:serviceworkers "Active Cache Name" UUID should not contain null bytes, r=bkelly (4cddea6a67)
- Bug 1221852 - SharedWorker.port should be always not null, r=smaug (a9800274dc)
- Bug 1261428: Clean up a bit more. r=bz (4977e3d7a5)
- Bug 890284. Stop splitting textnodes in the XML content sink. r=peterv (a46dfca1cf)
- Bug 1211708 Allow themes to specify XBL bindings even in unprivileged documents r=sicking (82cf1a4023)
- Bug 915962 - Part 1: Allow pressing space to scroll the document if an editable element or form control is not focused; r=roc (cdb934af03)
- Bug 915962 - Part 2: Add a test case for pressing space when a tabindex=-1 and a button element is focused; r=roc (17dcf5cfd0)
- Bug 915962 - Part 3: Do not crash when pressing the space bar without having an element focused; r=roc (2161e62bc3)
- Bug 1180761, cancel the event earlier so that space doesn't trigger checkbox change and scroll, r=neil (2425cb76ad)
- Bug 1259182 - Shrink keyCodeData. r=bz. (737204af84)
- Bug 1193567 - Check result of ReadID in nsXBLPrototypeBinding::Read(). r=wchen (c9b1c35bf3)
- Bug 1173344 - Remove an intermediary root from nsXBLProtoImplField's FieldGetterImpl; r=jandem (5f42dd2e48)
- Bug 1207494 - Part 14: Remove use of expression closure from dom/xbl/. r=bz (21c7d3825f)
- align tests (fe34b613d3)
- Bug 1223702 - Fix some errors about wifi direct. r=hchang (568d86054a)
- Bug 1166274 - Part 1: Handle the callback and IPC message of setStaticIpMode correctly. r=vchang (8fb8d7f3b7)
- Bug 1133665 - [Flame][Wifi] The SSID that has set to be binded with MAC address is not hightlighted when user taps it. r=hchang (3165471d13)
- Bug 1207494 - Part 13: Remove use of expression closure from dom/wifi/. r=henry (dd9ad23a8a)
- Bug 1251856 - Disable U2F in all releases (fix for 1231681). r=baku (24ada10566)
- align tests (dae9ecd0ee)
- var-let (11a3cb0878)
- Bug 1184822 - Use classId to get provider. r=fabrice (1288eccd06)
- Bug 1247410 - "test for _nomap ids does not work correctly". r=dougt (f736a04f08)
- Bug 1035097 - Changed the type from 'radio' to 'radioType'. r=jdm (f9a0079152)
- Bug 1177871 - Add a timeout to XHR request of WifiGeoPositionProvider. r=jdm (2f6aa87c20)
- align code to 978593 with POST and location structure (d8ba75a759)
- Bug 1221139 - Report actual exception string and traceback in ParseError. r=ahal (40a92d0b81)
- Bug 980788 - [manifestparser] Add greater-than/less-than (equal) support. r=ahal (59b0eb26f6)
- Backed out changeset 99c2fcc61cc2 (bug 958147) for B2G Desktop and Mulet checktest failures. (2500bf78cf)
- Bug 1182817 - [manifestparser] Fix exception in chunk_by_slice when there are two times more chunks than tests, r=chmanchester (e58351e09b)
- Bug 1150497 - Make manifestparser tags whitespace (instead of comma) delimited to conform to other attributes, r=chmanchester (92df1c4778)
- Bug 1203266 - Don't call normpath in the manifestparser on paths that don't contain '..'. r=ahal (0dc357170f)
- Bug 1245671: Fix Assert.rejects on release builds with DOM promises. r=markh (8208cfc1f9)
- Bug 1075157 - Change notDeepEqual to use ObjectUtils, like deepEqual does already. r=gfritzsche (27c46981a5)
- Bug 1147751 - Implement Assert.greater, Assert.greaterOrEqual, Assert.less and Assert.lessOrEqual. r=mikedeboer (f0c05e89ce)
- Bug 1252995 - recordTestCoverage modification. r=chmanchester (7c702a40ac)
- Bug 1139254 - Introduce a new jsm to register mock easily. r=gps (f29cf1c5de)
- Bug 1210586 - Create a Synced tabs sidebar r=markh (682c5ba719)
- Bug 1230685 - Replace function declarations with add_task statements in test_storage_value_array.js and test_unicode.js. r=mak (9822bf2215)
- Bug 1230683 - Replace try/catch with Assert.throws in test_storage_connection.js. r=mak (1c993fc37e)
- Bug 655722 - Rewrite _buildGUIDMap in the sync bookmark engine to use PlacesUtils.promiseBookmarksTree. r=mak (3795d26af0)
- Bug 1251057 - enable debug logging for rest.js requests and responses. r=adw (1062bcd113)
- Bug 503515 - Try and ensure exported certificates include an extension by default. r=keeler (505967ab7f)
- Bug 1017616 - Filter out some more unnecessary characters when exporting certs. r=keeler (e95838e362)
- Bug 1241614 - don't overflow:auto the container, use em to size the dialog to avoid hidpi visibility issues, r=dolske,ttaubert (8bd6c2b35b)
- Bug 1257783 - mach-bootstrap: ask git user to clone hg.mozilla.org with git-cinnabar. r=gps (a2b13e9a71)
- Bug 1221200 - Post: Change formatting and ordering. r=m On a CLOSED TREE because DONTBUILD NPOTB (4f836e9717)
- Bug 1251352 - Respect --no-interactive during Arch bootstrap; r=gps (c18d53ab05)
- Bug 1251810 - Update Arch pacman -U command in bootstrap to handle --no-interactive; r=gps (ca4af7bdd3)
- Bug 1260749 - quiet unpacking of Android SDK and NDK downloads; r=nalexander (72857b97d0)
- NO BUG - Bump version of mach to 0.6 (0d42c69744)
- Bug 1253697 - Support downloading debug artifact builds. r=nalexander (785ec97706)
- remove PM hack (58f6d40047)
- Bug 1266999 - Stop writing XPT_NAME in backend.mk; r=glandium (9641e22ac2)
- Bug 1267437 - Generate projects in the Visual Studio backend for every binary generated by the build; r=gps (19bc978276)
- Bug 1268752 - Bug fix in locating eslint with npm. r=gps (faf399aa58)
- Bug 1266851. Make <xmp> and <listing> use HTMLPreElement as their primary interface, per <whatwg/html#1015>.  r=peterv (6be7f9d6e9)
- Bug 1262184 - Block embed content loading when child of media element; r=bz (a297eeb378)
- Bug 1263696 - Block embed content loading when ancestor of object element with content; r=bz (89c143cbfe)
- Bug 1266077. Fix <base> href getter to follow the spec; it should be using the fallback base URI to resolve against, not the document URI. r=bkelly (e757b23a14)
- Bug 1168079 nsTextEditRules::CollapseSelectionToTrailingBRIfNeeded() should ensure that there is a selection before calling nsEditor::GetStartNodeAndOffset() r=ehsan (6c283bf3a7)
- Bug 898321 - Return success from nsTableEditor::GetCellAt if frame not found; r=ehsan (0d09143b95)
- Bug 387687 - wrap quotes in plain text replies to window. r=masayuki (ca51437018)
- Bug 1247483 - Only replace nodes in nsHTMLEditor::ReplaceOrphanedStructure if all nodes in node list are descendants of replacement node. r=ehsan (8416037da2)
- bug 1266496 - fire some selection events for proxied accessibles r=davidb (8806de7dd9)
- bug 1266518 - add a new event message for AccSelChangeEvents r=davidb (46af183cab)
- Bug 1261479 - Remove remaining USE_RCS_MK usage; r=chmanchester (6847c92baf)
- Bug 1259554 - Remove INSTALL/PP_TARGETS from build/Makefile.in; r=ted (2db863232a)
- Bug 1265799 - Disable b2g-inbound. r=Callek (9d059c0c1c)
- Bug 1156885 - num_ctors: post to perfherder, not graphserver. r=bhearsum (72e12bb442)
- Bug 1156885 - Fix and validate perfherder output;r=jmaher (ad0b6d7a35)
- Bug 1214948 - Add a script to build Linux clang using TaskCluster; r=ted (5a023d7966)
- Bug 1042132 - Part 1: Port build-clang.py to Windows; r=rail (e35a015146)
- Bug 953265: Adjust Opus bitrate in WebRTC to pass >8KHz audio, and comment r=bwc (b0be6a326e)
- Bug 1252908 - [beetmover] refresh AV database on every run r=rail a=testing DONTBUILD (cebdfcba77)
- Bug 1247428 - move release promotion Dockerfiles in tree r=rail DONTBUILD (05a083001d)
- Bug 1255273 - Partial mar files have two channel IDs r=nthomas DONTBUILD (950fcf2fc8)
- Bug 1259423 - freshclam fails to update the DB r=rail DONTBUILD (10792155c7)
- Bug 1266039 - Generate release promotion specific docker images as a part of release promotion process r=Callek a=release DONTBUILD (130ab416a9)
- Bug 1221473: Do not treat answer as authoritative wrt payload types. r=drno (d27409209e)
- Bug 1241321 - No RTCP stats for audio streams. r=rjesup (ec0222694e)
- bug 1250492 - use tl::Max instead of std::max to get rid of a static constructor r=jesup (3cebbc8969)
- Bug 1254187: Fix maxBitrate to respect simulcast. r=jesup (e569e54b57)
- Bug 1158931 - Fix static assertion compilation error; r=snorp (eb27881746)
- No bug, fix WebrtcMediaCodecVP8VideoCodec.cpp warnings (a983544581)
- Bug 1252737 - use size_t instead of uint32_t for InitEncode(). r=jesup (57c3abc9fa)
- Bug 1208371 - Never send more than one disabled frame in a row to the WebRTC encoder. r=jesup (ec0c28822b)
- Bug 1208371 - Do image format conversion async in MediaPipeline. r=jesup (032efec783)
- Bug 1266685 - Don't pass too many frames to the MediaPipelineTransmit VideoFrameConverter. r=jesup (21774a8d25)
- Bug 1266644 - Rename StreamBuffer to StreamTracks. r=jesup r=pehrsons (21906fe1f7)
- Bug 1208371 - Don't treat audio chunks as mutable in MediaPipeline. r=padenot (3878ef4332)
- Bug 1246310 - Let MediaPipelineReceive tracks start at 0. r=jesup (b468ff8d48)
- Bug 1266644 - Rename DOMMediaStream:: CreateXXXStream to DOMMediaStream:: CreateXXXStreamAsInput. r=jesup r=pehrsons (fe4b6d70bc)
- Bug 1234578: Add an assertion. r=drno, a=abillings (f1a2c8d841)
- bug 1250492 - include sstream in SdpMediaSection.h instead of iostream r=jesup (110b5c2eca)
- Bug 1264470 - a=identity is a long attribute, r=bwc (5848194fe9)
- Bug 1256750: Remove unnecessary sscanf_s parameter on windows, and fix format string everywhere else. r=jesup (371c0db476)
- Bug 1204082 - try strtoull instead. r=mt (a0313aa87c)
- Bug 1113443 - reject each media type with approriate default. r=bwc (a72ff312d1)
- Bug 1095793 - use mid if provided to place candidate in msection. r=bwc (2c29b21fac)
- Bug 1252699 - Set WEBRTC_DETECT_ARM_NEON when optional neon is requested. r=jesup (722e2043a5)
- Bug 1229475 - webrtc: Call opus tonality_analysis_init. r=jesup (1cf8cc2cd7)
- Bug 1254876: assert windows recording is shut down r=pkerr (1f2cb69073)
- Bug 1227481 - added a memset on aec. r=jesup (532026ce20)
- Bug 1254507 - Fix leak in WebRTC DesktopApplication class. r=jesup (54da72aeb4)
- Bug 1196542 - share only windows with non-zero area. r=pkerr (94595ec463)
- Bug 1202087 - Filter out non-shareable application for win8 or greater. r=jesup (d989956802)
- Bug 1216529 - WebRTC: Request camera permission before accessing camera APIs. r=gcp (24b6699226)
- Bug 1237630 - Part 1: Video freeze from WebRTC sender. r=rjesup (02daa8b5b7)
- Bug 1237630 - Part 2: remove LOG statement generating a now defunct error condition. r=rjesup (c6002ef12f)
- Bug 1248335: avoid using SvcInternal structure entirely, as system-vpx may not have it r=pkerr (ef9b21f20c)
- Bug 1234571: unregister encoded-frame callback when releasing codec databases r=pkerr (321bd5166b)
- Bug 820972 - Comment out colorTable[] because we don't need it. r=jesup. (60b10803d5)
- cleanup and missing test stuff (e2be0331d9)
- bits of 1210586 (b7c5255597)
- Merge remote-tracking branch 'upstream/dev' into winbuild (3a3bb0b315)
- layout/media: fix export symbol list, fix build bustage (f9f5bfe14c)
This commit is contained in:
2024-09-10 22:23:45 +08:00
parent 25a43ca130
commit 442996b56c
434 changed files with 22777 additions and 4035 deletions
+11
View File
@@ -1495,6 +1495,10 @@ a11y::ProxyEvent(ProxyAccessible* aTarget, uint32_t aEventType)
case nsIAccessibleEvent::EVENT_VALUE_CHANGE:
g_object_notify((GObject*)wrapper, "accessible-value");
break;
case nsIAccessibleEvent::EVENT_TEXT_SELECTION_CHANGED:
case nsIAccessibleEvent::EVENT_SELECTION_WITHIN:
g_signal_emit_by_name(wrapper, "selection_changed");
break;
}
}
@@ -1610,6 +1614,13 @@ MaiAtkObject::FireAtkShowHideEvent(AtkObject* aParent, bool aIsAdded,
g_signal_emit_by_name(aParent, signal_name, indexInParent, this, nullptr);
}
void
a11y::ProxySelectionEvent(ProxyAccessible*, ProxyAccessible* aWidget, uint32_t)
{
MaiAtkObject* obj = MAI_ATK_OBJECT(GetWrapperFor(aWidget));
g_signal_emit_by_name(obj, "selection_changed");
}
// static
void
AccessibleWrap::GetKeyBinding(Accessible* aAccessible, nsAString& aResult)
+2
View File
@@ -77,6 +77,8 @@ void ProxyTextChangeEvent(ProxyAccessible* aTarget, const nsString& aStr,
bool aFromUser);
void ProxyShowHideEvent(ProxyAccessible* aTarget, ProxyAccessible* aParent,
bool aInsert, bool aFromUser);
void ProxySelectionEvent(ProxyAccessible* aTarget, ProxyAccessible* aWidget,
uint32_t aType);
} // namespace a11y
} // namespace mozilla
+9
View File
@@ -885,6 +885,15 @@ Accessible::HandleAccEvent(AccEvent* aEvent)
event->IsFromUserInput());
break;
}
case nsIAccessibleEvent::EVENT_SELECTION:
case nsIAccessibleEvent::EVENT_SELECTION_ADD:
case nsIAccessibleEvent::EVENT_SELECTION_REMOVE: {
AccSelChangeEvent* selEvent = downcast_accEvent(aEvent);
uint64_t widgetID = selEvent->Widget()->IsDoc() ? 0 :
reinterpret_cast<uintptr_t>(selEvent->Widget());
ipcDoc->SendSelectionEvent(id, widgetID, aEvent->GetEventType());
break;
}
default:
ipcDoc->SendEvent(id, aEvent->GetEventType());
}
+25
View File
@@ -259,6 +259,31 @@ DocAccessibleParent::RecvTextChangeEvent(const uint64_t& aID,
return true;
}
bool
DocAccessibleParent::RecvSelectionEvent(const uint64_t& aID,
const uint64_t& aWidgetID,
const uint32_t& aType)
{
ProxyAccessible* target = GetAccessible(aID);
ProxyAccessible* widget = GetAccessible(aWidgetID);
if (!target || !widget) {
NS_ERROR("invalid id in selection event");
return true;
}
ProxySelectionEvent(target, widget, aType);
if (!nsCoreUtils::AccEventObserversExist()) {
return true;
}
xpcAccessibleGeneric* xpcTarget = GetXPCAccessible(target);
xpcAccessibleDocument* xpcDoc = GetAccService()->GetXPCDocument(this);
RefPtr<xpcAccEvent> event = new xpcAccEvent(aType, xpcTarget, xpcDoc,
nullptr, false);
nsCoreUtils::DispatchAccEvent(Move(event));
return true;
}
bool
DocAccessibleParent::RecvBindChildDoc(PDocAccessibleParent* aChildDoc, const uint64_t& aID)
{
+4
View File
@@ -64,6 +64,10 @@ public:
const bool& aIsInsert,
const bool& aFromUser) override;
virtual bool RecvSelectionEvent(const uint64_t& aID,
const uint64_t& aWidgetID,
const uint32_t& aType) override;
virtual bool RecvBindChildDoc(PDocAccessibleParent* aChildDoc, const uint64_t& aID) override;
void Unbind()
{
+1
View File
@@ -61,6 +61,7 @@ parent:
async CaretMoveEvent(uint64_t aID, int32_t aOffset);
async TextChangeEvent(uint64_t aID, nsString aStr, int32_t aStart, uint32_t aLen,
bool aIsInsert, bool aFromUser);
async SelectionEvent(uint64_t aID, uint64_t aWidgetID, uint32_t aType);
/*
* Tell the parent document to bind the existing document as a new child
+5
View File
@@ -106,6 +106,11 @@ void
ProxyShowHideEvent(ProxyAccessible*, ProxyAccessible*, bool, bool)
{
}
void
ProxySelectionEvent(ProxyAccessible*, ProxyAccessible*, uint32_t)
{
}
} // namespace a11y
} // namespace mozilla
+5
View File
@@ -54,3 +54,8 @@ void
a11y::ProxyShowHideEvent(ProxyAccessible*, ProxyAccessible*, bool, bool)
{
}
void
a11y::ProxySelectionEvent(ProxyAccessible*, ProxyAccessible*, uint32_t)
{
}
+7
View File
@@ -127,3 +127,10 @@ a11y::ProxyShowHideEvent(ProxyAccessible* aTarget, ProxyAccessible*, bool aInser
AccessibleWrap* wrapper = WrapperFor(aTarget);
AccessibleWrap::FireWinEvent(wrapper, event);
}
void
a11y::ProxySelectionEvent(ProxyAccessible* aTarget, ProxyAccessible*, uint32_t aType)
{
AccessibleWrap* wrapper = WrapperFor(aTarget);
AccessibleWrap::FireWinEvent(wrapper, aType);
}
+1
View File
@@ -17,6 +17,7 @@ const BUILTIN_SIDEBAR_MENUITEMS = exports.BUILTIN_SIDEBAR_MENUITEMS = [
'menu_socialSidebar',
'menu_historySidebar',
'menu_bookmarksSidebar',
'menu_tabsSidebar',
];
function isSidebarShowing(window) {
+3 -1
View File
@@ -218,7 +218,9 @@
key="key_gotoHistory"
observes="viewHistorySidebar"
label="&historyButton.label;"/>
<menuitem id="menu_tabsSidebar"
observes="viewTabsSidebar"
label="&syncedTabs.sidebar.label;"/>
</menupopup>
</menu>
<menuseparator/>
+4
View File
@@ -174,6 +174,10 @@
<broadcaster id="sync-setup-state"/>
<broadcaster id="sync-syncnow-state"/>
<broadcaster id="sync-reauth-state" hidden="true"/>
<broadcaster id="viewTabsSidebar" autoCheck="false" sidebartitle="&syncedTabs.sidebar.label;"
type="checkbox" group="sidebar"
sidebarurl="chrome://browser/content/syncedtabs/sidebar.xhtml"
oncommand="SidebarUI.toggle('viewTabsSidebar');"/>
#endif
<broadcaster id="workOfflineMenuitemState"/>
+1 -1
View File
@@ -178,7 +178,7 @@ var SidebarUI = {
return new Promise((resolve, reject) => {
let sidebarBroadcaster = document.getElementById(commandID);
if (!sidebarBroadcaster || sidebarBroadcaster.localName != "broadcaster") {
reject(new Error("Invalid sidebar broadcaster specified"));
reject(new Error("Invalid sidebar broadcaster specified: " + commandID));
return;
}
+12
View File
@@ -370,6 +370,18 @@
<tooltip id="dynamic-shortcut-tooltip"
onpopupshowing="UpdateDynamicShortcutTooltipText(this);"/>
<menupopup id="SyncedTabsSidebarContext">
<menuitem label="&syncedTabs.context.openTab.label;"
accesskey="&syncedTabs.context.openTab.accesskey;"
id="syncedTabsOpenSelected"/>
<menuitem label="&syncedTabs.context.bookmarkSingleTab.label;"
accesskey="&syncedTabs.context.bookmarkSingleTab.accesskey;"
id="syncedTabsBookmarkSelected"/>
<menuseparator/>
<menuitem label="&syncedTabs.context.refreshList.label;"
accesskey="&syncedTabs.context.refreshList.accesskey;"
id="syncedTabsRefresh"/>
</menupopup>
</popupset>
#ifdef CAN_DRAW_IN_TITLEBAR
+1
View File
@@ -22,6 +22,7 @@ DIRS += [
'sessionstore',
'shell',
'selfsupport',
'syncedtabs',
'statusbar',
]
@@ -0,0 +1,45 @@
/* 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 {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
this.EXPORTED_SYMBOLS = [
"EventEmitter"
];
// Simple event emitter abstraction for storage objects to use.
function EventEmitter () {
this._events = new Map();
}
EventEmitter.prototype = {
on(event, listener) {
if (this._events.has(event)) {
this._events.get(event).add(listener);
} else {
this._events.set(event, new Set([listener]));
}
},
off(event, listener) {
if (!this._events.has(event)) {
return;
}
this._events.get(event).delete(listener);
},
emit(event, ...args) {
if (!this._events.has(event)) {
return;
}
for (let listener of this._events.get(event).values()) {
try {
listener.apply(this, args);
} catch (e) {
Cu.reportError(e);
}
}
},
};
@@ -0,0 +1,165 @@
/* 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 {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource:///modules/syncedtabs/SyncedTabsDeckStore.js");
Cu.import("resource:///modules/syncedtabs/SyncedTabsDeckView.js");
Cu.import("resource:///modules/syncedtabs/SyncedTabsListStore.js");
Cu.import("resource:///modules/syncedtabs/TabListComponent.js");
Cu.import("resource:///modules/syncedtabs/TabListView.js");
let { getChromeWindow } = Cu.import("resource:///modules/syncedtabs/util.js", {});
XPCOMUtils.defineLazyGetter(this, "FxAccountsCommon", function () {
return Components.utils.import("resource://gre/modules/FxAccountsCommon.js", {});
});
let log = Cu.import("resource://gre/modules/Log.jsm", {})
.Log.repository.getLogger("Sync.RemoteTabs");
this.EXPORTED_SYMBOLS = [
"SyncedTabsDeckComponent"
];
/* SyncedTabsDeckComponent
* This component instantiates views and storage objects as well as defines
* behaviors that will be passed down to the views. This helps keep the views
* isolated and easier to test.
*/
function SyncedTabsDeckComponent({
window, SyncedTabs, fxAccounts, deckStore, listStore, listComponent, DeckView, getChromeWindowMock,
}) {
this._window = window;
this._SyncedTabs = SyncedTabs;
this._fxAccounts = fxAccounts;
this._DeckView = DeckView || SyncedTabsDeckView;
// used to stub during tests
this._getChromeWindow = getChromeWindowMock || getChromeWindow;
this._deckStore = deckStore || new SyncedTabsDeckStore();
this._syncedTabsListStore = listStore || new SyncedTabsListStore(SyncedTabs);
this.tabListComponent = listComponent || new TabListComponent({
window: this._window,
store: this._syncedTabsListStore,
View: TabListView,
SyncedTabs: SyncedTabs
});
};
SyncedTabsDeckComponent.prototype = {
PANELS: {
TABS_CONTAINER: "tabs-container",
TABS_FETCHING: "tabs-fetching",
NOT_AUTHED_INFO: "notAuthedInfo",
SINGLE_DEVICE_INFO: "singleDeviceInfo",
TABS_DISABLED: "tabs-disabled",
},
get container() {
return this._deckView ? this._deckView.container : null;
},
init() {
Services.obs.addObserver(this, this._SyncedTabs.TOPIC_TABS_CHANGED, false);
Services.obs.addObserver(this, FxAccountsCommon.ONLOGIN_NOTIFICATION, false);
// Go ahead and trigger sync
this._SyncedTabs.syncTabs()
.catch(Cu.reportError);
this._deckView = new this._DeckView(this._window, this.tabListComponent, {
onAndroidClick: event => this.openAndroidLink(event),
oniOSClick: event => this.openiOSLink(event),
onSyncPrefClick: event => this.openSyncPrefs(event)
});
this._deckStore.on("change", state => this._deckView.render(state));
// Trigger the initial rendering of the deck view
this._deckStore.setPanels(Object.values(this.PANELS));
// Set the initial panel to display
this.updatePanel();
},
uninit() {
Services.obs.removeObserver(this, this._SyncedTabs.TOPIC_TABS_CHANGED);
Services.obs.removeObserver(this, FxAccountsCommon.ONLOGIN_NOTIFICATION);
this._deckView.destroy();
},
observe(subject, topic, data) {
switch (topic) {
case this._SyncedTabs.TOPIC_TABS_CHANGED:
this._syncedTabsListStore.getData();
this.updatePanel();
break;
case FxAccountsCommon.ONLOGIN_NOTIFICATION:
this.updatePanel();
break;
default:
break;
}
},
// There's no good way to mock fxAccounts in browser tests where it's already
// been instantiated, so we have this method for stubbing.
_accountStatus() {
return this._fxAccounts.accountStatus();
},
getPanelStatus() {
return this._accountStatus().then(exists => {
if (!exists) {
return this.PANELS.NOT_AUTHED_INFO;
}
if (!this._SyncedTabs.isConfiguredToSyncTabs) {
return this.PANELS.TABS_DISABLED;
}
if (!this._SyncedTabs.hasSyncedThisSession) {
return this.PANELS.TABS_FETCHING;
}
return this._SyncedTabs.getTabClients().then(clients => {
if (clients.length) {
return this.PANELS.TABS_CONTAINER;
}
return this.PANELS.SINGLE_DEVICE_INFO;
});
})
.catch(err => {
Cu.reportError(err);
return this.PANELS.NOT_AUTHED_INFO;
});
},
updatePanel() {
// return promise for tests
return this.getPanelStatus()
.then(panelId => this._deckStore.selectPanel(panelId))
.catch(Cu.reportError);
},
openAndroidLink(event) {
let href = Services.prefs.getCharPref("identity.mobilepromo.android") + "synced-tabs-sidebar";
this._openUrl(href, event);
},
openiOSLink(event) {
let href = Services.prefs.getCharPref("identity.mobilepromo.ios") + "synced-tabs-sidebar";
this._openUrl(href, event);
},
_openUrl(url, event) {
this._window.openUILink(url, event);
},
openSyncPrefs() {
this._getChromeWindow(this._window).gSyncUI.openSetup();
}
};
@@ -0,0 +1,60 @@
/* 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 {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
let { EventEmitter } = Cu.import("resource:///modules/syncedtabs/EventEmitter.jsm", {});
this.EXPORTED_SYMBOLS = [
"SyncedTabsDeckStore"
];
/**
* SyncedTabsDeckStore
*
* This store keeps track of the deck view state, including the panels and which
* one is selected. The view listens for change events on the store, which are
* triggered whenever the state changes. If it's a small change, the state
* will have `isUpdatable` set to true so the view can skip rerendering the whole
* DOM.
*/
function SyncedTabsDeckStore() {
EventEmitter.call(this);
this._panels = [];
}
Object.assign(SyncedTabsDeckStore.prototype, EventEmitter.prototype, {
_change(isUpdatable = false) {
let panels = this._panels.map(panel => {
return {id: panel, selected: panel === this._selectedPanel};
});
this.emit("change", {panels, isUpdatable: isUpdatable});
},
/**
* Sets the selected panelId and triggers a change event.
* @param {String} panelId - ID of the panel to select.
*/
selectPanel(panelId) {
if (this._panels.indexOf(panelId) === -1 || this._selectedPanel === panelId) {
return;
}
this._selectedPanel = panelId;
this._change(true);
},
/**
* Update the set of panels in the deck and trigger a change event.
* @param {Array} panels - an array of IDs for each panel in the deck.
*/
setPanels(panels) {
if (panels === this._panels) {
return;
}
this._panels = panels || [];
this._change();
}
});
@@ -0,0 +1,111 @@
/* 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 {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
let { getChromeWindow } = Cu.import("resource:///modules/syncedtabs/util.js", {});
let log = Cu.import("resource://gre/modules/Log.jsm", {})
.Log.repository.getLogger("Sync.RemoteTabs");
this.EXPORTED_SYMBOLS = [
"SyncedTabsDeckView"
];
/**
* SyncedTabsDeckView
*
* Instances of SyncedTabsDeckView render DOM nodes from a given state.
* No state is kept internaly and the DOM will completely
* rerender unless the state flags `isUpdatable`, which helps
* make small changes without the overhead of a full rerender.
*/
const SyncedTabsDeckView = function (window, tabListComponent, props) {
this.props = props;
this._window = window;
this._doc = window.document;
this._tabListComponent = tabListComponent;
this._deckTemplate = this._doc.getElementById("deck-template");
this.container = this._doc.createElement("div");
};
SyncedTabsDeckView.prototype = {
render(state) {
if (state.isUpdatable) {
this.update(state);
} else {
this.create(state);
}
},
create(state) {
let deck = this._doc.importNode(this._deckTemplate.content, true).firstElementChild;
this._clearChilden();
let tabListWrapper = this._doc.createElement("div");
tabListWrapper.className = "tabs-container sync-state";
this._tabListComponent.init();
tabListWrapper.appendChild(this._tabListComponent.container);
deck.appendChild(tabListWrapper);
this.container.appendChild(deck);
this._generateDevicePromo();
this._attachListeners();
this.update(state);
},
_getBrowserBundle() {
return getChromeWindow(this._window).document.getElementById("bundle_browser");
},
_generateDevicePromo() {
let bundle = this._getBrowserBundle();
let formatArgs = ["android", "ios"].map(os => {
let link = this._doc.createElement("a");
link.textContent = bundle.getString(`appMenuRemoteTabs.mobilePromo.${os}`)
link.className = `${os}-link text-link`;
link.setAttribute("href", "#");
return link.outerHTML;
});
// Put it all together...
let contents = bundle.getFormattedString("appMenuRemoteTabs.mobilePromo", formatArgs);
this.container.querySelector(".device-promo").innerHTML = contents;
},
destroy() {
this._tabListComponent.uninit();
this.container.remove();
},
update(state) {
for (let panel of state.panels) {
if (panel.selected) {
this.container.getElementsByClassName(panel.id).item(0).classList.add("selected");
} else {
this.container.getElementsByClassName(panel.id).item(0).classList.remove("selected");
}
}
},
_clearChilden() {
while (this.container.firstChild) {
this.container.removeChild(this.container.firstChild);
}
},
_attachListeners() {
this.container.querySelector(".android-link").addEventListener("click", this.props.onAndroidClick);
this.container.querySelector(".ios-link").addEventListener("click", this.props.oniOSClick);
let syncPrefLinks = this.container.querySelectorAll(".sync-prefs");
for (let link of syncPrefLinks) {
link.addEventListener("click", this.props.onSyncPrefClick);
}
},
};
@@ -0,0 +1,228 @@
/* 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 {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
let { EventEmitter } = Cu.import("resource:///modules/syncedtabs/EventEmitter.jsm", {});
this.EXPORTED_SYMBOLS = [
"SyncedTabsListStore"
];
/**
* SyncedTabsListStore
*
* Instances of this store encapsulate all of the state associated with a synced tabs list view.
* The state includes the clients, their tabs, the row that is currently selected,
* and the filtered query.
*/
function SyncedTabsListStore(SyncedTabs) {
EventEmitter.call(this);
this._SyncedTabs = SyncedTabs;
this.data = [];
this._closedClients = {};
this._selectedRow = [-1, -1];
this.filter = "";
this.inputFocused = false;
}
Object.assign(SyncedTabsListStore.prototype, EventEmitter.prototype, {
// This internal method triggers the "change" event that views
// listen for. It denormalizes the state so that it's easier for
// the view to deal with. updateType hints to the view what
// actually needs to be rerendered or just updated, and can be
// empty (to (re)render everything), "searchbox" (to rerender just the tab list),
// or "all" (to skip rendering and just update all attributes of existing nodes).
_change(updateType) {
let selectedParent = this._selectedRow[0];
let selectedChild = this._selectedRow[1];
let rowSelected = false;
// clone the data so that consumers can't mutate internal storage
let data = Cu.cloneInto(this.data, {});
let tabCount = 0;
data.forEach((client, index) => {
client.closed = !!this._closedClients[client.id];
if (rowSelected || selectedParent < 0) {
return;
}
if (this.filter) {
if (selectedParent < tabCount + client.tabs.length) {
client.tabs[selectedParent - tabCount].selected = true;
client.tabs[selectedParent - tabCount].focused = !this.inputFocused;
rowSelected = true;
} else {
tabCount += client.tabs.length;
}
return;
}
if (selectedParent === index && selectedChild === -1) {
client.selected = true;
client.focused = !this.inputFocused;
rowSelected = true;
} else if (selectedParent === index) {
client.tabs[selectedChild].selected = true;
client.tabs[selectedChild].focused = !this.inputFocused;
rowSelected = true;
}
});
// If this were React the view would be smart enough
// to not re-render the whole list unless necessary. But it's
// not, so updateType is a hint to the view of what actually
// needs to be rerendered.
this.emit("change", {
clients: data,
canUpdateAll: updateType === "all",
canUpdateInput: updateType === "searchbox",
filter: this.filter,
inputFocused: this.inputFocused
});
},
/**
* Moves the row selection from a child to its parent,
* which occurs when the parent of a selected row closes.
*/
_selectParentRow() {
this._selectedRow[1] = -1;
},
_toggleBranch(id, closed) {
this._closedClients[id] = closed;
if (this._closedClients[id]) {
this._selectParentRow();
}
this._change("all");
},
_isOpen(client) {
return !this._closedClients[client.id];
},
moveSelectionDown() {
let branchRow = this._selectedRow[0];
let childRow = this._selectedRow[1];
let branch = this.data[branchRow];
if (this.filter) {
this.selectRow(branchRow + 1);
return;
}
if (branchRow < 0) {
this.selectRow(0, -1);
} else if ((!branch.tabs.length || childRow >= branch.tabs.length - 1 || !this._isOpen(branch)) && branchRow < this.data.length) {
this.selectRow(branchRow + 1, -1);
} else if(childRow < branch.tabs.length) {
this.selectRow(branchRow, childRow + 1);
}
},
moveSelectionUp() {
let branchRow = this._selectedRow[0];
let childRow = this._selectedRow[1];
let branch = this.data[branchRow];
if (this.filter) {
this.selectRow(branchRow - 1);
return;
}
if (branchRow < 0) {
this.selectRow(0, -1);
} else if (childRow < 0 && branchRow > 0) {
let prevBranch = this.data[branchRow - 1];
let newChildRow = this._isOpen(prevBranch) ? prevBranch.tabs.length - 1 : -1;
this.selectRow(branchRow - 1, newChildRow);
} else if (childRow >= 0) {
this.selectRow(branchRow, childRow - 1);
}
},
// Selects a row and makes sure the selection is within bounds
selectRow(parent, child) {
let maxParentRow = this.filter ? this._tabCount() : this.data.length;
let parentRow = parent;
if (parent <= -1) {
parentRow = 0;
} else if (parent >= maxParentRow) {
parentRow = maxParentRow - 1;
}
let childRow = child;
if (parentRow === -1 || this.filter || typeof child === "undefined" || child < -1) {
childRow = -1;
} else if (child >= this.data[parentRow].tabs.length) {
childRow = this.data[parentRow].tabs.length - 1;
}
if (this._selectedRow[0] === parentRow && this._selectedRow[1] === childRow) {
return;
}
this._selectedRow = [parentRow, childRow];
this.inputFocused = false;
this._change("all");
},
_tabCount() {
return this.data.reduce((prev, curr) => curr.tabs.length + prev, 0);
},
toggleBranch(id) {
this._toggleBranch(id, !this._closedClients[id]);
},
closeBranch(id) {
this._toggleBranch(id, true);
},
openBranch(id) {
this._toggleBranch(id, false);
},
focusInput() {
this.inputFocused = true;
this._change("update");
},
blurInput() {
this.inputFocused = false;
this._change("update");
},
clearFilter() {
this.filter = "";
this._selectedRow = [-1, -1];
return this.getData();
},
// Fetches data from the SyncedTabs module and triggers
// and update
getData(filter) {
let updateType;
if (typeof filter !== "undefined") {
this.filter = filter;
this._selectedRow = [-1, -1];
// When a filter is specified we tell the view that only the list
// needs to be rerendered so that it doesn't disrupt the input
// field's focus.
updateType = "searchbox";
}
// return promise for tests
return this._SyncedTabs.getTabClients(this.filter)
.then(result => {
this.data = result;
this._change(updateType);
})
.catch(Cu.reportError);
}
});
@@ -0,0 +1,114 @@
/* 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 {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
let log = Cu.import("resource://gre/modules/Log.jsm", {})
.Log.repository.getLogger("Sync.RemoteTabs");
this.EXPORTED_SYMBOLS = [
"TabListComponent"
];
/**
* TabListComponent
*
* The purpose of this component is to compose the view, state, and actions.
* It defines high level actions that act on the state and passes them to the
* view for it to trigger during user interaction. It also subscribes the view
* to state changes so it can rerender.
*/
function TabListComponent({window, store, View, SyncedTabs}) {
this._window = window;
this._store = store;
this._View = View;
// used to trigger Sync from context menu
this._SyncedTabs = SyncedTabs;
}
TabListComponent.prototype = {
get container() {
return this._view.container;
},
init() {
log.debug("Initializing TabListComponent");
this._view = new this._View(this._window, {
onSelectRow: (...args) => this.onSelectRow(...args),
onOpenTab: (...args) => this.onOpenTab(...args),
onMoveSelectionDown: (...args) => this.onMoveSelectionDown(...args),
onMoveSelectionUp: (...args) => this.onMoveSelectionUp(...args),
onToggleBranch: (...args) => this.onToggleBranch(...args),
onBookmarkTab: (...args) => this.onBookmarkTab(...args),
onSyncRefresh: (...args) => this.onSyncRefresh(...args),
onFilter: (...args) => this.onFilter(...args),
onClearFilter: (...args) => this.onClearFilter(...args),
onFilterFocus: (...args) => this.onFilterFocus(...args),
onFilterBlur: (...args) => this.onFilterBlur(...args)
});
this._store.on("change", state => this._view.render(state));
this._view.render({clients: []});
// get what's already available...
this._store.getData();
this._store.focusInput();
},
uninit() {
this._view.destroy();
},
onFilter(query) {
this._store.getData(query);
},
onClearFilter() {
this._store.clearFilter();
},
onFilterFocus() {
this._store.focusInput();
},
onFilterBlur() {
this._store.blurInput();
},
onSelectRow(position, id) {
this._store.selectRow(position[0], position[1]);
if (id) {
this._store.toggleBranch(id);
}
},
onMoveSelectionDown() {
this._store.moveSelectionDown();
},
onMoveSelectionUp() {
this._store.moveSelectionUp();
},
onToggleBranch(id) {
this._store.toggleBranch(id);
},
onBookmarkTab(uri, title) {
this._window.top.PlacesCommandHook
.bookmarkLink(this._window.PlacesUtils.bookmarksMenuFolderId, uri, title)
.catch(Cu.reportError);
},
onOpenTab(url, event) {
this._window.openUILink(url, event);
},
onSyncRefresh() {
this._SyncedTabs.syncTabs(true);
}
};
@@ -0,0 +1,438 @@
/* 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 {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
Cu.import("resource://gre/modules/Services.jsm");
let { getChromeWindow } = Cu.import("resource:///modules/syncedtabs/util.js", {});
let log = Cu.import("resource://gre/modules/Log.jsm", {})
.Log.repository.getLogger("Sync.RemoteTabs");
this.EXPORTED_SYMBOLS = [
"TabListView"
];
function getContextMenu(window) {
return getChromeWindow(window).document.getElementById("SyncedTabsSidebarContext");
}
/*
* TabListView
*
* Given a state, this object will render the corresponding DOM.
* It maintains no state of it's own. It listens for DOM events
* and triggers actions that may cause the state to change and
* ultimately the view to rerender.
*/
function TabListView(window, props) {
this.props = props;
this._window = window;
this._doc = this._window.document;
this._tabsContainerTemplate = this._doc.getElementById("tabs-container-template");
this._clientTemplate = this._doc.getElementById("client-template");
this._emptyClientTemplate = this._doc.getElementById("empty-client-template");
this._tabTemplate = this._doc.getElementById("tab-template");
this.container = this._doc.createElement("div");
this._setupContextMenu();
};
TabListView.prototype = {
render(state) {
// Don't rerender anything; just update attributes, e.g. selection
if (state.canUpdateAll) {
this._update(state);
return;
}
// Rerender the tab list
if (state.canUpdateInput) {
this._updateSearchBox(state);
this._createList(state);
return;
}
// Create the world anew
this._create(state);
},
// Create the initial DOM from templates
_create(state) {
let wrapper = this._doc.importNode(this._tabsContainerTemplate.content, true).firstElementChild;
this._clearChilden();
this.container.appendChild(wrapper);
this.tabsFilter = this.container.querySelector(".tabsFilter");
this.clearFilter = this.container.querySelector(".textbox-search-clear");
this.searchBox = this.container.querySelector(".search-box");
this.list = this.container.querySelector(".list");
this.searchIcon = this.container.querySelector(".textbox-search-icon");
if (state.filter) {
this.tabsFilter.value = state.filter;
}
this._createList(state);
this._updateSearchBox(state);
this._attachListeners();
},
_createList(state) {
this._clearChilden(this.list);
for (let client of state.clients) {
if (state.filter) {
this._renderFilteredClient(client);
} else {
this._renderClient(client);
}
}
},
destroy() {
this._teardownContextMenu();
this.container.remove();
},
_update(state) {
this._updateSearchBox(state);
for (let client of state.clients) {
let clientNode = this._doc.getElementById("item-" + client.id);
if (clientNode) {
this._updateClient(client, clientNode);
}
client.tabs.forEach((tab, index) => {
let tabNode = this._doc.getElementById('tab-' + client.id + '-' + index);
this._updateTab(tab, tabNode, index);
});
}
},
// Client rows are hidden when the list is filtered
_renderFilteredClient(client, filter) {
client.tabs.forEach((tab, index) => {
let node = this._renderTab(client, tab, index);
this.list.appendChild(node);
});
},
_renderClient(client) {
let itemNode = client.tabs.length ?
this._createClient(client) :
this._createEmptyClient(client);
this._updateClient(client, itemNode);
let tabsList = itemNode.querySelector(".item-tabs-list");
client.tabs.forEach((tab, index) => {
let node = this._renderTab(client, tab, index);
tabsList.appendChild(node);
});
this.list.appendChild(itemNode);
return itemNode;
},
_renderTab(client, tab, index) {
let itemNode = this._createTab(tab);
this._updateTab(tab, itemNode, index);
return itemNode;
},
_createClient(item) {
return this._doc.importNode(this._clientTemplate.content, true).firstElementChild;
},
_createEmptyClient(item) {
return this._doc.importNode(this._emptyClientTemplate.content, true).firstElementChild;
},
_createTab(item) {
return this._doc.importNode(this._tabTemplate.content, true).firstElementChild;
},
_clearChilden(node) {
let parent = node || this.container;
while (parent.firstChild) {
parent.removeChild(parent.firstChild);
}
},
_attachListeners() {
this.list.addEventListener("click", this.onClick.bind(this));
this.list.addEventListener("keydown", this.onKeyDown.bind(this));
this.tabsFilter.addEventListener("input", this.onFilter.bind(this));
this.tabsFilter.addEventListener("focus", this.onFilterFocus.bind(this));
this.tabsFilter.addEventListener("blur", this.onFilterBlur.bind(this));
this.clearFilter.addEventListener("click", this.onClearFilter.bind(this));
this.searchIcon.addEventListener("click", this.onFilterFocus.bind(this));
},
_updateSearchBox(state) {
if (state.filter) {
this.searchBox.classList.add("filtered");
} else {
this.searchBox.classList.remove("filtered");
}
if (state.inputFocused) {
this.searchBox.setAttribute("focused", true);
this.tabsFilter.focus();
} else {
this.searchBox.removeAttribute("focused");
}
},
/**
* Update the element representing an item, ensuring it's in sync with the
* underlying data.
* @param {client} item - Item to use as a source.
* @param {Element} itemNode - Element to update.
*/
_updateClient(item, itemNode) {
itemNode.setAttribute("id", "item-" + item.id);
itemNode.setAttribute("title", item.name);
if (item.closed) {
itemNode.classList.add("closed");
} else {
itemNode.classList.remove("closed");
}
if (item.selected) {
itemNode.classList.add("selected");
} else {
itemNode.classList.remove("selected");
}
if (item.focused) {
itemNode.focus();
}
itemNode.dataset.id = item.id;
itemNode.querySelector(".item-title").textContent = item.name;
let icon = itemNode.querySelector(".item-icon-container");
icon.style.backgroundImage = "url(" + item.icon + ")";
},
/**
* Update the element representing a tab, ensuring it's in sync with the
* underlying data.
* @param {tab} item - Item to use as a source.
* @param {Element} itemNode - Element to update.
*/
_updateTab(item, itemNode, index) {
itemNode.setAttribute("title", `${item.title}\n${item.url}`);
itemNode.setAttribute("id", "tab-" + item.client + '-' + index);
if (item.selected) {
itemNode.classList.add("selected");
} else {
itemNode.classList.remove("selected");
}
if (item.focused) {
itemNode.focus();
}
itemNode.dataset.url = item.url;
itemNode.querySelector(".item-title").textContent = item.title;
let icon = itemNode.querySelector(".item-icon-container");
icon.style.backgroundImage = "url(" + item.icon + ")";
},
onClick(event) {
let itemNode = this._findParentItemNode(event.target);
if (!itemNode) {
return;
}
if (itemNode.classList.contains("tab")) {
let url = itemNode.dataset.url;
if (url) {
this.props.onOpenTab(url, event);
}
}
if (event.target.classList.contains("item-twisty-container")) {
this.props.onToggleBranch(itemNode.dataset.id);
return;
}
this._selectRow(itemNode);
},
_selectRow(itemNode) {
this.props.onSelectRow(this._getSelectionPosition(itemNode), itemNode.dataset.id);
},
/**
* Handle a keydown event on the list box.
* @param {Event} event - Triggering event.
*/
onKeyDown(event) {
if (event.keyCode == this._window.KeyEvent.DOM_VK_DOWN) {
event.preventDefault();
this.props.onMoveSelectionDown();
} else if (event.keyCode == this._window.KeyEvent.DOM_VK_UP) {
event.preventDefault();
this.props.onMoveSelectionUp();
} else if (event.keyCode == this._window.KeyEvent.DOM_VK_RETURN) {
let selectedNode = this.container.querySelector('.item.selected');
if (selectedNode.dataset.url) {
this.props.onOpenTab(selectedNode.dataset.url, event);
} else if (selectedNode) {
this.props.onToggleBranch(selectedNode.dataset.id);
}
}
},
onBookmarkTab() {
let item = this.container.querySelector('.item.selected');
if (!item || !item.dataset.url) {
return;
}
let uri = item.dataset.url;
let title = item.querySelector(".item-title").textContent;
this.props.onBookmarkTab(uri, title);
},
onOpenSelected(event) {
let item = this.container.querySelector('.item.selected');
if (this._isTab(item) && item.dataset.url) {
this.props.onOpenTab(item.dataset.url, event);
}
},
onFilter(event) {
let query = event.target.value;
this.props.onFilter(query);
},
onClearFilter() {
this.props.onClearFilter();
},
onFilterFocus() {
this.props.onFilterFocus();
},
onFilterBlur() {
this.props.onFilterBlur();
},
// Set up the custom context menu
_setupContextMenu() {
this._handleContentContextMenu = event =>
this.handleContentContextMenu(event);
this._handleContentContextMenuCommand = event =>
this.handleContentContextMenuCommand(event);
Services.els.addSystemEventListener(this._window, "contextmenu", this._handleContentContextMenu, false);
let menu = getContextMenu(this._window);
menu.addEventListener("command", this._handleContentContextMenuCommand, true);
},
_teardownContextMenu() {
// Tear down context menu
Services.els.removeSystemEventListener(this._window, "contextmenu", this._handleContentContextMenu, false);
let menu = getContextMenu(this._window);
menu.removeEventListener("command", this._handleContentContextMenuCommand, true);
},
handleContentContextMenuCommand(event) {
let id = event.target.getAttribute("id");
switch (id) {
case "syncedTabsOpenSelected":
this.onOpenSelected(event);
break;
case "syncedTabsBookmarkSelected":
this.onBookmarkTab();
break;
case "syncedTabsRefresh":
this.props.onSyncRefresh();
break;
}
},
handleContentContextMenu(event) {
let itemNode = this._findParentItemNode(event.target);
if (itemNode) {
this._selectRow(itemNode);
}
let menu = getContextMenu(this._window);
this.adjustContextMenu(menu);
menu.openPopupAtScreen(event.screenX, event.screenY, true, event);
},
adjustContextMenu(menu) {
let item = this.container.querySelector('.item.selected');
let showTabOptions = this._isTab(item);
let el = menu.firstChild;
while (el) {
if (showTabOptions || el.getAttribute("id") === "syncedTabsRefresh") {
el.hidden = false;
} else {
el.hidden = true;
}
el = el.nextSibling;
}
},
/**
* Find the parent item element, from a given child element.
* @param {Element} node - Child element.
* @return {Element} Element for the item, or null if not found.
*/
_findParentItemNode(node) {
while (node && node !== this.list && node !== this._doc.documentElement &&
!node.classList.contains("item")) {
node = node.parentNode;
}
if (node !== this.list && node !== this._doc.documentElement) {
return node;
}
return null;
},
_findParentBranchNode(node) {
while (node && !node.classList.contains("list") && node !== this._doc.documentElement &&
!node.parentNode.classList.contains("list")) {
node = node.parentNode;
}
if (node !== this.list && node !== this._doc.documentElement) {
return node;
}
return null;
},
_getSelectionPosition(itemNode) {
let parent = this._findParentBranchNode(itemNode);
let parentPosition = this._indexOfNode(parent.parentNode, parent);
let childPosition = -1;
// if the node is not a client, find its position within the parent
if (parent !== itemNode) {
childPosition = this._indexOfNode(itemNode.parentNode, itemNode);
}
return [parentPosition, childPosition];
},
_indexOfNode(parent, child) {
return Array.prototype.indexOf.call(parent.childNodes, child);
},
_isTab(item) {
return item && item.classList.contains("tab");
}
};
+7
View File
@@ -0,0 +1,7 @@
# 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/.
browser.jar:
content/browser/syncedtabs/sidebar.xhtml
content/browser/syncedtabs/sidebar.js
+24
View File
@@ -0,0 +1,24 @@
# 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/.
JAR_MANIFESTS += ['jar.mn']
BROWSER_CHROME_MANIFESTS += ['test/browser/browser.ini']
XPCSHELL_TESTS_MANIFESTS += ['test/xpcshell/xpcshell.ini']
EXTRA_JS_MODULES.syncedtabs += [
'EventEmitter.jsm',
'SyncedTabsDeckComponent.js',
'SyncedTabsDeckStore.js',
'SyncedTabsDeckView.js',
'SyncedTabsListStore.js',
'TabListComponent.js',
'TabListView.js',
'util.js',
]
with Files('**'):
BUG_COMPONENT = ('Firefox', 'Synced tabs')
+30
View File
@@ -0,0 +1,30 @@
/* 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 {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://services-sync/SyncedTabs.jsm");
Cu.import("resource:///modules/syncedtabs/SyncedTabsDeckComponent.js");
XPCOMUtils.defineLazyModuleGetter(this, "fxAccounts",
"resource://gre/modules/FxAccounts.jsm");
this.syncedTabsDeckComponent = new SyncedTabsDeckComponent({window, SyncedTabs, fxAccounts});
let onLoaded = () => {
syncedTabsDeckComponent.init();
document.body.appendChild(syncedTabsDeckComponent.container);
};
let onUnloaded = () => {
removeEventListener("DOMContentLoaded", onLoaded);
removeEventListener("unload", onUnloaded);
syncedTabsDeckComponent.uninit();
};
addEventListener("DOMContentLoaded", onLoaded);
addEventListener("unload", onUnloaded);
@@ -0,0 +1,99 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- 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/. -->
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd" [
<!ENTITY % browserDTD SYSTEM "chrome://browser/locale/browser.dtd">
%browserDTD;
]>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
xml:lang="en" lang="en">
<head>
<script src="chrome://browser/content/syncedtabs/sidebar.js" type="application/javascript;version=1.8"></script>
<script type="application/javascript" src="chrome://browser/content/utilityOverlay.js"/>
<link rel="stylesheet" type="text/css" media="all" href="chrome://browser/skin/syncedtabs/sidebar.css"/>
<link rel="stylesheet" type="text/css" media="all" href="chrome://global/skin/"/>
<link rel="stylesheet" type="text/css" media="all" href="chrome://global/skin/textbox.css"/>
<title>&syncedTabs.sidebar.label;</title>
</head>
<body role="application">
<template id="client-template">
<div class="item client" role="option" tabindex="-1">
<div class="item-title-container">
<div class="item-twisty-container"></div>
<div class="item-icon-container"></div>
<p class="item-title"></p>
</div>
<div class="item-tabs-list"></div>
</div>
</template>
<template id="empty-client-template">
<div class="item empty client" role="option" tabindex="-1">
<div class="item-title-container">
<div class="item-twisty-container"></div>
<div class="item-icon-container"></div>
<p class="item-title"></p>
</div>
<div class="item-tabs-list">
<div class="item empty" role="option" tabindex="-1">
<div class="item-title-container">
<div class="item-icon-container"></div>
<p class="item-title">&syncedTabs.sidebar.notabs.label;</p>
</div>
</div>
</div>
</div>
</template>
<template id="tab-template">
<div class="item tab" role="option" tabindex="-1">
<div class="item-title-container">
<div class="item-icon-container"></div>
<p class="item-title"></p>
</div>
</div>
</template>
<template id="tabs-container-template">
<div class="tabs-container">
<div class="sidebar-search-container">
<div class="search-box compact">
<div class="textbox-input-box">
<input type="text" class="tabsFilter textbox-input"/>
<div class="textbox-search-icons">
<a class="textbox-search-clear"></a>
<a class="textbox-search-icon"></a>
</div>
</div>
</div>
</div>
<div class="list" role="listbox" tabindex="1"></div>
</div>
</template>
<template id="deck-template">
<div class="deck">
<div class="tabs-fetching sync-state">
<p>&syncedTabs.sidebar.fetching.label;</p>
</div>
<div class="notAuthedInfo sync-state">
<p>&syncedTabs.sidebar.notsignedin.label;</p>
<p><a href="#" class="sync-prefs text-link">&fxaSignIn.label;</a></p>
</div>
<div class="singleDeviceInfo sync-state">
<p>&syncedTabs.sidebar.noclients.label;</p>
<p class="device-promo"></p>
</div>
<div class="tabs-disabled sync-state">
<p>&syncedTabs.sidebar.tabsnotsyncing.label;</p>
<p><a href="#" class="sync-prefs text-link">&syncedTabs.sidebar.openprefs.label;</a></p>
</div>
</div>
</template>
</body>
</html>
@@ -0,0 +1,4 @@
[DEFAULT]
support-files = head.js
[browser_sidebar_syncedtabslist.js]
@@ -0,0 +1,410 @@
"use strict";
const FIXTURE = [
{
"id": "7cqCr77ptzX3",
"type": "client",
"name": "zcarter's Nightly on MacBook-Pro-25",
"icon": "chrome://browser/skin/sync-desktopIcon.png",
"tabs": [
{
"type": "tab",
"title": "Firefox for Android — Mobile Web browser — More ways to customize and protect your privacy — Mozilla",
"url": "https://www.mozilla.org/en-US/firefox/android/?utm_source=firefox-browser&utm_medium=firefox-browser&utm_campaign=synced-tabs-sidebar",
"icon": "chrome://mozapps/skin/places/defaultFavicon.png",
"client": "7cqCr77ptzX3",
"lastUsed": 1452124677
}
]
},
{
"id": "2xU5h-4bkWqA",
"type": "client",
"name": "laptop",
"icon": "chrome://browser/skin/sync-desktopIcon.png",
"tabs": [
{
"type": "tab",
"title": "Firefox for iOS — Mobile Web browser for your iPhone, iPad and iPod touch — Mozilla",
"url": "https://www.mozilla.org/en-US/firefox/ios/?utm_source=firefox-browser&utm_medium=firefox-browser&utm_campaign=synced-tabs-sidebar",
"icon": "moz-anno:favicon:https://www.mozilla.org/media/img/firefox/favicon.dc6635050bf5.ico",
"client": "2xU5h-4bkWqA",
"lastUsed": 1451519425
},
{
"type": "tab",
"title": "Firefox Nightly First Run Page",
"url": "https://www.mozilla.org/en-US/firefox/nightly/firstrun/?oldversion=45.0a1",
"icon": "moz-anno:favicon:https://www.mozilla.org/media/img/firefox/favicon-nightly.560395bbb2e1.png",
"client": "2xU5h-4bkWqA",
"lastUsed": 1451519420
},
{
// Should appear first for this client.
"type": "tab",
"title": "Mozilla Developer Network",
"url": "https://developer.mozilla.org/en-US/",
"icon": "moz-anno:favicon:https://developer.cdn.mozilla.net/static/img/favicon32.e02854fdcf73.png",
"client": "2xU5h-4bkWqA",
"lastUsed": 1451519725
}
]
},
{
"id": "OL3EJCsdb2JD",
"type": "client",
"name": "desktop",
"icon": "chrome://browser/skin/sync-desktopIcon.png",
"tabs": []
}
];
let originalSyncedTabsInternal = null;
function* testClean() {
let syncedTabsDeckComponent = window.SidebarUI.browser.contentWindow.syncedTabsDeckComponent;
let SyncedTabs = window.SidebarUI.browser.contentWindow.SyncedTabs;
syncedTabsDeckComponent._accountStatus.restore();
SyncedTabs._internal.getTabClients.restore();
SyncedTabs._internal = originalSyncedTabsInternal;
yield new Promise(resolve => {
window.SidebarUI.browser.contentWindow.addEventListener("unload", function listener() {
window.SidebarUI.browser.contentWindow.removeEventListener("unload", listener);
resolve();
});
SidebarUI.hide();
});
}
add_task(function* testSyncedTabsSidebarList() {
yield SidebarUI.show('viewTabsSidebar');
Assert.equal(SidebarUI.currentID, "viewTabsSidebar", "Sidebar should have SyncedTabs loaded");
let syncedTabsDeckComponent = SidebarUI.browser.contentWindow.syncedTabsDeckComponent;
let SyncedTabs = SidebarUI.browser.contentWindow.SyncedTabs;
Assert.ok(syncedTabsDeckComponent, "component exists");
originalSyncedTabsInternal = SyncedTabs._internal;
SyncedTabs._internal = {
isConfiguredToSyncTabs: true,
hasSyncedThisSession: true,
getTabClients() { return Promise.resolve([])},
syncTabs() {return Promise.resolve();},
};
sinon.stub(syncedTabsDeckComponent, "_accountStatus", ()=> Promise.resolve(true));
sinon.stub(SyncedTabs._internal, "getTabClients", ()=> Promise.resolve(Cu.cloneInto(FIXTURE, {})));
yield syncedTabsDeckComponent.updatePanel();
// This is a hacky way of waiting for the view to render. The view renders
// after the following promise (a different instance of which is triggered
// in updatePanel) resolves, so we wait for it here as well
yield syncedTabsDeckComponent.tabListComponent._store.getData();
Assert.ok(SyncedTabs._internal.getTabClients.called, "get clients called");
let selectedPanel = syncedTabsDeckComponent.container.querySelector(".sync-state.selected");
Assert.ok(selectedPanel.classList.contains("tabs-container"),
"tabs panel is selected");
Assert.equal(selectedPanel.querySelectorAll(".tab").length, 4,
"four tabs listed");
Assert.equal(selectedPanel.querySelectorAll(".client").length, 3,
"three clients listed");
Assert.equal(selectedPanel.querySelectorAll(".client")[2].querySelectorAll(".empty").length, 1,
"third client is empty");
// Verify that the tabs are sorted by last used time.
var expectedTabIndices = [[0], [2, 0, 1]];
Array.prototype.forEach.call(selectedPanel.querySelectorAll(".client"), (clientNode, i) => {
checkItem(clientNode, FIXTURE[i]);
Array.prototype.forEach.call(clientNode.querySelectorAll(".tab"), (tabNode, j) => {
let tabIndex = expectedTabIndices[i][j];
checkItem(tabNode, FIXTURE[i].tabs[tabIndex]);
});
});
});
add_task(testClean);
add_task(function* testSyncedTabsSidebarFilteredList() {
yield SidebarUI.show('viewTabsSidebar');
let syncedTabsDeckComponent = window.SidebarUI.browser.contentWindow.syncedTabsDeckComponent;
let SyncedTabs = window.SidebarUI.browser.contentWindow.SyncedTabs;
Assert.ok(syncedTabsDeckComponent, "component exists");
originalSyncedTabsInternal = SyncedTabs._internal;
SyncedTabs._internal = {
isConfiguredToSyncTabs: true,
hasSyncedThisSession: true,
getTabClients() { return Promise.resolve([])},
syncTabs() {return Promise.resolve();},
};
sinon.stub(syncedTabsDeckComponent, "_accountStatus", ()=> Promise.resolve(true));
sinon.stub(SyncedTabs._internal, "getTabClients", ()=> Promise.resolve(Cu.cloneInto(FIXTURE, {})));
yield syncedTabsDeckComponent.updatePanel();
// This is a hacky way of waiting for the view to render. The view renders
// after the following promise (a different instance of which is triggered
// in updatePanel) resolves, so we wait for it here as well
yield syncedTabsDeckComponent.tabListComponent._store.getData();
let filterInput = syncedTabsDeckComponent._window.document.querySelector(".tabsFilter");
filterInput.value = "filter text";
filterInput.blur();
yield syncedTabsDeckComponent.tabListComponent._store.getData("filter text");
let selectedPanel = syncedTabsDeckComponent.container.querySelector(".sync-state.selected");
Assert.ok(selectedPanel.classList.contains("tabs-container"),
"tabs panel is selected");
Assert.equal(selectedPanel.querySelectorAll(".tab").length, 4,
"four tabs listed");
Assert.equal(selectedPanel.querySelectorAll(".client").length, 0,
"no clients are listed");
Assert.equal(filterInput.value, "filter text",
"filter text box has correct value");
// Tabs should not be sorted when filter is active.
let FIXTURE_TABS = FIXTURE.reduce((prev, client) => prev.concat(client.tabs), []);
Array.prototype.forEach.call(selectedPanel.querySelectorAll(".tab"), (tabNode, i) => {
checkItem(tabNode, FIXTURE_TABS[i]);
});
// Removing the filter should resort tabs.
FIXTURE_TABS.sort((a, b) => b.lastUsed - a.lastUsed);
yield syncedTabsDeckComponent.tabListComponent._store.getData();
Array.prototype.forEach.call(selectedPanel.querySelectorAll(".tab"), (tabNode, i) => {
checkItem(tabNode, FIXTURE_TABS[i]);
});
});
add_task(testClean);
add_task(function* testSyncedTabsSidebarStatus() {
let accountExists = false;
yield SidebarUI.show('viewTabsSidebar');
let syncedTabsDeckComponent = window.SidebarUI.browser.contentWindow.syncedTabsDeckComponent;
let SyncedTabs = window.SidebarUI.browser.contentWindow.SyncedTabs;
originalSyncedTabsInternal = SyncedTabs._internal;
SyncedTabs._internal = {
isConfiguredToSyncTabs: false,
hasSyncedThisSession: false,
getTabClients() {},
syncTabs() {return Promise.resolve();},
};
Assert.ok(syncedTabsDeckComponent, "component exists");
sinon.spy(syncedTabsDeckComponent, "updatePanel");
sinon.spy(syncedTabsDeckComponent, "observe");
sinon.stub(syncedTabsDeckComponent, "_accountStatus", ()=> Promise.reject("Test error"));
yield syncedTabsDeckComponent.updatePanel();
let selectedPanel = syncedTabsDeckComponent.container.querySelector(".sync-state.selected");
Assert.ok(selectedPanel.classList.contains("notAuthedInfo"),
"not-authed panel is selected on auth error");
syncedTabsDeckComponent._accountStatus.restore();
sinon.stub(syncedTabsDeckComponent, "_accountStatus", ()=> Promise.resolve(accountExists));
yield syncedTabsDeckComponent.updatePanel();
selectedPanel = syncedTabsDeckComponent.container.querySelector(".sync-state.selected");
Assert.ok(selectedPanel.classList.contains("notAuthedInfo"),
"not-authed panel is selected");
accountExists = true;
yield syncedTabsDeckComponent.updatePanel();
selectedPanel = syncedTabsDeckComponent.container.querySelector(".sync-state.selected");
Assert.ok(selectedPanel.classList.contains("tabs-disabled"),
"tabs disabled panel is selected");
SyncedTabs._internal.isConfiguredToSyncTabs = true;
yield syncedTabsDeckComponent.updatePanel();
selectedPanel = syncedTabsDeckComponent.container.querySelector(".sync-state.selected");
Assert.ok(selectedPanel.classList.contains("tabs-fetching"),
"tabs fetch panel is selected");
SyncedTabs._internal.hasSyncedThisSession = true;
sinon.stub(SyncedTabs._internal, "getTabClients", ()=> Promise.resolve([]));
yield syncedTabsDeckComponent.updatePanel();
selectedPanel = syncedTabsDeckComponent.container.querySelector(".sync-state.selected");
Assert.ok(selectedPanel.classList.contains("singleDeviceInfo"),
"tabs fetch panel is selected");
SyncedTabs._internal.getTabClients.restore();
sinon.stub(SyncedTabs._internal, "getTabClients", ()=> Promise.resolve([{id: "mock"}]));
yield syncedTabsDeckComponent.updatePanel();
selectedPanel = syncedTabsDeckComponent.container.querySelector(".sync-state.selected");
Assert.ok(selectedPanel.classList.contains("tabs-container"),
"tabs panel is selected");
});
add_task(testClean);
add_task(function* testSyncedTabsSidebarContextMenu() {
yield SidebarUI.show('viewTabsSidebar');
let syncedTabsDeckComponent = window.SidebarUI.browser.contentWindow.syncedTabsDeckComponent;
let SyncedTabs = window.SidebarUI.browser.contentWindow.SyncedTabs;
Assert.ok(syncedTabsDeckComponent, "component exists");
originalSyncedTabsInternal = SyncedTabs._internal;
SyncedTabs._internal = {
isConfiguredToSyncTabs: true,
hasSyncedThisSession: true,
getTabClients() { return Promise.resolve([])},
syncTabs() {return Promise.resolve();},
};
sinon.stub(syncedTabsDeckComponent, "_accountStatus", ()=> Promise.resolve(true));
sinon.stub(SyncedTabs._internal, "getTabClients", ()=> Promise.resolve(Cu.cloneInto(FIXTURE, {})));
yield syncedTabsDeckComponent.updatePanel();
// This is a hacky way of waiting for the view to render. The view renders
// after the following promise (a different instance of which is triggered
// in updatePanel) resolves, so we wait for it here as well
yield syncedTabsDeckComponent.tabListComponent._store.getData();
info("Right-clicking the search box should show text-related actions");
let filterMenuItems = [
"menuitem[cmd=cmd_undo]",
"menuseparator",
// We don't check whether the commands are enabled due to platform
// differences. On OS X and Windows, "cut" and "copy" are always enabled
// for HTML inputs; on Linux, they're only enabled if text is selected.
"menuitem[cmd=cmd_cut]",
"menuitem[cmd=cmd_copy]",
"menuitem[cmd=cmd_paste]",
"menuitem[cmd=cmd_delete]",
"menuseparator",
"menuitem[cmd=cmd_selectAll]",
"menuseparator",
"menuitem#syncedTabsRefreshFilter",
];
yield* testContextMenu(syncedTabsDeckComponent,
"#SyncedTabsSidebarTabsFilterContext",
".tabsFilter",
filterMenuItems);
info("Right-clicking a tab should show additional actions");
let tabMenuItems = [
["menuitem#syncedTabsOpenSelected", { hidden: false }],
["menuitem#syncedTabsOpenSelectedInTab", { hidden: false }],
["menuitem#syncedTabsOpenSelectedInWindow", { hidden: false }],
["menuitem#syncedTabsOpenSelectedInPrivateWindow", { hidden: false }],
["menuseparator", { hidden: false }],
["menuitem#syncedTabsBookmarkSelected", { hidden: false }],
["menuitem#syncedTabsCopySelected", { hidden: false }],
["menuseparator", { hidden: false }],
["menuitem#syncedTabsRefresh", { hidden: false }],
];
yield* testContextMenu(syncedTabsDeckComponent,
"#SyncedTabsSidebarContext",
"#tab-7cqCr77ptzX3-0",
tabMenuItems);
info("Right-clicking a client shouldn't show any actions");
let sidebarMenuItems = [
["menuitem#syncedTabsOpenSelected", { hidden: true }],
["menuitem#syncedTabsOpenSelectedInTab", { hidden: true }],
["menuitem#syncedTabsOpenSelectedInWindow", { hidden: true }],
["menuitem#syncedTabsOpenSelectedInPrivateWindow", { hidden: true }],
["menuseparator", { hidden: true }],
["menuitem#syncedTabsBookmarkSelected", { hidden: true }],
["menuitem#syncedTabsCopySelected", { hidden: true }],
["menuseparator", { hidden: true }],
["menuitem#syncedTabsRefresh", { hidden: false }],
];
yield* testContextMenu(syncedTabsDeckComponent,
"#SyncedTabsSidebarContext",
"#item-OL3EJCsdb2JD",
sidebarMenuItems);
});
add_task(testClean);
function checkItem(node, item) {
Assert.ok(node.classList.contains("item"),
"Node should have .item class");
if (item.client) {
// tab items
Assert.equal(node.querySelector(".item-title").textContent, item.title,
"Node's title element's text should match item title");
Assert.ok(node.classList.contains("tab"),
"Node should have .tab class");
Assert.equal(node.dataset.url, item.url,
"Node's URL should match item URL");
Assert.equal(node.getAttribute("title"), item.title + "\n" + item.url,
"Tab node should have correct title attribute");
} else {
// client items
Assert.equal(node.querySelector(".item-title").textContent, item.name,
"Node's title element's text should match client name");
Assert.ok(node.classList.contains("client"),
"Node should have .client class");
Assert.equal(node.dataset.id, item.id,
"Node's ID should match item ID");
}
}
function* testContextMenu(syncedTabsDeckComponent, contextSelector, triggerSelector, menuSelectors) {
let contextMenu = document.querySelector(contextSelector);
let triggerElement = syncedTabsDeckComponent._window.document.querySelector(triggerSelector);
let isClosed = triggerElement.classList.contains("closed");
let promisePopupShown = BrowserTestUtils.waitForEvent(contextMenu, "popupshown");
let chromeWindow = triggerElement.ownerDocument.defaultView.top;
let rect = triggerElement.getBoundingClientRect();
let contentRect = chromeWindow.SidebarUI.browser.getBoundingClientRect();
// The offsets in `rect` are relative to the content window, but
// `synthesizeMouseAtPoint` calls `nsIDOMWindowUtils.sendMouseEvent`,
// which interprets the offsets relative to the containing *chrome* window.
// This means we need to account for the width and height of any elements
// outside the `browser` element, like `sidebarheader`.
let offsetX = contentRect.x + rect.x + (rect.width / 2);
let offsetY = contentRect.y + rect.y + (rect.height / 4);
yield EventUtils.synthesizeMouseAtPoint(offsetX, offsetY, {
type: "contextmenu",
button: 2,
}, chromeWindow);
yield promisePopupShown;
is(triggerElement.classList.contains("closed"), isClosed,
"Showing the context menu shouldn't toggle the tab list");
checkChildren(contextMenu, menuSelectors);
let promisePopupHidden = BrowserTestUtils.waitForEvent(contextMenu, "popuphidden");
contextMenu.hidePopup();
yield promisePopupHidden;
}
function checkChildren(node, selectors) {
is(node.children.length, selectors.length, "Menu item count doesn't match");
for (let index = 0; index < node.children.length; index++) {
let child = node.children[index];
let [selector, props] = [].concat(selectors[index]);
ok(selector, `Node at ${index} should have selector`);
ok(child.matches(selector), `Node ${
index} should match ${selector}`);
if (props) {
Object.keys(props).forEach(prop => {
is(child[prop], props[prop], `${prop} value at ${index} should match`);
});
}
}
}
@@ -0,0 +1,18 @@
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/Promise.jsm");
// Load mocking/stubbing library, sinon
// docs: http://sinonjs.org/docs/
let loader = Cc["@mozilla.org/moz/jssubscript-loader;1"].getService(Ci.mozIJSSubScriptLoader);
loader.loadSubScript("resource://testing-common/sinon-1.16.1.js");
registerCleanupFunction(function*() {
// Cleanup window or the test runner will throw an error
delete window.sinon;
delete window.setImmediate;
delete window.clearImmediate;
});
@@ -0,0 +1,29 @@
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyGetter(this, "FxAccountsCommon", function () {
return Components.utils.import("resource://gre/modules/FxAccountsCommon.js", {});
});
Cu.import("resource://gre/modules/Timer.jsm");
do_get_profile(); // fxa needs a profile directory for storage.
// Create a window polyfill so sinon can load
let window = {
document: {},
location: {},
setTimeout: setTimeout,
setInterval: setInterval,
clearTimeout: clearTimeout,
clearinterval: clearInterval
};
let self = window;
// Load mocking/stubbing library, sinon
// docs: http://sinonjs.org/docs/
let loader = Cc["@mozilla.org/moz/jssubscript-loader;1"].getService(Ci.mozIJSSubScriptLoader);
loader.loadSubScript("resource://testing-common/sinon-1.16.1.js");
@@ -0,0 +1,35 @@
"use strict";
let { EventEmitter } = Cu.import("resource:///modules/syncedtabs/EventEmitter.jsm", {});
add_task(function* testSingleListener() {
let eventEmitter = new EventEmitter();
let spy = sinon.spy();
eventEmitter.on("click", spy);
eventEmitter.emit("click", "foo", "bar");
Assert.ok(spy.calledOnce);
Assert.ok(spy.calledWith("foo", "bar"));
eventEmitter.off("click", spy);
eventEmitter.emit("click");
Assert.ok(spy.calledOnce);
});
add_task(function* testMultipleListeners() {
let eventEmitter = new EventEmitter();
let spy1 = sinon.spy();
let spy2 = sinon.spy();
eventEmitter.on("some_event", spy1);
eventEmitter.on("some_event", spy2);
eventEmitter.emit("some_event");
Assert.ok(spy1.calledOnce);
Assert.ok(spy2.calledOnce);
eventEmitter.off("some_event", spy1);
eventEmitter.emit("some_event");
Assert.ok(spy1.calledOnce);
Assert.ok(spy2.calledTwice);
});
@@ -0,0 +1,219 @@
"use strict";
let { SyncedTabs } = Cu.import("resource://services-sync/SyncedTabs.jsm", {});
let { SyncedTabsDeckComponent } = Cu.import("resource:///modules/syncedtabs/SyncedTabsDeckComponent.js", {});
let { TabListComponent } = Cu.import("resource:///modules/syncedtabs/TabListComponent.js", {});
let { SyncedTabsListStore } = Cu.import("resource:///modules/syncedtabs/SyncedTabsListStore.js", {});
let { SyncedTabsDeckStore } = Cu.import("resource:///modules/syncedtabs/SyncedTabsDeckStore.js", {});
let { TabListView } = Cu.import("resource:///modules/syncedtabs/TabListView.js", {});
let { DeckView } = Cu.import("resource:///modules/syncedtabs/SyncedTabsDeckView.js", {});
add_task(function* testInitUninit() {
let deckStore = new SyncedTabsDeckStore();
let listComponent = {};
let ViewMock = sinon.stub();
let view = {render: sinon.spy(), destroy: sinon.spy(), container: {}};
ViewMock.returns(view);
sinon.stub(SyncedTabs, "syncTabs", ()=> Promise.resolve());
sinon.spy(deckStore, "on");
sinon.stub(deckStore, "setPanels");
let component = new SyncedTabsDeckComponent({
window,
deckStore,
listComponent,
SyncedTabs,
DeckView: ViewMock,
});
sinon.stub(component, "updatePanel");
component.init();
Assert.ok(SyncedTabs.syncTabs.called);
SyncedTabs.syncTabs.restore();
Assert.ok(ViewMock.calledWithNew(), "view is instantiated");
Assert.equal(ViewMock.args[0][0], window);
Assert.equal(ViewMock.args[0][1], listComponent);
Assert.ok(ViewMock.args[0][2].onAndroidClick,
"view is passed onAndroidClick prop");
Assert.ok(ViewMock.args[0][2].oniOSClick,
"view is passed oniOSClick prop");
Assert.ok(ViewMock.args[0][2].onSyncPrefClick,
"view is passed onSyncPrefClick prop");
Assert.equal(component.container, view.container,
"component returns view's container");
Assert.ok(deckStore.on.calledOnce, "listener is added to store");
Assert.equal(deckStore.on.args[0][0], "change");
// Object.values only in nightly
let values = Object.keys(component.PANELS).map(k => component.PANELS[k]);
Assert.ok(deckStore.setPanels.calledWith(values),
"panels are set on deck store");
Assert.ok(component.updatePanel.called);
deckStore.emit("change", "mock state");
Assert.ok(view.render.calledWith("mock state"),
"view.render is called on state change");
component.uninit();
Assert.ok(view.destroy.calledOnce, "view is destroyed on uninit");
});
function waitForObserver() {
return new Promise((resolve, reject) => {
Services.obs.addObserver((subject, topic) => {
resolve();
}, SyncedTabs.TOPIC_TABS_CHANGED, false);
});
}
add_task(function* testObserver() {
let deckStore = new SyncedTabsDeckStore();
let listStore = new SyncedTabsListStore(SyncedTabs);
let listComponent = {};
let ViewMock = sinon.stub();
let view = {render: sinon.spy(), destroy: sinon.spy(), container: {}};
ViewMock.returns(view);
sinon.stub(SyncedTabs, "syncTabs", ()=> Promise.resolve());
sinon.spy(deckStore, "on");
sinon.stub(deckStore, "setPanels");
sinon.stub(listStore, "getData");
let component = new SyncedTabsDeckComponent({
window,
deckStore,
listStore,
listComponent,
SyncedTabs,
DeckView: ViewMock,
});
sinon.spy(component, "observe");
sinon.stub(component, "updatePanel");
component.init();
SyncedTabs.syncTabs.restore();
Assert.ok(component.updatePanel.called, "triggers panel update during init");
Services.obs.notifyObservers(null, SyncedTabs.TOPIC_TABS_CHANGED, "");
Assert.ok(component.observe.calledWith(null, SyncedTabs.TOPIC_TABS_CHANGED, ""),
"component is notified");
Assert.ok(listStore.getData.called, "gets list data");
Assert.ok(component.updatePanel.calledTwice, "triggers panel update");
Services.obs.notifyObservers(null, FxAccountsCommon.ONLOGIN_NOTIFICATION, "");
Assert.ok(component.observe.calledWith(null, FxAccountsCommon.ONLOGIN_NOTIFICATION, ""),
"component is notified of login");
Assert.equal(component.updatePanel.callCount, 3, "triggers panel update again");
});
add_task(function* testPanelStatus() {
let deckStore = new SyncedTabsDeckStore();
let listStore = new SyncedTabsListStore();
let listComponent = {};
let fxAccounts = {
accountStatus() {}
};
let SyncedTabsMock = {
getTabClients() {}
};
sinon.stub(listStore, "getData");
let component = new SyncedTabsDeckComponent({
fxAccounts,
deckStore,
listComponent,
SyncedTabs: SyncedTabsMock,
});
let isAuthed = false;
sinon.stub(fxAccounts, "accountStatus", ()=> Promise.resolve(isAuthed));
let result = yield component.getPanelStatus();
Assert.equal(result, component.PANELS.NOT_AUTHED_INFO);
isAuthed = true;
SyncedTabsMock.isConfiguredToSyncTabs = false;
result = yield component.getPanelStatus();
Assert.equal(result, component.PANELS.TABS_DISABLED);
SyncedTabsMock.isConfiguredToSyncTabs = true;
SyncedTabsMock.hasSyncedThisSession = false;
result = yield component.getPanelStatus();
Assert.equal(result, component.PANELS.TABS_FETCHING);
SyncedTabsMock.hasSyncedThisSession = true;
let clients = [];
sinon.stub(SyncedTabsMock, "getTabClients", ()=> Promise.resolve(clients));
result = yield component.getPanelStatus();
Assert.equal(result, component.PANELS.SINGLE_DEVICE_INFO);
clients = ["mock-client"];
result = yield component.getPanelStatus();
Assert.equal(result, component.PANELS.TABS_CONTAINER);
fxAccounts.accountStatus.restore();
sinon.stub(fxAccounts, "accountStatus", ()=> Promise.reject("err"));
result = yield component.getPanelStatus();
Assert.equal(result, component.PANELS.NOT_AUTHED_INFO);
sinon.stub(component, "getPanelStatus", ()=> Promise.resolve("mock-panelId"));
sinon.spy(deckStore, "selectPanel");
yield component.updatePanel();
Assert.ok(deckStore.selectPanel.calledWith("mock-panelId"));
});
add_task(function* testActions() {
let listComponent = {};
let windowMock = {
openUILink() {},
};
let chromeWindowMock = {
gSyncUI: {
openSetup() {}
}
};
sinon.spy(windowMock, "openUILink");
sinon.spy(chromeWindowMock.gSyncUI, "openSetup");
let getChromeWindowMock = sinon.stub();
getChromeWindowMock.returns(chromeWindowMock);
let component = new SyncedTabsDeckComponent({
window: windowMock,
getChromeWindowMock
});
let href = Services.prefs.getCharPref("identity.mobilepromo.android") + "synced-tabs-sidebar";
component.openAndroidLink("mock-event");
Assert.ok(windowMock.openUILink.calledWith(href, "mock-event"));
href = Services.prefs.getCharPref("identity.mobilepromo.ios") + "synced-tabs-sidebar";
component.openiOSLink("mock-event");
Assert.ok(windowMock.openUILink.calledWith(href, "mock-event"));
component.openSyncPrefs();
Assert.ok(getChromeWindowMock.calledWith(windowMock));
Assert.ok(chromeWindowMock.gSyncUI.openSetup.called);
});
@@ -0,0 +1,64 @@
"use strict";
let { SyncedTabsDeckStore } = Cu.import("resource:///modules/syncedtabs/SyncedTabsDeckStore.js", {});
add_task(function* testSelectUnkownPanel() {
let deckStore = new SyncedTabsDeckStore();
let spy = sinon.spy();
deckStore.on("change", spy);
deckStore.selectPanel("foo");
Assert.ok(!spy.called);
});
add_task(function* testSetPanels() {
let deckStore = new SyncedTabsDeckStore();
let spy = sinon.spy();
deckStore.on("change", spy);
deckStore.setPanels(["panel1", "panel2"]);
Assert.ok(spy.calledWith({
panels: [
{ id: "panel1", selected: false },
{ id: "panel2", selected: false },
],
isUpdatable: false
}));
});
add_task(function* testSelectPanel() {
let deckStore = new SyncedTabsDeckStore();
let spy = sinon.spy();
deckStore.setPanels(["panel1", "panel2"]);
deckStore.on("change", spy);
deckStore.selectPanel("panel2");
Assert.ok(spy.calledWith({
panels: [
{ id: "panel1", selected: false },
{ id: "panel2", selected: true },
],
isUpdatable: true
}));
deckStore.selectPanel("panel2");
Assert.ok(spy.calledOnce, "doesn't trigger unless panel changes");
});
add_task(function* testSetPanelsSameArray() {
let deckStore = new SyncedTabsDeckStore();
let spy = sinon.spy();
deckStore.on("change", spy);
let panels = ["panel1", "panel2"];
deckStore.setPanels(panels);
deckStore.setPanels(panels);
Assert.ok(spy.calledOnce, "doesn't trigger unless set of panels changes");
});
@@ -0,0 +1,266 @@
"use strict";
let { SyncedTabs } = Cu.import("resource://services-sync/SyncedTabs.jsm", {});
let { SyncedTabsListStore } = Cu.import("resource:///modules/syncedtabs/SyncedTabsListStore.js", {});
const FIXTURE = [
{
"id": "2xU5h-4bkWqA",
"type": "client",
"name": "laptop",
"icon": "chrome://browser/skin/sync-desktopIcon.png",
"tabs": [
{
"type": "tab",
"title": "Firefox for iOS — Mobile Web browser for your iPhone, iPad and iPod touch — Mozilla",
"url": "https://www.mozilla.org/en-US/firefox/ios/?utm_source=firefox-browser&utm_medium=firefox-browser&utm_campaign=synced-tabs-sidebar",
"icon": "moz-anno:favicon:https://www.mozilla.org/media/img/firefox/favicon.dc6635050bf5.ico",
"client": "2xU5h-4bkWqA",
"lastUsed": 1451519425
},
{
"type": "tab",
"title": "Firefox Nightly First Run Page",
"url": "https://www.mozilla.org/en-US/firefox/nightly/firstrun/?oldversion=45.0a1",
"icon": "moz-anno:favicon:https://www.mozilla.org/media/img/firefox/favicon-nightly.560395bbb2e1.png",
"client": "2xU5h-4bkWqA",
"lastUsed": 1451519420
}
]
},
{
"id": "OL3EJCsdb2JD",
"type": "client",
"name": "desktop",
"icon": "chrome://browser/skin/sync-desktopIcon.png",
"tabs": []
}
];
add_task(function* testGetDataEmpty() {
let store = new SyncedTabsListStore(SyncedTabs);
let spy = sinon.spy();
sinon.stub(SyncedTabs, "getTabClients", () => {
return Promise.resolve([]);
});
store.on("change", spy);
yield store.getData();
Assert.ok(SyncedTabs.getTabClients.calledWith(""));
Assert.ok(spy.calledWith({
clients: [],
canUpdateAll: false,
canUpdateInput: false,
filter: "",
inputFocused: false
}));
yield store.getData("filter");
Assert.ok(SyncedTabs.getTabClients.calledWith("filter"));
Assert.ok(spy.calledWith({
clients: [],
canUpdateAll: false,
canUpdateInput: true,
filter: "filter",
inputFocused: false
}));
SyncedTabs.getTabClients.restore();
});
add_task(function* testRowSelectionWithoutFilter() {
let store = new SyncedTabsListStore(SyncedTabs);
let spy = sinon.spy();
sinon.stub(SyncedTabs, "getTabClients", () => {
return Promise.resolve(FIXTURE);
});
yield store.getData();
SyncedTabs.getTabClients.restore();
store.on("change", spy);
store.selectRow(0, -1);
Assert.ok(spy.args[0][0].canUpdateAll, "can update the whole view");
Assert.ok(spy.args[0][0].clients[0].selected, "first client is selected");
store.moveSelectionUp();
Assert.ok(spy.calledOnce,
"can't move up past first client, no change triggered");
store.selectRow(0, 0);
Assert.ok(spy.args[1][0].clients[0].tabs[0].selected,
"first tab of first client is selected");
store.selectRow(0, 0);
Assert.ok(spy.calledTwice, "selecting same row doesn't trigger change");
store.selectRow(0, 1);
Assert.ok(spy.args[2][0].clients[0].tabs[1].selected,
"second tab of first client is selected");
store.selectRow(1);
Assert.ok(spy.args[3][0].clients[1].selected, "second client is selected");
store.moveSelectionDown();
Assert.equal(spy.callCount, 4,
"can't move selection down past last client, no change triggered");
store.moveSelectionUp();
Assert.equal(spy.callCount, 5,
"changed");
Assert.ok(spy.args[4][0].clients[0].tabs[FIXTURE[0].tabs.length - 1].selected,
"move selection up from client selects last tab of previous client");
store.moveSelectionUp();
Assert.ok(spy.args[5][0].clients[0].tabs[FIXTURE[0].tabs.length - 2].selected,
"move selection up from tab selects previous tab of client");
});
add_task(function* testToggleBranches() {
let store = new SyncedTabsListStore(SyncedTabs);
let spy = sinon.spy();
sinon.stub(SyncedTabs, "getTabClients", () => {
return Promise.resolve(FIXTURE);
});
yield store.getData();
SyncedTabs.getTabClients.restore();
store.selectRow(0);
store.on("change", spy);
let clientId = FIXTURE[0].id;
store.closeBranch(clientId);
Assert.ok(spy.args[0][0].clients[0].closed, "first client is closed");
store.openBranch(clientId);
Assert.ok(!spy.args[1][0].clients[0].closed, "first client is open");
store.toggleBranch(clientId);
Assert.ok(spy.args[2][0].clients[0].closed, "first client is toggled closed");
store.moveSelectionDown();
Assert.ok(spy.args[3][0].clients[1].selected,
"selection skips tabs if client is closed");
store.moveSelectionUp();
Assert.ok(spy.args[4][0].clients[0].selected,
"selection skips tabs if client is closed");
});
add_task(function* testRowSelectionWithFilter() {
let store = new SyncedTabsListStore(SyncedTabs);
let spy = sinon.spy();
sinon.stub(SyncedTabs, "getTabClients", () => {
return Promise.resolve(FIXTURE);
});
yield store.getData("filter");
SyncedTabs.getTabClients.restore();
store.on("change", spy);
store.selectRow(0);
Assert.ok(spy.args[0][0].clients[0].tabs[0].selected, "first tab is selected");
store.moveSelectionUp();
Assert.ok(spy.calledOnce,
"can't move up past first tab, no change triggered");
store.moveSelectionDown();
Assert.ok(spy.args[1][0].clients[0].tabs[1].selected,
"selection skips tabs if client is closed");
store.moveSelectionDown();
Assert.equal(spy.callCount, 2,
"can't move selection down past last tab, no change triggered");
store.selectRow(1);
Assert.equal(spy.callCount, 2,
"doesn't trigger change if same row selected");
});
add_task(function* testFilterAndClearFilter() {
let store = new SyncedTabsListStore(SyncedTabs);
let spy = sinon.spy();
sinon.stub(SyncedTabs, "getTabClients", () => {
return Promise.resolve(FIXTURE);
});
store.on("change", spy);
yield store.getData("filter");
Assert.ok(SyncedTabs.getTabClients.calledWith("filter"));
Assert.ok(!spy.args[0][0].canUpdateAll, "can't update all");
Assert.ok(spy.args[0][0].canUpdateInput, "can update just input");
store.selectRow(0);
Assert.equal(spy.args[1][0].filter, "filter");
Assert.ok(spy.args[1][0].clients[0].tabs[0].selected,
"tab is selected");
yield store.clearFilter();
Assert.ok(SyncedTabs.getTabClients.calledWith(""));
Assert.ok(!spy.args[2][0].canUpdateAll, "can't update all");
Assert.ok(!spy.args[2][0].canUpdateInput, "can't just update input");
Assert.equal(spy.args[2][0].filter, "");
Assert.ok(!spy.args[2][0].clients[0].tabs[0].selected,
"tab is no longer selected");
SyncedTabs.getTabClients.restore();
});
add_task(function* testFocusBlurInput() {
let store = new SyncedTabsListStore(SyncedTabs);
let spy = sinon.spy();
sinon.stub(SyncedTabs, "getTabClients", () => {
return Promise.resolve(FIXTURE);
});
store.on("change", spy);
yield store.getData();
SyncedTabs.getTabClients.restore();
Assert.ok(!spy.args[0][0].canUpdateAll, "must rerender all");
store.selectRow(0);
Assert.ok(!spy.args[1][0].inputFocused,
"input is not focused");
Assert.ok(spy.args[1][0].clients[0].selected,
"client is selected");
Assert.ok(spy.args[1][0].clients[0].focused,
"client is focused");
store.focusInput();
Assert.ok(spy.args[2][0].inputFocused,
"input is focused");
Assert.ok(spy.args[2][0].clients[0].selected,
"client is still selected");
Assert.ok(!spy.args[2][0].clients[0].focused,
"client is no longer focused");
store.blurInput();
Assert.ok(!spy.args[3][0].inputFocused,
"input is not focused");
Assert.ok(spy.args[3][0].clients[0].selected,
"client is selected");
Assert.ok(spy.args[3][0].clients[0].focused,
"client is focused");
});
@@ -0,0 +1,136 @@
"use strict";
let { SyncedTabs } = Cu.import("resource://services-sync/SyncedTabs.jsm", {});
let { TabListComponent } = Cu.import("resource:///modules/syncedtabs/TabListComponent.js", {});
let { SyncedTabsListStore } = Cu.import("resource:///modules/syncedtabs/SyncedTabsListStore.js", {});
let { View } = Cu.import("resource:///modules/syncedtabs/TabListView.js", {});
const ACTION_METHODS = [
"onSelectRow",
"onOpenTab",
"onMoveSelectionDown",
"onMoveSelectionUp",
"onToggleBranch",
"onBookmarkTab",
"onSyncRefresh",
"onFilter",
"onClearFilter",
"onFilterFocus",
"onFilterBlur",
];
add_task(function* testInitUninit() {
let store = new SyncedTabsListStore();
let ViewMock = sinon.stub();
let view = {render(){}, destroy(){}};
ViewMock.returns(view);
sinon.spy(view, 'render');
sinon.spy(view, 'destroy');
sinon.spy(store, "on");
sinon.stub(store, "getData");
sinon.stub(store, "focusInput");
let component = new TabListComponent({window, store, View: ViewMock, SyncedTabs});
for (let action of ACTION_METHODS) {
sinon.stub(component, action);
}
component.init();
Assert.ok(ViewMock.calledWithNew(), "view is instantiated");
Assert.ok(store.on.calledOnce, "listener is added to store");
Assert.equal(store.on.args[0][0], "change");
Assert.ok(view.render.calledWith({clients: []}),
"render is called on view instance");
Assert.ok(store.getData.calledOnce, "store gets initial data");
Assert.ok(store.focusInput.calledOnce, "input field is focused");
for (let method of ACTION_METHODS) {
let action = ViewMock.args[0][1][method];
Assert.ok(action, method + " action is passed to View");
action("foo", "bar");
Assert.ok(component[method].calledWith("foo", "bar"),
method + " action passed to View triggers the component method with args");
}
store.emit("change", "mock state");
Assert.ok(view.render.secondCall.calledWith("mock state"),
"view.render is called on state change");
component.uninit();
Assert.ok(view.destroy.calledOnce, "view is destroyed on uninit");
});
add_task(function* testActions() {
let store = new SyncedTabsListStore();
let clipboardHelperMock = {
copyString() {},
};
let windowMock = {
top: {
PlacesCommandHook: {
bookmarkLink() { return Promise.resolve(); }
},
PlacesUtils: { bookmarksMenuFolderId: "id" }
},
openUILinkIn() {}
};
let component = new TabListComponent({
window: windowMock, store, View: null, SyncedTabs,
clipboardHelper: clipboardHelperMock});
sinon.stub(store, "getData");
component.onFilter("query");
Assert.ok(store.getData.calledWith("query"));
sinon.stub(store, "clearFilter");
component.onClearFilter();
Assert.ok(store.clearFilter.called);
sinon.stub(store, "focusInput");
component.onFilterFocus();
Assert.ok(store.focusInput.called);
sinon.stub(store, "blurInput");
component.onFilterBlur();
Assert.ok(store.blurInput.called);
sinon.stub(store, "selectRow");
component.onSelectRow([-1, -1]);
Assert.ok(store.selectRow.calledWith(-1, -1));
sinon.stub(store, "moveSelectionDown");
component.onMoveSelectionDown();
Assert.ok(store.moveSelectionDown.called);
sinon.stub(store, "moveSelectionUp");
component.onMoveSelectionUp();
Assert.ok(store.moveSelectionUp.called);
sinon.stub(store, "toggleBranch");
component.onToggleBranch("foo-id");
Assert.ok(store.toggleBranch.calledWith("foo-id"));
sinon.spy(windowMock.top.PlacesCommandHook, "bookmarkLink");
component.onBookmarkTab("uri", "title");
Assert.equal(windowMock.top.PlacesCommandHook.bookmarkLink.args[0][1], "uri");
Assert.equal(windowMock.top.PlacesCommandHook.bookmarkLink.args[0][2], "title");
sinon.spy(windowMock, "openUILinkIn");
component.onOpenTab("uri", "where", "params");
Assert.ok(windowMock.openUILinkIn.calledWith("uri", "where", "params"));
sinon.spy(clipboardHelperMock, "copyString");
component.onCopyTabLocation("uri");
Assert.ok(clipboardHelperMock.copyString.calledWith("uri"));
sinon.stub(SyncedTabs, "syncTabs");
component.onSyncRefresh();
Assert.ok(SyncedTabs.syncTabs.calledWith(true));
SyncedTabs.syncTabs.restore();
});
@@ -0,0 +1,10 @@
[DEFAULT]
head = head.js
tail =
firefox-appdir = browser
[test_EventEmitter.js]
[test_SyncedTabsDeckStore.js]
[test_SyncedTabsListStore.js]
[test_SyncedTabsDeckComponent.js]
[test_TabListComponent.js]
+23
View File
@@ -0,0 +1,23 @@
/* 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 {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
this.EXPORTED_SYMBOLS = [
"getChromeWindow"
];
// Get the chrome (ie, browser) window hosting this content.
function getChromeWindow(window) {
return window
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsIDocShellTreeItem)
.rootTreeItem
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindow)
.wrappedJSObject;
}
@@ -394,6 +394,9 @@ These should match what Safari and other Apple applications use on OS X Lion. --
<!ENTITY appMenuSafeMode.label "Restart in Safe Mode…">
<!ENTITY customizeMenu.addToPanel.label "Add to Menu">
<!ENTITY customizeMenu.addToPanel.accesskey "M">
<!ENTITY appMenuRemoteTabs.sidebar.label "View Synced Tabs Sidebar">
<!ENTITY customizeMenu.removeFromToolbar.label "Remove from Toolbar">
<!ENTITY customizeMenu.removeFromToolbar.accesskey "R">
<!ENTITY customizeMenu.removeFromMenu.label "Remove from Menu">
@@ -729,6 +732,23 @@ you can use these alternative items. Otherwise, their values should be empty. -
<!-- LOCALIZATION NOTE (syncTabsMenu2.label): This appears in the history menu -->
<!ENTITY syncTabsMenu2.label "Tabs From Other Devices">
<!ENTITY syncedTabs.sidebar.label "Synced Tabs">
<!ENTITY syncedTabs.sidebar.fetching.label "Fetching Synced Tabs…">
<!ENTITY syncedTabs.sidebar.noclients.label "Sign in to Firefox from your other devices to view their tabs here.">
<!ENTITY syncedTabs.sidebar.notsignedin.label "Sign in to view a list of tabs from your other devices.">
<!ENTITY syncedTabs.sidebar.notabs.label "No open tabs">
<!ENTITY syncedTabs.sidebar.openprefs.label "Open &syncBrand.shortName.label; Preferences">
<!-- LOCALIZATION NOTE (syncedTabs.sidebar.tabsnotsyncing.label): This is shown
when Sync is configured but syncing tabs is disabled. -->
<!ENTITY syncedTabs.sidebar.tabsnotsyncing.label "Turn on tab syncing to view a list of tabs from your other devices.">
<!ENTITY syncedTabs.context.openTab.label "Open This Tab">
<!ENTITY syncedTabs.context.openTab.accesskey "O">
<!ENTITY syncedTabs.context.bookmarkSingleTab.label "Bookmark This Tab…">
<!ENTITY syncedTabs.context.bookmarkSingleTab.accesskey "B">
<!ENTITY syncedTabs.context.refreshList.label "Refresh List">
<!ENTITY syncedTabs.context.refreshList.accesskey "R">
<!ENTITY syncBrand.shortName.label "Sync">
<!ENTITY syncSetup.label "Set Up &syncBrand.shortName.label;…">
+1
View File
@@ -16,6 +16,7 @@ browser.jar:
skin/classic/browser/aboutSyncTabs.css
#endif
skin/classic/browser/aboutTabCrashed.css (../shared/aboutTabCrashed.css)
* skin/classic/browser/syncedtabs/sidebar.css (syncedtabs/sidebar.css)
skin/classic/browser/actionicon-tab.png
* skin/classic/browser/browser.css
* skin/classic/browser/devedition.css
@@ -0,0 +1,59 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
%include ../../shared/syncedtabs/sidebar.inc.css
/* These styles are intended to mimic XUL trees and the XUL search box. */
html {
border: 1px solid ThreeDShadow;
background-color: -moz-Field;
color: -moz-FieldText;
box-sizing: border-box;
}
.item {
-moz-padding-end: 0;
}
.item-title {
margin: 1px 0 0;
-moz-margin-end: 6px;
}
.search-box {
-moz-appearance: textfield;
cursor: text;
margin: 2px 4px;
border: 2px solid;
-moz-border-top-colors: ThreeDShadow ThreeDDarkShadow;
-moz-border-right-colors: ThreeDHighlight ThreeDLightShadow;
-moz-border-bottom-colors: ThreeDHighlight ThreeDLightShadow;
-moz-border-left-colors: ThreeDShadow ThreeDDarkShadow;
padding: 2px 2px 3px;
-moz-padding-start: 4px;
background-color: -moz-Field;
color: -moz-FieldText;
}
.textbox-search-clear {
background-image: url(moz-icon://stock/gtk-clear?size=menu);
background-repeat: no-repeat;
width: 16px;
height: 16px;
}
.textbox-search-icon {
background-image: url(moz-icon://stock/gtk-find?size=menu);
background-repeat: no-repeat;
width: 16px;
height: 16px;
}
.textbox-search-icon[searchbutton]:not([disabled]) ,
.textbox-search-clear:not([disabled]) {
cursor: pointer;
}
+3
View File
@@ -15,6 +15,9 @@ browser.jar:
skin/classic/browser/aboutSyncTabs.css
#endif
skin/classic/browser/aboutTabCrashed.css (../shared/aboutTabCrashed.css)
* skin/classic/browser/syncedtabs/sidebar.css (syncedtabs/sidebar.css)
skin/classic/browser/syncedtabs/arrow-open.svg (syncedtabs/arrow-open.svg)
skin/classic/browser/syncedtabs/arrow-closed.svg (syncedtabs/arrow-closed.svg)
skin/classic/browser/actionicon-tab.png
skin/classic/browser/appmenu-icons.png
skin/classic/browser/appmenu-dropmarker.png
Binary file not shown.

Before

Width:  |  Height:  |  Size: 291 B

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 352 B

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- 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/. -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="15" height="18" viewBox="0 0 15 18">
<path id="arrow" d="M14.999,9.006 L-0.001,17.998 L-0.001,0.013 L14.999,9.006 z" fill="#8C8C8C" />
</svg>

After

Width:  |  Height:  |  Size: 472 B

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- 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/. -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="18" height="15" viewBox="0 0 18 15">
<path id="arrow-down" d="M8.994,14.999 L0.002,-0.001 L17.987,-0.001 L8.994,14.999 z" fill="#8C8C8C" />
</svg>

After

Width:  |  Height:  |  Size: 477 B

+96
View File
@@ -0,0 +1,96 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
%include ../../shared/syncedtabs/sidebar.inc.css
/* These styles are intended to mimic XUL trees and the XUL search box. */
html {
}
.item {
color: -moz-DialogText;
}
.item.selected > .item-title-container {
color: HighlightText;
font-weight: bold;
}
.item.selected > .item-title-container {
background: linear-gradient(to bottom, rgba(156,172,204,1) 0%, rgba(116,135,172,1) 100%);
}
.item.selected:focus > .item-title-container {
background: linear-gradient(to bottom, rgba(95,144,209,1) 0%, rgba(39,90,173,1) 100%);
}
.item.client .item-twisty-container {
background-image: url(arrow-open.svg);
background-size: 9px 8px;
}
.item.client.closed .item-twisty-container {
background-image: url(arrow-closed.svg);
background-size: 7px 9px;
}
.sidebar-search-container {
border-bottom: 1px solid #bdbdbd;
}
.search-box {
-moz-appearance: searchfield;
padding: 1px;
font-size: 12px;
cursor: text;
margin: 4px 8px 10px;
border-width: 3px;
border-style: solid;
border-color: -moz-use-text-color;
border-image: none;
-moz-border-top-colors: transparent #888 #000;
-moz-border-right-colors: transparent #FFF #000;
-moz-border-bottom-colors: transparent #FFF #000;
-moz-border-left-colors: transparent #888 #000;
border-top-right-radius: 2px;
border-bottom-left-radius: 2px;
background-color: #FFF;
color: #000;
-moz-user-select: text;
text-shadow: none;
}
.search-box.compact > .textbox-input-box > .textbox-search-icons > .textbox-search-clear {
width: 11px;
height: 11px;
background-image: url(chrome://global/skin/icons/searchfield-small-cancel.png);
background-repeat: no-repeat;
}
.search-box.compact > .textbox-input-box > .textbox-search-icons > .textbox-search-clear:active:hover {
background-position: 11px 0;
}
.search-box.compact > .textbox-input-box > .textbox-search-icons > .textbox-search-icon {
display: none;
}
.search-box[focused="true"] {
-moz-border-top-colors: -moz-mac-focusring -moz-mac-focusring #000000;
-moz-border-right-colors: -moz-mac-focusring -moz-mac-focusring #000000;
-moz-border-bottom-colors: -moz-mac-focusring -moz-mac-focusring #000000;
-moz-border-left-colors: -moz-mac-focusring -moz-mac-focusring #000000;
}
.search-box.compact {
padding: 0px;
/* font size is in px because the XUL it was copied from uses px */
font-size: 11px;
}
.textbox-search-clear,
.textbox-search-icon {
margin-top: 1px;
}
@@ -0,0 +1,199 @@
% 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/.
/* These styles are intended to mimic XUL trees and the XUL search box. */
:root, body {
overflow-x: hidden;
}
body {
margin: 0;
font: message-box;
color: #333333;
-moz-user-select: none;
overflow: hidden;
}
.emptyListInfo {
cursor: default;
padding: 3em 1em;
text-align: center;
}
.list,
.item-tabs-list {
display: flex;
flex-flow: column;
flex-grow: 1;
}
.item.client {
opacity: 1;
max-height: unset;
display: unset;
}
.item.client.closed .item-tabs-list {
display: none;
}
.item {
display: inline-block;
opacity: 1;
flex: 1;
min-width: 0;
white-space: nowrap;
overflow: hidden;
outline: none;
color: -moz-FieldText;
}
.item.selected > .item-title-container {
background-color: -moz-cellhighlight;
color: -moz-cellhighlighttext;
font-weight: bold;
}
.item.selected:focus > .item-title-container {
background-color: Highlight;
color: HighlightText;
}
.client .item.tab > .item-title-container {
padding-inline-start: 35px;
}
.item.tab > .item-title-container {
padding-inline-start: 20px;
}
.item-icon-container {
min-width: 16px;
max-width: 16px;
min-height: 16px;
max-height: 16px;
margin-right: 5px;
background-size: 16px 16px;
background-size: contain;
background-repeat: no-repeat;
background-position: center;
}
.item-twisty-container {
min-width: 16px;
max-width: 16px;
min-height: 16px;
max-height: 16px;
margin-right: 5px;
background-size: contain;
background-repeat: no-repeat;
background-position: center;
}
.item-icon-container {
margin-left: 5px;
}
.item-title-container {
display: flex;
flex-flow: row;
overflow: hidden;
flex-grow: 1;
padding: 1px 0px 1px 0px;
}
.item-title {
flex-grow: 1;
overflow: hidden;
text-overflow: ellipsis;
margin: 0px;
line-height: 1.3;
}
.item[hidden] {
opacity: 0;
max-height: 0;
transition: opacity 150ms ease-in-out, max-height 150ms ease-in-out 150ms;
}
.item.empty .item-title-container {
color: #aeaeae;
}
.client .item.empty > .item-title-container {
padding-inline-start: 35px;
}
.text-input-box {
display: flex;
flex-flow: row nowrap;
}
.textbox-input-box {
display: flex;
flex-direction: row;
}
.tabsFilter {
flex: 1;
}
.sync-state > p {
padding-inline-end: 10px;
padding-inline-start: 10px;
color: #888;
}
.text-link {
color: rgb(0, 149, 221);
cursor: pointer;
}
.text-link:hover {
text-decoration: underline;
}
.text-link,
.text-link:focus {
margin: 0px;
padding: 0px;
border: 0px;
}
.deck .sync-state {
display: none;
opacity: 0;
transition: opacity 1.5s;
border-top: 1px solid #bdbdbd;
}
.deck .sync-state.tabs-container {
border-top: 0px;
}
.deck .sync-state.selected {
display: unset;
opacity: 100;
}
.item.client .item-twisty-container {
background-image: url(twisty-open.svg);
}
.item.client.closed .item-twisty-container {
background-image: url(twisty-closed.svg);
}
.textbox-search-clear:not([disabled]) {
cursor: default;
}
.textbox-search-icons .textbox-search-clear,
.filtered .textbox-search-icons .textbox-search-icon {
display: none;
}
.filtered .textbox-search-icons .textbox-search-clear {
display: block;
}
@@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- 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/. -->
<svg
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
version="1.1"
width="11"
height="11">
<defs>
<linearGradient
id="linearGradient3792">
<stop
style="stop-color:#c3baaa;stop-opacity:1"
offset="0" />
<stop
style="stop-color:#ffffff;stop-opacity:1"
offset="1" />
</linearGradient>
<linearGradient
x1="6.0530181"
y1="7.092885"
x2="2.8882971"
y2="1.7999334"
xlink:href="#linearGradient3792"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.0256411,0,0,1.0256411,-0.11538478,1.8846152)" />
<linearGradient
x1="6.0530181"
y1="7.092885"
x2="2.8882971"
y2="1.7999334"
xlink:href="#linearGradient3792"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.0256411,0,0,1.0256411,-0.11538478,1.8846152)" />
<linearGradient
x1="6.0530181"
y1="7.092885"
x2="2.8882971"
y2="1.7999334"
id="linearGradient2996"
xlink:href="#linearGradient3792"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.0256411,0,0,1.0256411,0.88461522,0.8846152)" />
</defs>
<rect
width="8"
height="8"
rx="1"
ry="1"
x="1.5"
y="1.5"
style="fill:url(#linearGradient2996);fill-opacity:1;stroke:#7898b5;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<path
d="M 5,3 5,5 3,5 3,6 5,6 5,8 6,8 6,6 8,6 8,5 6,5 6,3 5,3 z"
style="fill:#000000;fill-opacity:1;stroke:none" />
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

@@ -0,0 +1,62 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- 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/. -->
<svg
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
version="1.1"
width="11"
height="11">
<defs>
<linearGradient
id="linearGradient3792">
<stop
style="stop-color:#c3baaa;stop-opacity:1"
offset="0" />
<stop
style="stop-color:#ffffff;stop-opacity:1"
offset="1" />
</linearGradient>
<linearGradient
x1="6.0530181"
y1="7.092885"
x2="2.8882971"
y2="1.7999334"
xlink:href="#linearGradient3792"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.0256411,0,0,1.0256411,-0.11538478,1.8846152)" />
<linearGradient
x1="6.0530181"
y1="7.092885"
x2="2.8882971"
y2="1.7999334"
xlink:href="#linearGradient3792"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.0256411,0,0,1.0256411,-0.11538478,1.8846152)" />
<linearGradient
x1="6.0530181"
y1="7.092885"
x2="2.8882971"
y2="1.7999334"
id="linearGradient2996"
xlink:href="#linearGradient3792"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.0256411,0,0,1.0256411,0.88461522,0.8846152)" />
</defs>
<rect
width="8"
height="8"
rx="1"
ry="1"
x="1.5"
y="1.5"
style="fill:url(#linearGradient2996);fill-opacity:1;stroke:#7898b5;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<rect
width="5"
height="1"
x="3"
y="5"
style="fill:#000000;fill-opacity:1;stroke:none" />
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

+1
View File
@@ -15,6 +15,7 @@ browser.jar:
skin/classic/browser/aboutSyncTabs.css
#endif
skin/classic/browser/aboutTabCrashed.css (../shared/aboutTabCrashed.css)
* skin/classic/browser/syncedtabs/sidebar.css (syncedtabs/sidebar.css)
skin/classic/browser/actionicon-tab.png
skin/classic/browser/appmenu-icons.png
skin/classic/browser/appmenu-dropmarker.png
+2 -1
View File
@@ -37,7 +37,8 @@
@media (-moz-os-version: windows-vista),
(-moz-os-version: windows-win7) {
#bookmarksPanel,
#history-panel {
#history-panel,
#tabs-panel {
background-color: #EEF3FA;
}
}
@@ -0,0 +1,103 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
%include ../../shared/syncedtabs/sidebar.inc.css
/* These styles are intended to mimic XUL trees and the XUL search box. */
html {
background-color: #EEF3FA;
}
.item {
-moz-padding-end: 0;
}
.item-title {
margin: 1px 0 0;
}
.item-title {
-moz-margin-end: 6px;
}
.search-box {
-moz-appearance: textfield;
cursor: text;
margin: 2px 4px;
padding: 2px 2px 3px;
-moz-padding-start: 4px;
color: -moz-FieldText;
}
.textbox-search-icon {
width: 16px;
height: 16px;
background-image: url(chrome://global/skin/icons/Search-glass.png);
background-repeat: no-repeat;
display: block;
}
.textbox-search-icon:-moz-locale-dir(rtl) {
transform: scaleX(-1);
}
.textbox-search-icon[searchbutton]:not([disabled]) {
cursor: pointer;
}
.textbox-search-clear {
width: 16px;
height: 16px;
background-image: url(chrome://global/skin/icons/Search-close.png);
background-repeat: no-repeat;
}
.textbox-search-clear:not([disabled]) {
cursor: default;
}
.textbox-search-icon:not([disabled]) {
cursor: text;
}
.textbox-search-clear:not([disabled]):hover ,
.textbox-search-icon:not([disabled]):hover {
background-position: -16px 0;
}
.textbox-search-clear:not([disabled]):hover:active ,
.textbox-search-icon:not([disabled]):hover:active {
background-position: -32px 0;
}
.client .item.tab > .item-title-container {
padding-inline-start: 26px;
}
.item.tab > .item-title-container {
padding-inline-start: 14px;
}
.item-icon-container {
min-width: 16px;
max-width: 16px;
min-height: 16px;
max-height: 16px;
margin-right: 5px;
background-size: 16px 16px;
background-repeat: no-repeat;
background-position: center;
}
.item-twisty-container {
min-width: 12px;
max-width: 12px;
min-height: 12px;
max-height: 12px;
margin: 0 1px;
background-size: 16px 16px;
background-repeat: no-repeat;
background-position: center;
}
-42
View File
@@ -3,41 +3,12 @@
# 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_RCS_MK := 1
include $(topsrcdir)/config/makefiles/makeutils.mk
ifdef MOZ_APP_BASENAME
APP_INI_DEPS = $(topsrcdir)/config/milestone.txt
APP_INI_DEPS += $(DEPTH)/config/autoconf.mk
endif
# NOTE: Keep .gdbinit in the topsrcdir for people who run gdb from the topsrcdir.
# needs to be absolute to be distinct from $(topsrcdir)/.gdbinit
GDBINIT_OBJDIR_FILES = $(topsrcdir)/.gdbinit
GDBINIT_OBJDIR_DEST = $(topobjdir)
INSTALL_TARGETS += GDBINIT_OBJDIR
# Put a .lldbinit in the bin directory and the objdir, to be picked up
# automatically by LLDB when we debug executables using either of those two
# directories as the current working directory. The .lldbinit file will
# load $(topsrcdir)/.lldbinit, which is where the actual debugging commands are.
LLDBINIT_OBJDIR := .lldbinit.in
LLDBINIT_OBJDIR_PATH = $(DEPTH)
LLDBINIT_OBJDIR_FLAGS += -Dtopsrcdir=$(abspath $(topsrcdir))
PP_TARGETS += LLDBINIT_OBJDIR
LLDBINIT_FINAL_TARGET_FILES := $(DEPTH)/.lldbinit
LLDBINIT_FINAL_TARGET_DEST = $(FINAL_TARGET)
INSTALL_TARGETS += LLDBINIT_FINAL_TARGET
# Put the .ycm_extra_conf.py file at the root of the objdir. It is used by
# the vim plugin YouCompleteMe.
YCM_FILES := $(topsrcdir)/.ycm_extra_conf.py
YCM_DEST := $(topobjdir)
YCM_TARGET := export
INSTALL_TARGETS += YCM
ifdef MOZTTDIR
# Install the Firefox OS fonts.
include $(MOZTTDIR)/fonts.mk
@@ -46,19 +17,6 @@ MOZTT_FILES = $(patsubst external/moztt/%,$(MOZTTDIR)/%,$(filter external/moztt/
INSTALL_TARGETS += MOZTT
endif
ifdef MOZ_VALGRIND
_VALGRIND_DIR = $(DEPTH)/_valgrind
GARBAGE_DIRS += $(_VALGRIND_DIR)
_VALGRIND_FILES = \
$(topsrcdir)/build/valgrind/cross-architecture.sup \
$(topsrcdir)/build/valgrind/i386-redhat-linux-gnu.sup \
$(topsrcdir)/build/valgrind/x86_64-redhat-linux-gnu.sup \
$(NULL)
_VALGRIND_DEST = $(_VALGRIND_DIR)
INSTALL_TARGETS += _VALGRIND
endif
include $(topsrcdir)/config/rules.mk
TARGET_DEPTH = ..
@@ -13,6 +13,16 @@ optional arguments:
--clean Clean the build directory
```
Pre-requisites
--------------
* Working build toolchain.
* Subversion
* CMake
* Ninja
* Python 2.7
Please use the latest available CMake for your platform to avoid surprises.
Config file format
------------------
+94 -30
View File
@@ -18,6 +18,17 @@ import sys
DEBUG = os.getenv("DEBUG")
def symlink(source, link_name):
os_symlink = getattr(os, "symlink", None)
if callable(os_symlink):
os_symlink(source, link_name)
else:
if os.path.isdir(source):
# Fall back to copying the directory :(
copy_dir_contents(source, link_name)
def check_run(args):
global DEBUG
if DEBUG:
@@ -63,6 +74,10 @@ def updated_env(env):
def build_tar_package(tar, name, base, directory):
name = os.path.realpath(name)
# On Windows, we have to convert this into an msys path so that tar can
# understand it.
if is_windows():
name = name.replace('\\', '/').replace('c:', '/c')
run_in(base, [tar,
"-c",
"-%s" % ("J" if ".xz" in name else "j"),
@@ -74,7 +89,10 @@ def copy_dir_contents(src, dest):
for f in glob.glob("%s/*" % src):
try:
destname = "%s/%s" % (dest, os.path.basename(f))
shutil.copytree(f, destname)
if os.path.isdir(f):
shutil.copytree(f, destname)
else:
shutil.copy2(f, destname)
except OSError as e:
if e.errno == errno.ENOTDIR:
shutil.copy2(f, destname)
@@ -121,19 +139,18 @@ def build_and_use_libgcc(env, clang_dir):
shutil.rmtree(tempdir)
def svn_co(url, directory, revision):
check_run(["svn", "co", "-r", revision, url, directory])
def svn_co(source_dir, url, directory, revision):
run_in(source_dir, ["svn", "co", "-r", revision, url, directory])
def svn_update(directory, revision):
run_in(directory, ["svn", "update", "-r", revision])
def build_one_stage(env, src_dir, stage_dir, build_libcxx, build_type, assertions,
python_path):
with updated_env(env):
build_one_stage_aux(src_dir, stage_dir, build_libcxx, build_type,
assertions, python_path)
def build_one_stage(cc, cxx, src_dir, stage_dir, build_libcxx,
build_type, assertions, python_path):
build_one_stage_aux(cc, cxx, src_dir, stage_dir, build_libcxx,
build_type, assertions, python_path)
def get_platform():
@@ -141,10 +158,15 @@ def get_platform():
if p == "Darwin":
return "macosx64"
elif p == "Linux":
if platform.processor() == "x86_64":
if platform.architecture() == "AMD64":
return "linux64"
else:
return "linux32"
elif p == "Windows":
if platform.architecture() == "AMD64":
return "win64"
else:
return "win32"
else:
raise NotImplementedError("Not supported platform")
@@ -153,7 +175,15 @@ def is_darwin():
return platform.system() == "Darwin"
def build_one_stage_aux(src_dir, stage_dir, build_libcxx,
def is_linux():
return platform.system() == "Linux"
def is_windows():
return platform.system() == "Windows"
def build_one_stage_aux(cc, cxx, src_dir, stage_dir, build_libcxx,
build_type, assertions, python_path):
if not os.path.exists(stage_dir):
os.mkdir(stage_dir)
@@ -162,10 +192,12 @@ def build_one_stage_aux(src_dir, stage_dir, build_libcxx,
inst_dir = stage_dir + "/clang"
run_cmake = True
if os.path.exists(build_dir):
if os.path.exists(build_dir + "/build.ninja"):
run_cmake = False
cmake_args = ["-GNinja",
"-DCMAKE_C_COMPILER=%s" % cc,
"-DCMAKE_CXX_COMPILER=%s" % cxx,
"-DCMAKE_BUILD_TYPE=%s" % build_type,
"-DLLVM_TARGETS_TO_BUILD=X86;ARM",
"-DLLVM_ENABLE_ASSERTIONS=%s" % ("ON" if assertions else "OFF"),
@@ -182,6 +214,8 @@ if __name__ == "__main__":
# We use a directory in /builds/slave because the mozilla infrastructure
# cleans it up automatically.
base_dir = "/builds/slave/moz-toolchain"
if is_windows():
base_dir = "c:%s" % base_dir
source_dir = base_dir + "/src"
build_dir = base_dir + "/build"
@@ -194,6 +228,16 @@ if __name__ == "__main__":
if is_darwin():
os.environ['MACOSX_DEPLOYMENT_TARGET'] = '10.7'
exe_ext = ""
if is_windows():
exe_ext = ".exe"
cc_name = "clang"
cxx_name = "clang++"
if is_windows():
cc_name = "clang-cl"
cxx_name = "clang-cl"
parser = argparse.ArgumentParser()
parser.add_argument('-c', '--config', required=True,
type=argparse.FileType('r'),
@@ -243,7 +287,7 @@ if __name__ == "__main__":
gcc_dir = config["gcc_dir"]
if not os.path.exists(gcc_dir):
raise ValueError("gcc_dir must point to an existing path")
if not is_darwin() and gcc_dir is None:
if is_linux() and gcc_dir is None:
raise ValueError("Config file needs to set gcc_dir")
cc = None
if "cc" in config:
@@ -262,15 +306,10 @@ if __name__ == "__main__":
if not os.path.exists(source_dir):
os.makedirs(source_dir)
svn_co(llvm_repo, llvm_source_dir, llvm_revision)
svn_co(clang_repo, clang_source_dir, llvm_revision)
svn_co(compiler_repo, compiler_rt_source_dir, llvm_revision)
svn_co(libcxx_repo, libcxx_source_dir, llvm_revision)
os.symlink("../../clang", llvm_source_dir + "/tools/clang")
os.symlink("../../compiler-rt",
llvm_source_dir + "/projects/compiler-rt")
os.symlink("../../libcxx",
llvm_source_dir + "/projects/libcxx")
svn_co(source_dir, llvm_repo, llvm_source_dir, llvm_revision)
svn_co(source_dir, clang_repo, clang_source_dir, llvm_revision)
svn_co(source_dir, compiler_repo, compiler_rt_source_dir, llvm_revision)
svn_co(source_dir, libcxx_repo, libcxx_source_dir, llvm_revision)
for p in config.get("patches", {}).get(get_platform(), []):
patch(p, source_dir)
else:
@@ -279,6 +318,22 @@ if __name__ == "__main__":
svn_update(compiler_rt_source_dir, llvm_revision)
svn_update(libcxx_source_dir, llvm_revision)
symlinks = [(source_dir + "/clang",
llvm_source_dir + "/tools/clang"),
(source_dir + "/compiler-rt",
llvm_source_dir + "/projects/compiler-rt"),
(source_dir + "/libcxx",
llvm_source_dir + "/projects/libcxx")]
for l in symlinks:
# On Windows, we have to re-copy the whole directory every time.
if not is_windows() and os.path.islink(l[1]):
continue
if os.path.isdir(l[1]):
shutil.rmtree(l[1])
elif os.path.exists(l[1]):
os.unlink(l[1])
symlink(l[0], l[1])
if not os.path.exists(build_dir):
os.makedirs(build_dir)
@@ -292,7 +347,7 @@ if __name__ == "__main__":
extra_cxxflags = "-stdlib=libc++"
extra_cflags2 = ""
extra_cxxflags2 = "-stdlib=libc++"
else:
elif is_linux():
extra_cflags = "-static-libgcc"
extra_cxxflags = "-static-libgcc -static-libstdc++"
extra_cflags2 = "-fPIC --gcc-toolchain=%s" % gcc_dir
@@ -302,10 +357,15 @@ if __name__ == "__main__":
os.environ['LD_LIBRARY_PATH'] = '%s/lib64/:%s' % (gcc_dir, os.environ['LD_LIBRARY_PATH']);
else:
os.environ['LD_LIBRARY_PATH'] = '%s/lib64/' % gcc_dir
elif is_windows():
extra_cflags = ""
extra_cxxflags = ""
extra_cflags2 = ""
extra_cxxflags2 = ""
build_one_stage(
{"CC": cc + " %s" % extra_cflags,
"CXX": cxx + " %s" % extra_cxxflags},
cc + " %s" % extra_cflags,
cxx + " %s" % extra_cxxflags,
llvm_source_dir, stage1_dir, build_libcxx,
build_type, assertions, python_path)
@@ -314,8 +374,10 @@ if __name__ == "__main__":
stage2_inst_dir = stage2_dir + '/clang'
final_stage_dir = stage2_dir
build_one_stage(
{"CC": stage1_inst_dir + "/bin/clang %s" % extra_cflags2,
"CXX": stage1_inst_dir + "/bin/clang++ %s" % extra_cxxflags2},
stage1_inst_dir + "/bin/%s%s %s" %
(cc_name, exe_ext, extra_cflags2),
stage1_inst_dir + "/bin/%s%s %s" %
(cxx_name, exe_ext, extra_cxxflags2),
llvm_source_dir, stage2_dir, build_libcxx,
build_type, assertions, python_path)
@@ -323,19 +385,21 @@ if __name__ == "__main__":
stage3_dir = build_dir + '/stage3'
final_stage_dir = stage3_dir
build_one_stage(
{"CC": stage2_inst_dir + "/bin/clang %s" % extra_cflags2,
"CXX": stage2_inst_dir + "/bin/clang++ %s" % extra_cxxflags2},
stage2_inst_dir + "/bin/%s%s %s" %
(cc_name, exe_ext, extra_cflags2),
stage2_inst_dir + "/bin/%s%s %s" %
(cxx_name, exe_ext, extra_cxxflags2),
llvm_source_dir, stage3_dir, build_libcxx,
build_type, assertions, python_path)
if not is_darwin():
if is_linux():
final_stage_inst_dir = final_stage_dir + '/clang'
build_and_use_libgcc(
{"CC": cc + " %s" % extra_cflags,
"CXX": cxx + " %s" % extra_cxxflags},
final_stage_inst_dir)
if is_darwin():
if is_darwin() or is_windows():
build_tar_package("tar", "clang.tar.bz2", final_stage_dir, "clang")
else:
build_tar_package("tar", "clang.tar.xz", final_stage_dir, "clang")
@@ -0,0 +1,16 @@
{
"llvm_revision": "259916",
"stages": "3",
"build_libcxx": false,
"build_type": "Release",
"assertions": false,
"llvm_repo": "https://llvm.org/svn/llvm-project/llvm/trunk",
"clang_repo": "https://llvm.org/svn/llvm-project/cfe/trunk",
"compiler_repo": "https://llvm.org/svn/llvm-project/compiler-rt/trunk",
"libcxx_repo": "https://llvm.org/svn/llvm-project/libcxx/trunk",
"python_path": "c:/mozilla-build/python/python.exe",
"cc": "c:/Program Files (x86)/Microsoft Visual Studio 12.0/VC/BIN/amd64_x86/cl.exe",
"cxx": "c:/Program Files (x86)/Microsoft Visual Studio 12.0/VC/BIN/amd64_x86/cl.exe",
"patches": {
}
}
@@ -0,0 +1,16 @@
{
"llvm_revision": "259916",
"stages": "3",
"build_libcxx": false,
"build_type": "Release",
"assertions": false,
"llvm_repo": "https://llvm.org/svn/llvm-project/llvm/trunk",
"clang_repo": "https://llvm.org/svn/llvm-project/cfe/trunk",
"compiler_repo": "https://llvm.org/svn/llvm-project/compiler-rt/trunk",
"libcxx_repo": "https://llvm.org/svn/llvm-project/libcxx/trunk",
"python_path": "c:/mozilla-build/python/python.exe",
"cc": "c:/Program Files (x86)/Microsoft Visual Studio 12.0/VC/BIN/amd64/cl.exe",
"cxx": "c:/Program Files (x86)/Microsoft Visual Studio 12.0/VC/BIN/amd64/cl.exe",
"patches": {
}
}
+22
View File
@@ -71,3 +71,25 @@ if CONFIG['MOZ_APP_BASENAME']:
FINAL_TARGET_PP_FILES += ['update-settings.ini']
DEFINES['TOPOBJDIR'] = TOPOBJDIR
# NOTE: Keep .gdbinit in the topsrcdir for people who run gdb from the topsrcdir.
OBJDIR_FILES += ['/.gdbinit']
# Put a .lldbinit in the bin directory and the objdir, to be picked up
# automatically by LLDB when we debug executables using either of those two
# directories as the current working directory. The .lldbinit file will
# load $(topsrcdir)/.lldbinit, which is where the actual debugging commands are.
DEFINES['topsrcdir'] = TOPSRCDIR
FINAL_TARGET_PP_FILES += ['.lldbinit.in']
OBJDIR_FILES += ['!/dist/bin/.lldbinit']
# Put the .ycm_extra_conf.py file at the root of the objdir. It is used by
# the vim plugin YouCompleteMe.
OBJDIR_FILES += ['/.ycm_extra_conf.py']
if CONFIG['MOZ_VALGRIND']:
OBJDIR_FILES._valgrind += [
'valgrind/cross-architecture.sup',
'valgrind/i386-redhat-linux-gnu.sup',
'valgrind/x86_64-redhat-linux-gnu.sup',
]
+1 -1
View File
@@ -27,7 +27,7 @@ if test -z "$SCCACHE_DISABLE" -a -z "$no_sccache" -a -z "$MOZ_PGO_IS_SET" -a -z
;;
esac
;;
b2g-inbound|mozilla-inbound|fx-team)
mozilla-inbound|fx-team)
case "${master}" in
*use1.mozilla.com*)
bucket=mozilla-releng-s3-cache-us-east-1-prod
+63
View File
@@ -0,0 +1,63 @@
#!/usr/bin/python
import json
import re
import subprocess
import sys
def count_ctors(filename):
proc = subprocess.Popen(
['readelf', '-W', '-S', filename], stdout=subprocess.PIPE)
# Some versions of ld produce both .init_array and .ctors. So we have
# to check for both.
n_init_array_ctors = 0
have_init_array = False
n_ctors_ctors = 0
have_ctors = False
for line in proc.stdout:
f = line.split()
if len(f) != 11:
continue
# Don't try to int()-parse the header line for the section summaries.
if not re.match("\\[\\d+\\]", f[0]):
continue
section_name, contents, size, align = f[1], f[2], int(f[5], 16), int(f[10])
if section_name == ".ctors" and contents == "PROGBITS":
have_ctors = True
# Subtract 2 for the uintptr_t(-1) header and the null terminator.
n_ctors_ctors = size / align - 2
if section_name == ".init_array" and contents == "INIT_ARRAY":
have_init_array = True
n_init_array_ctors = size / align
if have_init_array:
# Even if we have .ctors, we shouldn't have any constructors in .ctors.
# Complain if .ctors does not look how we expect it to.
if have_ctors and n_ctors_ctors != 0:
print >>sys.stderr, "Unexpected .ctors contents for", filename
sys.exit(1)
return n_init_array_ctors
if have_ctors:
return n_ctors_ctors
# We didn't find anything; somebody switched initialization mechanisms on
# us, or the binary is completely busted. Complain either way.
print >>sys.stderr, "Couldn't find .init_array or .ctors in", filename
sys.exit(1)
if __name__ == '__main__':
for f in sys.argv[1:]:
perfherder_data = {
"framework": {"name": "build_metrics"},
"suites": [{
"name": "compiler_metrics",
"subtests": [{
"name": "num_constructors",
"value": count_ctors(f)
}]}
]
}
print "PERFHERDER_DATA: %s" % json.dumps(perfherder_data)
+18 -6
View File
@@ -86,6 +86,7 @@
#include "mozilla/EventStates.h"
#include "mozilla/Telemetry.h"
#include "mozilla/dom/HTMLObjectElementBinding.h"
#include "mozilla/dom/HTMLSharedObjectElement.h"
#ifdef XP_WIN
// Thanks so much, Microsoft! :(
@@ -2969,24 +2970,35 @@ nsObjectLoadingContent::LoadFallback(FallbackType aType, bool aNotify) {
aType = eFallbackAlternate;
}
// We'll set this to null no matter what now, doing it here means we'll load
// child embeds as we find them in the upcoming loop.
mType = eType_Null;
// Do a breadth-first traverse of node tree with the current element as root,
// looking for the first embed we can find.
nsTArray<nsINodeList*> childNodes;
if ((thisContent->IsHTMLElement(nsGkAtoms::object) ||
thisContent->IsHTMLElement(nsGkAtoms::applet)) &&
(aType == eFallbackUnsupported ||
aType == eFallbackDisabled ||
aType == eFallbackBlocklisted))
{
// Show alternate content instead, if it exists
for (nsIContent* child = thisContent->GetFirstChild();
child; child = child->GetNextSibling()) {
if (!child->IsHTMLElement(nsGkAtoms::param) &&
for (nsIContent* child = thisContent->GetFirstChild(); child;
child = child->GetNextNode(thisContent)) {
if (aType != eFallbackAlternate &&
!child->IsHTMLElement(nsGkAtoms::param) &&
nsStyleUtil::IsSignificantChild(child, true, false)) {
aType = eFallbackAlternate;
break;
}
if (child->IsHTMLElement(nsGkAtoms::embed)) {
HTMLSharedObjectElement* object = static_cast<HTMLSharedObjectElement*>(child);
if (object) {
object->StartObjectLoad(true, true);
}
}
}
}
mType = eType_Null;
mFallbackType = aType;
// Notify
@@ -0,0 +1,12 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/REC-html401-19991224/strict.dtd">
<html>
<head>
<title>Bug 1263696 - iframe that should not be loaded</title>
</head>
<body>
<script>
parent.SimpleTest.ok(false, "this iframe should not load");
</script>
</body>
</html>
@@ -0,0 +1,13 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/REC-html401-19991224/strict.dtd">
<html>
<head>
<title>Bug 1263696 - iframe that should be loaded</title>
</head>
<body>
<script>
parent.index = parent.index + 1;
parent.SimpleTest.ok(true, "this iframe should load");
</script>
</body>
</html>
+3
View File
@@ -154,6 +154,8 @@ support-files =
file_bug902350_frame.html
file_bug907892.html
file_bug945152.jar
file_bug1263696_frame_pass.html
file_bug1263696_frame_fail.html
file_general_document.html
file_html_in_xhr.html
file_html_in_xhr.sjs
@@ -856,3 +858,4 @@ skip-if = buildapp == 'b2g' #no ssl support
[test_document_register.html]
[test_bug962251.html]
[test_bug1259588.html]
[test_bug1263696.html]
+53
View File
@@ -0,0 +1,53 @@
<!DOCTYPE html>
<html>
<head>
<meta><charset="utf-8"/>
<title>Test Embed/Object Node Conflicts</title>
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<script type="application/javascript" src="plugin-utils.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
<script type="application/javascript">
SimpleTest.waitForExplicitFinish();
var index = 0;
function startTest() {
is(index, 12, "Should have loaded all passing frames.");
SimpleTest.finish();
}
</script>
</head>
<body onload="startTest()">
<object data="file_bug1263696_frame_pass.html" style="width: 100px; height: 100px">
<embed type="text/html" src="file_bug1263696_frame_fail.html" />
</object>
<object style="width: 100px; height: 100px" data="data:application/x-does-not-exist,test">
<embed type="text/html" src="file_bug1263696_frame_pass.html" />
</object>
<object style="width: 100px; height: 100px" data="data:application/x-does-not-exist,test">
<div></div>
<embed type="text/html" src="file_bug1263696_frame_pass.html" />
</object>
<object style="width: 100px; height: 100px" data="data:application/x-does-not-exist,test">
<div>
<embed type="text/html" src="file_bug1263696_frame_pass.html" />
</div>
</object>
<object style="width: 100px; height: 100px" data="data:application/x-does-not-exist,test">
<embed type="text/html" src="file_bug1263696_frame_pass.html" />
<embed type="text/html" src="file_bug1263696_frame_pass.html" />
<object data="file_bug1263696_frame_pass.html">
<embed type="text/html" src="file_bug1263696_frame_fail.html" />
</object>
<object data="data:application/x-does-not-exist,test">
<embed type="text/html" src="file_bug1263696_frame_pass.html" />
</object>
</object>
<div>
<object data="file_bug1263696_frame_pass.html" style="width: 100px; height: 100px"></object>
<embed type="text/html" src="file_bug1263696_frame_pass.html" />
</div>
<div>
<embed type="text/html" src="file_bug1263696_frame_pass.html" />
<object data="file_bug1263696_frame_pass.html" style="width: 100px; height: 100px"></object>
</div>
</body>
</html>
+1 -1
View File
@@ -2026,7 +2026,7 @@ HTMLMediaElement::CaptureStreamInternal(bool aFinishWhenEnded,
OutputMediaStream* out = mOutputStreams.AppendElement();
MediaStreamTrackSourceGetter* getter = new CaptureStreamTrackSourceGetter(this);
out->mStream = DOMMediaStream::CreateTrackUnionStream(window, aGraph, getter);
out->mStream = DOMMediaStream::CreateTrackUnionStreamAsInput(window, aGraph, getter);
out->mFinishWhenEnded = aFinishWhenEnded;
mAudioCaptured = true;
+1
View File
@@ -28,6 +28,7 @@ public:
// nsISupports
NS_DECL_ISUPPORTS_INHERITED
NS_IMPL_FROMCONTENT_HTML_WITH_TAG(HTMLObjectElement, object)
virtual int32_t TabIndexDefault() override;
#ifdef XP_MACOSX
+8
View File
@@ -64,6 +64,10 @@ HTMLPreElement::MapAttributesIntoRule(const nsMappedAttributes* aAttributes,
NS_IMETHODIMP_(bool)
HTMLPreElement::IsAttributeMapped(const nsIAtom* aAttribute) const
{
if (!mNodeInfo->Equals(nsGkAtoms::pre)) {
return nsGenericHTMLElement::IsAttributeMapped(aAttribute);
}
static const MappedAttributeEntry attributes[] = {
{ &nsGkAtoms::wrap },
{ nullptr },
@@ -80,6 +84,10 @@ HTMLPreElement::IsAttributeMapped(const nsIAtom* aAttribute) const
nsMapRuleToAttributesFunc
HTMLPreElement::GetAttributeMappingFunction() const
{
if (!mNodeInfo->Equals(nsGkAtoms::pre)) {
return nsGenericHTMLElement::GetAttributeMappingFunction();
}
return &MapAttributesIntoRule;
}
+3 -1
View File
@@ -62,13 +62,15 @@ NS_IMPL_STRING_ATTR(HTMLSharedElement, Target, target)
NS_IMETHODIMP
HTMLSharedElement::GetHref(nsAString& aValue)
{
MOZ_ASSERT(mNodeInfo->Equals(nsGkAtoms::base),
"This should only get called for <base> elements");
nsAutoString href;
GetAttr(kNameSpaceID_None, nsGkAtoms::href, href);
nsCOMPtr<nsIURI> uri;
nsIDocument* doc = OwnerDoc();
nsContentUtils::NewURIWithDocumentCharset(
getter_AddRefs(uri), href, doc, doc->GetDocumentURI());
getter_AddRefs(uri), href, doc, doc->GetFallbackBaseURI());
if (!uri) {
aValue = href;
+35 -6
View File
@@ -20,8 +20,9 @@
#ifdef XP_MACOSX
#include "mozilla/EventDispatcher.h"
#include "mozilla/dom/Event.h"
#include "mozilla/dom/HTMLObjectElement.h"
#endif
#include "mozilla/dom/HTMLObjectElement.h"
NS_IMPL_NS_NEW_HTML_ELEMENT_CHECK_PARSER(SharedObject)
@@ -84,7 +85,7 @@ HTMLSharedObjectElement::DoneAddingChildren(bool aHaveNotified)
// If we're already in a document, we need to trigger the load
// Otherwise, BindToTree takes care of that.
if (IsInComposedDoc()) {
StartObjectLoad(aHaveNotified);
StartObjectLoad(aHaveNotified, false);
}
}
}
@@ -192,7 +193,8 @@ HTMLSharedObjectElement::SetAttr(int32_t aNameSpaceID, nsIAtom *aName,
// a document, just in case that the caller wants to set additional
// attributes before inserting the node into the document.
if (aNotify && IsInComposedDoc() && mIsDoneAddingChildren &&
aNameSpaceID == kNameSpaceID_None && aName == URIAttrName()) {
aNameSpaceID == kNameSpaceID_None && aName == URIAttrName()
&& !BlockEmbedContentLoading()) {
return LoadObject(aNotify, true);
}
@@ -320,15 +322,16 @@ HTMLSharedObjectElement::GetAttributeMappingFunction() const
}
void
HTMLSharedObjectElement::StartObjectLoad(bool aNotify)
HTMLSharedObjectElement::StartObjectLoad(bool aNotify, bool aForceLoad)
{
// BindToTree can call us asynchronously, and we may be removed from the tree
// in the interim
if (!IsInComposedDoc() || !OwnerDoc()->IsActive()) {
if (!IsInComposedDoc() || !OwnerDoc()->IsActive() ||
BlockEmbedContentLoading()) {
return;
}
LoadObject(aNotify);
LoadObject(aNotify, aForceLoad);
SetIsNetworkCreated(false);
}
@@ -400,5 +403,31 @@ HTMLSharedObjectElement::GetContentPolicyType() const
}
}
bool
HTMLSharedObjectElement::BlockEmbedContentLoading()
{
// Only check on embed elements
if (!IsHTMLElement(nsGkAtoms::embed)) {
return false;
}
// Traverse up the node tree to see if we have any ancestors that may block us
// from loading
for (nsIContent* parent = GetParent(); parent; parent = parent->GetParent()) {
if (parent->IsAnyOfHTMLElements(nsGkAtoms::video, nsGkAtoms::audio)) {
return true;
}
// If we have an ancestor that is an object with a source, it'll have an
// associated displayed type. If that type is not null, don't load content
// for the embed.
if (HTMLObjectElement* object = HTMLObjectElement::FromContent(parent)) {
uint32_t type = object->DisplayedType();
if (type != eType_Null) {
return true;
}
}
}
return false;
}
} // namespace dom
} // namespace mozilla
+21 -6
View File
@@ -80,7 +80,7 @@ public:
nsresult CopyInnerTo(Element* aDest);
void StartObjectLoad() { StartObjectLoad(true); }
void StartObjectLoad() { StartObjectLoad(true, false); }
NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED_NO_UNLINK(HTMLSharedObjectElement,
nsGenericHTMLElement)
@@ -192,6 +192,11 @@ public:
return GetContentDocument();
}
/**
* Calls LoadObject with the correct arguments to start the plugin load.
*/
void StartObjectLoad(bool aNotify, bool aForceLoad);
protected:
// Override for nsImageLoadingContent.
nsIContent* AsContent() override { return this; }
@@ -199,11 +204,6 @@ protected:
private:
virtual ~HTMLSharedObjectElement();
/**
* Calls LoadObject with the correct arguments to start the plugin load.
*/
void StartObjectLoad(bool aNotify);
nsIAtom *URIAttrName() const
{
return mNodeInfo->Equals(nsGkAtoms::applet) ?
@@ -224,6 +224,21 @@ private:
static void MapAttributesIntoRule(const nsMappedAttributes* aAttributes,
nsRuleData* aData);
/**
* Decides whether we should load embed node content.
*
* If this is an embed node there are cases in which we should not try to load
* the content:
*
* - If the embed node is the child of a media element
* - If the embed node is the child of an object node that already has
* content being loaded.
*
* In these cases, this function will return false, which will cause
* us to skip calling LoadObject.
*/
bool BlockEmbedContentLoading();
};
} // namespace dom
+2 -2
View File
@@ -166,7 +166,7 @@ HTML_TAG("label", "Label");
HTML_TAG("legend", "Legend");
HTML_TAG("li", "LI");
HTML_TAG("link", "Link");
HTML_TAG("listing", "");
HTML_TAG("listing", "Pre");
HTML_TAG("main", "");
HTML_TAG("map", "Map");
HTML_TAG("mark", "");
@@ -223,7 +223,7 @@ HTML_TAG("u", "");
HTML_TAG("ul", "UList");
HTML_TAG("var", "");
HTML_TAG("wbr", "");
HTML_TAG("xmp", "");
HTML_TAG("xmp", "Pre");
function tagName(aTag) {
return "<" + aTag + ">";
+1 -1
View File
@@ -64,7 +64,7 @@ InputPort::Init(nsIInputPortData* aData, nsIInputPortListener* aListener, ErrorR
MediaStreamGraph* graph =
MediaStreamGraph::GetInstance(MediaStreamGraph::SYSTEM_THREAD_DRIVER,
AudioChannel::Normal);
mStream = DOMMediaStream::CreateSourceStream(GetOwner(), graph);
mStream = DOMMediaStream::CreateSourceStreamAsInput(GetOwner(), graph);
}
void
+2 -2
View File
@@ -172,8 +172,8 @@ XMLDocumentLoadPrincipalMismatch=Use of document.load forbidden on Documents tha
IndexedDBTransactionAbortNavigation=An IndexedDB transaction that was not yet complete has been aborted due to page navigation.
# LOCALIZATION NOTE: Do not translate Will-change, %1$S,%2$S are numbers.
IgnoringWillChangeOverBudgetWarning=Will-change memory consumption is too high. Budget limit is the document surface area multiplied by %1$S (%2$S px). Occurrences of will-change over the budget will be ignored.
# LOCALIZATION NOTE: Do not translate "ServiceWorker".
HittingMaxWorkersPerDomain=A ServiceWorker could not be started immediately because other documents in the same origin are already using the maximum number of workers. The ServiceWorker is now queued and will be started after some of the other workers have completed.
# LOCALIZATION NOTE: Do not translate "Worker".
HittingMaxWorkersPerDomain2=A Worker could not be started immediately because other documents in the same origin are already using the maximum number of workers. The Worker is now queued and will be started after some of the other workers have completed.
# LOCALIZATION NOTE: Do not translate "setVelocity", "PannerNode", "AudioListener", "speedOfSound" and "dopplerFactor"
PannerNodeDopplerWarning=Use of setVelocity on the PannerNode and AudioListener, and speedOfSound and dopplerFactor on the AudioListener are deprecated and those members will be removed. For more help https://developer.mozilla.org/en-US/docs/Web/API/AudioListener#Deprecated_features
# LOCALIZATION NOTE: Do not translate "Application Cache API", "AppCache" and "ServiceWorker".
+3 -3
View File
@@ -71,7 +71,7 @@ AudioCaptureStream::ProcessInput(GraphTime aFrom, GraphTime aTo,
}
uint32_t inputCount = mInputs.Length();
StreamBuffer::Track* track = EnsureTrack(mTrackId);
StreamTracks::Track* track = EnsureTrack(mTrackId);
// Notify the DOM everything is in order.
if (!mTrackCreated) {
for (uint32_t i = 0; i < mListeners.Length(); i++) {
@@ -101,7 +101,7 @@ AudioCaptureStream::ProcessInput(GraphTime aFrom, GraphTime aTo,
AudioSegment output;
for (uint32_t i = 0; i < inputCount; i++) {
MediaStream* s = mInputs[i]->GetSource();
StreamBuffer::TrackIter tracks(s->GetStreamBuffer(), MediaSegment::AUDIO);
StreamTracks::TrackIter tracks(s->GetStreamTracks(), MediaSegment::AUDIO);
while (!tracks.IsEnded()) {
AudioSegment* inputSegment = tracks->Get<AudioSegment>();
StreamTime inputStart = s->GraphTimeToStreamTimeWithBlocking(aFrom);
@@ -121,7 +121,7 @@ AudioCaptureStream::ProcessInput(GraphTime aFrom, GraphTime aTo,
}
// Regardless of the status of the input tracks, we go foward.
mBuffer.AdvanceKnownTracksTime(GraphTimeToStreamTimeWithBlocking((aTo)));
mTracks.AdvanceKnownTracksTime(GraphTimeToStreamTimeWithBlocking((aTo)));
}
void
+1 -1
View File
@@ -8,7 +8,7 @@
#include "MediaStreamGraph.h"
#include "AudioMixer.h"
#include "StreamBuffer.h"
#include "StreamTracks.h"
#include <algorithm>
namespace mozilla
+1 -1
View File
@@ -8,7 +8,7 @@
#include "DOMMediaStream.h"
#include "mozilla/dom/HTMLCanvasElement.h"
#include "StreamBuffer.h"
#include "StreamTracks.h"
class nsIPrincipal;
+20 -20
View File
@@ -853,9 +853,9 @@ DOMMediaStream::InitPlaybackStreamCommon(MediaStreamGraph* aGraph)
}
already_AddRefed<DOMMediaStream>
DOMMediaStream::CreateSourceStream(nsPIDOMWindowInner* aWindow,
MediaStreamGraph* aGraph,
MediaStreamTrackSourceGetter* aTrackSourceGetter)
DOMMediaStream::CreateSourceStreamAsInput(nsPIDOMWindowInner* aWindow,
MediaStreamGraph* aGraph,
MediaStreamTrackSourceGetter* aTrackSourceGetter)
{
RefPtr<DOMMediaStream> stream = new DOMMediaStream(aWindow, aTrackSourceGetter);
stream->InitSourceStream(aGraph);
@@ -863,9 +863,9 @@ DOMMediaStream::CreateSourceStream(nsPIDOMWindowInner* aWindow,
}
already_AddRefed<DOMMediaStream>
DOMMediaStream::CreateTrackUnionStream(nsPIDOMWindowInner* aWindow,
MediaStreamGraph* aGraph,
MediaStreamTrackSourceGetter* aTrackSourceGetter)
DOMMediaStream::CreateTrackUnionStreamAsInput(nsPIDOMWindowInner* aWindow,
MediaStreamGraph* aGraph,
MediaStreamTrackSourceGetter* aTrackSourceGetter)
{
RefPtr<DOMMediaStream> stream = new DOMMediaStream(aWindow, aTrackSourceGetter);
stream->InitTrackUnionStream(aGraph);
@@ -873,9 +873,9 @@ DOMMediaStream::CreateTrackUnionStream(nsPIDOMWindowInner* aWindow,
}
already_AddRefed<DOMMediaStream>
DOMMediaStream::CreateAudioCaptureStream(nsPIDOMWindowInner* aWindow,
nsIPrincipal* aPrincipal,
MediaStreamGraph* aGraph)
DOMMediaStream::CreateAudioCaptureStreamAsInput(nsPIDOMWindowInner* aWindow,
nsIPrincipal* aPrincipal,
MediaStreamGraph* aGraph)
{
// Audio capture doesn't create tracks dynamically
MediaStreamTrackSourceGetter* getter = nullptr;
@@ -1300,9 +1300,9 @@ DOMLocalMediaStream::StopImpl()
}
already_AddRefed<DOMLocalMediaStream>
DOMLocalMediaStream::CreateSourceStream(nsPIDOMWindowInner* aWindow,
MediaStreamGraph* aGraph,
MediaStreamTrackSourceGetter* aTrackSourceGetter)
DOMLocalMediaStream::CreateSourceStreamAsInput(nsPIDOMWindowInner* aWindow,
MediaStreamGraph* aGraph,
MediaStreamTrackSourceGetter* aTrackSourceGetter)
{
RefPtr<DOMLocalMediaStream> stream =
new DOMLocalMediaStream(aWindow, aTrackSourceGetter);
@@ -1311,9 +1311,9 @@ DOMLocalMediaStream::CreateSourceStream(nsPIDOMWindowInner* aWindow,
}
already_AddRefed<DOMLocalMediaStream>
DOMLocalMediaStream::CreateTrackUnionStream(nsPIDOMWindowInner* aWindow,
MediaStreamGraph* aGraph,
MediaStreamTrackSourceGetter* aTrackSourceGetter)
DOMLocalMediaStream::CreateTrackUnionStreamAsInput(nsPIDOMWindowInner* aWindow,
MediaStreamGraph* aGraph,
MediaStreamTrackSourceGetter* aTrackSourceGetter)
{
RefPtr<DOMLocalMediaStream> stream =
new DOMLocalMediaStream(aWindow, aTrackSourceGetter);
@@ -1332,9 +1332,9 @@ DOMAudioNodeMediaStream::~DOMAudioNodeMediaStream()
}
already_AddRefed<DOMAudioNodeMediaStream>
DOMAudioNodeMediaStream::CreateTrackUnionStream(nsPIDOMWindowInner* aWindow,
AudioNode* aNode,
MediaStreamGraph* aGraph)
DOMAudioNodeMediaStream::CreateTrackUnionStreamAsInput(nsPIDOMWindowInner* aWindow,
AudioNode* aNode,
MediaStreamGraph* aGraph)
{
RefPtr<DOMAudioNodeMediaStream> stream = new DOMAudioNodeMediaStream(aWindow, aNode);
stream->InitTrackUnionStream(aGraph);
@@ -1443,7 +1443,7 @@ DOMHwMediaStream::SetImageSize(uint32_t width, uint32_t height)
#endif
SourceMediaStream* srcStream = GetInputStream()->AsSourceStream();
StreamBuffer::Track* track = srcStream->FindTrack(TRACK_VIDEO_PRIMARY);
StreamTracks::Track* track = srcStream->FindTrack(TRACK_VIDEO_PRIMARY);
if (!track || !track->GetSegment()) {
return;
@@ -1479,7 +1479,7 @@ DOMHwMediaStream::SetOverlayImage(OverlayImage* aImage)
#endif
SourceMediaStream* srcStream = GetInputStream()->AsSourceStream();
StreamBuffer::Track* track = srcStream->FindTrack(TRACK_VIDEO_PRIMARY);
StreamTracks::Track* track = srcStream->FindTrack(TRACK_VIDEO_PRIMARY);
if (!track || !track->GetSegment()) {
return;
+19 -19
View File
@@ -10,7 +10,7 @@
#include "nsCycleCollectionParticipant.h"
#include "nsWrapperCache.h"
#include "StreamBuffer.h"
#include "StreamTracks.h"
#include "nsIDOMWindow.h"
#include "nsIPrincipal.h"
#include "mozilla/DOMEventTargetHelper.h"
@@ -484,25 +484,25 @@ public:
/**
* Create a DOMMediaStream whose underlying input stream is a SourceMediaStream.
*/
static already_AddRefed<DOMMediaStream> CreateSourceStream(nsPIDOMWindowInner* aWindow,
MediaStreamGraph* aGraph,
MediaStreamTrackSourceGetter* aTrackSourceGetter = nullptr);
static already_AddRefed<DOMMediaStream> CreateSourceStreamAsInput(nsPIDOMWindowInner* aWindow,
MediaStreamGraph* aGraph,
MediaStreamTrackSourceGetter* aTrackSourceGetter = nullptr);
/**
* Create a DOMMediaStream whose underlying input stream is a TrackUnionStream.
*/
static already_AddRefed<DOMMediaStream> CreateTrackUnionStream(nsPIDOMWindowInner* aWindow,
MediaStreamGraph* aGraph,
MediaStreamTrackSourceGetter* aTrackSourceGetter = nullptr);
static already_AddRefed<DOMMediaStream> CreateTrackUnionStreamAsInput(nsPIDOMWindowInner* aWindow,
MediaStreamGraph* aGraph,
MediaStreamTrackSourceGetter* aTrackSourceGetter = nullptr);
/**
* Create an DOMMediaStream whose underlying input stream is an
* AudioCaptureStream.
*/
static already_AddRefed<DOMMediaStream>
CreateAudioCaptureStream(nsPIDOMWindowInner* aWindow,
nsIPrincipal* aPrincipal,
MediaStreamGraph* aGraph);
CreateAudioCaptureStreamAsInput(nsPIDOMWindowInner* aWindow,
nsIPrincipal* aPrincipal,
MediaStreamGraph* aGraph);
void SetLogicalStreamStartTime(StreamTime aTime)
{
@@ -719,17 +719,17 @@ public:
* Create an nsDOMLocalMediaStream whose underlying stream is a SourceMediaStream.
*/
static already_AddRefed<DOMLocalMediaStream>
CreateSourceStream(nsPIDOMWindowInner* aWindow,
MediaStreamGraph* aGraph,
MediaStreamTrackSourceGetter* aTrackSourceGetter = nullptr);
CreateSourceStreamAsInput(nsPIDOMWindowInner* aWindow,
MediaStreamGraph* aGraph,
MediaStreamTrackSourceGetter* aTrackSourceGetter = nullptr);
/**
* Create an nsDOMLocalMediaStream whose underlying stream is a TrackUnionStream.
*/
static already_AddRefed<DOMLocalMediaStream>
CreateTrackUnionStream(nsPIDOMWindowInner* aWindow,
MediaStreamGraph* aGraph,
MediaStreamTrackSourceGetter* aTrackSourceGetter = nullptr);
CreateTrackUnionStreamAsInput(nsPIDOMWindowInner* aWindow,
MediaStreamGraph* aGraph,
MediaStreamTrackSourceGetter* aTrackSourceGetter = nullptr);
protected:
virtual ~DOMLocalMediaStream();
@@ -753,9 +753,9 @@ public:
* Create a DOMAudioNodeMediaStream whose underlying stream is a TrackUnionStream.
*/
static already_AddRefed<DOMAudioNodeMediaStream>
CreateTrackUnionStream(nsPIDOMWindowInner* aWindow,
AudioNode* aNode,
MediaStreamGraph* aGraph);
CreateTrackUnionStreamAsInput(nsPIDOMWindowInner* aWindow,
AudioNode* aNode,
MediaStreamGraph* aGraph);
protected:
~DOMAudioNodeMediaStream();
+1 -1
View File
@@ -14,7 +14,7 @@
#include "nsTArray.h"
#include "ImageTypes.h"
#include "MediaData.h"
#include "StreamBuffer.h" // for TrackID
#include "StreamTracks.h" // for TrackID
#include "TimeUnits.h"
namespace mozilla {
+3 -3
View File
@@ -835,7 +835,7 @@ public:
// not a problem here, we got explicit user content.
nsCOMPtr<nsIPrincipal> principal = window->GetExtantDoc()->NodePrincipal();
domStream =
DOMMediaStream::CreateAudioCaptureStream(window, principal, msg);
DOMMediaStream::CreateAudioCaptureStreamAsInput(window, principal, msg);
stream = msg->CreateSourceStream(nullptr); // Placeholder
msg->RegisterCaptureStreamForWindow(
@@ -921,8 +921,8 @@ public:
// avoid us blocking. Pass a simple TrackSourceGetter for potential
// fake tracks. Apart from them gUM never adds tracks dynamically.
domStream =
DOMLocalMediaStream::CreateSourceStream(window, msg,
new FakeTrackSourceGetter(principal));
DOMLocalMediaStream::CreateSourceStreamAsInput(window, msg,
new FakeTrackSourceGetter(principal));
if (mAudioDevice) {
nsString audioDeviceName;
+1 -1
View File
@@ -45,7 +45,7 @@ typedef int64_t MediaTime;
const int64_t MEDIA_TIME_MAX = TRACK_TICKS_MAX;
/**
* Media time relative to the start of a StreamBuffer.
* Media time relative to the start of a StreamTracks.
*/
typedef MediaTime StreamTime;
const StreamTime STREAM_TIME_MAX = MEDIA_TIME_MAX;
+41 -41
View File
@@ -101,7 +101,7 @@ MediaStreamGraphImpl::FinishStream(MediaStream* aStream)
return;
STREAM_LOG(LogLevel::Debug, ("MediaStream %p will finish", aStream));
#ifdef DEBUG
for (StreamBuffer::TrackIter track(aStream->mBuffer);
for (StreamTracks::TrackIter track(aStream->mTracks);
!track.IsEnded(); track.Next()) {
if (!track->IsEnded()) {
STREAM_LOG(LogLevel::Error,
@@ -112,7 +112,7 @@ MediaStreamGraphImpl::FinishStream(MediaStream* aStream)
}
#endif
aStream->mFinished = true;
aStream->mBuffer.AdvanceKnownTracksTime(STREAM_TIME_MAX);
aStream->mTracks.AdvanceKnownTracksTime(STREAM_TIME_MAX);
SetStreamOrderDirty();
}
@@ -120,7 +120,7 @@ MediaStreamGraphImpl::FinishStream(MediaStream* aStream)
void
MediaStreamGraphImpl::AddStreamGraphThread(MediaStream* aStream)
{
aStream->mBufferStartTime = mProcessedTime;
aStream->mTracksStartTime = mProcessedTime;
if (aStream->IsSuspended()) {
mSuspendedStreams.AppendElement(aStream);
STREAM_LOG(LogLevel::Debug, ("Adding media stream %p to the graph, in the suspended stream array", aStream));
@@ -180,14 +180,14 @@ MediaStreamGraphImpl::ExtractPendingInput(SourceMediaStream* aStream,
StreamTime t = aStream->GraphTimeToStreamTime(aDesiredUpToTime);
STREAM_LOG(LogLevel::Verbose, ("Calling NotifyPull aStream=%p t=%f current end=%f", aStream,
MediaTimeToSeconds(t),
MediaTimeToSeconds(aStream->mBuffer.GetEnd())));
if (t > aStream->mBuffer.GetEnd()) {
MediaTimeToSeconds(aStream->mTracks.GetEnd())));
if (t > aStream->mTracks.GetEnd()) {
*aEnsureNextIteration = true;
#ifdef DEBUG
if (aStream->mListeners.Length() == 0) {
STREAM_LOG(LogLevel::Error, ("No listeners in NotifyPull aStream=%p desired=%f current end=%f",
aStream, MediaTimeToSeconds(t),
MediaTimeToSeconds(aStream->mBuffer.GetEnd())));
MediaTimeToSeconds(aStream->mTracks.GetEnd())));
aStream->DumpTrackInfo();
}
#endif
@@ -206,7 +206,7 @@ MediaStreamGraphImpl::ExtractPendingInput(SourceMediaStream* aStream,
SourceMediaStream::TrackData* data = &aStream->mUpdateTracks[i];
aStream->ApplyTrackDisabling(data->mID, data->mData);
StreamTime offset = (data->mCommands & SourceMediaStream::TRACK_CREATE)
? data->mStart : aStream->mBuffer.FindTrack(data->mID)->GetSegment()->GetDuration();
? data->mStart : aStream->mTracks.FindTrack(data->mID)->GetSegment()->GetDuration();
for (MediaStreamListener* l : aStream->mListeners) {
l->NotifyQueuedTrackChanges(this, data->mID,
offset, data->mCommands, *data->mData);
@@ -227,14 +227,14 @@ MediaStreamGraphImpl::ExtractPendingInput(SourceMediaStream* aStream,
int64_t(segment->GetDuration())));
data->mEndOfFlushedData += segment->GetDuration();
aStream->mBuffer.AddTrack(data->mID, data->mStart, segment);
aStream->mTracks.AddTrack(data->mID, data->mStart, segment);
// The track has taken ownership of data->mData, so let's replace
// data->mData with an empty clone.
data->mData = segment->CreateEmptyClone();
data->mCommands &= ~SourceMediaStream::TRACK_CREATE;
notifiedTrackCreated = true;
} else if (data->mData->GetDuration() > 0) {
MediaSegment* dest = aStream->mBuffer.FindTrack(data->mID)->GetSegment();
MediaSegment* dest = aStream->mTracks.FindTrack(data->mID)->GetSegment();
STREAM_LOG(LogLevel::Verbose, ("SourceMediaStream %p track %d, advancing end from %lld to %lld",
aStream, data->mID,
int64_t(dest->GetDuration()),
@@ -243,7 +243,7 @@ MediaStreamGraphImpl::ExtractPendingInput(SourceMediaStream* aStream,
dest->AppendFrom(data->mData);
}
if (data->mCommands & SourceMediaStream::TRACK_END) {
aStream->mBuffer.FindTrack(data->mID)->SetEnded();
aStream->mTracks.FindTrack(data->mID)->SetEnded();
aStream->mUpdateTracks.RemoveElementAt(i);
}
}
@@ -253,10 +253,10 @@ MediaStreamGraphImpl::ExtractPendingInput(SourceMediaStream* aStream,
}
}
if (!aStream->mFinished) {
aStream->mBuffer.AdvanceKnownTracksTime(aStream->mUpdateKnownTracksTime);
aStream->mTracks.AdvanceKnownTracksTime(aStream->mUpdateKnownTracksTime);
}
}
if (aStream->mBuffer.GetEnd() > 0) {
if (aStream->mTracks.GetEnd() > 0) {
aStream->mHasCurrentData = true;
}
if (finished) {
@@ -271,7 +271,7 @@ MediaStreamGraphImpl::GraphTimeToStreamTimeWithBlocking(MediaStream* aStream,
MOZ_ASSERT(aTime <= mStateComputedTime,
"Don't ask about times where we haven't made blocking decisions yet");
return std::max<StreamTime>(0,
std::min(aTime, aStream->mStartBlocking) - aStream->mBufferStartTime);
std::min(aTime, aStream->mStartBlocking) - aStream->mTracksStartTime);
}
GraphTime
@@ -294,7 +294,7 @@ MediaStreamGraphImpl::UpdateCurrentTimeForStreams(GraphTime aPrevCurrentTime)
blockedTime);
STREAM_LOG(LogLevel::Verbose,
("MediaStream %p bufferStartTime=%f blockedTime=%f", stream,
MediaTimeToSeconds(stream->mBufferStartTime),
MediaTimeToSeconds(stream->mTracksStartTime),
MediaTimeToSeconds(blockedTime)));
stream->mStartBlocking = mStateComputedTime;
@@ -326,7 +326,7 @@ MediaStreamGraphImpl::UpdateCurrentTimeForStreams(GraphTime aPrevCurrentTime)
// out.
if (stream->mFinished && !stream->mNotifiedFinished &&
mProcessedTime >=
stream->StreamTimeToGraphTime(stream->GetStreamBuffer().GetAllTracksEnd())) {
stream->StreamTimeToGraphTime(stream->GetStreamTracks().GetAllTracksEnd())) {
stream->mNotifiedFinished = true;
SetStreamOrderDirty();
for (uint32_t j = 0; j < stream->mListeners.Length(); ++j) {
@@ -382,7 +382,7 @@ MediaStreamGraphImpl::ProcessChunkMetadata(GraphTime aPrevCurrentTime)
for (MediaStream* stream : AllStreams()) {
StreamTime iterationStart = stream->GraphTimeToStreamTime(aPrevCurrentTime);
StreamTime iterationEnd = stream->GraphTimeToStreamTime(mProcessedTime);
for (StreamBuffer::TrackIter tracks(stream->mBuffer);
for (StreamTracks::TrackIter tracks(stream->mTracks);
!tracks.IsEnded(); tracks.Next()) {
MediaSegment* segment = tracks->GetSegment();
if (!segment) {
@@ -416,13 +416,13 @@ MediaStreamGraphImpl::WillUnderrun(MediaStream* aStream,
// This stream isn't finished or suspended. We don't need to call
// StreamTimeToGraphTime since an underrun is the only thing that can block
// it.
GraphTime bufferEnd = aStream->GetBufferEnd() + aStream->mBufferStartTime;
GraphTime bufferEnd = aStream->GetTracksEnd() + aStream->mTracksStartTime;
#ifdef DEBUG
if (bufferEnd < mProcessedTime) {
STREAM_LOG(LogLevel::Error, ("MediaStream %p underrun, "
"bufferEnd %f < mProcessedTime %f (%lld < %lld), Streamtime %lld",
aStream, MediaTimeToSeconds(bufferEnd), MediaTimeToSeconds(mProcessedTime),
bufferEnd, mProcessedTime, aStream->GetBufferEnd()));
bufferEnd, mProcessedTime, aStream->GetTracksEnd()));
aStream->DumpTrackInfo();
NS_ASSERTION(bufferEnd >= mProcessedTime, "Buffer underran");
}
@@ -455,7 +455,7 @@ MediaStreamGraphImpl::AudioTrackPresent(bool& aNeedsAEC)
if (stream->AsAudioNodeStream()) {
audioTrackPresent = true;
} else {
for (StreamBuffer::TrackIter tracks(stream->GetStreamBuffer(), MediaSegment::AUDIO);
for (StreamTracks::TrackIter tracks(stream->GetStreamTracks(), MediaSegment::AUDIO);
!tracks.IsEnded(); tracks.Next()) {
audioTrackPresent = true;
}
@@ -749,7 +749,7 @@ MediaStreamGraphImpl::CreateOrDestroyAudioStreams(MediaStream* aStream)
return;
}
if (!aStream->GetStreamBuffer().GetAndResetTracksDirty() &&
if (!aStream->GetStreamTracks().GetAndResetTracksDirty() &&
!aStream->mAudioOutputStreams.IsEmpty()) {
return;
}
@@ -761,7 +761,7 @@ MediaStreamGraphImpl::CreateOrDestroyAudioStreams(MediaStream* aStream)
audioOutputStreamsFound.AppendElement(false);
}
for (StreamBuffer::TrackIter tracks(aStream->GetStreamBuffer(), MediaSegment::AUDIO);
for (StreamTracks::TrackIter tracks(aStream->GetStreamTracks(), MediaSegment::AUDIO);
!tracks.IsEnded(); tracks.Next()) {
uint32_t i;
for (i = 0; i < audioOutputStreamsFound.Length(); ++i) {
@@ -820,13 +820,13 @@ MediaStreamGraphImpl::PlayAudio(MediaStream* aStream)
ticksWritten = 0;
MediaStream::AudioOutputStream& audioOutput = aStream->mAudioOutputStreams[i];
StreamBuffer::Track* track = aStream->mBuffer.FindTrack(audioOutput.mTrackID);
StreamTracks::Track* track = aStream->mTracks.FindTrack(audioOutput.mTrackID);
AudioSegment* audio = track->Get<AudioSegment>();
AudioSegment output;
StreamTime offset = aStream->GraphTimeToStreamTime(mProcessedTime);
// We don't update aStream->mBufferStartTime here to account for time spent
// We don't update aStream->mTracksStartTime here to account for time spent
// blocked. Instead, we'll update it in UpdateCurrentTimeForStreams after
// the blocked period has completed. But we do need to make sure we play
// from the right offsets in the stream buffer, even if we've already
@@ -945,7 +945,7 @@ MediaStreamGraphImpl::PlayVideo(MediaStream* aStream)
PrincipalHandle lastPrincipalHandle = PRINCIPAL_HANDLE_NONE;
RefPtr<Image> blackImage;
MOZ_ASSERT(mProcessedTime >= aStream->mBufferStartTime, "frame position before buffer?");
MOZ_ASSERT(mProcessedTime >= aStream->mTracksStartTime, "frame position before buffer?");
// We only look at the non-blocking interval
StreamTime frameBufferTime = aStream->GraphTimeToStreamTime(mProcessedTime);
StreamTime bufferEndTime = aStream->GraphTimeToStreamTime(aStream->mStartBlocking);
@@ -957,7 +957,7 @@ MediaStreamGraphImpl::PlayVideo(MediaStream* aStream)
// Pick the last track that has a video chunk for the time, and
// schedule its frame.
chunk = nullptr;
for (StreamBuffer::TrackIter tracks(aStream->GetStreamBuffer(),
for (StreamTracks::TrackIter tracks(aStream->GetStreamTracks(),
MediaSegment::VIDEO);
!tracks.IsEnded();
tracks.Next()) {
@@ -1388,14 +1388,14 @@ MediaStreamGraphImpl::UpdateGraph(GraphTime aEndBlockingDecisions)
// The stream's not suspended, and since it's finished, underruns won't
// stop it playing out. So there's no blocking other than what we impose
// here.
GraphTime endTime = stream->GetStreamBuffer().GetAllTracksEnd() +
stream->mBufferStartTime;
GraphTime endTime = stream->GetStreamTracks().GetAllTracksEnd() +
stream->mTracksStartTime;
if (endTime <= mStateComputedTime) {
STREAM_LOG(LogLevel::Verbose, ("MediaStream %p is blocked due to being finished", stream));
stream->mStartBlocking = mStateComputedTime;
} else {
STREAM_LOG(LogLevel::Verbose, ("MediaStream %p is finished, but not blocked yet (end at %f, with blocking at %f)",
stream, MediaTimeToSeconds(stream->GetBufferEnd()),
stream, MediaTimeToSeconds(stream->GetTracksEnd()),
MediaTimeToSeconds(endTime)));
// Data can't be added to a finished stream, so underruns are irrelevant.
stream->mStartBlocking = std::min(endTime, aEndBlockingDecisions);
@@ -1459,7 +1459,7 @@ MediaStreamGraphImpl::Process()
} else {
ps->ProcessInput(mProcessedTime, mStateComputedTime,
ProcessedMediaStream::ALLOW_FINISH);
NS_WARN_IF_FALSE(stream->mBuffer.GetEnd() >=
NS_WARN_IF_FALSE(stream->mTracks.GetEnd() >=
GraphTimeToStreamTimeWithBlocking(stream, mStateComputedTime),
"Stream did not produce enough data");
}
@@ -1948,7 +1948,7 @@ MediaStreamGraphImpl::AppendMessage(UniquePtr<ControlMessage> aMessage)
}
MediaStream::MediaStream(DOMMediaStream* aWrapper)
: mBufferStartTime(0)
: mTracksStartTime(0)
, mStartBlocking(GRAPH_TIME_MAX)
, mSuspendedCount(0)
, mFinished(false)
@@ -1988,7 +1988,7 @@ MediaStream::SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const
// - mListeners - elements
// - mAudioOutputStream - elements
amount += mBuffer.SizeOfExcludingThis(aMallocSizeOf);
amount += mTracks.SizeOfExcludingThis(aMallocSizeOf);
amount += mAudioOutputs.ShallowSizeOfExcludingThis(aMallocSizeOf);
amount += mVideoOutputs.ShallowSizeOfExcludingThis(aMallocSizeOf);
amount += mListeners.ShallowSizeOfExcludingThis(aMallocSizeOf);
@@ -2023,7 +2023,7 @@ MediaStream::SetGraphImpl(MediaStreamGraphImpl* aGraph)
MOZ_ASSERT(!mGraph, "Should only be called once");
mGraph = aGraph;
mAudioChannelType = aGraph->AudioChannel();
mBuffer.InitGraphRate(aGraph->GraphRate());
mTracks.InitGraphRate(aGraph->GraphRate());
}
void
@@ -2039,16 +2039,16 @@ MediaStream::GraphTimeToStreamTime(GraphTime aTime)
NS_ASSERTION(mStartBlocking == GraphImpl()->mStateComputedTime ||
aTime <= mStartBlocking,
"Incorrectly ignoring blocking!");
return aTime - mBufferStartTime;
return aTime - mTracksStartTime;
}
GraphTime
MediaStream::StreamTimeToGraphTime(StreamTime aTime)
{
NS_ASSERTION(mStartBlocking == GraphImpl()->mStateComputedTime ||
aTime + mBufferStartTime <= mStartBlocking,
aTime + mTracksStartTime <= mStartBlocking,
"Incorrectly ignoring blocking!");
return aTime + mBufferStartTime;
return aTime + mTracksStartTime;
}
StreamTime
@@ -2063,16 +2063,16 @@ MediaStream::FinishOnGraphThread()
GraphImpl()->FinishStream(this);
}
StreamBuffer::Track*
StreamTracks::Track*
MediaStream::FindTrack(TrackID aID)
{
return mBuffer.FindTrack(aID);
return mTracks.FindTrack(aID);
}
StreamBuffer::Track*
StreamTracks::Track*
MediaStream::EnsureTrack(TrackID aTrackId)
{
StreamBuffer::Track* track = mBuffer.FindTrack(aTrackId);
StreamTracks::Track* track = mTracks.FindTrack(aTrackId);
if (!track) {
nsAutoPtr<MediaSegment> segment(new AudioSegment());
for (uint32_t j = 0; j < mListeners.Length(); ++j) {
@@ -2084,7 +2084,7 @@ MediaStream::EnsureTrack(TrackID aTrackId)
// change this.
l->NotifyFinishedTrackCreation(Graph());
}
track = &mBuffer.AddTrack(aTrackId, 0, segment.forget());
track = &mTracks.AddTrack(aTrackId, 0, segment.forget());
}
return track;
}
@@ -2400,7 +2400,7 @@ MediaStream::AddTrackListenerImpl(already_AddRefed<MediaStreamTrackListener> aLi
l->mListener = aListener;
l->mTrackID = aTrackID;
StreamBuffer::Track* track = FindTrack(aTrackID);
StreamTracks::Track* track = FindTrack(aTrackID);
if (!track) {
return;
}
+21 -21
View File
@@ -16,7 +16,7 @@
#include "AudioStream.h"
#include "nsTArray.h"
#include "nsIRunnable.h"
#include "StreamBuffer.h"
#include "StreamTracks.h"
#include "VideoFrameContainer.h"
#include "VideoSegment.h"
#include "MainThreadUtils.h"
@@ -477,7 +477,7 @@ struct TrackBound
* time. To ensure video plays in sync with audio, make sure that the same
* stream is playing both the audio and video.
*
* The data in a stream is managed by StreamBuffer. It consists of a set of
* The data in a stream is managed by StreamTracks. It consists of a set of
* tracks of various types that can start and end over time.
*
* Streams are explicitly managed. The client creates them via
@@ -541,7 +541,7 @@ public:
/**
* Returns sample rate of the graph.
*/
TrackRate GraphRate() { return mBuffer.GraphRate(); }
TrackRate GraphRate() { return mTracks.GraphRate(); }
// Control API.
// Since a stream can be played multiple ways, we need to combine independent
@@ -674,9 +674,9 @@ public:
* This must be idempotent.
*/
virtual void DestroyImpl();
StreamTime GetBufferEnd() { return mBuffer.GetEnd(); }
StreamTime GetTracksEnd() { return mTracks.GetEnd(); }
#ifdef DEBUG
void DumpTrackInfo() { return mBuffer.DumpTrackInfo(); }
void DumpTrackInfo() { return mTracks.DumpTrackInfo(); }
#endif
void SetAudioOutputVolumeImpl(void* aKey, float aVolume);
void AddAudioOutputImpl(void* aKey);
@@ -713,36 +713,36 @@ public:
{
return mConsumers.Length();
}
StreamBuffer& GetStreamBuffer() { return mBuffer; }
GraphTime GetStreamBufferStartTime() { return mBufferStartTime; }
StreamTracks& GetStreamTracks() { return mTracks; }
GraphTime GetStreamTracksStartTime() { return mTracksStartTime; }
double StreamTimeToSeconds(StreamTime aTime)
{
NS_ASSERTION(0 <= aTime && aTime <= STREAM_TIME_MAX, "Bad time");
return static_cast<double>(aTime)/mBuffer.GraphRate();
return static_cast<double>(aTime)/mTracks.GraphRate();
}
int64_t StreamTimeToMicroseconds(StreamTime aTime)
{
NS_ASSERTION(0 <= aTime && aTime <= STREAM_TIME_MAX, "Bad time");
return (aTime*1000000)/mBuffer.GraphRate();
return (aTime*1000000)/mTracks.GraphRate();
}
StreamTime SecondsToNearestStreamTime(double aSeconds)
{
NS_ASSERTION(0 <= aSeconds && aSeconds <= TRACK_TICKS_MAX/TRACK_RATE_MAX,
"Bad seconds");
return mBuffer.GraphRate() * aSeconds + 0.5;
return mTracks.GraphRate() * aSeconds + 0.5;
}
StreamTime MicrosecondsToStreamTimeRoundDown(int64_t aMicroseconds) {
return (aMicroseconds*mBuffer.GraphRate())/1000000;
return (aMicroseconds*mTracks.GraphRate())/1000000;
}
TrackTicks TimeToTicksRoundUp(TrackRate aRate, StreamTime aTime)
{
return RateConvertTicksRoundUp(aRate, mBuffer.GraphRate(), aTime);
return RateConvertTicksRoundUp(aRate, mTracks.GraphRate(), aTime);
}
StreamTime TicksToTimeRoundDown(TrackRate aRate, TrackTicks aTicks)
{
return RateConvertTicksRoundDown(mBuffer.GraphRate(), aRate, aTicks);
return RateConvertTicksRoundDown(mTracks.GraphRate(), aRate, aTicks);
}
/**
* Convert graph time to stream time. aTime must be <= mStateComputedTime
@@ -773,9 +773,9 @@ public:
/**
* Find track by track id.
*/
StreamBuffer::Track* FindTrack(TrackID aID);
StreamTracks::Track* FindTrack(TrackID aID);
StreamBuffer::Track* EnsureTrack(TrackID aTrack);
StreamTracks::Track* EnsureTrack(TrackID aTrack);
virtual void ApplyTrackDisabling(TrackID aTrackID, MediaSegment* aSegment, MediaSegment* aRawSegment = nullptr);
@@ -808,8 +808,8 @@ public:
protected:
void AdvanceTimeVaryingValuesToCurrentTime(GraphTime aCurrentTime, GraphTime aBlockedTime)
{
mBufferStartTime += aBlockedTime;
mBuffer.ForgetUpTo(aCurrentTime - mBufferStartTime);
mTracksStartTime += aBlockedTime;
mTracks.ForgetUpTo(aCurrentTime - mTracksStartTime);
}
void NotifyMainThreadListeners()
@@ -836,14 +836,14 @@ protected:
// This state is all initialized on the main thread but
// otherwise modified only on the media graph thread.
// Buffered data. The start of the buffer corresponds to mBufferStartTime.
// Buffered data. The start of the buffer corresponds to mTracksStartTime.
// Conceptually the buffer contains everything this stream has ever played,
// but we forget some prefix of the buffered data to bound the space usage.
StreamBuffer mBuffer;
StreamTracks mTracks;
// The time when the buffered data could be considered to have started playing.
// This increases over time to account for time the stream was blocked before
// mCurrentTime.
GraphTime mBufferStartTime;
GraphTime mTracksStartTime;
// Client-set volume of this stream
struct AudioOutput {
@@ -863,7 +863,7 @@ protected:
// GraphTime at which this stream starts blocking.
// This is only valid up to mStateComputedTime. The stream is considered to
// have not been blocked before mCurrentTime (its mBufferStartTime is increased
// have not been blocked before mCurrentTime (its mTracksStartTime is increased
// as necessary to account for that time instead).
GraphTime mStartBlocking;
+1 -1
View File
@@ -403,7 +403,7 @@ public:
void PlayVideo(MediaStream* aStream);
/**
* No more data will be forthcoming for aStream. The stream will end
* at the current buffer end point. The StreamBuffer's tracks must be
* at the current buffer end point. The StreamTracks's tracks must be
* explicitly set to finished by the caller.
*/
void OpenAudioInputImpl(int aID,
+1 -1
View File
@@ -10,7 +10,7 @@
#include "nsError.h"
#include "nsID.h"
#include "nsIPrincipal.h"
#include "StreamBuffer.h"
#include "StreamTracks.h"
#include "MediaTrackConstraints.h"
#include "mozilla/CORSMode.h"
#include "PrincipalChangeObserver.h"
-1
View File
@@ -1172,7 +1172,6 @@ RTCPeerConnection.prototype = {
return null;
}
sdp = this._localIdp.addIdentityAttribute(sdp);
return new this._win.RTCSessionDescription({ type: this._localType,
sdp: sdp });
},
+117
View File
@@ -0,0 +1,117 @@
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "StreamTracks.h"
#include "mozilla/Logging.h"
#include <algorithm>
namespace mozilla {
extern LazyLogModule gMediaStreamGraphLog;
#define STREAM_LOG(type, msg) MOZ_LOG(gMediaStreamGraphLog, type, msg)
#ifdef DEBUG
void
StreamTracks::DumpTrackInfo() const
{
STREAM_LOG(LogLevel::Info, ("DumpTracks: mTracksKnownTime %lld", mTracksKnownTime));
for (uint32_t i = 0; i < mTracks.Length(); ++i) {
Track* track = mTracks[i];
if (track->IsEnded()) {
STREAM_LOG(LogLevel::Info, ("Track[%d] %d: ended", i, track->GetID()));
} else {
STREAM_LOG(LogLevel::Info, ("Track[%d] %d: %lld", i, track->GetID(),
track->GetEnd()));
}
}
}
#endif
StreamTime
StreamTracks::GetEnd() const
{
StreamTime t = mTracksKnownTime;
for (uint32_t i = 0; i < mTracks.Length(); ++i) {
Track* track = mTracks[i];
if (!track->IsEnded()) {
t = std::min(t, track->GetEnd());
}
}
return t;
}
StreamTime
StreamTracks::GetAllTracksEnd() const
{
if (mTracksKnownTime < STREAM_TIME_MAX) {
// A track might be added.
return STREAM_TIME_MAX;
}
StreamTime t = 0;
for (uint32_t i = 0; i < mTracks.Length(); ++i) {
Track* track = mTracks[i];
if (!track->IsEnded()) {
return STREAM_TIME_MAX;
}
t = std::max(t, track->GetEnd());
}
return t;
}
StreamTracks::Track*
StreamTracks::FindTrack(TrackID aID)
{
if (aID == TRACK_NONE || mTracks.IsEmpty()) {
return nullptr;
}
// The tracks are sorted by ID. We can use a binary search.
uint32_t left = 0, right = mTracks.Length() - 1;
while (left <= right) {
uint32_t middle = (left + right) / 2;
if (mTracks[middle]->GetID() == aID) {
return mTracks[middle];
}
if (mTracks[middle]->GetID() > aID) {
if (middle == 0) {
break;
}
right = middle - 1;
} else {
left = middle + 1;
}
}
return nullptr;
}
void
StreamTracks::ForgetUpTo(StreamTime aTime)
{
// Only prune if there is a reasonable chunk (50ms @ 48kHz) to forget, so we
// don't spend too much time pruning segments.
const StreamTime minChunkSize = 2400;
if (aTime < mForgottenTime + minChunkSize) {
return;
}
mForgottenTime = aTime;
for (uint32_t i = 0; i < mTracks.Length(); ++i) {
Track* track = mTracks[i];
if (track->IsEnded() && track->GetEnd() <= aTime) {
mTracks.RemoveElementAt(i);
mTracksDirty = true;
--i;
continue;
}
StreamTime forgetTo = std::min(track->GetEnd() - 1, aTime);
track->ForgetUpTo(forgetTo);
}
}
} // namespace mozilla
+343
View File
@@ -0,0 +1,343 @@
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
#ifndef MOZILLA_STREAMTRACKS_H_
#define MOZILLA_STREAMTRACKS_H_
#include "MediaSegment.h"
#include "nsAutoPtr.h"
namespace mozilla {
/**
* Unique ID for track within a StreamTracks. Tracks from different
* StreamTrackss may have the same ID; this matters when appending StreamTrackss,
* since tracks with the same ID are matched. Only IDs greater than 0 are allowed.
*/
typedef int32_t TrackID;
const TrackID TRACK_NONE = 0;
const TrackID TRACK_INVALID = -1;
const TrackID TRACK_ANY = -2;
inline bool IsTrackIDExplicit(const TrackID& aId) {
return aId > TRACK_NONE;
}
inline TrackTicks RateConvertTicksRoundDown(TrackRate aOutRate,
TrackRate aInRate,
TrackTicks aTicks)
{
NS_ASSERTION(0 < aOutRate && aOutRate <= TRACK_RATE_MAX, "Bad out rate");
NS_ASSERTION(0 < aInRate && aInRate <= TRACK_RATE_MAX, "Bad in rate");
NS_WARN_IF_FALSE(0 <= aTicks && aTicks <= TRACK_TICKS_MAX, "Bad ticks"); // bug 957691
return (aTicks * aOutRate) / aInRate;
}
inline TrackTicks RateConvertTicksRoundUp(TrackRate aOutRate,
TrackRate aInRate, TrackTicks aTicks)
{
NS_ASSERTION(0 < aOutRate && aOutRate <= TRACK_RATE_MAX, "Bad out rate");
NS_ASSERTION(0 < aInRate && aInRate <= TRACK_RATE_MAX, "Bad in rate");
NS_ASSERTION(0 <= aTicks && aTicks <= TRACK_TICKS_MAX, "Bad ticks");
return (aTicks * aOutRate + aInRate - 1) / aInRate;
}
/**
* This object contains the decoded data for a stream's tracks.
* A StreamTracks can be appended to. Logically a StreamTracks only gets longer,
* but we also have the ability to "forget" data before a certain time that
* we know won't be used again. (We prune a whole number of seconds internally.)
*
* StreamTrackss should only be used from one thread at a time.
*
* A StreamTracks has a set of tracks that can be of arbitrary types ---
* the data for each track is a MediaSegment. The set of tracks can vary
* over the timeline of the StreamTracks.
*/
class StreamTracks
{
public:
/**
* Every track has a start time --- when it started in the StreamTracks.
* It has an end flag; when false, no end point is known; when true,
* the track ends when the data we have for the track runs out.
* Tracks have a unique ID assigned at creation. This allows us to identify
* the same track across StreamTrackss. A StreamTracks should never have
* two tracks with the same ID (even if they don't overlap in time).
* TODO Tracks can also be enabled and disabled over time.
* Takes ownership of aSegment.
*/
class Track final
{
Track(TrackID aID, StreamTime aStart, MediaSegment* aSegment)
: mStart(aStart),
mSegment(aSegment),
mID(aID),
mEnded(false)
{
MOZ_COUNT_CTOR(Track);
NS_ASSERTION(aID > TRACK_NONE, "Bad track ID");
NS_ASSERTION(0 <= aStart && aStart <= aSegment->GetDuration(), "Bad start position");
}
public:
~Track()
{
MOZ_COUNT_DTOR(Track);
}
template <class T> T* Get() const
{
if (mSegment->GetType() == T::StaticType()) {
return static_cast<T*>(mSegment.get());
}
return nullptr;
}
MediaSegment* GetSegment() const { return mSegment; }
TrackID GetID() const { return mID; }
bool IsEnded() const { return mEnded; }
StreamTime GetStart() const { return mStart; }
StreamTime GetEnd() const { return mSegment->GetDuration(); }
MediaSegment::Type GetType() const { return mSegment->GetType(); }
void SetEnded() { mEnded = true; }
void AppendFrom(Track* aTrack)
{
NS_ASSERTION(!mEnded, "Can't append to ended track");
NS_ASSERTION(aTrack->mID == mID, "IDs must match");
NS_ASSERTION(aTrack->mStart == 0, "Source track must start at zero");
NS_ASSERTION(aTrack->mSegment->GetType() == GetType(), "Track types must match");
mSegment->AppendFrom(aTrack->mSegment);
mEnded = aTrack->mEnded;
}
MediaSegment* RemoveSegment()
{
return mSegment.forget();
}
void ForgetUpTo(StreamTime aTime)
{
mSegment->ForgetUpTo(aTime);
}
void FlushAfter(StreamTime aNewEnd)
{
// Forget everything after a given endpoint
// a specified amount
mSegment->FlushAfter(aNewEnd);
}
size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const
{
size_t amount = aMallocSizeOf(this);
if (mSegment) {
amount += mSegment->SizeOfIncludingThis(aMallocSizeOf);
}
return amount;
}
private:
friend class StreamTracks;
// Start offset is in ticks at rate mRate
StreamTime mStart;
// The segment data starts at the start of the owning StreamTracks, i.e.,
// there's mStart silence/no video at the beginning.
nsAutoPtr<MediaSegment> mSegment;
// Unique ID
TrackID mID;
// True when the track ends with the data in mSegment
bool mEnded;
};
class MOZ_STACK_CLASS CompareTracksByID final
{
public:
bool Equals(Track* aA, Track* aB) const {
return aA->GetID() == aB->GetID();
}
bool LessThan(Track* aA, Track* aB) const {
return aA->GetID() < aB->GetID();
}
};
StreamTracks()
: mGraphRate(0)
, mTracksKnownTime(0)
, mForgottenTime(0)
, mTracksDirty(false)
#ifdef DEBUG
, mGraphRateIsSet(false)
#endif
{
MOZ_COUNT_CTOR(StreamTracks);
}
~StreamTracks()
{
MOZ_COUNT_DTOR(StreamTracks);
}
size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const
{
size_t amount = 0;
amount += mTracks.ShallowSizeOfExcludingThis(aMallocSizeOf);
for (size_t i = 0; i < mTracks.Length(); i++) {
amount += mTracks[i]->SizeOfIncludingThis(aMallocSizeOf);
}
return amount;
}
/**
* Initialize the graph rate for use in calculating StreamTimes from track
* ticks. Called when a MediaStream's graph pointer is initialized.
*/
void InitGraphRate(TrackRate aGraphRate)
{
mGraphRate = aGraphRate;
#if DEBUG
MOZ_ASSERT(!mGraphRateIsSet);
mGraphRateIsSet = true;
#endif
}
TrackRate GraphRate() const
{
MOZ_ASSERT(mGraphRateIsSet);
return mGraphRate;
}
/**
* Takes ownership of aSegment. Don't do this while iterating, or while
* holding a Track reference.
* aSegment must have aStart worth of null data.
*/
Track& AddTrack(TrackID aID, StreamTime aStart, MediaSegment* aSegment)
{
NS_ASSERTION(!FindTrack(aID), "Track with this ID already exists");
Track* track = new Track(aID, aStart, aSegment);
mTracks.InsertElementSorted(track, CompareTracksByID());
mTracksDirty = true;
if (mTracksKnownTime == STREAM_TIME_MAX) {
// There exists code like
// http://mxr.mozilla.org/mozilla-central/source/media/webrtc/signaling/src/mediapipeline/MediaPipeline.cpp?rev=96b197deb91e&mark=1292-1297#1292
NS_WARNING("Adding track to StreamTracks that should have no more tracks");
} else {
NS_ASSERTION(mTracksKnownTime <= aStart, "Start time too early");
}
return *track;
}
void AdvanceKnownTracksTime(StreamTime aKnownTime)
{
NS_ASSERTION(aKnownTime >= mTracksKnownTime, "Can't move tracks-known time earlier");
mTracksKnownTime = aKnownTime;
}
/**
* The end time for the StreamTracks is the latest time for which we have
* data for all tracks that haven't ended by that time.
*/
StreamTime GetEnd() const;
/**
* Returns the earliest time >= 0 at which all tracks have ended
* and all their data has been played out and no new tracks can be added,
* or STREAM_TIME_MAX if there is no such time.
*/
StreamTime GetAllTracksEnd() const;
#ifdef DEBUG
void DumpTrackInfo() const;
#endif
Track* FindTrack(TrackID aID);
class MOZ_STACK_CLASS TrackIter final
{
public:
/**
* Iterate through the tracks of aBuffer in order of ID.
*/
explicit TrackIter(const StreamTracks& aBuffer) :
mBuffer(&aBuffer.mTracks), mIndex(0), mMatchType(false) {}
/**
* Iterate through the tracks of aBuffer with type aType, in order of ID.
*/
TrackIter(const StreamTracks& aBuffer, MediaSegment::Type aType) :
mBuffer(&aBuffer.mTracks), mIndex(0), mType(aType), mMatchType(true) { FindMatch(); }
bool IsEnded() { return mIndex >= mBuffer->Length(); }
void Next()
{
++mIndex;
FindMatch();
}
Track* get() { return mBuffer->ElementAt(mIndex); }
Track& operator*() { return *mBuffer->ElementAt(mIndex); }
Track* operator->() { return mBuffer->ElementAt(mIndex); }
private:
void FindMatch()
{
if (!mMatchType)
return;
while (mIndex < mBuffer->Length() &&
mBuffer->ElementAt(mIndex)->GetType() != mType) {
++mIndex;
}
}
const nsTArray<nsAutoPtr<Track> >* mBuffer;
uint32_t mIndex;
MediaSegment::Type mType;
bool mMatchType;
};
friend class TrackIter;
/**
* Forget stream data before aTime; they will no longer be needed.
* Also can forget entire tracks that have ended at or before aTime.
* Can't be used to forget beyond GetEnd().
*/
void ForgetUpTo(StreamTime aTime);
/**
* Returns the latest time passed to ForgetUpTo.
*/
StreamTime GetForgottenDuration()
{
return mForgottenTime;
}
bool GetAndResetTracksDirty()
{
if (!mTracksDirty) {
return false;
}
mTracksDirty = false;
return true;
}
protected:
TrackRate mGraphRate; // StreamTime per second
// Any new tracks added will start at or after this time. In other words, the track
// list is complete and correct for all times less than this time.
StreamTime mTracksKnownTime;
StreamTime mForgottenTime;
private:
// All known tracks for this StreamTracks
nsTArray<nsAutoPtr<Track>> mTracks;
bool mTracksDirty;
#ifdef DEBUG
bool mGraphRateIsSet;
#endif
};
} // namespace mozilla
#endif /* MOZILLA_STREAMTRACKS_H_ */
+10 -10
View File
@@ -87,14 +87,14 @@ TrackUnionStream::TrackUnionStream(DOMMediaStream* aWrapper) :
allHaveCurrentData = false;
}
bool trackAdded = false;
for (StreamBuffer::TrackIter tracks(stream->GetStreamBuffer());
for (StreamTracks::TrackIter tracks(stream->GetStreamTracks());
!tracks.IsEnded(); tracks.Next()) {
bool found = false;
for (uint32_t j = 0; j < mTrackMap.Length(); ++j) {
TrackMapEntry* map = &mTrackMap[j];
if (map->mInputPort == mInputs[i] && map->mInputTrackID == tracks->GetID()) {
bool trackFinished;
StreamBuffer::Track* outputTrack = mBuffer.FindTrack(map->mOutputTrackID);
bool trackFinished = false;
StreamTracks::Track* outputTrack = mTracks.FindTrack(map->mOutputTrackID);
found = true;
if (!outputTrack || outputTrack->IsEnded() ||
!mInputs[i]->PassTrackThrough(tracks->GetID())) {
@@ -138,7 +138,7 @@ TrackUnionStream::TrackUnionStream(DOMMediaStream* aWrapper) :
// so we're finished now.
FinishOnGraphThread();
} else {
mBuffer.AdvanceKnownTracksTime(GraphTimeToStreamTimeWithBlocking(aTo));
mTracks.AdvanceKnownTracksTime(GraphTimeToStreamTimeWithBlocking(aTo));
}
if (allHaveCurrentData) {
// We can make progress if we're not blocked
@@ -146,7 +146,7 @@ TrackUnionStream::TrackUnionStream(DOMMediaStream* aWrapper) :
}
}
uint32_t TrackUnionStream::AddTrack(MediaInputPort* aPort, StreamBuffer::Track* aTrack,
uint32_t TrackUnionStream::AddTrack(MediaInputPort* aPort, StreamTracks::Track* aTrack,
GraphTime aFrom)
{
STREAM_LOG(LogLevel::Verbose, ("TrackUnionStream %p adding track %d for "
@@ -205,8 +205,8 @@ TrackUnionStream::TrackUnionStream(DOMMediaStream* aWrapper) :
aPort->GetSource(), aTrack->GetID());
}
segment->AppendNullData(outputStart);
StreamBuffer::Track* track =
&mBuffer.AddTrack(id, outputStart, segment.forget());
StreamTracks::Track* track =
&mTracks.AddTrack(id, outputStart, segment.forget());
STREAM_LOG(LogLevel::Debug, ("TrackUnionStream %p added track %d for input stream %p track %d, start ticks %lld",
this, track->GetID(), aPort->GetSource(), aTrack->GetID(),
(long long)outputStart));
@@ -246,7 +246,7 @@ TrackUnionStream::TrackUnionStream(DOMMediaStream* aWrapper) :
void TrackUnionStream::EndTrack(uint32_t aIndex)
{
StreamBuffer::Track* outputTrack = mBuffer.FindTrack(mTrackMap[aIndex].mOutputTrackID);
StreamTracks::Track* outputTrack = mTracks.FindTrack(mTrackMap[aIndex].mOutputTrackID);
if (!outputTrack || outputTrack->IsEnded())
return;
STREAM_LOG(LogLevel::Debug, ("TrackUnionStream %p ending track %d", this, outputTrack->GetID()));
@@ -269,12 +269,12 @@ TrackUnionStream::TrackUnionStream(DOMMediaStream* aWrapper) :
outputTrack->SetEnded();
}
void TrackUnionStream::CopyTrackData(StreamBuffer::Track* aInputTrack,
void TrackUnionStream::CopyTrackData(StreamTracks::Track* aInputTrack,
uint32_t aMapIndex, GraphTime aFrom, GraphTime aTo,
bool* aOutputTrackFinished)
{
TrackMapEntry* map = &mTrackMap[aMapIndex];
StreamBuffer::Track* outputTrack = mBuffer.FindTrack(map->mOutputTrackID);
StreamTracks::Track* outputTrack = mTracks.FindTrack(map->mOutputTrackID);
MOZ_ASSERT(outputTrack && !outputTrack->IsEnded(), "Can't copy to ended track");
MediaSegment* segment = map->mSegment;
+3 -3
View File
@@ -41,7 +41,7 @@ protected:
// We keep track IDs instead of track pointers because
// tracks can be removed without us being notified (e.g.
// when a finished track is forgotten.) When we need a Track*,
// we call StreamBuffer::FindTrack, which will return null if
// we call StreamTracks::FindTrack, which will return null if
// the track has been deleted.
TrackID mInputTrackID;
TrackID mOutputTrackID;
@@ -54,10 +54,10 @@ protected:
// Add the track to this stream, retaining its TrackID if it has never
// been previously used in this stream, allocating a new TrackID otherwise.
uint32_t AddTrack(MediaInputPort* aPort, StreamBuffer::Track* aTrack,
uint32_t AddTrack(MediaInputPort* aPort, StreamTracks::Track* aTrack,
GraphTime aFrom);
void EndTrack(uint32_t aIndex);
void CopyTrackData(StreamBuffer::Track* aInputTrack,
void CopyTrackData(StreamTracks::Track* aInputTrack,
uint32_t aMapIndex, GraphTime aFrom, GraphTime aTo,
bool* aOutputTrackFinished);
+1 -1
View File
@@ -10,7 +10,7 @@
#include "AudioSegment.h"
#include "EncodedFrameContainer.h"
#include "StreamBuffer.h"
#include "StreamTracks.h"
#include "TrackMetadataBase.h"
#include "VideoSegment.h"
#include "MediaStreamGraph.h"
+2 -2
View File
@@ -138,7 +138,7 @@ EXPORTS += [
'SeekTask.h',
'SelfRef.h',
'SharedBuffer.h',
'StreamBuffer.h',
'StreamTracks.h',
'ThreadPoolCOMListener.h',
'TimeUnits.h',
'TrackUnionStream.h',
@@ -243,7 +243,7 @@ UNIFIED_SOURCES += [
'RtspMediaResource.cpp',
'SeekJob.cpp',
'SeekTask.cpp',
'StreamBuffer.cpp',
'StreamTracks.cpp',
'TextTrack.cpp',
'TextTrackCue.cpp',
'TextTrackCueList.cpp',
@@ -8,7 +8,7 @@
<script type="application/javascript">
createHTML({
bug: "1087551",
title: "addCandidate behavior in different states"
title: "addIceCandidate behavior (local and remote) including invalid data"
});
var test;
@@ -71,6 +71,28 @@
sdpMLineIndex: 0});
return test.pcRemote._pc.addIceCandidate(candidate)
.then(ok(true, "Successfully added valid ICE candidate"));
},
// bug 1095793
function PC_REMOTE_ADD_MISMATCHED_MID_AND_LEVEL_CANDIDATE(test) {
var bogus = new mozRTCIceCandidate(
{candidate:"candidate:1 1 UDP 2130706431 192.168.2.1 50005 typ host",
sdpMLineIndex: 0,
sdpMid: "sdparta_1"});
return test.pcRemote._pc.addIceCandidate(bogus)
.then(
generateErrorCallback("addIceCandidate should have failed."),
err => {
is(err.name, "InvalidCandidateError", "Error is InvalidCandidateError");
}
);
},
function PC_REMOTE_ADD_MATCHING_MID_AND_LEVEL_CANDIDATE(test) {
var candidate = new mozRTCIceCandidate(
{candidate:"candidate:1 1 UDP 2130706431 192.168.2.1 50005 typ host",
sdpMLineIndex: 0,
sdpMid: "sdparta_0"});
return test.pcRemote._pc.addIceCandidate(candidate)
.then(ok(true, "Successfully added valid ICE candidate with matching mid and level"));
}
]);
test.run();

Some files were not shown because too many files have changed in this diff Show More