Skip to content

Commit

Permalink
Use the Async Clipboard API
Browse files Browse the repository at this point in the history
Copy/paste real image data, both from keyboard shortcuts AND from the menus now!
  • Loading branch information
1j01 committed Sep 14, 2019
1 parent 49f969b commit ad37213
Show file tree
Hide file tree
Showing 5 changed files with 157 additions and 48 deletions.
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,14 +96,14 @@ I want to bring good old paint into the modern era.
A lot of stuff isn't done yet.
See: [the big long todo list.](TODO.md)

Clipboard support is somewhat limited (in the web app).
You can copy with <kbd>Ctrl+C</kbd>, cut with <kbd>Ctrl+X</kbd>, and paste with <kbd>Ctrl+V</kbd>,
Full clipboard support in the web app requires a browser supporting the [Async Clipboard API w/ Images](https://developers.google.com/web/updates/2019/07/image-support-for-async-clipboard), namely Chrome 76+ at the time of writing.

In other browsers you can still can copy with <kbd>Ctrl+C</kbd>, cut with <kbd>Ctrl+X</kbd>, and paste with <kbd>Ctrl+V</kbd>,
but data copied from JS Paint can only be pasted into other instances of JS Paint.
There's no way for web apps to properly copy image data to the clipboard yet.
"[Support programmatical copying of images to clipboard](https://bugs.chromium.org/p/chromium/issues/detail?id=150835)"
is currently the top starred issue of chromium.
External images can be pasted in.

For full clipboard support, you need to install the [desktop app](#desktop-app).
There's also a [desktop app](#desktop-app) version you can install that has full clipboard support.
In which you can also set the wallpaper.


## Extended Editing
Expand All @@ -129,7 +129,7 @@ I want to make JS Paint to be able to edit...
and I've made it try not to save over the original SVG.
That's pretty decent SVG support for a 100% raster image editor.
* [Text files][TXT] (definitely just kidding maybe)
* Tesselating patterns, and textures on 3D models;
* Tessellating patterns, and textures on 3D models;
that might be a pipe dream, but [then again...](https://github.com/1j01/pipes) [hm...](https://github.com/1j01/mopaint)


Expand Down
6 changes: 3 additions & 3 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
* "To use a picture as the desktop background":
add a third step? It's not quite that easy (at least in the browser)
* "To create custom colors": way too OS-specific
(unless I'm gonna emulate the color selection dialogue)
(unless I'm gonna emulate the color selection dialog)
* "To enlarge the size of the viewing area" (`paint_enlarge_area.htm`):
jspaint currently allows you to draw while "Viewing the Bitmap"
* "To zoom in or out of a picture", "To type and format text":
Expand All @@ -32,8 +32,8 @@

### Visual

* Warning sign for "Save changes to X?" dialogue
* Error symbol for error message dialogues
* Warning sign for "Save changes to X?" dialog
* Error symbol for error message dialogs
* 3D inset border for inputs - SVG `image-border`?
* The window close button uses text; font rendering is not consistent
* The progress bar (Rendering GIF) is left native
Expand Down
58 changes: 29 additions & 29 deletions src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -353,37 +353,37 @@ $G.on("cut copy paste", function(e){

if(e.type === "copy" || e.type === "cut"){
if(selection && selection.canvas){
if(window.require && window.process){
const {clipboard, nativeImage} = require('electron');
selection.canvas.toBlob(function(blob){
sanity_check_blob(blob, function(){
blob_to_buffer(blob, function(err, buffer){
if(err){
return show_error_message("Failed to copy to clipboard! (Technically, failed to convert a Blob to a Buffer.)", err);
}
var native_image = nativeImage.createFromBuffer(buffer);
clipboard.writeImage(native_image);
try {
if (e.type === "cut") {
edit_cut();
} else {
edit_copy();
}
} catch(e) {
if(window.require && window.process){
// TODO: remove special electron handling for clipboard stuff if I can upgrade to a version that supports the async clipboard API with images
const {clipboard, nativeImage} = require('electron');
selection.canvas.toBlob(function(blob){
sanity_check_blob(blob, function(){
blob_to_buffer(blob, function(err, buffer){
if(err){
return show_error_message("Failed to copy to clipboard! (Technically, failed to convert a Blob to a Buffer.)", err);
}
var native_image = nativeImage.createFromBuffer(buffer);
clipboard.writeImage(native_image);
});
});
});
});
} else {
var data_url = selection.canvas.toDataURL();
cd.setData("text/x-data-uri; type=image/png", data_url);
cd.setData("text/uri-list", data_url);
cd.setData("URL", data_url);
// var svg = `
// <svg
// xmlns="http://www.w3.org/2000/svg" version="1.1"
// viewBox="0 0 ${selection.canvas.width} ${selection.canvas.height}" preserveAspectRatio="xMidYMid slice">
// <image xlink:href="${data_url}" x="0" y="0" height="${selection.canvas.height}" width="${selection.canvas.width}"/>
// </svg>
// `;
// cd.setData("image/svg+xml", svg);
// cd.setData("text/html", svg);
}
if(e.type === "cut"){
selection.destroy();
selection = null;
} else {
// works only for pasting within a jspaint instance
var data_url = selection.canvas.toDataURL();
cd.setData("text/x-data-uri; type=image/png", data_url);
cd.setData("text/uri-list", data_url);
cd.setData("URL", data_url);
}
if(e.type === "cut"){
delete_selection();
}
}
}
}else if(e.type === "paste"){
Expand Down
110 changes: 109 additions & 1 deletion src/functions.js
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ function show_resource_load_error_message(){
// NOTE: apparently distinguishing cross-origin errors is disallowed
var $w = $FormWindow().title("Error").addClass("dialogue-window");
$w.$main.html(
"<p>Failed to load image.</p>" +
"<p>Failed to load image from URL.</p>" +
"<p>Make sure to use an image host that supports " +
"<a href='https://en.wikipedia.org/wiki/Cross-origin_resource_sharing'>Cross-Origin Resource Sharing</a>" +
", such as <a href='https://imgur.com/'>Imgur</a>."
Expand Down Expand Up @@ -322,6 +322,7 @@ function paste_image_from_file(file){
var blob_url = URL.createObjectURL(file);
// paste_image_from_URI(blob_url);
load_image_from_URI(blob_url, function(err, img){
// TODO: this shouldn't really have the CORS error message, if it's from a blob URI
if(err){ return show_resource_load_error_message(); }
paste(img);
console.log("revokeObjectURL", blob_url);
Expand Down Expand Up @@ -610,6 +611,113 @@ function select_all(){
selection.instantiate();
}

const browserRecommendationForClipboardAccess = "Try using Chrome 76+";
async function edit_copy(execCommandFallback){
// TODO: DRY
if (!navigator.clipboard) {
if (execCommandFallback) {
if (document.queryCommandEnabled("copy")) { // not a reliable source for whether it'll work
document.execCommand("copy");
show_error_message("That copy probably didn't work. " + browserRecommendationForClipboardAccess);
} else {
show_error_message("Can't copy to the Clipboard. " + browserRecommendationForClipboardAccess);
}
} else {
throw new Error("Async Clipboard API not supported by this browser. " + browserRecommendationForClipboardAccess);
}
return;
}
if (!navigator.clipboard) {
show_error_message("The Async Clipboard API not supported by this browser. " + browserRecommendationForClipboardAccess);
}
// TODO: handle copying text (textarea or otherwise) w/ navigator.clipboard.writeText

selection.canvas.toBlob(function(blob) {
sanity_check_blob(blob, function(){
navigator.clipboard.write([
new ClipboardItem(Object.defineProperty({}, blob.type, {
value: blob,
enumerable: true,
}))
]).then(function(){
console.log("Copied image to the clipboard");
}, function(error){
show_error_message();
});
});
});
}
function edit_cut(execCommandFallback){
if (!navigator.clipboard) {
if (execCommandFallback) {
if (document.queryCommandEnabled("cut")) { // not a reliable source for whether it'll work
document.execCommand("cut");
show_error_message("That cut probably didn't work. " + browserRecommendationForClipboardAccess);
} else {
show_error_message("Can't copy to the Clipboard. " + browserRecommendationForClipboardAccess);
}
} else {
throw new Error("Async Clipboard API not supported by this browser. " + browserRecommendationForClipboardAccess);
}
return;
}
if (!navigator.clipboard) {
show_error_message("The Async Clipboard API not supported by this browser. " + browserRecommendationForClipboardAccess);
}
edit_copy();
delete_selection();
}
async function edit_paste(execCommandFallback){
if (!navigator.clipboard) {
if (execCommandFallback) {
if (document.queryCommandEnabled("paste")) { // not a reliable source for whether it'll work
document.execCommand("paste");
show_error_message("That paste probably didn't work. " + browserRecommendationForClipboardAccess);
} else {
show_error_message("Can't paste from the Clipboard. " + browserRecommendationForClipboardAccess);
}
} else {
throw new Error("Async Clipboard API not supported by this browser. " + browserRecommendationForClipboardAccess);
}
return;
}
if (!navigator.clipboard) {
show_error_message("The Async Clipboard API not supported by this browser. " + browserRecommendationForClipboardAccess);
}
try {
const clipboardItems = await navigator.clipboard.read();
console.log(clipboardItems);
const blob = await clipboardItems[0].getType("image/png");
paste_image_from_file(blob);
console.log("Image pasted.");
} catch(error) {
if (error.name === "NotFoundError") {
// TODO: support pasting text into textarea like with the text tool
try {
const clipboardText = await navigator.clipboard.readText();
if(
document.activeElement instanceof HTMLInputElement ||
document.activeElement instanceof HTMLTextAreaElement
){
document.execCommand("InsertText", false, clipboardText);
} else if(clipboardText) { // TODO: valid URL checking!
// and like share with the other thing that does this, does text/uri parsing
load_image_from_URI(clipboardText, function(err, img){
if(err){ return show_resource_load_error_message(); }
paste(img);
});
} else {
show_error_message("PNG image data not found on the Clipboard. (Nor a URL pointing to an image.)");
}
} catch(error) {
show_error_message("Failed to read from the Clipboard.", error);
}
} else {
show_error_message("Failed to read from the Clipboard.", error);
}
}
}

function image_invert(){
apply_image_transformation(function(original_canvas, original_ctx, new_canvas, new_ctx){
var id = original_ctx.getImageData(0, 0, original_canvas.width, original_canvas.height);
Expand Down
17 changes: 9 additions & 8 deletions src/menus.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,34 +131,35 @@ var menus = {
item: "Cu&t",
shortcut: "Ctrl+X",
enabled: function(){
// @TODO disable if no selection (image or text)
return is_electron;
// support cutting selected text with this menu item as well (e.g. in the text tool text box)
return !!selection;
},
action: function(){
document.execCommand("cut");
edit_cut(true);
},
description: "Cuts the selection and puts it on the Clipboard.",
},
{
item: "&Copy",
shortcut: "Ctrl+C",
enabled: function(){
// @TODO disable if no selection (image or text)
return is_electron;
// support copying selected text with this menu item as well (e.g. in the text tool text box)
return !!selection;
},
action: function(){
document.execCommand("copy");
edit_copy(true);
},
description: "Copies the selection and puts it on the Clipboard.",
},
{
item: "&Paste",
shortcut: "Ctrl+V",
enabled: function(){
return is_electron;
// TODO: disable if nothing in clipboard or wrong type (if we can access that)
return true;
},
action: function(){
document.execCommand("paste");
edit_paste(true);
},
description: "Inserts the contents of the Clipboard.",
},
Expand Down

0 comments on commit ad37213

Please sign in to comment.