mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
merge because ttaubert; a=#fx-team
This commit is contained in:
commit
e1a36d0912
119
browser/base/content/browser-thumbnails.js
Normal file
119
browser/base/content/browser-thumbnails.js
Normal file
@ -0,0 +1,119 @@
|
||||
#ifdef 0
|
||||
/* 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/. */
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Keeps thumbnails of open web pages up-to-date.
|
||||
*/
|
||||
let gBrowserThumbnails = {
|
||||
_captureDelayMS: 2000,
|
||||
|
||||
/**
|
||||
* Map of capture() timeouts assigned to their browsers.
|
||||
*/
|
||||
_timeouts: null,
|
||||
|
||||
/**
|
||||
* Cache for the PageThumbs module.
|
||||
*/
|
||||
_pageThumbs: null,
|
||||
|
||||
/**
|
||||
* List of tab events we want to listen for.
|
||||
*/
|
||||
_tabEvents: ["TabClose", "TabSelect"],
|
||||
|
||||
init: function Thumbnails_init() {
|
||||
gBrowser.addTabsProgressListener(this);
|
||||
|
||||
this._tabEvents.forEach(function (aEvent) {
|
||||
gBrowser.tabContainer.addEventListener(aEvent, this, false);
|
||||
}, this);
|
||||
|
||||
this._timeouts = new WeakMap();
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "_pageThumbs",
|
||||
"resource:///modules/PageThumbs.jsm", "PageThumbs");
|
||||
},
|
||||
|
||||
uninit: function Thumbnails_uninit() {
|
||||
gBrowser.removeTabsProgressListener(this);
|
||||
|
||||
this._tabEvents.forEach(function (aEvent) {
|
||||
gBrowser.tabContainer.removeEventListener(aEvent, this, false);
|
||||
}, this);
|
||||
|
||||
this._timeouts = null;
|
||||
this._pageThumbs = null;
|
||||
},
|
||||
|
||||
handleEvent: function Thumbnails_handleEvent(aEvent) {
|
||||
switch (aEvent.type) {
|
||||
case "TabSelect":
|
||||
this._delayedCapture(aEvent.target.linkedBrowser);
|
||||
break;
|
||||
case "TabClose": {
|
||||
let browser = aEvent.target.linkedBrowser;
|
||||
if (this._timeouts.has(browser)) {
|
||||
clearTimeout(this._timeouts.get(browser));
|
||||
this._timeouts.delete(browser);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* State change progress listener for all tabs.
|
||||
*/
|
||||
onStateChange: function Thumbnails_onStateChange(aBrowser, aWebProgress,
|
||||
aRequest, aStateFlags, aStatus) {
|
||||
if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
|
||||
aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK)
|
||||
this._delayedCapture(aBrowser);
|
||||
},
|
||||
|
||||
_capture: function Thumbnails_capture(aBrowser) {
|
||||
if (this._shouldCapture(aBrowser)) {
|
||||
let canvas = this._pageThumbs.capture(aBrowser.contentWindow);
|
||||
this._pageThumbs.store(aBrowser.currentURI.spec, canvas);
|
||||
}
|
||||
},
|
||||
|
||||
_delayedCapture: function Thumbnails_delayedCapture(aBrowser) {
|
||||
if (this._timeouts.has(aBrowser))
|
||||
clearTimeout(this._timeouts.get(aBrowser));
|
||||
|
||||
let timeout = setTimeout(function () {
|
||||
this._timeouts.delete(aBrowser);
|
||||
this._capture(aBrowser);
|
||||
}.bind(this), this._captureDelayMS);
|
||||
|
||||
this._timeouts.set(aBrowser, timeout);
|
||||
},
|
||||
|
||||
_shouldCapture: function Thumbnails_shouldCapture(aBrowser) {
|
||||
// There's no point in taking screenshot of loading pages.
|
||||
if (aBrowser.docShell.busyFlags != Ci.nsIDocShell.BUSY_FLAGS_NONE)
|
||||
return false;
|
||||
|
||||
// Don't take screenshots of about: pages.
|
||||
if (aBrowser.currentURI.schemeIs("about"))
|
||||
return false;
|
||||
|
||||
let channel = aBrowser.docShell.currentDocumentChannel;
|
||||
|
||||
try {
|
||||
// If the channel is a nsIHttpChannel get its http status code.
|
||||
let httpChannel = channel.QueryInterface(Ci.nsIHttpChannel);
|
||||
|
||||
// Continue only if we have a 2xx status code.
|
||||
return Math.floor(httpChannel.responseStatus / 100) == 2;
|
||||
} catch (e) {
|
||||
// Not a http channel, we just assume a success status code.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
};
|
@ -196,6 +196,7 @@ let gInitialPages = [
|
||||
#include browser-places.js
|
||||
#include browser-tabPreviews.js
|
||||
#include browser-tabview.js
|
||||
#include browser-thumbnails.js
|
||||
|
||||
#ifdef MOZ_SERVICES_SYNC
|
||||
#include browser-syncui.js
|
||||
@ -1699,6 +1700,7 @@ function delayedStartup(isLoadingBlank, mustLoadSidebar) {
|
||||
gSyncUI.init();
|
||||
#endif
|
||||
|
||||
gBrowserThumbnails.init();
|
||||
TabView.init();
|
||||
|
||||
setUrlAndSearchBarWidthForConditionalForwardButton();
|
||||
@ -1820,6 +1822,7 @@ function BrowserShutdown() {
|
||||
gPrefService.removeObserver(allTabs.prefName, allTabs);
|
||||
ctrlTab.uninit();
|
||||
TabView.uninit();
|
||||
gBrowserThumbnails.uninit();
|
||||
|
||||
try {
|
||||
FullZoom.destroy();
|
||||
|
@ -71,6 +71,7 @@ PARALLEL_DIRS = \
|
||||
shell \
|
||||
sidebar \
|
||||
tabview \
|
||||
thumbnails \
|
||||
migration \
|
||||
$(NULL)
|
||||
|
||||
|
2
browser/components/thumbnails/BrowserPageThumbs.manifest
Normal file
2
browser/components/thumbnails/BrowserPageThumbs.manifest
Normal file
@ -0,0 +1,2 @@
|
||||
component {5a4ae9b5-f475-48ae-9dce-0b4c1d347884} PageThumbsProtocol.js
|
||||
contract @mozilla.org/network/protocol;1?name=moz-page-thumb {5a4ae9b5-f475-48ae-9dce-0b4c1d347884}
|
23
browser/components/thumbnails/Makefile.in
Normal file
23
browser/components/thumbnails/Makefile.in
Normal file
@ -0,0 +1,23 @@
|
||||
# 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/.
|
||||
|
||||
DEPTH = ../../..
|
||||
topsrcdir = @top_srcdir@
|
||||
srcdir = @srcdir@
|
||||
VPATH = @srcdir@
|
||||
|
||||
include $(DEPTH)/config/autoconf.mk
|
||||
|
||||
EXTRA_COMPONENTS = \
|
||||
BrowserPageThumbs.manifest \
|
||||
PageThumbsProtocol.js \
|
||||
$(NULL)
|
||||
|
||||
ifdef ENABLE_TESTS
|
||||
DIRS += test
|
||||
endif
|
||||
|
||||
include $(topsrcdir)/config/rules.mk
|
||||
|
||||
XPIDL_FLAGS += -I$(topsrcdir)/browser/components/
|
448
browser/components/thumbnails/PageThumbsProtocol.js
Normal file
448
browser/components/thumbnails/PageThumbsProtocol.js
Normal file
@ -0,0 +1,448 @@
|
||||
/* 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/. */
|
||||
|
||||
/**
|
||||
* PageThumbsProtocol.js
|
||||
*
|
||||
* This file implements the moz-page-thumb:// protocol and the corresponding
|
||||
* channel delivering cached thumbnails.
|
||||
*
|
||||
* URL structure:
|
||||
*
|
||||
* moz-page-thumb://thumbnail?url=http%3A%2F%2Fwww.mozilla.org%2F
|
||||
*
|
||||
* This URL requests an image for 'http://www.mozilla.org/'.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const Cu = Components.utils;
|
||||
const Cc = Components.classes;
|
||||
const Cr = Components.results;
|
||||
const Ci = Components.interfaces;
|
||||
|
||||
Cu.import("resource:///modules/PageThumbs.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
|
||||
"resource://gre/modules/NetUtil.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Services",
|
||||
"resource://gre/modules/Services.jsm");
|
||||
|
||||
/**
|
||||
* Implements the thumbnail protocol handler responsible for moz-page-thumb: URIs.
|
||||
*/
|
||||
function Protocol() {
|
||||
}
|
||||
|
||||
Protocol.prototype = {
|
||||
/**
|
||||
* The scheme used by this protocol.
|
||||
*/
|
||||
get scheme() PageThumbs.scheme,
|
||||
|
||||
/**
|
||||
* The default port for this protocol (we don't support ports).
|
||||
*/
|
||||
get defaultPort() -1,
|
||||
|
||||
/**
|
||||
* The flags specific to this protocol implementation.
|
||||
*/
|
||||
get protocolFlags() {
|
||||
return Ci.nsIProtocolHandler.URI_IS_UI_RESOURCE |
|
||||
Ci.nsIProtocolHandler.URI_NORELATIVE |
|
||||
Ci.nsIProtocolHandler.URI_NOAUTH;
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates a new URI object that is suitable for loading by this protocol.
|
||||
* @param aSpec The URI string in UTF8 encoding.
|
||||
* @param aOriginCharset The charset of the document from which the URI originated.
|
||||
* @return The newly created URI.
|
||||
*/
|
||||
newURI: function Proto_newURI(aSpec, aOriginCharset) {
|
||||
let uri = Cc["@mozilla.org/network/simple-uri;1"].createInstance(Ci.nsIURI);
|
||||
uri.spec = aSpec;
|
||||
return uri;
|
||||
},
|
||||
|
||||
/**
|
||||
* Constructs a new channel from the given URI for this protocol handler.
|
||||
* @param aURI The URI for which to construct a channel.
|
||||
* @return The newly created channel.
|
||||
*/
|
||||
newChannel: function Proto_newChannel(aURI) {
|
||||
return new Channel(aURI);
|
||||
},
|
||||
|
||||
/**
|
||||
* Decides whether to allow a blacklisted port.
|
||||
* @return Always false, we'll never allow ports.
|
||||
*/
|
||||
allowPort: function () false,
|
||||
|
||||
classID: Components.ID("{5a4ae9b5-f475-48ae-9dce-0b4c1d347884}"),
|
||||
QueryInterface: XPCOMUtils.generateQI([Ci.nsIProtocolHandler])
|
||||
};
|
||||
|
||||
let NSGetFactory = XPCOMUtils.generateNSGetFactory([Protocol]);
|
||||
|
||||
/**
|
||||
* A channel implementation responsible for delivering cached thumbnails.
|
||||
*/
|
||||
function Channel(aURI) {
|
||||
this._uri = aURI;
|
||||
|
||||
// nsIChannel
|
||||
this.originalURI = aURI;
|
||||
|
||||
// nsIHttpChannel
|
||||
this._responseHeaders = {"content-type": PageThumbs.contentType};
|
||||
}
|
||||
|
||||
Channel.prototype = {
|
||||
/**
|
||||
* Tracks if the channel has been opened, yet.
|
||||
*/
|
||||
_wasOpened: false,
|
||||
|
||||
/**
|
||||
* Opens this channel asynchronously.
|
||||
* @param aListener The listener that receives the channel data when available.
|
||||
* @param aContext A custom context passed to the listener's methods.
|
||||
*/
|
||||
asyncOpen: function Channel_asyncOpen(aListener, aContext) {
|
||||
if (this._wasOpened)
|
||||
throw Cr.NS_ERROR_ALREADY_OPENED;
|
||||
|
||||
if (this.canceled)
|
||||
return;
|
||||
|
||||
this._listener = aListener;
|
||||
this._context = aContext;
|
||||
|
||||
this._isPending = true;
|
||||
this._wasOpened = true;
|
||||
|
||||
// Try to read the data from the thumbnail cache.
|
||||
this._readCache(function (aData) {
|
||||
// Update response if there's no data.
|
||||
if (!aData) {
|
||||
this._responseStatus = 404;
|
||||
this._responseText = "Not Found";
|
||||
}
|
||||
|
||||
this._startRequest();
|
||||
|
||||
if (!this.canceled) {
|
||||
this._addToLoadGroup();
|
||||
|
||||
if (aData)
|
||||
this._serveData(aData);
|
||||
|
||||
if (!this.canceled)
|
||||
this._stopRequest();
|
||||
}
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
/**
|
||||
* Reads a data stream from the cache entry.
|
||||
* @param aCallback The callback the data is passed to.
|
||||
*/
|
||||
_readCache: function Channel_readCache(aCallback) {
|
||||
let {url} = parseURI(this._uri);
|
||||
|
||||
// Return early if there's no valid URL given.
|
||||
if (!url) {
|
||||
aCallback(null);
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to get a cache entry.
|
||||
PageThumbsCache.getReadEntry(url, function (aEntry) {
|
||||
let inputStream = aEntry && aEntry.openInputStream(0);
|
||||
|
||||
function closeEntryAndFinish(aData) {
|
||||
if (aEntry) {
|
||||
aEntry.close();
|
||||
}
|
||||
aCallback(aData);
|
||||
}
|
||||
|
||||
// Check if we have a valid entry and if it has any data.
|
||||
if (!inputStream || !inputStream.available()) {
|
||||
closeEntryAndFinish();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Read the cache entry's data.
|
||||
NetUtil.asyncFetch(inputStream, function (aData, aStatus) {
|
||||
// We might have been canceled while waiting.
|
||||
if (this.canceled)
|
||||
return;
|
||||
|
||||
// Check if we have a valid data stream.
|
||||
if (!Components.isSuccessCode(aStatus) || !aData.available())
|
||||
aData = null;
|
||||
|
||||
closeEntryAndFinish(aData);
|
||||
}.bind(this));
|
||||
} catch (e) {
|
||||
closeEntryAndFinish();
|
||||
}
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
/**
|
||||
* Calls onStartRequest on the channel listener.
|
||||
*/
|
||||
_startRequest: function Channel_startRequest() {
|
||||
try {
|
||||
this._listener.onStartRequest(this, this._context);
|
||||
} catch (e) {
|
||||
// The listener might throw if the request has been canceled.
|
||||
this.cancel(Cr.NS_BINDING_ABORTED);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Calls onDataAvailable on the channel listener and passes the data stream.
|
||||
* @param aData The data to be delivered.
|
||||
*/
|
||||
_serveData: function Channel_serveData(aData) {
|
||||
try {
|
||||
let available = aData.available();
|
||||
this._listener.onDataAvailable(this, this._context, aData, 0, available);
|
||||
} catch (e) {
|
||||
// The listener might throw if the request has been canceled.
|
||||
this.cancel(Cr.NS_BINDING_ABORTED);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Calls onStopRequest on the channel listener.
|
||||
*/
|
||||
_stopRequest: function Channel_stopRequest() {
|
||||
try {
|
||||
this._listener.onStopRequest(this, this._context, this.status);
|
||||
} catch (e) {
|
||||
// This might throw but is generally ignored.
|
||||
}
|
||||
|
||||
// The request has finished, clean up after ourselves.
|
||||
this._cleanup();
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds this request to the load group, if any.
|
||||
*/
|
||||
_addToLoadGroup: function Channel_addToLoadGroup() {
|
||||
if (this.loadGroup)
|
||||
this.loadGroup.addRequest(this, this._context);
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes this request from its load group, if any.
|
||||
*/
|
||||
_removeFromLoadGroup: function Channel_removeFromLoadGroup() {
|
||||
if (!this.loadGroup)
|
||||
return;
|
||||
|
||||
try {
|
||||
this.loadGroup.removeRequest(this, this._context, this.status);
|
||||
} catch (e) {
|
||||
// This might throw but is ignored.
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Cleans up the channel when the request has finished.
|
||||
*/
|
||||
_cleanup: function Channel_cleanup() {
|
||||
this._removeFromLoadGroup();
|
||||
this.loadGroup = null;
|
||||
|
||||
this._isPending = false;
|
||||
|
||||
delete this._listener;
|
||||
delete this._context;
|
||||
},
|
||||
|
||||
/* :::::::: nsIChannel ::::::::::::::: */
|
||||
|
||||
contentType: PageThumbs.contentType,
|
||||
contentLength: -1,
|
||||
owner: null,
|
||||
contentCharset: null,
|
||||
notificationCallbacks: null,
|
||||
|
||||
get URI() this._uri,
|
||||
get securityInfo() null,
|
||||
|
||||
/**
|
||||
* Opens this channel synchronously. Not supported.
|
||||
*/
|
||||
open: function Channel_open() {
|
||||
// Synchronous data delivery is not implemented.
|
||||
throw Cr.NS_ERROR_NOT_IMPLEMENTED;
|
||||
},
|
||||
|
||||
/* :::::::: nsIHttpChannel ::::::::::::::: */
|
||||
|
||||
redirectionLimit: 10,
|
||||
requestMethod: "GET",
|
||||
allowPipelining: true,
|
||||
referrer: null,
|
||||
|
||||
get requestSucceeded() true,
|
||||
|
||||
_responseStatus: 200,
|
||||
get responseStatus() this._responseStatus,
|
||||
|
||||
_responseText: "OK",
|
||||
get responseStatusText() this._responseText,
|
||||
|
||||
/**
|
||||
* Checks if the server sent the equivalent of a "Cache-control: no-cache"
|
||||
* response header.
|
||||
* @return Always false.
|
||||
*/
|
||||
isNoCacheResponse: function () false,
|
||||
|
||||
/**
|
||||
* Checks if the server sent the equivalent of a "Cache-control: no-cache"
|
||||
* response header.
|
||||
* @return Always false.
|
||||
*/
|
||||
isNoStoreResponse: function () false,
|
||||
|
||||
/**
|
||||
* Returns the value of a particular request header. Not implemented.
|
||||
*/
|
||||
getRequestHeader: function Channel_getRequestHeader() {
|
||||
throw Cr.NS_ERROR_NOT_AVAILABLE;
|
||||
},
|
||||
|
||||
/**
|
||||
* This method is called to set the value of a particular request header.
|
||||
* Not implemented.
|
||||
*/
|
||||
setRequestHeader: function Channel_setRequestHeader() {
|
||||
if (this._wasOpened)
|
||||
throw Cr.NS_ERROR_IN_PROGRESS;
|
||||
},
|
||||
|
||||
/**
|
||||
* Call this method to visit all request headers. Not implemented.
|
||||
*/
|
||||
visitRequestHeaders: function () {},
|
||||
|
||||
/**
|
||||
* Gets the value of a particular response header.
|
||||
* @param aHeader The case-insensitive name of the response header to query.
|
||||
* @return The header value.
|
||||
*/
|
||||
getResponseHeader: function Channel_getResponseHeader(aHeader) {
|
||||
let name = aHeader.toLowerCase();
|
||||
if (name in this._responseHeaders)
|
||||
return this._responseHeaders[name];
|
||||
|
||||
throw Cr.NS_ERROR_NOT_AVAILABLE;
|
||||
},
|
||||
|
||||
/**
|
||||
* This method is called to set the value of a particular response header.
|
||||
* @param aHeader The case-insensitive name of the response header to query.
|
||||
* @param aValue The response header value to set.
|
||||
*/
|
||||
setResponseHeader: function Channel_setResponseHeader(aHeader, aValue, aMerge) {
|
||||
let name = aHeader.toLowerCase();
|
||||
if (!aValue && !aMerge)
|
||||
delete this._responseHeaders[name];
|
||||
else
|
||||
this._responseHeaders[name] = aValue;
|
||||
},
|
||||
|
||||
/**
|
||||
* Call this method to visit all response headers.
|
||||
* @param aVisitor The header visitor.
|
||||
*/
|
||||
visitResponseHeaders: function Channel_visitResponseHeaders(aVisitor) {
|
||||
for (let name in this._responseHeaders) {
|
||||
let value = this._responseHeaders[name];
|
||||
|
||||
try {
|
||||
aVisitor.visitHeader(name, value);
|
||||
} catch (e) {
|
||||
// The visitor can throw to stop the iteration.
|
||||
return;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/* :::::::: nsIRequest ::::::::::::::: */
|
||||
|
||||
loadFlags: Ci.nsIRequest.LOAD_NORMAL,
|
||||
loadGroup: null,
|
||||
|
||||
get name() this._uri.spec,
|
||||
|
||||
_status: Cr.NS_OK,
|
||||
get status() this._status,
|
||||
|
||||
_isPending: false,
|
||||
isPending: function () this._isPending,
|
||||
|
||||
resume: function () {},
|
||||
suspend: function () {},
|
||||
|
||||
/**
|
||||
* Cancels this request.
|
||||
* @param aStatus The reason for cancelling.
|
||||
*/
|
||||
cancel: function Channel_cancel(aStatus) {
|
||||
if (this.canceled)
|
||||
return;
|
||||
|
||||
this._isCanceled = true;
|
||||
this._status = aStatus;
|
||||
|
||||
this._cleanup();
|
||||
},
|
||||
|
||||
/* :::::::: nsIHttpChannelInternal ::::::::::::::: */
|
||||
|
||||
documentURI: null,
|
||||
|
||||
_isCanceled: false,
|
||||
get canceled() this._isCanceled,
|
||||
|
||||
QueryInterface: XPCOMUtils.generateQI([Ci.nsIChannel,
|
||||
Ci.nsIHttpChannel,
|
||||
Ci.nsIHttpChannelInternal,
|
||||
Ci.nsIRequest])
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses a given URI and extracts all parameters relevant to this protocol.
|
||||
* @param aURI The URI to parse.
|
||||
* @return The parsed parameters.
|
||||
*/
|
||||
function parseURI(aURI) {
|
||||
let {scheme, staticHost} = PageThumbs;
|
||||
let re = new RegExp("^" + scheme + "://" + staticHost + ".*?\\?");
|
||||
let query = aURI.spec.replace(re, "");
|
||||
let params = {};
|
||||
|
||||
query.split("&").forEach(function (aParam) {
|
||||
let [key, value] = aParam.split("=").map(decodeURIComponent);
|
||||
params[key.toLowerCase()] = value;
|
||||
});
|
||||
|
||||
return params;
|
||||
}
|
21
browser/components/thumbnails/test/Makefile.in
Normal file
21
browser/components/thumbnails/test/Makefile.in
Normal file
@ -0,0 +1,21 @@
|
||||
# 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/.
|
||||
|
||||
DEPTH = ../../../..
|
||||
topsrcdir = @top_srcdir@
|
||||
srcdir = @srcdir@
|
||||
VPATH = @srcdir@
|
||||
relativesrcdir = browser/components/thumbnails/test
|
||||
|
||||
include $(DEPTH)/config/autoconf.mk
|
||||
include $(topsrcdir)/config/rules.mk
|
||||
|
||||
_BROWSER_FILES = \
|
||||
browser_thumbnails_cache.js \
|
||||
browser_thumbnails_capture.js \
|
||||
head.js \
|
||||
$(NULL)
|
||||
|
||||
libs:: $(_BROWSER_FILES)
|
||||
$(INSTALL) $(foreach f,$^,"$f") $(DEPTH)/_tests/testing/mochitest/browser/$(relativesrcdir)
|
@ -0,0 +1,34 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
/**
|
||||
* These tests ensure that saving a thumbnail to the cache works. They also
|
||||
* retrieve the thumbnail and display it using an <img> element to compare
|
||||
* its pixel colors.
|
||||
*/
|
||||
function runTests() {
|
||||
// Create a new tab with a red background.
|
||||
yield addTab("data:text/html,<body bgcolor=ff0000></body>");
|
||||
let cw = gBrowser.selectedTab.linkedBrowser.contentWindow;
|
||||
|
||||
// Capture a thumbnail for the tab.
|
||||
let canvas = PageThumbs.capture(cw);
|
||||
|
||||
// Store the tab into the thumbnail cache.
|
||||
yield PageThumbs.store("key", canvas, next);
|
||||
|
||||
let {width, height} = canvas;
|
||||
let thumb = PageThumbs.getThumbnailURL("key", width, height);
|
||||
|
||||
// Create a new tab with an image displaying the previously stored thumbnail.
|
||||
yield addTab("data:text/html,<img src='" + thumb + "'/>" +
|
||||
"<canvas width=" + width + " height=" + height + "/>");
|
||||
|
||||
cw = gBrowser.selectedTab.linkedBrowser.contentWindow;
|
||||
let [img, canvas] = cw.document.querySelectorAll("img, canvas");
|
||||
|
||||
// Draw the image to a canvas and compare the pixel color values.
|
||||
let ctx = canvas.getContext("2d");
|
||||
ctx.drawImage(img, 0, 0, width, height);
|
||||
checkCanvasColor(ctx, 255, 0, 0, "we have a red image and canvas");
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
/**
|
||||
* These tests ensure that capturing a site's screenshot to a canvas actually
|
||||
* works.
|
||||
*/
|
||||
function runTests() {
|
||||
// Create a tab with a red background.
|
||||
yield addTab("data:text/html,<body bgcolor=ff0000></body>");
|
||||
checkCurrentThumbnailColor(255, 0, 0, "we have a red thumbnail");
|
||||
|
||||
// Load a page with a green background.
|
||||
yield navigateTo("data:text/html,<body bgcolor=00ff00></body>");
|
||||
checkCurrentThumbnailColor(0, 255, 0, "we have a green thumbnail");
|
||||
|
||||
// Load a page with a blue background.
|
||||
yield navigateTo("data:text/html,<body bgcolor=0000ff></body>");
|
||||
checkCurrentThumbnailColor(0, 0, 255, "we have a blue thumbnail");
|
||||
}
|
||||
|
||||
/**
|
||||
* Captures a thumbnail of the currently selected tab and checks the color of
|
||||
* the resulting canvas.
|
||||
* @param aRed The red component's intensity.
|
||||
* @param aGreen The green component's intensity.
|
||||
* @param aBlue The blue component's intensity.
|
||||
* @param aMessage The info message to print when checking the pixel color.
|
||||
*/
|
||||
function checkCurrentThumbnailColor(aRed, aGreen, aBlue, aMessage) {
|
||||
let tab = gBrowser.selectedTab;
|
||||
let cw = tab.linkedBrowser.contentWindow;
|
||||
|
||||
let canvas = PageThumbs.capture(cw);
|
||||
let ctx = canvas.getContext("2d");
|
||||
|
||||
checkCanvasColor(ctx, aRed, aGreen, aBlue, aMessage);
|
||||
}
|
93
browser/components/thumbnails/test/head.js
Normal file
93
browser/components/thumbnails/test/head.js
Normal file
@ -0,0 +1,93 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
Cu.import("resource:///modules/PageThumbs.jsm");
|
||||
|
||||
registerCleanupFunction(function () {
|
||||
while (gBrowser.tabs.length > 1)
|
||||
gBrowser.removeTab(gBrowser.tabs[1]);
|
||||
});
|
||||
|
||||
/**
|
||||
* Provide the default test function to start our test runner.
|
||||
*/
|
||||
function test() {
|
||||
TestRunner.run();
|
||||
}
|
||||
|
||||
/**
|
||||
* The test runner that controls the execution flow of our tests.
|
||||
*/
|
||||
let TestRunner = {
|
||||
/**
|
||||
* Starts the test runner.
|
||||
*/
|
||||
run: function () {
|
||||
waitForExplicitFinish();
|
||||
|
||||
this._iter = runTests();
|
||||
this.next();
|
||||
},
|
||||
|
||||
/**
|
||||
* Runs the next available test or finishes if there's no test left.
|
||||
*/
|
||||
next: function () {
|
||||
try {
|
||||
TestRunner._iter.next();
|
||||
} catch (e if e instanceof StopIteration) {
|
||||
finish();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Continues the current test execution.
|
||||
*/
|
||||
function next() {
|
||||
TestRunner.next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new tab with the given URI.
|
||||
* @param aURI The URI that's loaded in the tab.
|
||||
*/
|
||||
function addTab(aURI) {
|
||||
let tab = gBrowser.selectedTab = gBrowser.addTab(aURI);
|
||||
whenBrowserLoaded(tab.linkedBrowser);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a new URI into the currently selected tab.
|
||||
* @param aURI The URI to load.
|
||||
*/
|
||||
function navigateTo(aURI) {
|
||||
let browser = gBrowser.selectedTab.linkedBrowser;
|
||||
whenBrowserLoaded(browser);
|
||||
browser.loadURI(aURI);
|
||||
}
|
||||
|
||||
/**
|
||||
* Continues the current test execution when a load event for the given browser
|
||||
* has been received
|
||||
* @param aBrowser The browser to listen on.
|
||||
*/
|
||||
function whenBrowserLoaded(aBrowser) {
|
||||
aBrowser.addEventListener("load", function onLoad() {
|
||||
aBrowser.removeEventListener("load", onLoad, true);
|
||||
executeSoon(next);
|
||||
}, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the top-left pixel of a given canvas' 2d context for a given color.
|
||||
* @param aContext The 2D context of a canvas.
|
||||
* @param aRed The red component's intensity.
|
||||
* @param aGreen The green component's intensity.
|
||||
* @param aBlue The blue component's intensity.
|
||||
* @param aMessage The info message to print when comparing the pixel color.
|
||||
*/
|
||||
function checkCanvasColor(aContext, aRed, aGreen, aBlue, aMessage) {
|
||||
let [r, g, b] = aContext.getImageData(0, 0, 1, 1).data;
|
||||
ok(r == aRed && g == aGreen && b == aBlue, aMessage);
|
||||
}
|
@ -284,6 +284,7 @@
|
||||
@BINPATH@/components/nsSetDefaultBrowser.manifest
|
||||
@BINPATH@/components/nsSetDefaultBrowser.js
|
||||
@BINPATH@/components/BrowserPlaces.manifest
|
||||
@BINPATH@/components/BrowserPageThumbs.manifest
|
||||
@BINPATH@/components/nsPrivateBrowsingService.manifest
|
||||
@BINPATH@/components/nsPrivateBrowsingService.js
|
||||
@BINPATH@/components/toolkitsearch.manifest
|
||||
@ -347,6 +348,7 @@
|
||||
@BINPATH@/components/nsPlacesExpiration.js
|
||||
@BINPATH@/components/PlacesProtocolHandler.js
|
||||
@BINPATH@/components/PlacesCategoriesStarter.js
|
||||
@BINPATH@/components/PageThumbsProtocol.js
|
||||
@BINPATH@/components/nsDefaultCLH.manifest
|
||||
@BINPATH@/components/nsDefaultCLH.js
|
||||
@BINPATH@/components/nsContentPrefService.manifest
|
||||
|
@ -52,6 +52,7 @@ EXTRA_JS_MODULES = \
|
||||
openLocationLastURL.jsm \
|
||||
NetworkPrioritizer.jsm \
|
||||
offlineAppCache.jsm \
|
||||
PageThumbs.jsm \
|
||||
$(NULL)
|
||||
|
||||
ifeq ($(MOZ_WIDGET_TOOLKIT),windows)
|
||||
|
265
browser/modules/PageThumbs.jsm
Normal file
265
browser/modules/PageThumbs.jsm
Normal file
@ -0,0 +1,265 @@
|
||||
/* 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 EXPORTED_SYMBOLS = ["PageThumbs", "PageThumbsCache"];
|
||||
|
||||
const Cu = Components.utils;
|
||||
const Cc = Components.classes;
|
||||
const Ci = Components.interfaces;
|
||||
|
||||
const HTML_NAMESPACE = "http://www.w3.org/1999/xhtml";
|
||||
|
||||
/**
|
||||
* The default width for page thumbnails.
|
||||
*
|
||||
* Hint: This is the default value because the 'New Tab Page' is the only
|
||||
* client for now.
|
||||
*/
|
||||
const THUMBNAIL_WIDTH = 201;
|
||||
|
||||
/**
|
||||
* The default height for page thumbnails.
|
||||
*
|
||||
* Hint: This is the default value because the 'New Tab Page' is the only
|
||||
* client for now.
|
||||
*/
|
||||
const THUMBNAIL_HEIGHT = 127;
|
||||
|
||||
/**
|
||||
* The default background color for page thumbnails.
|
||||
*/
|
||||
const THUMBNAIL_BG_COLOR = "#fff";
|
||||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
|
||||
"resource://gre/modules/NetUtil.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Services",
|
||||
"resource://gre/modules/Services.jsm");
|
||||
|
||||
/**
|
||||
* Singleton providing functionality for capturing web page thumbnails and for
|
||||
* accessing them if already cached.
|
||||
*/
|
||||
let PageThumbs = {
|
||||
/**
|
||||
* The scheme to use for thumbnail urls.
|
||||
*/
|
||||
get scheme() "moz-page-thumb",
|
||||
|
||||
/**
|
||||
* The static host to use for thumbnail urls.
|
||||
*/
|
||||
get staticHost() "thumbnail",
|
||||
|
||||
/**
|
||||
* The thumbnails' image type.
|
||||
*/
|
||||
get contentType() "image/png",
|
||||
|
||||
/**
|
||||
* Gets the thumbnail image's url for a given web page's url.
|
||||
* @param aUrl The web page's url that is depicted in the thumbnail.
|
||||
* @return The thumbnail image's url.
|
||||
*/
|
||||
getThumbnailURL: function PageThumbs_getThumbnailURL(aUrl) {
|
||||
return this.scheme + "://" + this.staticHost +
|
||||
"?url=" + encodeURIComponent(aUrl);
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates a canvas containing a thumbnail depicting the given window.
|
||||
* @param aWindow The DOM window to capture a thumbnail from.
|
||||
* @return The newly created canvas containing the image data.
|
||||
*/
|
||||
capture: function PageThumbs_capture(aWindow) {
|
||||
let [sx, sy, sw, sh, scale] = this._determineCropRectangle(aWindow);
|
||||
|
||||
let canvas = this._createCanvas();
|
||||
let ctx = canvas.getContext("2d");
|
||||
|
||||
// Scale the canvas accordingly.
|
||||
ctx.scale(scale, scale);
|
||||
|
||||
try {
|
||||
// Draw the window contents to the canvas.
|
||||
ctx.drawWindow(aWindow, sx, sy, sw, sh, THUMBNAIL_BG_COLOR,
|
||||
ctx.DRAWWINDOW_DO_NOT_FLUSH);
|
||||
} catch (e) {
|
||||
// We couldn't draw to the canvas for some reason.
|
||||
}
|
||||
|
||||
return canvas;
|
||||
},
|
||||
|
||||
/**
|
||||
* Stores the image data contained in the given canvas to the underlying
|
||||
* storage.
|
||||
* @param aKey The key to use for the storage.
|
||||
* @param aCanvas The canvas containing the thumbnail's image data.
|
||||
* @param aCallback The function to be called when the canvas data has been
|
||||
* stored (optional).
|
||||
*/
|
||||
store: function PageThumbs_store(aKey, aCanvas, aCallback) {
|
||||
let self = this;
|
||||
|
||||
function finish(aSuccessful) {
|
||||
if (aCallback)
|
||||
aCallback(aSuccessful);
|
||||
}
|
||||
|
||||
// Get a writeable cache entry.
|
||||
PageThumbsCache.getWriteEntry(aKey, function (aEntry) {
|
||||
if (!aEntry) {
|
||||
finish(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract image data from the canvas.
|
||||
self._readImageData(aCanvas, function (aData) {
|
||||
let outputStream = aEntry.openOutputStream(0);
|
||||
|
||||
// Write the image data to the cache entry.
|
||||
NetUtil.asyncCopy(aData, outputStream, function (aResult) {
|
||||
let success = Components.isSuccessCode(aResult);
|
||||
if (success)
|
||||
aEntry.markValid();
|
||||
|
||||
aEntry.close();
|
||||
finish(success);
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Reads the image data from a given canvas and passes it to the callback.
|
||||
* @param aCanvas The canvas to read the image data from.
|
||||
* @param aCallback The function that the image data is passed to.
|
||||
*/
|
||||
_readImageData: function PageThumbs_readImageData(aCanvas, aCallback) {
|
||||
let dataUri = aCanvas.toDataURL(PageThumbs.contentType, "");
|
||||
let uri = Services.io.newURI(dataUri, "UTF8", null);
|
||||
|
||||
NetUtil.asyncFetch(uri, function (aData, aResult) {
|
||||
if (Components.isSuccessCode(aResult) && aData && aData.available())
|
||||
aCallback(aData);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Determines the crop rectangle for a given content window.
|
||||
* @param aWindow The content window.
|
||||
* @return An array containing x, y, width, heigh and the scale of the crop
|
||||
* rectangle.
|
||||
*/
|
||||
_determineCropRectangle: function PageThumbs_determineCropRectangle(aWindow) {
|
||||
let sx = 0;
|
||||
let sy = 0;
|
||||
let sw = aWindow.innerWidth;
|
||||
let sh = aWindow.innerHeight;
|
||||
|
||||
let scale = Math.max(THUMBNAIL_WIDTH / sw, THUMBNAIL_HEIGHT / sh);
|
||||
let scaledWidth = sw * scale;
|
||||
let scaledHeight = sh * scale;
|
||||
|
||||
if (scaledHeight > THUMBNAIL_HEIGHT) {
|
||||
sy = Math.floor(Math.abs((scaledHeight - THUMBNAIL_HEIGHT) / 2) / scale);
|
||||
sh -= 2 * sy;
|
||||
}
|
||||
|
||||
if (scaledWidth > THUMBNAIL_WIDTH) {
|
||||
sx = Math.floor(Math.abs((scaledWidth - THUMBNAIL_WIDTH) / 2) / scale);
|
||||
sw -= 2 * sx;
|
||||
}
|
||||
|
||||
return [sx, sy, sw, sh, scale];
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates a new hidden canvas element.
|
||||
* @return The newly created canvas.
|
||||
*/
|
||||
_createCanvas: function PageThumbs_createCanvas() {
|
||||
let doc = Services.appShell.hiddenDOMWindow.document;
|
||||
let canvas = doc.createElementNS(HTML_NAMESPACE, "canvas");
|
||||
canvas.mozOpaque = true;
|
||||
canvas.width = THUMBNAIL_WIDTH;
|
||||
canvas.height = THUMBNAIL_HEIGHT;
|
||||
return canvas;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* A singleton handling the storage of page thumbnails.
|
||||
*/
|
||||
let PageThumbsCache = {
|
||||
/**
|
||||
* Calls the given callback with a cache entry opened for reading.
|
||||
* @param aKey The key identifying the desired cache entry.
|
||||
* @param aCallback The callback that is called when the cache entry is ready.
|
||||
*/
|
||||
getReadEntry: function Cache_getReadEntry(aKey, aCallback) {
|
||||
// Try to open the desired cache entry.
|
||||
this._openCacheEntry(aKey, Ci.nsICache.ACCESS_READ, aCallback);
|
||||
},
|
||||
|
||||
/**
|
||||
* Calls the given callback with a cache entry opened for writing.
|
||||
* @param aKey The key identifying the desired cache entry.
|
||||
* @param aCallback The callback that is called when the cache entry is ready.
|
||||
*/
|
||||
getWriteEntry: function Cache_getWriteEntry(aKey, aCallback) {
|
||||
// Try to open the desired cache entry.
|
||||
this._openCacheEntry(aKey, Ci.nsICache.ACCESS_WRITE, aCallback);
|
||||
},
|
||||
|
||||
/**
|
||||
* Opens the cache entry identified by the given key.
|
||||
* @param aKey The key identifying the desired cache entry.
|
||||
* @param aAccess The desired access mode (see nsICache.ACCESS_* constants).
|
||||
* @param aCallback The function to be called when the cache entry was opened.
|
||||
*/
|
||||
_openCacheEntry: function Cache_openCacheEntry(aKey, aAccess, aCallback) {
|
||||
function onCacheEntryAvailable(aEntry, aAccessGranted, aStatus) {
|
||||
let validAccess = aAccess == aAccessGranted;
|
||||
let validStatus = Components.isSuccessCode(aStatus);
|
||||
|
||||
// Check if a valid entry was passed and if the
|
||||
// access we requested was actually granted.
|
||||
if (aEntry && !(validAccess && validStatus)) {
|
||||
aEntry.close();
|
||||
aEntry = null;
|
||||
}
|
||||
|
||||
aCallback(aEntry);
|
||||
}
|
||||
|
||||
let listener = this._createCacheListener(onCacheEntryAvailable);
|
||||
this._cacheSession.asyncOpenCacheEntry(aKey, aAccess, listener);
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns a cache listener implementing the nsICacheListener interface.
|
||||
* @param aCallback The callback to be called when the cache entry is available.
|
||||
* @return The new cache listener.
|
||||
*/
|
||||
_createCacheListener: function Cache_createCacheListener(aCallback) {
|
||||
return {
|
||||
onCacheEntryAvailable: aCallback,
|
||||
QueryInterface: XPCOMUtils.generateQI([Ci.nsICacheListener])
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Define a lazy getter for the cache session.
|
||||
*/
|
||||
XPCOMUtils.defineLazyGetter(PageThumbsCache, "_cacheSession", function () {
|
||||
return Services.cache.createSession(PageThumbs.scheme,
|
||||
Ci.nsICache.STORE_ON_DISK, true);
|
||||
});
|
@ -63,6 +63,8 @@ XPCOMUtils.defineLazyGetter(Services, "dirsvc", function () {
|
||||
});
|
||||
|
||||
let initTable = [
|
||||
["appShell", "@mozilla.org/appshell/appShellService;1", "nsIAppShellService"],
|
||||
["cache", "@mozilla.org/network/cache-service;1", "nsICacheService"],
|
||||
["console", "@mozilla.org/consoleservice;1", "nsIConsoleService"],
|
||||
["contentPrefs", "@mozilla.org/content-pref/service;1", "nsIContentPrefService"],
|
||||
["cookies", "@mozilla.org/cookiemanager;1", "nsICookieManager2"],
|
||||
|
Loading…
Reference in New Issue
Block a user