The printing back-end used to be in charge of opening up an error dialog when things go wrong with printing. Now we fire an event and let PrintUtils do the work of showing the error message. This has the added bonus of making the error messages work with e10s.
1128 lines
37 KiB
JavaScript
1128 lines
37 KiB
JavaScript
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
|
/* 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/. */
|
|
|
|
var Cc = Components.classes;
|
|
var Ci = Components.interfaces;
|
|
var Cu = Components.utils;
|
|
var Cr = Components.results;
|
|
|
|
Cu.import("resource://gre/modules/Services.jsm");
|
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
|
|
|
var global = this;
|
|
|
|
|
|
// Lazily load the finder code
|
|
addMessageListener("Finder:Initialize", function () {
|
|
let {RemoteFinderListener} = Cu.import("resource://gre/modules/RemoteFinder.jsm", {});
|
|
new RemoteFinderListener(global);
|
|
});
|
|
|
|
var ClickEventHandler = {
|
|
init: function init() {
|
|
this._scrollable = null;
|
|
this._scrolldir = "";
|
|
this._startX = null;
|
|
this._startY = null;
|
|
this._screenX = null;
|
|
this._screenY = null;
|
|
this._lastFrame = null;
|
|
this.autoscrollLoop = this.autoscrollLoop.bind(this);
|
|
|
|
Services.els.addSystemEventListener(global, "mousedown", this, true);
|
|
|
|
addMessageListener("Autoscroll:Stop", this);
|
|
},
|
|
|
|
isAutoscrollBlocker: function(node) {
|
|
let mmPaste = Services.prefs.getBoolPref("middlemouse.paste");
|
|
let mmScrollbarPosition = Services.prefs.getBoolPref("middlemouse.scrollbarPosition");
|
|
|
|
while (node) {
|
|
if ((node instanceof content.HTMLAnchorElement || node instanceof content.HTMLAreaElement) &&
|
|
node.hasAttribute("href")) {
|
|
return true;
|
|
}
|
|
|
|
if (mmPaste && (node instanceof content.HTMLInputElement ||
|
|
node instanceof content.HTMLTextAreaElement)) {
|
|
return true;
|
|
}
|
|
|
|
if (node instanceof content.XULElement && mmScrollbarPosition
|
|
&& (node.localName == "scrollbar" || node.localName == "scrollcorner")) {
|
|
return true;
|
|
}
|
|
|
|
node = node.parentNode;
|
|
}
|
|
return false;
|
|
},
|
|
|
|
findNearestScrollableElement: function(aNode) {
|
|
// this is a list of overflow property values that allow scrolling
|
|
const scrollingAllowed = ['scroll', 'auto'];
|
|
|
|
// go upward in the DOM and find any parent element that has a overflow
|
|
// area and can therefore be scrolled
|
|
for (this._scrollable = aNode; this._scrollable;
|
|
this._scrollable = this._scrollable.parentNode) {
|
|
// do not use overflow based autoscroll for <html> and <body>
|
|
// Elements or non-html elements such as svg or Document nodes
|
|
// also make sure to skip select elements that are not multiline
|
|
if (!(this._scrollable instanceof content.HTMLElement) ||
|
|
((this._scrollable instanceof content.HTMLSelectElement) && !this._scrollable.multiple)) {
|
|
continue;
|
|
}
|
|
|
|
var overflowx = this._scrollable.ownerDocument.defaultView
|
|
.getComputedStyle(this._scrollable, '')
|
|
.getPropertyValue('overflow-x');
|
|
var overflowy = this._scrollable.ownerDocument.defaultView
|
|
.getComputedStyle(this._scrollable, '')
|
|
.getPropertyValue('overflow-y');
|
|
// we already discarded non-multiline selects so allow vertical
|
|
// scroll for multiline ones directly without checking for a
|
|
// overflow property
|
|
var scrollVert = this._scrollable.scrollTopMax &&
|
|
(this._scrollable instanceof content.HTMLSelectElement ||
|
|
scrollingAllowed.indexOf(overflowy) >= 0);
|
|
|
|
// do not allow horizontal scrolling for select elements, it leads
|
|
// to visual artifacts and is not the expected behavior anyway
|
|
if (!(this._scrollable instanceof content.HTMLSelectElement) &&
|
|
this._scrollable.scrollLeftMax &&
|
|
scrollingAllowed.indexOf(overflowx) >= 0) {
|
|
this._scrolldir = scrollVert ? "NSEW" : "EW";
|
|
break;
|
|
} else if (scrollVert) {
|
|
this._scrolldir = "NS";
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!this._scrollable) {
|
|
this._scrollable = aNode.ownerDocument.defaultView;
|
|
if (this._scrollable.scrollMaxX > 0) {
|
|
this._scrolldir = this._scrollable.scrollMaxY > 0 ? "NSEW" : "EW";
|
|
} else if (this._scrollable.scrollMaxY > 0) {
|
|
this._scrolldir = "NS";
|
|
} else if (this._scrollable.frameElement) {
|
|
this.findNearestScrollableElement(this._scrollable.frameElement);
|
|
} else {
|
|
this._scrollable = null; // abort scrolling
|
|
}
|
|
}
|
|
},
|
|
|
|
startScroll: function(event) {
|
|
|
|
this.findNearestScrollableElement(event.originalTarget);
|
|
|
|
if (!this._scrollable)
|
|
return;
|
|
|
|
let [enabled] = sendSyncMessage("Autoscroll:Start",
|
|
{scrolldir: this._scrolldir,
|
|
screenX: event.screenX,
|
|
screenY: event.screenY});
|
|
if (!enabled) {
|
|
this._scrollable = null;
|
|
return;
|
|
}
|
|
|
|
Services.els.addSystemEventListener(global, "mousemove", this, true);
|
|
addEventListener("pagehide", this, true);
|
|
|
|
this._ignoreMouseEvents = true;
|
|
this._startX = event.screenX;
|
|
this._startY = event.screenY;
|
|
this._screenX = event.screenX;
|
|
this._screenY = event.screenY;
|
|
this._scrollErrorX = 0;
|
|
this._scrollErrorY = 0;
|
|
this._lastFrame = content.performance.now();
|
|
|
|
content.requestAnimationFrame(this.autoscrollLoop);
|
|
},
|
|
|
|
stopScroll: function() {
|
|
if (this._scrollable) {
|
|
this._scrollable.mozScrollSnap();
|
|
this._scrollable = null;
|
|
|
|
Services.els.removeSystemEventListener(global, "mousemove", this, true);
|
|
removeEventListener("pagehide", this, true);
|
|
}
|
|
},
|
|
|
|
accelerate: function(curr, start) {
|
|
const speed = 12;
|
|
var val = (curr - start) / speed;
|
|
|
|
if (val > 1)
|
|
return val * Math.sqrt(val) - 1;
|
|
if (val < -1)
|
|
return val * Math.sqrt(-val) + 1;
|
|
return 0;
|
|
},
|
|
|
|
roundToZero: function(num) {
|
|
if (num > 0)
|
|
return Math.floor(num);
|
|
return Math.ceil(num);
|
|
},
|
|
|
|
autoscrollLoop: function(timestamp) {
|
|
if (!this._scrollable) {
|
|
// Scrolling has been canceled
|
|
return;
|
|
}
|
|
|
|
// avoid long jumps when the browser hangs for more than
|
|
// |maxTimeDelta| ms
|
|
const maxTimeDelta = 100;
|
|
var timeDelta = Math.min(maxTimeDelta, timestamp - this._lastFrame);
|
|
// we used to scroll |accelerate()| pixels every 20ms (50fps)
|
|
var timeCompensation = timeDelta / 20;
|
|
this._lastFrame = timestamp;
|
|
|
|
var actualScrollX = 0;
|
|
var actualScrollY = 0;
|
|
// don't bother scrolling vertically when the scrolldir is only horizontal
|
|
// and the other way around
|
|
if (this._scrolldir != 'EW') {
|
|
var y = this.accelerate(this._screenY, this._startY) * timeCompensation;
|
|
var desiredScrollY = this._scrollErrorY + y;
|
|
actualScrollY = this.roundToZero(desiredScrollY);
|
|
this._scrollErrorY = (desiredScrollY - actualScrollY);
|
|
}
|
|
if (this._scrolldir != 'NS') {
|
|
var x = this.accelerate(this._screenX, this._startX) * timeCompensation;
|
|
var desiredScrollX = this._scrollErrorX + x;
|
|
actualScrollX = this.roundToZero(desiredScrollX);
|
|
this._scrollErrorX = (desiredScrollX - actualScrollX);
|
|
}
|
|
|
|
if (this._scrollable instanceof content.Window) {
|
|
this._scrollable.scrollBy(actualScrollX, actualScrollY);
|
|
} else { // an element with overflow
|
|
this._scrollable.scrollLeft += actualScrollX;
|
|
this._scrollable.scrollTop += actualScrollY;
|
|
}
|
|
content.requestAnimationFrame(this.autoscrollLoop);
|
|
},
|
|
|
|
handleEvent: function(event) {
|
|
if (event.type == "mousemove") {
|
|
this._screenX = event.screenX;
|
|
this._screenY = event.screenY;
|
|
} else if (event.type == "mousedown") {
|
|
if (event.isTrusted &
|
|
!event.defaultPrevented &&
|
|
event.button == 1 &&
|
|
!this._scrollable &&
|
|
!this.isAutoscrollBlocker(event.originalTarget)) {
|
|
this.startScroll(event);
|
|
}
|
|
} else if (event.type == "pagehide") {
|
|
if (this._scrollable) {
|
|
var doc =
|
|
this._scrollable.ownerDocument || this._scrollable.document;
|
|
if (doc == event.target) {
|
|
sendAsyncMessage("Autoscroll:Cancel");
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
receiveMessage: function(msg) {
|
|
switch (msg.name) {
|
|
case "Autoscroll:Stop": {
|
|
this.stopScroll();
|
|
break;
|
|
}
|
|
}
|
|
},
|
|
};
|
|
ClickEventHandler.init();
|
|
|
|
var PopupBlocking = {
|
|
popupData: null,
|
|
popupDataInternal: null,
|
|
|
|
init: function() {
|
|
addEventListener("DOMPopupBlocked", this, true);
|
|
addEventListener("pageshow", this, true);
|
|
addEventListener("pagehide", this, true);
|
|
|
|
addMessageListener("PopupBlocking:UnblockPopup", this);
|
|
},
|
|
|
|
receiveMessage: function(msg) {
|
|
switch (msg.name) {
|
|
case "PopupBlocking:UnblockPopup": {
|
|
let i = msg.data.index;
|
|
if (this.popupData && this.popupData[i]) {
|
|
let data = this.popupData[i];
|
|
let internals = this.popupDataInternal[i];
|
|
let dwi = internals.requestingWindow;
|
|
|
|
// If we have a requesting window and the requesting document is
|
|
// still the current document, open the popup.
|
|
if (dwi && dwi.document == internals.requestingDocument) {
|
|
dwi.open(data.popupWindowURI, data.popupWindowName, data.popupWindowFeatures);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
},
|
|
|
|
handleEvent: function(ev) {
|
|
switch (ev.type) {
|
|
case "DOMPopupBlocked":
|
|
return this.onPopupBlocked(ev);
|
|
case "pageshow":
|
|
return this.onPageShow(ev);
|
|
case "pagehide":
|
|
return this.onPageHide(ev);
|
|
}
|
|
},
|
|
|
|
onPopupBlocked: function(ev) {
|
|
if (!this.popupData) {
|
|
this.popupData = new Array();
|
|
this.popupDataInternal = new Array();
|
|
}
|
|
|
|
let obj = {
|
|
popupWindowURI: ev.popupWindowURI.spec,
|
|
popupWindowFeatures: ev.popupWindowFeatures,
|
|
popupWindowName: ev.popupWindowName
|
|
};
|
|
|
|
let internals = {
|
|
requestingWindow: ev.requestingWindow,
|
|
requestingDocument: ev.requestingWindow.document,
|
|
};
|
|
|
|
this.popupData.push(obj);
|
|
this.popupDataInternal.push(internals);
|
|
this.updateBlockedPopups(true);
|
|
},
|
|
|
|
onPageShow: function(ev) {
|
|
if (this.popupData) {
|
|
let i = 0;
|
|
while (i < this.popupData.length) {
|
|
// Filter out irrelevant reports.
|
|
if (this.popupDataInternal[i].requestingWindow &&
|
|
(this.popupDataInternal[i].requestingWindow.document ==
|
|
this.popupDataInternal[i].requestingDocument)) {
|
|
i++;
|
|
} else {
|
|
this.popupData.splice(i, 1);
|
|
this.popupDataInternal.splice(i, 1);
|
|
}
|
|
}
|
|
if (this.popupData.length == 0) {
|
|
this.popupData = null;
|
|
this.popupDataInternal = null;
|
|
}
|
|
this.updateBlockedPopups(false);
|
|
}
|
|
},
|
|
|
|
onPageHide: function(ev) {
|
|
if (this.popupData) {
|
|
this.popupData = null;
|
|
this.popupDataInternal = null;
|
|
this.updateBlockedPopups(false);
|
|
}
|
|
},
|
|
|
|
updateBlockedPopups: function(freshPopup) {
|
|
sendAsyncMessage("PopupBlocking:UpdateBlockedPopups",
|
|
{blockedPopups: this.popupData, freshPopup: freshPopup});
|
|
},
|
|
};
|
|
PopupBlocking.init();
|
|
|
|
XPCOMUtils.defineLazyGetter(this, "console", () => {
|
|
// Set up console.* for frame scripts.
|
|
let Console = Components.utils.import("resource://gre/modules/devtools/Console.jsm", {});
|
|
return new Console.ConsoleAPI();
|
|
});
|
|
|
|
var Printing = {
|
|
// Bug 1088061: nsPrintEngine's DoCommonPrint currently expects the
|
|
// progress listener passed to it to QI to an nsIPrintingPromptService
|
|
// in order to know that a printing progress dialog has been shown. That's
|
|
// really all the interface is used for, hence the fact that I don't actually
|
|
// implement the interface here. Bug 1088061 has been filed to remove
|
|
// this hackery.
|
|
QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
|
|
Ci.nsIPrintingPromptService]),
|
|
|
|
MESSAGES: [
|
|
"Printing:Preview:Enter",
|
|
"Printing:Preview:Exit",
|
|
"Printing:Preview:Navigate",
|
|
"Printing:Preview:UpdatePageCount",
|
|
"Printing:Print",
|
|
],
|
|
|
|
init() {
|
|
this.MESSAGES.forEach(msgName => addMessageListener(msgName, this));
|
|
addEventListener("PrintingError", this, true);
|
|
},
|
|
|
|
get shouldSavePrintSettings() {
|
|
return Services.prefs.getBoolPref("print.use_global_printsettings", false) &&
|
|
Services.prefs.getBoolPref("print.save_print_settings", false);
|
|
},
|
|
|
|
handleEvent(event) {
|
|
if (event.type == "PrintingError") {
|
|
let win = event.target.defaultView;
|
|
let wbp = win.QueryInterface(Ci.nsIInterfaceRequestor)
|
|
.getInterface(Ci.nsIWebBrowserPrint);
|
|
let nsresult = event.detail;
|
|
sendAsyncMessage("Printing:Error", {
|
|
isPrinting: wbp.doingPrint,
|
|
nsresult: nsresult,
|
|
});
|
|
}
|
|
},
|
|
|
|
receiveMessage(message) {
|
|
let objects = message.objects;
|
|
let data = message.data;
|
|
switch(message.name) {
|
|
case "Printing:Preview:Enter": {
|
|
this.enterPrintPreview(Services.wm.getOuterWindowWithId(data.windowID));
|
|
break;
|
|
}
|
|
|
|
case "Printing:Preview:Exit": {
|
|
this.exitPrintPreview();
|
|
break;
|
|
}
|
|
|
|
case "Printing:Preview:Navigate": {
|
|
this.navigate(data.navType, data.pageNum);
|
|
break;
|
|
}
|
|
|
|
case "Printing:Preview:UpdatePageCount": {
|
|
this.updatePageCount();
|
|
break;
|
|
}
|
|
|
|
case "Printing:Print": {
|
|
this.print(Services.wm.getOuterWindowWithId(data.windowID));
|
|
break;
|
|
}
|
|
}
|
|
},
|
|
|
|
getPrintSettings() {
|
|
try {
|
|
let PSSVC = Cc["@mozilla.org/gfx/printsettings-service;1"]
|
|
.getService(Ci.nsIPrintSettingsService);
|
|
|
|
let printSettings = PSSVC.globalPrintSettings;
|
|
if (!printSettings.printerName) {
|
|
printSettings.printerName = PSSVC.defaultPrinterName;
|
|
}
|
|
// First get any defaults from the printer
|
|
PSSVC.initPrintSettingsFromPrinter(printSettings.printerName,
|
|
printSettings);
|
|
// now augment them with any values from last time
|
|
PSSVC.initPrintSettingsFromPrefs(printSettings, true,
|
|
printSettings.kInitSaveAll);
|
|
|
|
return printSettings;
|
|
} catch(e) {
|
|
Components.utils.reportError(e);
|
|
}
|
|
|
|
return null;
|
|
},
|
|
|
|
enterPrintPreview(contentWindow) {
|
|
// We'll call this whenever we've finished reflowing the document, or if
|
|
// we errored out while attempting to print preview (in which case, we'll
|
|
// notify the parent that we've failed).
|
|
let notifyEntered = (error) => {
|
|
removeEventListener("printPreviewUpdate", onPrintPreviewReady);
|
|
sendAsyncMessage("Printing:Preview:Entered", {
|
|
failed: !!error,
|
|
});
|
|
};
|
|
|
|
let onPrintPreviewReady = () => {
|
|
notifyEntered();
|
|
};
|
|
|
|
// We have to wait for the print engine to finish reflowing all of the
|
|
// documents and subdocuments before we can tell the parent to flip to
|
|
// the print preview UI - otherwise, the print preview UI might ask for
|
|
// information (like the number of pages in the document) before we have
|
|
// our PresShells set up.
|
|
addEventListener("printPreviewUpdate", onPrintPreviewReady);
|
|
|
|
try {
|
|
let printSettings = this.getPrintSettings();
|
|
docShell.printPreview.printPreview(printSettings, contentWindow, this);
|
|
} catch(error) {
|
|
// This might fail if we, for example, attempt to print a XUL document.
|
|
// In that case, we inform the parent to bail out of print preview.
|
|
Components.utils.reportError(error);
|
|
notifyEntered(error);
|
|
}
|
|
},
|
|
|
|
exitPrintPreview() {
|
|
docShell.printPreview.exitPrintPreview();
|
|
},
|
|
|
|
print(contentWindow) {
|
|
let printSettings = this.getPrintSettings();
|
|
let rv = Cr.NS_OK;
|
|
try {
|
|
let print = contentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
|
|
.getInterface(Ci.nsIWebBrowserPrint);
|
|
print.print(printSettings, null);
|
|
} catch(e) {
|
|
// Pressing cancel is expressed as an NS_ERROR_ABORT return value,
|
|
// causing an exception to be thrown which we catch here.
|
|
if (e.result != Cr.NS_ERROR_ABORT) {
|
|
Cu.reportError(`In Printing:Print:Done handler, got unexpected rv
|
|
${e.result}.`);
|
|
sendAsyncMessage("Printing:Error", {
|
|
isPrinting: true,
|
|
nsresult: e.result,
|
|
});
|
|
}
|
|
}
|
|
|
|
if (this.shouldSavePrintSettings) {
|
|
let PSSVC = Cc["@mozilla.org/gfx/printsettings-service;1"]
|
|
.getService(Ci.nsIPrintSettingsService);
|
|
|
|
PSSVC.savePrintSettingsToPrefs(printSettings, true,
|
|
printSettings.kInitSaveAll);
|
|
PSSVC.savePrintSettingsToPrefs(printSettings, false,
|
|
printSettings.kInitSavePrinterName);
|
|
}
|
|
},
|
|
|
|
updatePageCount() {
|
|
let numPages = docShell.printPreview.printPreviewNumPages;
|
|
sendAsyncMessage("Printing:Preview:UpdatePageCount", {
|
|
numPages: numPages,
|
|
});
|
|
},
|
|
|
|
navigate(navType, pageNum) {
|
|
docShell.printPreview.printPreviewNavigate(navType, pageNum);
|
|
},
|
|
|
|
/* nsIWebProgressListener for print preview */
|
|
|
|
onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) {
|
|
sendAsyncMessage("Printing:Preview:StateChange", {
|
|
stateFlags: aStateFlags,
|
|
status: aStatus,
|
|
});
|
|
},
|
|
|
|
onProgressChange(aWebProgress, aRequest, aCurSelfProgress,
|
|
aMaxSelfProgress, aCurTotalProgress,
|
|
aMaxTotalProgress) {
|
|
sendAsyncMessage("Printing:Preview:ProgressChange", {
|
|
curSelfProgress: aCurSelfProgress,
|
|
maxSelfProgress: aMaxSelfProgress,
|
|
curTotalProgress: aCurTotalProgress,
|
|
maxTotalProgress: aMaxTotalProgress,
|
|
});
|
|
},
|
|
|
|
onLocationChange(aWebProgress, aRequest, aLocation, aFlags) {},
|
|
onStatusChange(aWebProgress, aRequest, aStatus, aMessage) {},
|
|
onSecurityChange(aWebProgress, aRequest, aState) {},
|
|
}
|
|
Printing.init();
|
|
|
|
function SwitchDocumentDirection(aWindow) {
|
|
// document.dir can also be "auto", in which case it won't change
|
|
if (aWindow.document.dir == "ltr" || aWindow.document.dir == "") {
|
|
aWindow.document.dir = "rtl";
|
|
} else if (aWindow.document.dir == "rtl") {
|
|
aWindow.document.dir = "ltr";
|
|
}
|
|
for (let run = 0; run < aWindow.frames.length; run++) {
|
|
SwitchDocumentDirection(aWindow.frames[run]);
|
|
}
|
|
}
|
|
|
|
addMessageListener("SwitchDocumentDirection", () => {
|
|
SwitchDocumentDirection(content.window);
|
|
});
|
|
|
|
var FindBar = {
|
|
/* Please keep in sync with toolkit/content/widgets/findbar.xml */
|
|
FIND_NORMAL: 0,
|
|
FIND_TYPEAHEAD: 1,
|
|
FIND_LINKS: 2,
|
|
FAYT_LINKS_KEY: "'".charCodeAt(0),
|
|
FAYT_TEXT_KEY: "/".charCodeAt(0),
|
|
|
|
_findMode: 0,
|
|
get _findAsYouType() {
|
|
return Services.prefs.getBoolPref("accessibility.typeaheadfind");
|
|
},
|
|
|
|
init() {
|
|
addMessageListener("Findbar:UpdateState", this);
|
|
Services.els.addSystemEventListener(global, "keypress", this, false);
|
|
Services.els.addSystemEventListener(global, "mouseup", this, false);
|
|
},
|
|
|
|
receiveMessage(msg) {
|
|
switch (msg.name) {
|
|
case "Findbar:UpdateState":
|
|
this._findMode = msg.data.findMode;
|
|
this._findAsYouType = msg.data.findAsYouType;
|
|
break;
|
|
}
|
|
},
|
|
|
|
handleEvent(event) {
|
|
switch (event.type) {
|
|
case "keypress":
|
|
this._onKeypress(event);
|
|
break;
|
|
case "mouseup":
|
|
this._onMouseup(event);
|
|
break;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Returns whether FAYT can be used for the given event in
|
|
* the current content state.
|
|
*/
|
|
_shouldFastFind() {
|
|
//XXXgijs: why all these shenanigans? Why not use the event's target?
|
|
let focusedWindow = {};
|
|
let elt = Services.focus.getFocusedElementForWindow(content, true, focusedWindow);
|
|
let win = focusedWindow.value;
|
|
let {BrowserUtils} = Cu.import("resource://gre/modules/BrowserUtils.jsm", {});
|
|
return BrowserUtils.shouldFastFind(elt, win);
|
|
},
|
|
|
|
_onKeypress(event) {
|
|
// Useless keys:
|
|
if (event.ctrlKey || event.altKey || event.metaKey || event.defaultPrevented) {
|
|
return;
|
|
}
|
|
// Not interested in random keypresses most of the time:
|
|
if (this._findMode == this.FIND_NORMAL && !this._findAsYouType &&
|
|
event.charCode != this.FAYT_LINKS_KEY && event.charCode != this.FAYT_TEXT_KEY) {
|
|
return;
|
|
}
|
|
|
|
// Check the focused element etc.
|
|
if (!this._shouldFastFind()) {
|
|
return;
|
|
}
|
|
|
|
let fakeEvent = {};
|
|
for (let k in event) {
|
|
if (typeof event[k] != "object" && typeof event[k] != "function") {
|
|
fakeEvent[k] = event[k];
|
|
}
|
|
}
|
|
// sendSyncMessage returns an array of the responses from all listeners
|
|
let rv = sendSyncMessage("Findbar:Keypress", fakeEvent);
|
|
if (rv.indexOf(false) !== -1) {
|
|
event.preventDefault();
|
|
return false;
|
|
}
|
|
},
|
|
|
|
_onMouseup(event) {
|
|
if (this._findMode != this.FIND_NORMAL)
|
|
sendAsyncMessage("Findbar:Mouseup");
|
|
},
|
|
};
|
|
FindBar.init();
|
|
|
|
// An event listener for custom "WebChannelMessageToChrome" events on pages.
|
|
addEventListener("WebChannelMessageToChrome", function (e) {
|
|
// If target is window then we want the document principal, otherwise fallback to target itself.
|
|
let principal = e.target.nodePrincipal ? e.target.nodePrincipal : e.target.document.nodePrincipal;
|
|
|
|
if (e.detail) {
|
|
sendAsyncMessage("WebChannelMessageToChrome", e.detail, { eventTarget: e.target }, principal);
|
|
} else {
|
|
Cu.reportError("WebChannel message failed. No message detail.");
|
|
}
|
|
}, true, true);
|
|
|
|
// This should be kept in sync with /browser/base/content.js.
|
|
// Add message listener for "WebChannelMessageToContent" messages from chrome scripts.
|
|
addMessageListener("WebChannelMessageToContent", function (e) {
|
|
if (e.data) {
|
|
// e.objects.eventTarget will be defined if sending a response to
|
|
// a WebChannelMessageToChrome event. An unsolicited send
|
|
// may not have an eventTarget defined, in this case send to the
|
|
// main content window.
|
|
let eventTarget = e.objects.eventTarget || content;
|
|
|
|
// Use nodePrincipal if available, otherwise fallback to document principal.
|
|
let targetPrincipal = eventTarget instanceof Ci.nsIDOMWindow ? eventTarget.document.nodePrincipal : eventTarget.nodePrincipal;
|
|
|
|
if (e.principal.subsumes(targetPrincipal)) {
|
|
// If eventTarget is a window, use it as the targetWindow, otherwise
|
|
// find the window that owns the eventTarget.
|
|
let targetWindow = eventTarget instanceof Ci.nsIDOMWindow ? eventTarget : eventTarget.ownerDocument.defaultView;
|
|
|
|
eventTarget.dispatchEvent(new targetWindow.CustomEvent("WebChannelMessageToContent", {
|
|
detail: Cu.cloneInto({
|
|
id: e.data.id,
|
|
message: e.data.message,
|
|
}, targetWindow),
|
|
}));
|
|
} else {
|
|
Cu.reportError("WebChannel message failed. Principal mismatch.");
|
|
}
|
|
} else {
|
|
Cu.reportError("WebChannel message failed. No message data.");
|
|
}
|
|
});
|
|
|
|
var AudioPlaybackListener = {
|
|
QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
|
|
|
|
init() {
|
|
Services.obs.addObserver(this, "audio-playback", false);
|
|
addMessageListener("AudioPlaybackMute", this);
|
|
addEventListener("unload", () => {
|
|
AudioPlaybackListener.uninit();
|
|
});
|
|
},
|
|
|
|
uninit() {
|
|
Services.obs.removeObserver(this, "audio-playback");
|
|
removeMessageListener("AudioPlaybackMute", this);
|
|
},
|
|
|
|
observe(subject, topic, data) {
|
|
if (topic === "audio-playback") {
|
|
if (subject && subject.top == global.content) {
|
|
let name = "AudioPlayback:";
|
|
name += (data === "active") ? "Start" : "Stop";
|
|
sendAsyncMessage(name);
|
|
}
|
|
}
|
|
},
|
|
|
|
receiveMessage(msg) {
|
|
if (msg.name == "AudioPlaybackMute") {
|
|
let utils = global.content.QueryInterface(Ci.nsIInterfaceRequestor)
|
|
.getInterface(Ci.nsIDOMWindowUtils);
|
|
utils.audioMuted = msg.data.type === "mute";
|
|
}
|
|
},
|
|
};
|
|
AudioPlaybackListener.init();
|
|
|
|
addMessageListener("Browser:PurgeSessionHistory", function BrowserPurgeHistory() {
|
|
let sessionHistory = docShell.QueryInterface(Ci.nsIWebNavigation).sessionHistory;
|
|
if (!sessionHistory) {
|
|
return;
|
|
}
|
|
|
|
// place the entry at current index at the end of the history list, so it won't get removed
|
|
if (sessionHistory.index < sessionHistory.count - 1) {
|
|
let indexEntry = sessionHistory.getEntryAtIndex(sessionHistory.index, false);
|
|
sessionHistory.QueryInterface(Components.interfaces.nsISHistoryInternal);
|
|
indexEntry.QueryInterface(Components.interfaces.nsISHEntry);
|
|
sessionHistory.addEntry(indexEntry, true);
|
|
}
|
|
|
|
let purge = sessionHistory.count;
|
|
if (global.content.location.href != "about:blank") {
|
|
--purge; // Don't remove the page the user's staring at from shistory
|
|
}
|
|
|
|
if (purge > 0) {
|
|
sessionHistory.PurgeHistory(purge);
|
|
}
|
|
});
|
|
|
|
var ViewSelectionSource = {
|
|
init: function () {
|
|
addMessageListener("ViewSource:GetSelection", this);
|
|
},
|
|
|
|
receiveMessage: function(message) {
|
|
if (message.name == "ViewSource:GetSelection") {
|
|
let selectionDetails;
|
|
try {
|
|
selectionDetails = message.objects.target ? this.getMathMLSelection(message.objects.target)
|
|
: this.getSelection();
|
|
} finally {
|
|
sendAsyncMessage("ViewSource:GetSelectionDone", selectionDetails);
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* A helper to get a path like FIXptr, but with an array instead of the
|
|
* "tumbler" notation.
|
|
* See FIXptr: http://lists.w3.org/Archives/Public/www-xml-linking-comments/2001AprJun/att-0074/01-NOTE-FIXptr-20010425.htm
|
|
*/
|
|
getPath: function(ancestor, node) {
|
|
var n = node;
|
|
var p = n.parentNode;
|
|
if (n == ancestor || !p)
|
|
return null;
|
|
var path = new Array();
|
|
if (!path)
|
|
return null;
|
|
do {
|
|
for (var i = 0; i < p.childNodes.length; i++) {
|
|
if (p.childNodes.item(i) == n) {
|
|
path.push(i);
|
|
break;
|
|
}
|
|
}
|
|
n = p;
|
|
p = n.parentNode;
|
|
} while (n != ancestor && p);
|
|
return path;
|
|
},
|
|
|
|
getSelection: function () {
|
|
// These are markers used to delimit the selection during processing. They
|
|
// are removed from the final rendering.
|
|
// We use noncharacter Unicode codepoints to minimize the risk of clashing
|
|
// with anything that might legitimately be present in the document.
|
|
// U+FDD0..FDEF <noncharacters>
|
|
const MARK_SELECTION_START = "\uFDD0";
|
|
const MARK_SELECTION_END = "\uFDEF";
|
|
|
|
var focusedWindow = Services.focus.focusedWindow || content;
|
|
var selection = focusedWindow.getSelection();
|
|
|
|
var range = selection.getRangeAt(0);
|
|
var ancestorContainer = range.commonAncestorContainer;
|
|
var doc = ancestorContainer.ownerDocument;
|
|
|
|
var startContainer = range.startContainer;
|
|
var endContainer = range.endContainer;
|
|
var startOffset = range.startOffset;
|
|
var endOffset = range.endOffset;
|
|
|
|
// let the ancestor be an element
|
|
var Node = doc.defaultView.Node;
|
|
if (ancestorContainer.nodeType == Node.TEXT_NODE ||
|
|
ancestorContainer.nodeType == Node.CDATA_SECTION_NODE)
|
|
ancestorContainer = ancestorContainer.parentNode;
|
|
|
|
// for selectAll, let's use the entire document, including <html>...</html>
|
|
// @see nsDocumentViewer::SelectAll() for how selectAll is implemented
|
|
try {
|
|
if (ancestorContainer == doc.body)
|
|
ancestorContainer = doc.documentElement;
|
|
} catch (e) { }
|
|
|
|
// each path is a "child sequence" (a.k.a. "tumbler") that
|
|
// descends from the ancestor down to the boundary point
|
|
var startPath = this.getPath(ancestorContainer, startContainer);
|
|
var endPath = this.getPath(ancestorContainer, endContainer);
|
|
|
|
// clone the fragment of interest and reset everything to be relative to it
|
|
// note: it is with the clone that we operate/munge from now on. Also note
|
|
// that we clone into a data document to prevent images in the fragment from
|
|
// loading and the like. The use of importNode here, as opposed to adoptNode,
|
|
// is _very_ important.
|
|
// XXXbz wish there were a less hacky way to create an untrusted document here
|
|
var isHTML = (doc.createElement("div").tagName == "DIV");
|
|
var dataDoc = isHTML ?
|
|
ancestorContainer.ownerDocument.implementation.createHTMLDocument("") :
|
|
ancestorContainer.ownerDocument.implementation.createDocument("", "", null);
|
|
ancestorContainer = dataDoc.importNode(ancestorContainer, true);
|
|
startContainer = ancestorContainer;
|
|
endContainer = ancestorContainer;
|
|
|
|
// Only bother with the selection if it can be remapped. Don't mess with
|
|
// leaf elements (such as <isindex>) that secretly use anynomous content
|
|
// for their display appearance.
|
|
var canDrawSelection = ancestorContainer.hasChildNodes();
|
|
var tmpNode;
|
|
if (canDrawSelection) {
|
|
var i;
|
|
for (i = startPath ? startPath.length-1 : -1; i >= 0; i--) {
|
|
startContainer = startContainer.childNodes.item(startPath[i]);
|
|
}
|
|
for (i = endPath ? endPath.length-1 : -1; i >= 0; i--) {
|
|
endContainer = endContainer.childNodes.item(endPath[i]);
|
|
}
|
|
|
|
// add special markers to record the extent of the selection
|
|
// note: |startOffset| and |endOffset| are interpreted either as
|
|
// offsets in the text data or as child indices (see the Range spec)
|
|
// (here, munging the end point first to keep the start point safe...)
|
|
if (endContainer.nodeType == Node.TEXT_NODE ||
|
|
endContainer.nodeType == Node.CDATA_SECTION_NODE) {
|
|
// do some extra tweaks to try to avoid the view-source output to look like
|
|
// ...<tag>]... or ...]</tag>... (where ']' marks the end of the selection).
|
|
// To get a neat output, the idea here is to remap the end point from:
|
|
// 1. ...<tag>]... to ...]<tag>...
|
|
// 2. ...]</tag>... to ...</tag>]...
|
|
if ((endOffset > 0 && endOffset < endContainer.data.length) ||
|
|
!endContainer.parentNode || !endContainer.parentNode.parentNode)
|
|
endContainer.insertData(endOffset, MARK_SELECTION_END);
|
|
else {
|
|
tmpNode = dataDoc.createTextNode(MARK_SELECTION_END);
|
|
endContainer = endContainer.parentNode;
|
|
if (endOffset === 0)
|
|
endContainer.parentNode.insertBefore(tmpNode, endContainer);
|
|
else
|
|
endContainer.parentNode.insertBefore(tmpNode, endContainer.nextSibling);
|
|
}
|
|
}
|
|
else {
|
|
tmpNode = dataDoc.createTextNode(MARK_SELECTION_END);
|
|
endContainer.insertBefore(tmpNode, endContainer.childNodes.item(endOffset));
|
|
}
|
|
|
|
if (startContainer.nodeType == Node.TEXT_NODE ||
|
|
startContainer.nodeType == Node.CDATA_SECTION_NODE) {
|
|
// do some extra tweaks to try to avoid the view-source output to look like
|
|
// ...<tag>[... or ...[</tag>... (where '[' marks the start of the selection).
|
|
// To get a neat output, the idea here is to remap the start point from:
|
|
// 1. ...<tag>[... to ...[<tag>...
|
|
// 2. ...[</tag>... to ...</tag>[...
|
|
if ((startOffset > 0 && startOffset < startContainer.data.length) ||
|
|
!startContainer.parentNode || !startContainer.parentNode.parentNode ||
|
|
startContainer != startContainer.parentNode.lastChild)
|
|
startContainer.insertData(startOffset, MARK_SELECTION_START);
|
|
else {
|
|
tmpNode = dataDoc.createTextNode(MARK_SELECTION_START);
|
|
startContainer = startContainer.parentNode;
|
|
if (startOffset === 0)
|
|
startContainer.parentNode.insertBefore(tmpNode, startContainer);
|
|
else
|
|
startContainer.parentNode.insertBefore(tmpNode, startContainer.nextSibling);
|
|
}
|
|
}
|
|
else {
|
|
tmpNode = dataDoc.createTextNode(MARK_SELECTION_START);
|
|
startContainer.insertBefore(tmpNode, startContainer.childNodes.item(startOffset));
|
|
}
|
|
}
|
|
|
|
// now extract and display the syntax highlighted source
|
|
tmpNode = dataDoc.createElementNS("http://www.w3.org/1999/xhtml", "div");
|
|
tmpNode.appendChild(ancestorContainer);
|
|
|
|
return { uri: (isHTML ? "view-source:data:text/html;charset=utf-8," :
|
|
"view-source:data:application/xml;charset=utf-8,")
|
|
+ encodeURIComponent(tmpNode.innerHTML),
|
|
drawSelection: canDrawSelection,
|
|
baseURI: doc.baseURI };
|
|
},
|
|
|
|
/**
|
|
* Reformat the source of a MathML node to highlight the node that was targetted.
|
|
*
|
|
* @param node
|
|
* Some element within the fragment of interest.
|
|
*/
|
|
getMathMLSelection: function(node) {
|
|
var Node = node.ownerDocument.defaultView.Node;
|
|
this._lineCount = 0;
|
|
this._startTargetLine = 0;
|
|
this._endTargetLine = 0;
|
|
this._targetNode = node;
|
|
if (this._targetNode && this._targetNode.nodeType == Node.TEXT_NODE)
|
|
this._targetNode = this._targetNode.parentNode;
|
|
|
|
// walk up the tree to the top-level element (e.g., <math>, <svg>)
|
|
var topTag = "math";
|
|
var topNode = this._targetNode;
|
|
while (topNode && topNode.localName != topTag) {
|
|
topNode = topNode.parentNode;
|
|
}
|
|
if (!topNode)
|
|
return;
|
|
|
|
// serialize
|
|
const VIEW_SOURCE_CSS = "resource://gre-resources/viewsource.css";
|
|
const BUNDLE_URL = "chrome://global/locale/viewSource.properties";
|
|
|
|
let bundle = Services.strings.createBundle(BUNDLE_URL);
|
|
var title = bundle.GetStringFromName("viewMathMLSourceTitle");
|
|
var wrapClass = this.wrapLongLines ? ' class="wrap"' : '';
|
|
var source =
|
|
'<!DOCTYPE html>'
|
|
+ '<html>'
|
|
+ '<head><title>' + title + '</title>'
|
|
+ '<link rel="stylesheet" type="text/css" href="' + VIEW_SOURCE_CSS + '">'
|
|
+ '<style type="text/css">'
|
|
+ '#target { border: dashed 1px; background-color: lightyellow; }'
|
|
+ '</style>'
|
|
+ '</head>'
|
|
+ '<body id="viewsource"' + wrapClass
|
|
+ ' onload="document.title=\''+title+'\'; document.getElementById(\'target\').scrollIntoView(true)">'
|
|
+ '<pre>'
|
|
+ this.getOuterMarkup(topNode, 0)
|
|
+ '</pre></body></html>'
|
|
; // end
|
|
|
|
return { uri: "data:text/html;charset=utf-8," + encodeURIComponent(source),
|
|
drawSelection: false, baseURI: node.ownerDocument.baseURI };
|
|
},
|
|
|
|
get wrapLongLines() {
|
|
return Services.prefs.getBoolPref("view_source.wrap_long_lines");
|
|
},
|
|
|
|
getInnerMarkup: function(node, indent) {
|
|
var str = '';
|
|
for (var i = 0; i < node.childNodes.length; i++) {
|
|
str += this.getOuterMarkup(node.childNodes.item(i), indent);
|
|
}
|
|
return str;
|
|
},
|
|
|
|
getOuterMarkup: function(node, indent) {
|
|
var Node = node.ownerDocument.defaultView.Node;
|
|
var newline = "";
|
|
var padding = "";
|
|
var str = "";
|
|
if (node == this._targetNode) {
|
|
this._startTargetLine = this._lineCount;
|
|
str += '</pre><pre id="target">';
|
|
}
|
|
|
|
switch (node.nodeType) {
|
|
case Node.ELEMENT_NODE: // Element
|
|
// to avoid the wide gap problem, '\n' is not emitted on the first
|
|
// line and the lines before & after the <pre id="target">...</pre>
|
|
if (this._lineCount > 0 &&
|
|
this._lineCount != this._startTargetLine &&
|
|
this._lineCount != this._endTargetLine) {
|
|
newline = "\n";
|
|
}
|
|
this._lineCount++;
|
|
for (var k = 0; k < indent; k++) {
|
|
padding += " ";
|
|
}
|
|
str += newline + padding
|
|
+ '<<span class="start-tag">' + node.nodeName + '</span>';
|
|
for (var i = 0; i < node.attributes.length; i++) {
|
|
var attr = node.attributes.item(i);
|
|
if (attr.nodeName.match(/^[-_]moz/)) {
|
|
continue;
|
|
}
|
|
str += ' <span class="attribute-name">'
|
|
+ attr.nodeName
|
|
+ '</span>=<span class="attribute-value">"'
|
|
+ this.unicodeToEntity(attr.nodeValue)
|
|
+ '"</span>';
|
|
}
|
|
if (!node.hasChildNodes()) {
|
|
str += "/>";
|
|
}
|
|
else {
|
|
str += ">";
|
|
var oldLine = this._lineCount;
|
|
str += this.getInnerMarkup(node, indent + 2);
|
|
if (oldLine == this._lineCount) {
|
|
newline = "";
|
|
padding = "";
|
|
}
|
|
else {
|
|
newline = (this._lineCount == this._endTargetLine) ? "" : "\n";
|
|
this._lineCount++;
|
|
}
|
|
str += newline + padding
|
|
+ '</<span class="end-tag">' + node.nodeName + '</span>>';
|
|
}
|
|
break;
|
|
case Node.TEXT_NODE: // Text
|
|
var tmp = node.nodeValue;
|
|
tmp = tmp.replace(/(\n|\r|\t)+/g, " ");
|
|
tmp = tmp.replace(/^ +/, "");
|
|
tmp = tmp.replace(/ +$/, "");
|
|
if (tmp.length != 0) {
|
|
str += '<span class="text">' + this.unicodeToEntity(tmp) + '</span>';
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if (node == this._targetNode) {
|
|
this._endTargetLine = this._lineCount;
|
|
str += '</pre><pre>';
|
|
}
|
|
return str;
|
|
},
|
|
|
|
unicodeToEntity: function(text) {
|
|
const charTable = {
|
|
'&': '&<span class="entity">amp;</span>',
|
|
'<': '&<span class="entity">lt;</span>',
|
|
'>': '&<span class="entity">gt;</span>',
|
|
'"': '&<span class="entity">quot;</span>'
|
|
};
|
|
|
|
function charTableLookup(letter) {
|
|
return charTable[letter];
|
|
}
|
|
|
|
function convertEntity(letter) {
|
|
try {
|
|
var unichar = this._entityConverter
|
|
.ConvertToEntity(letter, entityVersion);
|
|
var entity = unichar.substring(1); // extract '&'
|
|
return '&<span class="entity">' + entity + '</span>';
|
|
} catch (ex) {
|
|
return letter;
|
|
}
|
|
}
|
|
|
|
if (!this._entityConverter) {
|
|
try {
|
|
this._entityConverter = Cc["@mozilla.org/intl/entityconverter;1"]
|
|
.createInstance(Ci.nsIEntityConverter);
|
|
} catch(e) { }
|
|
}
|
|
|
|
const entityVersion = Ci.nsIEntityConverter.entityW3C;
|
|
|
|
var str = text;
|
|
|
|
// replace chars in our charTable
|
|
str = str.replace(/[<>&"]/g, charTableLookup);
|
|
|
|
// replace chars > 0x7f via nsIEntityConverter
|
|
str = str.replace(/[^\0-\u007f]/g, convertEntity);
|
|
|
|
return str;
|
|
}
|
|
};
|
|
|
|
ViewSelectionSource.init();
|