diff --git a/editor/src/document/menu_bar_message_handler.rs b/editor/src/document/menu_bar_message_handler.rs index 46826157a..26db8a818 100644 --- a/editor/src/document/menu_bar_message_handler.rs +++ b/editor/src/document/menu_bar_message_handler.rs @@ -129,6 +129,7 @@ impl PropertyHolder for MenuBarMessageHandler { MenuEntry { label: "Import…".into(), shortcut: Some(vec![Key::KeyControl, Key::KeyI]), + action: MenuEntry::create_action(|_| PortfolioMessage::Import.into()), ..MenuEntry::default() }, MenuEntry { diff --git a/editor/src/document/portfolio_message.rs b/editor/src/document/portfolio_message.rs index d51268d86..6f60271b8 100644 --- a/editor/src/document/portfolio_message.rs +++ b/editor/src/document/portfolio_message.rs @@ -45,6 +45,7 @@ pub enum PortfolioMessage { data: Vec, is_default: bool, }, + Import, LoadFont { font: Font, is_default: bool, diff --git a/editor/src/document/portfolio_message_handler.rs b/editor/src/document/portfolio_message_handler.rs index f21b63607..fe92b58f3 100644 --- a/editor/src/document/portfolio_message_handler.rs +++ b/editor/src/document/portfolio_message_handler.rs @@ -267,6 +267,12 @@ impl MessageHandler for Port responses.push_back(DocumentMessage::RenderDocument.into()); } } + Import => { + // This portfolio message wraps the frontend message so it can be listed as an action, which isn't possible for frontend messages + if self.active_document().is_some() { + responses.push_back(FrontendMessage::TriggerImport.into()); + } + } LoadFont { font, is_default } => { if !self.font_cache.loaded_font(&font) { responses.push_front(FrontendMessage::TriggerFontLoad { font, is_default }.into()); @@ -292,7 +298,8 @@ impl MessageHandler for Port } } OpenDocument => { - responses.push_back(FrontendMessage::TriggerFileUpload.into()); + // This portfolio message wraps the frontend message so it can be listed as an action, which isn't possible for frontend messages + responses.push_back(FrontendMessage::TriggerOpenDocument.into()); } OpenDocumentFile { document_name, @@ -493,10 +500,12 @@ impl MessageHandler for Port let mut common = actions!(PortfolioMessageDiscriminant; CloseActiveDocumentWithConfirmation, CloseAllDocuments, + Import, NextDocument, - PrevDocument, - PasteIntoFolder, + OpenDocument, Paste, + PasteIntoFolder, + PrevDocument, ); if let Some(document) = self.active_document() { diff --git a/editor/src/frontend/frontend_message.rs b/editor/src/frontend/frontend_message.rs index eee20af82..1d3866a05 100644 --- a/editor/src/frontend/frontend_message.rs +++ b/editor/src/frontend/frontend_message.rs @@ -24,10 +24,11 @@ pub enum FrontendMessage { // Trigger prefix: cause a browser API to do something TriggerAboutGraphiteLocalizedCommitDate { commit_date: String }, TriggerFileDownload { document: String, name: String }, - TriggerFileUpload, TriggerFontLoad { font: Font, is_default: bool }, + TriggerImport, TriggerIndexedDbRemoveDocument { document_id: u64 }, TriggerIndexedDbWriteDocument { document: String, details: FrontendDocumentDetails, version: String }, + TriggerOpenDocument, TriggerPaste, TriggerRasterDownload { document: String, name: String, mime: String, size: (f64, f64) }, TriggerRefreshBoundsOfViewports, diff --git a/editor/src/input/input_mapper.rs b/editor/src/input/input_mapper.rs index 874f2b171..b1182e833 100644 --- a/editor/src/input/input_mapper.rs +++ b/editor/src/input/input_mapper.rs @@ -145,8 +145,6 @@ impl Default for Mapping { entry! {action=ToolMessage::ResetColors, key_down=KeyX, modifiers=[KeyShift, KeyControl]}, entry! {action=ToolMessage::SwapColors, key_down=KeyX, modifiers=[KeyShift]}, entry! {action=ToolMessage::SelectRandomPrimaryColor, key_down=KeyC, modifiers=[KeyAlt]}, - // Editor Actions - entry! {action=FrontendMessage::TriggerFileUpload, key_down=KeyO, modifiers=[KeyControl]}, // Document actions entry! {action=DocumentMessage::Redo, key_down=KeyZ, modifiers=[KeyControl, KeyShift]}, entry! {action=DocumentMessage::Undo, key_down=KeyZ, modifiers=[KeyControl]}, @@ -187,6 +185,8 @@ impl Default for Mapping { entry! {action=MovementMessage::TranslateCanvasByViewportFraction { delta: DVec2::new(0., 1.) }, key_down=KeyPageUp}, entry! {action=MovementMessage::TranslateCanvasByViewportFraction { delta: DVec2::new(0., -1.) }, key_down=KeyPageDown}, // Portfolio actions + entry! {action=PortfolioMessage::OpenDocument, key_down=KeyO, modifiers=[KeyControl]}, + entry! {action=PortfolioMessage::Import, key_down=KeyI, modifiers=[KeyControl]}, entry! {action=DialogMessage::RequestNewDocumentDialog, key_down=KeyN, modifiers=[KeyControl]}, entry! {action=PortfolioMessage::NextDocument, key_down=KeyTab, modifiers=[KeyControl]}, entry! {action=PortfolioMessage::PrevDocument, key_down=KeyTab, modifiers=[KeyControl, KeyShift]}, diff --git a/frontend/src/components/window/workspace/Panel.vue b/frontend/src/components/window/workspace/Panel.vue index 1d2c2384a..ec4f2b1df 100644 --- a/frontend/src/components/window/workspace/Panel.vue +++ b/frontend/src/components/window/workspace/Panel.vue @@ -255,7 +255,7 @@ export default defineComponent({ this.editor.instance.new_document_dialog(); }, openDocument() { - this.editor.instance.open_file_upload(); + this.editor.instance.document_open(); }, }, components: { diff --git a/frontend/src/state-providers/portfolio.ts b/frontend/src/state-providers/portfolio.ts index 1cc5e77be..44c13429f 100644 --- a/frontend/src/state-providers/portfolio.ts +++ b/frontend/src/state-providers/portfolio.ts @@ -3,7 +3,7 @@ import { reactive, readonly } from "vue"; import { download, downloadBlob, upload } from "@/utility-functions/files"; import { Editor } from "@/wasm-communication/editor"; -import { TriggerFileDownload, TriggerRasterDownload, FrontendDocumentDetails, TriggerFileUpload, UpdateActiveDocument, UpdateOpenDocumentsList } from "@/wasm-communication/messages"; +import { TriggerFileDownload, TriggerRasterDownload, FrontendDocumentDetails, TriggerOpenDocument, TriggerImport, UpdateActiveDocument, UpdateOpenDocumentsList } from "@/wasm-communication/messages"; // eslint-disable-next-line @typescript-eslint/explicit-function-return-type export function createPortfolioState(editor: Editor) { @@ -22,11 +22,15 @@ export function createPortfolioState(editor: Editor) { const activeId = state.documents.findIndex((doc) => doc.id === updateActiveDocument.document_id); state.activeDocumentIndex = activeId; }); - editor.subscriptions.subscribeJsMessage(TriggerFileUpload, async () => { + editor.subscriptions.subscribeJsMessage(TriggerOpenDocument, async () => { const extension = editor.instance.file_save_suffix(); - const data = await upload(extension); + const data = await upload(extension, "text"); editor.instance.open_document_file(data.filename, data.content); }); + editor.subscriptions.subscribeJsMessage(TriggerImport, async () => { + const data = await upload("image/*", "data"); + editor.instance.paste_image(data.type, Uint8Array.from(data.content)); + }); editor.subscriptions.subscribeJsMessage(TriggerFileDownload, (triggerFileDownload) => { download(triggerFileDownload.name, triggerFileDownload.document); }); diff --git a/frontend/src/utility-functions/files.ts b/frontend/src/utility-functions/files.ts index 270f45fcb..18b51d108 100644 --- a/frontend/src/utility-functions/files.ts +++ b/frontend/src/utility-functions/files.ts @@ -18,22 +18,24 @@ export function download(filename: string, fileData: string): void { URL.revokeObjectURL(url); } -export async function upload(acceptedEextensions: string): Promise<{ filename: string; content: string }> { - return new Promise<{ filename: string; content: string }>((resolve, _) => { +export async function upload(acceptedExtensions: string, textOrData: T): Promise> { + return new Promise>((resolve, _) => { const element = document.createElement("input"); element.type = "file"; element.style.display = "none"; - element.accept = acceptedEextensions; + element.accept = acceptedExtensions; element.addEventListener( "change", async () => { if (element.files?.length) { const file = element.files[0]; - const filename = file.name; - const content = await file.text(); - resolve({ filename, content }); + const filename = file.name; + const type = file.type; + const content = (textOrData === "text" ? await file.text() : new Uint8Array(await file.arrayBuffer())) as UploadResultType; + + resolve({ filename, type, content }); } }, { capture: false, once: true } @@ -44,3 +46,5 @@ export async function upload(acceptedEextensions: string): Promise<{ filename: s // Once `element` goes out of scope, it has no references so it gets garbage collected along with its event listener, so `removeEventListener` is not needed }); } +export type UploadResult = { filename: string; type: string; content: UploadResultType }; +type UploadResultType = T extends "text" ? string : T extends "data" ? Uint8Array : never; diff --git a/frontend/src/wasm-communication/messages.ts b/frontend/src/wasm-communication/messages.ts index 535da44dc..78f2a4c6d 100644 --- a/frontend/src/wasm-communication/messages.ts +++ b/frontend/src/wasm-communication/messages.ts @@ -206,7 +206,9 @@ export class TriggerFileDownload extends JsMessage { readonly name!: string; } -export class TriggerFileUpload extends JsMessage {} +export class TriggerOpenDocument extends JsMessage {} + +export class TriggerImport extends JsMessage {} export class TriggerPaste extends JsMessage {} @@ -808,23 +810,22 @@ type MessageMaker = typeof JsMessage | JSMessageFactory; export const messageMakers: Record = { DisplayDialog, - DisplayDialogPanic, - UpdateDocumentLayerTreeStructure: newUpdateDocumentLayerTreeStructure, - DisplayEditableTextbox, - UpdateImageData, - DisplayRemoveEditableTextbox, DisplayDialogDismiss, + DisplayDialogPanic, + DisplayEditableTextbox, + DisplayRemoveEditableTextbox, + TriggerAboutGraphiteLocalizedCommitDate, + TriggerOpenDocument, TriggerFileDownload, - TriggerFileUpload, - TriggerIndexedDbRemoveDocument, TriggerFontLoad, + TriggerImport, + TriggerIndexedDbRemoveDocument, TriggerIndexedDbWriteDocument, TriggerPaste, TriggerRasterDownload, TriggerRefreshBoundsOfViewports, TriggerTextCommit, TriggerTextCopy, - TriggerAboutGraphiteLocalizedCommitDate, TriggerViewportResize, TriggerVisitLink, UpdateActiveDocument, @@ -832,21 +833,23 @@ export const messageMakers: Record = { UpdateDocumentArtboards, UpdateDocumentArtwork, UpdateDocumentBarLayout, - UpdateToolShelfLayout, UpdateDocumentLayerDetails, + UpdateDocumentLayerTreeStructure: newUpdateDocumentLayerTreeStructure, + UpdateDocumentModeLayout, UpdateDocumentOverlays, UpdateDocumentRulers, UpdateDocumentScrollbars, + UpdateImageData, UpdateInputHints, + UpdateLayerTreeOptionsLayout, + UpdateMenuBarLayout, UpdateMouseCursor, UpdateNodeGraphVisibility, UpdateOpenDocumentsList, UpdatePropertyPanelOptionsLayout, UpdatePropertyPanelSectionsLayout, - UpdateLayerTreeOptionsLayout, - UpdateDocumentModeLayout, UpdateToolOptionsLayout, + UpdateToolShelfLayout, UpdateWorkingColorsLayout, - UpdateMenuBarLayout, } as const; export type JsMessageType = keyof typeof messageMakers; diff --git a/frontend/wasm/src/editor_api.rs b/frontend/wasm/src/editor_api.rs index 4b12648fd..5f69a7c36 100644 --- a/frontend/wasm/src/editor_api.rs +++ b/frontend/wasm/src/editor_api.rs @@ -154,8 +154,8 @@ impl JsEditorHandle { self.dispatch(message); } - pub fn open_file_upload(&self) { - let message = FrontendMessage::TriggerFileUpload; + pub fn document_open(&self) { + let message = PortfolioMessage::OpenDocument; self.dispatch(message); }