Merge fx-team to m-c. a=merge

This commit is contained in:
Ryan VanderMeulen 2014-12-19 21:54:11 -05:00
commit 5826c0d2fe
32 changed files with 1259 additions and 158 deletions

View File

@ -423,9 +423,6 @@ SocialFlyout = {
iframe.removeEventListener("load", documentLoaded, true);
cb();
}, true);
// Force a layout flush by calling .clientTop so
// that the docShell of this frame is created
iframe.clientTop;
Social.setErrorListener(iframe, SocialFlyout.setFlyoutErrorMessage.bind(SocialFlyout))
iframe.setAttribute("src", aURL);
} else {
@ -1299,11 +1296,11 @@ SocialStatus = {
}
},
_onclose: function() {
let notificationFrameId = "social-status-" + origin;
let frame = document.getElementById(notificationFrameId);
_onclose: function(frame) {
frame.removeEventListener("close", this._onclose, true);
frame.removeEventListener("click", this._onclick, true);
if (frame.socialErrorListener)
frame.socialErrorListener.remove();
},
_onclick: function() {
@ -1318,8 +1315,9 @@ SocialStatus = {
PanelFrame.showPopup(window, aToolbarButton, "social", origin,
provider.statusURL, provider.getPageSize("status"),
(frame) => {
frame.addEventListener("close", this._onclose, true);
frame.addEventListener("close", () => { SocialStatus._onclose(frame) }, true);
frame.addEventListener("click", this._onclick, true);
Social.setErrorListener(frame, this.setPanelErrorMessage.bind(this));
});
Services.telemetry.getHistogramById("SOCIAL_TOOLBAR_BUTTONS").add(1);
},

View File

@ -204,18 +204,40 @@
});
}
}.bind(this);
contentWindow.addEventListener("socialMarkUpdate", markUpdate);
contentWindow.addEventListener("unload", function unload() {
let unload = () => {
contentWindow.removeEventListener("unload", unload);
contentWindow.removeEventListener("socialMarkUpdate", markUpdate);
});
if (this.content.socialErrorListener)
this.content.socialErrorListener.remove();
}
contentWindow.addEventListener("socialMarkUpdate", markUpdate);
contentWindow.addEventListener("unload", unload);
}
this.content.addEventListener("DOMContentLoaded", DOMContentLoaded, true);
Social.setErrorListener(this.content, this.setErrorMessage.bind(this));
this._loading = true;
this.content.setAttribute("src", endpoint);
]]></body>
</method>
<method name="setErrorMessage">
<parameter name="aNotificationFrame"/>
<body><![CDATA[
if (!aNotificationFrame)
return;
let src = aNotificationFrame.getAttribute("src");
aNotificationFrame.removeAttribute("src");
let origin = aNotificationFrame.getAttribute("origin");
aNotificationFrame.webNavigation.loadURI("about:socialerror?mode=tryAgainOnly&url=" +
encodeURIComponent(src) + "&origin=" +
encodeURIComponent(origin),
null, null, null, null);
// ensure the panel is open if error occurs on first click
this.openPanel();
]]></body>
</method>
<method name="openPanel">
<parameter name="aResetOnClose"/>
<body><![CDATA[

View File

@ -11,47 +11,6 @@ function gc() {
let openChatWindow = Cu.import("resource://gre/modules/MozSocialAPI.jsm", {}).openChatWindow;
// Support for going on and offline.
// (via browser/base/content/test/browser_bookmark_titles.js)
let origProxyType = Services.prefs.getIntPref('network.proxy.type');
function toggleOfflineStatus(goOffline) {
// Bug 968887 fix. when going on/offline, wait for notification before continuing
let deferred = Promise.defer();
if (!goOffline) {
Services.prefs.setIntPref('network.proxy.type', origProxyType);
}
if (goOffline != Services.io.offline) {
info("initial offline state " + Services.io.offline);
let expect = !Services.io.offline;
Services.obs.addObserver(function offlineChange(subject, topic, data) {
Services.obs.removeObserver(offlineChange, "network:offline-status-changed");
info("offline state changed to " + Services.io.offline);
is(expect, Services.io.offline, "network:offline-status-changed successful toggle");
deferred.resolve();
}, "network:offline-status-changed", false);
BrowserOffline.toggleOfflineStatus();
} else {
deferred.resolve();
}
if (goOffline) {
Services.prefs.setIntPref('network.proxy.type', 0);
// LOAD_FLAGS_BYPASS_CACHE isn't good enough. So clear the cache.
Services.cache2.clear();
}
return deferred.promise;
}
function goOffline() {
// Simulate a network outage with offline mode. (Localhost is still
// accessible in offline mode, so disable the test proxy as well.)
return toggleOfflineStatus(true);
}
function goOnline(callback) {
return toggleOfflineStatus(false);
}
function openPanel(url, panelCallback, loadCallback) {
// open a flyout
SocialFlyout.open(url, 0, panelCallback);

View File

@ -87,8 +87,7 @@ var tests = {
// we expect the addon install dialog to appear, we need to accept the
// install from the dialog.
let panel = document.getElementById("servicesInstall-notification");
PopupNotifications.panel.addEventListener("popupshown", function onpopupshown() {
PopupNotifications.panel.removeEventListener("popupshown", onpopupshown);
ensureEventFired(PopupNotifications.panel, "popupshown").then(() => {
info("servicesInstall-notification panel opened");
panel.button.click();
});
@ -121,8 +120,7 @@ var tests = {
testButtonOnEnable: function(next) {
let panel = document.getElementById("servicesInstall-notification");
PopupNotifications.panel.addEventListener("popupshown", function onpopupshown() {
PopupNotifications.panel.removeEventListener("popupshown", onpopupshown);
ensureEventFired(PopupNotifications.panel, "popupshown").then(() => {
info("servicesInstall-notification panel opened");
panel.button.click();
});
@ -187,8 +185,8 @@ var tests = {
// synthesize so the command event happens
EventUtils.synthesizeMouseAtCenter(btn, {});
// wait for the button to be marked, click to open panel
is(btn.panel.state, "closed", "panel should not be visible yet");
waitForCondition(function() btn.isMarked, function() {
is(btn.panel.state, "closed", "panel should not be visible yet");
EventUtils.synthesizeMouseAtCenter(btn, {});
}, "button is marked");
break;
@ -196,23 +194,21 @@ var tests = {
ok(true, "got the panel message " + e.data.result);
if (e.data.result == "shown") {
// unmark the page via the button in the page
let doc = btn.contentDocument;
let unmarkBtn = doc.getElementById("unmark");
ok(unmarkBtn, "got the panel unmark button");
EventUtils.sendMouseEvent({type: "click"}, unmarkBtn, btn.contentWindow);
ensureFrameLoaded(btn.content).then(() => {
let doc = btn.contentDocument;
let unmarkBtn = doc.getElementById("unmark");
ok(unmarkBtn, "testMarkPanel - got the panel unmark button");
EventUtils.sendMouseEvent({type: "click"}, unmarkBtn, btn.contentWindow);
});
} else {
// page should no longer be marked
port.close();
waitForCondition(function() !btn.isMarked, function() {
// cleanup after the page has been unmarked
gBrowser.tabContainer.addEventListener("TabClose", function onTabClose() {
gBrowser.tabContainer.removeEventListener("TabClose", onTabClose);
executeSoon(function () {
ok(btn.disabled, "button is disabled");
next();
});
ensureBrowserTabClosed(tab).then(() => {
ok(btn.disabled, "button is disabled");
next();
});
gBrowser.removeTab(tab);
}, "button unmarked");
}
break;
@ -222,6 +218,39 @@ var tests = {
});
},
testMarkPanelOffline: function(next) {
// click on panel to open and wait for visibility
let provider = Social._getProviderFromOrigin(manifest2.origin);
ok(provider.enabled, "provider is enabled");
let id = SocialMarks._toolbarHelper.idFromOrigin(manifest2.origin);
let widget = CustomizableUI.getWidget(id);
let btn = widget.forWindow(window).node;
ok(btn, "got a mark button");
// verify markbutton is disabled when there is no browser url
ok(btn.disabled, "button is disabled");
let activationURL = manifest2.origin + "/browser/browser/base/content/test/social/social_activate.html"
addTab(activationURL, function(tab) {
ok(!btn.disabled, "button is enabled");
goOffline().then(function() {
info("testing offline error page");
// wait for popupshown
ensureEventFired(btn.panel, "popupshown").then(() => {
info("marks panel is open");
ensureFrameLoaded(btn.content).then(() => {
is(btn.contentDocument.location.href.indexOf("about:socialerror?"), 0, "social error page is showing");
// cleanup after the page has been unmarked
ensureBrowserTabClosed(tab).then(() => {
ok(btn.disabled, "button is disabled");
goOnline().then(next);
});
});
});
btn.markCurrentPage();
});
});
},
testMarkPanelLoggedOut: function(next) {
// click on panel to open and wait for visibility
let provider = Social._getProviderFromOrigin(manifest2.origin);
@ -259,23 +288,21 @@ var tests = {
// our test marks the page during the load event (see
// social_mark.html) regardless of login state, unmark the page
// via the button in the page
let doc = btn.contentDocument;
let unmarkBtn = doc.getElementById("unmark");
ok(unmarkBtn, "got the panel unmark button");
EventUtils.sendMouseEvent({type: "click"}, unmarkBtn, btn.contentWindow);
ensureFrameLoaded(btn.content).then(() => {
let doc = btn.contentDocument;
let unmarkBtn = doc.getElementById("unmark");
ok(unmarkBtn, "testMarkPanelLoggedOut - got the panel unmark button");
EventUtils.sendMouseEvent({type: "click"}, unmarkBtn, btn.contentWindow);
});
} else {
// page should no longer be marked
port.close();
waitForCondition(function() !btn.isMarked, function() {
// cleanup after the page has been unmarked
gBrowser.tabContainer.addEventListener("TabClose", function onTabClose() {
gBrowser.tabContainer.removeEventListener("TabClose", onTabClose);
executeSoon(function () {
ok(btn.disabled, "button is disabled");
next();
});
ensureBrowserTabClosed(tab).then(() => {
ok(btn.disabled, "button is disabled");
next();
});
gBrowser.removeTab(tab);
}, "button unmarked");
}
break;
@ -323,11 +350,10 @@ var tests = {
}
info("INSTALLING " + manifest.origin);
let panel = document.getElementById("servicesInstall-notification");
PopupNotifications.panel.addEventListener("popupshown", function onpopupshown() {
PopupNotifications.panel.removeEventListener("popupshown", onpopupshown);
ensureEventFired(PopupNotifications.panel, "popupshown").then(() => {
info("servicesInstall-notification panel opened");
panel.button.click();
})
});
let activationURL = manifest.origin + "/browser/browser/base/content/test/social/social_activate.html"
let id = SocialMarks._toolbarHelper.idFromOrigin(manifest.origin);

View File

@ -57,8 +57,7 @@ var tests = {
// we expect the addon install dialog to appear, we need to accept the
// install from the dialog.
let panel = document.getElementById("servicesInstall-notification");
PopupNotifications.panel.addEventListener("popupshown", function onpopupshown() {
PopupNotifications.panel.removeEventListener("popupshown", onpopupshown);
ensureEventFired(PopupNotifications.panel, "popupshown").then(() => {
info("servicesInstall-notification panel opened");
panel.button.click();
})
@ -89,8 +88,7 @@ var tests = {
},
testButtonOnEnable: function(next) {
let panel = document.getElementById("servicesInstall-notification");
PopupNotifications.panel.addEventListener("popupshown", function onpopupshown() {
PopupNotifications.panel.removeEventListener("popupshown", onpopupshown);
ensureEventFired(PopupNotifications.panel, "popupshown").then(() => {
info("servicesInstall-notification panel opened");
panel.button.click();
});
@ -165,6 +163,39 @@ var tests = {
};
port.postMessage({topic: "test-init"});
},
testPanelOffline: function(next) {
// click on panel to open and wait for visibility
let provider = Social._getProviderFromOrigin(manifest2.origin);
ok(provider.enabled, "provider is enabled");
let id = SocialStatus._toolbarHelper.idFromOrigin(manifest2.origin);
let widget = CustomizableUI.getWidget(id);
let btn = widget.forWindow(window).node;
ok(btn, "got a status button");
let frameId = btn.getAttribute("notificationFrameId");
let frame = document.getElementById(frameId);
let port = provider.getWorkerPort();
port.postMessage({topic: "test-init"});
goOffline().then(function() {
info("testing offline error page");
// wait for popupshown
let panel = document.getElementById("social-notification-panel");
ensureEventFired(panel, "popupshown").then(() => {
ensureFrameLoaded(frame).then(() => {
is(frame.contentDocument.location.href.indexOf("about:socialerror?"), 0, "social error page is showing "+frame.contentDocument.location.href);
panel.hidePopup();
goOnline().then(next);
});
});
// reload after going offline, wait for unload to open panel
ensureEventFired(frame, "unload").then(() => {
btn.click();
});
frame.contentDocument.location.reload();
});
},
testButtonOnDisable: function(next) {
// enable the provider now
let provider = Social._getProviderFromOrigin(manifest2.origin);

View File

@ -395,6 +395,15 @@ function selectBrowserTab(tab, callback) {
gBrowser.selectedTab = tab;
}
function ensureEventFired(elem, event) {
let deferred = Promise.defer();
elem.addEventListener(event, function handler() {
elem.removeEventListener(event, handler, true);
deferred.resolve()
}, true);
return deferred.promise;
}
function loadIntoTab(tab, url, callback) {
tab.linkedBrowser.addEventListener("load", function tabLoad(event) {
tab.linkedBrowser.removeEventListener("load", tabLoad, true);
@ -403,6 +412,24 @@ function loadIntoTab(tab, url, callback) {
tab.linkedBrowser.loadURI(url);
}
function ensureBrowserTabClosed(tab) {
let promise = ensureEventFired(gBrowser.tabContainer, "TabClose");
gBrowser.removeTab(tab);
return promise;
}
function ensureFrameLoaded(frame) {
let deferred = Promise.defer();
if (frame.contentDocument && frame.contentDocument.readyState == "complete") {
deferred.resolve();
} else {
frame.addEventListener("load", function handler() {
frame.removeEventListener("load", handler, true);
deferred.resolve()
}, true);
}
return deferred.promise;
}
// chat test help functions
@ -593,3 +620,45 @@ function closeAllChats() {
chatbar.selectedChat.close();
}
}
// Support for going on and offline.
// (via browser/base/content/test/browser_bookmark_titles.js)
let origProxyType = Services.prefs.getIntPref('network.proxy.type');
function toggleOfflineStatus(goOffline) {
// Bug 968887 fix. when going on/offline, wait for notification before continuing
let deferred = Promise.defer();
if (!goOffline) {
Services.prefs.setIntPref('network.proxy.type', origProxyType);
}
if (goOffline != Services.io.offline) {
info("initial offline state " + Services.io.offline);
let expect = !Services.io.offline;
Services.obs.addObserver(function offlineChange(subject, topic, data) {
Services.obs.removeObserver(offlineChange, "network:offline-status-changed");
info("offline state changed to " + Services.io.offline);
is(expect, Services.io.offline, "network:offline-status-changed successful toggle");
deferred.resolve();
}, "network:offline-status-changed", false);
BrowserOffline.toggleOfflineStatus();
} else {
deferred.resolve();
}
if (goOffline) {
Services.prefs.setIntPref('network.proxy.type', 0);
// LOAD_FLAGS_BYPASS_CACHE isn't good enough. So clear the cache.
Services.cache2.clear();
}
return deferred.promise;
}
function goOffline() {
// Simulate a network outage with offline mode. (Localhost is still
// accessible in offline mode, so disable the test proxy as well.)
return toggleOfflineStatus(true);
}
function goOnline(callback) {
return toggleOfflineStatus(false);
}

View File

@ -1105,6 +1105,10 @@ this.MozLoopService = {
let isOwnerInRoom = false;
let isOtherInRoom = false;
if (!this.getLoopPref("gettingStarted.resumeOnFirstJoin")) {
return;
}
if (!room.participants) {
return;
}
@ -1524,6 +1528,10 @@ this.MozLoopService = {
},
resumeTour: function(aIncomingConversationState) {
if (!this.getLoopPref("gettingStarted.resumeOnFirstJoin")) {
return;
}
let url = this.getTourURL("resume-with-conversation", {
incomingConversation: aIncomingConversationState,
});

View File

@ -969,3 +969,29 @@ html, .fx-embedded, #main,
display: none;
}
}
.self-view-hidden-message {
/* Not displayed by default; display is turned on elsewhere when the
* self-view is actually hidden.
*/
display: none;
}
/* Avoid the privacy problem where a user can size the window so small that
* part of the self view is not shown. If the self view isn't completely
* displayable...
*/
@media screen and (max-height:160px) {
/* disable the self view */
.standalone .OT_publisher {
display: none;
}
/* and enable a message telling the user how to get it back */
.standalone .self-view-hidden-message {
display: inline;
position: relative;
top: 90px;
}
}

View File

@ -367,6 +367,9 @@ loop.standaloneRoomViews = (function(mozL10n) {
React.DOM.div({className: "conversation room-conversation"},
React.DOM.h2({className: "room-name"}, this.state.roomName),
React.DOM.div({className: "media nested"},
React.DOM.span({className: "self-view-hidden-message"},
mozL10n.get("self_view_hidden_message")
),
React.DOM.div({className: "video_wrapper remote_wrapper"},
React.DOM.div({className: "video_inner remote"})
),

View File

@ -367,6 +367,9 @@ loop.standaloneRoomViews = (function(mozL10n) {
<div className="conversation room-conversation">
<h2 className="room-name">{this.state.roomName}</h2>
<div className="media nested">
<span className="self-view-hidden-message">
{mozL10n.get("self_view_hidden_message")}
</span>
<div className="video_wrapper remote_wrapper">
<div className="video_inner remote"></div>
</div>

View File

@ -41,6 +41,8 @@ legal_text_and_links=By using {{clientShortname}} you agree to the {{terms_of_us
terms_of_use_link_text=Terms of use
privacy_notice_link_text=Privacy notice
invite_header_text=Invite someone to join you.
self_view_hidden_message=Self-view hidden but still being sent; resize window \
to show
## LOCALIZATION NOTE(brandShortname): This should not be localized and
## should remain "Firefox" for all locales.

View File

@ -327,6 +327,9 @@ function SocialErrorListener(iframe, errorHandler) {
this.setErrorMessage = errorHandler;
this.iframe = iframe;
iframe.socialErrorListener = this;
// Force a layout flush by calling .clientTop so that the docShell of this
// frame is created for the error listener
iframe.clientTop;
iframe.docShell.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebProgress)
.addProgressListener(this,

View File

@ -1318,7 +1318,10 @@ this.UITour = {
openMenuButton(menuBtn);
} else if (aMenuName == "loop") {
let toolbarButton = aWindow.LoopUI.toolbarButton;
if (!toolbarButton || !toolbarButton.node) {
// It's possible to have a node that isn't placed anywhere
if (!toolbarButton || !toolbarButton.node ||
!CustomizableUI.getPlacementOfWidget(toolbarButton.node.id)) {
log.debug("Can't show the Loop menu since the toolbarButton isn't placed");
return;
}

View File

@ -28,7 +28,6 @@
/* Tabs */
--tabs-toolbar-color: #F5F7FA;
--tab-background-color: #1C2126;
--tab-color: #ced3d9;
--tab-hover-background-color: #07090a;
--tab-separator-color: #474C50;
--tab-selection-color: #f5f7fa;
@ -88,10 +87,8 @@
--chrome-selection-color: #f5f7fa;
--chrome-selection-background-color: #4c9ed9;
--tab-color: #18191a;
--tab-background-color: #E3E4E6;
--tab-hover-background-color: #D7D8DA;
--tab-color: #18191a;
--tab-separator-color: #C6C6C7;
--tab-selection-color: #f5f7fa;
--tab-selection-background-color: #4c9ed9;
@ -283,7 +280,6 @@ window:not([chromehidden~="toolbar"]) #urlbar-wrapper {
/* We normally rely on other tab elements for pointer events, but this
theme hides those so we need it set here instead */
pointer-events: auto;
color: var(--tab-color);
background-color: var(--tab-background-color);
}
@ -298,7 +294,6 @@ window:not([chromehidden~="toolbar"]) #urlbar-wrapper {
.tabbrowser-arrowscrollbox > .scrollbutton-up:not([disabled]):hover,
.tabbrowser-tab:hover {
background-color: var(--tab-hover-background-color);
color: var(--tab-hover-color);
}
.tabbrowser-tab[selected] {

View File

@ -258,6 +258,171 @@ public class BrowserApp extends GeckoApp
private TilesRecorder mTilesRecorder;
private DragHelper mDragHelper;
private class DragHelper implements OuterLayout.DragCallback {
private int[] mToolbarLocation = new int[2]; // to avoid creation every time we need to check for toolbar location.
// When dragging horizontally, the area of mainlayout between left drag bound and right drag bound can
// be dragged. A touch on the right of that area will automatically close the view.
private int mStatusBarHeight;
public DragHelper() {
// If a layout round happens from the root, the offset placed by viewdraghelper gets forgotten and
// main layout gets replaced to offset 0.
((MainLayout) mMainLayout).setLayoutInterceptor(new LayoutInterceptor() {
@Override
public void onLayout() {
if (mRootLayout.isMoving()) {
mRootLayout.restoreTargetViewPosition();
}
}
});
}
@Override
public void onDragProgress(float progress) {
mBrowserToolbar.setToolBarButtonsAlpha(1.0f - progress);
mTabsPanel.translateInRange(progress);
}
@Override
public View getViewToDrag() {
return mMainLayout;
}
/**
* Since pressing the tabs button slides the main layout, whereas draghelper changes its offset, here we
* restore the position of mainlayout as if it was opened by pressing the button. This allows the closing
* mechanism to work.
*/
@Override
public void startDrag(boolean wasOpen) {
if (wasOpen) {
mTabsPanel.setHWLayerEnabled(true);
mMainLayout.offsetTopAndBottom(getDragRange());
mMainLayout.scrollTo(0, 0);
} else {
prepareTabsToShow();
mBrowserToolbar.hideVirtualKeyboard();
}
mBrowserToolbar.setContextMenuEnabled(false);
}
@Override
public void stopDrag(boolean stoppingToOpen) {
if (stoppingToOpen) {
mTabsPanel.setHWLayerEnabled(false);
mMainLayout.offsetTopAndBottom(-getDragRange());
mMainLayout.scrollTo(0, -getDragRange());
} else {
mTabsPanel.hideImmediately();
mTabsPanel.setHWLayerEnabled(false);
}
// Re-enabling context menu only while stopping to close.
if (stoppingToOpen) {
mBrowserToolbar.setContextMenuEnabled(false);
} else {
mBrowserToolbar.setContextMenuEnabled(true);
}
}
@Override
public int getDragRange() {
return mTabsPanel.getVerticalPanelHeight();
}
@Override
public int getOrderedChildIndex(int index) {
// See ViewDragHelper's findTopChildUnder method. ViewDragHelper looks for the topmost view in z order
// to understand what needs to be dragged. Here we are tampering Toast's index in case it's hidden,
// otherwise draghelper would try to drag it.
int mainLayoutIndex = mRootLayout.indexOfChild(mMainLayout);
if (index > mainLayoutIndex && (mToast == null || !mToast.isVisible())) {
return mainLayoutIndex;
} else {
return index;
}
}
@Override
public boolean canDrag(MotionEvent event) {
// if no current tab is active.
if (Tabs.getInstance().getSelectedTab() == null) {
return false;
}
// currently disabled for tablets.
if (HardwareUtils.isTablet()) {
return false;
}
// not enabled in editing mode.
if (mBrowserToolbar.isEditing()) {
return false;
}
return isInToolbarBounds((int) event.getRawY());
}
@Override
public boolean canInterceptEventWhileOpen(MotionEvent event) {
if (event.getActionMasked() != MotionEvent.ACTION_DOWN) {
return false;
}
// Need to check if are intercepting a touch on main layout since we might hit a visible toast.
if (mRootLayout.findTopChildUnder(event) == mMainLayout &&
isInToolbarBounds((int) event.getRawY())) {
return true;
}
return false;
}
private boolean isInToolbarBounds(int y) {
mBrowserToolbar.getLocationOnScreen(mToolbarLocation);
final int upperLimit = mToolbarLocation[1] + mBrowserToolbar.getMeasuredHeight();
final int lowerLimit = mToolbarLocation[1];
return (y > lowerLimit && y < upperLimit);
}
public void prepareTabsToShow() {
if (ensureTabsPanelExists()) {
// If we've just inflated the tabs panel, only show it once the current
// layout pass is done to avoid displayed temporary UI states during
// relayout.
final ViewTreeObserver vto = mTabsPanel.getViewTreeObserver();
if (vto.isAlive()) {
vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
mTabsPanel.getViewTreeObserver().removeGlobalOnLayoutListener(this);
prepareTabsToShow();
}
});
}
} else {
mTabsPanel.prepareToDrag();
}
}
public int getLowerLimit() {
return getStatusBarHeight();
}
private int getStatusBarHeight() {
if (mStatusBarHeight != 0) {
return mStatusBarHeight;
}
final int resourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
if (resourceId > 0) {
mStatusBarHeight = getResources().getDimensionPixelSize(resourceId);
return mStatusBarHeight;
}
Log.e(LOGTAG, "Unable to find statusbar height");
return 0;
}
}
@Override
public View onCreateView(final String name, final Context context, final AttributeSet attrs) {
final View view;
@ -648,6 +813,9 @@ public class BrowserApp extends GeckoApp
}
});
mDragHelper = new DragHelper();
mRootLayout.setDraggableCallback(mDragHelper);
// Set the maximum bits-per-pixel the favicon system cares about.
IconDirectoryEntry.setMaxBPP(GeckoAppShell.getScreenDepth());
@ -1364,6 +1532,7 @@ public class BrowserApp extends GeckoApp
invalidateOptionsMenu();
if (mTabsPanel != null) {
mRootLayout.reset();
updateSideBarState();
mTabsPanel.refresh();
}
@ -1387,6 +1556,10 @@ public class BrowserApp extends GeckoApp
});
}
private boolean isSideBar() {
return (HardwareUtils.isTablet() && getOrientation() == Configuration.ORIENTATION_LANDSCAPE);
}
private void updateSideBarState() {
if (NewTabletUI.isEnabled(this)) {
return;
@ -1395,7 +1568,7 @@ public class BrowserApp extends GeckoApp
if (mMainLayoutAnimator != null)
mMainLayoutAnimator.stop();
boolean isSideBar = (HardwareUtils.isTablet() && getOrientation() == Configuration.ORIENTATION_LANDSCAPE);
boolean isSideBar = isSideBar();
final int sidebarWidth = getResources().getDimensionPixelSize(R.dimen.tabs_sidebar_width);
ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) mTabsPanel.getLayoutParams();
@ -1407,6 +1580,7 @@ public class BrowserApp extends GeckoApp
mMainLayout.scrollTo(mainLayoutScrollX, 0);
mTabsPanel.setIsSideBar(isSideBar);
mRootLayout.updateDragHelperParameters();
}
@Override
@ -1717,7 +1891,7 @@ public class BrowserApp extends GeckoApp
@Override
public void onGlobalLayout() {
mTabsPanel.getViewTreeObserver().removeGlobalOnLayoutListener(this);
mTabsPanel.show(panel);
showTabs(panel);
}
});
}
@ -1806,10 +1980,13 @@ public class BrowserApp extends GeckoApp
if (!areTabsShown()) {
mTabsPanel.setVisibility(View.INVISIBLE);
mTabsPanel.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
mRootLayout.setClosed();
mBrowserToolbar.setContextMenuEnabled(true);
} else {
// Cancel editing mode to return to page content when the TabsPanel closes. We cancel
// it here because there are graphical glitches if it's canceled while it's visible.
mBrowserToolbar.cancelEdit();
mRootLayout.setOpen();
}
mTabsPanel.finishTabsAnimation();

View File

@ -160,8 +160,9 @@ public abstract class GeckoApp
// after a version upgrade.
private static final int CLEANUP_DEFERRAL_SECONDS = 15;
protected RelativeLayout mRootLayout;
protected OuterLayout mRootLayout;
protected RelativeLayout mMainLayout;
protected RelativeLayout mGeckoLayout;
private View mCameraView;
private OrientationEventListener mCameraOrientationEventListener;
@ -1277,7 +1278,7 @@ public abstract class GeckoApp
setContentView(getLayout());
// Set up Gecko layout.
mRootLayout = (RelativeLayout) findViewById(R.id.root_layout);
mRootLayout = (OuterLayout) findViewById(R.id.root_layout);
mGeckoLayout = (RelativeLayout) findViewById(R.id.gecko_layout);
mMainLayout = (RelativeLayout) findViewById(R.id.main_layout);
@ -2403,11 +2404,24 @@ public abstract class GeckoApp
public static class MainLayout extends RelativeLayout {
private TouchEventInterceptor mTouchEventInterceptor;
private MotionEventInterceptor mMotionEventInterceptor;
private LayoutInterceptor mLayoutInterceptor;
public MainLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (mLayoutInterceptor != null) {
mLayoutInterceptor.onLayout();
}
}
public void setLayoutInterceptor(LayoutInterceptor interceptor) {
mLayoutInterceptor = interceptor;
}
public void setTouchEventInterceptor(TouchEventInterceptor interceptor) {
mTouchEventInterceptor = interceptor;
}

View File

@ -0,0 +1,11 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* 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/. */
package org.mozilla.gecko;
public interface LayoutInterceptor {
public void onLayout();
}

View File

@ -0,0 +1,254 @@
/* 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/. */
package org.mozilla.gecko;
import android.content.Context;
import android.support.v4.view.ViewCompat;
import android.support.v4.widget.ViewDragHelper;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.widget.RelativeLayout;
/* Outerlayout is the container layout of all the main views. It allows mainlayout to be dragged while targeting
the toolbar and it's responsible for handling the dragprocess. It relies on ViewDragHelper to ease the drag process.
*/
public class OuterLayout extends RelativeLayout {
private final double AUTO_OPEN_SPEED_LIMIT = 800.0;
private ViewDragHelper mDragHelper;
private int mDraggingBorder;
private int mDragRange;
private boolean mIsOpen = false;
private int mDraggingState = ViewDragHelper.STATE_IDLE;
private DragCallback mDragCallback;
public static interface DragCallback {
public void startDrag(boolean wasOpen);
public void stopDrag(boolean stoppingToOpen);
public int getDragRange();
public int getOrderedChildIndex(int index);
public boolean canDrag(MotionEvent event);
public boolean canInterceptEventWhileOpen(MotionEvent event);
public void onDragProgress(float progress);
public View getViewToDrag();
public int getLowerLimit();
}
private class DragHelperCallback extends ViewDragHelper.Callback {
@Override
public void onViewDragStateChanged(int newState) {
if (newState == mDraggingState) { // no change
return;
}
// if the view stopped moving.
if ((mDraggingState == ViewDragHelper.STATE_DRAGGING || mDraggingState == ViewDragHelper.STATE_SETTLING) &&
newState == ViewDragHelper.STATE_IDLE) {
final float rangeToCheck = mDragRange;
final float lowerLimit = mDragCallback.getLowerLimit();
if (mDraggingBorder == lowerLimit) {
mIsOpen = false;
mDragCallback.onDragProgress(0);
} else if (mDraggingBorder == rangeToCheck) {
mIsOpen = true;
mDragCallback.onDragProgress(1);
}
mDragCallback.stopDrag(mIsOpen);
}
// The view was previuosly moving.
if (newState == ViewDragHelper.STATE_DRAGGING && !isMoving()) {
mDragCallback.startDrag(mIsOpen);
updateRanges();
}
mDraggingState = newState;
}
@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
mDraggingBorder = top;
final float progress = Math.min(1, ((float) top) / mDragRange);
mDragCallback.onDragProgress(progress);
}
@Override
public int getViewVerticalDragRange(View child) {
return mDragRange;
}
@Override
public int getOrderedChildIndex(int index) {
return mDragCallback.getOrderedChildIndex(index);
}
@Override
public boolean tryCaptureView(View view, int i) {
return (view.getId() == mDragCallback.getViewToDrag().getId());
}
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
return top;
}
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
final float rangeToCheck = mDragRange;
final float speedToCheck = yvel;
if (mDraggingBorder == mDragCallback.getLowerLimit()) {
return;
}
if (mDraggingBorder == rangeToCheck) {
return;
}
boolean settleToOpen = false;
// Speed has priority over position.
if (speedToCheck > AUTO_OPEN_SPEED_LIMIT) {
settleToOpen = true;
} else if (speedToCheck < -AUTO_OPEN_SPEED_LIMIT) {
settleToOpen = false;
} else if (mDraggingBorder > rangeToCheck / 2) {
settleToOpen = true;
} else if (mDraggingBorder < rangeToCheck / 2) {
settleToOpen = false;
}
final int settleDestX;
final int settleDestY;
if (settleToOpen) {
settleDestX = 0;
settleDestY = mDragRange;
} else {
settleDestX = 0;
settleDestY = mDragCallback.getLowerLimit();
}
if(mDragHelper.settleCapturedViewAt(settleDestX, settleDestY)) {
ViewCompat.postInvalidateOnAnimation(OuterLayout.this);
}
}
}
public OuterLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
private void updateRanges() {
// Need to wait for the tabs to show in order to fetch the right sizes.
mDragRange = mDragCallback.getDragRange() + mDragCallback.getLowerLimit();
}
private void updateOrientation() {
mDragHelper.setEdgeTrackingEnabled(0);
}
@Override
protected void onFinishInflate() {
mDragHelper = ViewDragHelper.create(this, 1.0f, new DragHelperCallback());
mIsOpen = false;
super.onFinishInflate();
}
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
if (mDragCallback.canDrag(event)) {
if (mDragHelper.shouldInterceptTouchEvent(event)) {
return true;
}
}
// Because while open the target layout is translated and draghelper does not catch it.
if (mIsOpen && mDragCallback.canInterceptEventWhileOpen(event)) {
return true;
}
return false;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// touch events can be passed to the helper if we target the toolbar or we are already dragging.
if (mDragCallback.canDrag(event) || mDraggingState == ViewDragHelper.STATE_DRAGGING) {
mDragHelper.processTouchEvent(event);
}
return true;
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
// The first time fennec is started, tabs might not have been created while we drag. In that case we need
// an arbitrary range to start dragging that will be updated as soon as the tabs are created.
if (mDragRange == 0) {
mDragRange = h / 2;
}
super.onSizeChanged(w, h, oldw, oldh);
}
@Override
public void computeScroll() { // needed for automatic settling.
if (mDragHelper.continueSettling(true)) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
/**
* To be called when closing the tabs from outside (i.e. when touching the main layout).
*/
public void setClosed() {
mIsOpen = false;
mDragHelper.abort();
}
/**
* To be called when opening the tabs from outside (i.e. when clicking on the tabs button).
*/
public void setOpen() {
mIsOpen = true;
mDragHelper.abort();
}
public void setDraggableCallback(DragCallback dragCallback) {
mDragCallback = dragCallback;
updateOrientation();
}
// If a change happens while we are dragging, we abort the dragging and set to open state.
public void reset() {
updateOrientation();
if (isMoving()) {
mDragHelper.abort();
if (mDragCallback != null) {
mDragCallback.stopDrag(false);
mDragCallback.onDragProgress(0f);
}
}
}
public void updateDragHelperParameters() {
mDragRange = mDragCallback.getDragRange() + mDragCallback.getLowerLimit();
updateOrientation();
}
public boolean isMoving() {
return (mDraggingState == ViewDragHelper.STATE_DRAGGING ||
mDraggingState == ViewDragHelper.STATE_SETTLING);
}
public boolean isOpen() {
return mIsOpen;
}
public View findTopChildUnder(MotionEvent event) {
return mDragHelper.findTopChildUnder((int) event.getX(), (int) event.getY());
}
public void restoreTargetViewPosition() {
mDragCallback.getViewToDrag().offsetTopAndBottom(mDraggingBorder);
}
}

View File

@ -326,6 +326,7 @@ gbjar.sources += [
'InputMethods.java',
'IntentHelper.java',
'JavaAddonManager.java',
'LayoutInterceptor.java',
'LocaleManager.java',
'Locales.java',
'lwt/LightweightTheme.java',
@ -349,6 +350,7 @@ gbjar.sources += [
'NotificationService.java',
'NSSBridge.java',
'OrderedBroadcastHelper.java',
'OuterLayout.java',
'preferences/AlignRightLinkPreference.java',
'preferences/AndroidImport.java',
'preferences/AndroidImportPreference.java',

View File

@ -3,7 +3,7 @@
- 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/. -->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
<org.mozilla.gecko.OuterLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:gecko="http://schemas.android.com/apk/res-auto"
android:id="@+id/root_layout"
android:layout_width="match_parent"
@ -134,4 +134,4 @@
android:layout="@layout/button_toast"
style="@style/Toast"/>
</RelativeLayout>
</org.mozilla.gecko.OuterLayout>

View File

@ -188,6 +188,8 @@
<dimen name="tab_history_bg_width">2dp</dimen>
<dimen name="tab_history_border_padding">2dp</dimen>
<dimen name="horizontal_drag_area">256dp</dimen>
<!-- Find-In-Page dialog dimensions. -->
<dimen name="find_in_page_text_margin_left">5dip</dimen>
<dimen name="find_in_page_text_margin_right">12dip</dimen>

View File

@ -14,6 +14,8 @@ import org.mozilla.gecko.R;
import org.mozilla.gecko.Telemetry;
import org.mozilla.gecko.TelemetryContract;
import org.mozilla.gecko.animation.PropertyAnimator;
import org.mozilla.gecko.Tab;
import org.mozilla.gecko.Tabs;
import org.mozilla.gecko.animation.ViewHelper;
import org.mozilla.gecko.lwt.LightweightTheme;
import org.mozilla.gecko.lwt.LightweightThemeDrawable;
@ -393,15 +395,40 @@ public class TabsPanel extends LinearLayout
}
public void show(Panel panelToShow) {
if (!isShown())
final boolean showAnimation = !mVisible;
prepareToShow(panelToShow);
if (isSideBar()) {
if (showAnimation) {
dispatchLayoutChange(getWidth(), getHeight());
}
} else {
int height = getVerticalPanelHeight();
dispatchLayoutChange(getWidth(), height);
}
}
public void prepareToDrag() {
Tab selectedTab = Tabs.getInstance().getSelectedTab();
if (selectedTab != null && selectedTab.isPrivate()) {
prepareToShow(TabsPanel.Panel.PRIVATE_TABS);
} else {
prepareToShow(TabsPanel.Panel.NORMAL_TABS);
}
if (mIsSideBar) {
prepareSidebarAnimation(getWidth());
}
}
public void prepareToShow(Panel panelToShow) {
if (!isShown()) {
setVisibility(View.VISIBLE);
}
if (mPanel != null) {
// Hide the old panel.
mPanel.hide();
}
final boolean showAnimation = !mVisible;
mVisible = true;
mCurrentPanel = panelToShow;
@ -431,20 +458,20 @@ public class TabsPanel extends LinearLayout
if (!HardwareUtils.hasMenuButton()) {
mMenuButton.setVisibility(View.VISIBLE);
mMenuButton.setEnabled(true);
mPopupMenu.setAnchor(mMenuButton);
} else {
mPopupMenu.setAnchor(mAddTab);
}
}
if (isSideBar()) {
if (showAnimation)
dispatchLayoutChange(getWidth(), getHeight());
} else {
int actionBarHeight = mContext.getResources().getDimensionPixelSize(R.dimen.browser_toolbar_height);
int height = actionBarHeight + getTabContainerHeight(mTabsContainer);
dispatchLayoutChange(getWidth(), height);
}
mHeaderVisible = true;
public void hideImmediately() {
mVisible = false;
setVisibility(View.INVISIBLE);
}
public int getVerticalPanelHeight() {
final int actionBarHeight = mContext.getResources().getDimensionPixelSize(R.dimen.browser_toolbar_height);
final int height = actionBarHeight + getTabContainerHeight(mTabsContainer);
return height;
}
public void hide() {
@ -488,6 +515,28 @@ public class TabsPanel extends LinearLayout
return mCurrentPanel;
}
public void setHWLayerEnabled(boolean enabled) {
if (Versions.preHC) {
return;
}
if (enabled) {
mHeader.setLayerType(View.LAYER_TYPE_HARDWARE, null);
mTabsContainer.setLayerType(View.LAYER_TYPE_HARDWARE, null);
} else {
mHeader.setLayerType(View.LAYER_TYPE_NONE, null);
mTabsContainer.setLayerType(View.LAYER_TYPE_NONE, null);
}
}
public void prepareSidebarAnimation(int tabsPanelWidth) {
if (mVisible) {
ViewHelper.setTranslationX(mHeader, -tabsPanelWidth);
ViewHelper.setTranslationX(mTabsContainer, -tabsPanelWidth);
// The footer view is only present on the sidebar, v11+.
ViewHelper.setTranslationX(mFooter, -tabsPanelWidth);
}
}
public void prepareTabsAnimation(PropertyAnimator animator) {
// Not worth doing this on pre-Honeycomb without proper
// hardware accelerated animations.
@ -497,13 +546,7 @@ public class TabsPanel extends LinearLayout
if (mIsSideBar) {
final int tabsPanelWidth = getWidth();
if (mVisible) {
ViewHelper.setTranslationX(mHeader, -tabsPanelWidth);
ViewHelper.setTranslationX(mTabsContainer, -tabsPanelWidth);
// The footer view is only present on the sidebar, v11+.
ViewHelper.setTranslationX(mFooter, -tabsPanelWidth);
}
prepareSidebarAnimation(tabsPanelWidth);
final int translationX = (mVisible ? 0 : -tabsPanelWidth);
animator.attach(mTabsContainer, PropertyAnimator.Property.TRANSLATION_X, translationX);
animator.attach(mHeader, PropertyAnimator.Property.TRANSLATION_X, translationX);
@ -523,8 +566,25 @@ public class TabsPanel extends LinearLayout
animator.attach(mHeader, PropertyAnimator.Property.TRANSLATION_Y, translationY);
}
mHeader.setLayerType(View.LAYER_TYPE_HARDWARE, null);
mTabsContainer.setLayerType(View.LAYER_TYPE_HARDWARE, null);
setHWLayerEnabled(true);
}
public void translateInRange(float progress) {
final Resources resources = getContext().getResources();
if (!mIsSideBar) {
final int toolbarHeight = resources.getDimensionPixelSize(R.dimen.browser_toolbar_height);
final int translationY = (int) - ((1 - progress) * toolbarHeight);
ViewHelper.setTranslationY(mHeader, translationY);
ViewHelper.setTranslationY(mTabsContainer, translationY);
mTabsContainer.setAlpha(progress);
} else {
final int tabsPanelWidth = getWidth();
prepareSidebarAnimation(tabsPanelWidth);
final int translationX = (int) - ((1 - progress) * tabsPanelWidth);
ViewHelper.setTranslationX(mHeader, translationX);
ViewHelper.setTranslationX(mTabsContainer, translationX);
ViewHelper.setTranslationX(mFooter, translationX);
}
}
public void finishTabsAnimation() {
@ -532,10 +592,9 @@ public class TabsPanel extends LinearLayout
return;
}
mHeader.setLayerType(View.LAYER_TYPE_NONE, null);
mTabsContainer.setLayerType(View.LAYER_TYPE_NONE, null);
setHWLayerEnabled(false);
// If the tabs panel is now hidden, call hide() on current panel and unset it as the current panel
// If the tray is now hidden, call hide() on current panel and unset it as the current panel
// to avoid hide() being called again when the layout is opened next.
if (!mVisible && mPanel != null) {
mPanel.hide();

View File

@ -141,6 +141,7 @@ public abstract class BrowserToolbar extends ThemedRelativeLayout
private final int shadowSize;
private final ToolbarPrefs prefs;
private boolean contextMenuEnabled = true;
public abstract boolean isAnimating();
@ -244,8 +245,8 @@ public abstract class BrowserToolbar extends ThemedRelativeLayout
setOnCreateContextMenuListener(new View.OnCreateContextMenuListener() {
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
// We don't the context menu while editing
if (isEditing()) {
// We don't the context menu while editing or while dragging
if (isEditing() || !contextMenuEnabled) {
return;
}
@ -570,16 +571,19 @@ public abstract class BrowserToolbar extends ThemedRelativeLayout
menuButton.setNextFocusDownId(nextId);
}
public void hideVirtualKeyboard() {
InputMethodManager imm =
(InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(tabsButton.getWindowToken(), 0);
}
private void toggleTabs() {
if (activity.areTabsShown()) {
if (activity.hasTabsSideBar())
activity.hideTabs();
} else {
// hide the virtual keyboard
InputMethodManager imm =
(InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(tabsButton.getWindowToken(), 0);
hideVirtualKeyboard();
Tab tab = Tabs.getInstance().getSelectedTab();
if (tab != null) {
if (!tab.isPrivate())
@ -674,6 +678,13 @@ public abstract class BrowserToolbar extends ThemedRelativeLayout
}
}
public void setToolBarButtonsAlpha(float alpha) {
ViewHelper.setAlpha(tabsCounter, alpha);
if (hasSoftMenuButton && !HardwareUtils.isTablet()) {
ViewHelper.setAlpha(menuIcon, alpha);
}
}
public void onEditSuggestion(String suggestion) {
if (!isEditing()) {
return;
@ -949,6 +960,10 @@ public abstract class BrowserToolbar extends ThemedRelativeLayout
return drawable;
}
public void setContextMenuEnabled(boolean enabled) {
contextMenuEnabled = enabled;
}
public static class TabEditingState {
// The edited text from the most recent time this tab was unselected.
protected String lastEditingText;

View File

@ -172,6 +172,12 @@ class BrowserToolbarNewTablet extends BrowserToolbarTabletBase {
// Do nothing.
}
@Override
public void setToolBarButtonsAlpha(float alpha) {
// Do nothing;
}
@Override
public void startEditing(final String url, final PropertyAnimator animator) {
// We already know the forward button state - no need to store it here.

View File

@ -173,4 +173,8 @@ public class ButtonToast {
hide(false, ReasonHidden.TIMEOUT);
}
};
public boolean isVisible() {
return (mView.getVisibility() == View.VISIBLE);
}
}

View File

@ -338,7 +338,7 @@ exports.dbg_assert = function dbg_assert(cond, e) {
if (!cond) {
return e;
}
}
};
/**

View File

@ -19,6 +19,7 @@ const PromiseDebugging = require("PromiseDebugging");
const Debugger = require("Debugger");
const xpcInspector = require("xpcInspector");
const mapURIToAddonID = require("./utils/map-uri-to-addon-id");
const ScriptStore = require("./utils/ScriptStore");
const { defer, resolve, reject, all } = require("devtools/toolkit/deprecated-sync-thenables");
const { CssLogic } = require("devtools/styleinspector/css-logic");
@ -433,6 +434,8 @@ function ThreadActor(aParent, aGlobal)
this._gripDepth = 0;
this._threadLifetimePool = null;
this._tabClosed = false;
this._scripts = null;
this._sources = null;
this._options = {
useSourceMaps: false,
@ -498,6 +501,14 @@ ThreadActor.prototype = {
return this._threadLifetimePool;
},
get scripts() {
if (!this._scripts) {
this._scripts = new ScriptStore();
this._scripts.addScripts(this.dbg.findScripts());
}
return this._scripts;
},
get sources() {
if (!this._sources) {
this._sources = new ThreadSources(this, this._options,
@ -574,6 +585,7 @@ ThreadActor.prototype = {
this.dbg.removeAllDebuggees();
}
this._sources = null;
this._scripts = null;
},
/**
@ -1320,9 +1332,11 @@ ThreadActor.prototype = {
* Get the script and source lists from the debugger.
*/
_discoverSources: function () {
// Only get one script per url.
// Only get one script per Debugger.Source.
const sourcesToScripts = new Map();
for (let s of this.dbg.findScripts()) {
const scripts = this.scripts.getAllScripts();
for (let i = 0, len = scripts.length; i < len; i++) {
let s = scripts[i];
if (s.source) {
sourcesToScripts.set(s.source, s);
}
@ -1944,6 +1958,15 @@ ThreadActor.prototype = {
*/
onNewScript: function (aScript, aGlobal) {
this.sources.sourcesForScript(aScript);
// XXX: The scripts must be added to the ScriptStore before restoring
// breakpoints in _addScript. If we try to add them to the ScriptStore
// inside _addScript, we can accidentally set a breakpoint in a top level
// script as a "closest match" because we wouldn't have added the child
// scripts to the ScriptStore yet.
this.scripts.addScript(aScript);
this.scripts.addScripts(aScript.getChildScripts());
this._addScript(aScript);
// |onNewScript| is only fired for top level scripts (AKA staticLevel == 0),
@ -1982,7 +2005,7 @@ ThreadActor.prototype = {
return;
}
for (let s of this.dbg.findScripts()) {
for (let s of this.scripts.getAllScripts()) {
this._addScript(s);
}
},
@ -2238,6 +2261,7 @@ SourceActor.prototype = {
get threadActor() { return this._threadActor; },
get dbg() { return this.threadActor.dbg; },
get scripts() { return this.threadActor.scripts; },
get source() { return this._source; },
get generatedSource() { return this._generatedSource; },
get breakpointActorMap() { return this.threadActor.breakpointActorMap; },
@ -2422,7 +2446,7 @@ SourceActor.prototype = {
**/
getExecutableOffsets: function (source, onlyLine) {
let offsets = new Set();
for (let s of this.threadActor.dbg.findScripts({ source: source })) {
for (let s of this.threadActor.scripts.getScriptsBySource(source)) {
for (let offset of s.getAllColumnOffsets()) {
offsets.add(onlyLine ? offset.lineNumber : offset);
}
@ -2893,16 +2917,13 @@ SourceActor.prototype = {
};
const actor = location.actor = this._getOrCreateBreakpointActor(location);
// Find all scripts matching the given location. We will almost
// always have a `source` object to query, but inline HTML scripts
// are all represented by 1 SourceActor even though they have
// separate source objects, so we need to query based on the url
// of the page for them.
const scripts = this.dbg.findScripts({
source: this.source || undefined,
url: this._originalUrl || undefined,
line: location.line,
});
// Find all scripts matching the given location. We will almost always have
// a `source` object to query, but multiple inline HTML scripts are all
// represented by a single SourceActor even though they have separate source
// objects, so we need to query based on the url of the page for them.
const scripts = this.source
? this.scripts.getScriptsBySourceAndLine(this.source, location.line)
: this.scripts.getScriptsByURLAndLine(this._originalUrl, location.line);
if (scripts.length === 0) {
// Since we did not find any scripts to set the breakpoint on now, return
@ -4967,6 +4988,9 @@ Debugger.Script.prototype.toString = function() {
if (this.url) {
output += this.url;
}
if (typeof this.staticLevel != "undefined") {
output += ":L" + this.staticLevel;
}
if (typeof this.startLine != "undefined") {
output += ":" + this.startLine;
if (this.lineCount && this.lineCount > 1) {

View File

@ -0,0 +1,207 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const { noop } = require("devtools/toolkit/DevToolsUtils");
/**
* A `ScriptStore` is a cache of `Debugger.Script` instances. It holds strong
* references to the cached scripts to alleviate the GC-sensitivity issues that
* plague `Debugger.prototype.findScripts`, but this means that its lifetime
* must be managed carefully. It is the `ScriptStore` user's responsibility to
* ensure that the `ScriptStore` stays up to date.
*
* Implementation Notes:
*
* The ScriptStore's prototype methods are very hot, in general. To help the
* JIT, they avoid ES6-isms and higher-order iteration functions, for the most
* part. You might be wondering why we don't maintain indices on, say,
* Debugger.Source for faster querying, if these methods are so hot. First, the
* hottest method is actually just getting all scripts; second, populating the
* store becomes prohibitively expensive. So we fall back to linear queries
* (which isn't so bad, because Debugger.prototype.findScripts is also linear).
*/
function ScriptStore() {
// Set of every Debugger.Script in the cache.
this._scripts = new NoDeleteSet;
}
module.exports = ScriptStore;
ScriptStore.prototype = {
// Populating a ScriptStore.
/**
* Add one script to the cache.
*
* @param Debugger.Script script
*/
addScript(script) {
this._scripts.add(script);
},
/**
* Add many scripts to the cache at once.
*
* @param Array scripts
* The set of Debugger.Scripts to add to the cache.
*/
addScripts(scripts) {
for (var i = 0, len = scripts.length; i < len; i++) {
this.addScript(scripts[i]);
}
},
// Querying a ScriptStore.
/**
* Get all the sources for which we have scripts cached.
*
* @returns Array of Debugger.Source
*/
getSources() {
return [...new Set(this._scripts.items.map(s => s.source))];
},
/**
* Get all the scripts in the cache.
*
* @returns read-only Array of Debugger.Script.
*
* NB: The ScriptStore retains ownership of the returned array, and the
* ScriptStore's consumers MUST NOT MODIFY its contents!
*/
getAllScripts() {
return this._scripts.items;
},
/**
* Get all scripts produced from the given source.
*
* @oaram Debugger.Source source
* @returns Array of Debugger.Script
*/
getScriptsBySource(source) {
var results = [];
var scripts = this._scripts.items;
var length = scripts.length;
for (var i = 0; i < length; i++) {
if (scripts[i].source === source) {
results.push(scripts[i]);
}
}
return results;
},
/**
* Get all scripts produced from the given source whose source code definition
* spans the given line.
*
* @oaram Debugger.Source source
* @param Number line
* @returns Array of Debugger.Script
*/
getScriptsBySourceAndLine(source, line) {
var results = [];
var scripts = this._scripts.items;
var length = scripts.length;
for (var i = 0; i < length; i++) {
var script = scripts[i];
if (script.source === source &&
script.startLine <= line &&
(script.startLine + script.lineCount) > line) {
results.push(script);
}
}
return results;
},
/**
* Get all scripts defined by a source at the given URL.
*
* @param String url
* @returns Array of Debugger.Script
*/
getScriptsByURL(url) {
var results = [];
var scripts = this._scripts.items;
var length = scripts.length;
for (var i = 0; i < length; i++) {
if (scripts[i].url === url) {
results.push(scripts[i]);
}
}
return results;
},
/**
* Get all scripts defined by a source a the given URL and whose source code
* definition spans the given line.
*
* @param String url
* @param Number line
* @returns Array of Debugger.Script
*/
getScriptsByURLAndLine(url, line) {
var results = [];
var scripts = this._scripts.items;
var length = scripts.length;
for (var i = 0; i < length; i++) {
var script = scripts[i];
if (script.url === url &&
script.startLine <= line &&
(script.startLine + script.lineCount) > line) {
results.push(script);
}
}
return results;
},
};
/**
* A set which can only grow, and does not support the delete operation.
* Provides faster iteration than the native Set by maintaining an array of all
* items, in addition to the internal set of all items, which allows direct
* iteration (without the iteration protocol and calling into C++, which are
* both expensive).
*/
function NoDeleteSet() {
this._set = new Set();
this.items = [];
}
NoDeleteSet.prototype = {
/**
* An array containing every item in the set for convenience and faster
* iteration. This is public for reading only, and consumers MUST NOT modify
* this array!
*/
items: null,
/**
* Add an item to the set.
*
* @param any item
*/
add(item) {
if (!this._set.has(item)) {
this._set.add(item);
this.items.push(item);
}
},
/**
* Return true if the item is in the set, false otherwise.
*
* @param any item
* @returns Boolean
*/
has(item) {
return this._set.has(item);
}
};

View File

@ -71,7 +71,8 @@ EXTRA_JS_MODULES.devtools.server.actors += [
EXTRA_JS_MODULES.devtools.server.actors.utils += [
'actors/utils/make-debugger.js',
'actors/utils/map-uri-to-addon-id.js'
'actors/utils/map-uri-to-addon-id.js',
'actors/utils/ScriptStore.js'
]
FAIL_ON_WARNINGS = True

View File

@ -0,0 +1,168 @@
/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
// Test the functionality of ScriptStore.
const ScriptStore = devtools.require("devtools/server/actors/utils/ScriptStore");
// Fixtures
const firstSource = "firstSource";
const secondSource = "secondSource";
const thirdSource = "thirdSource";
const scripts = new Set([
{
url: "a.js",
source: firstSource,
startLine: 1,
lineCount: 100,
global: "g1"
},
{
url: "a.js",
source: firstSource,
startLine: 1,
lineCount: 40,
global: "g1"
},
{
url: "a.js",
source: firstSource,
startLine: 50,
lineCount: 100,
global: "g1"
},
{
url: "a.js",
source: firstSource,
startLine: 60,
lineCount: 90,
global: "g1"
},
{
url: "index.html",
source: secondSource,
startLine: 150,
lineCount: 1,
global: "g2"
},
{
url: "index.html",
source: thirdSource,
startLine: 200,
lineCount: 100,
global: "g2"
},
{
url: "index.html",
source: thirdSource,
startLine: 250,
lineCount: 10,
global: "g2"
},
{
url: "index.html",
source: thirdSource,
startLine: 275,
lineCount: 5,
global: "g2"
}
]);
function contains(script, line) {
return script.startLine <= line &&
line < script.startLine + script.lineCount;
}
function run_test() {
testAddScript();
testAddScripts();
testGetSources();
testGetScriptsBySource();
testGetScriptsBySourceAndLine();
testGetScriptsByURL();
testGetScriptsByURLAndLine();
}
function testAddScript() {
const ss = new ScriptStore();
for (let s of scripts) {
ss.addScript(s);
}
equal(ss.getAllScripts().length, scripts.size);
for (let s of ss.getAllScripts()) {
ok(scripts.has(s));
}
}
function testAddScripts() {
const ss = new ScriptStore();
ss.addScripts([...scripts]);
equal(ss.getAllScripts().length, scripts.size);
for (let s of ss.getAllScripts()) {
ok(scripts.has(s));
}
}
function testGetSources() {
const ss = new ScriptStore();
ss.addScripts([...scripts])
const expected = new Set([firstSource, secondSource, thirdSource]);
const actual = ss.getSources();
equal(expected.size, actual.length);
for (let s of actual) {
ok(expected.has(s));
expected.delete(s);
}
}
function testGetScriptsBySource() {
const ss = new ScriptStore();
ss.addScripts([...scripts]);
const expected = [...scripts].filter(s => s.source === thirdSource);
const actual = ss.getScriptsBySource(thirdSource);
deepEqual(actual, expected);
}
function testGetScriptsBySourceAndLine() {
const ss = new ScriptStore();
ss.addScripts([...scripts]);
const expected = [...scripts].filter(
s => s.source === firstSource && contains(s, 65))
const actual = ss.getScriptsBySourceAndLine(firstSource, 65);
deepEqual(actual, expected);
}
function testGetScriptsByURL() {
const ss = new ScriptStore();
ss.addScripts([...scripts]);
const expected = [...scripts].filter(s => s.url === "index.html");
const actual = ss.getScriptsByURL("index.html");
deepEqual(actual, expected);
}
function testGetScriptsByURLAndLine() {
const ss = new ScriptStore();
ss.addScripts([...scripts]);
const expected = [...scripts].filter(
s => s.url === "index.html" && contains(s, 250))
const actual = ss.getScriptsByURLAndLine("index.html", 250);
deepEqual(actual, expected);
}

View File

@ -18,6 +18,7 @@ support-files =
tracerlocations.js
hello-actor.js
[test_ScriptStore.js]
[test_actor-registry-actor.js]
[test_nesting-01.js]
[test_nesting-02.js]

View File

@ -1045,22 +1045,30 @@ CycleCollectedJSRuntime::DeferredFinalize(DeferredFinalizeAppendFunction aAppend
void
CycleCollectedJSRuntime::DeferredFinalize(nsISupports* aSupports)
{
#if defined(XP_MACOSX) && defined(__LP64__)
// We'll crash here if aSupports is poisoned (== 0x5a5a5a5a5a5a5a5a). This
// is better (more informative) than crashing in ReleaseSliceNow(). See
// bug 997908. This patch should get backed out when bug 997908 gets fixed,
// or if it doesn't actually help diagnose that bug. Specifying a constraint
// of "r" for aSupports ensures %0 is a register. Without this, clang
// sometimes mishandles this inline assembly code, causing crashes. See
// bug 1091801.
__asm__ __volatile__("push %%rax;"
"push %%rdx;"
"movq %0, %%rax;"
"movq (%%rax), %%rdx;"
"pop %%rdx;"
"pop %%rax;" : : "r" (aSupports));
#endif
#ifdef MOZ_CRASHREPORTER
// Bug 997908's crashes (in ReleaseSliceNow()) might be caused by
// intermittent failures here in nsTArray::AppendElement(). So if we see
// any failures, deliberately crash and include diagnostic information in
// the crash report.
size_t oldLength = mDeferredSupports.Length();
nsISupports** itemPtr = mDeferredSupports.AppendElement(aSupports);
size_t newLength = mDeferredSupports.Length();
nsISupports* item = mDeferredSupports.ElementAt(newLength - 1);
if ((newLength - oldLength != 1) || !itemPtr ||
(*itemPtr != aSupports) || (item != aSupports)) {
nsAutoCString debugInfo;
debugInfo.AppendPrintf("\noldLength [%u], newLength [%u], aSupports [%p], item [%p], itemPtr [%p], *itemPtr [%p]",
oldLength, newLength, aSupports, item, itemPtr, itemPtr ? *itemPtr : NULL);
#define CRASH_MESSAGE "nsTArray::AppendElement() failed!"
CrashReporter::AppendAppNotesToCrashReport(NS_LITERAL_CSTRING("\nBug 997908: ") +
NS_LITERAL_CSTRING(CRASH_MESSAGE));
CrashReporter::AppendAppNotesToCrashReport(debugInfo);
MOZ_CRASH(CRASH_MESSAGE);
#undef CRASH_MESSAGE
}
#else
mDeferredSupports.AppendElement(aSupports);
#endif
}
void