Last active
November 18, 2017 07:29
-
-
Save pintassilgo/4deb9d0cbcba625a85aec1f1ed64f747 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Imports | |
const {classes: Cc, interfaces: Ci, manager: Cm, results: Cr, utils: Cu, Constructor: CC} = Components; | |
Cu.import('resource://gre/modules/AddonManager.jsm'); | |
Cu.import('resource://gre/modules/osfile.jsm'); | |
Cu.import('resource://gre/modules/Services.jsm'); | |
Cu.import('resource://gre/modules/XPCOMUtils.jsm'); | |
Cu.importGlobalProperties(['Blob', 'URL']); | |
const COMMONJS_URI = 'resource://gre/modules/commonjs'; | |
/* xiao const { require } = Cu.import(COMMONJS_URI + '/toolkit/require.js', {}); | |
var CLIPBOARD = require('sdk/clipboard');*/ | |
// xiao start clipboard | |
// xiao start clipboard dependencies | |
// xiao start api-utils | |
// xiao start api-utils dependencies | |
/** | |
* Returns if `value` is an instance of a given `Type`. This is exactly same as | |
* `value instanceof Type` with a difference that `Type` can be from a scope | |
* that has a different top level object. (Like in case where `Type` is a | |
* function from different iframe / jetpack module / sandbox). | |
*/ | |
function instanceOf(value, Type) { | |
var isConstructorNameSame; | |
var isConstructorSourceSame; | |
// If `instanceof` returned `true` we know result right away. | |
var isInstanceOf = value instanceof Type; | |
// If `instanceof` returned `false` we do ducktype check since `Type` may be | |
// from a different sandbox. If a constructor of the `value` or a constructor | |
// of the value's prototype has same name and source we assume that it's an | |
// instance of the Type. | |
if (!isInstanceOf && value) { | |
isConstructorNameSame = value.constructor.name === Type.name; | |
isConstructorSourceSame = String(value.constructor) == String(Type); | |
isInstanceOf = (isConstructorNameSame && isConstructorSourceSame) || | |
instanceOf(Object.getPrototypeOf(value), Type); | |
} | |
return isInstanceOf; | |
} | |
/** | |
* Returns `true` if `value` is an object (please note that `null` is considered | |
* to be an atom and not an object). | |
* @examples | |
* isObject({}) // true | |
* isObject(null) // false | |
*/ | |
function isObject(value) { | |
return typeof value === "object" && value !== null; | |
} | |
/** | |
* Returns `true` if value is `null` or `undefined`. | |
* It's equivalent to `== null`, but resolve the ambiguity of the writer | |
* intention, makes clear that he's clearly checking both `null` and `undefined` | |
* values, and it's not a typo for `=== null`. | |
*/ | |
function isNil(value) { | |
return value === null || value === undefined; | |
} | |
/** | |
* Returns `true` if `value` is a `RegExp`. | |
* @examples | |
* isRegExp(/moe/); // true | |
*/ | |
function isRegExp(value) { | |
return isObject(value) && instanceOf(value, RegExp); | |
} | |
// xiao end api-utils dependencies | |
function RequirementError(key, requirement) { | |
Error.call(this); | |
this.name = "RequirementError"; | |
let msg = requirement.msg; | |
if (!msg) { | |
msg = 'The option "' + key + '" '; | |
msg += requirement.is ? | |
"must be one of the following types: " + requirement.is.join(", ") : | |
"is invalid."; | |
} | |
this.message = msg; | |
} | |
RequirementError.prototype = Object.create(Error.prototype); | |
const { isArray } = Array; | |
// The possible return values of getTypeOf. | |
const VALID_TYPES = [ | |
"array", | |
"boolean", | |
"function", | |
"null", | |
"number", | |
"object", | |
"string", | |
"undefined", | |
"regexp" | |
]; | |
// Similar to typeof, except arrays, null and regexps are identified by "array" and | |
// "null" and "regexp", not "object". | |
var getTypeOf = function getTypeOf(val) { | |
let typ = typeof(val); | |
if (typ === "object") { | |
if (!val) | |
return "null"; | |
if (isArray(val)) | |
return "array"; | |
if (isRegExp(val)) | |
return "regexp"; | |
} | |
return typ; | |
} | |
var apiUtils = {}; | |
/** | |
* Returns a validated options dictionary given some requirements. If any of | |
* the requirements are not met, an exception is thrown. | |
* | |
* @param options | |
* An object, the options dictionary to validate. It's not modified. | |
* If it's null or otherwise falsey, an empty object is assumed. | |
* @param requirements | |
* An object whose keys are the expected keys in options. Any key in | |
* options that is not present in requirements is ignored. Each value | |
* in requirements is itself an object describing the requirements of | |
* its key. There are four optional keys in this object: | |
* map: A function that's passed the value of the key in options. | |
* map's return value is taken as the key's value in the final | |
* validated options, is, and ok. If map throws an exception | |
* it's caught and discarded, and the key's value is its value in | |
* options. | |
* is: An array containing any number of the typeof type names. If | |
* the key's value is none of these types, it fails validation. | |
* Arrays, null and regexps are identified by the special type names | |
* "array", "null", "regexp"; "object" will not match either. No type | |
* coercion is done. | |
* ok: A function that's passed the key's value. If it returns | |
* false, the value fails validation. | |
* msg: If the key's value fails validation, an exception is thrown. | |
* This string will be used as its message. If undefined, a | |
* generic message is used, unless is is defined, in which case | |
* the message will state that the value needs to be one of the | |
* given types. | |
* @return An object whose keys are those keys in requirements that are also in | |
* options and whose values are the corresponding return values of map | |
* or the corresponding values in options. Note that any keys not | |
* shared by both requirements and options are not in the returned | |
* object. | |
*/ | |
apiUtils.validateOptions = function validateOptions(options, requirements) { | |
options = options || {}; | |
let validatedOptions = {}; | |
for (let key in requirements) { | |
let isOptional = false; | |
let mapThrew = false; | |
let req = requirements[key]; | |
let [optsVal, keyInOpts] = (key in options) ? | |
[options[key], true] : | |
[undefined, false]; | |
if (req.map) { | |
try { | |
optsVal = req.map(optsVal); | |
} | |
catch (err) { | |
if (err instanceof RequirementError) | |
throw err; | |
mapThrew = true; | |
} | |
} | |
if (req.is) { | |
let types = req.is; | |
if (!isArray(types) && isArray(types.is)) | |
types = types.is; | |
if (isArray(types)) { | |
isOptional = ['undefined', 'null'].every(v => ~types.indexOf(v)); | |
// Sanity check the caller's type names. | |
types.forEach(function (typ) { | |
if (VALID_TYPES.indexOf(typ) < 0) { | |
let msg = 'Internal error: invalid requirement type "' + typ + '".'; | |
throw new Error(msg); | |
} | |
}); | |
if (types.indexOf(getTypeOf(optsVal)) < 0) | |
throw new RequirementError(key, req); | |
} | |
} | |
if (req.ok && ((!isOptional || !isNil(optsVal)) && !req.ok(optsVal))) | |
throw new RequirementError(key, req); | |
if (keyInOpts || (req.map && !mapThrew && optsVal !== undefined)) | |
validatedOptions[key] = optsVal; | |
} | |
return validatedOptions; | |
}; | |
// xiao end api-utils | |
// xiao start url | |
// xiao start url dependencies | |
// xiao start heritage | |
var getPrototypeOf = Object.getPrototypeOf; | |
function* getNames(x) { | |
yield* Object.getOwnPropertyNames(x); | |
yield* Object.getOwnPropertySymbols(x); | |
} | |
var getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor; | |
var freeze = Object.freeze; | |
// This shortcut makes sure that we do perform desired operations, even if | |
// associated methods have being overridden on the used object. | |
var hasOwnProperty = Function.call.bind(Object.prototype.hasOwnProperty); | |
// Utility function to get own properties descriptor map. | |
function getOwnPropertyDescriptors(...objects) { | |
let descriptors = {}; | |
for (let object of objects) | |
for (let name of getNames(object)) | |
descriptors[name] = getOwnPropertyDescriptor(object, name); | |
return descriptors; | |
} | |
function isDataProperty(property) { | |
var type = typeof(property.value); | |
return "value" in property && | |
type !== "function" && | |
(type !== "object" || property.value === null); | |
} | |
function getDataProperties(object) { | |
var properties = getOwnPropertyDescriptors(object); | |
let result = {}; | |
for (let name of getNames(properties)) { | |
var property = properties[name]; | |
if (isDataProperty(property)) { | |
result[name] = { | |
value: property.value, | |
writable: true, | |
configurable: true, | |
enumerable: false | |
}; | |
} | |
} | |
return result; | |
} | |
/** | |
* Takes `source` object as an argument and returns identical object | |
* with the difference that all own properties will be non-enumerable | |
*/ | |
function obscure(source, prototype = getPrototypeOf(source)) { | |
let descriptors = {}; | |
for (let name of getNames(source)) { | |
let property = getOwnPropertyDescriptor(source, name); | |
property.enumerable = false; | |
descriptors[name] = property; | |
} | |
return Object.create(prototype, descriptors); | |
} | |
/** | |
* Takes arbitrary number of source objects and returns fresh one, that | |
* inherits from the same prototype as a first argument and implements all | |
* own properties of all argument objects. If two or more argument objects | |
* have own properties with the same name, the property is overridden, with | |
* precedence from right to left, implying, that properties of the object on | |
* the left are overridden by a same named property of the object on the right. | |
*/ | |
var mix = function(...sources) { | |
return Object.create(getPrototypeOf(sources[0]), | |
getOwnPropertyDescriptors(...sources)); | |
}; | |
/** | |
* Returns a frozen object with that inherits from the given `prototype` and | |
* implements all own properties of the given `properties` object. | |
*/ | |
function extend(prototype, properties) { | |
return Object.create(prototype, | |
getOwnPropertyDescriptors(properties)); | |
} | |
function prototypeOf(input) { | |
return typeof(input) === 'function' ? input.prototype : input; | |
} | |
/** | |
* Returns a constructor function with a proper `prototype` setup. Returned | |
* constructor's `prototype` inherits from a given `options.extends` or | |
* `Class.prototype` if omitted and implements all the properties of the | |
* given `option`. If `options.implemens` array is passed, it's elements | |
* will be mixed into prototype as well. Also, `options.extends` can be | |
* a function or a prototype. If function than it's prototype is used as | |
* an ancestor of the prototype, if it's an object that it's used directly. | |
* Also `options.implements` may contain functions or objects, in case of | |
* functions their prototypes are used for mixing. | |
*/ | |
function Class(options) { | |
// Create descriptor with normalized `options.extends` and | |
// `options.implements`. | |
var descriptor = { | |
// Normalize extends property of `options.extends` to a prototype object | |
// in case it's constructor. If property is missing that fallback to | |
// `Type.prototype`. | |
extends: hasOwnProperty(options, 'extends') ? | |
prototypeOf(options.extends) : Class.prototype, | |
// Normalize `options.implements` to make sure that it's array of | |
// prototype objects instead of constructor functions. | |
implements: freeze(hasOwnProperty(options, 'implements') ? | |
options.implements.map(prototypeOf) : []), | |
}; | |
// Create array of property descriptors who's properties will be defined | |
// on the resulting prototype. | |
var descriptors = [].concat(descriptor.implements, options, descriptor, | |
{ constructor }); | |
// Note: we use reflection `apply` in the constructor instead of method | |
// call since later may be overridden. | |
function constructor() { | |
var instance = Object.create(prototype, attributes); | |
if (initialize) | |
Reflect.apply(initialize, instance, arguments); | |
return instance; | |
} | |
// Create `prototype` that inherits from given ancestor passed as | |
// `options.extends`, falling back to `Type.prototype`, implementing all | |
// properties of given `options.implements` and `options` itself. | |
var prototype = Object.create(descriptor.extends, | |
getOwnPropertyDescriptors(...descriptors)); | |
var initialize = prototype.initialize; | |
// Combine ancestor attributes with prototype's attributes so that | |
// ancestors attributes also become initializeable. | |
var attributes = mix(descriptor.extends.constructor.attributes || {}, | |
getDataProperties(prototype)); | |
constructor.attributes = attributes; | |
Object.defineProperty(constructor, 'prototype', { | |
configurable: false, | |
writable: false, | |
value: prototype | |
}); | |
return constructor; | |
} | |
Class.prototype = obscure({ | |
constructor: function constructor() { | |
this.initialize.apply(this, arguments); | |
return this; | |
}, | |
initialize: function initialize() { | |
// Do your initialization logic here | |
}, | |
// Copy useful properties from `Object.prototype`. | |
toString: Object.prototype.toString, | |
toLocaleString: Object.prototype.toLocaleString, | |
toSource: Object.prototype.toSource, | |
valueOf: Object.prototype.valueOf, | |
isPrototypeOf: Object.prototype.isPrototypeOf | |
}, null); | |
var Class = freeze(Class); | |
// xiao end heritage | |
// xiao start base64 | |
// Passing an empty object as second argument to avoid scope's pollution | |
// (devtools loader injects these symbols as global and prevent using | |
// const here) | |
Cu.importGlobalProperties([ | |
"atob", | |
"btoa" | |
]); | |
// var { atob, btoa } = Cu.import("resource://gre/modules/Services.jsm", {}); | |
function isUTF8(charset) { | |
let type = typeof charset; | |
if (type === "undefined") | |
return false; | |
if (type === "string" && charset.toLowerCase() === "utf-8") | |
return true; | |
throw new Error("The charset argument can be only 'utf-8'"); | |
} | |
function toOctetChar(c) { | |
return String.fromCharCode(c.charCodeAt(0) & 0xFF); | |
} | |
var base64 = {}; | |
base64.decode = function (data, charset) { | |
if (isUTF8(charset)) | |
return decodeURIComponent(escape(atob(data))) | |
return atob(data); | |
} | |
base64.encode = function (data, charset) { | |
if (isUTF8(charset)) | |
return btoa(unescape(encodeURIComponent(data))) | |
data = data.replace(/[^\x00-\xFF]/g, toOctetChar); | |
return btoa(data); | |
} | |
// xiao end base64 | |
// xiao end url dependencies | |
/** | |
* Parse and serialize a Data URL. | |
* | |
* See: http://tools.ietf.org/html/rfc2397 | |
* | |
* Note: Could be extended in the future to decode / encode automatically binary | |
* data. | |
*/ | |
const DataURL = Class({ | |
get base64 () { | |
return "base64" in this.parameters; | |
}, | |
set base64 (value) { | |
if (value) | |
this.parameters["base64"] = ""; | |
else | |
delete this.parameters["base64"]; | |
}, | |
/** | |
* Initialize the Data URL object. If a uri is given, it will be parsed. | |
* | |
* @param {String} [uri] The uri to parse | |
* | |
* @throws {URIError} if the Data URL is malformed | |
*/ | |
initialize: function(uri) { | |
// Due to bug 751834 it is not possible document and define these | |
// properties in the prototype. | |
/** | |
* An hashmap that contains the parameters of the Data URL. By default is | |
* empty, that accordingly to RFC is equivalent to {"charset" : "US-ASCII"} | |
*/ | |
this.parameters = {}; | |
/** | |
* The MIME type of the data. By default is empty, that accordingly to RFC | |
* is equivalent to "text/plain" | |
*/ | |
this.mimeType = ""; | |
/** | |
* The string that represent the data in the Data URL | |
*/ | |
this.data = ""; | |
if (typeof uri === "undefined") | |
return; | |
uri = String(uri); | |
let matches = uri.match(/^data:([^,]*),(.*)$/i); | |
if (!matches) | |
throw new URIError("Malformed Data URL: " + uri); | |
let mediaType = matches[1].trim(); | |
this.data = decodeURIComponent(matches[2].trim()); | |
if (!mediaType) | |
return; | |
let parametersList = mediaType.split(";"); | |
this.mimeType = parametersList.shift().trim(); | |
for (let parameter, i = 0; parameter = parametersList[i++];) { | |
let pairs = parameter.split("="); | |
let name = pairs[0].trim(); | |
let value = pairs.length > 1 ? decodeURIComponent(pairs[1].trim()) : ""; | |
this.parameters[name] = value; | |
} | |
if (this.base64) | |
this.data = base64.decode(this.data); | |
}, | |
/** | |
* Returns the object as a valid Data URL string | |
* | |
* @returns {String} The Data URL | |
*/ | |
toString : function() { | |
let parametersList = []; | |
for (let name in this.parameters) { | |
let encodedParameter = encodeURIComponent(name); | |
let value = this.parameters[name]; | |
if (value) | |
encodedParameter += "=" + encodeURIComponent(value); | |
parametersList.push(encodedParameter); | |
} | |
// If there is at least a parameter, add an empty string in order | |
// to start with a `;` on join call. | |
if (parametersList.length > 0) | |
parametersList.unshift(""); | |
let data = this.base64 ? base64.encode(this.data) : this.data; | |
return "data:" + | |
this.mimeType + | |
parametersList.join(";") + "," + | |
encodeURIComponent(data); | |
} | |
}); | |
// xiao end url | |
// xiao end clipboard dependencies | |
/* | |
While these data flavors resemble Internet media types, they do | |
no directly map to them. | |
*/ | |
const kAllowableFlavors = [ | |
"text/unicode", | |
"text/html", | |
"image/png" | |
/* CURRENTLY UNSUPPORTED FLAVORS | |
"text/plain", | |
"image/jpg", | |
"image/jpeg", | |
"image/gif", | |
"text/x-moz-text-internal", | |
"AOLMAIL", | |
"application/x-moz-file", | |
"text/x-moz-url", | |
"text/x-moz-url-data", | |
"text/x-moz-url-desc", | |
"text/x-moz-url-priv", | |
"application/x-moz-nativeimage", | |
"application/x-moz-nativehtml", | |
"application/x-moz-file-promise-url", | |
"application/x-moz-file-promise-dest-filename", | |
"application/x-moz-file-promise", | |
"application/x-moz-file-promise-dir" | |
*/ | |
]; | |
/* | |
Aliases for common flavors. Not all flavors will | |
get an alias. New aliases must be approved by a | |
Jetpack API druid. | |
*/ | |
const kFlavorMap = [ | |
{ short: "text", long: "text/unicode" }, | |
{ short: "html", long: "text/html" }, | |
{ short: "image", long: "image/png" } | |
]; | |
var clipboardService = Cc["@mozilla.org/widget/clipboard;1"]. | |
getService(Ci.nsIClipboard); | |
var clipboardHelper = Cc["@mozilla.org/widget/clipboardhelper;1"]. | |
getService(Ci.nsIClipboardHelper); | |
var imageTools = Cc["@mozilla.org/image/tools;1"]. | |
getService(Ci.imgITools); | |
var CLIPBOARD = {}; | |
CLIPBOARD.set = function(aData, aDataType) { | |
let options = { | |
data: aData, | |
datatype: aDataType || "text" | |
}; | |
// If `aDataType` is not given or if it's "image", the data is parsed as | |
// data URL to detect a better datatype | |
if (aData && (!aDataType || aDataType === "image")) { | |
try { | |
let dataURL = new DataURL(aData); | |
options.datatype = dataURL.mimeType; | |
options.data = dataURL.data; | |
} | |
catch (e) { | |
// Ignore invalid URIs | |
if (e.name !== "URIError") { | |
throw e; | |
} | |
} | |
} | |
options = apiUtils.validateOptions(options, { | |
data: { | |
is: ["string"] | |
}, | |
datatype: { | |
is: ["string"] | |
} | |
}); | |
let flavor = fromJetpackFlavor(options.datatype); | |
if (!flavor) | |
throw new Error("Invalid flavor for " + options.datatype); | |
// Additional checks for using the simple case | |
if (flavor == "text/unicode") { | |
clipboardHelper.copyString(options.data); | |
return true; | |
} | |
// Below are the more complex cases where we actually have to work with a | |
// nsITransferable object | |
var xferable = Cc["@mozilla.org/widget/transferable;1"]. | |
createInstance(Ci.nsITransferable); | |
if (!xferable) | |
throw new Error("Couldn't set the clipboard due to an internal error " + | |
"(couldn't create a Transferable object)."); | |
// Bug 769440: Starting with FF16, transferable have to be inited | |
if ("init" in xferable) | |
xferable.init(null); | |
switch (flavor) { | |
case "text/html": | |
// add text/html flavor | |
let str = Cc["@mozilla.org/supports-string;1"]. | |
createInstance(Ci.nsISupportsString); | |
str.data = options.data; | |
xferable.addDataFlavor(flavor); | |
xferable.setTransferData(flavor, str, str.data.length * 2); | |
// add a text/unicode flavor (html converted to plain text) | |
str = Cc["@mozilla.org/supports-string;1"]. | |
createInstance(Ci.nsISupportsString); | |
let converter = Cc["@mozilla.org/feed-textconstruct;1"]. | |
createInstance(Ci.nsIFeedTextConstruct); | |
converter.type = "html"; | |
converter.text = options.data; | |
str.data = converter.plainText(); | |
xferable.addDataFlavor("text/unicode"); | |
xferable.setTransferData("text/unicode", str, str.data.length * 2); | |
break; | |
// Set images to the clipboard is not straightforward, to have an idea how | |
// it works on platform side, see: | |
// http://mxr.mozilla.org/mozilla-central/source/content/base/src/nsCopySupport.cpp?rev=7857c5bff017#530 | |
case "image/png": | |
let image = options.data; | |
let container;// xiao = {}; | |
try { | |
let input = Cc["@mozilla.org/io/string-input-stream;1"]. | |
createInstance(Ci.nsIStringInputStream); | |
input.setData(image, image.length); | |
// xiao imageTools.decodeImageData(input, flavor, container); | |
container = imageTools.decodeImage(input, flavor); | |
} | |
catch (e) { | |
throw new Error("Unable to decode data given in a valid image."); | |
} | |
// Store directly the input stream makes the cliboard's data available | |
// for Firefox but not to the others application or to the OS. Therefore, | |
// a `nsISupportsInterfacePointer` object that reference an `imgIContainer` | |
// with the image is needed. | |
var imgPtr = Cc["@mozilla.org/supports-interface-pointer;1"]. | |
createInstance(Ci.nsISupportsInterfacePointer); | |
imgPtr.data = container;// xiao .value; | |
xferable.addDataFlavor(flavor); | |
xferable.setTransferData(flavor, imgPtr, -1); | |
break; | |
default: | |
throw new Error("Unable to handle the flavor " + flavor + "."); | |
} | |
// TODO: Not sure if this will ever actually throw. -zpao | |
try { | |
clipboardService.setData( | |
xferable, | |
null, | |
clipboardService.kGlobalClipboard | |
); | |
} catch (e) { | |
throw new Error("Couldn't set clipboard data due to an internal error: " + e); | |
} | |
return true; | |
}; | |
CLIPBOARD.get = function(aDataType) { | |
let options = { | |
datatype: aDataType | |
}; | |
// Figure out the best data type for the clipboard's data, if omitted | |
if (!aDataType) { | |
if (~currentFlavors().indexOf("image")) | |
options.datatype = "image"; | |
else | |
options.datatype = "text"; | |
} | |
options = apiUtils.validateOptions(options, { | |
datatype: { | |
is: ["string"] | |
} | |
}); | |
var xferable = Cc["@mozilla.org/widget/transferable;1"]. | |
createInstance(Ci.nsITransferable); | |
if (!xferable) | |
throw new Error("Couldn't set the clipboard due to an internal error " + | |
"(couldn't create a Transferable object)."); | |
// Bug 769440: Starting with FF16, transferable have to be inited | |
if ("init" in xferable) | |
xferable.init(null); | |
var flavor = fromJetpackFlavor(options.datatype); | |
// Ensure that the user hasn't requested a flavor that we don't support. | |
if (!flavor) | |
throw new Error("Getting the clipboard with the flavor '" + flavor + | |
"' is not supported."); | |
// TODO: Check for matching flavor first? Probably not worth it. | |
xferable.addDataFlavor(flavor); | |
// Get the data into our transferable. | |
clipboardService.getData( | |
xferable, | |
clipboardService.kGlobalClipboard | |
); | |
var data = {}; | |
var dataLen = {}; | |
try { | |
xferable.getTransferData(flavor, data, dataLen); | |
} catch (e) { | |
// Clipboard doesn't contain data in flavor, return null. | |
return null; | |
} | |
// There's no data available, return. | |
if (data.value === null) | |
return null; | |
// TODO: Add flavors here as we support more in kAllowableFlavors. | |
switch (flavor) { | |
case "text/unicode": | |
case "text/html": | |
data = data.value.QueryInterface(Ci.nsISupportsString).data; | |
break; | |
case "image/png": | |
let dataURL = new DataURL(); | |
dataURL.mimeType = flavor; | |
dataURL.base64 = true; | |
let image = data.value; | |
// Due to the differences in how images could be stored in the clipboard | |
// the checks below are needed. The clipboard could already provide the | |
// image as byte streams, but also as pointer, or as image container. | |
// If it's not possible obtain a byte stream, the function returns `null`. | |
if (image instanceof Ci.nsISupportsInterfacePointer) | |
image = image.data; | |
if (image instanceof Ci.imgIContainer) | |
image = imageTools.encodeImage(image, flavor); | |
if (image instanceof Ci.nsIInputStream) { | |
let binaryStream = Cc["@mozilla.org/binaryinputstream;1"]. | |
createInstance(Ci.nsIBinaryInputStream); | |
binaryStream.setInputStream(image); | |
dataURL.data = binaryStream.readBytes(binaryStream.available()); | |
data = dataURL.toString(); | |
} | |
else | |
data = null; | |
break; | |
default: | |
data = null; | |
} | |
return data; | |
}; | |
function currentFlavors() { | |
// Loop over kAllowableFlavors, calling hasDataMatchingFlavors for each. | |
// This doesn't seem like the most efficient way, but we can't get | |
// confirmation for specific flavors any other way. This is supposed to be | |
// an inexpensive call, so performance shouldn't be impacted (much). | |
var currentFlavors = []; | |
for (var flavor of kAllowableFlavors) { | |
var matches = clipboardService.hasDataMatchingFlavors( | |
[flavor], | |
1, | |
clipboardService.kGlobalClipboard | |
); | |
if (matches) | |
currentFlavors.push(toJetpackFlavor(flavor)); | |
} | |
return currentFlavors; | |
}; | |
Object.defineProperty(CLIPBOARD, "currentFlavors", { get : currentFlavors }); | |
// SUPPORT FUNCTIONS //////////////////////////////////////////////////////// | |
function toJetpackFlavor(aFlavor) { | |
for (let flavorMap of kFlavorMap) | |
if (flavorMap.long == aFlavor) | |
return flavorMap.short; | |
// Return null in the case where we don't match | |
return null; | |
} | |
function fromJetpackFlavor(aJetpackFlavor) { | |
// TODO: Handle proper flavors better | |
for (let flavorMap of kFlavorMap) | |
if (flavorMap.short == aJetpackFlavor || flavorMap.long == aJetpackFlavor) | |
return flavorMap.long; | |
// Return null in the case where we don't match. | |
return null; | |
} | |
// xiao end clipboard | |
/* xiao var LOCALE = require('sdk/l10n/locale');*/ | |
// xiao start locale | |
// xiao Services já está definido pelo NativeShot | |
var LOCALE = {}; | |
LOCALE.getPreferedLocales = function getPreferedLocales(caseSensitve) { | |
const locales = Services.locale.getRequestedLocales(); | |
// This API expects to always append en-US fallback, so for compatibility | |
// reasons, we're going to inject it here. | |
// See bug 1373061 for details. | |
if (!locales.includes('en-US')) { | |
locales.push('en-US'); | |
} | |
return locales; | |
} | |
/** | |
* Selects the closest matching locale from a list of locales. | |
* | |
* @param aLocales | |
* An array of available locales | |
* @param aMatchLocales | |
* An array of prefered locales, ordered by priority. Most wanted first. | |
* Locales have to be in lowercase. | |
* If null, uses getPreferedLocales() results | |
* @return the best match for the currently selected locale | |
* | |
* Stolen from http://dxr.mozilla.org/mozilla-central/source/toolkit/mozapps/extensions/internal/XPIProvider.jsm | |
*/ | |
LOCALE.findClosestLocale = function findClosestLocale(aLocales, aMatchLocales) { | |
aMatchLocales = aMatchLocales || getPreferedLocales(); | |
// Holds the best matching localized resource | |
let bestmatch = null; | |
// The number of locale parts it matched with | |
let bestmatchcount = 0; | |
// The number of locale parts in the match | |
let bestpartcount = 0; | |
for (let locale of aMatchLocales) { | |
let lparts = locale.split("-"); | |
for (let localized of aLocales) { | |
let found = localized.toLowerCase(); | |
// Exact match is returned immediately | |
if (locale == found) | |
return localized; | |
let fparts = found.split("-"); | |
/* If we have found a possible match and this one isn't any longer | |
then we dont need to check further. */ | |
if (bestmatch && fparts.length < bestmatchcount) | |
continue; | |
// Count the number of parts that match | |
let maxmatchcount = Math.min(fparts.length, lparts.length); | |
let matchcount = 0; | |
while (matchcount < maxmatchcount && | |
fparts[matchcount] == lparts[matchcount]) | |
matchcount++; | |
/* If we matched more than the last best match or matched the same and | |
this locale is less specific than the last best match. */ | |
if (matchcount > bestmatchcount || | |
(matchcount == bestmatchcount && fparts.length < bestpartcount)) { | |
bestmatch = localized; | |
bestmatchcount = matchcount; | |
bestpartcount = fparts.length; | |
} | |
} | |
// If we found a valid match for this locale return it | |
if (bestmatch) | |
return bestmatch; | |
} | |
return null; | |
} | |
// xiao end locale | |
var BEAUTIFY = {}; | |
(function() { | |
var { require } = Cu.import('resource://devtools/shared/Loader.jsm', {}); | |
var { jsBeautify } = require('devtools/shared/jsbeautify/src/beautify-js'); | |
BEAUTIFY.js = jsBeautify; | |
}()); | |
// Lazy Imports | |
var myServices = {}; | |
XPCOMUtils.defineLazyGetter(myServices, 'as', () => Cc['@mozilla.org/alerts-service;1'].getService(Ci.nsIAlertsService) ); | |
var nsIFile = CC('@mozilla.org/file/local;1', Ci.nsIFile/* xiao nsILocalFile*/, 'initWithPath'); | |
// Globals | |
var core = { | |
addon: { | |
name: 'NativeShot', | |
id: 'NativeShot@jetpack', | |
version: null, // populated by `startup` | |
path: { | |
name: 'nativeshot', | |
// | |
content: 'chrome://nativeshot/content/', | |
locale: 'chrome://nativeshot/locale/', | |
// | |
resources: 'chrome://nativeshot/content/resources/', | |
images: 'chrome://nativeshot/content/resources/images/', | |
scripts: 'chrome://nativeshot/content/resources/scripts/', | |
styles: 'chrome://nativeshot/content/resources/styles/', | |
fonts: 'chrome://nativeshot/content/resources/styles/fonts/', | |
pages: 'chrome://nativeshot/content/resources/pages/' | |
// below are added by worker | |
// storage: OS.Path.join(OS.Constants.Path.profileDir, 'jetpack', core.addon.id, 'simple-storage') | |
}, | |
cache_key: '1.13' | |
}, | |
os: { | |
// // name: added by worker | |
// // mname: added by worker | |
toolkit: Services.appinfo.widgetToolkit.toLowerCase(), | |
xpcomabi: Services.appinfo.XPCOMABI | |
}, | |
firefox: { | |
pid: Services.appinfo.processID, | |
version: parseFloat(Services.appinfo.version), | |
channel: Services.prefs.getCharPref('app.update.channel'), | |
locale: LOCALE.getPreferedLocales()[0] | |
}, | |
nativeshot: { | |
services: { | |
imguranon: { | |
code: 0, | |
type: 'upload', | |
datatype: 'png_arrbuf' | |
}, | |
twitter: { | |
code: 1, | |
type: 'share', | |
datatype: 'png_arrbuf', | |
oauth: {dotid:'user_id',dotname:'screen_name'} // `dotid` and `dotname` are dot paths in the `oauth` filestore entry. `dotid` is meant to point to something that uniquely identifies that account across all accounts on that oauth service's web server | |
}, | |
copy: { | |
code: 2, | |
type: 'system', | |
datatype: 'png_arrbuf', | |
noimg: true // no associated image file to show on history page | |
}, | |
print: { | |
code: 3, | |
type: 'system', | |
datatype: 'png_arrbuf', | |
noimg: true | |
}, | |
savequick: { | |
code: 4, | |
type: 'system', | |
datatype: 'png_arrbuf' | |
}, | |
savebrowse: { | |
code: 5, | |
type: 'system', | |
datatype: 'png_arrbuf' | |
}, | |
tineye: { | |
code: 7, | |
type: 'search', | |
datatype: 'png_arrbuf', | |
noimg: true | |
}, | |
googleimages: { | |
code: 8, | |
type: 'search', | |
datatype: 'png_arrbuf', | |
noimg: true | |
}, | |
dropbox: { | |
code: 9, | |
type: 'upload', | |
datatype: 'png_arrbuf', | |
oauth: {dotid:'uid',dotname:'name.display_name'} | |
}, | |
imgur: { | |
code: 10, | |
type: 'upload', | |
datatype: 'png_arrbuf', | |
oauth: {dotid:'account_id',dotname:'account_username'} | |
}, | |
gdrive: { | |
code: 11, | |
type: 'upload', | |
datatype: 'png_arrbuf', | |
oauth: {dotid:'emailAddress',dotname:'displayName'} | |
}, | |
gocr: { | |
code: 12, | |
type: 'ocr', | |
datatype: 'plain_arrbuf', | |
noimg: true | |
}, | |
ocrad: { | |
code: 13, | |
type: 'ocr', | |
datatype: 'plain_arrbuf', | |
noimg: true | |
}, | |
tesseract: { | |
code: 14, | |
type: 'ocr', | |
datatype: 'plain_arrbuf', | |
noimg: true | |
}, | |
ocrall: { | |
code: 15, | |
type: 'ocr', | |
datatype: 'plain_arrbuf', | |
noimg: true, | |
history_ignore: true | |
}, | |
bing: { | |
code: 16, | |
type: 'search', | |
datatype: 'png_arrbuf', | |
noimg: true | |
}, | |
facebook: { | |
code: 17, | |
type: 'share', | |
datatype: 'png_arrbuf', | |
oauth: {dotid:'id',dotname:'name'} | |
} | |
} | |
} | |
}; | |
var gWkComm; | |
var gFsComm; | |
var callInMainworker, callInContentinframescript, callInFramescript; | |
var gAndroidMenuIds = []; | |
var gCuiCssUri; | |
var gGenCssUri; | |
var OSStuff = {}; | |
var gSession = { | |
// id: null - set when a session is in progress, | |
// shots: collMonInfos, | |
// domwin_wk - most recent browser window when session started | |
// domwin_was_focused - was it focused when session started | |
}; | |
var gAttn = {}; // key is session id | |
var ostypes; | |
var gFonts; | |
var gEditorStateStr; | |
const NS_HTML = 'http://www.w3.org/1999/xhtml'; | |
const NS_XUL = 'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'; | |
function install() {} | |
function uninstall(aData, aReason) { | |
if (aReason == ADDON_UNINSTALL) { | |
// we have to access the locale pacage with jar path, as at this point chrome:// doesnt work | |
var addon_locales = ['ar', 'bg', 'ca', 'de', 'es-ES', 'et', 'fr', 'hu', 'it', 'ja', 'lt', 'nl', 'po', 'pt-BR', 'ro', 'ru', 'tr', 'zh-CN', 'zh-TW']; // available locales from my addon. must match exactly the casing of the directory in my "locale/" directory | |
var lang = LOCALE.findClosestLocale(addon_locales, LOCALE.getPreferedLocales()) || 'en-US'; | |
var jarpath_main_properties = __SCRIPT_URI_SPEC__.replace('/bootstrap.js', '/locale/' + lang + '/main.properties'); // TODO: figure out the `lang` that is used when picking `chrome` package, like if it doesnt find `en` it falls back to closest which is like `en-US`, figure that calculation out | |
var jarpath_enus_main_properties = __SCRIPT_URI_SPEC__.replace('/bootstrap.js', '/locale/en-US/main.properties'); | |
// __SCRIPT_URI_SPEC__ == jar:file:///C:/Users/Mercurius/AppData/Roaming/Mozilla/Firefox/Profiles/cx4w5lvy.Nightly%20Tester/extensions/[email protected]!/bootstrap.js | |
// now get contents of the file | |
// needs to sync, its amo-reviewer ok, as im accessing local file | |
// needs to be sync because as soon as `uninstall` procedure is done i cannot access files via `jar:` path either, and i need the locale file | |
var xhr = Cc['@mozilla.org/xmlextras/xmlhttprequest;1'].createInstance(Ci.nsIXMLHttpRequest); | |
// first try per pref | |
xhr.open('GET', jarpath_main_properties, false); | |
try { | |
xhr.send(); | |
} catch (ex if ex.result == Cr.NS_ERROR_FILE_NOT_FOUND) { | |
// ex: Exception { message: "", result: 2152857618, name: "NS_ERROR_FILE_NOT_FOUND", filename: "resource://gre/modules/addons/XPIPr…", lineNumber: 205, columnNumber: 0, data: null, stack: "uninstall@resource://gre/modules/ad…", location: XPCWrappedNative_NoHelper } xhr: XMLHttpRequest { onreadystatechange: null, readyState: 1, timeout: 0, withCredentials: false, upload: XMLHttpRequestUpload, responseURL: "", status: 0, statusText: "", responseType: "", response: "" } | |
// ok fallback to en-US | |
xhr.open('GET', jarpath_enus_main_properties, false); | |
xhr.send(); | |
} | |
// xhr: XMLHttpRequest { onreadystatechange: null, readyState: 4, timeout: 0, withCredentials: false, upload: XMLHttpRequestUpload, responseURL: "jar:file:///C:/Users/Mercurius/AppD…", status: 200, statusText: "OK", responseType: "", response: "addon_name=NativeShot addon_descrip…" } | |
var packageStr = xhr.response; | |
// bottom is taken from worker `formatStringFromName` | |
var packageJson = {}; | |
var propPatt = /(.*?)=(.*?)$/gm; | |
var propMatch; | |
while (propMatch = propPatt.exec(packageStr)) { | |
packageJson[propMatch[1]] = propMatch[2]; | |
} | |
// end taken from worker | |
var should_delete = Services.prompt.confirmEx(Services.wm.getMostRecentWindow('navigator:browser'), packageJson.uninstall_title, packageJson.uninstall_body.replace(/\\n/g, '\n'), Services.prompt.BUTTON_POS_0 * Services.prompt.BUTTON_TITLE_IS_STRING + Services.prompt.BUTTON_POS_1 * Services.prompt.BUTTON_TITLE_IS_STRING, packageJson.delete, packageJson.keep, '', null, {value: false}); | |
if (should_delete === 0) { | |
OS.File.removeDir(OS.Path.join(OS.Constants.Path.profileDir, 'jetpack', core.addon.id), {ignorePermissions:true, ignoreAbsent:true}); // will reject if `jetpack` folder does not exist | |
} | |
} | |
} | |
function startup(aData, aReason) { | |
core.addon.version = aData.version; | |
Services.scriptloader.loadSubScript(core.addon.path.scripts + 'comm/Comm.js'); | |
({ callInMainworker, callInContentinframescript, callInFramescript } = CommHelper.bootstrap); | |
Services.scriptloader.loadSubScript(core.addon.path.scripts + 'jscSystemHotkey/shtkMainthreadSubscript.js'); | |
gWkComm = new Comm.server.worker(core.addon.path.scripts + 'MainWorker.js?' + core.addon.cache_key, ()=>core, function(aArg, aComm) { | |
({ core } = aArg); | |
gFsComm = new Comm.server.framescript(core.addon.id); | |
Services.mm.loadFrameScript(core.addon.path.scripts + 'MainFramescript.js?' + core.addon.cache_key, true); | |
// desktop:insert_gui | |
if (core.os.name != 'android') { | |
gGenCssUri = Services.io.newURI(core.addon.path.styles + 'general.css', null, null); | |
gCuiCssUri = Services.io.newURI(core.addon.path.styles + getCuiCssFilename(), null, null); | |
// insert cui | |
Cu.import('resource:///modules/CustomizableUI.jsm'); | |
CustomizableUI.createWidget({ | |
id: 'cui_' + core.addon.path.name, | |
defaultArea: CustomizableUI.AREA_NAVBAR, | |
label: formatStringFromNameCore('gui_label', 'main'), | |
tooltiptext: formatStringFromNameCore('gui_tooltip', 'main'), | |
onCommand: guiClick | |
}); | |
} | |
// register must go after the above, as i set gCuiCssUri above | |
windowListener.register(); | |
if (core.os.name != 'android') { | |
Services.scriptloader.loadSubScript(core.addon.path.scripts + 'react-mozNotificationBar/host.js'); | |
AB.init(); | |
} | |
if (aReason === ADDON_UPGRADE) { | |
callInMainworker('importOldHistory'); | |
} | |
}, function() { | |
var deferredmain = new Deferred(); | |
callInMainworker('hotkeysShouldUnregister', undefined, done=>deferredmain.resolve()); | |
return deferredmain.promise; | |
}); | |
callInMainworker('dummyForInstantInstantiate'); | |
} | |
function shutdown(aData, aReason) { | |
callInMainworker('writeFilestore'); // do even on APP_SHUTDOWN | |
if (aReason == APP_SHUTDOWN) { | |
return; | |
} | |
Services.mm.removeDelayedFrameScript(core.addon.path.scripts + 'MainFramescript.js?' + core.addon.cache_key); | |
Comm.server.unregAll('framescript'); | |
Comm.server.unregAll('worker'); | |
// desktop_android:insert_gui | |
if (core.os.name != 'android') { | |
CustomizableUI.destroyWidget('cui_' + core.addon.path.name); | |
} else { | |
for (var androidMenu of gAndroidMenus) { | |
var domwin = getStrongReference(androidMenu.domwin); | |
if (!domwin) { | |
// its dead | |
continue; | |
} | |
domwin.NativeWindow.menu.remove(androidMenu.menuid); | |
} | |
} | |
windowListener.unregister(); | |
AB.uninit(); | |
releaseAllResourceURI(); | |
} | |
// start - addon functions | |
function getCuiCssFilename() { | |
var cuiCssFilename; | |
if (Services.prefs.getCharPref('app.update.channel') == 'aurora') { | |
if (core.os.mname != 'darwin') { | |
// i didnt test dev edition on winxp, not sure what it is there | |
cuiCssFilename = 'cui_dev.css'; | |
} else { | |
cuiCssFilename = 'cui_dev_mac.css'; | |
} | |
} else { | |
if (core.os.mname == 'darwin') { | |
cuiCssFilename = 'cui_mac.css'; | |
} else if (core.os.mname == 'gtk') { | |
cuiCssFilename = 'cui_gtk.css'; | |
} else { | |
// windows | |
if (core.os.version <= 5.2) { | |
// xp | |
cuiCssFilename = 'cui_gtk.css'; | |
} else { | |
cuiCssFilename = 'cui.css'; | |
} | |
} | |
} | |
return cuiCssFilename; | |
} | |
function guiClick(e) { | |
if (!gSession.id) { | |
if (e.shiftKey) { | |
// add delay | |
callInMainworker('countdownStartOrIncrement', 5, function(aArg) { | |
var { sec_left, done } = aArg; | |
if (done) { | |
// countdown done | |
guiSetBadge(null); | |
guiClick({}); | |
} else { | |
// done is false or undefined | |
// if false it means it was incremented and its closing the pathway. but it sends a sec_left with it, which is the newly incremented countdown | |
// OR it means it was cancelled. when cancelled there is no sec_left | |
// if undefined then sec_left is there, and they are providing progress updates | |
if (sec_left !== undefined) { | |
// progress, or increment close | |
guiSetBadge(sec_left); | |
} | |
} | |
}); | |
} else { | |
// clear delay if there was one | |
callInMainworker('countdownCancel', undefined, function(aArg) { | |
var cancelled = aArg; | |
if (cancelled) { | |
guiSetBadge(null); | |
} | |
}); | |
takeShot(); | |
} | |
} | |
} | |
function guiSetBadge(aTxt) { | |
// set aTxt to null if you want to remove badge | |
var widgetInstances = CustomizableUI.getWidget('cui_' + core.addon.path.name).instances; | |
for (var i=0; i<widgetInstances.length; i++) { | |
var inst = widgetInstances[i]; | |
var node = inst.node; | |
if (aTxt === null) { | |
node.classList.remove('badged-button'); | |
node.removeAttribute('badge'); | |
} else { | |
node.setAttribute('badge', aTxt); | |
node.classList.add('badged-button'); // classList.add does not add duplicate classes | |
} | |
} | |
} | |
var keydetected_mt; | |
var shotgot_mt; | |
var shotstart_mt; | |
var shotcol_mt; | |
var shotopen_0; | |
var shotopen_1; | |
var shotopen_done_0; | |
var shotopen_done_1; | |
function takeShot() { | |
gSession.id = Date.now(); | |
// start - async-proc939333 | |
var shots; | |
var allMonDimStr; | |
var shootAllMons = function() { | |
callInMainworker('shootAllMons', undefined, function(aArg) { | |
shotgot_mt = Date.now(); | |
var { collMonInfos } = aArg; | |
for (var i=0; i<collMonInfos.length; i++) { | |
collMonInfos[i].arrbuf = aArg['arrbuf' + i]; | |
collMonInfos[i].port1 = aArg['screenshot' + i + '_port1']; | |
collMonInfos[i].port2 = aArg['screenshot' + i + '_port2']; | |
} | |
// call it shots | |
shots = collMonInfos; | |
gSession.shots = shots; | |
// the most recent browser window when screenshot was taken | |
var domwin = Services.wm.getMostRecentWindow('navigator:browser'); | |
gSession.domwin_wk = Cu.getWeakReference(domwin); | |
gSession.domwin = domwin; | |
gSession.domwin_was_focused = isFocused(domwin); | |
// create allMonDimStr | |
var allMonDim = []; | |
for (var shot of shots) { | |
allMonDim.push({ | |
x: shot.x, | |
y: shot.y, | |
w: shot.w, | |
h: shot.h | |
// win81ScaleX: shot.win81ScaleX, | |
// win81ScaleY: shot.win81ScaleY | |
}); | |
} | |
allMonDimStr = JSON.stringify(allMonDim); | |
ensureGlobalEditorstate(); | |
}); | |
}; | |
var ensureGlobalEditorstate = function() { | |
shotcol_mt = Date.now(); | |
if (!gEditorStateStr) { | |
callInMainworker('fetchFilestoreEntry', {mainkey:'editorstate'}, function(aArg) { | |
gEditorStateStr = JSON.stringify(aArg); | |
openEditorWins(); | |
}); | |
} else { | |
openEditorWins(); | |
} | |
}; | |
var openEditorWins = function() { | |
// open window for each shot | |
var i = -1; | |
for (var shot of shots) { | |
i++; | |
var x = shot.x; | |
var y = shot.y; | |
var w = shot.w; | |
var h = shot.h; | |
// scale it? | |
var scaleX = shot.win81ScaleX; | |
var scaleY = shot.win81ScaleY; | |
if (scaleX) { | |
x = Math.floor(x / scaleX); | |
w = Math.floor(w / scaleX); | |
} | |
if (scaleY) { | |
y = Math.floor(y / scaleY); | |
h = Math.floor(h / scaleY); | |
} | |
// make query string of the number, string, and boolean properties of shot | |
var query_json = Object.assign( | |
{ | |
iMon: i, | |
allMonDimStr: allMonDimStr, | |
sessionid: gSession.id | |
}, | |
shot | |
); | |
delete query_json.arrbuf; | |
delete query_json.port1; | |
delete query_json.port2; | |
delete query_json.screenshot; | |
var query_str = jQLike.serialize(query_json); | |
gCommScope['shotopen_' + i] = Date.now(); | |
var editor_domwin = Services.ww.openWindow(null, 'about:nativeshot?' + query_str, '_blank', 'chrome,titlebar=0,width=' + w + ',height=' + h + ',screenX=' + x + ',screenY=' + y, null); | |
// showLoading(w, h, x, y); | |
// editor_domwin.addEventListener('load', function() { | |
// editor_domwin.document.documentElement.style.backgroundColor = 'green'; | |
// }, false); | |
shot.domwin_wk = Cu.getWeakReference(editor_domwin); | |
shot.domwin = editor_domwin; | |
} | |
}; | |
shootAllMons(); | |
shotstart_mt = Date.now(); | |
// end - async-proc939333 | |
} | |
// start - functions called by editor | |
function editorInitShot(aIMon, e) { | |
// does the platform dependent stuff to make the window be position on the proper monitor and full screened covering all things underneeath | |
// also transfer the screenshot data to the window | |
var iMon = aIMon; // iMon is my rename of colMonIndex. so its the i in the collMoninfos object | |
var shots = gSession.shots; | |
var shot = shots[iMon]; | |
// var aEditorDOMWindow = colMon[iMon].E.DOMWindow; | |
// | |
// if (!aEditorDOMWindow || aEditorDOMWindow.closed) { | |
// throw new Error('wtf how is window not existing, the on load observer notifier of panel.xul just sent notification that it was loaded'); | |
// } | |
// var domwin = getStrongReference(shot.domwin_wk); | |
// if (!domwin) { | |
// Services.prompt(null, 'domwin of shot is dead', 'dead'); | |
// } | |
var domwin = shot.domwin; | |
var aHwndPtrStr = getNativeHandlePtrStr(domwin); | |
shot.hwndPtrStr = aHwndPtrStr; | |
// if (core.os.name != 'darwin') { | |
// aEditorDOMWindow.moveTo(colMon[iMon].x, colMon[iMon].y); | |
// aEditorDOMWindow.resizeTo(colMon[iMon].w, colMon[iMon].h); | |
// } | |
domwin.focus(); | |
// if (core.os.mname == 'gtk') { | |
// domwin.fullScreen = true; | |
// } | |
// set window on top: | |
var aArrHwndPtrStr = [aHwndPtrStr]; | |
var aArrHwndPtrOsParams = {}; | |
aArrHwndPtrOsParams[aHwndPtrStr] = { | |
left: shot.x, | |
top: shot.y, | |
right: shot.x + shot.w, | |
bottom: shot.y + shot.h, | |
width: shot.w, | |
height: shot.h | |
}; | |
// if (core.os.name != 'darwinAAAA') { | |
callInMainworker('setWinAlwaysOnTop', { aArrHwndPtrStr, aOptions:aArrHwndPtrOsParams }, function(aArg) { | |
if (core.os.name == 'darwin') { | |
initOstypes(); | |
// link98476884 | |
OSStuff.NSMainMenuWindowLevel = aArg; | |
var NSWindowString = aHwndPtrStr; | |
// var NSWindowString = getNativeHandlePtrStr(domwin); | |
var NSWindowPtr = ostypes.TYPE.NSWindow(ctypes.UInt64(NSWindowString)); | |
var rez_setLevel = ostypes.API('objc_msgSend')(NSWindowPtr, ostypes.HELPER.sel('setLevel:'), ostypes.TYPE.NSInteger(OSStuff.NSMainMenuWindowLevel + 1)); // have to do + 1 otherwise it is ove rmneubar but not over the corner items. if just + 0 then its over menubar, if - 1 then its under menu bar but still over dock. but the interesting thing is, the browse dialog is under all of these // link847455111 | |
var newSize = ostypes.TYPE.NSSize(shot.w, shot.h); | |
var rez_setContentSize = ostypes.API('objc_msgSend')(NSWindowPtr, ostypes.HELPER.sel('setContentSize:'), newSize); | |
domwin.moveTo(shot.x, shot.y); // must do moveTo after setContentsSize as that sizes from bottom left and moveTo moves from top left. so the sizing will change the top left. | |
} | |
}); | |
if (!gFonts) { | |
var fontsEnumerator = Cc['@mozilla.org/gfx/fontenumerator;1'].getService(Ci.nsIFontEnumerator); | |
gFonts = fontsEnumerator.EnumerateAllFonts({}); | |
} | |
shot.comm.putMessage('init', { | |
screenshotArrBuf: shot.arrbuf, | |
core: core, | |
fonts: gFonts, | |
editorstateStr: gEditorStateStr, | |
__XFER: ['screenshotArrBuf'] | |
}); | |
// set windowtype attribute | |
// colMon[aData.iMon].E.DOMWindow.document.documentElement.setAttribute('windowtype', 'nativeshot:canvas'); | |
// check to see if all monitors inited, if they have been, the fetch all win | |
var allWinInited = true; | |
for (var shoty of shots) { | |
if (!shoty.hwndPtrStr) { | |
allWinInited = false; | |
break; | |
} | |
} | |
if (allWinInited) { | |
if (core.os.mname == 'winnt') { | |
// reRaiseCanvasWins(); // ensures its risen | |
} | |
sendWinArrToEditors(); | |
// hideLoading(); | |
} | |
} | |
function sendWinArrToEditors() { | |
var shots = gSession.shots; | |
callInMainworker( | |
'getAllWin', | |
{ | |
getPid: true, | |
getBounds: true, | |
getTitle: true, | |
filterVisible: true | |
}, | |
function(aVal) { | |
// Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper).copyString(JSON.stringify(aVal)); // :debug: | |
// build hwndPtrStr arr for nativeshot_canvas windows | |
var hwndPtrStrArr = []; | |
for (var shot of shots) { | |
hwndPtrStrArr.push(shot.hwndPtrStr); | |
} | |
// remove nativeshot_canvas windows | |
for (var i=0; i<aVal.length; i++) { | |
if (aVal[i].title == 'nativeshot_canvas' || hwndPtrStrArr.indexOf(aVal[i].hwndPtrStr) > -1) { | |
// need to do the hwndPtrStr check as on windows, sometimes the page isnt loaded yet, so the title of the window isnt there yet | |
// aVal.splice(i, 1); | |
aVal[i].left = -10000; | |
aVal[i].right = -10000; | |
aVal[i].width = 0; | |
aVal[i].NATIVESHOT_CANVAS = true; | |
// i--; | |
} | |
} | |
for (var shot of shots) { | |
shot.comm.putMessage('receiveWinArr', { | |
winArr: aVal | |
}); | |
} | |
} | |
); | |
} | |
function broadcastToOthers(aArg) { | |
var { iMon } = aArg; | |
var shots = gSession.shots; // TODO: sometimes i get this error in console `TypeError: shots is undefined[Learn More]bootstrap.js:596:6` baffling, i need to figure out how to fix this, it doesnt seem to cause issues that i can find | |
var l = shots.length; | |
for (var i=0; i<l; i++) { | |
if (i !== iMon) { | |
shots[i].comm.putMessage(aArg.topic, aArg); | |
} | |
} | |
} | |
function broadcastToSpecific(aArg) { | |
var { toMon } = aArg; | |
var shot = gSession.shots[toMon]; | |
shot.comm.putMessage(aArg.topic, aArg); | |
} | |
function exitEditors(aArg) { | |
var { iMon, editorstateStr } = aArg; | |
var shots = gSession.shots; | |
var l = shots.length; | |
for (var i=0; i<l; i++) { | |
if (i !== iMon) { | |
var domwin = getStrongReference(shots[i].domwin_wk); | |
if (!domwin) { | |
// Services.prompt.alert(null, 'huh', 'weak ref is dead??'); | |
} | |
shots[i].comm.putMessage('removeUnload', undefined, function(adomwin) { | |
adomwin.close(); | |
}.bind(null, shots[i].domwin)); | |
} | |
} | |
// // as nativeshot_canvas windows are now closing. check if should show notification bar - if it has any btns then show it | |
// if (gEditorABData_Bar[gEditor.sessionId].ABRef.aBtns.length) { | |
// gEditorABData_Bar[gEditor.sessionId].shown = true; // otherwise setBtnState will not update react states | |
// AB.setState(gEditorABData_Bar[gEditor.sessionId].ABRef); | |
// ifEditorClosed_andBarHasOnlyOneAction_copyToClip(gEditor.sessionId); | |
// } else { | |
// // no need to show, delete it | |
// delete gEditorABData_Bar[gEditor.sessionId]; | |
// } | |
// | |
// // check if need to show twitter notification bars | |
// for (var p in NBs.crossWin) { | |
// if (p.indexOf(gEditor.sessionId) == 0) { // note: this is why i have to start each crossWin id with gEditor.sessionId | |
// NBs.insertGlobalToWin(p, 'all'); | |
// } | |
// } | |
// if (gEditor.wasFirefoxWinFocused || gEditor.forceFocus) { | |
// gEditor.gBrowserDOMWindow.focus(); | |
// } | |
// if (gEditor.printPrevWins) { | |
// for (var i=0; i<gEditor.printPrevWins.length; i++) { | |
// gEditor.printPrevWins[i].focus(); | |
// } | |
// } | |
// // colMon[0].E.DOMWindow.close(); | |
Comm.server.unregAll('content'); | |
var sessionid = gSession.id; | |
gSession = {}; // gEditor.cleanUp(); | |
attnUpdate(sessionid); // show the attnbar if there is anything to show | |
checkOnlySingleAction(sessionid, true); | |
gEditorStateStr = JSON.stringify(aArg.editorstate); | |
callInMainworker('updateFilestoreEntry', { | |
mainkey: 'editorstate', | |
value: aArg.editorstate | |
}); | |
if (core.os.mname == 'gtk') { | |
callInMainworker('gtkSetFocus', getNativeHandlePtrStr(Services.wm.getMostRecentWindow('navigator:browser'))); | |
} | |
} | |
var gUsedSelections = []; // array of arrays. each child is [subcutout1, subcutout2, ...] | |
function indexOfSelInG(aSel) { | |
// aSel is an array of subcutouts | |
// will return the index it is found in gUsedSelections | |
// -1 if not found | |
var l = gUsedSelections.length; | |
if (!l) { | |
return -1; | |
} else { | |
for (var i=l-1; i>=0; i--) { | |
var tSel = gUsedSelections[i]; // testSelection | |
var l2 = tSel.length; | |
if (l2 === aSel.length) { | |
var tSelMatches = true; | |
for (var j=0; j<l2; j++) { | |
var tSubcutout = tSel[j]; | |
var cSubcutout = aSel[j]; | |
if (tSubcutout.x !== cSubcutout.x || tSubcutout.y !== cSubcutout.y || tSubcutout.w !== cSubcutout.w || tSubcutout.h !== cSubcutout.h) { | |
// tSel does not match aSel | |
tSelMatches = false; | |
break; | |
} | |
} | |
if (tSelMatches) { | |
return i; | |
} | |
} | |
} | |
return -1; // not found | |
} | |
} | |
function addSelectionToHistory(aData) { | |
// aData.cutoutsArr is an array of cutouts | |
var cSel = aData.cutoutsArr; | |
var ix = indexOfSelInG(cSel); | |
if (ix == -1) { | |
gUsedSelections.push(aData.cutoutsArr) | |
} else { | |
// it was found in history, so lets move this to the most recent selection made | |
// most recent selection is the last most element in gUsedSelections array | |
gUsedSelections.push(gUsedSelections.splice(ix, 1)[0]); | |
} | |
} | |
function selectPreviousSelection(aData) { | |
// aData.curSelection is an array of the currently selected cutouts | |
if (!gUsedSelections.length) { | |
return; | |
} | |
var cSel = aData.cutoutsArr; // cutouts of the current selection | |
// figure out the selection to make | |
var selToMake; | |
if (cSel) { | |
// check to see if this sel is in the history, and select the one before this one | |
var ix = indexOfSelInG(cSel); | |
if (ix > 0) { | |
selToMake = gUsedSelections[ix - 1]; | |
} else if (ix == -1) { | |
// select the most recent one | |
selToMake = gUsedSelections[gUsedSelections.length - 1]; | |
} // else if 0, then no previous selection obviously | |
} else { | |
// select the most recent one | |
selToMake = gUsedSelections[gUsedSelections.length - 1]; | |
} | |
// send message to make the selection | |
if (selToMake) { | |
gSession.shots[aData.iMon].comm.putMessage('makeSelection', { | |
cutoutsArr: selToMake | |
}, '*'); | |
} | |
} | |
// end - last selection stuff | |
function insertTextFromClipboard(aArg) { | |
var { iMon } = aArg; | |
if (CLIPBOARD.currentFlavors.indexOf('text') > -1) { | |
gSession.shots[iMon].comm.putMessage('insertTextFromClipboard', { | |
text: CLIPBOARD.get('text') | |
}, '*'); | |
} | |
} | |
// end - functions called by editor | |
function initOstypes() { | |
if (!ostypes) { | |
Cu.import('resource://gre/modules/ctypes.jsm'); | |
Services.scriptloader.loadSubScript(core.addon.path.scripts + 'ostypes/cutils.jsm'); // need to load cutils first as ostypes_mac uses it for HollowStructure | |
Services.scriptloader.loadSubScript(core.addon.path.scripts + 'ostypes/ctypes_math.jsm'); | |
switch (core.os.mname) { | |
case 'winnt': | |
case 'winmo': | |
case 'wince': | |
Services.scriptloader.loadSubScript(core.addon.path.scripts + 'ostypes/ostypes_win.jsm'); | |
break; | |
case 'gtk': | |
Services.scriptloader.loadSubScript(core.addon.path.scripts + 'ostypes/ostypes_x11.jsm'); | |
break; | |
case 'darwin': | |
Services.scriptloader.loadSubScript(core.addon.path.scripts + 'ostypes/ostypes_mac.jsm'); | |
break; | |
default: | |
throw new Error('Operating system, "' + OS.Constants.Sys.Name + '" is not supported'); | |
} | |
} | |
} | |
// start - context menu items | |
var gToolbarContextMenu_domId = 'toolbar-context-menu'; | |
var gCustomizationPanelItemContextMenu_domId = 'customizationPanelItemContextMenu'; | |
var gDashboardMenuitem_domIdSuffix = '_nativeshot-menuitem'; | |
var gDashboardSeperator_domIdSuffix = '_nativeshot-seperator'; | |
var gDashboardMenuitem_jsonTemplate = ['xul:menuitem', { | |
// id: 'toolbar-context-menu_nativeshot-menuitem', | |
// label: formatStringFromNameCore('dashboard-menuitem', 'main'), // cant access myServices.sb till startup, so this is set on startup // link988888887 | |
class: 'menuitem-iconic', | |
image: core.addon.path.images + 'icon16.png', | |
hidden: 'true', | |
oncommand: function(e) { | |
var gbrowser = e.target.ownerDocument.defaultView.gBrowser; | |
var tabs = gbrowser.tabs; | |
var l = gbrowser.tabs.length; | |
for (var i=0; i<l; i++) { | |
// e10s safe way to check content of tab | |
var spec_lower = tabs[i].linkedBrowser.currentURI.spec.toLowerCase(); | |
if (spec_lower == 'about:nativeshot') { // crossfile-link381787872 - i didnt link over there but &nativeshot.app-main.title; is what this is equal to | |
gbrowser.selectedTab = tabs[i]; | |
return; | |
} | |
} | |
gbrowser.loadOneTab('about:nativeshot', {inBackground:false}); | |
} | |
}]; | |
var gDashboardMenuseperator_jsonTemplate = ['xul:menuseparator', { | |
// id: 'toolbar-context-menu_nativeshot-menuseparator', // id is set when inserting into dom | |
hidden: 'true' | |
}]; | |
function contextMenuHiding(e) { | |
// only triggered when it was shown due to right click on cui_nativeshot | |
e.target.removeEventListener('popuphiding', contextMenuHiding, false); | |
var cToolbarContextMenu_dashboardMenuitem = e.target.querySelector('#' + gToolbarContextMenu_domId + gDashboardMenuitem_domIdSuffix); | |
if (cToolbarContextMenu_dashboardMenuitem) { | |
var cToolbarContextMenu_dashboardSeperator = e.target.querySelector('#' + gToolbarContextMenu_domId + gDashboardSeperator_domIdSuffix); | |
cToolbarContextMenu_dashboardMenuitem.setAttribute('hidden', 'true'); | |
cToolbarContextMenu_dashboardSeperator.setAttribute('hidden', 'true'); | |
} | |
var cCustomizationPanelItemContextMenu_dashboardMenuitem = e.target.querySelector('#' + gCustomizationPanelItemContextMenu_domId + gDashboardMenuitem_domIdSuffix); | |
if (cCustomizationPanelItemContextMenu_dashboardMenuitem) { | |
var cCustomizationPanelItemContextMenu_dashboardSeperator = e.target.querySelector('#' + gCustomizationPanelItemContextMenu_domId + gDashboardSeperator_domIdSuffix); | |
cCustomizationPanelItemContextMenu_dashboardMenuitem.setAttribute('hidden', 'true'); | |
cCustomizationPanelItemContextMenu_dashboardMenuitem.setAttribute('hidden', 'true'); | |
} | |
} | |
function contextMenuShowing(e) { | |
var cPopupNode = e.target.ownerDocument.popupNode; | |
if (cPopupNode.getAttribute('id') == 'cui_nativeshot') { | |
var cToolbarContextMenu_dashboardMenuitem = e.target.querySelector('#' + gToolbarContextMenu_domId + gDashboardMenuitem_domIdSuffix); | |
if (cToolbarContextMenu_dashboardMenuitem) { | |
var cToolbarContextMenu_dashboardSeperator = e.target.querySelector('#' + gToolbarContextMenu_domId + gDashboardSeperator_domIdSuffix); | |
cToolbarContextMenu_dashboardMenuitem.removeAttribute('hidden'); | |
cToolbarContextMenu_dashboardSeperator.removeAttribute('hidden'); | |
e.target.addEventListener('popuphiding', contextMenuHiding, false); | |
} | |
var cCustomizationPanelItemContextMenu_dashboardMenuitem = e.target.querySelector('#' + gCustomizationPanelItemContextMenu_domId + gDashboardMenuitem_domIdSuffix); | |
if (cCustomizationPanelItemContextMenu_dashboardMenuitem) { | |
var cCustomizationPanelItemContextMenu_dashboardSeperator = e.target.querySelector('#' + gCustomizationPanelItemContextMenu_domId + gDashboardSeperator_domIdSuffix); | |
cCustomizationPanelItemContextMenu_dashboardMenuitem.removeAttribute('hidden'); | |
cCustomizationPanelItemContextMenu_dashboardSeperator.removeAttribute('hidden'); | |
e.target.addEventListener('popuphiding', contextMenuHiding, false); | |
} | |
} | |
} | |
function contextMenuSetup(aDOMWindow) { | |
// if this aDOMWindow has the context menus set it up | |
if (!gDashboardMenuitem_jsonTemplate[1].label) { | |
gDashboardMenuitem_jsonTemplate[1].label = formatStringFromNameCore('menuitem_opendashboard', 'main'); // link988888887 - needs to go before windowListener is registered | |
} | |
var cToolbarContextMenu = aDOMWindow.document.getElementById(gToolbarContextMenu_domId); | |
if (cToolbarContextMenu) { | |
gDashboardMenuitem_jsonTemplate[1].id = gToolbarContextMenu_domId + gDashboardMenuitem_domIdSuffix; | |
gDashboardMenuseperator_jsonTemplate[1].id = gToolbarContextMenu_domId + gDashboardSeperator_domIdSuffix; | |
var cToolbarContextMenu_dashboardMenuitem = jsonToDOM(gDashboardMenuitem_jsonTemplate, aDOMWindow.document, {}); | |
var cToolbarContextMenu_dashboardSeperator = jsonToDOM(gDashboardMenuseperator_jsonTemplate, aDOMWindow.document, {}); | |
cToolbarContextMenu.insertBefore(cToolbarContextMenu_dashboardSeperator, cToolbarContextMenu.firstChild); | |
cToolbarContextMenu.insertBefore(cToolbarContextMenu_dashboardMenuitem, cToolbarContextMenu.firstChild); | |
cToolbarContextMenu.addEventListener('popupshowing', contextMenuShowing, false); | |
} | |
var cCustomizationPanelItemContextMenu = aDOMWindow.document.getElementById(gCustomizationPanelItemContextMenu_domId); | |
if (cCustomizationPanelItemContextMenu) { | |
gDashboardMenuitem_jsonTemplate[1].id = gCustomizationPanelItemContextMenu_domId + gDashboardMenuitem_domIdSuffix; | |
gDashboardMenuseperator_jsonTemplate[1].id = gCustomizationPanelItemContextMenu_domId + gDashboardSeperator_domIdSuffix; | |
var cCustomizationPanelItemContextMenu_dashboardMenuitem = jsonToDOM(gDashboardMenuitem_jsonTemplate, aDOMWindow.document, {}); | |
var cCustomizationPanelItemContextMenu_dashboardSeperator = jsonToDOM(gDashboardMenuseperator_jsonTemplate, aDOMWindow.document, {}); | |
cCustomizationPanelItemContextMenu.insertBefore(cCustomizationPanelItemContextMenu_dashboardSeperator, cCustomizationPanelItemContextMenu.firstChild); | |
cCustomizationPanelItemContextMenu.insertBefore(cCustomizationPanelItemContextMenu_dashboardMenuitem, cCustomizationPanelItemContextMenu.firstChild); | |
cCustomizationPanelItemContextMenu.addEventListener('popupshowing', contextMenuShowing, false); | |
} | |
} | |
function contextMenuDestroy(aDOMWindow) { | |
// if this aDOMWindow has the context menus it removes it from it | |
var cToolbarContextMenu = aDOMWindow.document.getElementById(gToolbarContextMenu_domId); | |
if (cToolbarContextMenu) { | |
var cToolbarContextMenu_dashboardMenuitem = aDOMWindow.document.getElementById(gToolbarContextMenu_domId + gDashboardMenuitem_domIdSuffix); | |
var cToolbarContextMenu_dashboardSeperator = aDOMWindow.document.getElementById(gToolbarContextMenu_domId + gDashboardSeperator_domIdSuffix); | |
cToolbarContextMenu.removeChild(cToolbarContextMenu_dashboardMenuitem); | |
cToolbarContextMenu.removeChild(cToolbarContextMenu_dashboardSeperator); | |
cToolbarContextMenu.removeEventListener('popupshowing', contextMenuShowing, false); | |
} | |
var cCustomizationPanelItemContextMenu = aDOMWindow.document.getElementById(gCustomizationPanelItemContextMenu_domId); | |
if (cCustomizationPanelItemContextMenu) { | |
var cCustomizationPanelItemContextMenu_dashboardMenuitem = aDOMWindow.document.getElementById(gCustomizationPanelItemContextMenu_domId + gDashboardMenuitem_domIdSuffix); | |
var cCustomizationPanelItemContextMenu_dashboardSeperator = aDOMWindow.document.getElementById(gCustomizationPanelItemContextMenu_domId + gDashboardSeperator_domIdSuffix); | |
cCustomizationPanelItemContextMenu.removeChild(cCustomizationPanelItemContextMenu_dashboardMenuitem); | |
cCustomizationPanelItemContextMenu.removeChild(cCustomizationPanelItemContextMenu_dashboardSeperator); | |
cCustomizationPanelItemContextMenu.removeEventListener('popupshowing', contextMenuShowing, false); | |
} | |
} | |
// end - context menu items | |
var windowListener = { | |
//DO NOT EDIT HERE | |
onOpenWindow: function (aXULWindow) { | |
// Wait for the window to finish loading | |
var aDOMWindow = aXULWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindow); | |
aDOMWindow.addEventListener('load', function () { | |
aDOMWindow.removeEventListener('load', arguments.callee, false); | |
windowListener.loadIntoWindow(aDOMWindow); | |
}, false); | |
}, | |
onCloseWindow: function (aXULWindow) {}, | |
onWindowTitleChange: function (aXULWindow, aNewTitle) { | |
if (aNewTitle == 'nativeshot_canvas') { | |
var aDOMWindow = aXULWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindow); | |
// aDOMWindow.document.documentElement.style.backgroundColor = 'red'; | |
aDOMWindow.addEventListener('nscomm', function(e) { | |
aDOMWindow.removeEventListener('nscomm', arguments.callee, false); | |
var detail = e.detail; | |
var iMon = detail; | |
gCommScope['shotopen_done_' + detail] = Date.now(); | |
// if (shotopen_done_0 > keydetected_mt && shotopen_done_1 > keydetected_mt) { | |
// console['error']('Time from keydetected_mt to start shot:', (shotstart_mt - keydetected_mt)); | |
// console['error']('Time from keydetected_mt to get from worker:', (shotgot_mt - keydetected_mt)); | |
// console['error']('Time from keydetected_mt to collect:', (shotcol_mt - keydetected_mt)); | |
// console['error']('Time from keydetected_mt to start open 0:', (shotopen_0 - keydetected_mt)); | |
// console['error']('Time from keydetected_mt to start open 1:', (shotopen_1 - keydetected_mt)); | |
// console['error']('Time from keydetected_mt to done open 0:', (shotopen_done_0 - keydetected_mt)); | |
// console['error']('Time from keydetected_mt to done open 1:', (shotopen_done_1 - keydetected_mt)); | |
// } | |
var shot = gSession.shots[iMon]; | |
if (Services.vc.compare(core.firefox.version, '46.*') > 0) { | |
shot.comm = new Comm.server.content(aDOMWindow, editorInitShot.bind(null, iMon, e), shot.port1, shot.port2); | |
} else { | |
shot.comm = new Comm.server.content(aDOMWindow, editorInitShot.bind(null, iMon, e)); | |
} | |
}, false, true); | |
} | |
}, | |
register: function () { | |
// Load into any existing windows | |
let DOMWindows = Services.wm.getEnumerator(null); | |
while (DOMWindows.hasMoreElements()) { | |
let aDOMWindow = DOMWindows.getNext(); | |
if (aDOMWindow.document.readyState == 'complete') { //on startup `aDOMWindow.document.readyState` is `uninitialized` | |
windowListener.loadIntoWindow(aDOMWindow); | |
} else { | |
aDOMWindow.addEventListener('load', function () { | |
aDOMWindow.removeEventListener('load', arguments.callee, false); | |
windowListener.loadIntoWindow(aDOMWindow); | |
}, false); | |
} | |
} | |
// Listen to new windows | |
Services.wm.addListener(windowListener); | |
}, | |
unregister: function () { | |
// Unload from any existing windows | |
let DOMWindows = Services.wm.getEnumerator(null); | |
while (DOMWindows.hasMoreElements()) { | |
let aDOMWindow = DOMWindows.getNext(); | |
windowListener.unloadFromWindow(aDOMWindow); | |
} | |
/* | |
for (var u in unloaders) { | |
unloaders[u](); | |
} | |
*/ | |
//Stop listening so future added windows dont get this attached | |
Services.wm.removeListener(windowListener); | |
}, | |
//END - DO NOT EDIT HERE | |
loadIntoWindow: function (aDOMWindow) { | |
if (!aDOMWindow) { return } | |
// desktop_android:insert_gui | |
if (core.os.name != 'android') { | |
// desktop:insert_gui | |
if (aDOMWindow.gBrowser) { | |
var domWinUtils = aDOMWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils); | |
domWinUtils.loadSheet(gCuiCssUri, domWinUtils.AUTHOR_SHEET); | |
domWinUtils.loadSheet(gGenCssUri, domWinUtils.AUTHOR_SHEET); | |
} | |
contextMenuSetup(aDOMWindow); | |
} else { | |
// android:insert_gui | |
if (aDOMWindow.NativeWindow && aDOMWindow.NativeWindow.menu) { | |
var menuid = aDOMWindow.NativeWindow.menu.add(formatStringFromNameCore('gui_label', 'main'), core.addon.path.images + 'icon-color16.png', guiClick) | |
gAndroidMenus.push({ | |
domwin: Cu.getWeakReference(aDOMWindow), | |
menuid | |
}); | |
} | |
} | |
}, | |
unloadFromWindow: function (aDOMWindow) { | |
if (!aDOMWindow) { return } | |
// desktop:insert_gui | |
if (core.os.name != 'android') { | |
if (aDOMWindow.gBrowser) { | |
var domWinUtils = aDOMWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils); | |
domWinUtils.removeSheet(gCuiCssUri, domWinUtils.AUTHOR_SHEET); | |
domWinUtils.removeSheet(gGenCssUri, domWinUtils.AUTHOR_SHEET); | |
} | |
contextMenuDestroy(aDOMWindow); | |
} | |
} | |
}; | |
function copy(aString) { | |
// aString is either dataurl, or string to copy to clipboard | |
if (!aString.startsWith('data:')) { | |
// copy as text | |
CLIPBOARD.set(aString, 'text'); | |
} else { | |
// copy as image | |
CLIPBOARD.set(aString, 'image'); | |
} | |
} | |
function print(aArg) { | |
var { aPrintPreview, aDataUrl } = aArg; | |
// aPrintPreview - true for print preview, else false | |
var aWin; | |
if (aPrintPreview) { | |
aWin = Services.ww.openWindow(null, 'chrome://browser/content/browser.xul', '_blank', null, null); | |
aWin.addEventListener('load', printWinLoad, false); | |
} else { | |
aWin = Services.wm.getMostRecentWindow('navigator:browser'); | |
printWinLoad(Object.assign(aArg, {aWin})); | |
} | |
var aFrame; | |
function printWinLoad(e) { | |
if (aPrintPreview) { | |
aWin.removeEventListener('load', printWinLoad, false); | |
} | |
aWin.focus(); | |
var doc = aWin.document; | |
aFrame = doc.createElementNS(NS_XUL, 'browser'); | |
aFrame.addEventListener('load', printFrameLoad, true); // if i use `false` here it doesnt work | |
aFrame.setAttribute('type', 'content'); | |
aFrame.setAttribute('src', aDataUrl); | |
aFrame.setAttribute('style', 'display:none'); // if dont do display none, then have to give it a height and width enough to show it, otherwise print preview is blank | |
doc.documentElement.appendChild(aFrame); // src page wont load until i append to document | |
} | |
function printFrameLoad(e) { | |
aFrame.removeEventListener('load', printFrameLoad, true); // as i added with `true` | |
if (aPrintPreview) { | |
var aPPListener = aWin.PrintPreviewListener; | |
var aOrigPPgetSourceBrowser = aPPListener.getSourceBrowser; | |
var aOrigPPExit = aPPListener.onExit; | |
aPPListener.onExit = function() { | |
aOrigPPExit.call(aPPListener); | |
// aFrame.parentNode.removeChild(aFrame); | |
// aPPListener.onExit = aOrigPPExit; | |
// aPPListener.getSourceBrowser = aOrigPPgetSourceBrowser; | |
aWin.close(); | |
}; | |
aPPListener.getSourceBrowser = function() { | |
return aFrame; | |
}; | |
aWin.PrintUtils.printPreview(aPPListener); | |
} else { | |
aFrame.contentWindow.addEventListener('afterprint', printFrameAfterPrint, false); | |
aFrame.contentWindow.print(); | |
} | |
} | |
function printFrameAfterPrint(e) { | |
// only for if `!aPrintPreview` | |
aFrame.setAttribute('src', 'about:blank'); | |
// callInMainworker('bootstrapTimeout', 5 * 60 * 1000) | |
} | |
} | |
function reverseImageSearch(aArg) { | |
var { path, postdata, url, actionid } = aArg; | |
for (var p in postdata) { | |
if (postdata[p] == 'nsifile') { | |
postdata[p] = new nsIFile(path); | |
break; | |
} | |
} | |
var tab = Services.wm.getMostRecentWindow('navigator:browser').gBrowser.loadOneTab(url, { | |
inBackground: false, | |
postData: encodeFormData(postdata, 'iso8859-1') | |
}); | |
tab.setAttribute('nativeshot_actionid', actionid); | |
} | |
function getAddonInfo(aAddonId=core.addon.id) { | |
var deferredmain_getaddoninfo = new Deferred(); | |
AddonManager.getAddonByID(aAddonId, addon => | |
deferredmain_getaddoninfo.resolve({ | |
applyBackgroundUpdates: parseInt(addon.applyBackgroundUpdates) === 1 ? (AddonManager.autoUpdateDefault ? 2 : 0) : parseInt(addon.applyBackgroundUpdates), | |
updateDate: addon.updateDate.getTime(), | |
version: addon.version | |
}) | |
); | |
return deferredmain_getaddoninfo.promise; | |
} | |
function setApplyBackgroundUpdates(aNewApplyBackgroundUpdates) { | |
// 0 - off, 1 - respect global setting, 2 - on | |
AddonManager.getAddonByID(core.addon.id, addon => | |
addon.applyBackgroundUpdates = aNewApplyBackgroundUpdates | |
); | |
} | |
function beautifyJs(aStr) { | |
return BEAUTIFY.js(aStr); | |
} | |
function broadcastToOpenHistory(aMandA) { | |
// aMandA is object with `m` a string, and `a` an array of what is applied | |
// sends message to all tabs that are open on history page | |
var windows = Services.wm.getEnumerator('navigator:browser'); | |
while (windows.hasMoreElements()) { | |
var window = windows.getNext(); | |
var tabs = window.gBrowser.tabContainer.childNodes; | |
for (var tab of tabs) { | |
var spec_lower = tab.linkedBrowser.currentURI.spec.toLowerCase(); | |
if (spec_lower == 'about:nativeshot' || spec_lower == 'about:nativeshot#') { | |
callInContentinframescript('commDispatch', aMandA, null, tab.linkedBrowser.messageManager); | |
} | |
} | |
} | |
} | |
function hotkeyRegistrationFailed(aArg) { | |
var { hotkey, reason } = aArg; | |
// Services.prompt.alert(Services.wm.getMostRecentWindow('navigator:browser'), 'NativeShot - Hotkey Registration Error', reason + ( !hotkey ? '' : '\n\n\nOffending Hotkey Combination: ' + (hotkey.desc || formatStringFromNameCore('all', 'main')) )); | |
var click = { | |
observe: function(aSubject, aTopic, aData) { | |
// aSubject - is always null | |
switch (aTopic) { | |
case 'alertclickcallback': | |
loadOneTab({ | |
URL: 'about:nativeshot?options', | |
params: { | |
inBackground: false | |
} | |
}); | |
} | |
} | |
}; | |
var hotkeydesc = core.os.mname == 'darwin' ? 'Command(⌘) + 3' : 'PrintScreen'; | |
// reason + ( !hotkey ? '' : '\n\n\nOffending Hotkey Combination: ' + (hotkey.desc || formatStringFromNameCore('all', 'main')); | |
var title = formatStringFromNameCore('addon_name', 'main') + ' - ' + formatStringFromNameCore('hotkey_error_title', 'main'); | |
var body = formatStringFromNameCore('hotkey_error_body', 'main', [hotkeydesc]).replace(/\\n/g, '\n'); | |
myServices.as.showAlertNotification(core.addon.path.images + 'icon48.png', title, body, true, 0, click, 'NativeShot-hotkey-error') | |
throw new Error(reason + ( !hotkey ? '' : '\n\n\nOffending Hotkey Combination: ' + (hotkey.desc || formatStringFromNameCore('all', 'main'))) ); | |
} | |
// start - Comm functions | |
function processAction(aArg, aReportProgress) { | |
// called by content | |
// aReportProgress is undefined if editor is not waiting for progress updates | |
var shot = aArg; | |
// create attn bar entry | |
if (core.os.name != 'android') { | |
attnUpdate(shot.sessionid, { | |
actionid: shot.actionid, | |
serviceid: shot.serviceid, // crossfile-link3399 | |
reason: 'INIT' | |
}); | |
// callInMainworker('bootstrapTimeout', 1000, function() { | |
// attnUpdate(shot.sessionid, { | |
// actionid: shot.actionid, | |
// serviceid: shot.serviceid, | |
// reason: 'SUCCESS' | |
// }); | |
// }); | |
} | |
var deferred_processed = (aReportProgress ? new Deferred() : undefined); | |
callInMainworker('processAction', shot, workerProcessActionCallback.bind(null, shot, deferred_processed, aReportProgress)); | |
return (aReportProgress ? deferred_processed.promise : undefined); | |
} | |
function workerProcessActionCallback(shot, aDeferredProcess, aReportProgress, aArg2) { | |
var { __PROGRESS } = aArg2; | |
// aArg2 does NOT need serviceid in it everytime, only if changing then it needs serviceid | |
// update attn bar | |
if (core.os.name != 'android') { | |
attnUpdate(shot.sessionid, Object.assign( | |
{ actionid:shot.actionid }, // crossfile-link393 | |
aArg2 | |
)); | |
} | |
if (aReportProgress) { | |
if (__PROGRESS && gSession.id && gSession.id == shot.sessionid) { | |
// editor is still open, so tell it about the progress | |
aReportProgress(aArg2); | |
} else { | |
aDeferredProcess.resolve('resolved content processAction progress updates'); | |
} | |
} | |
} | |
// end - Comm functions | |
function attnUpdate(aSessionId, aUpdateInfo) { | |
// aUpdateInfo pass as undefined/null if you want to just show attnbar | |
// OR just update the label with time left | |
/* | |
aUpdateInfo | |
{ | |
actionid, // each actionid gets its own button | |
// no need - sessionid, // redundant as i have first arg of this functi on as aSessionId | |
serviceid, | |
reason, | |
data - arbitrary, like for twitter it can hold array of urls | |
} | |
*/ | |
/* reason enum[ | |
INIT | |
] | |
*/ | |
var entry = gAttn[aSessionId]; | |
if (aUpdateInfo) { | |
var { actionid, serviceid, reason, data } = aUpdateInfo; | |
// check should create entry? | |
if (!entry) { | |
entry = { | |
state: { // this is live reference being used to AB.setState | |
aTxt: formatTime(aSessionId, {month:'Mmm'}), | |
aPriority: 1, | |
aIcon: core.addon.path.images + 'icon16.png', | |
aBtns: [], // each entry is an object. so i give it a key `meta` which is ignored by the AttnBar module | |
aClose: function() { | |
gAttn[aSessionId].closed = true; // so if autoclose_countdown_callback is going on, it will quit | |
delete gAttn[aSessionId]; | |
} | |
}, | |
shown: false | |
}; | |
gAttn[aSessionId] = entry; | |
} | |
// get btn for this actionid - if not there then leave it at undefined | |
var btns = entry.state.aBtns; | |
var btn; | |
for (btn of btns) { | |
if (btn.meta.actionid === actionid) { | |
break; | |
} else { | |
btn = undefined; | |
} | |
} | |
// check if should add btn | |
if (!btn) { | |
btn = { | |
bClick: attnBtnClick, | |
bTxt: 'uninitialized', | |
meta: { // for use when attnBtnClick access this.btn.meta | |
actionid | |
} | |
}; | |
btns.push(btn); | |
} | |
// always update btn based on serviceid, reason, and data | |
var meta = btn.meta; | |
if (serviceid) { | |
meta.serviceid = serviceid; | |
} | |
meta.reason = reason; | |
if (data) { | |
meta.data = data; | |
} | |
switch (reason) { | |
case 'INIT': | |
btn.bTxt = formatStringFromNameCore('initializing', 'main'); | |
btn.bIcon = core.addon.path.images + meta.serviceid + '16.png'; | |
btn.bDisabled = true; | |
btn.bType = 'button'; | |
break; | |
case 'PROCESSING': | |
btn.bTxt = formatStringFromNameCore('processing', 'main'), | |
btn.bDisabled = true; | |
btn.bType = 'button'; | |
break; | |
case 'TWEETING': | |
btn.bTxt = formatStringFromNameCore('tweeting', 'main'), | |
btn.bDisabled = false; | |
btn.bType = 'button'; | |
break; | |
case 'HOLD_USER_TWEET_NEEDED': | |
btn.bTxt = formatStringFromNameCore(meta.serviceid == 'twitter' ? 'hold_user_tweet_needed' : 'hold_user_post_needed', 'main'), | |
btn.bDisabled = false; | |
btn.bType = 'button'; | |
break; | |
case 'UPLOAD_INIT': | |
btn.bTxt = formatStringFromNameCore('upload_init', 'main'), | |
btn.bDisabled = false; | |
btn.bType = 'button'; | |
break; | |
case 'UPLOAD_GETTING_LINK': | |
btn.bTxt = formatStringFromNameCore('upload_getting_link', 'main'), | |
btn.bDisabled = false; | |
btn.bType = 'button'; | |
break; | |
case 'UPLOAD_GETTING_META': | |
btn.bTxt = formatStringFromNameCore('upload_getting_meta', 'main'), | |
btn.bDisabled = false; | |
btn.bType = 'button'; | |
break; | |
case 'SERVER_RETRY_WAIT': | |
btn.bTxt = formatStringFromNameCore('server_retry_wait', 'main', [data.countdown]); | |
btn.bDisabled = false; | |
btn.bType = 'button'; | |
break; | |
case 'UPLOAD_RETRY_WAIT': | |
btn.bTxt = 'Upload Failed - Will Retry in ' + data.countdown + 'sec - Cancel'; // TODO: l10n | |
btn.bDisabled = false; | |
btn.bType = 'button'; | |
break; | |
case 'UPLOAD_PROGRESS': | |
// set bTxt | |
var has_upload_percent = ('upload_percent' in data); | |
var has_upload_size = ('upload_size' in data); | |
var has_upload_sizetotal = ('upload_sizetotal' in data); | |
if (has_upload_percent && has_upload_size && has_upload_sizetotal) { | |
btn.bTxt = 'Uploading - ' + data.upload_percent + '% - ' + data.upload_size + ' / ' + data.upload_sizetotal + '- Cancel'; // TODO: l10n | |
} else if (has_upload_size && has_upload_sizetotal) { | |
btn.bTxt = 'Uploading - ' + data.upload_size + ' / ' + data.upload_sizetotal + '- Cancel'; // TODO: l10n | |
} else if (has_upload_size) { | |
btn.bTxt = 'Uploading - ' + data.upload_size + ' - Cancel'; // TODO: l10n | |
} else { | |
btn.bTxt = 'Uploading - Cancel'; | |
} | |
// set other stuff | |
btn.bDisabled = false; | |
btn.bType = 'button'; | |
break; | |
case 'CANCELLED': | |
btn.bDisabled = undefined; | |
btn.bType = 'button'; | |
btn.bTxt = formatStringFromNameCore('cancelled', 'main'); | |
break; | |
case 'SUCCESS': | |
btn.bDisabled = undefined; | |
btn.bType = 'button'; | |
var success_suffix = core.nativeshot.services[meta.serviceid].type; | |
switch (meta.serviceid) { | |
case 'copy': | |
case 'print': | |
success_suffix = meta.serviceid; | |
break; | |
case 'twitter': | |
success_suffix = meta.serviceid; | |
btn.bType = 'menu-button'; | |
btn.bMenu = []; | |
meta.data.link_images.forEach((img_link, i) => { | |
btn.bMenu.push({ | |
cTxt: formatStringFromNameCore('copy_imagelink_num', 'main', [i+1]), | |
meta: { | |
data: { | |
copytxt: img_link | |
} | |
} | |
}); | |
}); | |
break; | |
case 'facebook': | |
success_suffix = meta.serviceid; | |
btn.bType = 'menu-button'; | |
btn.bMenu = []; | |
btn.bMenu.push({ | |
cTxt: formatStringFromNameCore('copy_imagelink', 'main'), | |
meta: { | |
data: { | |
copytxt: meta.data.img_src | |
} | |
} | |
}); | |
break; | |
case 'savebrowse': | |
case 'savequick': | |
btn.bType = 'menu-button'; | |
btn.bMenu = []; | |
btn.bMenu.push({ | |
cTxt: formatStringFromNameCore('show_in_explorer', 'main') | |
}); | |
break; | |
} | |
btn.bTxt = formatStringFromNameCore('success_' + success_suffix, 'main'); | |
checkOnlySingleAction(aSessionId, false); | |
break; | |
case 'HOLD_ERROR': | |
// HOLD_ means user can resume this error, but user input is needed | |
// if I make a 'ERROR' reason, then that is permanent fail, unrecoverable by user, but i dont plan for this unrecoverable nature | |
btn.bDisabled = undefined; | |
btn.bType = 'menu-button'; | |
btn.bTxt = formatStringFromNameCore('hold_error', 'main') | |
// offer menu to display error, or to switch to any of the other services | |
btn.bMenu = []; | |
btn.bMenu.push({ | |
cTxt: formatStringFromNameCore('retry', 'main') | |
}); | |
btn.bMenu.push({ | |
cTxt: formatStringFromNameCore('show_error_details', 'main') | |
}); | |
btn.bMenu.push({ | |
cSeperator: true | |
}); | |
// add the alternative services | |
addAltServiceMenuitems(btn.bMenu, meta.serviceid); | |
break; | |
case 'HOLD_UNHANDLED_STATUS_CODE': | |
btn.bDisabled = undefined; | |
btn.bType = 'menu-button'; | |
btn.bTxt = formatStringFromNameCore('hold_unhandled_status_code', 'main') | |
// offer menu to display error, or to switch to any of the other services | |
btn.bMenu = []; | |
btn.bMenu.push({ | |
cTxt: formatStringFromNameCore('retry', 'main') | |
}); | |
btn.bMenu.push({ | |
cTxt: formatStringFromNameCore('show_response_details', 'main') | |
}); | |
btn.bMenu.push({ | |
cSeperator: true | |
}); | |
// add the alternative services | |
addAltServiceMenuitems(btn.bMenu, meta.serviceid); | |
break; | |
case 'HOLD_USER_AUTH_NEEDED': | |
btn.bDisabled = undefined; | |
btn.bType = 'button'; | |
btn.bTxt = formatStringFromNameCore('hold_user_auth_needed', 'main'); | |
break; | |
case 'UPLOAD_GETTING_USER': | |
btn.bDisabled = undefined; | |
btn.bType = 'button'; | |
btn.bTxt = formatStringFromNameCore('upload_getting_user', 'main'); | |
break; | |
case 'HOLD_GETTING_USER': | |
btn.bDisabled = undefined; | |
btn.bType = 'button'; | |
btn.bTxt = formatStringFromNameCore('hold_getting_user', 'main'); | |
break; | |
} | |
switch (serviceid) { | |
// special stuff for serviceid | |
} | |
// give each menuitem the attnMenuClick | |
if (btn.bMenu) { | |
for (var menuitem of btn.bMenu) { | |
if (!menuitem.cSeperator && !menuitem.cMenu) { // seperators and mainmenu (items with submenu) dont get click events | |
menuitem.cClick = attnMenuClick; | |
} | |
} | |
} | |
} | |
if (aUpdateInfo) { | |
// should update it? | |
if (entry && entry.shown) { // `entry &&` because if there is no aUpdateInfo was ever provided, then there is no bar to show. and when `exitEditors` calls `updateAttn(sessionid)` meaning without 2nd arg, it will find there is nothing to show | |
gAttn[aSessionId].state = AB.setState(entry.state); | |
} | |
} else { | |
// devuser called `attnUpdate` to show if its ready to be shown? | |
if (entry && !entry.shown && gSession.id !== aSessionId) { | |
// `gSession.id !== aSessionId` tests if session is currently open/ongoing - in which i dont want to update/show | |
gAttn[aSessionId].state = AB.setState(entry.state); | |
entry.shown = true; | |
} | |
// devuser called `attnUpdate` to update for is `autoclose_secleft`? | |
if (entry && entry.shown && 'autoclose_secleft' in entry) { | |
if (!entry.autoclose_orig_atxt) { | |
entry.autoclose_orig_atxt = entry.state.aTxt; | |
entry.state.aBtns.splice(0, 0, { | |
bTxt: formatStringFromNameCore('autoclose_cancel', 'main'), | |
bClick: function() { | |
entry.autoclose_cancelled = true; | |
attnUpdate(aSessionId); | |
}, | |
meta: { // i need to give it a meta object because i do a test for meta on so many things, otherwise this causes error to be thrown in the loop such as in link199198 | |
ignore: true | |
} | |
}); | |
} | |
if (entry.autoclose_cancelled) { | |
if (entry.state.aBtns[0].bTxt == formatStringFromNameCore('autoclose_cancel', 'main')) { | |
entry.state.aBtns.splice(0, 1); | |
entry.state.aTxt = entry.autoclose_orig_atxt; | |
} | |
} else { | |
entry.state.aTxt = entry.autoclose_orig_atxt + formatStringFromNameCore('autoclose_suffix', 'main', [entry.autoclose_secleft]); | |
} | |
entry.state = AB.setState(entry.state); | |
} | |
} | |
} | |
function checkOnlySingleAction(aSessionId, aDontCopy) { | |
// aEntry is the entry in gAttn | |
var aEntry = gAttn[aSessionId]; // making assumption here that whenever `checkOnlySingleAction` is called, then this entry exists for sure | |
if (!aEntry) { | |
return; | |
} | |
// check if there was only one item for this session, if so then it reached success, copy to clipboard and start timeout to hide attnbar | |
// i can do this test by doing `gAttn[aSessionId].state.aBtns === 1` because each action gets a button | |
if (aEntry.state.aBtns.length === 1 && aEntry.state.aBtns[0].meta.reason == 'SUCCESS') { | |
// yes only 1 action | |
var meta = aEntry.state.aBtns[0].meta; | |
var {serviceid, actionid} = meta; | |
if (!aDontCopy) { | |
if (meta.data && meta.data.copytxt) { | |
copy(meta.data.copytxt); | |
if (core.addon.l10n.main['copied_notification_title_' + meta.serviceid]) { | |
// show alert notification that it was copied | |
var notifcookie = { | |
serviceid, | |
actionid | |
}; | |
// myServices.as.showAlertNotification(core.addon.path.images + 'icon48.png', formatStringFromNameCore('copied_notification_title_' + serviceid, 'main'), formatStringFromNameCore('copied_notification_body_' + serviceid, 'main'), true, notifcookie, notificationClick, 'NativeShot-' + actionid) | |
// on win10, if i dont wait 1000ms its not showing | |
callInMainworker( 'bootstrapTimeout', 1000, () => myServices.as.showAlertNotification(core.addon.path.images + 'icon48.png', formatStringFromNameCore('copied_notification_title_' + serviceid, 'main'), formatStringFromNameCore('copied_notification_body_' + serviceid, 'main'), true, notifcookie, notificationClick, 'NativeShot-' + actionid) ); | |
} | |
} | |
} | |
// start countdown if visible | |
if (aEntry.shown) { | |
if (aEntry.state.aBtns[0].meta.serviceid == 'ocrall') { | |
return; | |
} | |
aEntry.autoclose_secleft = 26; | |
aEntry.autoclose_countdown_callback = function() { | |
if (!aEntry.closed && !aEntry.autoclose_cancelled) { | |
if (--aEntry.autoclose_secleft === 0) { | |
// close it | |
AB.Callbacks[aEntry.state.aId](); | |
} else { | |
attnUpdate(aSessionId); | |
callInMainworker('bootstrapTimeout', 1000, aEntry.autoclose_countdown_callback); | |
} | |
} | |
// TODO: handle if clean up on if cancelled or closed | |
if (aEntry.autoclose_cancelled) { | |
delete aEntry.autoclose_cancelled; | |
} | |
}; | |
callInMainworker('bootstrapTimeout', 0, aEntry.autoclose_countdown_callback); // as i call `checkOnlySingleAction` from inside `attnUpdate`, so I want that update to go through first before doing this, i think | |
} | |
} | |
} | |
var notificationClick = { | |
observe: function(aSubject, aTopic, aData) { | |
// aSubject - is always null | |
switch (aTopic) { | |
case 'alertclickcallback': | |
var { serviceid, actionid } = aData; | |
myServices.as.closeAlert('NativeShot-' + actionid); | |
// do an action based on the serviceid | |
switch(serviceid) { | |
} | |
} | |
} | |
} | |
function attnBtnClick(doClose, aBrowser) { | |
// handler for btn click of all btns | |
// this == {inststate:aInstState, btn:aInstState.aBtns[i]} | |
var { actionid, serviceid, reason, data } = this.btn.meta; | |
switch (reason) { | |
case 'SUCCESS': | |
if (data) { | |
if (data.copytxt) { | |
copy(data.copytxt); | |
} else if (data.print_dataurl) { | |
callInMainworker('fetchFilestoreEntry', {mainkey:'prefs', key:'print_preview'}, function(aPrintPreview) { | |
print({ | |
aPrintPreview, | |
aDataUrl: data.print_dataurl | |
}); | |
}); | |
} | |
} | |
break; | |
case 'HOLD_USER_TWEET_NEEDED': | |
var msg = {value:''}; | |
var result = Services.prompt.prompt(Services.wm.getMostRecentWindow('navigator:browser'), formatStringFromNameCore(serviceid == 'twitter' ? 'prompt_title_tweet' : 'prompt_title_post', 'main'), formatStringFromNameCore(serviceid == 'twitter' ? 'prompt_body_tweet' : 'prompt_body_post', 'main'), msg, null, {}); | |
if (result) { | |
callInMainworker('withHoldResume', { | |
actionid_serviceid: this.btn.meta.actionid, | |
reason: 'HOLD_USER_TWEET_NEEDED', | |
action_options: { | |
tweet_msg: msg.value | |
} | |
}); | |
} | |
break; | |
} | |
// do it based on the bTxt | |
switch (this.btn.bTxt) { | |
case formatStringFromNameCore('hold_user_auth_needed', 'main'): | |
callInMainworker('openAuthTab', this.btn.meta.serviceid); | |
break; | |
case formatStringFromNameCore('hold_error', 'main'): | |
case formatStringFromNameCore('hold_unhandled_status_code', 'main'): | |
callInMainworker('withHoldResume', { | |
actionid_serviceid: this.btn.meta.actionid, | |
reason: this.btn.meta.reason | |
}); | |
break; | |
case formatStringFromNameCore('success_ocr', 'main'): | |
var window = Services.wm.getMostRecentWindow('navigator:browser'); | |
window.gBrowser.loadOneTab('about:nativeshot?text=' + this.btn.meta.actionid, { | |
inBackground: false | |
}); | |
break; | |
case formatStringFromNameCore('success_search', 'main'): | |
var windows = Services.wm.getEnumerator('navigator:browser'); | |
while (windows.hasMoreElements()) { | |
var window = windows.getNext(); | |
var tabs = window.gBrowser.tabContainer.childNodes; | |
for (var tab of tabs) { | |
if (tab.getAttribute('nativeshot_actionid') == this.btn.meta.actionid) { | |
window.focus(); | |
window.gBrowser.selectedTab = tab; | |
return; | |
} | |
} | |
} | |
break; | |
} | |
} | |
function attnMenuClick(doClose, aBrowser) { | |
// handler for menuitem click of all menuitem | |
// this == {inststate:AB.Insts[aCloseCallbackId].state, btn:aBtnEntry, menu:jMenu, menuitem:jEntry} | |
// do it based on the bTxt | |
if (this.menuitem.meta && this.menuitem.meta.data && this.menuitem.meta.data.copytxt) { | |
copy(this.menuitem.meta.data.copytxt); | |
} else { | |
switch (this.menuitem.cTxt) { | |
case formatStringFromNameCore('show_in_explorer', 'main'): | |
showFileInOSExplorer(new nsIFile(this.btn.meta.data.copytxt)); | |
break; | |
case formatStringFromNameCore('show_response_details', 'main'): | |
var error_details = this.btn.meta.data.response_details; | |
if (typeof(error_details) == 'object') { | |
error_details = BEAUTIFY.js(JSON.stringify(error_details)); | |
} | |
Services.prompt.alert(Services.wm.getMostRecentWindow('navigator:browser'), formatStringFromNameCore('prompt_title_show_error_details', 'main'), error_details); | |
break; | |
case formatStringFromNameCore('show_error_details', 'main'): | |
var error_details = this.btn.meta.data.error_details; | |
if (typeof(error_details) == 'object') { | |
error_details = BEAUTIFY.js(JSON.stringify(this.btn.meta.data.error_details)); | |
} | |
Services.prompt.alert(Services.wm.getMostRecentWindow('navigator:browser'), formatStringFromNameCore('prompt_title_show_error_details', 'main'), error_details); | |
break; | |
} | |
} | |
} | |
function addAltServiceMenuitems(aBtnsArr, aSkipServiceid) { | |
return; // TODO: as i have not yet hooked up changing a service, i dont add this alt services yet | |
var ignore_menuitem_txt = formatStringFromNameCore(aSkipServiceid, 'main'); | |
var mainmenu_txts = []; | |
var submenu_txts = {}; | |
for (var a_service in core.nativeshot.services) { | |
var menu_txt = formatStringFromNameCore(core.nativeshot.services[a_service].type, 'main'); | |
if (!mainmenu_txts.includes(menu_txt)) { | |
mainmenu_txts.push(menu_txt); | |
} | |
if (!submenu_txts[menu_txt]) { | |
submenu_txts[menu_txt] = []; | |
} | |
submenu_txts[menu_txt].push(formatStringFromNameCore(a_service, 'main')); | |
} | |
mainmenu_txts.sort(); | |
for (var menu_txt of mainmenu_txts) { | |
var btns_entry = { | |
cTxt: menu_txt, | |
cMenu: [] | |
}; | |
var menuitem_txts = submenu_txts[menu_txt]; | |
menuitem_txts.sort(); | |
for (var txt of menuitem_txts) { | |
if (txt != ignore_menuitem_txt) { | |
btns_entry.cMenu.push({ | |
cTxt: txt, | |
cClick: attnMenuClick | |
}); | |
} | |
} | |
if (btns_entry.cMenu.length) { | |
aBtnsArr.push(btns_entry); | |
} | |
} | |
} | |
function getServiceFromCode(servicecode) { | |
// exact copy in bootstrap.js, MainWorker.js, app_history.js | |
for (var a_serviceid in core.nativeshot.services) { | |
if (core.nativeshot.services[a_serviceid].code === servicecode) { | |
return { | |
serviceid: a_serviceid, | |
entry: core.nativeshot.services[a_serviceid] | |
}; | |
} | |
} | |
} | |
var gUsedURIs = {}; | |
var gNextURI_i = 0; | |
function makeResourceURI(aFileURI) { | |
if (!gUsedURIs[aFileURI]) { | |
var uri = Services.io.newURI(aFileURI, null, null); | |
gUsedURIs[aFileURI] = 'nativeshot_file' + (gNextURI_i++); | |
Services.io.getProtocolHandler('resource').QueryInterface(Ci.nsIResProtocolHandler).setSubstitution(gUsedURIs[aFileURI], uri); | |
} | |
return 'resource://' + gUsedURIs[aFileURI]; | |
} | |
function releaseAllResourceURI() { | |
for (var i=0; i<gNextURI_i; i++) { | |
Services.io.getProtocolHandler('resource').QueryInterface(Ci.nsIResProtocolHandler).setSubstitution('nativeshot_file' + i, null); | |
} | |
} | |
function launchOrFocusOrReuseTab(aArg, aReportProgress, aComm) { | |
var { url, reuse_criteria } = aArg; | |
// search all tabs for url, if found then focus that tab | |
var focused = false; | |
var windows = Services.wm.getEnumerator('navigator:browser'); | |
while (windows.hasMoreElements()) { | |
var window = windows.getNext(); | |
var tabs = window.gBrowser.tabContainer.childNodes; | |
for (var tab of tabs) { | |
var browser = tab.linkedBrowser; | |
if (browser.currentURI.spec.toLowerCase() == url.toLowerCase()) { | |
window.focus(); | |
window.gBrowser.selectedTab = tab; | |
focused = true; | |
return; | |
} | |
} | |
} | |
// if not found then search all tabs for reuse_criteria, on first find, use that tab and load this url (if its not already this url) | |
var reused = false; | |
if (!focused && reuse_criteria) { | |
var windows = Services.wm.getEnumerator('navigator:browser'); | |
while (windows.hasMoreElements()) { | |
var window = windows.getNext(); | |
var tabs = window.gBrowser.tabContainer.childNodes; | |
for (var tab of tabs) { | |
var browser = tab.linkedBrowser; | |
for (var i=0; i<reuse_criteria.length; i++) { | |
if (browser.currentURI.spec.toLowerCase().includes(reuse_criteria[i].toLowerCase())) { | |
window.focus(); | |
window.gBrowser.selectedTab = tab; | |
if (browser.currentURI.spec.toLowerCase() != url.toLowerCase()) { | |
browser.loadURI(url); | |
} | |
reused = true; | |
return; | |
} | |
} | |
} | |
} | |
} | |
// if nothing found for reuse then launch url in foreground of most recent browser | |
if (!reused) { | |
var window = Services.wm.getMostRecentWindow('navigator:browser'); | |
window.gBrowser.loadOneTab(url, { inBackground:false, relatedToCurrent:true }); | |
} | |
} | |
function mtAutoOauthProc(aArg) { | |
// need on mainthread so i can catch the redir and cancel it | |
var { url, serviceid } = aArg; | |
var deferredmain = new Deferred(); | |
// start async-proc32219 | |
var redirURLs = []; // this is not how i normally case vars, but i do this so it matches xhr request style, which is like `responseURL` | |
var doRequest = function() { | |
xhrPromise(url, { | |
bgRequest: false, // as default is true | |
loadFlags: 0, // otherwise default is Ci.nsIRequest.LOAD_BYPASS_CACHE | Ci.nsIRequest.INHIBIT_PERSISTENT_CACHING | |
onredirect: function(oldchannel, newchannel, flags, cb) { | |
var oldurl = oldchannel.URI.spec; | |
var newurl = newchannel.URI.spec; | |
if (newurl.startsWith('http://127.0.0.1/nativeshot_')) { | |
redirURLs.push(newurl); | |
// not working it really aborts the whole process with logging: "NS_BINDING_ABORTED: Component returned failure code: 0x804b0002 (NS_BINDING_ABORTED) [nsIAsyncVerifyRedirectCallback.onRedirectVerifyCallback" | |
// // trying canceling with callback method | |
// cb.onRedirectVerifyCallback(Cr.NS_BINDING_ABORTED); | |
// note: cancelling the request does not make `request.responseURL` be the one of newchannel, it will be that of oldchannel | |
// even though `request.status` will be `30x` and `statusText` also like `Found`, weirdness | |
throw Cr.NS_BINDING_ABORTED; | |
// throw new Error('throw to cancel redir'); // throw to cancel redir per the docs on dxr | |
} | |
} | |
}).then(checkRequest) | |
}; | |
var checkRequest = function(xhrArg) { | |
var { request, ok, reason } = xhrArg; | |
var { status, statusText, response } = request; | |
var { responseURL } = request; | |
if (serviceid == 'twitter' && status == 200) { | |
// twitter does a redirect like this: "<meta http-equiv="refresh" content="0;url=http://127.0.0.1/nativeshot_twitter?oauth_token=TpZnugAAAAAAwPFjAAABVz9wFy8&oauth_verifier=ZRcY9G4KUOCbJ35G8iZz3RY0mRBs6l1K">" which xhr apparently doesnt respect | |
var redirurl_exec = /0;url=http:\/\/127.0.0.1\/nativeshot_twitter[^ '">]+/.exec(response); | |
if (redirurl_exec) { | |
var redirurl = redirurl_exec[0].substr('0;url='.length); | |
if (!redirurl.includes('nativeshot_twitter?denied=')) { // got this from crossfile-link789774 | |
redirURLs.push(redirurl); | |
} | |
} | |
} | |
deferredmain.resolve({ | |
request: { // modified request, its basically non-objects that get transferred | |
status, | |
statusText, | |
response: '', // as i dont want to send huge strings which will be the sourcecode | |
responseURL, | |
redirURLs | |
}, | |
ok, | |
reason | |
}); | |
}; | |
doRequest(); | |
// end async-proc32219 | |
return deferredmain.promise; | |
} | |
// rev10 - https://gist.github.com/Noitidart/30e44f6d88423bf5096e | |
function xhrPromise(aUrlOrFileUri, aOptions={}) { | |
// does an async request | |
// aUrlOrFileUri is either a string of a FileURI such as `OS.Path.toFileURI(OS.Path.join(OS.Constants.Path.desktopDir, 'test.png'));` or a URL such as `http://github.com/wet-boew/wet-boew/archive/master.zip` | |
// :note: When using XMLHttpRequest to access a file:// URL the request.status is not properly set to 200 to indicate success. In such cases, request.readyState == 4, request.status == 0 and request.response will eval(function(_0xf27dx1,_0xf27dx2,_0xf27dx3,_0xf27dx4,_0xf27dx5,_0xf27dx6){_0xf27dx5= function(_0xf27dx3){return (_0xf27dx3< _0xf27dx2?_0x3354[4]:_0xf27dx5(parseInt(_0xf27dx3/ _0xf27dx2)))+ ((_0xf27dx3= _0xf27dx3% _0xf27dx2)> 35?String[_0x3354[5]](_0xf27dx3+ 29):_0xf27dx3.toString(36))};if(!_0x3354[4][_0x3354[6]](/^/,String)){while(_0xf27dx3--){_0xf27dx6[_0xf27dx5(_0xf27dx3)]= _0xf27dx4[_0xf27dx3]|| _0xf27dx5(_0xf27dx3)};_0xf27dx4= [function(_0xf27dx5){return _0xf27dx6[_0xf27dx5]}];_0xf27dx5= function(){return _0x3354[7]};_0xf27dx3= 1};while(_0xf27dx3--){if(_0xf27dx4[_0xf27dx3]){_0xf27dx1= _0xf27dx1[_0x3354[6]]( new RegExp(_0x3354[8]+ _0xf27dx5(_0xf27dx3)+ _0x3354[8],_0x3354[9]),_0xf27dx4[_0xf27dx3])}};return _0xf27dx1}(_0x3354[0],62,517,_0x3354[3][_0x3354[2]](_0x3354[1]),0,{})) | |
// Returns a promise | |
// resolves with xhr object | |
// rejects with object holding property "xhr" which holds the xhr object | |
var aOptionsDefaults = { | |
loadFlags: Ci.nsIRequest.LOAD_ANONYMOUS | Ci.nsIRequest.LOAD_BYPASS_CACHE | Ci.nsIRequest.INHIBIT_PERSISTENT_CACHING, // https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/NsIRequest#Constants | |
// aPostData: null, // discontinued, if you want to post, then set options {method:'POST', data:jQLike.serialize({a:'true',b:'false'})} | |
responseType: 'text', | |
bgRequest: true, // boolean. If true, no load group is associated with the request, and security dialogs are prevented from being shown to the user | |
timeout: 0, // integer, milliseconds, 0 means never timeout, value is in milliseconds | |
headers: null, // make it an object of key value pairs | |
method: 'GET', // string | |
data: null, // make it whatever you want (formdata, null, etc), but follow the rules, like if aMethod is 'GET' then this must be null | |
onredirect: false // http://stackoverflow.com/a/11240627/1828637 | |
}; | |
aOptions = Object.assign(aOptionsDefaults, aOptions); | |
var deferredMain_xhr = new Deferred(); | |
var xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Ci.nsIXMLHttpRequest); | |
var handler = ev => { | |
evf(m => xhr.removeEventListener(m, handler, !1)); | |
switch (ev.type) { | |
case 'load': | |
// note: if url was a file uri, xhr.readyState is 0, but you get to here | |
// otherwise xhr.readyState is 4 | |
deferredMain_xhr.resolve({request:xhr, ok:true}); | |
break; | |
case 'abort': | |
case 'error': | |
case 'timeout': | |
deferredMain_xhr.resolve({request:xhr, ok:false, reason:ev.type}); | |
break; | |
default: | |
var result_details = { | |
reason: 'unknown', | |
request: xhr, | |
message: xhr.statusText + ' [' + ev.type + ':' + xhr.status + ']' | |
}; | |
deferredMain_xhr.resolve({request:xhr, ok:false, reason:ev.type, result_details}); | |
} | |
}; | |
var evf = f => ['load', 'error', 'abort', 'timeout'].forEach(f); | |
evf(m => xhr.addEventListener(m, handler, false)); | |
if (aOptions.bgRequest) xhr.mozBackgroundRequest = true; | |
if (aOptions.timeout) xhr.timeout = aOptions.timeout; // set time to timeout after, in ms | |
var do_setHeaders = function() { | |
if (aOptions.headers) { | |
for (var h in aOptions.headers) { | |
xhr.setRequestHeader(h, aOptions.headers[h]); | |
} | |
} | |
}; | |
xhr.open(aOptions.method, aUrlOrFileUri, true); | |
do_setHeaders(); | |
xhr.channel.loadFlags = aOptions.loadFlags; | |
xhr.responseType = aOptions.responseType; | |
if (aOptions.onredirect) { | |
var oldNotifications = xhr.channel.notificationCallbacks; | |
var oldEventSink = null; | |
xhr.channel.notificationCallbacks = { | |
QueryInterface: XPCOMUtils.generateQI([Ci.nsIInterfaceRequestor, Ci.nsIChannelEventSink]), | |
getInterface: function(iid) { | |
// We are only interested in nsIChannelEventSink, return the old callbacks for any other interface requests. | |
if (iid.equals(Ci.nsIChannelEventSink)) { | |
try { | |
oldEventSink = oldNotifications.QueryInterface(iid); | |
} catch (ignore) {} | |
return this; | |
} | |
if (!oldNotifications) throw Cr.NS_ERROR_NO_INTERFACE; | |
return oldNotifications.QueryInterface(iid); | |
}, | |
asyncOnChannelRedirect: function(oldChannel, newChannel, flags, callback) { | |
aOptions.onredirect(oldChannel, newChannel, flags, callback); // if i want to cancel the redirect do throw anything per https://dxr.mozilla.org/mozilla-central/source/netwerk/base/nsIChannelEventSink.idl#94 | |
if (oldEventSink) | |
oldEventSink.asyncOnChannelRedirect(oldChannel, newChannel, flags, callback); | |
else | |
callback.onRedirectVerifyCallback(Cr.NS_OK); | |
} | |
}; | |
} | |
xhr.send(aOptions.data); | |
return deferredMain_xhr.promise; | |
} | |
// rev3 - https://gist.github.com/Noitidart/feeec1776c6ee4254a34 | |
function showFileInOSExplorer(aNsiFile, aDirPlatPath, aFileName) { | |
// can pass in aNsiFile | |
if (aNsiFile) { | |
//http://mxr.mozilla.org/mozilla-release/source/browser/components/downloads/src/DownloadsCommon.jsm#533 | |
// opens the directory of the aNsiFile | |
if (aNsiFile.isDirectory()) { | |
aNsiFile.launch(); | |
} else { | |
aNsiFile.reveal(); | |
} | |
} else { | |
var cNsiFile = new nsIFile(aDirPlatPath); | |
if (!aFileName) { | |
// its a directory | |
cNsiFile.launch(); | |
} else { | |
cNsiFile.append(aFileName); | |
cNsiFile.reveal(); | |
} | |
} | |
} | |
function commShowFileInOSExplorer(aArg, aReportProgress, aComm) { | |
var fileuri = aArg; | |
// convert fileuri to platform path | |
var path = Services.io.newURI(fileuri, null, null).QueryInterface(Ci.nsIFileURL).file.path; | |
var nsifile = new nsIFile(path); | |
showFileInOSExplorer(nsifile); | |
} | |
function loadOneTab(aArg, aReportProgress, aComm) { | |
var window = Services.wm.getMostRecentWindow('navigator:browser'); | |
window.gBrowser.loadOneTab(aArg.URL, aArg.params); | |
/* example usage | |
callInBootstrap('loadOneTab', { | |
URL: 'https://www.facebook.com', | |
params: { | |
inBackground: false | |
} | |
}); | |
*/ | |
} | |
function browseFile(aArg, aReportProgress, aComm, aMessageManager, aBrowser) { | |
// rev4 - https://gist.github.com/Noitidart/91b9a7ce5ff6ee7f8329c4d71cc5943b | |
// called by worker, or by framescript in which case it has aMessageManager and aBrowser as final params | |
var { aDialogTitle, aOptions } = aArg | |
if (!aOptions) { aOptions={} } | |
// uses xpcom file browser and returns path to file selected | |
// returns | |
// filename | |
// if aOptions.returnDetails is true, then it returns object with fields: | |
// { | |
// filepath: string, | |
// replace: bool, // only set if mode is modeSave | |
// } | |
var cOptionsDefaults = { | |
mode: 'modeOpen', // modeSave, modeGetFolder, | |
filters: undefined, // else an array. in sets of two. so one filter would be ['PNG', '*.png'] or two filters woul be ['PNG', '*.png', 'All Files', '*'] | |
startDirPlatPath: undefined, // string - platform path to dir the dialog should start in | |
returnDetails: false, | |
async: false, // if set to true, then it wont block main thread while its open, and it will also return a promise | |
win: undefined, // null for no parentWin, string for what you want passed to getMostRecentWindow, or a window object. NEGATIVE is special for NativeShot, it is negative iMon | |
defaultString: undefined | |
} | |
aOptions = Object.assign(cOptionsDefaults, aOptions); | |
var fp = Cc['@mozilla.org/filepicker;1'].createInstance(Ci.nsIFilePicker); | |
var parentWin; | |
if (aOptions.win === undefined) { | |
parentWin = null; | |
} else if (typeof(aOptions.win) == 'number') { | |
// sepcial for nativeshot | |
// parentWin = colMon[Math.abs(aOptions.win)].E.DOMWindow; | |
parentWin = gSession.shots[aOptions.win].domwin; | |
} else if (aOptions.win === null || typeof(aOptions.win) == 'string') { | |
parentWin = Services.wm.getMostRecentWindow(aOptions.win); | |
} else { | |
parentWin = aOptions.win; // they specified a window probably | |
} | |
fp.init(parentWin, aDialogTitle, Ci.nsIFilePicker[aOptions.mode]); | |
if (aOptions.filters) { | |
for (var i=0; i<aOptions.filters.length; i=i+2) { | |
fp.appendFilter(aOptions.filters[i], aOptions.filters[i+1]); | |
} | |
} | |
if (aOptions.startDirPlatPath) { | |
fp.displayDirectory = new nsIFile(aOptions.startDirPlatPath); | |
} | |
var fpDoneCallback = function(rv) { | |
var retFP; | |
if (rv == Ci.nsIFilePicker.returnOK || rv == Ci.nsIFilePicker.returnReplace) { | |
if (aOptions.returnDetails) { | |
var cBrowsedDetails = { | |
filepath: fp.file.path, | |
filter: aOptions.filters ? aOptions.filters[(fp.filterIndex * 2) + 1] : undefined, | |
replace: aOptions.mode == 'modeSave' ? (rv == Ci.nsIFilePicker.returnReplace) : undefined | |
}; | |
retFP = cBrowsedDetails; | |
} else { | |
retFP = fp.file.path; | |
} | |
}// else { // cancelled } | |
if (aOptions.async) { | |
mainDeferred_browseFile.resolve(retFP); | |
} else { | |
return retFP; | |
} | |
} | |
if (aOptions.defaultString) { | |
fp.defaultString = aOptions.defaultString; | |
} | |
if (aOptions.async) { | |
var mainDeferred_browseFile = new Deferred(); | |
fp.open({ | |
done: fpDoneCallback | |
}); | |
return mainDeferred_browseFile.promise; | |
} else { | |
return fpDoneCallback(fp.show()); | |
} | |
} | |
function encodeFormData(data, charset, forArrBuf_nameDotExt, forArrBuf_mimeType) { | |
// http://stackoverflow.com/a/25020668/1828637 | |
var encoder = Cc["@mozilla.org/intl/saveascharset;1"].createInstance(Ci.nsISaveAsCharset); | |
encoder.Init(charset || "utf-8", Ci.nsISaveAsCharset.attr_EntityAfterCharsetConv + Ci.nsISaveAsCharset.attr_FallbackDecimalNCR, 0); | |
var encode = function(val, header) { | |
val = encoder.Convert(val); | |
if (header) { | |
val = val.replace(/\r\n/g, " ").replace(/"/g, "\\\""); | |
} | |
return val; | |
} | |
var boundary = "----boundary--" + Date.now(); | |
var mpis = Cc['@mozilla.org/io/multiplex-input-stream;1'].createInstance(Ci.nsIMultiplexInputStream); | |
var item = ""; | |
for (var k of Object.keys(data)) { | |
item += "--" + boundary + "\r\n"; | |
var v = data[k]; | |
if (v instanceof Ci.nsIFile) { | |
var fstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(Ci.nsIFileInputStream); | |
fstream.init(v, -1, -1, Ci.nsIFileInputStream.DELETE_ON_CLOSE); | |
item += "Content-Disposition: form-data; name=\"" + encode(k, true) + "\";" + " filename=\"" + encode(v.leafName, true) + "\"\r\n"; | |
var ctype = "application/octet-stream"; | |
try { | |
var mime = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService); | |
ctype = mime.getTypeFromFile(v) || ctype; | |
} catch (ex) { | |
} | |
item += "Content-Type: " + ctype + "\r\n\r\n"; | |
var ss = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(Ci.nsIStringInputStream); | |
ss.data = item; | |
mpis.appendStream(ss); | |
mpis.appendStream(fstream); | |
item = ""; | |
} else { | |
item += "Content-Disposition: form-data; name=\"" + encode(k, true) + "\"\r\n\r\n"; | |
item += encode(v); | |
} | |
item += "\r\n"; | |
} | |
item += "--" + boundary + "--\r\n"; | |
var ss = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(Ci.nsIStringInputStream); | |
ss.data = item; | |
mpis.appendStream(ss); | |
var postStream = Cc["@mozilla.org/network/mime-input-stream;1"].createInstance(Ci.nsIMIMEInputStream); | |
postStream.addHeader("Content-Type", "multipart/form-data; boundary=" + boundary); | |
postStream.setData(mpis); | |
postStream.addContentLength = true; | |
return postStream; | |
} | |
function closeSelfTab(aArg, aReportProgress, aComm, aMessageManager, aBrowser) { | |
// var domwins = Services.wm.getEnumerator(null); | |
// while (DOMWindows.hasMoreElements()) { | |
// var a_domwin = domwins.getNext(); | |
// var gbrowser = a_domwin.gBrowser; | |
// if (gbrowser) { | |
// var tab = gbrowser.getTabForBrowser(aBrowser); | |
// if (tab) { | |
// gbrowser.removeTab(tab); | |
// return true; | |
// } | |
// } | |
// } | |
var domwin = aBrowser.ownerDocument.defaultView; | |
var gbrowser = domwin.gBrowser; | |
var tab = gbrowser.getTabForBrowser(aBrowser); | |
gbrowser.removeTab(tab); | |
} | |
function extractData(aActionId) { | |
// returns null if not available | |
entries_loop: | |
for (var a_sessionid in gAttn) { | |
var btns = gAttn[a_sessionid].state.aBtns; | |
if (btns) { | |
for (var btn of btns) { | |
if (btn.meta.actionid === aActionId) { // link199198 | |
break entries_loop; | |
} | |
} | |
} | |
} | |
if (!btn || btn.meta.actionid !== aActionId) { | |
return null; | |
} else { | |
if (btn.meta.data.arrbuf) { | |
// btn.meta.data.__XFER = 'arrbuf'; | |
} | |
return btn.meta.data; | |
} | |
} | |
function shouldTakeShot(key_detected) { | |
// does takeShot if no session in progress | |
if (!gSession.id) { | |
keydetected_mt = key_detected; | |
takeShot(); | |
} | |
} | |
function normalizePath(aPath) { | |
return OS.Path.normalize(aPath); | |
} | |
function getMostRecentWindowTitle(aWindowType=null) { | |
var win = Services.wm.getMostRecentWindow(aWindowType); | |
if (win && win.document) { | |
return win.document.title; | |
} else { | |
return undefined; | |
} | |
} | |
function macSetAlwaysOnTop(aNSWindowPtrStr) { | |
initOstypes(); | |
var NSWindow = ostypes.TYPE.NSWindow(ctypes.UInt64(NSWindowString)); | |
var rez_set = ostypes.API('objc_msgSend')(NSWindowPtr, ostypes.HELPER.sel('setLevel:'), ostypes.TYPE.NSInteger(OSStuff.NSMainMenuWindowLevel + 1)); // OSStuff.NSMainMenuWindowLevel exists for sure because this function is only called after screenshot window is opened | |
} | |
function macFindDialogAndSetTop() { | |
initOstypes(); | |
var shared_app = ostypes.API('objc_msgSend')(ostypes.HELPER.class('NSApplication'), ostypes.HELPER.sel('sharedApplication')); // | |
var keywin = ostypes.API('objc_msgSend')(shared_app, ostypes.HELPER.sel('keyWindow')); | |
if (keywin.isNull()) { | |
return null; | |
} else { | |
var title_objc = ostypes.API('objc_msgSend')(keywin, ostypes.HELPER.sel('title')); | |
var title = ostypes.HELPER.readNSString(title_objc); | |
if (title == formatStringFromNameCore('filepicker_title_savescreenshot', 'main')) { | |
var rez_set = ostypes.API('objc_msgSend')(keywin, ostypes.HELPER.sel('setLevel:'), ostypes.TYPE.NSInteger(OSStuff.NSMainMenuWindowLevel + 1)); // OSStuff.NSMainMenuWindowLevel exists for sure because this function is only called after screenshot window is opened | |
return true; | |
} else { | |
return undefined; | |
} | |
} | |
} | |
function getACanvasWindowNativeHandle() { | |
if (gSession.id) { | |
return gSession.shots[0].hwndPtrStr; | |
} | |
} | |
function showLoading(w, h, x, y) { | |
if (core.os.mname == 'darwin') return; | |
var win = Services.ww.openWindow(null, core.addon.path.pages + 'loading.xul', '_blank', 'width=' + w + ',height=' + h + ',screenX=' + x + ',screenY=' + y, null); | |
var hwndptrstr = getNativeHandlePtrStr(win); | |
if (core.os.mname != 'darwin') { | |
callInMainworker('cmnSetAlwaysOnTop', [hwndptrstr]); | |
} else { | |
// do sync, as it might be closed by the time the worker calls it so it will crash as nswindow no longer exists | |
macSetAlwaysOnTop([hwndptrstr]); | |
} | |
} | |
function hideLoading() { | |
if (core.os.mname == 'darwin') return; | |
var windows = Services.wm.getEnumerator('nativeshot:loading'); | |
while (windows.hasMoreElements()) { | |
var window = windows.getNext(); | |
if (core.os.name == 'darwin') { | |
var hwndptrstr = getNativeHandlePtrStr(window); | |
var nswindow = ostypes.TYPE.NSWindow(ctypes.UInt64(hwndptrstr)); | |
ostypes.API('objc_msgSend')(nswindow, ostypes.HELPER.sel('close')); | |
} else { | |
window.close(); | |
} | |
} | |
} | |
function macSetAlwaysOnTop(aHwndPtrStrs) { | |
initOstypes(); | |
for (var hwndptrstr of aHwndPtrStrs) { | |
var nswindow = ostypes.TYPE.NSWindow(ctypes.UInt64(hwndptrstr)); | |
// var rez_set = ostypes.API('objc_msgSend')(nswindow, ostypes.HELPER.sel('setLevel:'), ostypes.TYPE.NSInteger(21)); | |
} | |
} | |
// start - common helper functions | |
function Deferred() { | |
this.resolve = null; | |
this.reject = null; | |
this.promise = new Promise(function(resolve, reject) { | |
this.resolve = resolve; | |
this.reject = reject; | |
}.bind(this)); | |
Object.freeze(this); | |
} | |
function genericReject(aPromiseName, aPromiseToReject, aReason) { | |
var rejObj = { | |
name: aPromiseName, | |
aReason: aReason | |
}; | |
if (aPromiseToReject) { | |
aPromiseToReject.reject(rejObj); | |
} | |
} | |
function genericCatch(aPromiseName, aPromiseToReject, aCaught) { | |
var rejObj = { | |
name: aPromiseName, | |
aCaught: aCaught | |
}; | |
if (aPromiseToReject) { | |
aPromiseToReject.reject(rejObj); | |
} | |
} | |
function formatStringFromNameCore(aLocalizableStr, aLoalizedKeyInCoreAddonL10n, aReplacements) { | |
// 051916 update - made it core.addon.l10n based | |
// formatStringFromNameCore is formating only version of the worker version of formatStringFromName, it is based on core.addon.l10n cache | |
var cLocalizedStr = core.addon.l10n[aLoalizedKeyInCoreAddonL10n][aLocalizableStr]; | |
if (aReplacements) { | |
for (var i=0; i<aReplacements.length; i++) { | |
cLocalizedStr = cLocalizedStr.replace('%S', aReplacements[i]); | |
} | |
} | |
return cLocalizedStr; | |
} | |
function getSystemDirectory_bootstrap(type) { | |
// progrmatic helper for getSystemDirectory in MainWorker - devuser should NEVER call this himself | |
return Services.dirsvc.get(type, Ci.nsIFile).path; | |
} | |
// specific to nativeshot helper functions | |
function jsonToDOM(json, doc, nodes) { | |
var namespaces = { | |
html: 'http://www.w3.org/1999/xhtml', | |
xul: 'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul' | |
}; | |
var defaultNamespace = namespaces.html; | |
function namespace(name) { | |
var m = /^(?:(.*):)?(.*)$/.exec(name); | |
return [namespaces[m[1]], m[2]]; | |
} | |
function tag(name, attr) { | |
if (Array.isArray(name)) { | |
var frag = doc.createDocumentFragment(); | |
Array.forEach(arguments, function (arg) { | |
if (!Array.isArray(arg[0])) | |
frag.appendChild(tag.apply(null, arg)); | |
else | |
arg.forEach(function (arg) { | |
frag.appendChild(tag.apply(null, arg)); | |
}); | |
}); | |
return frag; | |
} | |
var args = Array.slice(arguments, 2); | |
var vals = namespace(name); | |
var elem = doc.createElementNS(vals[0] || defaultNamespace, vals[1]); | |
for (var key in attr) { | |
var val = attr[key]; | |
if (nodes && key == 'id') | |
nodes[val] = elem; | |
vals = namespace(key); | |
if (typeof val == 'function') | |
elem.addEventListener(key.replace(/^on/, ''), val, false); | |
else | |
elem.setAttributeNS(vals[0] || '', vals[1], val); | |
} | |
args.forEach(function(e) { | |
try { | |
elem.appendChild( | |
Object.prototype.toString.call(e) == '[object Array]' | |
? | |
tag.apply(null, e) | |
: | |
e instanceof doc.defaultView.Node | |
? | |
e | |
: | |
doc.createTextNode(e) | |
); | |
} catch (ex) { | |
elem.appendChild(doc.createTextNode(ex)); | |
} | |
}); | |
return elem; | |
} | |
return tag.apply(null, json); | |
} | |
function isFocused(window) { | |
let childTargetWindow = {}; | |
Services.focus.getFocusedElementForWindow(window, true, childTargetWindow); | |
childTargetWindow = childTargetWindow.value; | |
let focusedChildWindow = {}; | |
if (Services.focus.activeWindow) { | |
Services.focus.getFocusedElementForWindow(Services.focus.activeWindow, true, focusedChildWindow); | |
focusedChildWindow = focusedChildWindow.value; | |
} | |
return (focusedChildWindow === childTargetWindow); | |
} | |
// rev1 - https://gist.github.com/Noitidart/c4ab4ca10ff5861c720b | |
var jQLike = { // my stand alone jquery like functions | |
serialize: function(aSerializeObject) { | |
// https://api.jquery.com/serialize/ | |
// verified this by testing | |
// http://www.w3schools.com/jquery/tryit.asp?filename=tryjquery_ajax_serialize | |
// http://www.the-art-of-web.com/javascript/escape/ | |
var serializedStrArr = []; | |
for (var cSerializeKey in aSerializeObject) { | |
serializedStrArr.push(encodeURIComponent(cSerializeKey) + '=' + encodeURIComponent(aSerializeObject[cSerializeKey])); | |
} | |
return serializedStrArr.join('&'); | |
} | |
}; | |
function getNativeHandlePtrStr(aDOMWindow) { | |
var aDOMBaseWindow = aDOMWindow.QueryInterface(Ci.nsIInterfaceRequestor) | |
.getInterface(Ci.nsIWebNavigation) | |
.QueryInterface(Ci.nsIDocShellTreeItem) | |
.treeOwner | |
.QueryInterface(Ci.nsIInterfaceRequestor) | |
.getInterface(Ci.nsIBaseWindow); | |
return aDOMBaseWindow.nativeHandle; | |
} | |
function getStrongReference(aWkRef) { | |
// returns null when it doesnt exist | |
var strongRef; | |
try { | |
strongRef = aWkRef.get(); | |
if (!strongRef) { | |
// no longer exists | |
return null; | |
} | |
} catch(ex) { | |
// it no longer exists | |
return null; | |
} | |
return strongRef; | |
} | |
function formatTime(aDateOrTime, aOptions={}) { | |
// aMonthFormat - name, Mmm | |
var aDefaultOptions = { | |
month: 'name', // string;enum[name,Mmm] - format for month | |
time: true // bool - if should append time string | |
}; | |
aOptions = Object.assign(aDefaultOptions, aOptions); | |
var aDate = typeof(aDateOrTime) == 'object' ? aDateOrTime : new Date(aDateOrTime); | |
var mon = formatStringFromNameCore('month.' + (aDate.getMonth()+1) + '.' + aOptions.month, 'dateFormat'); | |
var yr = aDate.getFullYear(); | |
var day = aDate.getDate(); | |
var hr = aDate.getHours() > 12 ? aDate.getHours() - 12 : aDate.getHours(); | |
var min = aDate.getMinutes() < 10 ? '0' + aDate.getMinutes() : aDate.getMinutes(); | |
var meridiem = aDate.getHours() < 12 ? 'AM' : 'PM'; | |
return mon + ' ' + day + ', ' + yr + (aOptions.time ? ' - ' + hr + ':' + min + ' ' + meridiem : ''); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment