mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-30 15:38:11 +08:00
@@ -1204,7 +1204,7 @@ impl MessageHandler<DocumentMessage, (&InputPreprocessorMessageHandler, &FontCac
|
|||||||
.into(),
|
.into(),
|
||||||
);
|
);
|
||||||
|
|
||||||
let mouse = mouse.map_or(ipp.mouse.position, |pos| pos.into());
|
let mouse = mouse.map_or(ipp.viewport_bounds.center(), |pos| pos.into());
|
||||||
let transform = DAffine2::from_translation(mouse - ipp.viewport_bounds.top_left).to_cols_array();
|
let transform = DAffine2::from_translation(mouse - ipp.viewport_bounds.top_left).to_cols_array();
|
||||||
responses.push_back(DocumentOperation::SetLayerTransformInViewport { path, transform }.into());
|
responses.push_back(DocumentOperation::SetLayerTransformInViewport { path, transform }.into());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -176,8 +176,13 @@ impl PropertyHolder for MenuBarMessageHandler {
|
|||||||
action: MenuEntry::create_action(|_| PortfolioMessage::Copy { clipboard: Clipboard::Device }.into()),
|
action: MenuEntry::create_action(|_| PortfolioMessage::Copy { clipboard: Clipboard::Device }.into()),
|
||||||
..MenuEntry::default()
|
..MenuEntry::default()
|
||||||
},
|
},
|
||||||
// TODO: Fix this
|
MenuEntry {
|
||||||
// { label: "Paste", icon: "Paste", shortcut: ["KeyControl", "KeyV"], action: async (): Promise<void> => editor.instance.paste() },
|
label: "Paste".into(),
|
||||||
|
icon: Some("Paste".into()),
|
||||||
|
shortcut: Some(vec![Key::KeyControl, Key::KeyV]),
|
||||||
|
action: MenuEntry::create_action(|_| FrontendMessage::TriggerPaste.into()),
|
||||||
|
..MenuEntry::default()
|
||||||
|
},
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ pub enum FrontendMessage {
|
|||||||
TriggerFontLoad { font: Font, is_default: bool },
|
TriggerFontLoad { font: Font, is_default: bool },
|
||||||
TriggerIndexedDbRemoveDocument { document_id: u64 },
|
TriggerIndexedDbRemoveDocument { document_id: u64 },
|
||||||
TriggerIndexedDbWriteDocument { document: String, details: FrontendDocumentDetails, version: String },
|
TriggerIndexedDbWriteDocument { document: String, details: FrontendDocumentDetails, version: String },
|
||||||
|
TriggerPaste,
|
||||||
TriggerRasterDownload { document: String, name: String, mime: String, size: (f64, f64) },
|
TriggerRasterDownload { document: String, name: String, mime: String, size: (f64, f64) },
|
||||||
TriggerTextCommit,
|
TriggerTextCommit,
|
||||||
TriggerTextCopy { copy_text: String },
|
TriggerTextCopy { copy_text: String },
|
||||||
|
|||||||
@@ -3,10 +3,10 @@
|
|||||||
|
|
||||||
<div class="unsupported-modal-backdrop" v-if="apiUnsupported" ref="unsupported">
|
<div class="unsupported-modal-backdrop" v-if="apiUnsupported" ref="unsupported">
|
||||||
<LayoutCol class="unsupported-modal">
|
<LayoutCol class="unsupported-modal">
|
||||||
<h2>Your browser currently doesn't support Graphite</h2>
|
<h2>This browser currently doesn't support Graphite</h2>
|
||||||
<p>Unfortunately, some features won't work properly. Please upgrade to a modern browser such as Firefox, Chrome, Edge, or Safari version 15 or later.</p>
|
<p>Unfortunately, some features won't work properly. Please upgrade to a modern browser such as Firefox, Chrome, Edge, or Safari version 15 or later.</p>
|
||||||
<p>
|
<p>
|
||||||
Your browser is missing support for the
|
This browser is missing support for the
|
||||||
<a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt64Array#browser_compatibility" target="_blank"><code>BigInt64Array</code></a> JavaScript
|
<a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt64Array#browser_compatibility" target="_blank"><code>BigInt64Array</code></a> JavaScript
|
||||||
API which is required for using the editor. However, you can still explore the user interface.
|
API which is required for using the editor. However, you can still explore the user interface.
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ import { DialogState } from "@/state-providers/dialog";
|
|||||||
import { FullscreenState } from "@/state-providers/fullscreen";
|
import { FullscreenState } from "@/state-providers/fullscreen";
|
||||||
import { PortfolioState } from "@/state-providers/portfolio";
|
import { PortfolioState } from "@/state-providers/portfolio";
|
||||||
import { makeKeyboardModifiersBitfield, textInputCleanup, getLatinKey } from "@/utility-functions/keyboard-entry";
|
import { makeKeyboardModifiersBitfield, textInputCleanup, getLatinKey } from "@/utility-functions/keyboard-entry";
|
||||||
|
import { stripIndents } from "@/utility-functions/strip-indents";
|
||||||
import { Editor } from "@/wasm-communication/editor";
|
import { Editor } from "@/wasm-communication/editor";
|
||||||
|
import { TriggerPaste } from "@/wasm-communication/messages";
|
||||||
|
|
||||||
type EventName = keyof HTMLElementEventMap | keyof WindowEventHandlersEventMap | "modifyinputfield";
|
type EventName = keyof HTMLElementEventMap | keyof WindowEventHandlersEventMap | "modifyinputfield";
|
||||||
type EventListenerTarget = {
|
type EventListenerTarget = {
|
||||||
@@ -14,6 +16,16 @@ export function createInputManager(editor: Editor, container: HTMLElement, dialo
|
|||||||
const app = window.document.querySelector("[data-app]") as HTMLElement | undefined;
|
const app = window.document.querySelector("[data-app]") as HTMLElement | undefined;
|
||||||
app?.focus();
|
app?.focus();
|
||||||
|
|
||||||
|
let viewportPointerInteractionOngoing = false;
|
||||||
|
let textInput = undefined as undefined | HTMLDivElement;
|
||||||
|
let canvasFocused = true;
|
||||||
|
|
||||||
|
function blurApp(): void {
|
||||||
|
canvasFocused = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event listeners
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const listeners: { target: EventListenerTarget; eventName: EventName; action: (event: any) => void; options?: boolean | AddEventListenerOptions }[] = [
|
const listeners: { target: EventListenerTarget; eventName: EventName; action: (event: any) => void; options?: boolean | AddEventListenerOptions }[] = [
|
||||||
{ target: window, eventName: "resize", action: (): void => onWindowResize(container) },
|
{ target: window, eventName: "resize", action: (): void => onWindowResize(container) },
|
||||||
@@ -37,12 +49,15 @@ export function createInputManager(editor: Editor, container: HTMLElement, dialo
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
let viewportPointerInteractionOngoing = false;
|
// Event bindings
|
||||||
let textInput = undefined as undefined | HTMLDivElement;
|
|
||||||
let canvasFocused = true;
|
|
||||||
|
|
||||||
function blurApp(): void {
|
function bindListeners(): void {
|
||||||
canvasFocused = false;
|
// Add event bindings for the lifetime of the application
|
||||||
|
listeners.forEach(({ target, eventName, action, options }) => target.addEventListener(eventName, action, options));
|
||||||
|
}
|
||||||
|
function unbindListeners(): void {
|
||||||
|
// Remove event bindings after the lifetime of the application (or on hot-module replacement during development)
|
||||||
|
listeners.forEach(({ target, eventName, action, options }) => target.removeEventListener(eventName, action, options));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keyboard events
|
// Keyboard events
|
||||||
@@ -259,26 +274,80 @@ export function createInputManager(editor: Editor, container: HTMLElement, dialo
|
|||||||
file.arrayBuffer().then((buffer): void => {
|
file.arrayBuffer().then((buffer): void => {
|
||||||
const u8Array = new Uint8Array(buffer);
|
const u8Array = new Uint8Array(buffer);
|
||||||
|
|
||||||
editor.instance.paste_image(file.type, u8Array, undefined, undefined);
|
editor.instance.paste_image(file.type, u8Array);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function targetIsTextField(target: EventTarget | HTMLElement | null): boolean {
|
// Frontend message subscriptions
|
||||||
return target instanceof HTMLElement && (target.nodeName === "INPUT" || target.nodeName === "TEXTAREA" || target.isContentEditable);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Event bindings
|
editor.subscriptions.subscribeJsMessage(TriggerPaste, async () => {
|
||||||
|
// In the try block, attempt to read from the Clipboard API, which may not have permission and may not be supported in all browsers
|
||||||
|
// In the catch block, explain to the user why the paste failed and how to fix or work around the problem
|
||||||
|
try {
|
||||||
|
// Attempt to check if the clipboard permission is denied, and throw an error if that is the case
|
||||||
|
// In Firefox, the `clipboard-read` permission isn't supported, so attemping to query it throws an error
|
||||||
|
// In Safari, the entire Permissions API isn't supported, so the query never occurs and this block is skipped without an error and we assume we might have permission
|
||||||
|
const clipboardRead = "clipboard-read" as PermissionName;
|
||||||
|
const permission = await navigator.permissions?.query({ name: clipboardRead });
|
||||||
|
if (permission?.state === "denied") throw new Error("Permission denied");
|
||||||
|
|
||||||
function bindListeners(): void {
|
// Read the clipboard contents if the Clipboard API is available
|
||||||
// Add event bindings for the lifetime of the application
|
const clipboardItems = await navigator.clipboard.read();
|
||||||
listeners.forEach(({ target, eventName, action, options }) => target.addEventListener(eventName, action, options));
|
if (!clipboardItems) throw new Error("Clipboard API unsupported");
|
||||||
}
|
|
||||||
function unbindListeners(): void {
|
// Read any layer data or images from the clipboard
|
||||||
// Remove event bindings after the lifetime of the application (or on hot-module replacement during development)
|
Array.from(clipboardItems).forEach(async (item) => {
|
||||||
listeners.forEach(({ target, eventName, action, options }) => target.removeEventListener(eventName, action, options));
|
// Read plain text and, if it is a layer, pass it to the editor
|
||||||
}
|
if (item.types.includes("text/plain")) {
|
||||||
|
const blob = await item.getType("text/plain");
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (): void => {
|
||||||
|
const text = reader.result as string;
|
||||||
|
|
||||||
|
if (text.startsWith("graphite/layer: ")) {
|
||||||
|
editor.instance.paste_serialized_data(text.substring(16, text.length));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsText(blob);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read an image from the clipboard and pass it to the editor to be loaded
|
||||||
|
const imageType = item.types.find((type) => type.startsWith("image/"));
|
||||||
|
if (imageType) {
|
||||||
|
const blob = await item.getType(imageType);
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (): void => {
|
||||||
|
const u8Array = new Uint8Array(reader.result as ArrayBuffer);
|
||||||
|
|
||||||
|
editor.instance.paste_image(imageType, u8Array);
|
||||||
|
};
|
||||||
|
reader.readAsArrayBuffer(blob);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
const unsupported = stripIndents`
|
||||||
|
This browser does not support reading from the clipboard.
|
||||||
|
Use the keyboard shortcut to paste instead.
|
||||||
|
`;
|
||||||
|
const denied = stripIndents`
|
||||||
|
The browser's clipboard permission has been denied.
|
||||||
|
|
||||||
|
Open the browser's website settings (usually accessible
|
||||||
|
just left of the URL) to allow this permission.
|
||||||
|
`;
|
||||||
|
|
||||||
|
const matchMessage = {
|
||||||
|
"clipboard-read": unsupported,
|
||||||
|
"Clipboard API unsupported": unsupported,
|
||||||
|
"Permission denied": denied,
|
||||||
|
};
|
||||||
|
const message = Object.entries(matchMessage).find(([key]) => String(err).includes(key))?.[1] || String(err);
|
||||||
|
|
||||||
|
editor.instance.error_dialog("Cannot access clipboard", message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Initialization
|
// Initialization
|
||||||
|
|
||||||
@@ -290,3 +359,7 @@ export function createInputManager(editor: Editor, container: HTMLElement, dialo
|
|||||||
// Return the destructor
|
// Return the destructor
|
||||||
return unbindListeners;
|
return unbindListeners;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function targetIsTextField(target: EventTarget | HTMLElement | null): boolean {
|
||||||
|
return target instanceof HTMLElement && (target.nodeName === "INPUT" || target.nodeName === "TEXTAREA" || target.isContentEditable);
|
||||||
|
}
|
||||||
|
|||||||
@@ -198,6 +198,8 @@ export class TriggerFileDownload extends JsMessage {
|
|||||||
|
|
||||||
export class TriggerFileUpload extends JsMessage {}
|
export class TriggerFileUpload extends JsMessage {}
|
||||||
|
|
||||||
|
export class TriggerPaste extends JsMessage {}
|
||||||
|
|
||||||
export class TriggerRasterDownload extends JsMessage {
|
export class TriggerRasterDownload extends JsMessage {
|
||||||
readonly document!: string;
|
readonly document!: string;
|
||||||
|
|
||||||
@@ -584,6 +586,7 @@ export const messageMakers: Record<string, MessageMaker> = {
|
|||||||
TriggerIndexedDbRemoveDocument,
|
TriggerIndexedDbRemoveDocument,
|
||||||
TriggerFontLoad,
|
TriggerFontLoad,
|
||||||
TriggerIndexedDbWriteDocument,
|
TriggerIndexedDbWriteDocument,
|
||||||
|
TriggerPaste,
|
||||||
TriggerRasterDownload,
|
TriggerRasterDownload,
|
||||||
TriggerTextCommit,
|
TriggerTextCommit,
|
||||||
TriggerTextCopy,
|
TriggerTextCopy,
|
||||||
|
|||||||
Reference in New Issue
Block a user