Bug 828088 - Rework richgrid and richgriditem bindings to use css columns for down-then-across grids. Hardening downloads and tests. r=fryn

--HG--
extra : amend_source : 745c26b0287819fd4d002e9ca193c13618bbca0a
This commit is contained in:
Sam Foster 2013-06-26 21:54:06 -07:00
parent 7deb49e1b6
commit d232cab7e4
14 changed files with 635 additions and 276 deletions

View File

@ -328,8 +328,8 @@ TopSitesView.prototype = {
for (let idx=0; idx < length; idx++) {
let isNew = !tileset.children[idx],
item = tileset.children[idx] || document.createElement("richgriditem"),
site = sites[idx];
let item = isNew ? tileset.createItemElement(site.title, site.url) : tileset.children[idx];
this.updateTile(item, site);
if (isNew) {

View File

@ -191,7 +191,9 @@
if (!referrer)
document.getAnonymousElementByAttribute(this, "anonid", "showpage-button").setAttribute("disabled", "true");
let file = Downloads._getLocalFile(this.getAttribute("target"));
if (!file) {
throw new Error("download-done: Couldn't bind item with target: "+this.getAttribute("target"));
}
let mimeService = Components.classes["@mozilla.org/mime;1"].getService(Components.interfaces.nsIMIMEService);
let mimeType;
try {

View File

@ -3,7 +3,6 @@
- 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/. -->
<bindings
xmlns="http://www.mozilla.org/xbl"
xmlns:xbl="http://www.mozilla.org/xbl"
@ -14,13 +13,17 @@
extends="chrome://global/content/bindings/general.xml#basecontrol">
<content>
<html:div id="grid-div" anonid="grid" class="richgrid-grid">
<html:div id="grid-div" anonid="grid" class="richgrid-grid" xbl:inherits="compact">
<children/>
</html:div>
</content>
<implementation implements="nsIDOMXULSelectControlElement">
<property name="_grid" readonly="true" onget="return document.getAnonymousElementByAttribute(this, 'anonid', 'grid');"/>
<property name="isBound" readonly="true" onget="return !!this._grid"/>
<property name="isArranging" readonly="true" onget="return !!this._scheduledArrangeItemsTimerId"/>
<field name="controller">null</field>
<!-- nsIDOMXULMultiSelectControlElement (not fully implemented) -->
@ -85,6 +88,8 @@
<parameter name="aItem"/>
<body>
<![CDATA[
if(!this.isBound)
return;
if (this.controller)
this.controller.handleItemClick(aItem);
]]>
@ -96,6 +101,8 @@
<parameter name="aEvent"/>
<body>
<![CDATA[
if(!this.isBound)
return;
// we'll republish this as a selectionchange event on the grid
aEvent.stopPropagation();
this.toggleItemSelection(aItem);
@ -180,7 +187,7 @@
<parameter name="aSkipArrange"/>
<body>
<![CDATA[
let addition = this._createItemElement(aLabel, aValue);
let addition = this.createItemElement(aLabel, aValue);
this.appendChild(addition);
if (!aSkipArrange)
this.arrangeItems();
@ -190,12 +197,14 @@
</method>
<method name="clearAll">
<parameter name="aSkipArrange"/>
<body>
<![CDATA[
while (this.firstChild) {
this.removeChild(this.firstChild);
}
this._grid.style.width = "0px";
if (!aSkipArrange)
this.arrangeItems();
]]>
</body>
</method>
@ -208,7 +217,7 @@
<body>
<![CDATA[
let existing = this.getItemAtIndex(anIndex);
let addition = this._createItemElement(aLabel, aValue);
let addition = this.createItemElement(aLabel, aValue);
if (existing) {
this.insertBefore(addition, existing);
} else {
@ -338,39 +347,39 @@
<field name="_columnCount">0</field>
<property name="columnCount" readonly="true" onget="return this._columnCount;"/>
<!-- define a height where we consider an item not yet rendered
10 is the height of the empty item (padding/border etc. only) -->
<field name="_itemHeightRenderThreshold">10</field>
<property name="_containerRect">
<property name="_containerSize">
<getter><![CDATA[
// return the rect that represents our bounding box
let containerNode = this.parentNode;
// Autocomplete is a binding within a binding, so we have to step
// up an additional parentNode.
let container = null;
if (this.parentNode.id == "results-vbox" ||
this.parentNode.id == "searches-vbox")
container = this.parentNode.parentNode.getBoundingClientRect();
else
container = this.parentNode.getBoundingClientRect();
return container;
if (containerNode.id == "results-vbox" ||
containerNode.id == "searches-vbox")
containerNode = containerNode.parentNode;
let rect = containerNode.getBoundingClientRect();
// return falsy if the container has no height
return rect.height ? {
width: rect.width,
height: rect.height
} : null;
]]></getter>
</property>
<property name="_itemRect">
<property name="_itemSize">
<getter><![CDATA[
// return the rect that represents an item in the grid
// return the dimensions that represent an item in the grid
// TODO: when we remove the need for DOM item measurement, 0 items will not be a problem
let item = this.itemCount ? this.getItemAtIndex(0) : null;
if (item) {
let gridItemRect = item.getBoundingClientRect();
if (gridItemRect.height > this._itemHeightRenderThreshold) {
return gridItemRect;
}
// grab tile/item dimensions
this._tileSizes = this._getTileSizes();
let type = this.getAttribute("tiletype") || "default";
let dims = this._tileSizes && this._tileSizes[type];
if (!dims) {
throw new Error("Missing tile sizes for '" + type + "' type");
}
return null;
return dims;
]]></getter>
</property>
@ -378,13 +387,18 @@
<property name="_canLayout" readonly="true">
<getter>
<![CDATA[
let gridItemRect = this._itemRect;
if (!(this._grid && this._grid.style)) {
return false;
}
let gridItemSize = this._itemSize;
// If we don't have valid item dimensions we can't arrange yet
if (!(gridItemRect && gridItemRect.height)) {
if (!(gridItemSize && gridItemSize.height)) {
return false;
}
let container = this._containerRect;
let container = this._containerSize;
// If we don't have valid container dimensions we can't arrange yet
if (!(container && container.height)) {
return false;
@ -420,15 +434,14 @@
if (this.hasAttribute("deferlayout")) {
return;
}
if (!this._canLayout) {
// try again later
this._scheduleArrangeItems();
return;
}
let gridItemRect = this._itemRect;
let container = this._containerRect;
let itemDims = this._itemSize;
let containerDims = this._containerSize;
// reset the flags
if (this._scheduledArrangeItemsTimerId) {
@ -437,21 +450,30 @@
}
this._scheduledArrangeItemsTries = 0;
// We favor overflowing horizontally, not vertically
let maxRowCount = Math.floor(container.height / gridItemRect.height) - 1;
// clear explicit width and columns before calculating from avail. height again
let gridStyle = this._grid.style;
gridStyle.removeProperty('min-width');
gridStyle.removeProperty('-moz-column-count');
this._rowCount = this.getAttribute("rows");
this._columnCount = this.getAttribute("columns");
// We favor overflowing horizontally, not vertically (rows then colums)
// rows attribute = max rows
let maxRowCount = Math.min(this.getAttribute("rows") || Infinity, Math.floor(containerDims.height / itemDims.height));
this._rowCount = Math.min(this.itemCount, maxRowCount);
if (!this._rowCount) {
this._rowCount = Math.min(this.itemCount, maxRowCount);
// columns attribute = min cols
this._columnCount = this.itemCount ?
Math.max(
// at least 1 column when there are items
this.getAttribute("columns") || 1,
Math.ceil(this.itemCount / this._rowCount)
) : this.getAttribute("columns") || 0;
// width is typically auto, cap max columns by truncating items collection
// or, setting max-width style property with overflow hidden
// '0' is an invalid value, just leave the property unset when 0 columns
if (this._columnCount) {
gridStyle.MozColumnCount = this._columnCount;
}
if (!this._columnCount){
this._columnCount = Math.ceil(this.itemCount / this._rowCount);
}
this._grid.style.width = (this._columnCount * gridItemRect.width) + "px";
]]>
</body>
</method>
@ -486,6 +508,7 @@
<![CDATA[
if (this.controller && this.controller.gridBoundCallback != undefined)
this.controller.gridBoundCallback();
// set up cross-slide gesture handling for multiple-selection grids
if ("undefined" !== typeof CrossSlide && "multiple" == this.getAttribute("seltype")) {
this._xslideHandler = new CrossSlide.Handler(this, {
@ -495,8 +518,9 @@
this.addEventListener("touchmove", this._xslideHandler, false);
this.addEventListener("touchend", this._xslideHandler, false);
}
// XXX This event was never actually implemented (bug 223411).
var event = document.createEvent("Events");
let event = document.createEvent("Events");
event.initEvent("contentgenerated", true, true);
this.dispatchEvent(event);
]]>
@ -511,6 +535,62 @@
}
]]>
</destructor>
<property name="tileWidth" readonly="true" onget="return this._itemSize.width"/>
<property name="tileHeight" readonly="true" onget="return this._itemSize.height"/>
<field name="_tileStyleSheetName">"tiles.css"</field>
<method name="_getTileSizes">
<body>
<![CDATA[
// Tile sizes are constants, this avoids the need to measure a rendered item before grid layout
// The defines.inc used by the theme CSS is the single source of truth for these values
// This method locates and parses out (just) those dimensions from the stylesheet
let typeSizes = this.ownerDocument.defaultView._richgridTileSizes;
if (typeSizes && typeSizes["default"]) {
return typeSizes;
}
// cache sizes on the global window object, for reuse between bound nodes
typeSizes = this.ownerDocument.defaultView._richgridTileSizes = {};
let sheets = this.ownerDocument.styleSheets;
// the (first matching) rules that will give us tile type => width/height values
let typeSelectors = {
"richgriditem" : "default",
"richgriditem[customImage]": "thumbnail",
"richgriditem[compact]": "compact"
};
let rules, sheet;
for (let i=0; (sheet=sheets[i]); i++) {
if (sheet.href && sheet.href.endsWith( this._tileStyleSheetName )) {
rules = sheet.cssRules;
break;
}
}
if (rules) {
// walk the stylesheet rules until we've matched all our selectors
for (let i=0, rule;(rule=rules[i]); i++) {
let type = rule.selectorText && typeSelectors[rule.selectorText];
if (type) {
let sizes = typeSizes[type] = {};
typeSelectors[type] = null;
delete typeSelectors[type];
// we assume px unit for tile dimension values
sizes.width = parseInt(rule.style.getPropertyValue("width"));
sizes.height = parseInt(rule.style.getPropertyValue("height"));
}
if (!Object.keys(typeSelectors).length)
break;
}
} else {
throw new Error("Failed to find stylesheet to parse out richgriditem dimensions\n");
}
return typeSizes;
]]>
</body>
</method>
<method name="_isIndexInBounds">
<parameter name="anIndex"/>
<body>
@ -520,13 +600,12 @@
</body>
</method>
<method name="_createItemElement">
<method name="createItemElement">
<parameter name="aLabel"/>
<parameter name="aValue"/>
<body>
<![CDATA[
let item = this.ownerDocument.createElement("richgriditem");
item.control = this;
item.setAttribute("label", aLabel);
if (aValue)
item.setAttribute("value", aValue);
@ -541,7 +620,7 @@
if (this.suppressOnSelect || this._suppressOnSelect)
return;
var event = document.createEvent("Events");
let event = document.createEvent("Events");
event.initEvent("select", true, true);
this.dispatchEvent(event);
]]>
@ -555,7 +634,7 @@
if (this.suppressOnSelect || this._suppressOnSelect)
return;
var event = document.createEvent("Events");
let event = document.createEvent("Events");
event.initEvent("selectionchange", true, true);
this.dispatchEvent(event);
]]>
@ -630,22 +709,30 @@
</handler>
</handlers>
</binding>
<binding id="richgrid-item">
<content>
<xul:vbox anonid="anon-richgrid-item" class="richgrid-item-content" xbl:inherits="customImage">
<xul:hbox class="richgrid-icon-container" xbl:inherits="customImage">
<xul:box class="richgrid-icon-box"><xul:image anonid="anon-richgrid-item-icon" xbl:inherits="src=iconURI"/></xul:box>
<xul:box flex="1" />
</xul:hbox>
<xul:description anonid="anon-richgrid-item-label" class="richgrid-item-desc" xbl:inherits="value=label" crop="end"/>
</xul:vbox>
<html:div anonid="anon-tile" class="tile-content" xbl:inherits="customImage">
<html:div class="tile-start-container" xbl:inherits="customImage">
<html:div class="tile-icon-box"><xul:image anonid="anon-tile-icon" xbl:inherits="src=iconURI"/></html:div>
</html:div>
<html:div anonid="anon-tile-label" class="tile-desc" xbl:inherits="xbl:text=label"/>
</html:div>
</content>
<implementation>
<property name="_box" onget="return document.getAnonymousElementByAttribute(this, 'anonid', 'anon-richgrid-item');"/>
<property name="_textbox" onget="return document.getAnonymousElementByAttribute(this, 'class', 'richgrid-item-desc');"/>
<property name="_icon" onget="return document.getAnonymousElementByAttribute(this, 'anonid', 'anon-richgrid-item-icon');"/>
<property name="_label" onget="return document.getAnonymousElementByAttribute(this, 'anonid', 'anon-richgrid-item-label');"/>
<property name="isBound" readonly="true" onget="return !!this._icon"/>
<constructor>
<![CDATA[
this.refresh();
]]>
</constructor>
<property name="_boundNode" onget="return document.getAnonymousElementByAttribute(this, 'anonid', 'anon-tile').parentNode;"/>
<property name="_contentBox" onget="return document.getAnonymousElementByAttribute(this, 'class', 'tile-content');"/>
<property name="_textbox" onget="return document.getAnonymousElementByAttribute(this, 'class', 'tile-desc');"/>
<property name="_top" onget="return document.getAnonymousElementByAttribute(this, 'class', 'tile-start-container');"/>
<property name="_icon" onget="return document.getAnonymousElementByAttribute(this, 'anonid', 'anon-tile-icon');"/>
<property name="_label" onget="return document.getAnonymousElementByAttribute(this, 'anonid', 'anon-tile-label');"/>
<property name="iconSrc"
onset="this._icon.src = val; this.setAttribute('iconURI', val);"
onget="return this._icon.src;" />
@ -663,16 +750,11 @@
onget="return this.hasAttribute('pinned')"
onset="if (val) { this.setAttribute('pinned', val) } else this.removeAttribute('pinned');"/>
<constructor>
<![CDATA[
this.refresh();
]]>
</constructor>
<method name="refresh">
<body>
<![CDATA[
// Prevent an exception in case binding is not done yet.
if (!this._icon)
if(!this.isBound)
return;
// Seed the binding properties from bound-node attribute values
@ -694,7 +776,7 @@
<property name="control">
<getter><![CDATA[
var parent = this.parentNode;
let parent = this.parentNode;
while (parent) {
if (parent instanceof Components.interfaces.nsIDOMXULSelectControlElement)
return parent;
@ -708,12 +790,10 @@
<setter><![CDATA[
if (val) {
this.setAttribute("customColor", val);
this._box.style.backgroundColor = val;
this._textbox.style.backgroundColor = val;
this._contentBox.style.backgroundColor = val;
} else {
this.removeAttribute("customColor");
this._box.style.removeProperty("background-color");
this._textbox.style.removeProperty("background-color");
this._contentBox.style.removeProperty("background-color");
}
]]></setter>
</property>
@ -722,19 +802,21 @@
<setter><![CDATA[
if (val) {
this.setAttribute("customImage", val);
this._box.style.backgroundImage = val;
this._top.style.backgroundImage = val;
} else {
this.removeAttribute("customImage");
this._box.style.removeProperty("background-image");
this._top.style.removeProperty("background-image");
}
]]></setter>
</property>
<method name="refreshBackgroundImage">
<body><![CDATA[
if(!this.isBound)
return;
if (this.backgroundImage) {
this._box.style.removeProperty("background-image");
this._box.style.setProperty("background-image", this.backgroundImage);
this._top.style.removeProperty("background-image");
this._top.style.setProperty("background-image", this.backgroundImage);
}
]]></body>
</method>
@ -772,12 +854,11 @@
<handler event="contextmenu">
<![CDATA[
// fires for right-click, long-click and (keyboard) contextmenu input
// TODO: handle cross-slide event when it becomes available,
// .. using contextmenu is a stop-gap measure to allow us to
// toggle the selected state of tiles in a grid
this.control.handleItemContextMenu(this, event);
]]>
</handler>
</handlers>
</binding>
</bindings>

View File

@ -9,6 +9,7 @@
<?xml-stylesheet href="chrome://browser/skin/forms.css" type="text/css"?>
<?xml-stylesheet href="chrome://browser/content/browser.css" type="text/css"?>
<?xml-stylesheet href="chrome://browser/skin/cssthrobber.css" type="text/css"?>
<?xml-stylesheet href="chrome://browser/skin/tiles.css" type="text/css"?>
<!DOCTYPE window [
<!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd">
@ -194,7 +195,7 @@
<scrollbox id="start-scrollbox" orient="horizontal" observes="bcast_preciseInput" flex="1">
<vbox id="start-topsites" class="meta-section">
<label class="meta-section-title" value="&startTopSitesHeader.label;"/>
<richgrid id="start-topsites-grid" rows="3" columns="3" seltype="multiple" flex="1"/>
<richgrid id="start-topsites-grid" rows="3" columns="3" tiletype="thumbnail" seltype="multiple" flex="1"/>
</vbox>
<vbox id="start-bookmarks" class="meta-section">
<label class="meta-section-title" value="&startBookmarksHeader.label;"/>
@ -210,8 +211,9 @@
</vbox>
</scrollbox>
</hbox>
<!-- snapped view -->
<vbox id="snapped-start" class="start-page" observes="bcast_windowState">
<hbox id="snapped-start" class="start-page" flex="1" observes="bcast_windowState">
<scrollbox id="snapped-scrollbox" orient="vertical" flex="1">
<vbox id="snapped-topsites">
<label class="meta-section-title" value="&startTopSitesHeader.label;"/>
@ -224,7 +226,7 @@
<label id="snappedRemoteTabsLabel" class="meta-section-title" value="&snappedRemoteTabsHeader.label;"
onclick="PanelUI.show('remotetabs-container');" inputProcessing="true"/>
</scrollbox>
</vbox>
</hbox>
<!-- Autocompletion interface -->
<box id="start-autocomplete" observes="bcast_windowState"/>
</hbox>

View File

@ -20,8 +20,14 @@ var Downloads = {
_getLocalFile: function dh__getLocalFile(aFileURI) {
// XXX it's possible that using a null char-set here is bad
const fileUrl = Services.io.newURI(aFileURI.spec || aFileURI, null, null).QueryInterface(Ci.nsIFileURL);
let spec = ('string' == typeof aFileURI) ? aFileURI : aFileURI.spec;
let fileUrl;
try {
fileUrl = Services.io.newURI(spec, null, null).QueryInterface(Ci.nsIFileURL);
} catch (ex) {
Util.dumpLn("_getLocalFile: Caught exception creating newURI from file spec: "+aFileURI.spec+": " + ex.message);
return;
}
return fileUrl.file.clone().QueryInterface(Ci.nsILocalFile);
},
@ -43,13 +49,21 @@ var Downloads = {
openDownload: function dh_openDownload(aDownload) {
// expects xul item
let id = aDownload.getAttribute("downloadId");
let download = this.manager.getDownload(id)
let download = this.manager.getDownload(id);
let fileURI = download.target;
let file = this._getLocalFile(fileURI);
if (!(fileURI && fileURI.spec)) {
Util.dumpLn("Cant open download "+id+", fileURI is invalid");
return;
}
let file = this._getLocalFile(fileURI);
try {
file.launch();
} catch (ex) { }
file && file.launch();
} catch (ex) {
Util.dumpLn("Failed to open download, with id: "+id+", download target URI spec: " + fileURI.spec);
Util.dumpLn("Failed download source:"+(aDownload.source && aDownload.source.spec));
}
},
removeDownload: function dh_removeDownload(aDownload) {
@ -66,14 +80,24 @@ var Downloads = {
cancelDownload: function dh_cancelDownload(aDownload) {
let id = aDownload.getAttribute("downloadId");
let download = this.manager.getDownload(id)
let download = this.manager.getDownload(id);
this.manager.cancelDownload(id);
let fileURI = download.target;
let file = this._getLocalFile(fileURI);
if (file.exists())
file.remove(false);
if (!(fileURI && fileURI.spec)) {
Util.dumpLn("Cant remove download file for: "+id+", fileURI is invalid");
return;
}
let file = this._getLocalFile(fileURI);
try {
if (file && file.exists())
file.remove(false);
} catch (ex) {
Util.dumpLn("Failed to cancel download, with id: "+id+", download target URI spec: " + fileURI.spec);
Util.dumpLn("Failed download source:"+(aDownload.source && aDownload.source.spec));
}
},
pauseDownload: function dh_pauseDownload(aDownload) {
@ -93,7 +117,7 @@ var Downloads = {
showPage: function dh_showPage(aDownload) {
let id = aDownload.getAttribute("downloadId");
let download = this.manager.getDownload(id)
let download = this.manager.getDownload(id);
let uri = this._getReferrerOrSource(download);
if (uri)
BrowserUI.newTab(uri, Browser.selectedTab);
@ -197,13 +221,14 @@ DownloadsView.prototype = {
},
_getAttrsForDownload: function dv__getAttrsForDownload(aDownload) {
// expects a DownloadManager download object
// params: nsiDownload
return {
typeName: 'download',
downloadId: aDownload.id,
downloadGuid: aDownload.guid,
name: aDownload.displayName,
target: aDownload.target,
// use the stringified version of the target nsIURI for the item attribute
target: aDownload.target.spec,
iconURI: "moz-icon://" + aDownload.displayName + "?size=64",
date: DownloadUtils.getReadableDates(new Date())[0],
domain: DownloadUtils.getURIHost(aDownload.source.spec)[0],
@ -215,6 +240,8 @@ DownloadsView.prototype = {
_updateItemWithAttrs: function dv__updateItemWithAttrs(anItem, aAttrs) {
for (let name in aAttrs)
anItem.setAttribute(name, aAttrs[name]);
if (anItem.refresh)
anItem.refresh();
},
_getDownloadSize: function dv__getDownloadSize (aSize) {
@ -322,8 +349,7 @@ DownloadsView.prototype = {
},
clearDownloads: function dv_clearDownloads() {
while (this._set.itemCount > 0)
this._set.removeItemAt(0);
this._set.clearAll();
},
addDownload: function dv_addDownload(aDownload) {

View File

@ -120,9 +120,8 @@ function test() {
/////////////////////////////////////
// shared test setup
function resetDownloads(){
var defd = Promise.defer();
// do the reset, resolve the defd when done
// TODO (sfoster) clear out downloads db, reset relevant state
// clear out existing and any pending downloads in the db
// returns a promise
let promisedResult = getPromisedDbResult(
"DELETE FROM moz_downloads"
@ -190,7 +189,7 @@ function addDownloadRow(aDataRow) {
function gen_addDownloadRows(aDataRows){
if (!aDataRows.length) {
yield;
yield null;
}
try {
@ -238,17 +237,17 @@ gTests.push({
yield resetDownloads();
let downloadslist = document.getElementById("downloads-list");
let downloadsList = document.getElementById("downloads-list");
// wait for the richgrid to announce its readiness
// .. fail a test if the timeout is exceeded
let isReady = waitForEvent(downloadslist, "DownloadsReady", 2000);
let isReady = waitForEvent(downloadsList, "DownloadsReady", 2000);
// tickle the view to cause it to refresh itself
DownloadsPanelView._view.getDownloads();
yield isReady;
let count = downloadslist.children.length;
let count = downloadsList.children.length;
is(count, 0, "Zero items in grid view with empty downloads db");
}
});
@ -272,11 +271,16 @@ gTests.push({
{ endTime: 1180493839859232, state: nsIDM.DOWNLOAD_CANCELED },
{ endTime: 1180493839859231, state: nsIDM.DOWNLOAD_BLOCKED_PARENTAL },
{ endTime: 1180493839859230, state: nsIDM.DOWNLOAD_DIRTY },
{ endTime: 1180493839859229, state: nsIDM.DOWNLOAD_BLOCKED_POLICY },
{ endTime: 1180493839859229, state: nsIDM.DOWNLOAD_BLOCKED_POLICY }
];
yield resetDownloads();
DownloadsPanelView._view.getDownloads();
// Test item data and count. This also tests the ordering of the display.
let downloadsList = document.getElementById("downloads-list");
// wait for the richgrid to announce its readiness
// .. fail a test if the timeout is exceeded
let isReady = waitForEvent(downloadsList, "DownloadsReady", 2000);
// NB: beware display limits which might cause mismatch btw. rendered item and db rows
@ -285,12 +289,6 @@ gTests.push({
// we're going to add stuff to the downloads db.
yield spawn( gen_addDownloadRows( DownloadData ) );
// Test item data and count. This also tests the ordering of the display.
let downloadslist = document.getElementById("downloads-list");
// wait for the richgrid to announce its readiness
// .. fail a test if the timeout is exceeded
let isReady = waitForEvent(downloadslist, "DownloadsReady", 2000);
// tickle the view to cause it to refresh itself
DownloadsPanelView._view.getDownloads();
@ -300,11 +298,11 @@ gTests.push({
ok(false, "DownloadsReady event never fired");
}
is(downloadslist.children.length, DownloadData.length,
is(downloadsList.children.length, DownloadData.length,
"There is the correct number of richlistitems");
for (let i = 0; i < downloadslist.children.length; i++) {
let element = downloadslist.children[i];
for (let i = 0; i < downloadsList.children.length; i++) {
let element = downloadsList.children[i];
let id = element.getAttribute("downloadId");
let dataItem = Downloads.manager.getDownload(id); // nsIDownload object
@ -365,10 +363,10 @@ gTests.push({
yield spawn( gen_addDownloadRows( DownloadData ) );
// Test item data and count. This also tests the ordering of the display.
let downloadslist = document.getElementById("downloads-list");
let downloadsList = document.getElementById("downloads-list");
// wait for the richgrid to announce its readiness
// .. fail a test if the timeout is exceeded
let isReady = waitForEvent(downloadslist, "DownloadsReady", 2000);
let isReady = waitForEvent(downloadsList, "DownloadsReady", 2000);
// tickle the view to cause it to refresh itself
DownloadsPanelView._view.getDownloads();
@ -395,7 +393,7 @@ gTests.push({
is(downloadRows.length, 3, "Correct number of downloads in the db before removal");
// remove the first one
let itemNode = downloadslist.children[0];
let itemNode = downloadsList.children[0];
let id = itemNode.getAttribute("downloadId");
// check the file exists
let download = Downloads.manager.getDownload( id );
@ -421,7 +419,7 @@ gTests.push({
is(downloadRows.length, 2, "Correct number of downloads in the db after removal");
is(2, downloadslist.children.length,
is(2, downloadsList.children.length,
"Removing a download updates the items list");
ok(file && file.exists(), "File still exists after download removal");

View File

@ -7,15 +7,32 @@
<?xml-stylesheet href="chrome://browser/skin/platform.css" type="text/css"?>
<?xml-stylesheet href="chrome://browser/skin/browser.css" type="text/css"?>
<?xml-stylesheet href="chrome://browser/content/browser.css" type="text/css"?>
<?xml-stylesheet href="chrome://browser/skin/tiles.css" type="text/css"?>
<!DOCTYPE window []>
<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<vbox flex="1">
<vbox id="alayout">
<richgrid id="grid_layout" seltype="single" flex="1">
</richgrid>
</vbox>
<vbox style="height:600px">
<hbox>
<richgrid id="clearGrid" seltype="single" flex="1" rows="2">
<richgriditem value="about:blank" id="clearGrid_item1" label="First item"/>
<richgriditem value="about:blank" id="clearGrid_item2" label="2nd item"/>
<richgriditem value="about:blank" id="clearGrid_item1" label="First item"/>
</richgrid>
</hbox>
<hbox>
<richgrid id="emptyGrid" seltype="single" flex="1" rows="2">
</richgrid>
</hbox>
<hbox>
<richgrid id="grid1" seltype="single" flex="1">
<richgriditem value="about:blank" id="grid1_item1" label="First item"/>
<richgriditem value="about:blank" id="grid1_item2" label="2nd item"/>
</richgrid>
</hbox>
<hbox>

View File

@ -20,7 +20,7 @@ gTests.push({
ok(grid, "#grid1 is found");
is(typeof grid.clearSelection, "function", "#grid1 has the binding applied");
is(grid.children.length, 1, "#grid1 has a single item");
is(grid.children.length, 2, "#grid1 has a 2 items");
is(grid.children[0].control, grid, "#grid1 item's control points back at #grid1'");
}
});
@ -62,9 +62,81 @@ gTests.push({
desc: "arrangeItems",
run: function() {
// implements an arrangeItems method, with optional cols, rows signature
let grid = doc.querySelector("#grid1");
let container = doc.getElementById("alayout");
let grid = doc.querySelector("#grid_layout");
is(typeof grid.arrangeItems, "function", "arrangeItems is a function on the grid");
todo(false, "Test outcome of arrangeItems with cols and rows arguments");
ok(grid.tileHeight, "grid has truthy tileHeight value");
ok(grid.tileWidth, "grid has truthy tileWidth value");
// make the container big enough for 3 rows
container.style.height = 3 * grid.tileHeight + 20 + "px";
// add some items
grid.appendItem("test title", "about:blank", true);
grid.appendItem("test title", "about:blank", true);
grid.appendItem("test title", "about:blank", true);
grid.appendItem("test title", "about:blank", true);
grid.appendItem("test title", "about:blank", true);
grid.arrangeItems();
// they should all fit nicely in a 3x2 grid
is(grid.rowCount, 3, "rowCount is calculated correctly for a given container height and tileheight");
is(grid.columnCount, 2, "columnCount is calculated correctly for a given container maxWidth and tilewidth");
// squish the available height
// should overflow (clip) a 2x2 grid
let under3rowsHeight = (3 * grid.tileHeight -20) + "px";
container.style.height = under3rowsHeight;
grid.arrangeItems();
is(grid.rowCount, 2, "rowCount is re-calculated correctly for a given container height");
}
});
gTests.push({
desc: "clearAll",
run: function() {
let grid = doc.getElementById("clearGrid");
grid.arrangeItems();
// grid has rows=2 so we expect at least 2 rows and 2 columns with 3 items
is(typeof grid.clearAll, "function", "clearAll is a function on the grid");
is(grid.itemCount, 3, "grid has 3 items initially");
is(grid.rowCount, 2, "grid has 2 rows initially");
is(grid.columnCount, 2, "grid has 2 cols initially");
let arrangeSpy = spyOnMethod(grid, "arrangeItems");
grid.clearAll();
is(grid.itemCount, 0, "grid has 0 itemCount after clearAll");
is(grid.children.length, 0, "grid has 0 children after clearAll");
is(grid.rowCount, 0, "grid has 0 rows when empty");
is(grid.columnCount, 0, "grid has 0 cols when empty");
is(arrangeSpy.callCount, 1, "arrangeItems is called once when we clearAll");
arrangeSpy.restore();
}
});
gTests.push({
desc: "empty grid",
run: function() {
let grid = doc.getElementById("emptyGrid");
grid.arrangeItems();
yield waitForCondition(() => !grid.isArranging);
// grid has rows=2 but 0 items
ok(grid.isBound, "binding was applied");
is(grid.itemCount, 0, "empty grid has 0 items");
is(grid.rowCount, 0, "empty grid has 0 rows");
is(grid.columnCount, 0, "empty grid has 0 cols");
let columnsNode = grid._grid;
let cStyle = doc.defaultView.getComputedStyle(columnsNode);
is(cStyle.getPropertyValue("-moz-column-count"), "auto", "empty grid has -moz-column-count: auto");
}
});

View File

@ -784,6 +784,24 @@ function runTests() {
});
}
// wrap a method with a spy that records how and how many times it gets called
// the spy is returned; use spy.restore() to put the original back
function spyOnMethod(aObj, aMethod) {
let origFunc = aObj[aMethod];
let spy = function() {
spy.calledWith = Array.slice(arguments);
spy.callCount++;
return (spy.returnValue = origFunc.apply(aObj, arguments));
};
spy.callCount = 0;
spy.restore = function() {
return (aObj[aMethod] = origFunc);
};
return (aObj[aMethod] = spy);
}
// replace a method with a stub that records how and how many times it gets called
// the stub is returned; use stub.restore() to put the original back
function stubMethod(aObj, aMethod) {
let origFunc = aObj[aMethod];
let func = function() {

View File

@ -122,7 +122,7 @@
opacity: 0;
transform: scale(0, 0);
}
100% {
opacity: 1;
transform: scale(1, 1);
@ -168,7 +168,7 @@ documenttab[closing] > .documenttab-container {
font-size: @metro_font_normal@;
width: @thumbnail_width@;
padding: 4px @metro_spacing_snormal@ 8px;
background: #000;
opacity: 0.95;
color: #fff;
@ -223,7 +223,7 @@ documenttab[selected] .documenttab-selection {
}
.selection-overlay {
pointer-events: none;
pointer-events: none;
}
.selection-overlay:-moz-focusring {
@ -732,13 +732,15 @@ setting[type="radio"] > vbox {
visibility: collapse;
}
/*tile content should be on same line in snapped view */
#snapped-topsites-grid > richgriditem > .richgrid-item-content {
-moz-box-orient: horizontal;
/* startUI sections, grids */
#start-container .meta-section {
/* allot space for at least a single column */
min-width: @grid_double_column_width@;
}
[viewstate="snapped"] .canSnapTiles .richgrid-item-desc {
-moz-margin-start: 8px;
#start-topsites {
/* allot space for 3 tile columns for the topsites grid */
min-width: calc(3 * @grid_double_column_width@);
}
/* if snapped, hide the fullscreen awesome screen, if viewstate is anything
@ -761,9 +763,9 @@ setting[type="radio"] > vbox {
#start-container[viewstate="snapped"] .meta-section {
margin: 0px;
min-width: @grid_double_column_width@;
}
/* Browser Content Areas ----------------------------------------------------- */
/* Hide the browser while the start UI is visible */

View File

@ -29,8 +29,16 @@
%define thumbnail_width 232px
%define thumbnail_height 148px
%define grid_column_width 131px
%define grid_double_column_width 262px
%define grid_row_height 86px
%define grid_double_row_height 172px
%define compactgrid_column_width 62px
%define compactgrid_row_height 62px
%define tile_border_color #dbdcde
%define tile_width 200px
%define tile_spacing 12px
%define scroller_thickness 4px
%define scroller_minimum 8px

View File

@ -15,6 +15,7 @@ chrome.jar:
skin/config.css (config.css)
* skin/forms.css (forms.css)
* skin/platform.css (platform.css)
* skin/tiles.css (tiles.css)
skin/touchcontrols.css (touchcontrols.css)
skin/netError.css (netError.css)
% override chrome://global/skin/about.css chrome://browser/skin/about.css

View File

@ -447,148 +447,6 @@ notification {
}
/* Rich Grid ---------------------------------------------------------------- */
richgrid {
display: -moz-box;
-moz-box-sizing: border-box;
}
richgrid .meta-grid {
display: block;
}
richgriditem {
padding: @metro_spacing_small@;
}
richgriditem .richgrid-item-content {
border: @metro_border_thin@ solid @tile_border_color@;
box-shadow: 0 0 @metro_spacing_snormal@ rgba(0, 0, 0, 0.1);
-moz-box-sizing: border-box;
padding: 10px 8px 6px 8px;
position: relative;
}
.richgrid-item-content {
background: #fff;
}
richgriditem[selected] .richgrid-item-content::after {
content: "";
pointer-events: none;
display: block;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
background-image: url(chrome://browser/skin/images/tile-selected-check-hdpi.png);
background-origin: border-box;
background-position: right 0 top 0;
background-repeat: no-repeat;
/* scale the image whatever the dppx */
background-size: 35px 35px;
border: @metro_border_xthick@ solid @selected_color@;
}
richgriditem[crosssliding] {
z-index: 1;
}
/* ease the return to original position when cross-sliding */
richgriditem:not([crosssliding]) {
transition: transform ease-out 0.2s;
}
richgriditem .richgrid-icon-container {
padding-bottom: 2px;
}
richgriditem .richgrid-icon-box {
padding: 4px;
background: #fff;
opacity: 1.0;
}
/* tile pinned-state indication */
richgriditem[pinned] .richgrid-item-content::before {
pointer-events:none;
content: "";
display: block;
position: absolute;
width: 35px;
height: 35px;
right: 0;
left: auto;
top: 0;
background-image: url(chrome://browser/skin/images/pinned-hdpi.png);
background-position: center;
background-repeat: no-repeat;
/* scale the image whatever the dppx */
background-size: 70px 70px;
}
/* Selected _and_ pinned tiles*/
richgriditem[selected][pinned] .richgrid-item-content::before {
background-position: right -@metro_border_xthick@ top -@metro_border_xthick@;
width: 70px;
height: 70px;
}
richgriditem[pinned]:-moz-locale-dir(rtl) .richgrid-item-content::before {
left: 0;
right: auto;
}
richgriditem[customColor] {
color: #f1f1f1;
}
richgriditem[customImage] {
color: #1a1a1a;
}
richgriditem[customColor] .richgrid-icon-box {
opacity: 0.8;
background-color: #fff;
}
.richgrid-item-content[customImage] {
height: 160px;
width: 250px;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
-moz-box-pack: end;
padding: 0px;
}
/* hide icon if there is an image background */
.richgrid-icon-container[customImage] {
visibility: collapse;
}
.richgrid-item-desc {
width: @tile_width@;
font-size: @metro_font_normal@;
margin-left: 0px;
padding-left: 0px !important;
}
.richgrid-item-content[customImage] > .richgrid-item-desc {
background: hsla(0,2%,98%,.95);
/*margin-bottom: 0px;
margin-right: 0px;*/
margin: 0px;
}
richgriditem image {
width: 24px;
height: 24px;
list-style-image: url("chrome://browser/skin/images/identity-icons-generic.png");
}
/* Dialogs ----------------------------------------------------------------- */
.modal-block,

View File

@ -0,0 +1,274 @@
/* 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/. */
/* Tile grid ------------------------------------------------------------- */
%filter substitution
%include defines.inc
/*
*****************************************************
The following rules define the key tile dimensions
They are (also) snarfed via the CSSOM as the dimensions used in the #richgrid binding
*****************************************************
*/
richgriditem {
width: @grid_double_column_width@;
height: @grid_row_height@;
}
richgriditem[customImage] {
width: @grid_double_column_width@;
height: @grid_double_row_height@;
}
richgriditem[compact] {
width: auto;
height: @compactgrid_row_height@;
}
/*
*****************************************************
*/
richgrid {
display: -moz-box;
}
richgrid > .richgrid-grid {
-moz-column-width: @grid_double_column_width@; /* tile width (2x unit + gutter) */
min-width: @grid_double_column_width@; /* min 1 column */
min-height: @grid_double_row_height@; /* 2 rows (or 1 double rows) minimum; multiple of tile_height */
-moz-column-fill: auto; /* do not attempt to balance content between columns */
-moz-column-gap: 0;
-moz-column-count: auto;
display: block;
-moz-box-sizing: content-box;
overflow-x: hidden; /* clipping will only kick in if an explicit width is set */
transition: 100ms transform ease-out;
}
richgriditem {
display: block;
position: relative;
width: @grid_double_column_width@;
height: @grid_row_height@;
-moz-box-sizing: border-box;
-moz-column-gap: 0;
overflow:hidden;
cursor: default;
transition: 300ms height ease-out,
150ms opacity ease-out,
100ms transform ease-out;
}
.tile-content {
display: block;
position: absolute;
background-color: #fff;
background-origin: padding-box;
/* content positioning within the grid "cell"
gives us the gutters/spacing between tiles */
top: 2px; right: 6px; bottom: 10px; left: 6px;
border: @metro_border_thin@ solid @tile_border_color@;
box-shadow: 0 0 5px 0 rgba(0, 0, 0, 0.1);
transition: 150ms transform ease-out;
}
.tile-start-container {
position: absolute;
top: 0;
bottom: 0;
right: 0;
left: 20px;
background: hsla(0,2%,98%,.95);
padding: 8px;
}
.tile-icon-box {
display: inline-block;
padding: 4px;
background: #fff;
opacity: 1.0;
}
.tile-icon-box > image {
width: 24px;
height: 24px;
list-style-image: url("chrome://browser/skin/images/identity-icons-generic.png");
}
.tile-desc {
display: block;
position: absolute;
bottom: 0;
right: 0;
left: 20px; /* the colored bar in the default tile is the background color peeking through */
z-index: 1;
padding: 4px 8px;
color: #333;
margin: 0;
-moz-margin-start: 0;
display: block;
font-size: 20px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
richgriditem.collapsed {
height: 0!important;
overflow: hidden;
opacity: 0;
}
richgriditem.collapsed > .tile-content {
transform: scaleY(0);
transition: 150ms transform ease-out 150ms;
}
richgriditem:active {
z-index: 2;
}
/* thumbnail variation */
richgriditem[customImage] {
width: @grid_double_column_width@;
height: @grid_double_row_height@;
-moz-box-pack: end;
padding: 0px;
color: #1a1a1a;
}
richgriditem[customImage] .tile-desc {
background: transparent;
margin: 0px;
left: 0;
}
richgriditem[customImage] > .tile-content > .tile-desc {
/* ensure thumbnail labels get their color from the parent richgriditem element */
color: inherit;
}
/* put the image in place of the icon if there is an image background */
richgriditem[customImage] > .tile-content > .tile-start-container {
background-size: cover;
background-position: top left;
background-repeat: no-repeat;
position: absolute;
top: 0;
bottom: 32px; /* TODO: should be some em value? */;
right: 0;
left: 0;
background-color: hsla(0,2%,98%,.95);
}
richgriditem[customImage] .tile-icon-box {
visibility: collapse;
}
/* selected tile indicator */
richgriditem[selected] > .tile-content::after {
content: "";
pointer-events: none;
display: block;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 1;
background-image: url(chrome://browser/skin/images/tile-selected-check-hdpi.png);
background-origin: border-box;
background-position: right 0 top 0;
background-repeat: no-repeat;
/* scale the image whatever the dppx */
background-size: 35px 35px;
border: @metro_border_xthick@ solid @selected_color@;
}
richgriditem[crosssliding] {
z-index: 10;
}
/* ease the return to original position when cross-sliding */
richgriditem:not([crosssliding]) {
transition: transform ease-out 0.2s;
}
/* tile pinned-state indication */
richgriditem[pinned] > .tile-content::before {
pointer-events:none;
content: "";
display: block;
position: absolute;
top: 0;
right: 0;
left: auto;
z-index: 1;
width: 35px;
height: 35px;
background-image: url(chrome://browser/skin/images/pinned-hdpi.png);
background-position: center;
background-repeat: no-repeat;
/* scale the image whatever the dppx */
background-size: 70px 70px;
}
/* Selected _and_ pinned tiles*/
richgriditem[selected][pinned] > .tile-content::before {
background-position: right -@metro_border_xthick@ top -@metro_border_xthick@;
width: 70px;
height: 70px;
}
richgriditem[pinned]:-moz-locale-dir(rtl) > .tile-content::before {
left: 0;
right: auto;
}
richgriditem[customColor] {
color: #f1f1f1;
}
/* Snapped-view variation
We use the compact, single-column grid treatment for <=320px */
@media (max-width: 330px) {
richgrid > .richgrid-grid {
-moz-column-width: auto!important; /* let it flow */
-moz-column-count: auto!important; /* let it flow */
height: auto; /* let it flow */
min-width: 280px;
transition: 100ms transform ease-out;
}
richgriditem {
width: @grid_double_column_width@;
overflow: hidden;
height: @compactgrid_row_height@;
}
.tile-desc {
top: 0;
left: 44px; /* label goes to the right of the favicon */
right: 0;
padding: 8px;
}
.tile-start-container {
position: absolute;
top: 0;
bottom: 0;
right: 0;
left: 6px;
background: #fff;
padding: 8px;
}
.tile-icon-box {
padding: 2px;
background: #fff;
opacity: 1.0;
}
}