mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 14:18:04 +08:00
Move file filter creation from frontend to editor (#4351)
This commit is contained in:
@@ -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,
|
||||
});
|
||||
@@ -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 },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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::keyboard::{Key, ModifierKeys};
|
||||
pub use graphite_editor::messages::input_mapper::utility_types::pointer::{EditorPointerState as PointerState, EditorPosition as Position, MouseKeys, ScrollDelta};
|
||||
pub use graphite_editor::messages::prelude::DocumentId;
|
||||
@@ -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,
|
||||
|
||||
@@ -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::{
|
||||
@@ -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 {
|
||||
@@ -128,8 +136,6 @@ pub enum FrontendMessage {
|
||||
},
|
||||
TriggerOpenLaunchDocuments,
|
||||
TriggerLoadPreferences,
|
||||
TriggerOpen,
|
||||
TriggerImport,
|
||||
TriggerSavePreferences {
|
||||
#[cfg_attr(feature = "wasm", tsify(type = "unknown"))]
|
||||
preferences: PreferencesMessageHandler,
|
||||
|
||||
@@ -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))]
|
||||
@@ -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>,
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
@@ -1089,6 +1090,10 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
||||
name,
|
||||
path,
|
||||
folder,
|
||||
filters: vec![FileFilter {
|
||||
name: "Graphite Document".into(),
|
||||
extensions: vec![extension.into()],
|
||||
}],
|
||||
content: content.into(),
|
||||
})
|
||||
});
|
||||
|
||||
@@ -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::keyboard::Key;
|
||||
use crate::messages::input_mapper::utility_types::macros::{action_shortcut, action_shortcut_manual};
|
||||
use crate::messages::layout::utility_types::widget_prelude::*;
|
||||
@@ -501,6 +501,10 @@ 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 {
|
||||
@@ -508,6 +512,10 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
||||
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) => {
|
||||
@@ -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());
|
||||
|
||||
@@ -730,17 +730,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 {
|
||||
@@ -751,6 +747,7 @@ impl NodeGraphExecutor {
|
||||
responses.add(FrontendMessage::TriggerSaveFile {
|
||||
name,
|
||||
folder,
|
||||
filters,
|
||||
content: svg.into_bytes().into(),
|
||||
});
|
||||
} else {
|
||||
@@ -804,6 +801,7 @@ impl NodeGraphExecutor {
|
||||
responses.add(FrontendMessage::TriggerSaveFile {
|
||||
name,
|
||||
folder,
|
||||
filters,
|
||||
content: encoded.into(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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");
|
||||
@@ -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(",");
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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)]
|
||||
|
||||
Reference in New Issue
Block a user