Merge fx-team to m-c.

This commit is contained in:
Ryan VanderMeulen 2013-12-09 17:21:47 -05:00
commit 99581e1939
31 changed files with 642 additions and 234 deletions

View File

@ -477,7 +477,7 @@ let CustomizableUIInternal = {
this.insertWidgetBefore(node, currentNode, container, aArea);
if (gResetting) {
this.notifyListeners("onWidgetReset", id, aArea);
this.notifyListeners("onWidgetReset", node, container);
}
}
@ -527,7 +527,7 @@ let CustomizableUIInternal = {
}
if (gResetting) {
this.notifyListeners("onAreaReset", aArea);
this.notifyListeners("onAreaReset", aArea, container);
}
} finally {
this.endBatchUpdate();
@ -668,6 +668,8 @@ let CustomizableUIInternal = {
}
let area = gAreas.get(aArea);
let isToolbar = area.get("type") == CustomizableUI.TYPE_TOOLBAR;
let isOverflowable = isToolbar && area.get("overflowable");
let showInPrivateBrowsing = gPalette.has(aWidgetId)
? gPalette.get(aWidgetId).showInPrivateBrowsing
: true;
@ -679,12 +681,15 @@ let CustomizableUIInternal = {
continue;
}
let container = areaNode.customizationTarget;
let widgetNode = container.ownerDocument.getElementById(aWidgetId);
let widgetNode = window.document.getElementById(aWidgetId);
if (!widgetNode) {
ERROR("Widget not found, unable to remove");
continue;
}
let container = areaNode.customizationTarget;
if (isOverflowable) {
container = areaNode.overflowable.getContainerFor(widgetNode);
}
this.notifyListeners("onWidgetBeforeDOMChange", widgetNode, null, container, true);
@ -702,7 +707,7 @@ let CustomizableUIInternal = {
}
this.notifyListeners("onWidgetAfterDOMChange", widgetNode, null, container, true);
if (area.get("type") == CustomizableUI.TYPE_TOOLBAR) {
if (isToolbar) {
areaNode.setAttribute("currentset", gPlacements.get(aArea).join(','));
}
@ -2094,47 +2099,330 @@ this.CustomizableUI = {
get WIDE_PANEL_CLASS() "panel-wide-item",
get PANEL_COLUMN_COUNT() 3,
/**
* Add a listener object that will get fired for various events regarding
* customization.
*
* @param aListener the listener object to add
*
* Not all event handler methods need to be defined.
* CustomizableUI will catch exceptions. Events are dispatched
* synchronously on the UI thread, so if you can delay any/some of your
* processing, that is advisable. The following event handlers are supported:
* - onWidgetAdded(aWidgetId, aArea, aPosition)
* Fired when a widget is added to an area. aWidgetId is the widget that
* was added, aArea the area it was added to, and aPosition the position
* in which it was added.
* - onWidgetMoved(aWidgetId, aArea, aOldPosition, aNewPosition)
* Fired when a widget is moved within its area. aWidgetId is the widget
* that was moved, aArea the area it was moved in, aOldPosition its old
* position, and aNewPosition its new position.
* - onWidgetRemoved(aWidgetId, aArea)
* Fired when a widget is removed from its area. aWidgetId is the widget
* that was removed, aArea the area it was removed from.
*
* - onWidgetBeforeDOMChange(aNode, aNextNode, aContainer, aIsRemoval)
* Fired *before* a widget's DOM node is acted upon by CustomizableUI
* (to add, move or remove it). aNode is the DOM node changed, aNextNode
* the DOM node (if any) before which a widget will be inserted,
* aContainer the *actual* DOM container (could be an overflow panel in
* case of an overflowable toolbar), and aWasRemoval is true iff the
* action about to happen is the removal of the DOM node.
* - onWidgetAfterDOMChange(aNode, aNextNode, aContainer, aWasRemoval)
* Like onWidgetBeforeDOMChange, but fired after the change to the DOM
* node of the widget.
*
* - onWidgetReset(aNode, aContainer)
* Fired after a reset to default placements moves a widget's node to a
* different location. aNode is the widget's node, aContainer is the
* area it was moved into (NB: it might already have been there and been
* moved to a different position!)
* - onAreaReset(aArea, aContainer)
* Fired after a reset to default placements is complete on an area's
* DOM node. Note that this is fired for each DOM node. aArea is the area
* that was reset, aContainer the DOM node that was reset.
*
* - onWidgetCreated(aWidgetId)
* Fired when a widget with id aWidgetId has been created, but before it
* is added to any placements or any DOM nodes have been constructed.
* Only fired for API-based widgets.
* - onWidgetAfterCreation(aWidgetId, aArea)
* Fired after a widget with id aWidgetId has been created, and has been
* added to either its default area or the area in which it was placed
* previously. If the widget has no default area and/or it has never
* been placed anywhere, aArea may be null. Only fired for API-based
* widgets.
* - onWidgetDestroyed(aWidgetId)
* Fired when widgets are destroyed. aWidgetId is the widget that is
* being destroyed. Only fired for API-based widgets.
* - onWidgetInstanceRemoved(aWidgetId, aDocument)
* Fired when a window is unloaded and a widget's instance is destroyed
* because of this. Only fired for API-based widgets.
*
* - onWidgetDrag(aWidgetId, aArea)
* Fired both when and after customize mode drag handling system tries
* to determine the width and height of widget aWidgetId when dragged to a
* different area. aArea will be the area the item is dragged to, or
* undefined after the measurements have been done and the node has been
* moved back to its 'regular' area.
*
* - onCustomizeStart(aWindow)
* Fired when opening customize mode in aWindow.
* - onCustomizeEnd(aWindow)
* Fired when exiting customize mode in aWindow.
*/
addListener: function(aListener) {
CustomizableUIInternal.addListener(aListener);
},
/**
* Remove a listener added with addListener
* @param aListener the listener object to remove
*/
removeListener: function(aListener) {
CustomizableUIInternal.removeListener(aListener);
},
/**
* Register a customizable area with CustomizableUI.
* @param aName the name of the area to register. Can only contain
* alphanumeric characters, dashes (-) and underscores (_).
* @param aProps the properties of the area. The following properties are
* recognized:
* - type: the type of area. Either TYPE_TOOLBAR (default) or
* TYPE_MENU_PANEL;
* - anchor: for a menu panel or overflowable toolbar, the
* anchoring node for the panel.
* - legacy: set to true if you want customizableui to
* automatically migrate the currentset attribute
* - overflowable: set to true if your toolbar is overflowable.
* This requires an anchor, and only has an
* effect for toolbars.
* - defaultPlacements: an array of widget IDs making up the
* default contents of the area
*/
registerArea: function(aName, aProperties) {
CustomizableUIInternal.registerArea(aName, aProperties);
},
/**
* Register a concrete node for a registered area. This method is automatically
* called from any toolbar in the main browser window that has its
* "customizable" attribute set to true. There should normally be no need to
* call it yourself.
*
* Note that ideally, you should register your toolbar using registerArea
* before any of the toolbars have their XBL bindings constructed (which
* will happen when they're added to the DOM and are not hidden). If you
* don't, and your toolbar has a defaultset attribute, CustomizableUI will
* register it automatically. If your toolbar does not have a defaultset
* attribute, the node will be saved for processing when you call
* registerArea. Note that CustomizableUI won't restore state in the area,
* allow the user to customize it in customize mode, or otherwise deal
* with it, until the area has been registered.
*/
registerToolbarNode: function(aToolbar, aExistingChildren) {
CustomizableUIInternal.registerToolbarNode(aToolbar, aExistingChildren);
},
/**
* Register the menu panel node. This method should not be called by anyone
* apart from the built-in PanelUI.
* @param aPanel the panel DOM node being registered.
*/
registerMenuPanel: function(aPanel) {
CustomizableUIInternal.registerMenuPanel(aPanel);
},
/**
* Unregister a customizable area. The inverse of registerArea.
*
* Unregistering an area will remove all the (removable) widgets in the
* area, which will return to the panel, and destroy all other traces
* of the area within CustomizableUI. Note that this means the *contents*
* of the area's DOM nodes will be moved to the panel or removed, but
* the area's DOM nodes *themselves* will stay.
*
* Furthermore, by default the placements of the area will be kept in the
* saved state (!) and restored if you re-register the area at a later
* point. This is useful for e.g. add-ons that get disabled and then
* re-enabled (e.g. when they update).
*
* You can override this last behaviour (and destroy the placements
* information in the saved state) by passing true for aDestroyPlacements.
*
* @param aName the name of the area to unregister
* @param aDestroyPlacements whether to destroy the placements information
* for the area, too.
*/
unregisterArea: function(aName, aDestroyPlacements) {
CustomizableUIInternal.unregisterArea(aName, aDestroyPlacements);
},
/**
* Add a widget to an area.
* If the area to which you try to add is not known to CustomizableUI,
* this will throw.
* If the area to which you try to add has not yet been restored from its
* legacy state, this will postpone the addition.
* If the area to which you try to add is the same as the area in which
* the widget is currently placed, this will do the same as
* moveWidgetWithinArea.
* If the widget cannot be removed from its original location, this will
* no-op.
*
* This will fire an onWidgetAdded notification,
* and an onWidgetBeforeDOMChange and onWidgetAfterDOMChange notification
* for each window CustomizableUI knows about.
*
* @param aWidgetId the widget to add
* @param aArea the area to add the widget to
* @param aPosition the position at which to add the widget. If you do not
* pass a position, the widget will be added to the end
* of the area.
*/
addWidgetToArea: function(aWidgetId, aArea, aPosition) {
CustomizableUIInternal.addWidgetToArea(aWidgetId, aArea, aPosition);
},
/**
* Remove a widget from its area. If the widget cannot be removed from its
* area, or is not in any area, this will no-op. Otherwise, this will fire an
* onWidgetRemoved notification, and an onWidgetBeforeDOMChange and
* onWidgetAfterDOMChange notification for each window CustomizableUI knows
* about.
*
* @param aWidgetId the widget to remove
*/
removeWidgetFromArea: function(aWidgetId) {
CustomizableUIInternal.removeWidgetFromArea(aWidgetId);
},
/**
* Move a widget within an area.
* If the widget is not in any area, this will no-op.
* If the widget is already at the indicated position, this will no-op.
*
* Otherwise, this will move the widget and fire an onWidgetMoved notification,
* and an onWidgetBeforeDOMChange and onWidgetAfterDOMChange notification for
* each window CustomizableUI knows about.
*
* @param aWidgetid the widget to move
* @param aPosition the position to move the widget to.
* Negative values or values greater than the number of
* widgets will be interpreted to mean moving the widget to
* respectively the first or last position.
*/
moveWidgetWithinArea: function(aWidgetId, aPosition) {
CustomizableUIInternal.moveWidgetWithinArea(aWidgetId, aPosition);
},
/**
* Ensure a XUL-based widget created in a window after areas were
* initialized moves to its correct position.
* This is roughly equivalent to manually looking up the position and using
* insertItem in the old API, but a lot less work for consumers.
* Always prefer this over using toolbar.insertItem (which might no-op
* because it delegates to addWidgetToArea) or, worse, moving items in the
* DOM yourself.
*
* @param aWidgetId the widget that was just created
* @param aWindow the window in which you want to ensure it was added.
*
* NB: why is this API per-window, you wonder? Because if you need this,
* presumably you yourself need to create the widget in all the windows
* and need to loop through them anyway.
*/
ensureWidgetPlacedInWindow: function(aWidgetId, aWindow) {
return CustomizableUIInternal.ensureWidgetPlacedInWindow(aWidgetId, aWindow);
},
/**
* Start a batch update of items.
* During a batch update, the customization state is not saved to the user's
* preferences file, in order to reduce (possibly sync) IO.
* Calls to begin/endBatchUpdate may be nested.
*
* Callers should ensure that NO MATTER WHAT they call endBatchUpdate once
* for each call to endBatchUpdate, even if there are exceptions in the
* code in the batch update. Otherwise, for the duration of the
* Firefox session, customization state is never saved. Typically, you
* would do this using a try...finally block.
*/
beginBatchUpdate: function() {
CustomizableUIInternal.beginBatchUpdate();
},
/**
* End a batch update. See the documentation for beginBatchUpdate above.
*
* State is not saved if we believe it is identical to the last known
* saved state. State is only ever saved when all batch updates have
* finished (ie there has been 1 endBatchUpdate call for each
* beginBatchUpdate call). If any of the endBatchUpdate calls pass
* aForceDirty=true, we will flush to the prefs file.
*
* @param aForceDirty force CustomizableUI to flush to the prefs file when
* all batch updates have finished.
*/
endBatchUpdate: function(aForceDirty) {
CustomizableUIInternal.endBatchUpdate(aForceDirty);
},
/**
* Create a widget.
*
* To create a widget, you should pass an object with its desired
* properties. The following properties are supported:
*
* - id: the ID of the widget (required).
* - type: a string indicating the type of widget. Possible types
* are:
* 'button' - for simple button widgets (the default)
* 'view' - for buttons that open a panel or subview,
* depending on where they are placed.
* 'custom' - for fine-grained control over the creation
* of the widget.
* - viewId: Only useful for views (and required there): the id of the
* <panelview> that should be shown when clicking the widget.
* - onBuild(aDoc): Only useful for custom widgets (and required there); a
* function that will be invoked with the document in which
* to build a widget. Should return the DOM node that has
* been constructed.
* - onCreated(aNode): Attached to all widgets; a function that will be invoked
* whenever the widget has a DOM node constructed, passing the
* constructed node as an argument.
* - onCommand(aEvt): Only useful for button widgets; a function that will be
* invoked when the user activates the button.
* - onClick(aEvt): Attached to all widgets; a function that will be invoked
* when the user clicks the widget.
* - onViewShowing(aEvt): Only useful for views; a function that will be
* invoked when a user shows your view.
* - onViewHiding(aEvt): Only useful for views; a function that will be
* invoked when a user hides your view.
* - tooltiptext: string to use for the tooltip of the widget
* - label: string to use for the label of the widget
* - removable: whether the widget is removable (optional, default: false)
* - overflows: whether widget can overflow when in an overflowable
* toolbar (optional, default: true)
* - defaultArea: default area to add the widget to
* (optional, default: none)
* - shortcutId: id of an element that has a shortcut for this widget
* (optional, default: null). This is only used to display
* the shortcut as part of the tooltip for builtin widgets
* (which have strings inside
* customizableWidgets.properties). If you're in an add-on,
* you should not set this property.
* - showInPrivateBrowsing: whether to show the widget in private browsing
* mode (optional, default: true)
*
* @param aProperties the specifications for the widget.
*/
createWidget: function(aProperties) {
return CustomizableUIInternal.wrapWidget(
CustomizableUIInternal.createWidget(aProperties)
);
},
/**
* Destroy a widget
*
* If the widget is part of the default placements in an area, this will
* remove it from there. It will also remove any DOM instances. However,
* it will keep the widget in the placements for whatever area it was
* in at the time. You can remove it from there yourself by calling
* CustomizableUI.removeWidgetFromArea(aWidgetId).
*
* @param aWidgetId the widget to destroy
*/
destroyWidget: function(aWidgetId) {
CustomizableUIInternal.destroyWidget(aWidgetId);
},
@ -2765,7 +3053,14 @@ OverflowableToolbar.prototype = {
return [this._target, null];
}
return [this._list, nextNode];
}
},
getContainerFor: function(aNode) {
if (aNode.classList.contains("overflowedItem")) {
return this._list;
}
return this._target;
},
};
// When IDs contain special characters, we need to escape them for use with querySelector:

View File

@ -406,8 +406,8 @@ const CustomizableWidgets = [{
updateZoomResetButton();
}.bind(this),
onWidgetReset: function(aWidgetId) {
if (aWidgetId != this.id)
onWidgetReset: function(aWidgetNode) {
if (aWidgetNode != node)
return;
updateCombinedWidgetStyle(node, this.currentArea, true);
updateZoomResetButton();
@ -506,8 +506,8 @@ const CustomizableWidgets = [{
updateCombinedWidgetStyle(node);
}.bind(this),
onWidgetReset: function(aWidgetId) {
if (aWidgetId != this.id)
onWidgetReset: function(aWidgetNode) {
if (aWidgetNode != node)
return;
updateCombinedWidgetStyle(node, this.currentArea);
}.bind(this),
@ -814,4 +814,4 @@ if (isWin8OrHigher()) {
});
}
#endif
#endif
#endif

View File

@ -101,7 +101,7 @@ AreaPositionManager.prototype = {
let doc = aContainer.ownerDocument;
let draggedItem = doc.getElementById(aDraggedItemId);
// If dragging a wide item, always pick the first item in a row:
if (draggedItem &&
if (this._inPanel && draggedItem &&
draggedItem.classList.contains(CustomizableUI.WIDE_PANEL_CLASS)) {
return this._firstInRow(closest);
}

View File

@ -149,7 +149,8 @@ ToolbarView.prototype = {
_onStepOverPressed: function() {
if (DebuggerController.activeThread.paused) {
DebuggerController.StackFrames.currentFrameDepth = -1;
DebuggerController.activeThread.stepOver();
let warn = DebuggerController._ensureResumptionOrder;
DebuggerController.activeThread.stepOver(warn);
}
},
@ -159,7 +160,8 @@ ToolbarView.prototype = {
_onStepInPressed: function() {
if (DebuggerController.activeThread.paused) {
DebuggerController.StackFrames.currentFrameDepth = -1;
DebuggerController.activeThread.stepIn();
let warn = DebuggerController._ensureResumptionOrder;
DebuggerController.activeThread.stepIn(warn);
}
},
@ -169,7 +171,8 @@ ToolbarView.prototype = {
_onStepOutPressed: function() {
if (DebuggerController.activeThread.paused) {
DebuggerController.StackFrames.currentFrameDepth = -1;
DebuggerController.activeThread.stepOut();
let warn = DebuggerController._ensureResumptionOrder;
DebuggerController.activeThread.stepOut(warn);
}
},

View File

@ -484,22 +484,22 @@ function testOriginalRawDataIntegrity(arr, obj) {
function testAnonymousHeaders(fooScope, anonymousVar, anonymousScope, barVar, bazProperty) {
is(fooScope.header, true,
"A named scope should have a header visible.");
is(fooScope.target.hasAttribute("non-header"), false,
is(fooScope.target.hasAttribute("untitled"), false,
"The non-header attribute should not be applied to scopes with headers.");
is(anonymousScope.header, false,
"An anonymous scope should have a header visible.");
is(anonymousScope.target.hasAttribute("non-header"), true,
is(anonymousScope.target.hasAttribute("untitled"), true,
"The non-header attribute should not be applied to scopes without headers.");
is(barVar.header, true,
"A named variable should have a header visible.");
is(barVar.target.hasAttribute("non-header"), false,
is(barVar.target.hasAttribute("untitled"), false,
"The non-header attribute should not be applied to variables with headers.");
is(anonymousVar.header, false,
"An anonymous variable should have a header visible.");
is(anonymousVar.target.hasAttribute("non-header"), true,
is(anonymousVar.target.hasAttribute("untitled"), true,
"The non-header attribute should not be applied to variables without headers.");
}

View File

@ -72,29 +72,29 @@ function testVariablesAndPropertiesFiltering() {
is(constr2Var.expanded, true,
"The constr2Var should be expanded.");
is(localScope.target.querySelectorAll(".variables-view-variable:not([non-match])").length, 1,
is(localScope.target.querySelectorAll(".variables-view-variable:not([unmatched])").length, 1,
"There should be 1 variable displayed in the local scope.");
is(withScope.target.querySelectorAll(".variables-view-variable:not([non-match])").length, 0,
is(withScope.target.querySelectorAll(".variables-view-variable:not([unmatched])").length, 0,
"There should be 0 variables displayed in the with scope.");
is(functionScope.target.querySelectorAll(".variables-view-variable:not([non-match])").length, 0,
is(functionScope.target.querySelectorAll(".variables-view-variable:not([unmatched])").length, 0,
"There should be 0 variables displayed in the function scope.");
isnot(globalScope.target.querySelectorAll(".variables-view-variable:not([non-match])").length, 0,
isnot(globalScope.target.querySelectorAll(".variables-view-variable:not([unmatched])").length, 0,
"There should be some variables displayed in the global scope.");
is(withScope.target.querySelectorAll(".variables-view-property:not([non-match])").length, 0,
is(withScope.target.querySelectorAll(".variables-view-property:not([unmatched])").length, 0,
"There should be 0 properties displayed in the with scope.");
is(functionScope.target.querySelectorAll(".variables-view-property:not([non-match])").length, 0,
is(functionScope.target.querySelectorAll(".variables-view-property:not([unmatched])").length, 0,
"There should be 0 properties displayed in the function scope.");
is(globalScope.target.querySelectorAll(".variables-view-property:not([non-match])").length, 0,
is(globalScope.target.querySelectorAll(".variables-view-property:not([unmatched])").length, 0,
"There should be 0 properties displayed in the global scope.");
is(localScope.target.querySelectorAll(".variables-view-variable:not([non-match]) > .title > .name")[0].getAttribute("value"),
is(localScope.target.querySelectorAll(".variables-view-variable:not([unmatched]) > .title > .name")[0].getAttribute("value"),
"__proto__", "The only inner variable displayed should be '__proto__'");
is(localScope.target.querySelectorAll(".variables-view-property:not([non-match]) > .title > .name")[0].getAttribute("value"),
is(localScope.target.querySelectorAll(".variables-view-property:not([unmatched]) > .title > .name")[0].getAttribute("value"),
"constructor", "The first inner property displayed should be 'constructor'");
is(localScope.target.querySelectorAll(".variables-view-property:not([non-match]) > .title > .name")[1].getAttribute("value"),
is(localScope.target.querySelectorAll(".variables-view-property:not([unmatched]) > .title > .name")[1].getAttribute("value"),
"__proto__", "The second inner property displayed should be '__proto__'");
is(localScope.target.querySelectorAll(".variables-view-property:not([non-match]) > .title > .name")[2].getAttribute("value"),
is(localScope.target.querySelectorAll(".variables-view-property:not([unmatched]) > .title > .name")[2].getAttribute("value"),
"constructor", "The third inner property displayed should be 'constructor'");
}

View File

@ -72,36 +72,36 @@ function testVariablesAndPropertiesFiltering() {
is(constr2Var.expanded, true,
"The constr2Var should be expanded.");
is(localScope.target.querySelectorAll(".variables-view-variable:not([non-match])").length, 1,
is(localScope.target.querySelectorAll(".variables-view-variable:not([unmatched])").length, 1,
"There should be 1 variable displayed in the local scope.");
is(withScope.target.querySelectorAll(".variables-view-variable:not([non-match])").length, 0,
is(withScope.target.querySelectorAll(".variables-view-variable:not([unmatched])").length, 0,
"There should be 0 variables displayed in the with scope.");
is(functionScope.target.querySelectorAll(".variables-view-variable:not([non-match])").length, 0,
is(functionScope.target.querySelectorAll(".variables-view-variable:not([unmatched])").length, 0,
"There should be 0 variables displayed in the function scope.");
is(globalScope.target.querySelectorAll(".variables-view-variable:not([non-match])").length, 0,
is(globalScope.target.querySelectorAll(".variables-view-variable:not([unmatched])").length, 0,
"There should be no variables displayed in the global scope.");
is(localScope.target.querySelectorAll(".variables-view-property:not([non-match])").length, 4,
is(localScope.target.querySelectorAll(".variables-view-property:not([unmatched])").length, 4,
"There should be 4 properties displayed in the local scope.");
is(withScope.target.querySelectorAll(".variables-view-property:not([non-match])").length, 0,
is(withScope.target.querySelectorAll(".variables-view-property:not([unmatched])").length, 0,
"There should be 0 properties displayed in the with scope.");
is(functionScope.target.querySelectorAll(".variables-view-property:not([non-match])").length, 0,
is(functionScope.target.querySelectorAll(".variables-view-property:not([unmatched])").length, 0,
"There should be 0 properties displayed in the function scope.");
is(globalScope.target.querySelectorAll(".variables-view-property:not([non-match])").length, 0,
is(globalScope.target.querySelectorAll(".variables-view-property:not([unmatched])").length, 0,
"There should be 0 properties displayed in the global scope.");
is(localScope.target.querySelectorAll(".variables-view-variable:not([non-match]) > .title > .name")[0].getAttribute("value"),
is(localScope.target.querySelectorAll(".variables-view-variable:not([unmatched]) > .title > .name")[0].getAttribute("value"),
"__proto__", "The only inner variable displayed should be '__proto__'");
is(localScope.target.querySelectorAll(".variables-view-property:not([non-match]) > .title > .name")[0].getAttribute("value"),
is(localScope.target.querySelectorAll(".variables-view-property:not([unmatched]) > .title > .name")[0].getAttribute("value"),
"constructor", "The first inner property displayed should be 'constructor'");
is(localScope.target.querySelectorAll(".variables-view-property:not([non-match]) > .title > .name")[1].getAttribute("value"),
is(localScope.target.querySelectorAll(".variables-view-property:not([unmatched]) > .title > .name")[1].getAttribute("value"),
"__proto__", "The second inner property displayed should be '__proto__'");
is(localScope.target.querySelectorAll(".variables-view-property:not([non-match]) > .title > .name")[2].getAttribute("value"),
is(localScope.target.querySelectorAll(".variables-view-property:not([unmatched]) > .title > .name")[2].getAttribute("value"),
"constructor", "The third inner property displayed should be 'constructor'");
is(localScope.target.querySelectorAll(".variables-view-property:not([non-match]) > .title > .name")[3].getAttribute("value"),
is(localScope.target.querySelectorAll(".variables-view-property:not([unmatched]) > .title > .name")[3].getAttribute("value"),
"name", "The fourth inner property displayed should be 'name'");
is(localScope.target.querySelectorAll(".variables-view-property:not([non-match]) > .title > .value")[3].getAttribute("value"),
is(localScope.target.querySelectorAll(".variables-view-property:not([unmatched]) > .title > .value")[3].getAttribute("value"),
"\"Function\"", "The fourth inner property displayed should be '\"Function\"'");
}

View File

@ -58,20 +58,20 @@ function testVariablesAndPropertiesFiltering() {
is(globalScope.expanded, true,
"The globalScope should be expanded.");
is(localScope.target.querySelectorAll(".variables-view-variable:not([non-match])").length, 1,
is(localScope.target.querySelectorAll(".variables-view-variable:not([unmatched])").length, 1,
"There should be 1 variable displayed in the local scope.");
is(withScope.target.querySelectorAll(".variables-view-variable:not([non-match])").length, 0,
is(withScope.target.querySelectorAll(".variables-view-variable:not([unmatched])").length, 0,
"There should be 0 variables displayed in the with scope.");
is(functionScope.target.querySelectorAll(".variables-view-variable:not([non-match])").length, 0,
is(functionScope.target.querySelectorAll(".variables-view-variable:not([unmatched])").length, 0,
"There should be 0 variables displayed in the function scope.");
is(localScope.target.querySelectorAll(".variables-view-property:not([non-match])").length, 0,
is(localScope.target.querySelectorAll(".variables-view-property:not([unmatched])").length, 0,
"There should be 0 properties displayed in the local scope.");
is(withScope.target.querySelectorAll(".variables-view-property:not([non-match])").length, 0,
is(withScope.target.querySelectorAll(".variables-view-property:not([unmatched])").length, 0,
"There should be 0 properties displayed in the with scope.");
is(functionScope.target.querySelectorAll(".variables-view-property:not([non-match])").length, 0,
is(functionScope.target.querySelectorAll(".variables-view-property:not([unmatched])").length, 0,
"There should be 0 properties displayed in the function scope.");
is(globalScope.target.querySelectorAll(".variables-view-property:not([non-match])").length, 0,
is(globalScope.target.querySelectorAll(".variables-view-property:not([unmatched])").length, 0,
"There should be 0 properties displayed in the global scope.");
}
@ -79,9 +79,9 @@ function testVariablesAndPropertiesFiltering() {
typeText(gSearchBox, "*one");
testFiltered("one");
isnot(globalScope.target.querySelectorAll(".variables-view-variable:not([non-match])").length, 0,
isnot(globalScope.target.querySelectorAll(".variables-view-variable:not([unmatched])").length, 0,
"There should be some variables displayed in the global scope.");
is(localScope.target.querySelectorAll(".variables-view-variable:not([non-match]) > .title > .name")[0].getAttribute("value"),
is(localScope.target.querySelectorAll(".variables-view-variable:not([unmatched]) > .title > .name")[0].getAttribute("value"),
"one", "The only inner variable displayed should be 'one'");
}
@ -104,9 +104,9 @@ function testVariablesAndPropertiesFiltering() {
typeText(gSearchBox, "*two");
testFiltered("two");
is(globalScope.target.querySelectorAll(".variables-view-variable:not([non-match])").length, 0,
is(globalScope.target.querySelectorAll(".variables-view-variable:not([unmatched])").length, 0,
"There should be no variables displayed in the global scope.");
is(localScope.target.querySelectorAll(".variables-view-variable:not([non-match]) > .title > .name")[0].getAttribute("value"),
is(localScope.target.querySelectorAll(".variables-view-variable:not([unmatched]) > .title > .name")[0].getAttribute("value"),
"two", "The only inner variable displayed should be 'two'");
}

View File

@ -58,7 +58,7 @@ function testVariablesAndPropertiesFiltering() {
assertScopeExpansion([true, true, true, true]);
assertVariablesCountAtLeast([0, 0, 1, 0]);
is(functionScope.target.querySelectorAll(".variables-view-variable:not([non-match]) > .title > .name")[0].getAttribute("value"),
is(functionScope.target.querySelectorAll(".variables-view-variable:not([unmatched]) > .title > .name")[0].getAttribute("value"),
"arguments", "The arguments pseudoarray should be visible.");
is(functionScope.get("arguments").expanded, false,
"The arguments pseudoarray in functionScope should not be expanded.");
@ -69,12 +69,12 @@ function testVariablesAndPropertiesFiltering() {
assertScopeExpansion([true, true, true, true]);
assertVariablesCountAtLeast([0, 0, 1, 1]);
is(functionScope.target.querySelectorAll(".variables-view-variable:not([non-match]) > .title > .name")[0].getAttribute("value"),
is(functionScope.target.querySelectorAll(".variables-view-variable:not([unmatched]) > .title > .name")[0].getAttribute("value"),
"arguments", "The arguments pseudoarray should be visible.");
is(functionScope.get("arguments").expanded, false,
"The arguments pseudoarray in functionScope should not be expanded.");
is(globalScope.target.querySelectorAll(".variables-view-variable:not([non-match]) > .title > .name")[0].getAttribute("value"),
is(globalScope.target.querySelectorAll(".variables-view-variable:not([unmatched]) > .title > .name")[0].getAttribute("value"),
"EventTarget", "The EventTarget object should be visible.");
is(globalScope.get("EventTarget").expanded, false,
"The EventTarget object in globalScope should not be expanded.");
@ -85,17 +85,17 @@ function testVariablesAndPropertiesFiltering() {
assertScopeExpansion([true, true, true, true]);
assertVariablesCountAtLeast([0, 1, 3, 1]);
is(functionScope.target.querySelectorAll(".variables-view-variable:not([non-match]) > .title > .name")[0].getAttribute("value"),
is(functionScope.target.querySelectorAll(".variables-view-variable:not([unmatched]) > .title > .name")[0].getAttribute("value"),
"aNumber", "The aNumber param should be visible.");
is(functionScope.get("aNumber").expanded, false,
"The aNumber param in functionScope should not be expanded.");
is(functionScope.target.querySelectorAll(".variables-view-variable:not([non-match]) > .title > .name")[1].getAttribute("value"),
is(functionScope.target.querySelectorAll(".variables-view-variable:not([unmatched]) > .title > .name")[1].getAttribute("value"),
"a", "The a variable should be visible.");
is(functionScope.get("a").expanded, false,
"The a variable in functionScope should not be expanded.");
is(functionScope.target.querySelectorAll(".variables-view-variable:not([non-match]) > .title > .name")[2].getAttribute("value"),
is(functionScope.target.querySelectorAll(".variables-view-variable:not([unmatched]) > .title > .name")[2].getAttribute("value"),
"arguments", "The arguments pseudoarray should be visible.");
is(functionScope.get("arguments").expanded, false,
"The arguments pseudoarray in functionScope should not be expanded.");
@ -106,37 +106,37 @@ function testVariablesAndPropertiesFiltering() {
assertScopeExpansion([true, true, true, true]);
assertVariablesCountAtLeast([4, 1, 3, 1]);
is(localScope.target.querySelectorAll(".variables-view-variable:not([non-match]) > .title > .name")[0].getAttribute("value"),
is(localScope.target.querySelectorAll(".variables-view-variable:not([unmatched]) > .title > .name")[0].getAttribute("value"),
"this", "The this reference should be visible.");
is(localScope.get("this").expanded, false,
"The this reference in localScope should not be expanded.");
is(localScope.target.querySelectorAll(".variables-view-variable:not([non-match]) > .title > .name")[1].getAttribute("value"),
is(localScope.target.querySelectorAll(".variables-view-variable:not([unmatched]) > .title > .name")[1].getAttribute("value"),
"one", "The one variable should be visible.");
is(localScope.get("one").expanded, false,
"The one variable in localScope should not be expanded.");
is(localScope.target.querySelectorAll(".variables-view-variable:not([non-match]) > .title > .name")[2].getAttribute("value"),
is(localScope.target.querySelectorAll(".variables-view-variable:not([unmatched]) > .title > .name")[2].getAttribute("value"),
"two", "The two variable should be visible.");
is(localScope.get("two").expanded, false,
"The two variable in localScope should not be expanded.");
is(localScope.target.querySelectorAll(".variables-view-variable:not([non-match]) > .title > .name")[3].getAttribute("value"),
is(localScope.target.querySelectorAll(".variables-view-variable:not([unmatched]) > .title > .name")[3].getAttribute("value"),
"__proto__", "The __proto__ reference should be visible.");
is(localScope.get("__proto__").expanded, false,
"The __proto__ reference in localScope should not be expanded.");
is(functionScope.target.querySelectorAll(".variables-view-variable:not([non-match]) > .title > .name")[0].getAttribute("value"),
is(functionScope.target.querySelectorAll(".variables-view-variable:not([unmatched]) > .title > .name")[0].getAttribute("value"),
"aNumber", "The aNumber param should be visible.");
is(functionScope.get("aNumber").expanded, false,
"The aNumber param in functionScope should not be expanded.");
is(functionScope.target.querySelectorAll(".variables-view-variable:not([non-match]) > .title > .name")[1].getAttribute("value"),
is(functionScope.target.querySelectorAll(".variables-view-variable:not([unmatched]) > .title > .name")[1].getAttribute("value"),
"a", "The a variable should be visible.");
is(functionScope.get("a").expanded, false,
"The a variable in functionScope should not be expanded.");
is(functionScope.target.querySelectorAll(".variables-view-variable:not([non-match]) > .title > .name")[2].getAttribute("value"),
is(functionScope.target.querySelectorAll(".variables-view-variable:not([unmatched]) > .title > .name")[2].getAttribute("value"),
"arguments", "The arguments pseudoarray should be visible.");
is(functionScope.get("arguments").expanded, false,
"The arguments pseudoarray in functionScope should not be expanded.");
@ -162,19 +162,19 @@ function testVariablesAndPropertiesFiltering() {
}
function assertVariablesCountAtLeast(aCounts) {
ok(localScope.target.querySelectorAll(".variables-view-variable:not([non-match])").length >= aCounts[0],
ok(localScope.target.querySelectorAll(".variables-view-variable:not([unmatched])").length >= aCounts[0],
"There should be " + aCounts[0] +
" variable displayed in the local scope (" + step + ").");
ok(withScope.target.querySelectorAll(".variables-view-variable:not([non-match])").length >= aCounts[1],
ok(withScope.target.querySelectorAll(".variables-view-variable:not([unmatched])").length >= aCounts[1],
"There should be " + aCounts[1] +
" variable displayed in the with scope (" + step + ").");
ok(functionScope.target.querySelectorAll(".variables-view-variable:not([non-match])").length >= aCounts[2],
ok(functionScope.target.querySelectorAll(".variables-view-variable:not([unmatched])").length >= aCounts[2],
"There should be " + aCounts[2] +
" variable displayed in the function scope (" + step + ").");
ok(globalScope.target.querySelectorAll(".variables-view-variable:not([non-match])").length >= aCounts[3],
ok(globalScope.target.querySelectorAll(".variables-view-variable:not([unmatched])").length >= aCounts[3],
"There should be " + aCounts[3] +
" variable displayed in the global scope (" + step + ").");

View File

@ -19,9 +19,9 @@ function test() {
is(tooltip.querySelectorAll(".variables-view-container").length, 1,
"There should be one variables view container added to the tooltip.");
is(tooltip.querySelectorAll(".variables-view-scope[non-header]").length, 1,
is(tooltip.querySelectorAll(".variables-view-scope[untitled]").length, 1,
"There should be one scope with no header displayed.");
is(tooltip.querySelectorAll(".variables-view-variable[non-header]").length, 1,
is(tooltip.querySelectorAll(".variables-view-variable[untitled]").length, 1,
"There should be one variable with no header displayed.");
is(tooltip.querySelectorAll(".variables-view-property").length, 2,

View File

@ -19,9 +19,9 @@ function test() {
is(tooltip.querySelectorAll(".variables-view-container").length, 1,
"There should be one variables view container added to the tooltip.");
is(tooltip.querySelectorAll(".variables-view-scope[non-header]").length, 1,
is(tooltip.querySelectorAll(".variables-view-scope[untitled]").length, 1,
"There should be one scope with no header displayed.");
is(tooltip.querySelectorAll(".variables-view-variable[non-header]").length, 1,
is(tooltip.querySelectorAll(".variables-view-variable[untitled]").length, 1,
"There should be one variable with no header displayed.");
is(tooltip.querySelectorAll(".variables-view-property").length, 7,

View File

@ -33,9 +33,9 @@ function test() {
is(tooltip.querySelectorAll(".devtools-tooltip-simple-text").length, 0,
"There should be no simple text node added to the tooltip.");
is(tooltip.querySelectorAll(".variables-view-scope[non-header]").length, 1,
is(tooltip.querySelectorAll(".variables-view-scope[untitled]").length, 1,
"There should be one scope with no header displayed.");
is(tooltip.querySelectorAll(".variables-view-variable[non-header]").length, 1,
is(tooltip.querySelectorAll(".variables-view-variable[untitled]").length, 1,
"There should be one variable with no header displayed.");
ok(tooltip.querySelectorAll(".variables-view-property").length >= propertyCount,

View File

@ -1476,7 +1476,7 @@ Scope.prototype = {
if (this._isHeaderVisible || !this._nameString) {
return;
}
this._target.removeAttribute("non-header");
this._target.removeAttribute("untitled");
this._isHeaderVisible = true;
},
@ -1489,7 +1489,7 @@ Scope.prototype = {
return;
}
this.expand();
this._target.setAttribute("non-header", "");
this._target.setAttribute("untitled", "");
this._isHeaderVisible = false;
},
@ -1928,10 +1928,10 @@ Scope.prototype = {
}
if (aStatus) {
this._isMatch = true;
this.target.removeAttribute("non-match");
this.target.removeAttribute("unmatched");
} else {
this._isMatch = false;
this.target.setAttribute("non-match", "");
this.target.setAttribute("unmatched", "");
}
},

View File

@ -50,9 +50,9 @@
overflow: hidden;
}
.variables-view-scope[non-header] > .title,
.variable-or-property[non-header] > .title,
.variable-or-property[non-match] > .title {
.variables-view-scope[untitled] > .title,
.variable-or-property[untitled] > .title,
.variable-or-property[unmatched] > .title {
display: none;
}

View File

@ -308,10 +308,11 @@ var BrowserUI = {
isStartURI: function isStartURI(aURI) {
aURI = aURI || Browser.selectedBrowser.currentURI.spec;
return aURI == kStartURI;
return aURI == kStartURI || aURI == "about:home";
},
updateStartURIAttributes: function (aURI) {
let wasStart = Elements.windowState.hasAttribute("startpage");
aURI = aURI || Browser.selectedBrowser.currentURI.spec;
if (this.isStartURI(aURI)) {
ContextUI.displayNavbar();
@ -319,6 +320,13 @@ var BrowserUI = {
} else if (aURI != "about:blank") { // about:blank is loaded briefly for new tabs; ignore it
Elements.windowState.removeAttribute("startpage");
}
let isStart = Elements.windowState.hasAttribute("startpage");
if (wasStart != isStart) {
let event = document.createEvent("Events");
event.initEvent("StartUIChange", true, true);
Browser.selectedBrowser.dispatchEvent(event);
}
},
getDisplayURI: function(browser) {

View File

@ -1305,6 +1305,7 @@ Tab.prototype = {
}
browser.addEventListener("pageshow", onPageShowEvent, true);
browser.addEventListener("DOMWindowCreated", this, false);
browser.addEventListener("StartUIChange", this, false);
Elements.browsers.addEventListener("SizeChanged", this, false);
browser.messageManager.addMessageListener("Content:StateChange", this);
@ -1316,12 +1317,19 @@ Tab.prototype = {
updateViewport: function (aEvent) {
// <meta name=viewport> is not yet supported; just use the browser size.
this.browser.setWindowSize(this.browser.clientWidth, this.browser.clientHeight);
let browser = this.browser;
// On the start page we add padding to keep the browser above the navbar.
let paddingBottom = parseInt(getComputedStyle(browser).paddingBottom, 10);
let height = browser.clientHeight - paddingBottom;
browser.setWindowSize(browser.clientWidth, height);
},
handleEvent: function (aEvent) {
switch (aEvent.type) {
case "DOMWindowCreated":
case "StartUIChange":
this.updateViewport();
break;
case "SizeChanged":
@ -1354,6 +1362,7 @@ Tab.prototype = {
destroy: function destroy() {
this._browser.messageManager.removeMessageListener("Content:StateChange", this);
this._browser.removeEventListener("DOMWindowCreated", this, false);
this._browser.removeEventListener("StartUIChange", this, false);
Elements.browsers.removeEventListener("SizeChanged", this, false);
clearTimeout(this._updateThumbnailTimeout);

View File

@ -467,7 +467,7 @@
cursor: text;
}
.variable-or-property:not([non-header]) > .variables-view-element-details {
.variable-or-property:not([untitled]) > .variables-view-element-details {
-moz-margin-start: 10px;
}
@ -632,7 +632,7 @@
min-height: 24px;
}
.variable-or-property[non-match] {
.variable-or-property[unmatched] {
border: none;
margin: 0;
}

View File

@ -461,7 +461,7 @@
cursor: text;
}
.variable-or-property:not([non-header]) > .variables-view-element-details {
.variable-or-property:not([untitled]) > .variables-view-element-details {
-moz-margin-start: 10px;
}
@ -626,7 +626,7 @@
min-height: 24px;
}
.variable-or-property[non-match] {
.variable-or-property[unmatched] {
border: none;
margin: 0;
}

View File

@ -464,7 +464,7 @@
cursor: text;
}
.variable-or-property:not([non-header]) > .variables-view-element-details {
.variable-or-property:not([untitled]) > .variables-view-element-details {
-moz-margin-start: 10px;
}
@ -629,7 +629,7 @@
min-height: 24px;
}
.variable-or-property[non-match] {
.variable-or-property[unmatched] {
border: none;
margin: 0;
}

View File

@ -91,10 +91,14 @@ classes.dex: proguard-jars
@echo 'DX classes.dex'
$(DX) --dex --output=classes.dex jars-proguarded $(ANDROID_COMPAT_LIB)
ifdef MOZ_DEBUG
PROGUARD_PASSES=1
ifdef MOZ_DISABLE_PROGUARD
PROGUARD_PASSES=0
else
PROGUARD_PASSES=6
ifdef MOZ_DEBUG
PROGUARD_PASSES=1
else
PROGUARD_PASSES=6
endif
endif
proguard-jars: $(ALL_JARS)

View File

@ -19,6 +19,7 @@ import org.mozilla.gecko.db.BrowserContract.Schema;
import org.mozilla.gecko.db.BrowserContract.SyncColumns;
import org.mozilla.gecko.db.BrowserContract.Thumbnails;
import org.mozilla.gecko.db.BrowserContract.URLColumns;
import org.mozilla.gecko.db.PerProfileDatabases.DatabaseHelperFactory;
import org.mozilla.gecko.gfx.BitmapUtils;
import org.mozilla.gecko.mozglue.RobocopTarget;
import org.mozilla.gecko.sync.Utils;
@ -67,6 +68,8 @@ public class BrowserProvider extends ContentProvider {
private static final String LOGTAG = "GeckoBrowserProvider";
private Context mContext;
private PerProfileDatabases<BrowserDatabaseHelper> mDatabases;
static final String DATABASE_NAME = "browser.db";
static final int DATABASE_VERSION = 17;
@ -313,8 +316,6 @@ public class BrowserProvider extends ContentProvider {
SEARCH_SUGGEST_PROJECTION_MAP = Collections.unmodifiableMap(map);
}
private HashMap<String, DatabaseHelper> mDatabasePerProfile;
private interface BookmarkMigrator {
public void updateForNewTable(ContentValues bookmark);
}
@ -364,8 +365,8 @@ public class BrowserProvider extends ContentProvider {
}
}
final class DatabaseHelper extends SQLiteOpenHelper {
public DatabaseHelper(Context context, String databasePath) {
final class BrowserDatabaseHelper extends SQLiteOpenHelper {
public BrowserDatabaseHelper(Context context, String databasePath) {
super(context, databasePath, null, DATABASE_VERSION);
}
@ -1953,63 +1954,9 @@ public class BrowserProvider extends ContentProvider {
}
}
private DatabaseHelper getDatabaseHelperForProfile(String profile, boolean isTest) {
// Each profile has a separate browser.db database. The target
// profile is provided using a URI query argument in each request
// to our content provider.
// Always fallback to default profile if none has been provided.
if (TextUtils.isEmpty(profile)) {
profile = GeckoProfile.get(mContext).getName();
}
DatabaseHelper dbHelper;
synchronized (this) {
dbHelper = mDatabasePerProfile.get(profile);
if (dbHelper != null) {
return dbHelper;
}
String databasePath = getDatabasePath(profile, isTest);
// Before bug 768532, the database was located outside if the
// profile on Android 2.2. Make sure it is moved inside the profile
// directory.
if (Build.VERSION.SDK_INT == 8) {
File oldPath = mContext.getDatabasePath("browser-" + profile + ".db");
if (oldPath.exists()) {
oldPath.renameTo(new File(databasePath));
}
}
dbHelper = new DatabaseHelper(getContext(), databasePath);
mDatabasePerProfile.put(profile, dbHelper);
DBUtils.ensureDatabaseIsNotLocked(dbHelper, databasePath);
}
debug("Created database helper for profile: " + profile);
return dbHelper;
}
@RobocopTarget
public String getDatabasePath(String profile, boolean isTest) {
trace("Getting database path for profile: " + profile);
if (isTest) {
return DATABASE_NAME;
}
File profileDir = GeckoProfile.get(mContext, profile).getDir();
if (profileDir == null) {
debug("Couldn't find directory for profile: " + profile);
return null;
}
String databasePath = new File(profileDir, DATABASE_NAME).getAbsolutePath();
debug("Successfully created database path for profile: " + databasePath);
return databasePath;
return mDatabases.getDatabasePathForProfile(profile, isTest);
}
private SQLiteDatabase getReadableDatabase(Uri uri) {
@ -2020,7 +1967,7 @@ public class BrowserProvider extends ContentProvider {
if (uri != null)
profile = uri.getQueryParameter(BrowserContract.PARAM_PROFILE);
return getDatabaseHelperForProfile(profile, isTest(uri)).getReadableDatabase();
return mDatabases.getDatabaseHelperForProfile(profile, isTest(uri)).getReadableDatabase();
}
private SQLiteDatabase getWritableDatabase(Uri uri) {
@ -2031,7 +1978,7 @@ public class BrowserProvider extends ContentProvider {
if (uri != null)
profile = uri.getQueryParameter(BrowserContract.PARAM_PROFILE);
return getDatabaseHelperForProfile(profile, isTest(uri)).getWritableDatabase();
return mDatabases.getDatabaseHelperForProfile(profile, isTest(uri)).getWritableDatabase();
}
private void cleanupSomeDeletedRecords(Uri fromUri, Uri targetUri, String tableName) {
@ -2178,7 +2125,13 @@ public class BrowserProvider extends ContentProvider {
synchronized (this) {
mContext = getContext();
mDatabasePerProfile = new HashMap<String, DatabaseHelper>();
mDatabases = new PerProfileDatabases<BrowserDatabaseHelper>(
getContext(), DATABASE_NAME, new DatabaseHelperFactory<BrowserDatabaseHelper>() {
@Override
public BrowserDatabaseHelper makeDatabaseHelper(Context context, String databasePath) {
return new BrowserDatabaseHelper(context, databasePath);
}
});
}
return true;

View File

@ -0,0 +1,81 @@
/* 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.db;
import java.io.File;
import java.util.HashMap;
import org.mozilla.gecko.GeckoProfile;
import android.content.Context;
import android.database.sqlite.SQLiteOpenHelper;
import android.text.TextUtils;
/**
* Manages a set of per-profile database storage helpers.
*/
public class PerProfileDatabases<T extends SQLiteOpenHelper> {
private final HashMap<String, T> mStorages = new HashMap<String, T>();
private final Context mContext;
private final String mDatabaseName;
private final DatabaseHelperFactory<T> mHelperFactory;
public interface DatabaseHelperFactory<T> {
public T makeDatabaseHelper(Context context, String databasePath);
}
public PerProfileDatabases(final Context context, final String databaseName, final DatabaseHelperFactory<T> helperFactory) {
mContext = context;
mDatabaseName = databaseName;
mHelperFactory = helperFactory;
}
public String getDatabasePathForProfile(String profile) {
return getDatabasePathForProfile(profile, false);
}
public String getDatabasePathForProfile(String profile, boolean isTest) {
if (isTest) {
return mDatabaseName;
}
final File profileDir = GeckoProfile.get(mContext, profile).getDir();
if (profileDir == null) {
return null;
}
return new File(profileDir, mDatabaseName).getAbsolutePath();
}
public T getDatabaseHelperForProfile(String profile) {
return getDatabaseHelperForProfile(profile, false);
}
public T getDatabaseHelperForProfile(String profile, boolean isTest) {
// Always fall back to default profile if none has been provided.
if (TextUtils.isEmpty(profile)) {
profile = GeckoProfile.get(mContext).getName();
}
synchronized (this) {
if (mStorages.containsKey(profile)) {
return mStorages.get(profile);
}
final String databasePath = getDatabasePathForProfile(profile, isTest);
if (databasePath == null) {
throw new IllegalStateException("Database path is null for profile: " + profile);
}
final T helper = mHelperFactory.makeDatabaseHelper(mContext, databasePath);
DBUtils.ensureDatabaseIsNotLocked(helper, databasePath);
mStorages.put(profile, helper);
return helper;
}
}
}

View File

@ -12,6 +12,7 @@ import java.util.Map;
import org.mozilla.gecko.GeckoProfile;
import org.mozilla.gecko.db.BrowserContract.Clients;
import org.mozilla.gecko.db.BrowserContract.Tabs;
import org.mozilla.gecko.db.PerProfileDatabases.DatabaseHelperFactory;
import org.mozilla.gecko.mozglue.RobocopTarget;
import android.content.ContentProvider;
@ -33,6 +34,8 @@ public class TabsProvider extends ContentProvider {
private static final String LOGTAG = "GeckoTabsProvider";
private Context mContext;
private PerProfileDatabases<TabsDatabaseHelper> mDatabases;
static final String DATABASE_NAME = "tabs.db";
static final int DATABASE_VERSION = 2;
@ -85,8 +88,6 @@ public class TabsProvider extends ContentProvider {
CLIENTS_PROJECTION_MAP = Collections.unmodifiableMap(map);
}
private HashMap<String, DatabaseHelper> mDatabasePerProfile;
static final String selectColumn(String table, String column) {
return table + "." + column + " = ?";
}
@ -107,8 +108,8 @@ public class TabsProvider extends ContentProvider {
}
}
final class DatabaseHelper extends SQLiteOpenHelper {
public DatabaseHelper(Context context, String databasePath) {
final class TabsDatabaseHelper extends SQLiteOpenHelper {
public TabsDatabaseHelper(Context context, String databasePath) {
super(context, databasePath, null, DATABASE_VERSION);
}
@ -207,59 +208,9 @@ public class TabsProvider extends ContentProvider {
}
}
private DatabaseHelper getDatabaseHelperForProfile(String profile) {
// Each profile has a separate tabs.db database. The target
// profile is provided using a URI query argument in each request
// to our content provider.
// Always fallback to default profile if none has been provided.
if (TextUtils.isEmpty(profile)) {
profile = GeckoProfile.get(getContext()).getName();
}
DatabaseHelper dbHelper;
synchronized (this) {
dbHelper = mDatabasePerProfile.get(profile);
if (dbHelper != null) {
return dbHelper;
}
String databasePath = getDatabasePath(profile);
// Before bug 768532, the database was located outside if the
// profile on Android 2.2. Make sure it is moved inside the profile
// directory.
if (Build.VERSION.SDK_INT == 8) {
File oldPath = mContext.getDatabasePath("tabs-" + profile + ".db");
if (oldPath.exists()) {
oldPath.renameTo(new File(databasePath));
}
}
dbHelper = new DatabaseHelper(getContext(), databasePath);
mDatabasePerProfile.put(profile, dbHelper);
DBUtils.ensureDatabaseIsNotLocked(dbHelper, databasePath);
}
debug("Created database helper for profile: " + profile);
return dbHelper;
}
@RobocopTarget
private String getDatabasePath(String profile) {
trace("Getting database path for profile: " + profile);
File profileDir = GeckoProfile.get(mContext, profile).getDir();
if (profileDir == null) {
debug("Couldn't find directory for profile: " + profile);
return null;
}
String databasePath = new File(profileDir, DATABASE_NAME).getAbsolutePath();
debug("Successfully created database path for profile: " + databasePath);
return databasePath;
return mDatabases.getDatabasePathForProfile(profile);
}
private SQLiteDatabase getReadableDatabase(Uri uri) {
@ -270,7 +221,7 @@ public class TabsProvider extends ContentProvider {
if (uri != null)
profile = uri.getQueryParameter(BrowserContract.PARAM_PROFILE);
return getDatabaseHelperForProfile(profile).getReadableDatabase();
return mDatabases.getDatabaseHelperForProfile(profile).getReadableDatabase();
}
private SQLiteDatabase getWritableDatabase(Uri uri) {
@ -281,7 +232,7 @@ public class TabsProvider extends ContentProvider {
if (uri != null)
profile = uri.getQueryParameter(BrowserContract.PARAM_PROFILE);
return getDatabaseHelperForProfile(profile).getWritableDatabase();
return mDatabases.getDatabaseHelperForProfile(profile).getWritableDatabase();
}
@Override
@ -290,7 +241,13 @@ public class TabsProvider extends ContentProvider {
synchronized (this) {
mContext = getContext();
mDatabasePerProfile = new HashMap<String, DatabaseHelper>();
mDatabases = new PerProfileDatabases<TabsDatabaseHelper>(
getContext(), DATABASE_NAME, new DatabaseHelperFactory<TabsDatabaseHelper>() {
@Override
public TabsDatabaseHelper makeDatabaseHelper(Context context, String databasePath) {
return new TabsDatabaseHelper(context, databasePath);
}
});
}
return true;

View File

@ -117,6 +117,7 @@ gbjar.sources += [
'db/LocalBrowserDB.java',
'db/PasswordsProvider.java',
'db/PerProfileContentProvider.java',
'db/PerProfileDatabases.java',
'db/TabsProvider.java',
'Distribution.java',
'DoorHanger.java',

View File

@ -261,19 +261,22 @@ BreakpointStore.prototype = {
* @param nsIJSInspector inspector
* The underlying JS inspector we use to enter and exit nested event
* loops.
* @param ThreadActor thread
* The thread actor instance that owns this EventLoopStack.
* @param DebuggerServerConnection connection
* The remote protocol connection associated with this event loop stack.
* @param Object hooks
* An object with the following properties:
* - url: The URL string of the debuggee we are spinning an event loop
* for.
* - preNest: function called before entering a nested event loop
* - postNest: function called after exiting a nested event loop
* @param ThreadActor thread
* The thread actor instance that owns this EventLoopStack.
*/
function EventLoopStack({ inspector, thread, hooks }) {
function EventLoopStack({ inspector, thread, connection, hooks }) {
this._inspector = inspector;
this._hooks = hooks;
this._thread = thread;
this._connection = connection;
}
EventLoopStack.prototype = {
@ -301,6 +304,14 @@ EventLoopStack.prototype = {
return url;
},
/**
* The DebuggerServerConnection of the debugger who pushed the event loop on
* top of the stack
*/
get lastConnection() {
return this._inspector.lastNestRequestor._connection;
},
/**
* Push a new nested event loop onto the stack.
*
@ -310,6 +321,7 @@ EventLoopStack.prototype = {
return new EventLoop({
inspector: this._inspector,
thread: this._thread,
connection: this._connection,
hooks: this._hooks
});
}
@ -323,14 +335,17 @@ EventLoopStack.prototype = {
* The JS Inspector that runs nested event loops.
* @param ThreadActor thread
* The thread actor that is creating this nested event loop.
* @param DebuggerServerConnection connection
* The remote protocol connection associated with this event loop.
* @param Object hooks
* The same hooks object passed into EventLoopStack during its
* initialization.
*/
function EventLoop({ inspector, thread, hooks }) {
function EventLoop({ inspector, thread, connection, hooks }) {
this._inspector = inspector;
this._thread = thread;
this._hooks = hooks;
this._connection = connection;
this.enter = this.enter.bind(this);
this.resolve = this.resolve.bind(this);
@ -414,11 +429,6 @@ function ThreadActor(aHooks, aGlobal)
this._frameActors = [];
this._hooks = aHooks;
this.global = aGlobal;
this._nestedEventLoops = new EventLoopStack({
inspector: DebuggerServer.xpcInspector,
hooks: aHooks,
thread: this
});
// A map of actorID -> actor for breakpoints created and managed by the server.
this._hiddenBreakpoints = new Map();
@ -675,6 +685,15 @@ ThreadActor.prototype = {
update(this._options, aRequest.options || {});
// Initialize an event loop stack. This can't be done in the constructor,
// because this.conn is not yet initialized by the actor pool at that time.
this._nestedEventLoops = new EventLoopStack({
inspector: DebuggerServer.xpcInspector,
hooks: this._hooks,
connection: this.conn,
thread: this
});
if (!this.dbg) {
this._initDebugger();
}
@ -992,7 +1011,8 @@ ThreadActor.prototype = {
// different tabs or multiple debugger clients connected to the same tab)
// only allow resumption in a LIFO order.
if (this._nestedEventLoops.size && this._nestedEventLoops.lastPausedUrl
&& this._nestedEventLoops.lastPausedUrl !== this._hooks.url) {
&& (this._nestedEventLoops.lastPausedUrl !== this._hooks.url
|| this._nestedEventLoops.lastConnection !== this.conn)) {
return {
error: "wrongOrder",
message: "trying to resume in the wrong order.",

View File

@ -72,7 +72,8 @@ PackageUploadActor.prototype = {
* and also be locked.
*/
done: function (aRequest) {
return this._file.close();
this._file.close();
return {};
},
/**
@ -81,7 +82,6 @@ PackageUploadActor.prototype = {
*/
remove: function (aRequest) {
this._cleanupFile();
return {};
},

View File

@ -6,15 +6,26 @@
// ThreadActor.prototype.synchronize.
const { defer } = devtools.require("sdk/core/promise");
var gClient;
var gThreadActor;
function run_test() {
initTestDebuggerServer();
let gDebuggee = addTestGlobal("test-nesting");
gClient = new DebuggerClient(DebuggerServer.connectPipe());
gClient.connect(function () {
attachTestTabAndResume(gClient, "test-nesting", function (aResponse, aTabClient, aThreadClient) {
// Reach over the protocol connection and get a reference to the thread actor.
gThreadActor = aThreadClient._transport._serverConnection.getActor(aThreadClient._actor);
test_nesting();
});
});
do_test_pending();
test_nesting();
}
function test_nesting() {
const thread = new DebuggerServer.ThreadActor(DebuggerServer);
const thread = gThreadActor;
const { resolve, reject, promise } = defer();
let currentStep = 0;
@ -34,5 +45,5 @@ function test_nesting() {
// There shouldn't be any nested event loops anymore
do_check_eq(thread._nestedEventLoops.size, 0);
do_test_finished();
finishClient(gClient);
}

View File

@ -6,15 +6,26 @@
// loops when requested.
const { defer } = devtools.require("sdk/core/promise");
var gClient;
var gThreadActor;
function run_test() {
initTestDebuggerServer();
let gDebuggee = addTestGlobal("test-nesting");
gClient = new DebuggerClient(DebuggerServer.connectPipe());
gClient.connect(function () {
attachTestTabAndResume(gClient, "test-nesting", function (aResponse, aTabClient, aThreadClient) {
// Reach over the protocol connection and get a reference to the thread actor.
gThreadActor = aThreadClient._transport._serverConnection.getActor(aThreadClient._actor);
test_nesting();
});
});
do_test_pending();
test_nesting();
}
function test_nesting() {
const thread = new DebuggerServer.ThreadActor(DebuggerServer);
const thread = gThreadActor;
const { resolve, reject, promise } = defer();
// The following things should happen (in order):
@ -64,5 +75,5 @@ function test_nesting() {
// There shouldn't be any nested event loops anymore
do_check_eq(thread._nestedEventLoops.size, 0);
do_test_finished();
finishClient(gClient);
}

View File

@ -0,0 +1,50 @@
/* -*- Mode: javascript; js-indent-level: 2; -*- */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
// Test that we can detect nested event loops in tabs with the same URL.
const { defer } = devtools.require("sdk/core/promise");
var gClient1, gClient2;
function run_test() {
initTestDebuggerServer();
addTestGlobal("test-nesting1");
addTestGlobal("test-nesting1");
// Conect the first client to the first debuggee.
gClient1 = new DebuggerClient(DebuggerServer.connectPipe());
gClient1.connect(function () {
attachTestThread(gClient1, "test-nesting1", function (aResponse, aTabClient, aThreadClient) {
start_second_connection();
});
});
do_test_pending();
}
function start_second_connection() {
gClient2 = new DebuggerClient(DebuggerServer.connectPipe());
gClient2.connect(function () {
attachTestThread(gClient2, "test-nesting1", function (aResponse, aTabClient, aThreadClient) {
test_nesting();
});
});
}
function test_nesting() {
const { resolve, reject, promise } = defer();
gClient1.activeThread.resume(aResponse => {
do_check_eq(aResponse.error, "wrongOrder");
gClient2.activeThread.resume(aResponse => {
do_check_true(!aResponse.error);
do_check_eq(aResponse.from, gClient2.activeThread.actor);
gClient1.activeThread.resume(aResponse => {
do_check_true(!aResponse.error);
do_check_eq(aResponse.from, gClient1.activeThread.actor);
gClient1.close(() => finishClient(gClient2));
});
});
});
}

View File

@ -71,6 +71,10 @@ TestTabActor.prototype = {
return { wrappedJSObject: this._global };
},
get url() {
return this._global.__name;
},
form: function() {
let response = { actor: this.actorID, title: this._global.__name };

View File

@ -18,6 +18,7 @@ support-files =
[test_nesting-01.js]
[test_nesting-02.js]
[test_nesting-03.js]
[test_forwardingprefix.js]
[test_getyoungestframe.js]
[test_nsjsinspector.js]