mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
merge fx-team to mozilla-central
This commit is contained in:
commit
b2148d7f1c
@ -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);
|
||||
|
@ -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));
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -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);
|
||||
|
@ -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]
|
||||
|
19
browser/base/content/test/newtab/browser_newtab_bug991111.js
Normal file
19
browser/base/content/test/newtab/browser_newtab_bug991111.js
Normal 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");
|
||||
}
|
41
browser/base/content/test/newtab/browser_newtab_bug991210.js
Normal file
41
browser/base/content/test/newtab/browser_newtab_bug991210.js
Normal 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);
|
||||
}
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
|
@ -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()
|
||||
|
@ -25,7 +25,7 @@ function test() {
|
||||
openInspector((aInspector, aToolbox) => {
|
||||
inspector = aInspector;
|
||||
markupView = inspector.markup;
|
||||
inspector.once("inspector-updated", startTests);
|
||||
startTests();
|
||||
});
|
||||
}, content);
|
||||
}, true);
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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()
|
||||
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
2841
browser/extensions/shumway/content/playerglobal/playerglobal.json
Normal file
2841
browser/extensions/shumway/content/playerglobal/playerglobal.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -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
@ -1,2 +1,2 @@
|
||||
0.8.6
|
||||
4728574
|
||||
0.8.271
|
||||
2717b0c
|
||||
|
@ -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) {
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
|
@ -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());
|
||||
|
@ -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>() {
|
||||
|
@ -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);
|
||||
|
95
mobile/android/base/home/PanelRefreshLayout.java
Normal file
95
mobile/android/base/home/PanelRefreshLayout.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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',
|
||||
|
@ -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>
|
||||
|
||||
|
795
mobile/android/base/widget/GeckoSwipeRefreshLayout.java
Normal file
795
mobile/android/base/widget/GeckoSwipeRefreshLayout.java
Normal 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]);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -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, "&");
|
||||
s = s.replace(/>/g, ">");
|
||||
s = s.replace(/</g, "<");
|
||||
s = s.replace(/"/g, """);
|
||||
s = s.replace(/'/g, "'");
|
||||
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, "&");
|
||||
s = s.replace(/>/g, ">");
|
||||
s = s.replace(/</g, "<");
|
||||
s = s.replace(/"/g, """);
|
||||
s = s.replace(/'/g, "'");
|
||||
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()
|
||||
});
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
|
@ -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"/>
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
};
|
@ -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)
|
||||
|
@ -427,6 +427,9 @@
|
||||
@BINPATH@/components/TestInterfaceJS.manifest
|
||||
#endif
|
||||
|
||||
@BINPATH@/components/Downloads.manifest
|
||||
@BINPATH@/components/DownloadLegacy.js
|
||||
|
||||
; Modules
|
||||
@BINPATH@/modules/*
|
||||
|
||||
|
232
mobile/android/modules/DownloadNotifications.jsm
Normal file
232
mobile/android/modules/DownloadNotifications.jsm
Normal 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);
|
||||
}
|
||||
}
|
@ -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) {
|
||||
|
@ -7,6 +7,7 @@
|
||||
EXTRA_JS_MODULES += [
|
||||
'Accounts.jsm',
|
||||
'ContactService.jsm',
|
||||
'DownloadNotifications.jsm',
|
||||
'HelperApps.jsm',
|
||||
'Home.jsm',
|
||||
'HomeProvider.jsm',
|
||||
|
@ -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;
|
||||
|
@ -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.
|
||||
|
@ -15,3 +15,9 @@
|
||||
|
||||
; Persian
|
||||
!define fa_rtl
|
||||
|
||||
; Uyghur
|
||||
!define ug_rtl
|
||||
|
||||
; Urdu
|
||||
!define ur_rtl
|
||||
|
@ -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);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user