gecko/addon-sdk/source/lib/sdk/clipboard.js

338 lines
9.6 KiB
JavaScript

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
module.metadata = {
"stability": "stable",
"engines": {
// TODO Fennec Support 789757
"Firefox": "*",
"SeaMonkey": "*",
"Thunderbird": "*"
}
};
const { Cc, Ci } = require("chrome");
const { DataURL } = require("./url");
const apiUtils = require("./deprecated/api-utils");
/*
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);
exports.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 = {};
try {
let input = Cc["@mozilla.org/io/string-input-stream;1"].
createInstance(Ci.nsIStringInputStream);
input.setData(image, image.length);
imageTools.decodeImageData(input, flavor, container);
}
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.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;
};
exports.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(exports, "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;
}