Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 9 additions & 12 deletions desktop/wrapper/src/intercept_frontend_message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,25 @@ use graphite_editor::messages::layout::utility_types::layout_widget::LayoutTarge
use graphite_editor::messages::prelude::FrontendMessage;

use super::DesktopWrapperMessageDispatcher;
use super::messages::{DesktopFrontendMessage, FileFilter, OpenFileDialogContext, SaveFileDialogContext};
use super::messages::{DesktopFrontendMessage, OpenFileDialogContext, SaveFileDialogContext};

pub(super) fn intercept_frontend_message(dispatcher: &mut DesktopWrapperMessageDispatcher, message: FrontendMessage) -> Option<FrontendMessage> {
match message {
FrontendMessage::RenderOverlays { context } => {
dispatcher.respond(DesktopFrontendMessage::UpdateOverlays(context.take_scene()));
}
FrontendMessage::TriggerOpen => {
FrontendMessage::TriggerOpen { filters } => {
dispatcher.respond(DesktopFrontendMessage::OpenFileDialog {
title: "Open Document".to_string(),
filters: vec![],
filters,
multiple: true,
context: OpenFileDialogContext::Open,
});
}
FrontendMessage::TriggerImport => {
FrontendMessage::TriggerImport { filters } => {
dispatcher.respond(DesktopFrontendMessage::OpenFileDialog {
title: "Import File".to_string(),
filters: vec![],
filters,
multiple: false,
context: OpenFileDialogContext::Import,
});
Expand All @@ -31,32 +31,29 @@ pub(super) fn intercept_frontend_message(dispatcher: &mut DesktopWrapperMessageD
name,
path,
folder,
filters,
content,
} => {
let content = content.into_vec();
if let Some(path) = path {
dispatcher.respond(DesktopFrontendMessage::WriteFile { path, content });
} else {
let extension = std::path::Path::new(&name).extension().and_then(|extension| extension.to_str()).unwrap_or("*").to_string();
dispatcher.respond(DesktopFrontendMessage::SaveFileDialog {
title: "Save Document".to_string(),
default_filename: name,
default_folder: folder,
filters: vec![FileFilter {
name: "Graphite Document".to_string(),
extensions: vec![extension],
}],
filters,
context: SaveFileDialogContext::Document { document_id, content },
});
}
}
FrontendMessage::TriggerSaveFile { name, folder, content } => {
FrontendMessage::TriggerSaveFile { name, folder, filters, content } => {
let content = content.into_vec();
dispatcher.respond(DesktopFrontendMessage::SaveFileDialog {
title: "Save File".to_string(),
default_filename: name,
default_folder: folder,
filters: Vec::new(),
filters,
context: SaveFileDialogContext::File { content },
});
}
Expand Down
7 changes: 1 addition & 6 deletions desktop/wrapper/src/messages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::path::PathBuf;

pub(crate) use graphite_editor::messages::prelude::Message as EditorMessage;

pub use graphite_editor::messages::frontend::utility_types::{DocumentInfo, PersistedState};
pub use graphite_editor::messages::frontend::utility_types::{DocumentInfo, FileFilter, PersistedState};
pub use graphite_editor::messages::input_mapper::utility_types::input_keyboard::{Key, ModifierKeys};
pub use graphite_editor::messages::input_mapper::utility_types::input_mouse::{EditorMouseState as MouseState, EditorPosition as Position, MouseKeys};
pub use graphite_editor::messages::prelude::DocumentId;
Expand Down Expand Up @@ -100,11 +100,6 @@ pub enum DesktopWrapperMessage {
LoadThirdPartyLicenses { text: String },
}

pub struct FileFilter {
pub name: String,
pub extensions: Vec<String>,
}

#[derive(Clone, Copy)]
pub enum OpenFileDialogContext {
Open,
Expand Down
12 changes: 9 additions & 3 deletions editor/src/messages/frontend/frontend_message.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use super::IconName;
use super::utility_types::{MouseCursorIcon, PersistedState};
use crate::messages::app_window::app_window_message_handler::AppWindowPlatform;
use crate::messages::frontend::utility_types::{DocumentInfo, EyedropperPreviewImage, RasterizedImage};
use crate::messages::frontend::utility_types::{DocumentInfo, EyedropperPreviewImage, FileFilter, RasterizedImage};
use crate::messages::input_mapper::utility_types::misc::ActionShortcut;
use crate::messages::layout::utility_types::widget_prelude::*;
use crate::messages::portfolio::document::node_graph::utility_types::{
Expand Down Expand Up @@ -87,16 +87,24 @@ pub enum FrontendMessage {
commit_date: String,
},
TriggerDisplayThirdPartyLicensesDialog,
TriggerOpen {
filters: Vec<FileFilter>,
},
TriggerImport {
filters: Vec<FileFilter>,
},
TriggerSaveDocument {
document_id: DocumentId,
name: String,
path: Option<PathBuf>,
folder: Option<PathBuf>,
filters: Vec<FileFilter>,
content: serde_bytes::ByteBuf,
},
TriggerSaveFile {
name: String,
folder: Option<PathBuf>,
filters: Vec<FileFilter>,
content: serde_bytes::ByteBuf,
},
TriggerExportImage {
Expand Down Expand Up @@ -128,8 +136,6 @@ pub enum FrontendMessage {
},
TriggerOpenLaunchDocuments,
TriggerLoadPreferences,
TriggerOpen,
TriggerImport,
TriggerSavePreferences {
#[cfg_attr(feature = "wasm", tsify(type = "unknown"))]
preferences: PreferencesMessageHandler,
Expand Down
27 changes: 27 additions & 0 deletions editor/src/messages/frontend/utility_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,26 @@ impl FileType {
FileType::Svg => "image/svg+xml",
}
}

pub fn extension(self) -> &'static str {
match self {
FileType::Png => "png",
FileType::Jpg => "jpg",
FileType::Svg => "svg",
}
}

pub fn file_filter(self) -> FileFilter {
let name = match self {
FileType::Png => "PNG Image",
FileType::Jpg => "JPEG Image",
FileType::Svg => "SVG Image",
};
FileFilter {
name: name.into(),
extensions: vec![self.extension().into()],
}
}
}

#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
Expand Down Expand Up @@ -91,3 +111,10 @@ pub struct RasterizedImage {
pub height: u32,
pub pixels: serde_bytes::ByteBuf,
}

#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Clone, Debug, Default, Eq, PartialEq, Hash, serde::Serialize, serde::Deserialize)]
pub struct FileFilter {
pub name: String,
pub extensions: Vec<String>,
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use crate::consts::{
ASYMPTOTIC_EFFECT, BLEND_COUNT_PER_LAYER, COLOR_OVERLAY_GRAY, DEFAULT_DOCUMENT_NAME, FILE_EXTENSION, GDD_FILE_EXTENSION, LAYER_INDENT_OFFSET, NODE_CHAIN_WIDTH, SCALE_EFFECT, SCROLLBAR_SPACING,
VIEWPORT_ROTATE_SNAP_INTERVAL,
};
use crate::messages::frontend::utility_types::FileFilter;
use crate::messages::input_mapper::utility_types::macros::action_shortcut;
use crate::messages::layout::utility_types::widget_prelude::*;
use crate::messages::portfolio::document::data_panel::{DataPanelMessageContext, DataPanelMessageHandler};
Expand Down Expand Up @@ -1085,6 +1086,10 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
name,
path,
folder,
filters: vec![FileFilter {
name: "Graphite Document".into(),
extensions: vec![extension.into()],
}],
content: content.into(),
})
});
Expand Down
31 changes: 28 additions & 3 deletions editor/src/messages/portfolio/portfolio_message_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use crate::application::{Editor, generate_uuid};
use crate::consts::{DEFAULT_DOCUMENT_NAME, FILE_EXTENSION, GDD_FILE_EXTENSION};
use crate::messages::animation::TimingInformation;
use crate::messages::dialog::simple_dialogs;
use crate::messages::frontend::utility_types::{DocumentInfo, PersistedState};
use crate::messages::frontend::utility_types::{DocumentInfo, FileFilter, PersistedState};
use crate::messages::input_mapper::utility_types::input_keyboard::Key;
use crate::messages::input_mapper::utility_types::macros::{action_shortcut, action_shortcut_manual};
use crate::messages::layout::utility_types::widget_prelude::*;
Expand Down Expand Up @@ -501,13 +501,21 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
responses.add(FrontendMessage::TriggerSaveFile {
name: filename,
folder: None,
filters: vec![FileFilter {
name: "Graphite Document".into(),
extensions: vec![FILE_EXTENSION.into()],
}],
content: serde_bytes::ByteBuf::from(content),
});
} else {
match build_recovery_zip(&files) {
Ok(zip_bytes) => responses.add(FrontendMessage::TriggerSaveFile {
name: format!("{FOLDER_NAME}.zip"),
folder: None,
filters: vec![FileFilter {
name: "Zip Archive".into(),
extensions: vec!["zip".into()],
}],
content: serde_bytes::ByteBuf::from(zip_bytes),
}),
Err(e) => {
Expand Down Expand Up @@ -652,11 +660,28 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
}
PortfolioMessage::Open => {
// This portfolio message wraps the frontend message so it can be listed as an action, which isn't possible for frontend messages
responses.add(FrontendMessage::TriggerOpen);
responses.add(FrontendMessage::TriggerOpen {
filters: vec![
FileFilter {
name: "Graphite Document".into(),
extensions: vec![FILE_EXTENSION.into(), GDD_FILE_EXTENSION.into()],
},
FileFilter {
name: "Image".into(),
extensions: vec!["svg".into(), "png".into(), "jpg".into(), "jpeg".into(), "bmp".into(), "gif".into()],
},
],
});
}
PortfolioMessage::Import => {
// This portfolio message wraps the frontend message so it can be listed as an action, which isn't possible for frontend messages
responses.add(FrontendMessage::TriggerImport);
// TODO: Also offer the Graphite document filter once importing Graphite documents as nodes is supported
responses.add(FrontendMessage::TriggerImport {
filters: vec![FileFilter {
name: "Image".into(),
extensions: vec!["svg".into(), "png".into(), "jpg".into(), "jpeg".into(), "bmp".into(), "gif".into()],
}],
});
}
PortfolioMessage::OpenFile { path, content } => {
let name = path.file_stem().map(|n| n.to_string_lossy().to_string());
Expand Down
10 changes: 4 additions & 6 deletions editor/src/node_graph_executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -732,17 +732,13 @@ impl NodeGraphExecutor {
..
} = export_config;

let file_extension = match file_type {
FileType::Svg => "svg",
FileType::Png => "png",
FileType::Jpg => "jpg",
};
let base_name = match (artboard_name, artboard_count) {
(Some(artboard_name), count) if count > 1 => format!("{name} - {artboard_name}"),
_ => name,
};
let name = format!("{base_name}.{file_extension}");
let name = format!("{base_name}.{}", file_type.extension());
let folder = document.path.as_ref().and_then(|path| path.parent()).map(|parent| parent.to_path_buf());
let filters = vec![file_type.file_filter()];

match node_graph_output {
TaggedValue::RenderOutput(RenderOutput {
Expand All @@ -753,6 +749,7 @@ impl NodeGraphExecutor {
responses.add(FrontendMessage::TriggerSaveFile {
name,
folder,
filters,
content: svg.into_bytes().into(),
});
} else {
Expand Down Expand Up @@ -806,6 +803,7 @@ impl NodeGraphExecutor {
responses.add(FrontendMessage::TriggerSaveFile {
name,
folder,
filters,
content: encoded.into(),
});
}
Expand Down
11 changes: 5 additions & 6 deletions frontend/src/stores/portfolio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { SvelteMap } from "svelte/reactivity";
import { writable } from "svelte/store";
import type { Writable } from "svelte/store";
import type { SubscriptionsRouter } from "/src/subscriptions-router";
import { downloadFile, downloadFileBlob, upload } from "/src/utility-functions/files";
import { acceptStringFromFilters, downloadFile, downloadFileBlob, upload } from "/src/utility-functions/files";
import { rasterizeSVG } from "/src/utility-functions/rasterization";
import { patchLayout } from "/src/utility-functions/widgets";
import type { EditorWrapper, DocumentInfo, LayerPanelEntry, LayerStructureEntry, Layout, WorkspacePanelLayout } from "/wrapper/pkg/graphite_wasm_wrapper";
Expand Down Expand Up @@ -93,14 +93,13 @@ export function createPortfolioStore(subscriptions: SubscriptionsRouter, editor:
}
});

subscriptions.subscribeFrontendMessage("TriggerOpen", async () => {
const files = await upload(`image/*,.${editor.fileExtension()},.${editor.gddFileExtension()}`, "data", true);
subscriptions.subscribeFrontendMessage("TriggerOpen", async ({ filters }) => {
const files = await upload(acceptStringFromFilters(filters), "data", true);
files.forEach((file) => editor.openFile(file.filename, file.content));
});

subscriptions.subscribeFrontendMessage("TriggerImport", async () => {
// TODO: Use the same `accept` string as in the `TriggerOpen` handler once importing Graphite documents as nodes is supported
const data = await upload("image/*", "data");
subscriptions.subscribeFrontendMessage("TriggerImport", async ({ filters }) => {
const data = await upload(acceptStringFromFilters(filters), "data");
editor.importFile(data.filename, data.content);
});

Expand Down
10 changes: 8 additions & 2 deletions frontend/src/utility-functions/files.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { extractPixelData } from "/src/utility-functions/rasterization";
import type { EditorWrapper } from "/wrapper/pkg/graphite_wasm_wrapper";
import type { EditorWrapper, FileFilter } from "/wrapper/pkg/graphite_wasm_wrapper";

export function downloadFileURL(filename: string, url: string) {
const element = document.createElement("a");
Expand Down Expand Up @@ -91,8 +91,14 @@ export async function pasteFile(item: DataTransferItem, editor: EditorWrapper, m
} else if (file.type.startsWith("image/")) {
const imageData = await extractPixelData(file);
editor.pasteImage(file.name, new Uint8Array(imageData.data), imageData.width, imageData.height, mouse?.[0], mouse?.[1], insertParentId, insertIndex);
} else if (file.name.endsWith("." + editor.fileExtension())) {
} else {
// TODO: When we eventually have sub-documents, this should be changed to import the document as a node instead of opening it in a separate tab
editor.openFile(file.name, await file.bytes());
}
}

export function acceptStringFromFilters(filters: FileFilter[]): string {
const extensions = filters.flatMap((filter) => filter.extensions);
const imageMime = extensions.some((extension) => ["svg", "png", "jpg", "jpeg", "bmp", "gif", "webp", "avif", "tif", "tiff"].includes(extension)) ? ["image/*"] : [];
return [...imageMime, ...extensions.map((extension) => `.${extension}`)].join(",");
}
4 changes: 2 additions & 2 deletions frontend/src/utility-functions/network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,11 @@ export async function loadDemoArtwork(editor: EditorWrapper) {
if (!demoArtwork) return;

try {
const url = new URL(`/demo-artwork/${demoArtwork}.${editor.fileExtension()}`, document.location.href);
const url = new URL(`/demo-artwork/${demoArtwork}.graphite`, document.location.href);
const response = await fetch(url);
if (!response.ok) throw new Error();

const filename = url.pathname.split("/").pop() || `Untitled.${editor.fileExtension()}`;
const filename = url.pathname.split("/").pop() || "Untitled.graphite";
const content = await response.bytes();
editor.openFile(filename, content);

Expand Down
10 changes: 0 additions & 10 deletions frontend/wrapper/src/editor_wrapper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -240,16 +240,6 @@ impl EditorWrapper {
cfg!(debug_assertions)
}

#[wasm_bindgen(js_name = fileExtension)]
pub fn file_extension(&self) -> String {
"graphite".into()
}

#[wasm_bindgen(js_name = gddFileExtension)]
pub fn gdd_file_extension(&self) -> String {
"gdd".into()
}

/// Load persisted browser storage state (web only; on desktop, persistence is handled natively and this is never triggered)
#[cfg(all(feature = "web", not(feature = "native")))]
#[wasm_bindgen(js_name = loadPersistedState)]
Expand Down
Loading