merge fx-team to mozilla-central

This commit is contained in:
Carsten "Tomcat" Book 2014-04-16 14:52:39 +02:00
commit b2148d7f1c
46 changed files with 13652 additions and 11406 deletions

View File

@ -37,7 +37,7 @@ let gGrid = {
get sites() [cell.site for each (cell in this.cells)],
// Tells whether the grid has already been initialized.
get ready() !!this._node,
get ready() !!this._ready,
/**
* Initializes the grid.
@ -46,7 +46,11 @@ let gGrid = {
init: function Grid_init() {
this._node = document.getElementById("newtab-grid");
this._createSiteFragment();
this._render();
this._renderGrid();
gLinks.populateCache(() => {
this._renderSites();
this._ready = true;
});
addEventListener("load", this);
addEventListener("resize", this);
},
@ -69,14 +73,6 @@ let gGrid = {
handleEvent: function Grid_handleEvent(aEvent) {
switch (aEvent.type) {
case "load":
// Save the cell's computed height/width including margin and border
let refCell = document.querySelector(".newtab-cell");
this._cellMargin = parseFloat(getComputedStyle(refCell).marginTop) * 2;
this._cellHeight = refCell.offsetHeight + this._cellMargin;
this._cellWidth = refCell.offsetWidth + this._cellMargin;
this._resizeGrid();
break;
case "resize":
this._resizeGrid();
break;
@ -97,8 +93,11 @@ let gGrid = {
}, this);
// Render the grid again.
this._render();
this._resizeGrid();
if (this._shouldRenderGrid()) {
this._renderGrid();
this._resizeGrid();
}
this._renderSites();
},
/**
@ -185,21 +184,18 @@ let gGrid = {
}
},
/**
* Renders the grid.
*/
_render: function Grid_render() {
if (this._shouldRenderGrid()) {
this._renderGrid();
}
this._renderSites();
},
/**
* Make sure the correct number of rows and columns are visible
*/
_resizeGrid: function Grid_resizeGrid() {
// Save the cell's computed height/width including margin and border
if (this._cellMargin === undefined) {
let refCell = document.querySelector(".newtab-cell");
this._cellMargin = parseFloat(getComputedStyle(refCell).marginTop) * 2;
this._cellHeight = refCell.offsetHeight + this._cellMargin;
this._cellWidth = refCell.offsetWidth + this._cellMargin;
}
let availSpace = document.documentElement.clientHeight - this._cellMargin -
document.querySelector("#newtab-margin-top").offsetHeight;
let visibleRows = Math.floor(availSpace / this._cellHeight);

View File

@ -144,19 +144,17 @@ let gPage = {
attributeFilter: ["allow-background-captures"],
});
gLinks.populateCache(function () {
// Initialize and render the grid.
gGrid.init();
// Initialize and render the grid.
gGrid.init();
// Initialize the drop target shim.
gDropTargetShim.init();
// Initialize the drop target shim.
gDropTargetShim.init();
#ifdef XP_MACOSX
// Workaround to prevent a delay on MacOSX due to a slow drop animation.
document.addEventListener("dragover", this, false);
document.addEventListener("drop", this, false);
// Workaround to prevent a delay on MacOSX due to a slow drop animation.
document.addEventListener("dragover", this, false);
document.addEventListener("drop", this, false);
#endif
}.bind(this));
},
/**

View File

@ -169,7 +169,10 @@ Site.prototype = {
this._node.addEventListener("dragstart", this, false);
this._node.addEventListener("dragend", this, false);
this._node.addEventListener("mouseover", this, false);
this._node.addEventListener("click", this, false);
// XXX bug 991111 - Not all click events are correctly triggered when
// listening from the xhtml node, so listen from the xul window and filter
addEventListener("click", this, false);
// Specially treat the sponsored icon to prevent regular hover effects
let sponsored = this._querySelector(".newtab-control-sponsored");
@ -240,7 +243,11 @@ Site.prototype = {
handleEvent: function Site_handleEvent(aEvent) {
switch (aEvent.type) {
case "click":
this._onClick(aEvent);
// Check the bitmask if the click event is for the site's descendants
if (this._node.compareDocumentPosition(aEvent.target) &
this._node.DOCUMENT_POSITION_CONTAINED_BY) {
this._onClick(aEvent);
}
break;
case "mouseover":
this._node.removeEventListener("mouseover", this, false);

View File

@ -15,6 +15,8 @@ skip-if = os == "mac" # Intermittent failures, bug 898317
[browser_newtab_bug752841.js]
[browser_newtab_bug765628.js]
[browser_newtab_bug876313.js]
[browser_newtab_bug991111.js]
[browser_newtab_bug991210.js]
[browser_newtab_disable.js]
[browser_newtab_drag_drop.js]
[browser_newtab_drag_drop_ext.js]

View File

@ -0,0 +1,19 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
function runTests() {
yield setLinks("0");
yield addNewTabPageTab();
// Remember if the click handler was triggered
let cell = getCell(0);
let clicked = false;
cell.site._onClick = e => {
clicked = true;
executeSoon(TestRunner.next);
};
// Send a middle-click and make sure it happened
yield EventUtils.synthesizeMouseAtCenter(cell.node, {button: 1}, getContentWindow());
ok(clicked, "middle click triggered click listener");
}

View File

@ -0,0 +1,41 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
const PRELOAD_PREF = "browser.newtab.preload";
function runTests() {
// turn off preload to ensure that a newtab page loads
Services.prefs.setBoolPref(PRELOAD_PREF, false);
// add a test provider that waits for load
let afterLoadProvider = {
getLinks: function(callback) {
this.callback = callback;
},
addObserver: function() {},
};
NewTabUtils.links.addProvider(afterLoadProvider);
// wait until about:newtab loads before calling provider callback
addNewTabPageTab();
let browser = gWindow.gBrowser.selectedTab.linkedBrowser;
yield browser.addEventListener("load", function onLoad() {
browser.removeEventListener("load", onLoad, true);
// afterLoadProvider.callback has to be called asynchronously to make grid
// initilize after "load" event was handled
executeSoon(() => afterLoadProvider.callback([]));
}, true);
let {_cellMargin, _cellHeight, _cellWidth, node} = getGrid();
isnot(_cellMargin, null, "grid has a computed cell margin");
isnot(_cellHeight, null, "grid has a computed cell height");
isnot(_cellWidth, null, "grid has a computed cell width");
let {height, maxHeight, maxWidth} = node.style;
isnot(height, "", "grid has a computed grid height");
isnot(maxHeight, "", "grid has a computed grid max-height");
isnot(maxWidth, "", "grid has a computed grid max-width");
// restore original state
NewTabUtils.links.removeProvider(afterLoadProvider);
Services.prefs.clearUserPref(PRELOAD_PREF);
}

View File

@ -6,11 +6,6 @@
*/
function runTests() {
if (NewTabUtils.allPages.updateScheduledForHiddenPages) {
// Wait for dynamic updates triggered by the previous test to finish.
yield whenPagesUpdated(null, true);
}
// First, start with an empty page. setLinks will trigger a hidden page
// update because it calls clearHistory. We need to wait for that update to
// happen so that the next time we wait for a page update below, we catch the

View File

@ -14,7 +14,8 @@ Cu.import("resource://gre/modules/NewTabUtils.jsm", tmp);
Cc["@mozilla.org/moz/jssubscript-loader;1"]
.getService(Ci.mozIJSSubScriptLoader)
.loadSubScript("chrome://browser/content/sanitize.js", tmp);
let {Promise, NewTabUtils, Sanitizer} = tmp;
Cu.import("resource://gre/modules/Timer.jsm", tmp);
let {Promise, NewTabUtils, Sanitizer, clearTimeout} = tmp;
let uri = Services.io.newURI("about:newtab", null, null);
let principal = Services.scriptSecurityManager.getNoAppCodebasePrincipal(uri);
@ -30,6 +31,13 @@ registerCleanupFunction(function () {
Services.prefs.clearUserPref(PREF_NEWTAB_ENABLED);
Services.prefs.clearUserPref(PREF_NEWTAB_DIRECTORYSOURCE);
// Stop any update timers to prevent unexpected updates in later tests
let timer = NewTabUtils.allPages._scheduleUpdateTimeout;
if (timer) {
clearTimeout(timer);
delete NewTabUtils.allPages._scheduleUpdateTimeout;
}
});
/**

View File

@ -36,14 +36,12 @@ function test()
// Make sure the body element is selected initially.
node = doc.querySelector("body");
inspector.once("inspector-updated", () => {
is(inspector.selection.node, node, "Body should be selected initially.");
node = doc.querySelector("h1")
inspector.once("inspector-updated", highlightHeaderNode);
let bc = inspector.breadcrumbs;
bc.nodeHierarchy[bc.currentIndex].button.focus();
EventUtils.synthesizeKey("VK_RIGHT", { });
});
is(inspector.selection.node, node, "Body should be selected initially.");
node = doc.querySelector("h1")
inspector.once("inspector-updated", highlightHeaderNode);
let bc = inspector.breadcrumbs;
bc.nodeHierarchy[bc.currentIndex].button.focus();
EventUtils.synthesizeKey("VK_RIGHT", {});
}
function highlightHeaderNode()

View File

@ -25,7 +25,7 @@ function test() {
openInspector((aInspector, aToolbox) => {
inspector = aInspector;
markupView = inspector.markup;
inspector.once("inspector-updated", startTests);
startTests();
});
}, content);
}, true);

View File

@ -35,9 +35,7 @@ function createDocument() {
// Open the inspector, start the picker mode, and start the tests
openInspector(aInspector => {
inspector = aInspector;
inspector.once("inspector-updated", () => {
inspector.toolbox.highlighterUtils.startPicker().then(runTests);
});
inspector.toolbox.highlighterUtils.startPicker().then(runTests);
});
}, false);

View File

@ -13,9 +13,7 @@ function test() {
openInspector(aInspector => {
inspector = aInspector;
inspector.once("inspector-updated", () => {
inspector.toolbox.highlighter.showBoxModel(getNodeFront(div)).then(runTest);
});
inspector.toolbox.highlighter.showBoxModel(getNodeFront(div)).then(runTest);
});
}

View File

@ -1,189 +1,142 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
// Test that locking the pseudoclass displays correctly in the ruleview
let DOMUtils = Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
const PSEUDO = ":hover";
const TEST_URL = 'data:text/html,' +
'<head>' +
' <style>div {color:red;} div:hover {color:blue;}</style>' +
'</head>' +
'<body>' +
' <div id="parent-div">' +
' <div id="div-1">test div</div>' +
' <div id="div-2">test div2</div>' +
' </div>' +
'</body>';
let doc;
let parentDiv, div, div2;
let inspector;
let ruleview;
waitForExplicitFinish();
let pseudo = ":hover";
function test()
{
waitForExplicitFinish();
function test() {
ignoreAllUncaughtExceptions();
gBrowser.selectedTab = gBrowser.addTab();
gBrowser.selectedBrowser.addEventListener("load", function() {
gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
doc = content.document;
waitForFocus(createDocument, content);
waitForFocus(startTests, content);
}, true);
content.location = "data:text/html,pseudo-class lock tests";
content.location = TEST_URL;
}
function createDocument()
{
parentDiv = doc.createElement("div");
parentDiv.textContent = "parent div";
let startTests = Task.async(function*() {
let {toolbox, inspector, view} = yield openRuleView();
yield selectNode("#div-1", inspector);
div = doc.createElement("div");
div.textContent = "test div";
yield performTests(inspector, view);
div2 = doc.createElement("div");
div2.textContent = "test div2";
yield finishUp(toolbox);
finish();
});
let head = doc.getElementsByTagName('head')[0];
let style = doc.createElement('style');
let rules = doc.createTextNode('div { color: red; } div:hover { color: blue; }');
function* performTests(inspector, ruleview) {
yield togglePseudoClass(inspector);
yield testAdded(inspector, ruleview);
style.appendChild(rules);
head.appendChild(style);
parentDiv.appendChild(div);
parentDiv.appendChild(div2);
doc.body.appendChild(parentDiv);
yield togglePseudoClass(inspector);
yield testRemoved();
yield testRemovedFromUI(inspector, ruleview);
openInspector(selectNode);
yield togglePseudoClass(inspector);
yield testNavigate(inspector, ruleview);
}
function selectNode(aInspector)
{
inspector = aInspector;
function* togglePseudoClass(inspector) {
info("Toggle the pseudoclass, wait for the pseudoclass event and wait for the refresh of the rule view");
waitForView("ruleview", () => {
ruleview = inspector.sidebar.getWindowForTab("ruleview").ruleview.view;
inspector.sidebar.select("ruleview");
inspector.selection.setNode(div, "test");
inspector.once("inspector-updated", performTests);
});
let onPseudo = inspector.selection.once("pseudoclass");
let onRefresh = inspector.once("rule-view-refreshed");
inspector.togglePseudoClass(PSEUDO);
yield onPseudo;
yield onRefresh;
}
function performTests()
{
// toggle the class
inspector.togglePseudoClass(pseudo);
function* testNavigate(inspector, ruleview) {
yield selectNode("#parent-div", inspector);
// Wait for the "pseudoclass" event so we know the
// inspector has been told of the pseudoclass lock change.
inspector.selection.once("pseudoclass", () => {
inspector.once("rule-view-refreshed", () => {
testAdded(() => {
// Change the pseudo class and give the rule view time to update.
inspector.togglePseudoClass(pseudo);
inspector.selection.once("pseudoclass", () => {
inspector.once("rule-view-refreshed", () => {
testRemoved();
testRemovedFromUI(() => {
// toggle it back on
inspector.togglePseudoClass(pseudo);
inspector.selection.once("pseudoclass", () => {
inspector.once("rule-view-refreshed", () => {
testNavigate(() => {
// close the inspector
finishUp();
});
});
});
});
});
});
});
});
});
info("Make sure the pseudoclass is still on after navigating to a parent");
is(DOMUtils.hasPseudoClassLock(getNode("#div-1"), PSEUDO), true,
"pseudo-class lock is still applied after inspecting ancestor");
let onPseudo = inspector.selection.once("pseudoclass");
yield selectNode("#div-2", inspector);
yield onPseudo;
info("Make sure the pseudoclass is removed after navigating to a non-hierarchy node");
is(DOMUtils.hasPseudoClassLock(getNode("#div-1"), PSEUDO), false,
"pseudo-class lock is removed after inspecting sibling node");
yield selectNode("#div-1", inspector);
yield togglePseudoClass(inspector);
yield inspector.once("computed-view-refreshed");
}
function testNavigate(callback)
{
inspector.selection.setNode(parentDiv, "test");
inspector.once("inspector-updated", () => {
// make sure it's still on after naving to parent
is(DOMUtils.hasPseudoClassLock(div, pseudo), true,
"pseudo-class lock is still applied after inspecting ancestor");
inspector.selection.setNode(div2, "test");
inspector.selection.once("pseudoclass", () => {
// make sure it's removed after naving to a non-hierarchy node
is(DOMUtils.hasPseudoClassLock(div, pseudo), false,
"pseudo-class lock is removed after inspecting sibling node");
// toggle it back on
inspector.selection.setNode(div, "test");
inspector.once("inspector-updated", () => {
inspector.togglePseudoClass(pseudo);
inspector.once("computed-view-refreshed", callback);
});
});
});
}
function showPickerOn(node, cb)
{
function showPickerOn(node, inspector) {
let highlighter = inspector.toolbox.highlighter;
highlighter.showBoxModel(getNodeFront(node)).then(cb);
return highlighter.showBoxModel(getNodeFront(node));
}
function testAdded(cb)
{
// lock is applied to it and ancestors
let node = div;
function* testAdded(inspector, ruleview) {
info("Make sure the pseudoclass lock is applied to #div-1 and its ancestors");
let node = getNode("#div-1");
do {
is(DOMUtils.hasPseudoClassLock(node, pseudo), true,
is(DOMUtils.hasPseudoClassLock(node, PSEUDO), true,
"pseudo-class lock has been applied");
node = node.parentNode;
} while (node.parentNode)
// ruleview contains pseudo-class rule
info("Check that the ruleview contains the pseudo-class rule");
let rules = ruleview.element.querySelectorAll(".ruleview-rule.theme-separator");
is(rules.length, 3, "rule view is showing 3 rules for pseudo-class locked div");
is(rules[1]._ruleEditor.rule.selectorText, "div:hover", "rule view is showing " + pseudo + " rule");
is(rules[1]._ruleEditor.rule.selectorText, "div:hover", "rule view is showing " + PSEUDO + " rule");
// Show the highlighter by starting the pick mode and hovering over the div
showPickerOn(div, () => {
// infobar selector contains pseudo-class
let pseudoClassesBox = getHighlighter().querySelector(".highlighter-nodeinfobar-pseudo-classes");
is(pseudoClassesBox.textContent, pseudo, "pseudo-class in infobar selector");
inspector.toolbox.highlighter.hideBoxModel().then(cb);
});
info("Show the highlighter on #div-1");
yield showPickerOn(getNode("#div-1"), inspector);
info("Check that the infobar selector contains the pseudo-class");
let pseudoClassesBox = getHighlighter().querySelector(".highlighter-nodeinfobar-pseudo-classes");
is(pseudoClassesBox.textContent, PSEUDO, "pseudo-class in infobar selector");
yield inspector.toolbox.highlighter.hideBoxModel();
}
function testRemoved()
{
// lock removed from node and ancestors
let node = div;
function* testRemoved() {
info("Make sure the pseudoclass lock is removed from #div-1 and its ancestors");
let node = getNode("#div-1");
do {
is(DOMUtils.hasPseudoClassLock(node, pseudo), false,
is(DOMUtils.hasPseudoClassLock(node, PSEUDO), false,
"pseudo-class lock has been removed");
node = node.parentNode;
} while (node.parentNode)
}
function testRemovedFromUI(cb)
{
// ruleview no longer contains pseudo-class rule
function* testRemovedFromUI(inspector, ruleview) {
info("Check that the ruleview no longer contains the pseudo-class rule");
let rules = ruleview.element.querySelectorAll(".ruleview-rule.theme-separator");
is(rules.length, 2, "rule view is showing 2 rules after removing lock");
showPickerOn(div, () => {
let pseudoClassesBox = getHighlighter().querySelector(".highlighter-nodeinfobar-pseudo-classes");
is(pseudoClassesBox.textContent, "", "pseudo-class removed from infobar selector");
inspector.toolbox.highlighter.hideBoxModel().then(cb);
});
yield showPickerOn(getNode("#div-1"), inspector);
let pseudoClassesBox = getHighlighter().querySelector(".highlighter-nodeinfobar-pseudo-classes");
is(pseudoClassesBox.textContent, "", "pseudo-class removed from infobar selector");
yield inspector.toolbox.highlighter.hideBoxModel();
}
function finishUp()
{
gDevTools.once("toolbox-destroyed", function() {
testRemoved();
inspector = ruleview = null;
doc = div = null;
gBrowser.removeCurrentTab();
finish();
});
let target = TargetFactory.forTab(gBrowser.selectedTab);
let toolbox = gDevTools.getToolbox(target);
function* finishUp(toolbox) {
let onDestroy = gDevTools.once("toolbox-destroyed");
toolbox.destroy();
yield onDestroy;
yield testRemoved(getNode("#div-1"));
gBrowser.removeCurrentTab();
}

View File

@ -41,12 +41,152 @@ SimpleTest.registerCleanupFunction(() => {
Services.prefs.clearUserPref("devtools.inspector.activeSidebar");
});
function openInspector(callback)
{
/**
* Simple DOM node accesor function that takes either a node or a string css
* selector as argument and returns the corresponding node
* @param {String|DOMNode} nodeOrSelector
* @return {DOMNode}
*/
function getNode(nodeOrSelector) {
return typeof nodeOrSelector === "string" ?
content.document.querySelector(nodeOrSelector) :
nodeOrSelector;
}
/**
* Set the inspector's current selection to a node or to the first match of the
* given css selector
* @param {InspectorPanel} inspector The instance of InspectorPanel currently
* loaded in the toolbox
* @param {String} reason Defaults to "test" which instructs the inspector not
* to highlight the node upon selection
* @param {String} reason Defaults to "test" which instructs the inspector not to highlight the node upon selection
* @return a promise that resolves when the inspector is updated with the new
* node
*/
function selectNode(nodeOrSelector, inspector, reason="test") {
info("Selecting the node " + nodeOrSelector);
let node = getNode(nodeOrSelector);
let updated = inspector.once("inspector-updated");
inspector.selection.setNode(node, reason);
return updated;
}
/**
* Open the toolbox, with the inspector tool visible.
* @param {Function} cb Optional callback, if you don't want to use the returned
* promise
* @return a promise that resolves when the inspector is ready
*/
let openInspector = Task.async(function*(cb) {
info("Opening the inspector");
let target = TargetFactory.forTab(gBrowser.selectedTab);
gDevTools.showToolbox(target, "inspector").then(function(toolbox) {
callback(toolbox.getCurrentPanel(), toolbox);
}).then(null, console.error);
let inspector, toolbox;
// Checking if the toolbox and the inspector are already loaded
// The inspector-updated event should only be waited for if the inspector
// isn't loaded yet
toolbox = gDevTools.getToolbox(target);
if (toolbox) {
inspector = toolbox.getPanel("inspector");
if (inspector) {
info("Toolbox and inspector already open");
if (cb) {
return cb(inspector, toolbox);
} else {
return {
toolbox: toolbox,
inspector: inspector
};
}
}
}
info("Opening the toolbox");
toolbox = yield gDevTools.showToolbox(target, "inspector");
yield waitForToolboxFrameFocus(toolbox);
inspector = toolbox.getPanel("inspector");
info("Waiting for the inspector to update");
yield inspector.once("inspector-updated");
if (cb) {
return cb(inspector, toolbox);
} else {
return {
toolbox: toolbox,
inspector: inspector
};
}
});
/**
* Wait for the toolbox frame to receive focus after it loads
* @param {Toolbox} toolbox
* @return a promise that resolves when focus has been received
*/
function waitForToolboxFrameFocus(toolbox) {
info("Making sure that the toolbox's frame is focused");
let def = promise.defer();
let win = toolbox.frame.contentWindow;
waitForFocus(def.resolve, win);
return def.promise;
}
/**
* Open the toolbox, with the inspector tool visible, and the sidebar that
* corresponds to the given id selected
* @return a promise that resolves when the inspector is ready and the sidebar
* view is visible and ready
*/
let openInspectorSideBar = Task.async(function*(id) {
let {toolbox, inspector} = yield openInspector();
if (!hasSideBarTab(inspector, id)) {
info("Waiting for the " + id + " sidebar to be ready");
yield inspector.sidebar.once(id + "-ready");
}
info("Selecting the " + id + " sidebar");
inspector.sidebar.select(id);
return {
toolbox: toolbox,
inspector: inspector,
view: inspector.sidebar.getWindowForTab(id)[id].view
};
});
/**
* Open the toolbox, with the inspector tool visible, and the computed-view
* sidebar tab selected.
* @return a promise that resolves when the inspector is ready and the computed
* view is visible and ready
*/
function openComputedView() {
return openInspectorSideBar("computedview");
}
/**
* Open the toolbox, with the inspector tool visible, and the rule-view
* sidebar tab selected.
* @return a promise that resolves when the inspector is ready and the rule
* view is visible and ready
*/
function openRuleView() {
return openInspectorSideBar("ruleview");
}
/**
* Checks whether the inspector's sidebar corresponding to the given id already
* exists
* @param {InspectorPanel}
* @param {String}
* @return {Boolean}
*/
function hasSideBarTab(inspector, id) {
return !!inspector.sidebar.getWindowForTab(id);
}
function getActiveInspector()

File diff suppressed because it is too large Load Diff

View File

@ -4010,7 +4010,7 @@ for (var i = 0; i < 288; ++i)
bitLengths[i] = i < 144 || i > 279 ? 8 : i < 256 ? 9 : 7;
var fixedLiteralTable = makeHuffmanTable(bitLengths);
function makeHuffmanTable(bitLengths) {
var maxBits = max.apply(null, bitLengths);
var maxBits = Math.max.apply(null, bitLengths);
var numLengths = bitLengths.length;
var size = 1 << maxBits;
var codes = new Uint32Array(size);
@ -4042,7 +4042,14 @@ function createInflatedStream(bytes, outputLength) {
available: 0,
completed: false
};
var state = {};
var state = {
header: null,
distanceTable: null,
literalTable: null,
sym: null,
len: null,
sym2: null
};
do {
inflateBlock(stream, output, state);
} while (!output.completed && stream.pos < stream.end);
@ -4050,7 +4057,7 @@ function createInflatedStream(bytes, outputLength) {
}
var InflateNoDataError = {};
function inflateBlock(stream, output, state) {
var header = state.header !== undefined ? state.header : state.header = readBits(stream.bytes, stream, 3);
var header = state.header !== null ? state.header : state.header = readBits(stream.bytes, stream, 3);
switch (header >> 1) {
case 0:
stream.align();
@ -4066,10 +4073,7 @@ function inflateBlock(stream, output, state) {
var begin = pos + 4;
var end = stream.pos = begin + len;
var sbytes = stream.bytes, dbytes = output.data;
splice.apply(dbytes, [
output.available,
len
].concat(slice.call(sbytes, begin, end)));
dbytes.set(sbytes.subarray(begin, end), output.available);
output.available += len;
break;
case 1:
@ -4077,7 +4081,7 @@ function inflateBlock(stream, output, state) {
break;
case 2:
var distanceTable, literalTable;
if (state.distanceTable !== undefined) {
if (state.distanceTable !== null) {
distanceTable = state.distanceTable;
literalTable = state.literalTable;
} else {
@ -4130,13 +4134,13 @@ function inflateBlock(stream, output, state) {
literalTable = state.literalTable = makeHuffmanTable(bitLengths);
}
inflate(stream, output, literalTable, distanceTable, state);
delete state.distanceTable;
delete state.literalTable;
state.distanceTable = null;
state.literalTable = null;
break;
default:
fail('unknown block type', 'inflate');
}
delete state.header;
state.header = null;
output.completed = !(!(header & 1));
}
function readBits(bytes, stream, size) {
@ -4165,22 +4169,22 @@ function inflate(stream, output, literalTable, distanceTable, state) {
var pos = output.available;
var dbytes = output.data;
var sbytes = stream.bytes;
var sym = state.sym !== undefined ? state.sym : readCode(sbytes, stream, literalTable);
var sym = state.sym !== null ? state.sym : readCode(sbytes, stream, literalTable);
while (sym !== 256) {
if (sym < 256) {
dbytes[pos++] = sym;
} else {
state.sym = sym;
sym -= 257;
var len = state.len !== undefined ? state.len : state.len = lengthCodes[sym] + readBits(sbytes, stream, lengthExtraBits[sym]);
var sym2 = state.sym2 !== undefined ? state.sym2 : state.sym2 = readCode(sbytes, stream, distanceTable);
var len = state.len !== null ? state.len : state.len = lengthCodes[sym] + readBits(sbytes, stream, lengthExtraBits[sym]);
var sym2 = state.sym2 !== null ? state.sym2 : state.sym2 = readCode(sbytes, stream, distanceTable);
var distance = distanceCodes[sym2] + readBits(sbytes, stream, distanceExtraBits[sym2]);
var i = pos - distance;
while (len--)
dbytes[pos++] = dbytes[i++];
delete state.sym2;
delete state.len;
delete state.sym;
state.sym2 = null;
state.len = null;
state.sym = null;
}
output.available = pos;
sym = readCode(sbytes, stream, literalTable);
@ -5827,7 +5831,7 @@ function readTags(context, stream, swfVersion, final, onprogress, onexception) {
var tag = null;
if (context._readTag) {
tag = context._readTag;
delete context._readTag;
context._readTag = null;
}
try {
while (stream.pos < stream.end) {
@ -5951,7 +5955,14 @@ function CompressedPipe(target, length) {
this.state = {
bitBuffer: 0,
bitLength: 0,
compression: {}
compression: {
header: null,
distanceTable: null,
literalTable: null,
sym: null,
len: null,
sym2: null
}
};
this.output = {
data: new Uint8Array(length),

File diff suppressed because one or more lines are too long

View File

@ -1,2 +1,2 @@
0.8.6
4728574
0.8.271
2717b0c

View File

@ -80,6 +80,11 @@ function fallback() {
FirefoxCom.requestSync('fallback', null)
}
var playerglobalInfo = {
abcs: SHUMWAY_ROOT + "playerglobal/playerglobal.abcs",
catalog: SHUMWAY_ROOT + "playerglobal/playerglobal.json"
};
function runViewer() {
var flashParams = JSON.parse(FirefoxCom.requestSync('getPluginParams', null));
FileLoadingService.setBaseUrl(flashParams.baseUrl);
@ -222,7 +227,7 @@ var FileLoadingService = {
case "open": this.onopen(); break;
case "close":
this.onclose();
delete FileLoadingService.sessions[sessionId];
FileLoadingService.sessions[sessionId] = null;
console.log('Session #' + sessionId +': closed');
break;
case "error":
@ -258,6 +263,9 @@ var FileLoadingService = {
};
function parseSwf(url, movieParams, objectParams) {
var enableVerifier = Shumway.AVM2.Runtime.enableVerifier;
var EXECUTION_MODE = Shumway.AVM2.Runtime.EXECUTION_MODE;
var compilerSettings = JSON.parse(
FirefoxCom.requestSync('getCompilerSettings', null));
enableVerifier.value = compilerSettings.verifier;
@ -274,7 +282,7 @@ function parseSwf(url, movieParams, objectParams) {
FirefoxCom.request('endActivation', null);
}
createAVM2(builtinPath, playerGlobalPath, avm1Path,
createAVM2(builtinPath, playerglobalInfo, avm1Path,
compilerSettings.sysCompiler ? EXECUTION_MODE.COMPILE : EXECUTION_MODE.INTERPRET,
compilerSettings.appCompiler ? EXECUTION_MODE.COMPILE : EXECUTION_MODE.INTERPRET,
function (avm2) {

View File

@ -142,6 +142,8 @@ toolbarseparator {
#PersonalToolbar {
padding: 0 4px 4px;
/* 4px padding ^ plus 19px personal-bookmarks (see below) */
min-height: 23px;
}
#navigator-toolbox > toolbar:not(#TabsToolbar):-moz-lwtheme {
@ -157,7 +159,7 @@ toolbarseparator {
/* ----- BOOKMARK TOOLBAR ----- */
#personal-bookmarks {
min-height: 17px; /* 16px button height + 1px margin-bottom */
min-height: 19px; /* 16px button height + 2px padding + 1px margin-bottom */
}
toolbarbutton.chevron {

View File

@ -1286,16 +1286,10 @@ abstract public class BrowserApp extends GeckoApp
public void showPrivateTabs() {
showTabs(TabsPanel.Panel.PRIVATE_TABS);
}
@Override
public void showRemoteTabs() {
showTabs(TabsPanel.Panel.REMOTE_TABS);
}
/**
* Ensure the TabsPanel view is properly inflated and returns
* true when the view has been inflated, false otherwise.
*/
* Ensure the TabsPanel view is properly inflated and returns
* true when the view has been inflated, false otherwise.
*/
private boolean ensureTabsPanelExists() {
if (mTabsPanel != null) {
return false;

View File

@ -533,10 +533,6 @@ public abstract class GeckoApp
public void showPrivateTabs() { }
public void showRemoteTabs() { }
private void showTabs(TabsPanel.Panel panel) { }
public void hideTabs() { }
/**
@ -1394,6 +1390,13 @@ public abstract class GeckoApp
* aware of the locale.
*
* Now we can display strings!
*
* You can think of this as being something like a second phase of onCreate,
* where you can do string-related operations. Use this in place of embedding
* strings in view XML.
*
* By contrast, onConfigurationChanged does some locale operations, but is in
* response to device changes.
*/
@Override
public void onLocaleReady(final String locale) {
@ -1403,11 +1406,12 @@ public abstract class GeckoApp
// The URL bar hint needs to be populated.
TextView urlBar = (TextView) findViewById(R.id.url_bar_title);
if (urlBar == null) {
return;
if (urlBar != null) {
final String hint = getResources().getString(R.string.url_bar_default_text);
urlBar.setHint(hint);
} else {
Log.d(LOGTAG, "No URL bar in GeckoApp. Not loading localized hint string.");
}
final String hint = getResources().getString(R.string.url_bar_default_text);
urlBar.setHint(hint);
// Allow onConfigurationChanged to take care of the rest.
onConfigurationChanged(getResources().getConfiguration());

View File

@ -613,6 +613,7 @@ public final class HomeConfig {
private final String mBackImageUrl;
private final String mFilter;
private final EmptyViewConfig mEmptyViewConfig;
private final EnumSet<Flags> mFlags;
private static final String JSON_KEY_TYPE = "type";
private static final String JSON_KEY_DATASET = "dataset";
@ -621,6 +622,11 @@ public final class HomeConfig {
private static final String JSON_KEY_BACK_IMAGE_URL = "backImageUrl";
private static final String JSON_KEY_FILTER = "filter";
private static final String JSON_KEY_EMPTY = "empty";
private static final String JSON_KEY_REFRESH_ENABLED = "refreshEnabled";
public enum Flags {
REFRESH_ENABLED
}
public ViewConfig(int index, JSONObject json) throws JSONException, IllegalArgumentException {
mIndex = index;
@ -638,6 +644,11 @@ public final class HomeConfig {
mEmptyViewConfig = null;
}
mFlags = EnumSet.noneOf(Flags.class);
if (json.optBoolean(JSON_KEY_REFRESH_ENABLED, false)) {
mFlags.add(Flags.REFRESH_ENABLED);
}
validate();
}
@ -651,6 +662,7 @@ public final class HomeConfig {
mBackImageUrl = in.readString();
mFilter = in.readString();
mEmptyViewConfig = (EmptyViewConfig) in.readParcelable(getClass().getClassLoader());
mFlags = (EnumSet<Flags>) in.readSerializable();
validate();
}
@ -664,13 +676,14 @@ public final class HomeConfig {
mBackImageUrl = viewConfig.mBackImageUrl;
mFilter = viewConfig.mFilter;
mEmptyViewConfig = viewConfig.mEmptyViewConfig;
mFlags = viewConfig.mFlags.clone();
validate();
}
public ViewConfig(int index, ViewType type, String datasetId, ItemType itemType,
ItemHandler itemHandler, String backImageUrl, String filter,
EmptyViewConfig emptyViewConfig) {
EmptyViewConfig emptyViewConfig, EnumSet<Flags> flags) {
mIndex = index;
mType = type;
mDatasetId = datasetId;
@ -679,6 +692,7 @@ public final class HomeConfig {
mBackImageUrl = backImageUrl;
mFilter = filter;
mEmptyViewConfig = emptyViewConfig;
mFlags = flags;
validate();
}
@ -699,6 +713,10 @@ public final class HomeConfig {
if (mItemHandler == null) {
throw new IllegalArgumentException("Can't create ViewConfig with null item handler");
}
if (mFlags == null) {
throw new IllegalArgumentException("Can't create ViewConfig with null flags");
}
}
public int getIndex() {
@ -733,6 +751,10 @@ public final class HomeConfig {
return mEmptyViewConfig;
}
public boolean isRefreshEnabled() {
return mFlags.contains(Flags.REFRESH_ENABLED);
}
public JSONObject toJSON() throws JSONException {
final JSONObject json = new JSONObject();
@ -753,6 +775,10 @@ public final class HomeConfig {
json.put(JSON_KEY_EMPTY, mEmptyViewConfig.toJSON());
}
if (mFlags.contains(Flags.REFRESH_ENABLED)) {
json.put(JSON_KEY_REFRESH_ENABLED, true);
}
return json;
}
@ -771,6 +797,7 @@ public final class HomeConfig {
dest.writeString(mBackImageUrl);
dest.writeString(mFilter);
dest.writeParcelable(mEmptyViewConfig, 0);
dest.writeSerializable(mFlags);
}
public static final Creator<ViewConfig> CREATOR = new Creator<ViewConfig>() {

View File

@ -373,6 +373,11 @@ abstract class PanelLayout extends FrameLayout {
if (view instanceof DatasetBacked) {
DatasetBacked datasetBacked = (DatasetBacked) view;
datasetBacked.setFilterManager(new PanelFilterManager(viewState));
if (viewConfig.isRefreshEnabled()) {
view = new PanelRefreshLayout(getContext(), view,
mPanelConfig.getId(), viewConfig.getIndex());
}
}
viewState.setView(view);

View File

@ -0,0 +1,95 @@
/* -*- 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.home;
import org.mozilla.gecko.R;
import org.mozilla.gecko.GeckoAppShell;
import org.mozilla.gecko.GeckoEvent;
import org.mozilla.gecko.home.PanelLayout.DatasetBacked;
import org.mozilla.gecko.home.PanelLayout.FilterManager;
import org.mozilla.gecko.widget.GeckoSwipeRefreshLayout;
import org.mozilla.gecko.widget.GeckoSwipeRefreshLayout.OnRefreshListener;
import org.json.JSONException;
import org.json.JSONObject;
import android.content.Context;
import android.database.Cursor;
import android.util.Log;
import android.view.View;
/**
* Used to wrap a {@code DatasetBacked} ListView or GridView to give the child view swipe-to-refresh
* capabilities.
*
* This view acts as a decorator to forward the {@code DatasetBacked} methods to the child view
* while providing the refresh gesture support on top of it.
*/
class PanelRefreshLayout extends GeckoSwipeRefreshLayout implements DatasetBacked {
private static final String LOGTAG = "GeckoPanelRefreshLayout";
private static final String JSON_KEY_PANEL_ID = "panelId";
private static final String JSON_KEY_VIEW_INDEX = "viewIndex";
private final String panelId;
private final int viewIndex;
private final DatasetBacked datasetBacked;
/**
* @param context Android context.
* @param childView ListView or GridView. Must implement {@code DatasetBacked}.
* @param panelId The ID from the {@code PanelConfig}.
* @param viewIndex The index from the {@code ViewConfig}.
*/
public PanelRefreshLayout(Context context, View childView, String panelId, int viewIndex) {
super(context);
if (!(childView instanceof DatasetBacked)) {
throw new IllegalArgumentException("View must implement DatasetBacked to be refreshed");
}
this.panelId = panelId;
this.viewIndex = viewIndex;
this.datasetBacked = (DatasetBacked) childView;
setOnRefreshListener(new RefreshListener());
addView(childView);
// Must be called after the child view has been added.
setColorScheme(R.color.swipe_refresh_orange, R.color.swipe_refresh_white,
R.color.swipe_refresh_orange, R.color.swipe_refresh_white);
}
@Override
public void setDataset(Cursor cursor) {
datasetBacked.setDataset(cursor);
setRefreshing(false);
}
@Override
public void setFilterManager(FilterManager manager) {
datasetBacked.setFilterManager(manager);
}
private class RefreshListener implements OnRefreshListener {
@Override
public void onRefresh() {
final JSONObject response = new JSONObject();
try {
response.put(JSON_KEY_PANEL_ID, panelId);
response.put(JSON_KEY_VIEW_INDEX, viewIndex);
} catch (JSONException e) {
Log.e(LOGTAG, "Could not create refresh message", e);
return;
}
final GeckoEvent event =
GeckoEvent.createBroadcastEvent("HomePanels:RefreshView", response.toString());
GeckoAppShell.sendEventToGecko(event);
}
}
}

View File

@ -267,6 +267,7 @@ gbjar.sources += [
'home/PanelLayout.java',
'home/PanelListView.java',
'home/PanelManager.java',
'home/PanelRefreshLayout.java',
'home/PanelViewAdapter.java',
'home/PanelViewItemHandler.java',
'home/PinSiteDialog.java',
@ -405,6 +406,7 @@ gbjar.sources += [
'widget/FlowLayout.java',
'widget/GeckoActionProvider.java',
'widget/GeckoPopupMenu.java',
'widget/GeckoSwipeRefreshLayout.java',
'widget/GeckoViewFlipper.java',
'widget/IconTabWidget.java',
'widget/SquaredImageView.java',

View File

@ -91,5 +91,9 @@
<color name="home_last_tab_bar_bg">#FFF5F7F9</color>
<color name="panel_image_item_background">#D1D9E1</color>
<!-- Swipe to refresh colors -->
<color name="swipe_refresh_orange">#FFFFC26C</color>
<color name="swipe_refresh_white">#FFFFFFFF</color>
</resources>

View File

@ -0,0 +1,795 @@
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.mozilla.gecko.widget;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.Animation;
import android.view.animation.Animation.AnimationListener;
import android.view.animation.AnimationUtils;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.Interpolator;
import android.view.animation.Transformation;
import android.widget.AbsListView;
/**
* GeckoSwipeRefreshLayout is mostly lifted from Android's support library (v4) with these
* modfications:
* - Removes elastic "rubber banding" effect when overscrolling the child view.
* - Changes the height of the progress bar to match the height of HomePager's page indicator.
* - Uses a rectangle rather than a circle for the SwipeProgressBar indicator.
*
* This class also embeds package-access dependent classes SwipeProgressBar and
* BakedBezierInterpolator.
*
* Original source: https://android.googlesource.com/platform/frameworks/support/+/
* android-support-lib-19.1.0/v4/java/android/support/v4/widget/SwipeRefreshLayout.java
*/
public class GeckoSwipeRefreshLayout extends ViewGroup {
private static final long RETURN_TO_ORIGINAL_POSITION_TIMEOUT = 300;
private static final float ACCELERATE_INTERPOLATION_FACTOR = 1.5f;
private static final float DECELERATE_INTERPOLATION_FACTOR = 2f;
// Reduce the height (from 4 to 3) of the progress bar to match HomePager's page indicator.
private static final float PROGRESS_BAR_HEIGHT = 3;
private static final float MAX_SWIPE_DISTANCE_FACTOR = .6f;
private static final int REFRESH_TRIGGER_DISTANCE = 120;
private SwipeProgressBar mProgressBar; //the thing that shows progress is going
private View mTarget; //the content that gets pulled down
private int mOriginalOffsetTop;
private OnRefreshListener mListener;
private MotionEvent mDownEvent;
private int mFrom;
private boolean mRefreshing = false;
private int mTouchSlop;
private float mDistanceToTriggerSync = -1;
private float mPrevY;
private int mMediumAnimationDuration;
private float mFromPercentage = 0;
private float mCurrPercentage = 0;
private int mProgressBarHeight;
private int mCurrentTargetOffsetTop;
// Target is returning to its start offset because it was cancelled or a
// refresh was triggered.
private boolean mReturningToStart;
private final DecelerateInterpolator mDecelerateInterpolator;
private final AccelerateInterpolator mAccelerateInterpolator;
private static final int[] LAYOUT_ATTRS = new int[] {
android.R.attr.enabled
};
private final Animation mAnimateToStartPosition = new Animation() {
@Override
public void applyTransformation(float interpolatedTime, Transformation t) {
int targetTop = 0;
if (mFrom != mOriginalOffsetTop) {
targetTop = (mFrom + (int)((mOriginalOffsetTop - mFrom) * interpolatedTime));
}
int offset = targetTop - mTarget.getTop();
final int currentTop = mTarget.getTop();
if (offset + currentTop < 0) {
offset = 0 - currentTop;
}
setTargetOffsetTopAndBottom(offset);
}
};
private Animation mShrinkTrigger = new Animation() {
@Override
public void applyTransformation(float interpolatedTime, Transformation t) {
float percent = mFromPercentage + ((0 - mFromPercentage) * interpolatedTime);
mProgressBar.setTriggerPercentage(percent);
}
};
private final AnimationListener mReturnToStartPositionListener = new BaseAnimationListener() {
@Override
public void onAnimationEnd(Animation animation) {
// Once the target content has returned to its start position, reset
// the target offset to 0
mCurrentTargetOffsetTop = 0;
}
};
private final AnimationListener mShrinkAnimationListener = new BaseAnimationListener() {
@Override
public void onAnimationEnd(Animation animation) {
mCurrPercentage = 0;
}
};
private final Runnable mReturnToStartPosition = new Runnable() {
@Override
public void run() {
mReturningToStart = true;
animateOffsetToStartPosition(mCurrentTargetOffsetTop + getPaddingTop(),
mReturnToStartPositionListener);
}
};
// Cancel the refresh gesture and animate everything back to its original state.
private final Runnable mCancel = new Runnable() {
@Override
public void run() {
mReturningToStart = true;
// Timeout fired since the user last moved their finger; animate the
// trigger to 0 and put the target back at its original position
if (mProgressBar != null) {
mFromPercentage = mCurrPercentage;
mShrinkTrigger.setDuration(mMediumAnimationDuration);
mShrinkTrigger.setAnimationListener(mShrinkAnimationListener);
mShrinkTrigger.reset();
mShrinkTrigger.setInterpolator(mDecelerateInterpolator);
startAnimation(mShrinkTrigger);
}
animateOffsetToStartPosition(mCurrentTargetOffsetTop + getPaddingTop(),
mReturnToStartPositionListener);
}
};
/**
* Simple constructor to use when creating a GeckoSwipeRefreshLayout from code.
* @param context
*/
public GeckoSwipeRefreshLayout(Context context) {
this(context, null);
}
/**
* Constructor that is called when inflating GeckoSwipeRefreshLayout from XML.
* @param context
* @param attrs
*/
public GeckoSwipeRefreshLayout(Context context, AttributeSet attrs) {
super(context, attrs);
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
mMediumAnimationDuration = getResources().getInteger(
android.R.integer.config_mediumAnimTime);
setWillNotDraw(false);
mProgressBar = new SwipeProgressBar(this);
final DisplayMetrics metrics = getResources().getDisplayMetrics();
mProgressBarHeight = (int) (metrics.density * PROGRESS_BAR_HEIGHT);
mDecelerateInterpolator = new DecelerateInterpolator(DECELERATE_INTERPOLATION_FACTOR);
mAccelerateInterpolator = new AccelerateInterpolator(ACCELERATE_INTERPOLATION_FACTOR);
final TypedArray a = context.obtainStyledAttributes(attrs, LAYOUT_ATTRS);
setEnabled(a.getBoolean(0, true));
a.recycle();
}
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
removeCallbacks(mCancel);
removeCallbacks(mReturnToStartPosition);
}
@Override
public void onDetachedFromWindow() {
super.onDetachedFromWindow();
removeCallbacks(mReturnToStartPosition);
removeCallbacks(mCancel);
}
private void animateOffsetToStartPosition(int from, AnimationListener listener) {
mFrom = from;
mAnimateToStartPosition.reset();
mAnimateToStartPosition.setDuration(mMediumAnimationDuration);
mAnimateToStartPosition.setAnimationListener(listener);
mAnimateToStartPosition.setInterpolator(mDecelerateInterpolator);
mTarget.startAnimation(mAnimateToStartPosition);
}
/**
* Set the listener to be notified when a refresh is triggered via the swipe
* gesture.
*/
public void setOnRefreshListener(OnRefreshListener listener) {
mListener = listener;
}
private void setTriggerPercentage(float percent) {
if (percent == 0f) {
// No-op. A null trigger means it's uninitialized, and setting it to zero-percent
// means we're trying to reset state, so there's nothing to reset in this case.
mCurrPercentage = 0;
return;
}
mCurrPercentage = percent;
mProgressBar.setTriggerPercentage(percent);
}
/**
* Notify the widget that refresh state has changed. Do not call this when
* refresh is triggered by a swipe gesture.
*
* @param refreshing Whether or not the view should show refresh progress.
*/
public void setRefreshing(boolean refreshing) {
if (mRefreshing != refreshing) {
ensureTarget();
mCurrPercentage = 0;
mRefreshing = refreshing;
if (mRefreshing) {
mProgressBar.start();
} else {
mProgressBar.stop();
}
}
}
/**
* Set the four colors used in the progress animation. The first color will
* also be the color of the bar that grows in response to a user swipe
* gesture.
*
* @param colorRes1 Color resource.
* @param colorRes2 Color resource.
* @param colorRes3 Color resource.
* @param colorRes4 Color resource.
*/
public void setColorScheme(int colorRes1, int colorRes2, int colorRes3, int colorRes4) {
ensureTarget();
final Resources res = getResources();
final int color1 = res.getColor(colorRes1);
final int color2 = res.getColor(colorRes2);
final int color3 = res.getColor(colorRes3);
final int color4 = res.getColor(colorRes4);
mProgressBar.setColorScheme(color1, color2, color3,color4);
}
/**
* @return Whether the SwipeRefreshWidget is actively showing refresh
* progress.
*/
public boolean isRefreshing() {
return mRefreshing;
}
private void ensureTarget() {
// Don't bother getting the parent height if the parent hasn't been laid out yet.
if (mTarget == null) {
if (getChildCount() > 1 && !isInEditMode()) {
throw new IllegalStateException(
"GeckoSwipeRefreshLayout can host only one direct child");
}
mTarget = getChildAt(0);
mOriginalOffsetTop = mTarget.getTop() + getPaddingTop();
}
if (mDistanceToTriggerSync == -1) {
if (getParent() != null && ((View)getParent()).getHeight() > 0) {
final DisplayMetrics metrics = getResources().getDisplayMetrics();
mDistanceToTriggerSync = (int) Math.min(
((View) getParent()) .getHeight() * MAX_SWIPE_DISTANCE_FACTOR,
REFRESH_TRIGGER_DISTANCE * metrics.density);
}
}
}
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
mProgressBar.draw(canvas);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
final int width = getMeasuredWidth();
final int height = getMeasuredHeight();
mProgressBar.setBounds(0, 0, width, mProgressBarHeight);
if (getChildCount() == 0) {
return;
}
final View child = getChildAt(0);
final int childLeft = getPaddingLeft();
final int childTop = mCurrentTargetOffsetTop + getPaddingTop();
final int childWidth = width - getPaddingLeft() - getPaddingRight();
final int childHeight = height - getPaddingTop() - getPaddingBottom();
child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
}
@Override
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (getChildCount() > 1 && !isInEditMode()) {
throw new IllegalStateException("GeckoSwipeRefreshLayout can host only one direct child");
}
if (getChildCount() > 0) {
getChildAt(0).measure(
MeasureSpec.makeMeasureSpec(
getMeasuredWidth() - getPaddingLeft() - getPaddingRight(),
MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(
getMeasuredHeight() - getPaddingTop() - getPaddingBottom(),
MeasureSpec.EXACTLY));
}
}
/**
* @return Whether it is possible for the child view of this layout to
* scroll up. Override this if the child view is a custom view.
*/
public boolean canChildScrollUp() {
if (android.os.Build.VERSION.SDK_INT < 14) {
if (mTarget instanceof AbsListView) {
final AbsListView absListView = (AbsListView) mTarget;
return absListView.getChildCount() > 0
&& (absListView.getFirstVisiblePosition() > 0 || absListView.getChildAt(0)
.getTop() < absListView.getPaddingTop());
} else {
return mTarget.getScrollY() > 0;
}
} else {
return ViewCompat.canScrollVertically(mTarget, -1);
}
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
ensureTarget();
boolean handled = false;
if (mReturningToStart && ev.getAction() == MotionEvent.ACTION_DOWN) {
mReturningToStart = false;
}
if (isEnabled() && !mReturningToStart && !canChildScrollUp()) {
handled = onTouchEvent(ev);
}
return !handled ? super.onInterceptTouchEvent(ev) : handled;
}
@Override
public void requestDisallowInterceptTouchEvent(boolean b) {
// Nope.
}
@Override
public boolean onTouchEvent(MotionEvent event) {
final int action = event.getAction();
boolean handled = false;
switch (action) {
case MotionEvent.ACTION_DOWN:
mCurrPercentage = 0;
mDownEvent = MotionEvent.obtain(event);
mPrevY = mDownEvent.getY();
break;
case MotionEvent.ACTION_MOVE:
if (mDownEvent != null && !mReturningToStart) {
final float eventY = event.getY();
float yDiff = eventY - mDownEvent.getY();
if (yDiff > mTouchSlop) {
// User velocity passed min velocity; trigger a refresh
if (yDiff > mDistanceToTriggerSync) {
// User movement passed distance; trigger a refresh
startRefresh();
handled = true;
break;
} else {
// Just track the user's movement
setTriggerPercentage(
mAccelerateInterpolator.getInterpolation(
yDiff / mDistanceToTriggerSync));
float offsetTop = yDiff;
if (mPrevY > eventY) {
offsetTop = yDiff - mTouchSlop;
}
// Removed this call to disable "rubber-band" overscroll effect.
// updateContentOffsetTop((int) offsetTop);
if (mPrevY > eventY && (mTarget.getTop() < mTouchSlop)) {
// If the user puts the view back at the top, we
// don't need to. This shouldn't be considered
// cancelling the gesture as the user can restart from the top.
removeCallbacks(mCancel);
} else {
updatePositionTimeout();
}
mPrevY = event.getY();
handled = true;
}
}
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
if (mDownEvent != null) {
mDownEvent.recycle();
mDownEvent = null;
}
break;
}
return handled;
}
private void startRefresh() {
removeCallbacks(mCancel);
mReturnToStartPosition.run();
setRefreshing(true);
mListener.onRefresh();
}
private void updateContentOffsetTop(int targetTop) {
final int currentTop = mTarget.getTop();
if (targetTop > mDistanceToTriggerSync) {
targetTop = (int) mDistanceToTriggerSync;
} else if (targetTop < 0) {
targetTop = 0;
}
setTargetOffsetTopAndBottom(targetTop - currentTop);
}
private void setTargetOffsetTopAndBottom(int offset) {
mTarget.offsetTopAndBottom(offset);
mCurrentTargetOffsetTop = mTarget.getTop();
}
private void updatePositionTimeout() {
removeCallbacks(mCancel);
postDelayed(mCancel, RETURN_TO_ORIGINAL_POSITION_TIMEOUT);
}
/**
* Classes that wish to be notified when the swipe gesture correctly
* triggers a refresh should implement this interface.
*/
public interface OnRefreshListener {
public void onRefresh();
}
/**
* Simple AnimationListener to avoid having to implement unneeded methods in
* AnimationListeners.
*/
private class BaseAnimationListener implements AnimationListener {
@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
}
@Override
public void onAnimationRepeat(Animation animation) {
}
}
/**
* The only modification to this class is the shape of the refresh indicator to be a rectangle
* rather than a circle.
*/
private static final class SwipeProgressBar {
// Default progress animation colors are grays.
private final static int COLOR1 = 0xB3000000;
private final static int COLOR2 = 0x80000000;
private final static int COLOR3 = 0x4d000000;
private final static int COLOR4 = 0x1a000000;
// The duration of the animation cycle.
private static final int ANIMATION_DURATION_MS = 2000;
// The duration of the animation to clear the bar.
private static final int FINISH_ANIMATION_DURATION_MS = 1000;
// Interpolator for varying the speed of the animation.
private static final Interpolator INTERPOLATOR = BakedBezierInterpolator.getInstance();
private final Paint mPaint = new Paint();
private final RectF mClipRect = new RectF();
private float mTriggerPercentage;
private long mStartTime;
private long mFinishTime;
private boolean mRunning;
// Colors used when rendering the animation,
private int mColor1;
private int mColor2;
private int mColor3;
private int mColor4;
private View mParent;
private Rect mBounds = new Rect();
public SwipeProgressBar(View parent) {
mParent = parent;
mColor1 = COLOR1;
mColor2 = COLOR2;
mColor3 = COLOR3;
mColor4 = COLOR4;
}
/**
* Set the four colors used in the progress animation. The first color will
* also be the color of the bar that grows in response to a user swipe
* gesture.
*
* @param color1 Integer representation of a color.
* @param color2 Integer representation of a color.
* @param color3 Integer representation of a color.
* @param color4 Integer representation of a color.
*/
void setColorScheme(int color1, int color2, int color3, int color4) {
mColor1 = color1;
mColor2 = color2;
mColor3 = color3;
mColor4 = color4;
}
/**
* Update the progress the user has made toward triggering the swipe
* gesture. and use this value to update the percentage of the trigger that
* is shown.
*/
void setTriggerPercentage(float triggerPercentage) {
mTriggerPercentage = triggerPercentage;
mStartTime = 0;
ViewCompat.postInvalidateOnAnimation(mParent);
}
/**
* Start showing the progress animation.
*/
void start() {
if (!mRunning) {
mTriggerPercentage = 0;
mStartTime = AnimationUtils.currentAnimationTimeMillis();
mRunning = true;
mParent.postInvalidate();
}
}
/**
* Stop showing the progress animation.
*/
void stop() {
if (mRunning) {
mTriggerPercentage = 0;
mFinishTime = AnimationUtils.currentAnimationTimeMillis();
mRunning = false;
mParent.postInvalidate();
}
}
/**
* @return Return whether the progress animation is currently running.
*/
boolean isRunning() {
return mRunning || mFinishTime > 0;
}
void draw(Canvas canvas) {
final int width = mBounds.width();
final int height = mBounds.height();
final int cx = width / 2;
final int cy = height / 2;
boolean drawTriggerWhileFinishing = false;
int restoreCount = canvas.save();
canvas.clipRect(mBounds);
if (mRunning || (mFinishTime > 0)) {
long now = AnimationUtils.currentAnimationTimeMillis();
long elapsed = (now - mStartTime) % ANIMATION_DURATION_MS;
long iterations = (now - mStartTime) / ANIMATION_DURATION_MS;
float rawProgress = (elapsed / (ANIMATION_DURATION_MS / 100f));
// If we're not running anymore, that means we're running through
// the finish animation.
if (!mRunning) {
// If the finish animation is done, don't draw anything, and
// don't repost.
if ((now - mFinishTime) >= FINISH_ANIMATION_DURATION_MS) {
mFinishTime = 0;
return;
}
// Otherwise, use a 0 opacity alpha layer to clear the animation
// from the inside out. This layer will prevent the circles from
// drawing within its bounds.
long finishElapsed = (now - mFinishTime) % FINISH_ANIMATION_DURATION_MS;
float finishProgress = (finishElapsed / (FINISH_ANIMATION_DURATION_MS / 100f));
float pct = (finishProgress / 100f);
// Radius of the circle is half of the screen.
float clearRadius = width / 2 * INTERPOLATOR.getInterpolation(pct);
mClipRect.set(cx - clearRadius, 0, cx + clearRadius, height);
canvas.saveLayerAlpha(mClipRect, 0, 0);
// Only draw the trigger if there is a space in the center of
// this refreshing view that needs to be filled in by the
// trigger. If the progress view is just still animating, let it
// continue animating.
drawTriggerWhileFinishing = true;
}
// First fill in with the last color that would have finished drawing.
if (iterations == 0) {
canvas.drawColor(mColor1);
} else {
if (rawProgress >= 0 && rawProgress < 25) {
canvas.drawColor(mColor4);
} else if (rawProgress >= 25 && rawProgress < 50) {
canvas.drawColor(mColor1);
} else if (rawProgress >= 50 && rawProgress < 75) {
canvas.drawColor(mColor2);
} else {
canvas.drawColor(mColor3);
}
}
// Then draw up to 4 overlapping concentric circles of varying radii, based on how far
// along we are in the cycle.
// progress 0-50 draw mColor2
// progress 25-75 draw mColor3
// progress 50-100 draw mColor4
// progress 75 (wrap to 25) draw mColor1
if ((rawProgress >= 0 && rawProgress <= 25)) {
float pct = (((rawProgress + 25) * 2) / 100f);
drawCircle(canvas, cx, cy, mColor1, pct);
}
if (rawProgress >= 0 && rawProgress <= 50) {
float pct = ((rawProgress * 2) / 100f);
drawCircle(canvas, cx, cy, mColor2, pct);
}
if (rawProgress >= 25 && rawProgress <= 75) {
float pct = (((rawProgress - 25) * 2) / 100f);
drawCircle(canvas, cx, cy, mColor3, pct);
}
if (rawProgress >= 50 && rawProgress <= 100) {
float pct = (((rawProgress - 50) * 2) / 100f);
drawCircle(canvas, cx, cy, mColor4, pct);
}
if ((rawProgress >= 75 && rawProgress <= 100)) {
float pct = (((rawProgress - 75) * 2) / 100f);
drawCircle(canvas, cx, cy, mColor1, pct);
}
if (mTriggerPercentage > 0 && drawTriggerWhileFinishing) {
// There is some portion of trigger to draw. Restore the canvas,
// then draw the trigger. Otherwise, the trigger does not appear
// until after the bar has finished animating and appears to
// just jump in at a larger width than expected.
canvas.restoreToCount(restoreCount);
restoreCount = canvas.save();
canvas.clipRect(mBounds);
drawTrigger(canvas, cx, cy);
}
// Keep running until we finish out the last cycle.
ViewCompat.postInvalidateOnAnimation(mParent);
} else {
// Otherwise if we're in the middle of a trigger, draw that.
if (mTriggerPercentage > 0 && mTriggerPercentage <= 1.0) {
drawTrigger(canvas, cx, cy);
}
}
canvas.restoreToCount(restoreCount);
}
private void drawTrigger(Canvas canvas, int cx, int cy) {
mPaint.setColor(mColor1);
// Use a rectangle to instead of a circle as per UX.
// canvas.drawCircle(cx, cy, cx * mTriggerPercentage, mPaint);
canvas.drawRect(cx - cx * mTriggerPercentage, 0, cx + cx * mTriggerPercentage,
mBounds.bottom, mPaint);
}
/**
* Draws a circle centered in the view.
*
* @param canvas the canvas to draw on
* @param cx the center x coordinate
* @param cy the center y coordinate
* @param color the color to draw
* @param pct the percentage of the view that the circle should cover
*/
private void drawCircle(Canvas canvas, float cx, float cy, int color, float pct) {
mPaint.setColor(color);
canvas.save();
canvas.translate(cx, cy);
float radiusScale = INTERPOLATOR.getInterpolation(pct);
canvas.scale(radiusScale, radiusScale);
canvas.drawCircle(0, 0, cx, mPaint);
canvas.restore();
}
/**
* Set the drawing bounds of this SwipeProgressBar.
*/
void setBounds(int left, int top, int right, int bottom) {
mBounds.left = left;
mBounds.top = top;
mBounds.right = right;
mBounds.bottom = bottom;
}
}
private static final class BakedBezierInterpolator implements Interpolator {
private static final BakedBezierInterpolator INSTANCE = new BakedBezierInterpolator();
public final static BakedBezierInterpolator getInstance() {
return INSTANCE;
}
/**
* Use getInstance instead of instantiating.
*/
private BakedBezierInterpolator() {
super();
}
/**
* Lookup table values.
* Generated using a Bezier curve from (0,0) to (1,1) with control points:
* P0 (0,0)
* P1 (0.4, 0)
* P2 (0.2, 1.0)
* P3 (1.0, 1.0)
*
* Values sampled with x at regular intervals between 0 and 1.
*/
private static final float[] VALUES = new float[] {
0.0f, 0.0002f, 0.0009f, 0.0019f, 0.0036f, 0.0059f, 0.0086f, 0.0119f, 0.0157f, 0.0209f,
0.0257f, 0.0321f, 0.0392f, 0.0469f, 0.0566f, 0.0656f, 0.0768f, 0.0887f, 0.1033f, 0.1186f,
0.1349f, 0.1519f, 0.1696f, 0.1928f, 0.2121f, 0.237f, 0.2627f, 0.2892f, 0.3109f, 0.3386f,
0.3667f, 0.3952f, 0.4241f, 0.4474f, 0.4766f, 0.5f, 0.5234f, 0.5468f, 0.5701f, 0.5933f,
0.6134f, 0.6333f, 0.6531f, 0.6698f, 0.6891f, 0.7054f, 0.7214f, 0.7346f, 0.7502f, 0.763f,
0.7756f, 0.7879f, 0.8f, 0.8107f, 0.8212f, 0.8326f, 0.8415f, 0.8503f, 0.8588f, 0.8672f,
0.8754f, 0.8833f, 0.8911f, 0.8977f, 0.9041f, 0.9113f, 0.9165f, 0.9232f, 0.9281f, 0.9328f,
0.9382f, 0.9434f, 0.9476f, 0.9518f, 0.9557f, 0.9596f, 0.9632f, 0.9662f, 0.9695f, 0.9722f,
0.9753f, 0.9777f, 0.9805f, 0.9826f, 0.9847f, 0.9866f, 0.9884f, 0.9901f, 0.9917f, 0.9931f,
0.9944f, 0.9955f, 0.9964f, 0.9973f, 0.9981f, 0.9986f, 0.9992f, 0.9995f, 0.9998f, 1.0f, 1.0f
};
private static final float STEP_SIZE = 1.0f / (VALUES.length - 1);
@Override
public float getInterpolation(float input) {
if (input >= 1.0f) {
return 1.0f;
}
if (input <= 0f) {
return 0f;
}
int position = Math.min(
(int)(input * (VALUES.length - 1)),
VALUES.length - 2);
float quantized = position * STEP_SIZE;
float difference = input - quantized;
float weight = difference / STEP_SIZE;
return VALUES[position] + weight * (VALUES[position + 1] - VALUES[position]);
}
}
}

View File

@ -1,617 +1,354 @@
/* 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/. */
* 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/. */
let Ci = Components.interfaces, Cc = Components.classes, Cu = Components.utils;
"use strict";
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/DownloadUtils.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/PluralForm.jsm");
Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
const Cu = Components.utils;
Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
XPCOMUtils.defineLazyModuleGetter(this, "Downloads", "resource://gre/modules/Downloads.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "DownloadUtils", "resource://gre/modules/DownloadUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PluralForm", "resource://gre/modules/PluralForm.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Services", "resource://gre/modules/Services.jsm");
let gStrings = Services.strings.createBundle("chrome://browser/locale/aboutDownloads.properties");
XPCOMUtils.defineLazyGetter(this, "strings",
() => Services.strings.createBundle("chrome://browser/locale/aboutDownloads.properties"));
let downloadTemplate =
"<li downloadGUID='{guid}' class='list-item' role='button' state='{state}' contextmenu='downloadmenu'>" +
"<img class='icon' src='{icon}'/>" +
"<div class='details'>" +
"<div class='row'>" +
// This is a hack so that we can crop this label in its center
"<xul:label class='title' crop='center' value='{target}'/>" +
"<div class='date'>{date}</div>" +
"</div>" +
"<div class='size'>{size}</div>" +
"<div class='domain'>{domain}</div>" +
"<div class='displayState'>{displayState}</div>" +
"</div>" +
"</li>";
function deleteDownload(download) {
download.finalize(true).then(null, Cu.reportError);
OS.File.remove(download.target.path).then(null, ex => {
if (!(ex instanceof OS.File.Error && ex.becauseNoSuchFile)) {
Cu.reportError(ex);
}
});
}
XPCOMUtils.defineLazyGetter(window, "gChromeWin", function ()
window.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsIDocShellTreeItem)
.rootTreeItem
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindow)
.QueryInterface(Ci.nsIDOMChromeWindow));
let contextMenu = {
_items: [],
_targetDownload: null,
init: function () {
let element = document.getElementById("downloadmenu");
element.addEventListener("click",
event => event.download = this._targetDownload,
true);
var ContextMenus = {
target: null,
init: function() {
document.addEventListener("contextmenu", this, false);
document.getElementById("contextmenu-open").addEventListener("click", this.open.bind(this), false);
document.getElementById("contextmenu-retry").addEventListener("click", this.retry.bind(this), false);
document.getElementById("contextmenu-remove").addEventListener("click", this.remove.bind(this), false);
document.getElementById("contextmenu-pause").addEventListener("click", this.pause.bind(this), false);
document.getElementById("contextmenu-resume").addEventListener("click", this.resume.bind(this), false);
document.getElementById("contextmenu-cancel").addEventListener("click", this.cancel.bind(this), false);
document.getElementById("contextmenu-removeall").addEventListener("click", this.removeAll.bind(this), false);
this.items = [
{ name: "open", states: [Downloads._dlmgr.DOWNLOAD_FINISHED] },
{ name: "retry", states: [Downloads._dlmgr.DOWNLOAD_FAILED, Downloads._dlmgr.DOWNLOAD_CANCELED] },
{ name: "remove", states: [Downloads._dlmgr.DOWNLOAD_FINISHED,Downloads._dlmgr.DOWNLOAD_FAILED, Downloads._dlmgr.DOWNLOAD_CANCELED] },
{ name: "removeall", states: [Downloads._dlmgr.DOWNLOAD_FINISHED,Downloads._dlmgr.DOWNLOAD_FAILED, Downloads._dlmgr.DOWNLOAD_CANCELED] },
{ name: "pause", states: [Downloads._dlmgr.DOWNLOAD_DOWNLOADING] },
{ name: "resume", states: [Downloads._dlmgr.DOWNLOAD_PAUSED] },
{ name: "cancel", states: [Downloads._dlmgr.DOWNLOAD_DOWNLOADING, Downloads._dlmgr.DOWNLOAD_NOTSTARTED, Downloads._dlmgr.DOWNLOAD_QUEUED, Downloads._dlmgr.DOWNLOAD_PAUSED] },
this._items = [
new ContextMenuItem("open",
download => download.succeeded,
download => download.launch().then(null, Cu.reportError)),
new ContextMenuItem("retry",
download => download.error ||
(download.canceled && !download.hasPartialData),
download => download.start().then(null, Cu.reportError)),
new ContextMenuItem("remove",
download => download.stopped,
download => {
Downloads.getList(Downloads.ALL)
.then(list => list.remove(download))
.then(null, Cu.reportError);
deleteDownload(download);
}),
new ContextMenuItem("pause",
download => !download.stopped,
download => download.cancel().then(null, Cu.reportError)),
new ContextMenuItem("resume",
download => download.canceled && download.hasPartialData,
download => download.start().then(null, Cu.reportError)),
new ContextMenuItem("cancel",
download => !download.stopped ||
(download.canceled && download.hasPartialData),
download => {
download.cancel().then(null, Cu.reportError);
download.removePartialData().then(null, Cu.reportError);
}),
// following menu item is a global action
new ContextMenuItem("removeall",
() => downloadLists.finished.length > 0,
() => downloadLists.removeFinished())
];
},
handleEvent: function(event) {
// store the target of context menu events so that we know which app to act on
this.target = event.target;
while (!this.target.hasAttribute("contextmenu")) {
this.target = this.target.parentNode;
addContextMenuEventListener: function (element) {
element.addEventListener("contextmenu", this.onContextMenu.bind(this));
},
onContextMenu: function (event) {
let target = event.target;
while (target && !target.download) {
target = target.parentNode;
}
if (!this.target)
if (!target) {
Cu.reportError("No download found for context menu target");
event.preventDefault();
return;
let state = parseInt(this.target.getAttribute("state"));
for (let i = 0; i < this.items.length; i++) {
var item = this.items[i];
let enabled = (item.states.indexOf(state) > -1);
if (enabled)
document.getElementById("contextmenu-" + item.name).removeAttribute("hidden");
else
document.getElementById("contextmenu-" + item.name).setAttribute("hidden", "true");
}
},
// Open shown only for downloads that completed successfully
open: function(event) {
Downloads.openDownload(this.target);
this.target = null;
},
// Retry shown when its failed, canceled, blocked(covered in failed, see _getState())
retry: function (event) {
Downloads.retryDownload(this.target);
this.target = null;
},
// Remove shown when its canceled, finished, failed(failed includes blocked and dirty, see _getState())
remove: function (event) {
Downloads.removeDownload(this.target);
this.target = null;
},
// Pause shown when item is currently downloading
pause: function (event) {
Downloads.pauseDownload(this.target);
this.target = null;
},
// Resume shown for paused items only
resume: function (event) {
Downloads.resumeDownload(this.target);
this.target = null;
},
// Cancel shown when its downloading, notstarted, queued or paused
cancel: function (event) {
Downloads.cancelDownload(this.target);
this.target = null;
},
removeAll: function(event) {
Downloads.removeAll();
this.target = null;
// capture the target download for menu items to use in a click event
this._targetDownload = target.download;
for (let item of this._items) {
item.updateVisibility(target.download);
}
}
};
function ContextMenuItem(name, isVisible, action) {
this.element = document.getElementById("contextmenu-" + name);
this.isVisible = isVisible;
this.element.addEventListener("click", event => action(event.download));
}
let Downloads = {
init: function dl_init() {
function onClick(evt) {
let target = evt.target;
while (target.nodeName != "li") {
target = target.parentNode;
if (!target)
return;
}
Downloads.openDownload(target);
}
this._normalList = document.getElementById("normal-downloads-list");
this._privateList = document.getElementById("private-downloads-list");
this._normalList.addEventListener("click", onClick, false);
this._privateList.addEventListener("click", onClick, false);
this._dlmgr = Cc["@mozilla.org/download-manager;1"].getService(Ci.nsIDownloadManager);
this._dlmgr.addPrivacyAwareListener(this);
Services.obs.addObserver(this, "last-pb-context-exited", false);
Services.obs.addObserver(this, "download-manager-remove-download-guid", false);
// If we have private downloads, show them all immediately. If we were to
// add them asynchronously, there's a small chance we could get a
// "last-pb-context-exited" notification before downloads are added to the
// list, meaning we'd show private downloads without any private tabs open.
let privateEntries = this.getDownloads({ isPrivate: true });
this._stepAddEntries(privateEntries, this._privateList, privateEntries.length);
// Add non-private downloads
let normalEntries = this.getDownloads({ isPrivate: false });
this._stepAddEntries(normalEntries, this._normalList, 1, this._scrollToSelectedDownload.bind(this));
ContextMenus.init();
},
uninit: function dl_uninit() {
let contextmenus = gChromeWin.NativeWindow.contextmenus;
contextmenus.remove(this.openMenuItem);
contextmenus.remove(this.removeMenuItem);
contextmenus.remove(this.pauseMenuItem);
contextmenus.remove(this.resumeMenuItem);
contextmenus.remove(this.retryMenuItem);
contextmenus.remove(this.cancelMenuItem);
contextmenus.remove(this.deleteAllMenuItem);
this._dlmgr.removeListener(this);
Services.obs.removeObserver(this, "last-pb-context-exited");
Services.obs.removeObserver(this, "download-manager-remove-download-guid");
},
onProgressChange: function(aWebProgress, aRequest, aCurSelfProgress, aMaxSelfProgress,
aCurTotalProgress, aMaxTotalProgress, aDownload) { },
onDownloadStateChange: function(aState, aDownload) {
switch (aDownload.state) {
case Ci.nsIDownloadManager.DOWNLOAD_FAILED:
case Ci.nsIDownloadManager.DOWNLOAD_CANCELED:
case Ci.nsIDownloadManager.DOWNLOAD_BLOCKED_PARENTAL:
case Ci.nsIDownloadManager.DOWNLOAD_DIRTY:
case Ci.nsIDownloadManager.DOWNLOAD_FINISHED:
// For all "completed" states, move them after active downloads
this._moveDownloadAfterActive(this._getElementForDownload(aDownload.guid));
// Fall-through the rest
case Ci.nsIDownloadManager.DOWNLOAD_SCANNING:
case Ci.nsIDownloadManager.DOWNLOAD_QUEUED:
case Ci.nsIDownloadManager.DOWNLOAD_DOWNLOADING:
let item = this._getElementForDownload(aDownload.guid);
if (item)
this._updateDownloadRow(item, aDownload);
else
this._insertDownloadRow(aDownload);
break;
}
},
onStateChange: function(aWebProgress, aRequest, aState, aStatus, aDownload) { },
onSecurityChange: function(aWebProgress, aRequest, aState, aDownload) { },
observe: function (aSubject, aTopic, aData) {
switch (aTopic) {
case "last-pb-context-exited":
this._privateList.innerHTML = "";
break;
case "download-manager-remove-download-guid": {
let guid = aSubject.QueryInterface(Ci.nsISupportsCString).data;
this._removeItem(this._getElementForDownload(guid));
break;
}
}
},
_moveDownloadAfterActive: function dl_moveDownloadAfterActive(aItem) {
// Move downloads that just reached a "completed" state below any active
try {
// Iterate down until we find a non-active download
let next = aItem.nextElementSibling;
while (next && this._inProgress(next.getAttribute("state")))
next = next.nextElementSibling;
// Move the item
aItem.parentNode.insertBefore(aItem, next);
} catch (ex) {
this.logError("_moveDownloadAfterActive() " + ex);
}
},
_inProgress: function dl_inProgress(aState) {
return [
this._dlmgr.DOWNLOAD_NOTSTARTED,
this._dlmgr.DOWNLOAD_QUEUED,
this._dlmgr.DOWNLOAD_DOWNLOADING,
this._dlmgr.DOWNLOAD_PAUSED,
this._dlmgr.DOWNLOAD_SCANNING,
].indexOf(parseInt(aState)) != -1;
},
_insertDownloadRow: function dl_insertDownloadRow(aDownload) {
let updatedState = this._getState(aDownload.state);
let item = this._createItem(downloadTemplate, {
guid: aDownload.guid,
target: aDownload.displayName,
icon: "moz-icon://" + aDownload.displayName + "?size=64",
date: DownloadUtils.getReadableDates(new Date())[0],
domain: DownloadUtils.getURIHost(aDownload.source.spec)[0],
size: this._getDownloadSize(aDownload.size),
displayState: this._getStateString(updatedState),
state: updatedState
});
list = aDownload.isPrivate ? this._privateList : this._normalList;
list.insertAdjacentHTML("afterbegin", item);
},
_getDownloadSize: function dl_getDownloadSize(aSize) {
if (aSize > 0) {
let displaySize = DownloadUtils.convertByteUnits(aSize);
return displaySize.join(""); // [0] is size, [1] is units
}
return gStrings.GetStringFromName("downloadState.unknownSize");
},
// Not all states are displayed as-is on mobile, some are translated to a generic state
_getState: function dl_getState(aState) {
let str;
switch (aState) {
// Downloading and Scanning states show up as "Downloading"
case this._dlmgr.DOWNLOAD_DOWNLOADING:
case this._dlmgr.DOWNLOAD_SCANNING:
str = this._dlmgr.DOWNLOAD_DOWNLOADING;
break;
// Failed, Dirty and Blocked states show up as "Failed"
case this._dlmgr.DOWNLOAD_FAILED:
case this._dlmgr.DOWNLOAD_DIRTY:
case this._dlmgr.DOWNLOAD_BLOCKED_POLICY:
case this._dlmgr.DOWNLOAD_BLOCKED_PARENTAL:
str = this._dlmgr.DOWNLOAD_FAILED;
break;
/* QUEUED and NOTSTARTED are not translated as they
dont fall under a common state but we still need
to display a common "status" on the UI */
default:
str = aState;
}
return str;
},
// Note: This doesn't cover all states as some of the states are translated in _getState()
_getStateString: function dl_getStateString(aState) {
let str;
switch (aState) {
case this._dlmgr.DOWNLOAD_DOWNLOADING:
str = "downloadState.downloading";
break;
case this._dlmgr.DOWNLOAD_CANCELED:
str = "downloadState.canceled";
break;
case this._dlmgr.DOWNLOAD_FAILED:
str = "downloadState.failed";
break;
case this._dlmgr.DOWNLOAD_PAUSED:
str = "downloadState.paused";
break;
// Queued and Notstarted show up as "Starting..."
case this._dlmgr.DOWNLOAD_QUEUED:
case this._dlmgr.DOWNLOAD_NOTSTARTED:
str = "downloadState.starting";
break;
default:
return "";
}
return gStrings.GetStringFromName(str);
},
_updateItem: function dl_updateItem(aItem, aValues) {
for (let i in aValues) {
aItem.querySelector("." + i).textContent = aValues[i];
}
},
_initStatement: function dv__initStatement(aIsPrivate) {
let dbConn = aIsPrivate ? this._dlmgr.privateDBConnection : this._dlmgr.DBConnection;
return dbConn.createStatement(
"SELECT guid, name, source, state, startTime, endTime, referrer, " +
"currBytes, maxBytes, state IN (?1, ?2, ?3, ?4, ?5) isActive " +
"FROM moz_downloads " +
"ORDER BY isActive DESC, endTime DESC, startTime DESC");
},
_createItem: function _createItem(aTemplate, aValues) {
function htmlEscape(s) {
s = s.replace(/&/g, "&amp;");
s = s.replace(/>/g, "&gt;");
s = s.replace(/</g, "&lt;");
s = s.replace(/"/g, "&quot;");
s = s.replace(/'/g, "&apos;");
return s;
}
let t = aTemplate;
for (let key in aValues) {
if (aValues.hasOwnProperty(key)) {
let regEx = new RegExp("{" + key + "}", "g");
let value = htmlEscape(aValues[key].toString());
t = t.replace(regEx, value);
}
}
return t;
},
_getEntry: function dv__getEntry(aStmt) {
try {
if (!aStmt.executeStep()) {
return null;
}
let updatedState = this._getState(aStmt.row.state);
// Try to get the attribute values from the statement
return {
guid: aStmt.row.guid,
target: aStmt.row.name,
icon: "moz-icon://" + aStmt.row.name + "?size=64",
date: DownloadUtils.getReadableDates(new Date(aStmt.row.endTime / 1000))[0],
domain: DownloadUtils.getURIHost(aStmt.row.source)[0],
size: this._getDownloadSize(aStmt.row.maxBytes),
displayState: this._getStateString(updatedState),
state: updatedState
};
} catch (e) {
// Something went wrong when stepping or getting values, so clear and quit
this.logError("_getEntry() " + e);
aStmt.reset();
return null;
}
},
_stepAddEntries: function dv__stepAddEntries(aEntries, aList, aNumItems, aCallback) {
if (aEntries.length == 0){
if (aCallback)
aCallback();
return;
}
let attrs = aEntries.shift();
let item = this._createItem(downloadTemplate, attrs);
aList.insertAdjacentHTML("beforeend", item);
// Add another item to the list if we should; otherwise, let the UI update
// and continue later
if (aNumItems > 1) {
this._stepAddEntries(aEntries, aList, aNumItems - 1, aCallback);
} else {
// Use a shorter delay for earlier downloads to display them faster
let delay = Math.min(aList.itemCount * 10, 300);
setTimeout(function () {
this._stepAddEntries(aEntries, aList, 5, aCallback);
}.bind(this), delay);
}
},
getDownloads: function dl_getDownloads(aParams) {
aParams = aParams || {};
let stmt = this._initStatement(aParams.isPrivate);
stmt.reset();
stmt.bindInt32Parameter(0, Ci.nsIDownloadManager.DOWNLOAD_NOTSTARTED);
stmt.bindInt32Parameter(1, Ci.nsIDownloadManager.DOWNLOAD_DOWNLOADING);
stmt.bindInt32Parameter(2, Ci.nsIDownloadManager.DOWNLOAD_PAUSED);
stmt.bindInt32Parameter(3, Ci.nsIDownloadManager.DOWNLOAD_QUEUED);
stmt.bindInt32Parameter(4, Ci.nsIDownloadManager.DOWNLOAD_SCANNING);
let entries = [];
while (entry = this._getEntry(stmt)) {
entries.push(entry);
}
stmt.finalize();
return entries;
},
_getElementForDownload: function dl_getElementForDownload(aKey) {
return document.body.querySelector("li[downloadGUID='" + aKey + "']");
},
_getDownloadForElement: function dl_getDownloadForElement(aElement, aCallback) {
let guid = aElement.getAttribute("downloadGUID");
this._dlmgr.getDownloadByGUID(guid, function(status, download) {
if (!Components.isSuccessCode(status)) {
return;
}
aCallback(download);
});
},
_removeItem: function dl_removeItem(aItem) {
// Make sure we have an item to remove
if (!aItem)
return;
aItem.parentNode.removeChild(aItem);
},
openDownload: function dl_openDownload(aItem) {
this._getDownloadForElement(aItem, function(aDownload) {
if (aDownload.state !== Ci.nsIDownloadManager.DOWNLOAD_FINISHED) {
// Do not open unfinished downloads.
return;
}
try {
let f = aDownload.targetFile;
if (f) f.launch();
} catch (ex) {
this.logError("openDownload() " + ex, aDownload);
}
}.bind(this));
},
removeDownload: function dl_removeDownload(aItem) {
this._getDownloadForElement(aItem, function(aDownload) {
if (aDownload.targetFile) {
OS.File.remove(aDownload.targetFile.path).then(null, function onError(reason) {
if (!(reason instanceof OS.File.Error && reason.becauseNoSuchFile)) {
this.logError("removeDownload() " + reason, aDownload);
}
}.bind(this));
}
aDownload.remove();
}.bind(this));
},
removeAll: function dl_removeAll() {
let title = gStrings.GetStringFromName("downloadAction.deleteAll");
let messageForm = gStrings.GetStringFromName("downloadMessage.deleteAll");
let elements = document.body.querySelectorAll("li[state='" + this._dlmgr.DOWNLOAD_FINISHED + "']," +
"li[state='" + this._dlmgr.DOWNLOAD_CANCELED + "']," +
"li[state='" + this._dlmgr.DOWNLOAD_FAILED + "']");
let message = PluralForm.get(elements.length, messageForm)
.replace("#1", elements.length);
let flags = Services.prompt.BUTTON_POS_0 * Services.prompt.BUTTON_TITLE_OK +
Services.prompt.BUTTON_POS_1 * Services.prompt.BUTTON_TITLE_CANCEL;
let choice = Services.prompt.confirmEx(null, title, message, flags,
null, null, null, null, {});
if (choice == 0) {
for (let i = 0; i < elements.length; i++) {
this.removeDownload(elements[i]);
}
}
},
pauseDownload: function dl_pauseDownload(aItem) {
this._getDownloadForElement(aItem, function(aDownload) {
try {
aDownload.pause();
this._updateDownloadRow(aItem, aDownload);
} catch (ex) {
this.logError("Error: pauseDownload() " + ex, aDownload);
}
}.bind(this));
},
resumeDownload: function dl_resumeDownload(aItem) {
this._getDownloadForElement(aItem, function(aDownload) {
try {
aDownload.resume();
this._updateDownloadRow(aItem, aDownload);
} catch (ex) {
this.logError("resumeDownload() " + ex, aDownload);
}
}.bind(this));
},
retryDownload: function dl_retryDownload(aItem) {
this._getDownloadForElement(aItem, function(aDownload) {
try {
this._removeItem(aItem);
aDownload.retry();
} catch (ex) {
this.logError("retryDownload() " + ex, aDownload);
}
}.bind(this));
},
cancelDownload: function dl_cancelDownload(aItem) {
this._getDownloadForElement(aItem, function(aDownload) {
OS.File.remove(aDownload.targetFile.path).then(null, function onError(reason) {
if (!(reason instanceof OS.File.Error && reason.becauseNoSuchFile)) {
this.logError("cancelDownload() " + reason, aDownload);
}
}.bind(this));
aDownload.cancel();
this._updateDownloadRow(aItem, aDownload);
}.bind(this));
},
_updateDownloadRow: function dl_updateDownloadRow(aItem, aDownload) {
try {
let updatedState = this._getState(aDownload.state);
aItem.setAttribute("state", updatedState);
this._updateItem(aItem, {
size: this._getDownloadSize(aDownload.size),
displayState: this._getStateString(updatedState),
date: DownloadUtils.getReadableDates(new Date())[0]
});
} catch (ex) {
this.logError("_updateDownloadRow() " + ex, aDownload);
}
},
/**
* In case a specific downloadId was passed while opening, scrolls the list to
* the given elemenet
*/
_scrollToSelectedDownload : function dl_scrollToSelected() {
let spec = document.location.href;
let pos = spec.indexOf("?");
let query = "";
if (pos >= 0)
query = spec.substring(pos + 1);
// Just assume the query is "id=<id>"
let id = query.substring(3);
if (!id) {
return;
}
downloadElement = this._getElementForDownload(id);
if (!downloadElement) {
return;
}
downloadElement.scrollIntoView();
},
/**
* Logs the error to the console.
*
* @param aMessage error message to log
* @param aDownload (optional) if given, and if the download is private, the
* log message is suppressed
*/
logError: function dl_logError(aMessage, aDownload) {
if (!aDownload || !aDownload.isPrivate) {
console.log("Error: " + aMessage);
}
},
QueryInterface: function (aIID) {
if (!aIID.equals(Ci.nsIDownloadProgressListener) &&
!aIID.equals(Ci.nsISupports))
throw Components.results.NS_ERROR_NO_INTERFACE;
return this;
ContextMenuItem.prototype = {
updateVisibility: function (download) {
this.element.hidden = !this.isVisible(download);
}
};
function DownloadListView(type, listElementId) {
this.listElement = document.getElementById(listElementId);
contextMenu.addContextMenuEventListener(this.listElement);
this.items = new Map();
Downloads.getList(type)
.then(list => list.addView(this))
.then(null, Cu.reportError);
window.addEventListener("unload", event => {
Downloads.getList(type)
.then(list => list.removeView(this))
.then(null, Cu.reportError);
});
}
document.addEventListener("DOMContentLoaded", Downloads.init.bind(Downloads), true);
window.addEventListener("unload", Downloads.uninit.bind(Downloads), false);
DownloadListView.prototype = {
get finished() {
let finished = [];
for (let download of this.items.keys()) {
if (download.stopped && (!download.hasPartialData || download.error)) {
finished.push(download);
}
}
return finished;
},
insertOrMoveItem: function (item) {
var compare = (a, b) => {
// active downloads always before stopped downloads
if (a.stopped != b.stopped) {
return b.stopped ? -1 : 1
}
// most recent downloads first
return b.startTime - a.startTime;
};
let insertLocation = this.listElement.firstChild;
while (insertLocation && compare(item.download, insertLocation.download) > 0) {
insertLocation = insertLocation.nextElementSibling;
}
this.listElement.insertBefore(item.element, insertLocation);
},
onDownloadAdded: function (download) {
let item = new DownloadItem(download);
this.items.set(download, item);
this.insertOrMoveItem(item);
},
onDownloadChanged: function (download) {
let item = this.items.get(download);
if (!item) {
Cu.reportError("No DownloadItem found for download");
return;
}
if (item.stateChanged) {
this.insertOrMoveItem(item);
}
item.onDownloadChanged();
},
onDownloadRemoved: function (download) {
let item = this.items.get(download);
if (!item) {
Cu.reportError("No DownloadItem found for download");
return;
}
this.items.delete(download);
this.listElement.removeChild(item.element);
}
};
let downloadLists = {
init: function () {
this.publicDownloads = new DownloadListView(Downloads.PUBLIC, "public-downloads-list");
this.privateDownloads = new DownloadListView(Downloads.PRIVATE, "private-downloads-list");
},
get finished() {
return this.publicDownloads.finished.concat(this.privateDownloads.finished);
},
removeFinished: function () {
let finished = this.finished;
if (finished.length == 0) {
return;
}
let title = strings.GetStringFromName("downloadAction.deleteAll");
let messageForm = strings.GetStringFromName("downloadMessage.deleteAll");
let message = PluralForm.get(finished.length, messageForm).replace("#1", finished.length);
if (Services.prompt.confirm(null, title, message)) {
Downloads.getList(Downloads.ALL)
.then(list => {
for (let download of finished) {
list.remove(download).then(null, Cu.reportError);
deleteDownload(download);
}
}, Cu.reportError);
}
}
};
function DownloadItem(download) {
this._download = download;
this._updateFromDownload();
this._domain = DownloadUtils.getURIHost(download.source.url)[0];
this._fileName = this._htmlEscape(OS.Path.basename(download.target.path));
this._iconUrl = "moz-icon://" + this._fileName + "?size=64";
this._startDate = this._htmlEscape(DownloadUtils.getReadableDates(download.startTime)[0]);
this._element = this.createElement();
}
const kDownloadStatePropertyNames = [
"stopped",
"succeeded",
"canceled",
"error",
"startTime"
];
DownloadItem.prototype = {
_htmlEscape : function (s) {
s = s.replace(/&/g, "&amp;");
s = s.replace(/>/g, "&gt;");
s = s.replace(/</g, "&lt;");
s = s.replace(/"/g, "&quot;");
s = s.replace(/'/g, "&apos;");
return s;
},
_updateFromDownload: function () {
this._state = {};
kDownloadStatePropertyNames.forEach(
name => this._state[name] = this._download[name],
this);
},
get stateChanged() {
return kDownloadStatePropertyNames.some(
name => this._state[name] != this._download[name],
this);
},
get download() this._download,
get element() this._element,
createElement: function() {
let template = document.getElementById("download-item");
// TODO: use this once <template> is working
// let element = document.importNode(template.content, true);
// simulate a <template> node...
let element = template.cloneNode(true);
element.removeAttribute("id");
element.removeAttribute("style");
// launch the download if clicked
element.addEventListener("click", this.onClick.bind(this));
// set download as an expando property for the context menu
element.download = this.download;
// fill in template placeholders
this.updateElement(element);
return element;
},
updateElement: function (element) {
element.querySelector(".date").textContent = this.startDate;
element.querySelector(".domain").textContent = this.domain;
element.querySelector(".icon").src = this.iconUrl;
element.querySelector(".size").textContent = this.size;
element.querySelector(".state").textContent = this.stateDescription;
element.querySelector(".title").setAttribute("value", this.fileName);
},
onClick: function (event) {
if (this.download.succeeded) {
this.download.launch().then(null, Cu.reportError);
}
},
onDownloadChanged: function () {
this._updateFromDownload();
this.updateElement(this.element);
},
// template properties below
get domain() this._domain,
get fileName() this._fileName,
get id() this._id,
get iconUrl() this._iconUrl,
get size() {
if (this.download.hasProgress) {
return DownloadUtils.convertByteUnits(this.download.totalBytes).join("");
}
return strings.GetStringFromName("downloadState.unknownSize");
},
get startDate() {
return this._startDate;
},
get stateDescription() {
let name;
if (this.download.error) {
name = "downloadState.failed";
} else if (this.download.canceled) {
if (this.download.hasPartialData) {
name = "downloadState.paused";
} else {
name = "downloadState.canceled";
}
} else if (!this.download.stopped) {
if (this.download.currentBytes > 0) {
name = "downloadState.downloading";
} else {
name = "downloadState.starting";
}
}
if (name) {
return strings.GetStringFromName(name);
}
return "";
}
};
window.addEventListener("DOMContentLoaded", event => {
contextMenu.init();
downloadLists.init()
});

View File

@ -35,11 +35,27 @@
<menuitem id="contextmenu-removeall" label="&aboutDownloads.removeAll;"></menuitem>
</menu>
<!--template id="download-item"-->
<li id="download-item" class="list-item" role="button" contextmenu="downloadmenu" style="display: none">
<img class="icon" src=""/>
<div class="details">
<div class="row">
<!-- This is a hack so that we can crop this label in its center -->
<xul:label class="title" crop="center" value=""/>
<div class="date"></div>
</div>
<div class="size"></div>
<div class="domain"></div>
<div class="state"></div>
</div>
</li>
<!--/template-->
<div class="header">
<div>&aboutDownloads.header;</div>
</div>
<ul id="private-downloads-list" class="list"></ul>
<ul id="normal-downloads-list" class="list"></ul>
<ul id="public-downloads-list" class="list"></ul>
<span id="no-downloads-indicator">&aboutDownloads.empty;</span>
<script type="application/javascript;version=1.8" src="chrome://browser/content/aboutDownloads.js"/>
</body>

View File

@ -13,6 +13,7 @@ let Cr = Components.results;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/AddonManager.jsm");
Cu.import("resource://gre/modules/DownloadNotifications.jsm");
Cu.import("resource://gre/modules/FileUtils.jsm");
Cu.import("resource://gre/modules/JNI.jsm");
Cu.import('resource://gre/modules/Payment.jsm');
@ -131,7 +132,7 @@ XPCOMUtils.defineLazyModuleGetter(this, "CharsetMenu",
// Lazily-loaded JS modules that use observer notifications
[
["Home", ["HomeBanner:Get", "HomePanels:Get", "HomePanels:Authenticate",
["Home", ["HomeBanner:Get", "HomePanels:Get", "HomePanels:Authenticate", "HomePanels:RefreshView",
"HomePanels:Installed", "HomePanels:Uninstalled"], "resource://gre/modules/Home.jsm"],
].forEach(module => {
let [name, notifications, resource] = module;
@ -360,7 +361,7 @@ var BrowserApp = {
NativeWindow.init();
LightWeightThemeWebInstaller.init();
Downloads.init();
DownloadNotifications.init();
FormAssistant.init();
IndexedDB.init();
HealthReportStatusListener.init();
@ -648,7 +649,7 @@ var BrowserApp = {
function(aTarget) {
aTarget.muted = true;
});
NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.unmute"),
NativeWindow.contextmenus.mediaContext("media-muted"),
function(aTarget) {
@ -741,6 +742,7 @@ var BrowserApp = {
shutdown: function shutdown() {
NativeWindow.uninit();
DownloadNotifications.uninit();
LightWeightThemeWebInstaller.uninit();
FormAssistant.uninit();
IndexedDB.uninit();
@ -1810,7 +1812,7 @@ var NativeWindow = {
return;
sendMessageToJava({
type: "Menu:Update",
type: "Menu:Update",
id: aId,
options: aOptions
});
@ -1836,7 +1838,7 @@ var NativeWindow = {
* automatically dismiss before this time.
* checkbox: A string to appear next to a checkbox under the notification
* message. The button callback functions will be called with
* the checked state as an argument.
* the checked state as an argument.
*/
show: function(aMessage, aValue, aButtons, aTabID, aOptions) {
if (aButtons == null) {
@ -2233,7 +2235,7 @@ var NativeWindow = {
mode: SelectionHandler.SELECT_AT_POINT,
x: x,
y: y
})) {
})) {
SelectionHandler.attachCaret(target);
}
}
@ -3110,7 +3112,7 @@ Tab.prototype = {
viewportWidth - 15);
},
/**
/**
* Reloads the tab with the desktop mode setting.
*/
reloadWithMode: function (aDesktopMode) {
@ -3740,7 +3742,7 @@ Tab.prototype = {
if (sizes == "any") {
// Since Java expects an integer, use -1 to represent icons with sizes="any"
maxSize = -1;
maxSize = -1;
} else {
let tokens = sizes.split(" ");
tokens.forEach(function(token) {
@ -6580,7 +6582,7 @@ var IdentityHandler = {
.QueryInterface(Components.interfaces.nsISSLStatusProvider)
.SSLStatus;
// Don't pass in the actual location object, since it can cause us to
// Don't pass in the actual location object, since it can cause us to
// hold on to the window object too long. Just pass in the fields we
// care about. (bug 424829)
let locationObj = {};
@ -6630,7 +6632,7 @@ var IdentityHandler = {
return result;
}
// Otherwise, we don't know the cert owner
result.owner = Strings.browser.GetStringFromName("identity.ownerUnknown3");
@ -7242,7 +7244,7 @@ var WebappsUI = {
favicon.src = WebappsUI.DEFAULT_ICON;
}
};
favicon.src = aIconURL;
},
@ -8419,3 +8421,21 @@ HTMLContextMenuItem.prototype = Object.create(ContextMenuItem.prototype, {
}
},
});
/**
* CID of Downloads.jsm's implementation of nsITransfer.
*/
const kTransferCid = Components.ID("{1b4c85df-cbdd-4bb6-b04e-613caece083c}");
/**
* Contract ID of the service implementing nsITransfer.
*/
const kTransferContractId = "@mozilla.org/transfer;1";
// Override Toolkit's nsITransfer implementation with the one from the
// JavaScript API for downloads. This will eventually be removed when
// nsIDownloadManager will not be available anymore (bug 851471). The
// old code in this module will be removed in bug 899110.
Components.manager.QueryInterface(Ci.nsIComponentRegistrar)
.registerFactory(kTransferCid, "",
kTransferContractId, null);

View File

@ -11,7 +11,6 @@
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<script type="application/javascript" src="chrome://browser/content/browser.js"/>
<script type="application/javascript" src="chrome://browser/content/downloads.js"/>
<deck id="browsers" flex="1"/>

View File

@ -1,298 +0,0 @@
// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
/* 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";
let Cu = Components.utils;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
function dump(a) {
Cc["@mozilla.org/consoleservice;1"].getService(Ci.nsIConsoleService).logStringMessage(a);
}
XPCOMUtils.defineLazyModuleGetter(this, "Notifications",
"resource://gre/modules/Notifications.jsm");
const URI_GENERIC_ICON_DOWNLOAD = "drawable://alert_download";
const URI_PAUSE_ICON = "drawable://pause";
const URI_CANCEL_ICON = "drawable://close";
const URI_RESUME_ICON = "drawable://play";
XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
var Downloads = {
_initialized: false,
_dlmgr: null,
_progressAlert: null,
_privateDownloads: [],
_showingPrompt: false,
_downloadsIdMap: {},
_getLocalFile: function dl__getLocalFile(aFileURI) {
// if this is a URL, get the file from that
// XXX it's possible that using a null char-set here is bad
const fileUrl = Services.io.newURI(aFileURI, null, null).QueryInterface(Ci.nsIFileURL);
return fileUrl.file.clone().QueryInterface(Ci.nsILocalFile);
},
init: function dl_init() {
if (this._initialized)
return;
this._initialized = true;
// Monitor downloads and display alerts
this._dlmgr = Cc["@mozilla.org/download-manager;1"].getService(Ci.nsIDownloadManager);
this._progressAlert = new AlertDownloadProgressListener();
this._dlmgr.addPrivacyAwareListener(this._progressAlert);
Services.obs.addObserver(this, "last-pb-context-exited", true);
},
openDownload: function dl_openDownload(aDownload) {
let fileUri = aDownload.target.spec;
let guid = aDownload.guid;
let f = this._getLocalFile(fileUri);
try {
f.launch();
} catch (ex) {
// in case we are not able to open the file (i.e. there is no app able to handle it)
// we just open the browser tab showing it
BrowserApp.addTab("about:downloads?id=" + guid);
}
},
cancelDownload: function dl_cancelDownload(aDownload) {
aDownload.cancel();
let fileURI = aDownload.target.spec;
let f = this._getLocalFile(fileURI);
OS.File.remove(f.path);
},
showCancelConfirmPrompt: function dl_showCancelConfirmPrompt(aDownload) {
if (this._showingPrompt)
return;
this._showingPrompt = true;
// Open a prompt that offers a choice to cancel the download
let title = Strings.browser.GetStringFromName("downloadCancelPromptTitle");
let message = Strings.browser.GetStringFromName("downloadCancelPromptMessage");
let flags = Services.prompt.BUTTON_POS_0 * Services.prompt.BUTTON_TITLE_YES +
Services.prompt.BUTTON_POS_1 * Services.prompt.BUTTON_TITLE_NO;
let choice = Services.prompt.confirmEx(null, title, message, flags,
null, null, null, null, {});
if (choice == 0)
this.cancelDownload(aDownload);
this._showingPrompt = false;
},
handleClickEvent: function dl_handleClickEvent(aDownload) {
// Only open the downloaded file if the download is complete
if (aDownload.state == Ci.nsIDownloadManager.DOWNLOAD_FINISHED)
this.openDownload(aDownload);
else if (aDownload.state == Ci.nsIDownloadManager.DOWNLOAD_DOWNLOADING ||
aDownload.state == Ci.nsIDownloadManager.DOWNLOAD_PAUSED)
this.showCancelConfirmPrompt(aDownload);
},
clickCallback: function dl_clickCallback(aDownloadId) {
this._dlmgr.getDownloadByGUID(aDownloadId, (function(status, download) {
if (Components.isSuccessCode(status))
this.handleClickEvent(download);
}).bind(this));
},
pauseClickCallback: function dl_buttonPauseCallback(aDownloadId) {
this._dlmgr.getDownloadByGUID(aDownloadId, (function(status, download) {
if (Components.isSuccessCode(status))
download.pause();
}).bind(this));
},
resumeClickCallback: function dl_buttonPauseCallback(aDownloadId) {
this._dlmgr.getDownloadByGUID(aDownloadId, (function(status, download) {
if (Components.isSuccessCode(status))
download.resume();
}).bind(this));
},
cancelClickCallback: function dl_buttonPauseCallback(aDownloadId) {
this._dlmgr.getDownloadByGUID(aDownloadId, (function(status, download) {
if (Components.isSuccessCode(status))
this.cancelDownload(download);
}).bind(this));
},
notificationCanceledCallback: function dl_notifCancelCallback(aId, aDownloadId) {
let notificationId = this._downloadsIdMap[aDownloadId];
if (notificationId && notificationId == aId)
delete this._downloadsIdMap[aDownloadId];
},
createNotification: function dl_createNotif(aDownload, aOptions) {
let notificationId = Notifications.create(aOptions);
this._downloadsIdMap[aDownload.guid] = notificationId;
},
updateNotification: function dl_updateNotif(aDownload, aOptions) {
let notificationId = this._downloadsIdMap[aDownload.guid];
if (notificationId)
Notifications.update(notificationId, aOptions);
},
cancelNotification: function dl_cleanNotif(aDownload) {
Notifications.cancel(this._downloadsIdMap[aDownload.guid]);
delete this._downloadsIdMap[aDownload.guid];
},
// observer for last-pb-context-exited
observe: function dl_observe(aSubject, aTopic, aData) {
let download;
while ((download = this._privateDownloads.pop())) {
try {
let notificationId = aDownload.guid;
Notifications.clear(notificationId);
Downloads.removeNotification(download);
} catch (e) {
dump("Error removing private download: " + e);
}
}
},
QueryInterface: function (aIID) {
if (!aIID.equals(Ci.nsISupports) &&
!aIID.equals(Ci.nsIObserver) &&
!aIID.equals(Ci.nsISupportsWeakReference))
throw Components.results.NS_ERROR_NO_INTERFACE;
return this;
}
};
const PAUSE_BUTTON = {
buttonId: "pause",
title : Strings.browser.GetStringFromName("alertDownloadsPause"),
icon : URI_PAUSE_ICON,
onClicked: function (aId, aCookie) {
Downloads.pauseClickCallback(aCookie);
}
};
const CANCEL_BUTTON = {
buttonId: "cancel",
title : Strings.browser.GetStringFromName("alertDownloadsCancel"),
icon : URI_CANCEL_ICON,
onClicked: function (aId, aCookie) {
Downloads.cancelClickCallback(aCookie);
}
};
const RESUME_BUTTON = {
buttonId: "resume",
title : Strings.browser.GetStringFromName("alertDownloadsResume"),
icon: URI_RESUME_ICON,
onClicked: function (aId, aCookie) {
Downloads.resumeClickCallback(aCookie);
}
};
function DownloadNotifOptions (aDownload, aTitle, aMessage) {
this.icon = URI_GENERIC_ICON_DOWNLOAD;
this.onCancel = function (aId, aCookie) {
Downloads.notificationCanceledCallback(aId, aCookie);
}
this.onClick = function (aId, aCookie) {
Downloads.clickCallback(aCookie);
}
this.title = aTitle;
this.message = aMessage;
this.buttons = null;
this.cookie = aDownload.guid;
this.persistent = true;
}
function DownloadProgressNotifOptions (aDownload, aButtons) {
DownloadNotifOptions.apply(this, [aDownload, aDownload.displayName, aDownload.percentComplete + "%"]);
this.ongoing = true;
this.progress = aDownload.percentComplete;
this.buttons = aButtons;
}
// AlertDownloadProgressListener is used to display progress in the alert notifications.
function AlertDownloadProgressListener() { }
AlertDownloadProgressListener.prototype = {
//////////////////////////////////////////////////////////////////////////////
//// nsIDownloadProgressListener
onProgressChange: function(aWebProgress, aRequest, aCurSelfProgress, aMaxSelfProgress, aCurTotalProgress, aMaxTotalProgress, aDownload) {
let strings = Strings.browser;
let availableSpace = -1;
try {
// diskSpaceAvailable is not implemented on all systems
let availableSpace = aDownload.targetFile.diskSpaceAvailable;
} catch(ex) { }
let contentLength = aDownload.size;
if (availableSpace > 0 && contentLength > 0 && contentLength > availableSpace) {
Downloads.updateNotification(aDownload, new DownloadNotifOptions(aDownload,
strings.GetStringFromName("alertDownloadsNoSpace"),
strings.GetStringFromName("alertDownloadsSize")));
aDownload.cancel();
}
if (aDownload.percentComplete == -1) {
// Undetermined progress is not supported yet
return;
}
Downloads.updateNotification(aDownload, new DownloadProgressNotifOptions(aDownload, [PAUSE_BUTTON, CANCEL_BUTTON]));
},
onDownloadStateChange: function(aState, aDownload) {
let state = aDownload.state;
switch (state) {
case Ci.nsIDownloadManager.DOWNLOAD_QUEUED: {
NativeWindow.toast.show(Strings.browser.GetStringFromName("alertDownloadsToast"), "long");
Downloads.createNotification(aDownload, new DownloadNotifOptions(aDownload,
Strings.browser.GetStringFromName("alertDownloadsStart2"),
aDownload.displayName));
break;
}
case Ci.nsIDownloadManager.DOWNLOAD_PAUSED: {
Downloads.updateNotification(aDownload, new DownloadProgressNotifOptions(aDownload, [RESUME_BUTTON, CANCEL_BUTTON]));
break;
}
case Ci.nsIDownloadManager.DOWNLOAD_FAILED:
case Ci.nsIDownloadManager.DOWNLOAD_CANCELED:
case Ci.nsIDownloadManager.DOWNLOAD_BLOCKED_PARENTAL:
case Ci.nsIDownloadManager.DOWNLOAD_DIRTY:
case Ci.nsIDownloadManager.DOWNLOAD_FINISHED: {
Downloads.cancelNotification(aDownload);
if (aDownload.isPrivate) {
let index = Downloads._privateDownloads.indexOf(aDownload);
if (index != -1) {
Downloads._privateDownloads.splice(index, 1);
}
}
if (state == Ci.nsIDownloadManager.DOWNLOAD_FINISHED) {
Downloads.createNotification(aDownload, new DownloadNotifOptions(aDownload,
Strings.browser.GetStringFromName("alertDownloadsDone2"),
aDownload.displayName));
}
break;
}
}
},
onStateChange: function(aWebProgress, aRequest, aState, aStatus, aDownload) { },
onSecurityChange: function(aWebProgress, aRequest, aState, aDownload) { },
//////////////////////////////////////////////////////////////////////////////
//// nsISupports
QueryInterface: function (aIID) {
if (!aIID.equals(Ci.nsIDownloadProgressListener) &&
!aIID.equals(Ci.nsISupports))
throw Components.results.NS_ERROR_NO_INTERFACE;
return this;
}
};

View File

@ -34,7 +34,6 @@ chrome.jar:
* content/browser.js (content/browser.js)
content/bindings/checkbox.xml (content/bindings/checkbox.xml)
content/bindings/settings.xml (content/bindings/settings.xml)
content/downloads.js (content/downloads.js)
content/netError.xhtml (content/netError.xhtml)
content/SelectHelper.js (content/SelectHelper.js)
content/SelectionHandler.js (content/SelectionHandler.js)

View File

@ -427,6 +427,9 @@
@BINPATH@/components/TestInterfaceJS.manifest
#endif
@BINPATH@/components/Downloads.manifest
@BINPATH@/components/DownloadLegacy.js
; Modules
@BINPATH@/modules/*

View File

@ -0,0 +1,232 @@
/* 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";
this.EXPORTED_SYMBOLS = ["DownloadNotifications"];
const Cu = Components.utils;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Downloads", "resource://gre/modules/Downloads.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Notifications", "resource://gre/modules/Notifications.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Services", "resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyGetter(this, "strings",
() => Services.strings.createBundle("chrome://browser/locale/browser.properties"));
Object.defineProperty(this, "nativeWindow",
{ get: () => Services.wm.getMostRecentWindow("navigator:browser").NativeWindow });
const kButtons = {
PAUSE: new DownloadNotificationButton("pause",
"drawable://pause",
"alertDownloadsPause",
notification => notification.pauseDownload()),
RESUME: new DownloadNotificationButton("resume",
"drawable://play",
"alertDownloadsResume",
notification => notification.resumeDownload()),
CANCEL: new DownloadNotificationButton("cancel",
"drawable://close",
"alertDownloadsCancel",
notification => notification.cancelDownload())
};
let notifications = new Map();
var DownloadNotifications = {
init: function () {
if (!this._viewAdded) {
Downloads.getList(Downloads.ALL)
.then(list => list.addView(this))
.then(null, Cu.reportError);
this._viewAdded = true;
}
},
uninit: function () {
if (this._viewAdded) {
Downloads.getList(Downloads.ALL)
.then(list => list.removeView(this))
.then(null, Cu.reportError);
for (let notification of notifications.values()) {
notification.hide();
}
this._viewAdded = false;
}
},
onDownloadAdded: function (download) {
let notification = new DownloadNotification(download);
notifications.set(download, notification);
notification.showOrUpdate();
if (download.currentBytes == 0) {
nativeWindow.toast.show(strings.GetStringFromName("alertDownloadsToast"), "long");
}
},
onDownloadChanged: function (download) {
let notification = notifications.get(download);
if (!notification) {
Cu.reportError("Download doesn't have a notification.");
return;
}
notification.showOrUpdate();
},
onDownloadRemoved: function (download) {
let notification = notifications.get(download);
if (!notification) {
Cu.reportError("Download doesn't have a notification.");
return;
}
notification.hide();
notifications.delete(download);
}
};
function DownloadNotification(download) {
this.download = download;
this._fileName = OS.Path.basename(download.target.path);
this.id = null;
}
DownloadNotification.prototype = {
_updateFromDownload: function () {
this._downloading = !this.download.stopped;
this._paused = this.download.canceled && this.download.hasPartialData;
this._succeeded = this.download.succeeded;
this._show = this._downloading || this._paused || this._succeeded;
},
get options() {
if (!this._show) {
return null;
}
let options = {
icon : "drawable://alert_download",
onClick : (id, cookie) => this.onClick(),
onCancel : (id, cookie) => this._notificationId = null,
cookie : this.download
};
if (this._downloading) {
if (this.download.currentBytes == 0) {
this._updateOptionsForStatic(options, "alertDownloadsStart2");
} else {
this._updateOptionsForOngoing(options, [kButtons.PAUSE, kButtons.CANCEL]);
}
} else if (this._paused) {
this._updateOptionsForOngoing(options, [kButtons.RESUME, kButtons.CANCEL]);
} else if (this._succeeded) {
options.persistent = false;
this._updateOptionsForStatic(options, "alertDownloadsDone2");
}
return options;
},
_updateOptionsForStatic : function (options, titleName) {
options.title = strings.GetStringFromName(titleName);
options.message = this._fileName;
},
_updateOptionsForOngoing: function (options, buttons) {
options.title = this._fileName;
options.message = this.download.progress + "%";
options.buttons = buttons;
options.ongoing = true;
options.progress = this.download.progress;
options.persistent = true;
},
showOrUpdate: function () {
this._updateFromDownload();
if (this._show) {
if (!this.id) {
this.id = Notifications.create(this.options);
} else {
Notifications.update(this.id, this.options);
}
} else {
this.hide();
}
},
hide: function () {
if (this.id) {
Notifications.cancel(this.id);
this.id = null;
}
},
onClick: function () {
if (this.download.succeeded) {
this.download.launch().then(null, Cu.reportError);
} else {
ConfirmCancelPrompt.show(this);
}
},
pauseDownload: function () {
this.download.cancel().then(null, Cu.reportError);
},
resumeDownload: function () {
this.download.start().then(null, Cu.reportError);
},
cancelDownload: function () {
this.hide();
this.download.cancel().then(null, Cu.reportError);
this.download.removePartialData().then(null, Cu.reportError);
}
};
var ConfirmCancelPrompt = {
showing: false,
show: function (downloadNotification) {
if (this.showing) {
return;
}
this.showing = true;
// Open a prompt that offers a choice to cancel the download
let title = strings.GetStringFromName("downloadCancelPromptTitle");
let message = strings.GetStringFromName("downloadCancelPromptMessage");
if (Services.prompt.confirm(null, title, message)) {
downloadNotification.cancelDownload();
}
this.showing = false;
}
};
function DownloadNotificationButton(buttonId, iconUrl, titleStringName, onClicked) {
this.buttonId = buttonId;
this.title = strings.GetStringFromName(titleStringName);
this.icon = iconUrl;
this.onClicked = (id, download) => {
let notification = notifications.get(download);
if (!notification) {
Cu.reportError("No DownloadNotification for button");
return;
}
onClicked(notification);
}
}

View File

@ -218,6 +218,25 @@ let HomePanels = (function () {
options.auth.authenticate();
},
"HomePanels:RefreshView": function handlePanelsRefreshView(data) {
data = JSON.parse(data);
let options = _registeredPanels[data.panelId]();
let view = options.views[data.viewIndex];
if (!view) {
throw "Home.panels: Invalid view for panel.id = " + data.panelId
+ ", view.index = " + data.viewIndex;
}
if (!view.onrefresh || typeof view.onrefresh !== "function") {
throw "Home.panels: Invalid onrefresh for panel.id = " + data.panelId
+ ", view.index = " + data.viewIndex;
}
view.onrefresh();
},
"HomePanels:Installed": function handlePanelsInstalled(id) {
let options = _registeredPanels[id]();
if (!options.oninstall) {
@ -316,6 +335,10 @@ let HomePanels = (function () {
if (!view.dataset) {
throw "Home.panels: No dataset provided for view: panel.id = " + this.id + ", view.type = " + view.type;
}
if (view.onrefresh) {
view.refreshEnabled = true;
}
}
if (options.auth) {

View File

@ -7,6 +7,7 @@
EXTRA_JS_MODULES += [
'Accounts.jsm',
'ContactService.jsm',
'DownloadNotifications.jsm',
'HelperApps.jsm',
'Home.jsm',
'HomeProvider.jsm',

View File

@ -51,7 +51,7 @@ li:active div.details,
display: inline;
}
.displayState {
.state {
color: gray;
margin-bottom: -3px; /* Prevent overflow that hides bottom border */
}
@ -65,7 +65,7 @@ li:active div.details,
display: none;
}
#private-downloads-list:empty + #normal-downloads-list:empty + #no-downloads-indicator {
#private-downloads-list:empty + #public-downloads-list:empty + #no-downloads-indicator {
display: block;
text-align: center;
padding-top: 3.9em;

View File

@ -281,10 +281,6 @@ let AllPages = {
}
},
get updateScheduledForHiddenPages() {
return !!this._scheduleUpdateTimeout;
},
/**
* Implements the nsIObserver interface to get notified when the preference
* value changes or when a new copy of a page thumbnail is available.

View File

@ -15,3 +15,9 @@
; Persian
!define fa_rtl
; Uyghur
!define ug_rtl
; Urdu
!define ur_rtl

View File

@ -488,6 +488,7 @@ MetroInput::OnPointerNonTouch(UI::Input::IPointerPoint* aPoint) {
WidgetMouseEvent::eReal,
WidgetMouseEvent::eNormal);
event->button = button;
aPoint->get_PointerId(&event->pointerId);
InitGeckoMouseEventFromPointerPoint(event, aPoint);
DispatchAsyncEventIgnoreStatus(event);
}