diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js index 6feca4f1d923..00cec4a595ab 100644 --- a/browser/app/profile/firefox.js +++ b/browser/app/profile/firefox.js @@ -1041,6 +1041,7 @@ pref("devtools.scratchpad.enabled", true); // Enable the Style Editor. pref("devtools.styleeditor.enabled", true); +pref("devtools.styleeditor.transitions", true); // Enable tools for Chrome development. pref("devtools.chrome.enabled", false); diff --git a/browser/base/content/browser-thumbnails.js b/browser/base/content/browser-thumbnails.js new file mode 100644 index 000000000000..59a6f743a4d1 --- /dev/null +++ b/browser/base/content/browser-thumbnails.js @@ -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; + } + } +}; diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js index e0283b7ef1df..55b43e9488e6 100644 --- a/browser/base/content/browser.js +++ b/browser/base/content/browser.js @@ -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(); diff --git a/browser/components/Makefile.in b/browser/components/Makefile.in index eca4d1fb8967..a7bc311e53f2 100644 --- a/browser/components/Makefile.in +++ b/browser/components/Makefile.in @@ -71,6 +71,7 @@ PARALLEL_DIRS = \ shell \ sidebar \ tabview \ + thumbnails \ migration \ $(NULL) diff --git a/browser/components/thumbnails/BrowserPageThumbs.manifest b/browser/components/thumbnails/BrowserPageThumbs.manifest new file mode 100644 index 000000000000..8dfc0597b261 --- /dev/null +++ b/browser/components/thumbnails/BrowserPageThumbs.manifest @@ -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} diff --git a/browser/components/thumbnails/Makefile.in b/browser/components/thumbnails/Makefile.in new file mode 100644 index 000000000000..bdc0aee6ac96 --- /dev/null +++ b/browser/components/thumbnails/Makefile.in @@ -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/ diff --git a/browser/components/thumbnails/PageThumbsProtocol.js b/browser/components/thumbnails/PageThumbsProtocol.js new file mode 100644 index 000000000000..3ee7a7f2d696 --- /dev/null +++ b/browser/components/thumbnails/PageThumbsProtocol.js @@ -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; +} diff --git a/browser/components/thumbnails/test/Makefile.in b/browser/components/thumbnails/test/Makefile.in new file mode 100644 index 000000000000..014bf0828a31 --- /dev/null +++ b/browser/components/thumbnails/test/Makefile.in @@ -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) diff --git a/browser/components/thumbnails/test/browser_thumbnails_cache.js b/browser/components/thumbnails/test/browser_thumbnails_cache.js new file mode 100644 index 000000000000..c0917f74fffd --- /dev/null +++ b/browser/components/thumbnails/test/browser_thumbnails_cache.js @@ -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 element to compare + * its pixel colors. + */ +function runTests() { + // Create a new tab with a red background. + yield addTab("data:text/html,"); + 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," + + ""); + + 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"); +} diff --git a/browser/components/thumbnails/test/browser_thumbnails_capture.js b/browser/components/thumbnails/test/browser_thumbnails_capture.js new file mode 100644 index 000000000000..38886159659d --- /dev/null +++ b/browser/components/thumbnails/test/browser_thumbnails_capture.js @@ -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,"); + checkCurrentThumbnailColor(255, 0, 0, "we have a red thumbnail"); + + // Load a page with a green background. + yield navigateTo("data:text/html,"); + checkCurrentThumbnailColor(0, 255, 0, "we have a green thumbnail"); + + // Load a page with a blue background. + yield navigateTo("data:text/html,"); + 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); +} diff --git a/browser/components/thumbnails/test/head.js b/browser/components/thumbnails/test/head.js new file mode 100644 index 000000000000..65eb8c4003b2 --- /dev/null +++ b/browser/components/thumbnails/test/head.js @@ -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); +} diff --git a/browser/devtools/styleeditor/StyleEditor.jsm b/browser/devtools/styleeditor/StyleEditor.jsm index 5b5c4f6a5645..092e2bd89831 100644 --- a/browser/devtools/styleeditor/StyleEditor.jsm +++ b/browser/devtools/styleeditor/StyleEditor.jsm @@ -59,6 +59,23 @@ const UPDATE_STYLESHEET_THROTTLE_DELAY = 500; // @see StyleEditor._persistExpando const STYLESHEET_EXPANDO = "-moz-styleeditor-stylesheet-"; +const TRANSITIONS_PREF = "devtools.styleeditor.transitions"; + +const TRANSITION_CLASS = "moz-styleeditor-transitioning"; +const TRANSITION_DURATION_MS = 500; +const TRANSITION_RULE = "\ +:root.moz-styleeditor-transitioning, :root.moz-styleeditor-transitioning * {\ +-moz-transition-duration: " + TRANSITION_DURATION_MS + "ms !important; \ +-moz-transition-delay: 0ms !important;\ +-moz-transition-timing-function: ease-out !important;\ +-moz-transition-property: all !important;\ +}"; + +/** + * Style Editor module-global preferences + */ +const TRANSITIONS_ENABLED = Services.prefs.getBoolPref(TRANSITIONS_PREF); + /** * StyleEditor constructor. @@ -107,6 +124,9 @@ function StyleEditor(aDocument, aStyleSheet) // this is to perform pending updates before editor closing this._onWindowUnloadBinding = this._onWindowUnload.bind(this); + + this._transitionRefCount = 0; + this._focusOnSourceEditorReady = false; } @@ -405,8 +425,13 @@ StyleEditor.prototype = { * Arguments: (StyleEditor editor) * @see inputElement * - * onCommit: Called when changes have been committed/applied - * to the live DOM style sheet. + * onUpdate: Called when changes are being applied to the live + * DOM style sheet but might not be complete from + * a WYSIWYG perspective (eg. transitioned update). + * Arguments: (StyleEditor editor) + * + * onCommit: Called when changes have been completely committed + * /applied to the live DOM style sheet. * Arguments: (StyleEditor editor) * } * @@ -640,15 +665,50 @@ StyleEditor.prototype = { let source = this._state.text; let oldNode = this.styleSheet.ownerNode; let oldIndex = this.styleSheetIndex; - - let newNode = this.contentDocument.createElement("style"); + let content = this.contentDocument; + let newNode = content.createElement("style"); newNode.setAttribute("type", "text/css"); - newNode.appendChild(this.contentDocument.createTextNode(source)); + newNode.appendChild(content.createTextNode(source)); oldNode.parentNode.replaceChild(newNode, oldNode); - this._styleSheet = this.contentDocument.styleSheets[oldIndex]; + this._styleSheet = content.styleSheets[oldIndex]; this._persistExpando(); + if (!TRANSITIONS_ENABLED) { + this._triggerAction("Update"); + this._triggerAction("Commit"); + return; + } + + // Insert the global transition rule + // Use a ref count to make sure we do not add it multiple times.. and remove + // it only when all pending StyleEditor-generated transitions ended. + if (!this._transitionRefCount) { + this._styleSheet.insertRule(TRANSITION_RULE, 0); + content.documentElement.classList.add(TRANSITION_CLASS); + } + + this._transitionRefCount++; + + // Set up clean up and commit after transition duration (+10% buffer) + // @see _onTransitionEnd + content.defaultView.setTimeout(this._onTransitionEnd.bind(this), + Math.floor(TRANSITION_DURATION_MS * 1.1)); + + this._triggerAction("Update"); + }, + + /** + * This cleans up class and rule added for transition effect and then trigger + * Commit as the changes have been completed. + */ + _onTransitionEnd: function SE__onTransitionEnd() + { + if (--this._transitionRefCount == 0) { + this.contentDocument.documentElement.classList.remove(TRANSITION_CLASS); + this.styleSheet.deleteRule(0); + } + this._triggerAction("Commit"); }, diff --git a/browser/devtools/styleeditor/StyleEditorChrome.jsm b/browser/devtools/styleeditor/StyleEditorChrome.jsm index b71a8d15bbbd..097fda5746b8 100644 --- a/browser/devtools/styleeditor/StyleEditorChrome.jsm +++ b/browser/devtools/styleeditor/StyleEditorChrome.jsm @@ -156,11 +156,14 @@ StyleEditorChrome.prototype = { aContentWindow.addEventListener("unload", onContentUnload, false); if (aContentWindow.document.readyState == "complete") { + this._root.classList.remove("loading"); this._populateChrome(); return; } else { + this._root.classList.add("loading"); let onContentReady = function () { aContentWindow.removeEventListener("load", onContentReady, false); + this._root.classList.remove("loading"); this._populateChrome(); }.bind(this); aContentWindow.addEventListener("load", onContentReady, false); @@ -299,7 +302,7 @@ StyleEditorChrome.prototype = { }, /** - * Reset the chrome UI to an empty state. + * Reset the chrome UI to an empty and ready state. */ _resetChrome: function SEC__resetChrome() { @@ -309,6 +312,12 @@ StyleEditorChrome.prototype = { this._editors = []; this._view.removeAll(); + + // (re)enable UI + let matches = this._root.querySelectorAll("toolbarbutton,input,select"); + for (let i = 0; i < matches.length; ++i) { + matches[i].removeAttribute("disabled"); + } }, /** diff --git a/browser/devtools/styleeditor/splitview.css b/browser/devtools/styleeditor/splitview.css index cf1f68eb0690..1bc270225104 100644 --- a/browser/devtools/styleeditor/splitview.css +++ b/browser/devtools/styleeditor/splitview.css @@ -46,6 +46,10 @@ box, -moz-box-pack: center; } +.loading .splitview-nav-container > .placeholder { + display: none !important; +} + .splitview-controller, .splitview-main { -moz-box-flex: 0; diff --git a/browser/devtools/styleeditor/styleeditor.xul b/browser/devtools/styleeditor/styleeditor.xul index 86c12abf9355..b29f64d40795 100644 --- a/browser/devtools/styleeditor/styleeditor.xul +++ b/browser/devtools/styleeditor/styleeditor.xul @@ -54,18 +54,20 @@ persist="screenX screenY width height sizemode"> - + + label="&newButton.label;" + disabled="true"/> + label="&importButton.label;" + disabled="true"/> */ -const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test//test-bug-630733-response-redirect-headers.sjs"; +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-bug-630733-response-redirect-headers.sjs"; let lastFinishedRequests = {}; diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_632817.js b/browser/devtools/webconsole/test/browser_webconsole_bug_632817.js index 08d37ffa8827..c2143a3c0e4d 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_bug_632817.js +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_632817.js @@ -4,9 +4,9 @@ // Tests that network log messages bring up the network panel. -const TEST_NETWORK_REQUEST_URI = "http://example.com/browser/browser/devtools/webconsole/test//test-network-request.html"; +const TEST_NETWORK_REQUEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-network-request.html"; -const TEST_IMG = "http://example.com/browser/browser/devtools/webconsole/test//test-image.png"; +const TEST_IMG = "http://example.com/browser/browser/devtools/webconsole/test/test-image.png"; const TEST_DATA_JSON_CONTENT = '{ id: "test JSON data", myArray: [ "foo", "bar", "baz", "biff" ] }'; diff --git a/browser/devtools/webconsole/test/browser_webconsole_console_extras.js b/browser/devtools/webconsole/test/browser_webconsole_console_extras.js index bf37db6e580e..c9c612122438 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_console_extras.js +++ b/browser/devtools/webconsole/test/browser_webconsole_console_extras.js @@ -37,7 +37,7 @@ // Tests that the basic console.log()-style APIs and filtering work. -const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test//test-console-extras.html"; +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console-extras.html"; function test() { addTab(TEST_URI); diff --git a/browser/devtools/webconsole/test/browser_webconsole_live_filtering_of_message_types.js b/browser/devtools/webconsole/test/browser_webconsole_live_filtering_of_message_types.js index d913e565c097..150f601563dd 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_live_filtering_of_message_types.js +++ b/browser/devtools/webconsole/test/browser_webconsole_live_filtering_of_message_types.js @@ -40,7 +40,7 @@ // Tests that the message type filter checkboxes work. -const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test//test-console.html"; +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html"; function test() { addTab(TEST_URI); diff --git a/browser/devtools/webconsole/test/browser_webconsole_live_filtering_on_search_strings.js b/browser/devtools/webconsole/test/browser_webconsole_live_filtering_on_search_strings.js index 6952459971ce..a18fa3db5745 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_live_filtering_on_search_strings.js +++ b/browser/devtools/webconsole/test/browser_webconsole_live_filtering_on_search_strings.js @@ -40,7 +40,7 @@ // Tests that the text filter box works. -const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test//test-console.html"; +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html"; function test() { addTab(TEST_URI); diff --git a/browser/devtools/webconsole/test/browser_webconsole_log_node_classes.js b/browser/devtools/webconsole/test/browser_webconsole_log_node_classes.js index ff96d12137ae..c6a36ca82011 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_log_node_classes.js +++ b/browser/devtools/webconsole/test/browser_webconsole_log_node_classes.js @@ -41,7 +41,7 @@ // Tests that console logging via the console API produces nodes of the correct // CSS classes. -const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test//test-console.html"; +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html"; function test() { addTab(TEST_URI); diff --git a/browser/devtools/webconsole/test/browser_webconsole_message_node_id.js b/browser/devtools/webconsole/test/browser_webconsole_message_node_id.js index 72f47062f12b..cf30b1a4a761 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_message_node_id.js +++ b/browser/devtools/webconsole/test/browser_webconsole_message_node_id.js @@ -35,7 +35,7 @@ * * ***** END LICENSE BLOCK ***** */ -const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test//test-console.html"; +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html"; function test() { addTab(TEST_URI); diff --git a/browser/devtools/webconsole/test/browser_webconsole_netlogging.js b/browser/devtools/webconsole/test/browser_webconsole_netlogging.js index b8264a37b711..3f663b90f1c1 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_netlogging.js +++ b/browser/devtools/webconsole/test/browser_webconsole_netlogging.js @@ -12,9 +12,9 @@ // Tests that network log messages bring up the network panel. -const TEST_NETWORK_REQUEST_URI = "http://example.com/browser/browser/devtools/webconsole/test//test-network-request.html"; +const TEST_NETWORK_REQUEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-network-request.html"; -const TEST_IMG = "http://example.com/browser/browser/devtools/webconsole/test//test-image.png"; +const TEST_IMG = "http://example.com/browser/browser/devtools/webconsole/test/test-image.png"; const TEST_DATA_JSON_CONTENT = '{ id: "test JSON data", myArray: [ "foo", "bar", "baz", "biff" ] }'; diff --git a/browser/devtools/webconsole/test/browser_webconsole_network_panel.js b/browser/devtools/webconsole/test/browser_webconsole_network_panel.js index eba609df8b70..6c0251c259b4 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_network_panel.js +++ b/browser/devtools/webconsole/test/browser_webconsole_network_panel.js @@ -41,9 +41,9 @@ // Tests that the network panel works. -const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test//test-console.html"; -const TEST_IMG = "http://example.com/browser/browser/devtools/webconsole/test//test-image.png"; -const TEST_ENCODING_ISO_8859_1 = "http://example.com/browser/browser/devtools/webconsole/test//test-encoding-ISO-8859-1.html"; +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html"; +const TEST_IMG = "http://example.com/browser/browser/devtools/webconsole/test/test-image.png"; +const TEST_ENCODING_ISO_8859_1 = "http://example.com/browser/browser/devtools/webconsole/test/test-encoding-ISO-8859-1.html"; let testDriver; diff --git a/browser/devtools/webconsole/test/browser_webconsole_registries.js b/browser/devtools/webconsole/test/browser_webconsole_registries.js index 754d1c6b2ba7..51119273ce10 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_registries.js +++ b/browser/devtools/webconsole/test/browser_webconsole_registries.js @@ -41,7 +41,7 @@ // Tests that the HUD service keeps an accurate registry of all the Web Console // instances. -const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test//test-console.html"; +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html"; function test() { addTab(TEST_URI); diff --git a/browser/devtools/webconsole/test/browser_webconsole_view_source.js b/browser/devtools/webconsole/test/browser_webconsole_view_source.js index 69a517a16100..9fe3088b402c 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_view_source.js +++ b/browser/devtools/webconsole/test/browser_webconsole_view_source.js @@ -4,7 +4,7 @@ // Tests that source URLs in the Web Console can be clicked to display the // standard View Source window. -const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test//test-error.html"; +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-error.html"; function test() { expectUncaughtException(); diff --git a/browser/devtools/webconsole/test/test-bug-593003-iframe-wrong-hud.html b/browser/devtools/webconsole/test/test-bug-593003-iframe-wrong-hud.html index 54b66866812b..e2ec9dd54046 100644 --- a/browser/devtools/webconsole/test/test-bug-593003-iframe-wrong-hud.html +++ b/browser/devtools/webconsole/test/test-bug-593003-iframe-wrong-hud.html @@ -8,6 +8,6 @@

WebConsole test: iframe associated to the wrong HUD.

+ src="http://example.com/browser/browser/devtools/webconsole/test/test-bug-593003-iframe-wrong-hud-iframe.html"> diff --git a/browser/devtools/webconsole/test/test-network-request.html b/browser/devtools/webconsole/test/test-network-request.html index 9c42ecab1bc5..7d95dd6d70b3 100644 --- a/browser/devtools/webconsole/test/test-network-request.html +++ b/browser/devtools/webconsole/test/test-network-request.html @@ -27,7 +27,7 @@

Heads Up Display HTTP Logging Testpage

This page is used to test the HTTP logging.

-
+

diff --git a/browser/installer/package-manifest.in b/browser/installer/package-manifest.in index af912ae732d2..38c07ea94622 100644 --- a/browser/installer/package-manifest.in +++ b/browser/installer/package-manifest.in @@ -283,6 +283,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 @@ -346,6 +347,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 diff --git a/browser/modules/Makefile.in b/browser/modules/Makefile.in index e093d192fa4c..8819d128fa27 100644 --- a/browser/modules/Makefile.in +++ b/browser/modules/Makefile.in @@ -52,6 +52,7 @@ EXTRA_JS_MODULES = \ openLocationLastURL.jsm \ NetworkPrioritizer.jsm \ offlineAppCache.jsm \ + PageThumbs.jsm \ $(NULL) ifeq ($(MOZ_WIDGET_TOOLKIT),windows) diff --git a/browser/modules/PageThumbs.jsm b/browser/modules/PageThumbs.jsm new file mode 100644 index 000000000000..37b24b7ec44d --- /dev/null +++ b/browser/modules/PageThumbs.jsm @@ -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); +}); diff --git a/browser/themes/gnomestripe/devtools/splitview.css b/browser/themes/gnomestripe/devtools/splitview.css index 836e7d3812dd..5c082b3c9471 100644 --- a/browser/themes/gnomestripe/devtools/splitview.css +++ b/browser/themes/gnomestripe/devtools/splitview.css @@ -41,6 +41,12 @@ color: white; } +.loading .splitview-nav-container { + background-image: url(chrome://global/skin/icons/loading_16.png); + background-repeat: no-repeat; + background-position: center center; +} + .splitview-nav { -moz-appearance: none; margin: 0; @@ -70,7 +76,6 @@ .placeholder { -moz-box-flex: 1; - -moz-box-back: center; text-align: center; } diff --git a/browser/themes/gnomestripe/devtools/styleeditor.css b/browser/themes/gnomestripe/devtools/styleeditor.css index 6ed3edded54e..8ab81bab4858 100644 --- a/browser/themes/gnomestripe/devtools/styleeditor.css +++ b/browser/themes/gnomestripe/devtools/styleeditor.css @@ -36,6 +36,10 @@ * * ***** END LICENSE BLOCK ***** */ +#style-editor-chrome { + background-color: hsl(208,11%,27%); +} + .stylesheet-title, .stylesheet-name { text-decoration: none; diff --git a/browser/themes/pinstripe/devtools/splitview.css b/browser/themes/pinstripe/devtools/splitview.css index 836e7d3812dd..5c082b3c9471 100644 --- a/browser/themes/pinstripe/devtools/splitview.css +++ b/browser/themes/pinstripe/devtools/splitview.css @@ -41,6 +41,12 @@ color: white; } +.loading .splitview-nav-container { + background-image: url(chrome://global/skin/icons/loading_16.png); + background-repeat: no-repeat; + background-position: center center; +} + .splitview-nav { -moz-appearance: none; margin: 0; @@ -70,7 +76,6 @@ .placeholder { -moz-box-flex: 1; - -moz-box-back: center; text-align: center; } diff --git a/browser/themes/pinstripe/devtools/styleeditor.css b/browser/themes/pinstripe/devtools/styleeditor.css index 6ed3edded54e..8ab81bab4858 100644 --- a/browser/themes/pinstripe/devtools/styleeditor.css +++ b/browser/themes/pinstripe/devtools/styleeditor.css @@ -36,6 +36,10 @@ * * ***** END LICENSE BLOCK ***** */ +#style-editor-chrome { + background-color: hsl(208,11%,27%); +} + .stylesheet-title, .stylesheet-name { text-decoration: none; diff --git a/browser/themes/winstripe/devtools/splitview.css b/browser/themes/winstripe/devtools/splitview.css index 645390e8b1b9..054701dde56b 100644 --- a/browser/themes/winstripe/devtools/splitview.css +++ b/browser/themes/winstripe/devtools/splitview.css @@ -41,6 +41,12 @@ color: white; } +.loading .splitview-nav-container { + background-image: url(chrome://global/skin/icons/loading_16.png); + background-repeat: no-repeat; + background-position: center center; +} + .splitview-nav { -moz-appearance: none; margin: 0; @@ -70,7 +76,6 @@ .placeholder { -moz-box-flex: 1; - -moz-box-back: center; text-align: center; } diff --git a/browser/themes/winstripe/devtools/styleeditor.css b/browser/themes/winstripe/devtools/styleeditor.css index bd933c15ccf7..34446cfff27d 100644 --- a/browser/themes/winstripe/devtools/styleeditor.css +++ b/browser/themes/winstripe/devtools/styleeditor.css @@ -36,6 +36,10 @@ * * ***** END LICENSE BLOCK ***** */ +#style-editor-chrome { + background-color: hsl(211,21%,26%); +} + .stylesheet-title, .stylesheet-name { text-decoration: none; diff --git a/toolkit/content/Services.jsm b/toolkit/content/Services.jsm index 8a2ce8fa7e91..6bef52e71ebf 100644 --- a/toolkit/content/Services.jsm +++ b/toolkit/content/Services.jsm @@ -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"],