Improve UX of importing vs. opening files (#3661)
* wip * fix drag and drop * fix * fix tests * fix tests * fix warning * Partial code review * add dialog * fix web * fix web * push back release candidate expiry * Code review * Reduce code duplication for pasting files in frontend --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
parent
781fa7ae95
commit
2be7790d4d
|
|
@ -180,7 +180,7 @@ impl App {
|
||||||
if let Some(path) = futures::executor::block_on(show_dialog)
|
if let Some(path) = futures::executor::block_on(show_dialog)
|
||||||
&& let Ok(content) = fs::read(&path)
|
&& let Ok(content) = fs::read(&path)
|
||||||
{
|
{
|
||||||
let message = DesktopWrapperMessage::OpenFileDialogResult { path, content, context };
|
let message = DesktopWrapperMessage::FileDialogResult { path, content, context };
|
||||||
app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message));
|
app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -550,7 +550,7 @@ impl ApplicationHandler for App {
|
||||||
for path in paths {
|
for path in paths {
|
||||||
match fs::read(&path) {
|
match fs::read(&path) {
|
||||||
Ok(content) => {
|
Ok(content) => {
|
||||||
let message = DesktopWrapperMessage::OpenFile { path, content };
|
let message = DesktopWrapperMessage::ImportFile { path, content };
|
||||||
self.app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message));
|
self.app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message));
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|
|
||||||
|
|
@ -190,7 +190,7 @@ impl DocumentStore {
|
||||||
|
|
||||||
fn document_path(id: &DocumentId) -> std::path::PathBuf {
|
fn document_path(id: &DocumentId) -> std::path::PathBuf {
|
||||||
let mut path = crate::dirs::app_autosave_documents_dir();
|
let mut path = crate::dirs::app_autosave_documents_dir();
|
||||||
path.push(format!("{:x}.graphite", id.0));
|
path.push(format!("{:x}.{}", id.0, graphite_desktop_wrapper::FILE_EXTENSION));
|
||||||
path
|
path
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
use graphene_std::Color;
|
|
||||||
use graphene_std::raster::Image;
|
|
||||||
use graphite_editor::messages::clipboard::utility_types::ClipboardContentRaw;
|
use graphite_editor::messages::clipboard::utility_types::ClipboardContentRaw;
|
||||||
use graphite_editor::messages::prelude::*;
|
use graphite_editor::messages::prelude::*;
|
||||||
|
|
||||||
|
|
@ -14,9 +12,9 @@ pub(super) fn handle_desktop_wrapper_message(dispatcher: &mut DesktopWrapperMess
|
||||||
DesktopWrapperMessage::Input(message) => {
|
DesktopWrapperMessage::Input(message) => {
|
||||||
dispatcher.queue_editor_message(EditorMessage::InputPreprocessor(message));
|
dispatcher.queue_editor_message(EditorMessage::InputPreprocessor(message));
|
||||||
}
|
}
|
||||||
DesktopWrapperMessage::OpenFileDialogResult { path, content, context } => match context {
|
DesktopWrapperMessage::FileDialogResult { path, content, context } => match context {
|
||||||
OpenFileDialogContext::Document => {
|
OpenFileDialogContext::Open => {
|
||||||
dispatcher.queue_desktop_wrapper_message(DesktopWrapperMessage::OpenDocument { path, content });
|
dispatcher.queue_desktop_wrapper_message(DesktopWrapperMessage::OpenFile { path, content });
|
||||||
}
|
}
|
||||||
OpenFileDialogContext::Import => {
|
OpenFileDialogContext::Import => {
|
||||||
dispatcher.queue_desktop_wrapper_message(DesktopWrapperMessage::ImportFile { path, content });
|
dispatcher.queue_desktop_wrapper_message(DesktopWrapperMessage::ImportFile { path, content });
|
||||||
|
|
@ -35,78 +33,11 @@ pub(super) fn handle_desktop_wrapper_message(dispatcher: &mut DesktopWrapperMess
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
DesktopWrapperMessage::OpenFile { path, content } => {
|
DesktopWrapperMessage::OpenFile { path, content } => {
|
||||||
let extension = path.extension().and_then(|s| s.to_str()).unwrap_or_default().to_lowercase();
|
let message = PortfolioMessage::OpenFile { path, content };
|
||||||
match extension.as_str() {
|
|
||||||
"graphite" => {
|
|
||||||
dispatcher.queue_desktop_wrapper_message(DesktopWrapperMessage::OpenDocument { path, content });
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
dispatcher.queue_desktop_wrapper_message(DesktopWrapperMessage::ImportFile { path, content });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
DesktopWrapperMessage::OpenDocument { path, content } => {
|
|
||||||
let Ok(content) = String::from_utf8(content) else {
|
|
||||||
tracing::warn!("Document file is invalid: {}", path.display());
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let message = PortfolioMessage::OpenDocumentFile {
|
|
||||||
document_name: None,
|
|
||||||
document_path: Some(path),
|
|
||||||
document_serialized_content: content,
|
|
||||||
};
|
|
||||||
dispatcher.queue_editor_message(message);
|
dispatcher.queue_editor_message(message);
|
||||||
}
|
}
|
||||||
DesktopWrapperMessage::ImportFile { path, content } => {
|
DesktopWrapperMessage::ImportFile { path, content } => {
|
||||||
let extension = path.extension().and_then(|s| s.to_str()).unwrap_or_default().to_lowercase();
|
let message = PortfolioMessage::ImportFile { path, content };
|
||||||
match extension.as_str() {
|
|
||||||
"svg" => {
|
|
||||||
dispatcher.queue_desktop_wrapper_message(DesktopWrapperMessage::ImportSvg { path, content });
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
dispatcher.queue_desktop_wrapper_message(DesktopWrapperMessage::ImportImage { path, content });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
DesktopWrapperMessage::ImportSvg { path, content } => {
|
|
||||||
let Ok(content) = String::from_utf8(content) else {
|
|
||||||
tracing::warn!("Svg file is invalid: {}", path.display());
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let message = PortfolioMessage::PasteSvg {
|
|
||||||
name: path.file_stem().map(|s| s.to_string_lossy().to_string()),
|
|
||||||
svg: content,
|
|
||||||
mouse: None,
|
|
||||||
parent_and_insert_index: None,
|
|
||||||
};
|
|
||||||
dispatcher.queue_editor_message(message);
|
|
||||||
}
|
|
||||||
DesktopWrapperMessage::ImportImage { path, content } => {
|
|
||||||
let name = path.file_stem().and_then(|s| s.to_str()).map(|s| s.to_string());
|
|
||||||
let extension = path.extension().and_then(|s| s.to_str()).unwrap_or_default().to_lowercase();
|
|
||||||
let Some(image_format) = image::ImageFormat::from_extension(&extension) else {
|
|
||||||
tracing::warn!("Unsupported file type: {}", path.display());
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let reader = image::ImageReader::with_format(std::io::Cursor::new(content), image_format);
|
|
||||||
let Ok(image) = reader.decode() else {
|
|
||||||
tracing::error!("Failed to decode image: {}", path.display());
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let width = image.width();
|
|
||||||
let height = image.height();
|
|
||||||
|
|
||||||
// TODO: Handle Image formats with more than 8 bits per channel
|
|
||||||
let image_data = image.to_rgba8();
|
|
||||||
let image = Image::<Color>::from_image_data(image_data.as_raw(), width, height);
|
|
||||||
let message = PortfolioMessage::PasteImage {
|
|
||||||
name,
|
|
||||||
image,
|
|
||||||
mouse: None,
|
|
||||||
parent_and_insert_index: None,
|
|
||||||
};
|
|
||||||
dispatcher.queue_editor_message(message);
|
dispatcher.queue_editor_message(message);
|
||||||
}
|
}
|
||||||
DesktopWrapperMessage::PollNodeGraphEvaluation => dispatcher.poll_node_graph_evaluation(),
|
DesktopWrapperMessage::PollNodeGraphEvaluation => dispatcher.poll_node_graph_evaluation(),
|
||||||
|
|
|
||||||
|
|
@ -10,29 +10,17 @@ pub(super) fn intercept_frontend_message(dispatcher: &mut DesktopWrapperMessageD
|
||||||
FrontendMessage::RenderOverlays { context } => {
|
FrontendMessage::RenderOverlays { context } => {
|
||||||
dispatcher.respond(DesktopFrontendMessage::UpdateOverlays(context.take_scene()));
|
dispatcher.respond(DesktopFrontendMessage::UpdateOverlays(context.take_scene()));
|
||||||
}
|
}
|
||||||
FrontendMessage::TriggerOpenDocument => {
|
FrontendMessage::TriggerOpen => {
|
||||||
dispatcher.respond(DesktopFrontendMessage::OpenFileDialog {
|
dispatcher.respond(DesktopFrontendMessage::OpenFileDialog {
|
||||||
title: "Open Document".to_string(),
|
title: "Open Document".to_string(),
|
||||||
filters: vec![FileFilter {
|
filters: vec![],
|
||||||
name: "Graphite".to_string(),
|
context: OpenFileDialogContext::Open,
|
||||||
extensions: vec!["graphite".to_string()],
|
|
||||||
}],
|
|
||||||
context: OpenFileDialogContext::Document,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
FrontendMessage::TriggerImport => {
|
FrontendMessage::TriggerImport => {
|
||||||
dispatcher.respond(DesktopFrontendMessage::OpenFileDialog {
|
dispatcher.respond(DesktopFrontendMessage::OpenFileDialog {
|
||||||
title: "Import File".to_string(),
|
title: "Import File".to_string(),
|
||||||
filters: vec![
|
filters: vec![],
|
||||||
FileFilter {
|
|
||||||
name: "Svg".to_string(),
|
|
||||||
extensions: vec!["svg".to_string()],
|
|
||||||
},
|
|
||||||
FileFilter {
|
|
||||||
name: "Image".to_string(),
|
|
||||||
extensions: vec!["png".to_string(), "jpg".to_string(), "jpeg".to_string(), "bmp".to_string()],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
context: OpenFileDialogContext::Import,
|
context: OpenFileDialogContext::Import,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ use graph_craft::wasm_application_io::WasmApplicationIo;
|
||||||
use graphite_editor::application::{Editor, Environment, Host, Platform};
|
use graphite_editor::application::{Editor, Environment, Host, Platform};
|
||||||
use graphite_editor::messages::prelude::{FrontendMessage, Message};
|
use graphite_editor::messages::prelude::{FrontendMessage, Message};
|
||||||
|
|
||||||
|
pub use graphite_editor::consts::FILE_EXTENSION;
|
||||||
// TODO: Remove usage of this reexport in desktop create and remove this line
|
// TODO: Remove usage of this reexport in desktop create and remove this line
|
||||||
pub use graphene_std::Color;
|
pub use graphene_std::Color;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,7 @@ pub enum DesktopFrontendMessage {
|
||||||
pub enum DesktopWrapperMessage {
|
pub enum DesktopWrapperMessage {
|
||||||
FromWeb(Box<EditorMessage>),
|
FromWeb(Box<EditorMessage>),
|
||||||
Input(InputMessage),
|
Input(InputMessage),
|
||||||
OpenFileDialogResult {
|
FileDialogResult {
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
content: Vec<u8>,
|
content: Vec<u8>,
|
||||||
context: OpenFileDialogContext,
|
context: OpenFileDialogContext,
|
||||||
|
|
@ -88,10 +88,6 @@ pub enum DesktopWrapperMessage {
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
context: SaveFileDialogContext,
|
context: SaveFileDialogContext,
|
||||||
},
|
},
|
||||||
OpenDocument {
|
|
||||||
path: PathBuf,
|
|
||||||
content: Vec<u8>,
|
|
||||||
},
|
|
||||||
OpenFile {
|
OpenFile {
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
content: Vec<u8>,
|
content: Vec<u8>,
|
||||||
|
|
@ -100,14 +96,6 @@ pub enum DesktopWrapperMessage {
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
content: Vec<u8>,
|
content: Vec<u8>,
|
||||||
},
|
},
|
||||||
ImportSvg {
|
|
||||||
path: PathBuf,
|
|
||||||
content: Vec<u8>,
|
|
||||||
},
|
|
||||||
ImportImage {
|
|
||||||
path: PathBuf,
|
|
||||||
content: Vec<u8>,
|
|
||||||
},
|
|
||||||
PollNodeGraphEvaluation,
|
PollNodeGraphEvaluation,
|
||||||
UpdateMaximized {
|
UpdateMaximized {
|
||||||
maximized: bool,
|
maximized: bool,
|
||||||
|
|
@ -153,7 +141,7 @@ pub struct FileFilter {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum OpenFileDialogContext {
|
pub enum OpenFileDialogContext {
|
||||||
Document,
|
Open,
|
||||||
Import,
|
Import,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -571,10 +571,9 @@ mod test {
|
||||||
"Demo artwork '{document_name}' has more than 1 line (remember to open and re-save it in Graphite)",
|
"Demo artwork '{document_name}' has more than 1 line (remember to open and re-save it in Graphite)",
|
||||||
);
|
);
|
||||||
|
|
||||||
let responses = editor.editor.handle_message(PortfolioMessage::OpenDocumentFile {
|
let responses = editor.editor.handle_message(PortfolioMessage::OpenFile {
|
||||||
document_name: Some(document_name.to_string()),
|
path: file_name.into(),
|
||||||
document_path: None,
|
content: document_serialized_content.bytes().collect(),
|
||||||
document_serialized_content,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check if the graph renders
|
// Check if the graph renders
|
||||||
|
|
|
||||||
|
|
@ -107,7 +107,6 @@ pub enum FrontendMessage {
|
||||||
font: Font,
|
font: Font,
|
||||||
url: String,
|
url: String,
|
||||||
},
|
},
|
||||||
TriggerImport,
|
|
||||||
TriggerPersistenceRemoveDocument {
|
TriggerPersistenceRemoveDocument {
|
||||||
#[serde(rename = "documentId")]
|
#[serde(rename = "documentId")]
|
||||||
document_id: DocumentId,
|
document_id: DocumentId,
|
||||||
|
|
@ -122,7 +121,8 @@ pub enum FrontendMessage {
|
||||||
TriggerLoadRestAutoSaveDocuments,
|
TriggerLoadRestAutoSaveDocuments,
|
||||||
TriggerOpenLaunchDocuments,
|
TriggerOpenLaunchDocuments,
|
||||||
TriggerLoadPreferences,
|
TriggerLoadPreferences,
|
||||||
TriggerOpenDocument,
|
TriggerOpen,
|
||||||
|
TriggerImport,
|
||||||
TriggerSavePreferences {
|
TriggerSavePreferences {
|
||||||
preferences: PreferencesMessageHandler,
|
preferences: PreferencesMessageHandler,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -442,7 +442,7 @@ pub fn input_mappings(zoom_with_scroll: bool) -> Mapping {
|
||||||
entry!(KeyDown(Tab); modifiers=[Control, Shift], action_dispatch=PortfolioMessage::PrevDocument),
|
entry!(KeyDown(Tab); modifiers=[Control, Shift], action_dispatch=PortfolioMessage::PrevDocument),
|
||||||
entry!(KeyDown(KeyW); modifiers=[Accel], action_dispatch=PortfolioMessage::CloseActiveDocumentWithConfirmation),
|
entry!(KeyDown(KeyW); modifiers=[Accel], action_dispatch=PortfolioMessage::CloseActiveDocumentWithConfirmation),
|
||||||
entry!(KeyDown(KeyW); modifiers=[Accel, Alt], action_dispatch=PortfolioMessage::CloseAllDocumentsWithConfirmation),
|
entry!(KeyDown(KeyW); modifiers=[Accel, Alt], action_dispatch=PortfolioMessage::CloseAllDocumentsWithConfirmation),
|
||||||
entry!(KeyDown(KeyO); modifiers=[Accel], action_dispatch=PortfolioMessage::OpenDocument),
|
entry!(KeyDown(KeyO); modifiers=[Accel], action_dispatch=PortfolioMessage::Open),
|
||||||
entry!(KeyDown(KeyI); modifiers=[Accel], action_dispatch=PortfolioMessage::Import),
|
entry!(KeyDown(KeyI); modifiers=[Accel], action_dispatch=PortfolioMessage::Import),
|
||||||
entry!(KeyDown(KeyX); modifiers=[Accel], action_dispatch=PortfolioMessage::Cut { clipboard: Clipboard::Device }),
|
entry!(KeyDown(KeyX); modifiers=[Accel], action_dispatch=PortfolioMessage::Cut { clipboard: Clipboard::Device }),
|
||||||
entry!(KeyDown(KeyC); modifiers=[Accel], action_dispatch=PortfolioMessage::Copy { clipboard: Clipboard::Device }),
|
entry!(KeyDown(KeyC); modifiers=[Accel], action_dispatch=PortfolioMessage::Copy { clipboard: Clipboard::Device }),
|
||||||
|
|
|
||||||
|
|
@ -120,8 +120,8 @@ impl LayoutHolder for MenuBarMessageHandler {
|
||||||
MenuListEntry::new("Open…")
|
MenuListEntry::new("Open…")
|
||||||
.label("Open…")
|
.label("Open…")
|
||||||
.icon("Folder")
|
.icon("Folder")
|
||||||
.tooltip_shortcut(action_shortcut!(PortfolioMessageDiscriminant::OpenDocument))
|
.tooltip_shortcut(action_shortcut!(PortfolioMessageDiscriminant::Open))
|
||||||
.on_commit(|_| PortfolioMessage::OpenDocument.into()),
|
.on_commit(|_| PortfolioMessage::Open.into()),
|
||||||
MenuListEntry::new("Open Demo Artwork…")
|
MenuListEntry::new("Open Demo Artwork…")
|
||||||
.label("Open Demo Artwork…")
|
.label("Open Demo Artwork…")
|
||||||
.icon("Image")
|
.icon("Image")
|
||||||
|
|
@ -161,7 +161,8 @@ impl LayoutHolder for MenuBarMessageHandler {
|
||||||
.label("Import…")
|
.label("Import…")
|
||||||
.icon("FileImport")
|
.icon("FileImport")
|
||||||
.tooltip_shortcut(action_shortcut!(PortfolioMessageDiscriminant::Import))
|
.tooltip_shortcut(action_shortcut!(PortfolioMessageDiscriminant::Import))
|
||||||
.on_commit(|_| PortfolioMessage::Import.into()),
|
.on_commit(|_| PortfolioMessage::Import.into())
|
||||||
|
.disabled(no_active_document),
|
||||||
MenuListEntry::new("Export…")
|
MenuListEntry::new("Export…")
|
||||||
.label("Export…")
|
.label("Export…")
|
||||||
.icon("FileExport")
|
.icon("FileExport")
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,6 @@ pub enum PortfolioMessage {
|
||||||
font_style: String,
|
font_style: String,
|
||||||
data: Vec<u8>,
|
data: Vec<u8>,
|
||||||
},
|
},
|
||||||
Import,
|
|
||||||
LoadDocumentResources {
|
LoadDocumentResources {
|
||||||
document_id: DocumentId,
|
document_id: DocumentId,
|
||||||
},
|
},
|
||||||
|
|
@ -66,7 +65,16 @@ pub enum PortfolioMessage {
|
||||||
name: String,
|
name: String,
|
||||||
},
|
},
|
||||||
NextDocument,
|
NextDocument,
|
||||||
OpenDocument,
|
Open,
|
||||||
|
Import,
|
||||||
|
OpenFile {
|
||||||
|
path: PathBuf,
|
||||||
|
content: Vec<u8>,
|
||||||
|
},
|
||||||
|
ImportFile {
|
||||||
|
path: PathBuf,
|
||||||
|
content: Vec<u8>,
|
||||||
|
},
|
||||||
OpenDocumentFile {
|
OpenDocumentFile {
|
||||||
document_name: Option<String>,
|
document_name: Option<String>,
|
||||||
document_path: Option<PathBuf>,
|
document_path: Option<PathBuf>,
|
||||||
|
|
@ -82,11 +90,13 @@ pub enum PortfolioMessage {
|
||||||
to_front: bool,
|
to_front: bool,
|
||||||
select_after_open: bool,
|
select_after_open: bool,
|
||||||
},
|
},
|
||||||
ToggleResetNodesToDefinitionsOnOpen,
|
OpenImage {
|
||||||
PasteIntoFolder {
|
name: Option<String>,
|
||||||
clipboard: Clipboard,
|
image: Image<Color>,
|
||||||
parent: LayerNodeIdentifier,
|
},
|
||||||
insert_index: usize,
|
OpenSvg {
|
||||||
|
name: Option<String>,
|
||||||
|
svg: String,
|
||||||
},
|
},
|
||||||
PasteSerializedData {
|
PasteSerializedData {
|
||||||
data: String,
|
data: String,
|
||||||
|
|
@ -94,9 +104,6 @@ pub enum PortfolioMessage {
|
||||||
PasteSerializedVector {
|
PasteSerializedVector {
|
||||||
data: String,
|
data: String,
|
||||||
},
|
},
|
||||||
CenterPastedLayers {
|
|
||||||
layers: Vec<LayerNodeIdentifier>,
|
|
||||||
},
|
|
||||||
PasteImage {
|
PasteImage {
|
||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
image: Image<Color>,
|
image: Image<Color>,
|
||||||
|
|
@ -109,6 +116,15 @@ pub enum PortfolioMessage {
|
||||||
mouse: Option<(f64, f64)>,
|
mouse: Option<(f64, f64)>,
|
||||||
parent_and_insert_index: Option<(LayerNodeIdentifier, usize)>,
|
parent_and_insert_index: Option<(LayerNodeIdentifier, usize)>,
|
||||||
},
|
},
|
||||||
|
// TODO: Unused except by tests, remove?
|
||||||
|
PasteIntoFolder {
|
||||||
|
clipboard: Clipboard,
|
||||||
|
parent: LayerNodeIdentifier,
|
||||||
|
insert_index: usize,
|
||||||
|
},
|
||||||
|
CenterPastedLayers {
|
||||||
|
layers: Vec<LayerNodeIdentifier>,
|
||||||
|
},
|
||||||
PrevDocument,
|
PrevDocument,
|
||||||
RequestWelcomeScreenButtonsLayout,
|
RequestWelcomeScreenButtonsLayout,
|
||||||
RequestStatusBarInfoLayout,
|
RequestStatusBarInfoLayout,
|
||||||
|
|
@ -132,6 +148,7 @@ pub enum PortfolioMessage {
|
||||||
document_id: DocumentId,
|
document_id: DocumentId,
|
||||||
ignore_hash: bool,
|
ignore_hash: bool,
|
||||||
},
|
},
|
||||||
|
ToggleResetNodesToDefinitionsOnOpen,
|
||||||
ToggleDataPanelOpen,
|
ToggleDataPanelOpen,
|
||||||
TogglePropertiesPanelOpen,
|
TogglePropertiesPanelOpen,
|
||||||
ToggleLayersPanelOpen,
|
ToggleLayersPanelOpen,
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ use crate::messages::portfolio::document::utility_types::clipboards::{Clipboard,
|
||||||
use crate::messages::portfolio::document::utility_types::network_interface::OutputConnector;
|
use crate::messages::portfolio::document::utility_types::network_interface::OutputConnector;
|
||||||
use crate::messages::portfolio::document::utility_types::nodes::SelectedNodes;
|
use crate::messages::portfolio::document::utility_types::nodes::SelectedNodes;
|
||||||
use crate::messages::portfolio::document_migration::*;
|
use crate::messages::portfolio::document_migration::*;
|
||||||
|
use crate::messages::portfolio::utility_types::FileContent;
|
||||||
use crate::messages::preferences::SelectionMode;
|
use crate::messages::preferences::SelectionMode;
|
||||||
use crate::messages::prelude::*;
|
use crate::messages::prelude::*;
|
||||||
use crate::messages::tool::common_functionality::graph_modification_utils;
|
use crate::messages::tool::common_functionality::graph_modification_utils;
|
||||||
|
|
@ -27,11 +28,13 @@ use derivative::*;
|
||||||
use glam::{DAffine2, DVec2};
|
use glam::{DAffine2, DVec2};
|
||||||
use graph_craft::document::NodeId;
|
use graph_craft::document::NodeId;
|
||||||
use graphene_std::Color;
|
use graphene_std::Color;
|
||||||
|
use graphene_std::raster_types::Image;
|
||||||
use graphene_std::renderer::Quad;
|
use graphene_std::renderer::Quad;
|
||||||
use graphene_std::subpath::BezierHandles;
|
use graphene_std::subpath::BezierHandles;
|
||||||
use graphene_std::text::Font;
|
use graphene_std::text::Font;
|
||||||
use graphene_std::vector::misc::HandleId;
|
use graphene_std::vector::misc::HandleId;
|
||||||
use graphene_std::vector::{PointId, SegmentId, Vector, VectorModificationType};
|
use graphene_std::vector::{PointId, SegmentId, Vector, VectorModificationType};
|
||||||
|
use std::path::PathBuf;
|
||||||
use std::vec;
|
use std::vec;
|
||||||
|
|
||||||
#[derive(ExtractField)]
|
#[derive(ExtractField)]
|
||||||
|
|
@ -426,10 +429,6 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
PortfolioMessage::EditorPreferences => self.executor.update_editor_preferences(preferences.editor_preferences()),
|
PortfolioMessage::EditorPreferences => self.executor.update_editor_preferences(preferences.editor_preferences()),
|
||||||
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);
|
|
||||||
}
|
|
||||||
PortfolioMessage::LoadDocumentResources { document_id } => {
|
PortfolioMessage::LoadDocumentResources { document_id } => {
|
||||||
let catalog = &self.persistent_data.font_catalog;
|
let catalog = &self.persistent_data.font_catalog;
|
||||||
|
|
||||||
|
|
@ -465,9 +464,75 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
||||||
responses.add(PortfolioMessage::SelectDocument { document_id: next_id });
|
responses.add(PortfolioMessage::SelectDocument { document_id: next_id });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
PortfolioMessage::OpenDocument => {
|
PortfolioMessage::Open => {
|
||||||
// This portfolio message wraps the frontend message so it can be listed as an action, which isn't possible for frontend messages
|
// 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::TriggerOpenDocument);
|
responses.add(FrontendMessage::TriggerOpen);
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
PortfolioMessage::OpenFile { path, content } => {
|
||||||
|
let name = path.file_stem().map(|n| n.to_string_lossy().to_string());
|
||||||
|
match Self::read_file(&path, content) {
|
||||||
|
FileContent::Document(content) => {
|
||||||
|
responses.add(PortfolioMessage::OpenDocumentFile {
|
||||||
|
document_name: name,
|
||||||
|
document_path: Some(path),
|
||||||
|
document_serialized_content: content,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
FileContent::Svg(svg) => {
|
||||||
|
responses.add(PortfolioMessage::OpenSvg { name, svg });
|
||||||
|
}
|
||||||
|
FileContent::Image(image) => {
|
||||||
|
responses.add(PortfolioMessage::OpenImage { name, image });
|
||||||
|
}
|
||||||
|
FileContent::Unsupported => {
|
||||||
|
// TODO: Show a more thoughtfully designed error message to the user
|
||||||
|
responses.add(DialogMessage::DisplayDialogError {
|
||||||
|
title: "Unsupported format".into(),
|
||||||
|
description: "This file cannot be opened because it is not a supported image file type.".into(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PortfolioMessage::ImportFile { path, content } => {
|
||||||
|
let name = path.file_stem().map(|n| n.to_string_lossy().to_string());
|
||||||
|
match Self::read_file(&path, content) {
|
||||||
|
FileContent::Document(content) => {
|
||||||
|
// TODO: Consider importing a document as a node into the current document
|
||||||
|
// For now treat importing a document as opening it
|
||||||
|
responses.add(PortfolioMessage::OpenDocumentFile {
|
||||||
|
document_name: name,
|
||||||
|
document_path: Some(path),
|
||||||
|
document_serialized_content: content,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
FileContent::Svg(svg) => {
|
||||||
|
responses.add(PortfolioMessage::PasteSvg {
|
||||||
|
name,
|
||||||
|
svg,
|
||||||
|
mouse: None,
|
||||||
|
parent_and_insert_index: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
FileContent::Image(image) => {
|
||||||
|
responses.add(PortfolioMessage::PasteImage {
|
||||||
|
name,
|
||||||
|
image,
|
||||||
|
mouse: None,
|
||||||
|
parent_and_insert_index: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
FileContent::Unsupported => {
|
||||||
|
// TODO: Show a more thoughtfully designed error message to the user
|
||||||
|
responses.add(DialogMessage::DisplayDialogError {
|
||||||
|
title: "Unsupported format".into(),
|
||||||
|
description: "This file cannot be imported because it is not a supported image file type.".into(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
PortfolioMessage::OpenDocumentFile {
|
PortfolioMessage::OpenDocumentFile {
|
||||||
document_name,
|
document_name,
|
||||||
|
|
@ -596,6 +661,47 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
||||||
responses.add(PortfolioMessage::SelectDocument { document_id });
|
responses.add(PortfolioMessage::SelectDocument { document_id });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
PortfolioMessage::OpenImage { name, image } => {
|
||||||
|
responses.add(PortfolioMessage::NewDocumentWithName {
|
||||||
|
name: name.clone().unwrap_or(DEFAULT_DOCUMENT_NAME.into()),
|
||||||
|
});
|
||||||
|
|
||||||
|
responses.add(DocumentMessage::PasteImage {
|
||||||
|
name,
|
||||||
|
image,
|
||||||
|
mouse: None,
|
||||||
|
parent_and_insert_index: None,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait for the document to be rendered so the click targets can be calculated in order to determine the artboard size that will encompass the pasted image
|
||||||
|
responses.add(DeferMessage::AfterGraphRun {
|
||||||
|
messages: vec![DocumentMessage::WrapContentInArtboard { place_artboard_at_origin: true }.into()],
|
||||||
|
});
|
||||||
|
responses.add(DeferMessage::AfterNavigationReady {
|
||||||
|
messages: vec![DocumentMessage::ZoomCanvasToFitAll.into()],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
PortfolioMessage::OpenSvg { name, svg } => {
|
||||||
|
responses.add(PortfolioMessage::NewDocumentWithName {
|
||||||
|
name: name.clone().unwrap_or(DEFAULT_DOCUMENT_NAME.into()),
|
||||||
|
});
|
||||||
|
|
||||||
|
responses.add(DocumentMessage::PasteSvg {
|
||||||
|
name,
|
||||||
|
svg,
|
||||||
|
mouse: None,
|
||||||
|
parent_and_insert_index: None,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait for the document to be rendered so the click targets can be calculated in order to determine the artboard size that will encompass the pasted SVG
|
||||||
|
responses.add(DeferMessage::AfterGraphRun {
|
||||||
|
messages: vec![DocumentMessage::WrapContentInArtboard { place_artboard_at_origin: true }.into()],
|
||||||
|
});
|
||||||
|
responses.add(DeferMessage::AfterNavigationReady {
|
||||||
|
messages: vec![DocumentMessage::ZoomCanvasToFitAll.into()],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// TODO: Unused except by tests, remove?
|
||||||
PortfolioMessage::PasteIntoFolder { clipboard, parent, insert_index } => {
|
PortfolioMessage::PasteIntoFolder { clipboard, parent, insert_index } => {
|
||||||
let mut all_new_ids = Vec::new();
|
let mut all_new_ids = Vec::new();
|
||||||
let paste = |entry: &CopyBufferEntry, responses: &mut VecDeque<_>, all_new_ids: &mut Vec<NodeId>| {
|
let paste = |entry: &CopyBufferEntry, responses: &mut VecDeque<_>, all_new_ids: &mut Vec<NodeId>| {
|
||||||
|
|
@ -856,29 +962,15 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
||||||
mouse,
|
mouse,
|
||||||
parent_and_insert_index,
|
parent_and_insert_index,
|
||||||
} => {
|
} => {
|
||||||
let create_document = self.documents.is_empty();
|
if self.documents.is_empty() {
|
||||||
|
responses.add(PortfolioMessage::OpenImage { name, image });
|
||||||
if create_document {
|
} else {
|
||||||
responses.add(PortfolioMessage::NewDocumentWithName {
|
|
||||||
name: name.clone().unwrap_or(DEFAULT_DOCUMENT_NAME.into()),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
responses.add(DocumentMessage::PasteImage {
|
responses.add(DocumentMessage::PasteImage {
|
||||||
name,
|
name,
|
||||||
image,
|
image,
|
||||||
mouse,
|
mouse,
|
||||||
parent_and_insert_index,
|
parent_and_insert_index,
|
||||||
});
|
});
|
||||||
|
|
||||||
if create_document {
|
|
||||||
// Wait for the document to be rendered so the click targets can be calculated in order to determine the artboard size that will encompass the pasted image
|
|
||||||
responses.add(DeferMessage::AfterGraphRun {
|
|
||||||
messages: vec![DocumentMessage::WrapContentInArtboard { place_artboard_at_origin: true }.into()],
|
|
||||||
});
|
|
||||||
responses.add(DeferMessage::AfterNavigationReady {
|
|
||||||
messages: vec![DocumentMessage::ZoomCanvasToFitAll.into()],
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
PortfolioMessage::PasteSvg {
|
PortfolioMessage::PasteSvg {
|
||||||
|
|
@ -887,30 +979,15 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
||||||
mouse,
|
mouse,
|
||||||
parent_and_insert_index,
|
parent_and_insert_index,
|
||||||
} => {
|
} => {
|
||||||
let create_document = self.documents.is_empty();
|
if self.documents.is_empty() {
|
||||||
|
responses.add(PortfolioMessage::OpenSvg { name, svg });
|
||||||
if create_document {
|
} else {
|
||||||
responses.add(PortfolioMessage::NewDocumentWithName {
|
|
||||||
name: name.clone().unwrap_or(DEFAULT_DOCUMENT_NAME.into()),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
responses.add(DocumentMessage::PasteSvg {
|
responses.add(DocumentMessage::PasteSvg {
|
||||||
name,
|
name,
|
||||||
svg,
|
svg,
|
||||||
mouse,
|
mouse,
|
||||||
parent_and_insert_index,
|
parent_and_insert_index,
|
||||||
});
|
});
|
||||||
|
|
||||||
if create_document {
|
|
||||||
// Wait for the document to be rendered so the click targets can be calculated in order to determine the artboard size that will encompass the pasted image
|
|
||||||
responses.add(DeferMessage::AfterGraphRun {
|
|
||||||
messages: vec![DocumentMessage::WrapContentInArtboard { place_artboard_at_origin: true }.into()],
|
|
||||||
});
|
|
||||||
|
|
||||||
responses.add(DeferMessage::AfterNavigationReady {
|
|
||||||
messages: vec![DocumentMessage::ZoomCanvasToFitAll.into()],
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
PortfolioMessage::PrevDocument => {
|
PortfolioMessage::PrevDocument => {
|
||||||
|
|
@ -940,9 +1017,9 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
||||||
TextButton::new("Open Document")
|
TextButton::new("Open Document")
|
||||||
.icon(Some("Folder".into()))
|
.icon(Some("Folder".into()))
|
||||||
.flush(true)
|
.flush(true)
|
||||||
.on_commit(|_| PortfolioMessage::OpenDocument.into())
|
.on_commit(|_| PortfolioMessage::Open.into())
|
||||||
.widget_instance(),
|
.widget_instance(),
|
||||||
ShortcutLabel::new(action_shortcut!(PortfolioMessageDiscriminant::OpenDocument)).widget_instance(),
|
ShortcutLabel::new(action_shortcut!(PortfolioMessageDiscriminant::Open)).widget_instance(),
|
||||||
],
|
],
|
||||||
vec![
|
vec![
|
||||||
TextButton::new("Open Demo Artwork")
|
TextButton::new("Open Demo Artwork")
|
||||||
|
|
@ -1202,21 +1279,22 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
||||||
|
|
||||||
fn actions(&self) -> ActionList {
|
fn actions(&self) -> ActionList {
|
||||||
let mut common = actions!(PortfolioMessageDiscriminant;
|
let mut common = actions!(PortfolioMessageDiscriminant;
|
||||||
CloseActiveDocumentWithConfirmation,
|
Open,
|
||||||
CloseAllDocuments,
|
|
||||||
CloseAllDocumentsWithConfirmation,
|
|
||||||
Import,
|
|
||||||
NextDocument,
|
|
||||||
OpenDocument,
|
|
||||||
PasteIntoFolder,
|
|
||||||
PrevDocument,
|
|
||||||
ToggleRulers,
|
|
||||||
ToggleDataPanelOpen,
|
ToggleDataPanelOpen,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Extend with actions that require an active document
|
// Extend with actions that require an active document
|
||||||
if let Some(document) = self.active_document() {
|
if let Some(document) = self.active_document() {
|
||||||
common.extend(document.actions());
|
common.extend(document.actions());
|
||||||
|
common.extend(actions!(PortfolioMessageDiscriminant;
|
||||||
|
CloseActiveDocumentWithConfirmation,
|
||||||
|
CloseAllDocuments,
|
||||||
|
CloseAllDocumentsWithConfirmation,
|
||||||
|
ToggleRulers,
|
||||||
|
NextDocument,
|
||||||
|
PrevDocument,
|
||||||
|
Import,
|
||||||
|
));
|
||||||
|
|
||||||
// Extend with actions that must have a selected layer
|
// Extend with actions that must have a selected layer
|
||||||
if document.network_interface.selected_nodes().selected_layers(document.metadata()).next().is_some() {
|
if document.network_interface.selected_nodes().selected_layers(document.metadata()).next().is_some() {
|
||||||
|
|
@ -1281,6 +1359,32 @@ impl PortfolioMessageHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn read_file(path: &PathBuf, content: Vec<u8>) -> FileContent {
|
||||||
|
let extension = path.extension().and_then(|ext| ext.to_str()).unwrap_or_default().to_lowercase();
|
||||||
|
match extension.as_str() {
|
||||||
|
FILE_EXTENSION => match String::from_utf8(content) {
|
||||||
|
Ok(content) => FileContent::Document(content),
|
||||||
|
Err(_) => FileContent::Unsupported,
|
||||||
|
},
|
||||||
|
"svg" => match String::from_utf8(content) {
|
||||||
|
Ok(content) => FileContent::Svg(content),
|
||||||
|
Err(_) => FileContent::Unsupported,
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
let format = image::guess_format(&content).unwrap_or_else(|_| image::ImageFormat::from_path(path).unwrap_or(image::ImageFormat::Png));
|
||||||
|
match image::load_from_memory_with_format(&content, format) {
|
||||||
|
Ok(image) => {
|
||||||
|
// TODO: Handle Image formats with more than 8 bits per channel
|
||||||
|
let image_data = image.to_rgba8();
|
||||||
|
let image = Image::<Color>::from_image_data(image_data.as_raw(), image.width(), image.height());
|
||||||
|
FileContent::Image(image)
|
||||||
|
}
|
||||||
|
Err(_) => FileContent::Unsupported,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn load_document(&mut self, mut new_document: DocumentMessageHandler, document_id: DocumentId, layers_panel_open: bool, responses: &mut VecDeque<Message>, to_front: bool) {
|
fn load_document(&mut self, mut new_document: DocumentMessageHandler, document_id: DocumentId, layers_panel_open: bool, responses: &mut VecDeque<Message>, to_front: bool) {
|
||||||
if to_front {
|
if to_front {
|
||||||
self.document_ids.push_front(document_id);
|
self.document_ids.push_front(document_id);
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
use graphene_std::Color;
|
||||||
|
use graphene_std::raster::Image;
|
||||||
use graphene_std::text::{Font, FontCache};
|
use graphene_std::text::{Font, FontCache};
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
|
|
@ -104,3 +106,14 @@ impl From<String> for PanelType {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub enum FileContent {
|
||||||
|
/// A Graphite document.
|
||||||
|
Document(String),
|
||||||
|
/// A bitmap image.
|
||||||
|
Image(Image<Color>),
|
||||||
|
/// An SVG file string.
|
||||||
|
Svg(String),
|
||||||
|
/// Any other unsupported/unrecognized file type.
|
||||||
|
Unsupported,
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,9 @@
|
||||||
} from "@graphite/messages";
|
} from "@graphite/messages";
|
||||||
import type { AppWindowState } from "@graphite/state-providers/app-window";
|
import type { AppWindowState } from "@graphite/state-providers/app-window";
|
||||||
import type { DocumentState } from "@graphite/state-providers/document";
|
import type { DocumentState } from "@graphite/state-providers/document";
|
||||||
|
import { pasteFile } from "@graphite/utility-functions/files";
|
||||||
import { textInputCleanup } from "@graphite/utility-functions/keyboard-entry";
|
import { textInputCleanup } from "@graphite/utility-functions/keyboard-entry";
|
||||||
import { extractPixelData, rasterizeSVGCanvas } from "@graphite/utility-functions/rasterization";
|
import { rasterizeSVGCanvas } from "@graphite/utility-functions/rasterization";
|
||||||
import { setupViewportResizeObserver, cleanupViewportResizeObserver } from "@graphite/utility-functions/viewports";
|
import { setupViewportResizeObserver, cleanupViewportResizeObserver } from "@graphite/utility-functions/viewports";
|
||||||
|
|
||||||
import EyedropperPreview, { ZOOM_WINDOW_DIMENSIONS } from "@graphite/components/floating-menus/EyedropperPreview.svelte";
|
import EyedropperPreview, { ZOOM_WINDOW_DIMENSIONS } from "@graphite/components/floating-menus/EyedropperPreview.svelte";
|
||||||
|
|
@ -130,36 +131,14 @@
|
||||||
})($document.toolShelfLayout[0]);
|
})($document.toolShelfLayout[0]);
|
||||||
|
|
||||||
function dropFile(e: DragEvent) {
|
function dropFile(e: DragEvent) {
|
||||||
const { dataTransfer } = e;
|
if (!e.dataTransfer) return;
|
||||||
const [x, y] = e.target instanceof Element && e.target.closest("[data-viewport]") ? [e.clientX, e.clientY] : [undefined, undefined];
|
|
||||||
if (!dataTransfer) return;
|
let mouse: [number, number] | undefined = undefined;
|
||||||
|
if (e.target instanceof Element && e.target.closest("[data-viewport]")) mouse = [e.clientX, e.clientY];
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
Array.from(dataTransfer.items).forEach(async (item) => {
|
Array.from(e.dataTransfer.items).forEach(async (item) => await pasteFile(item, editor, mouse));
|
||||||
const file = item.getAsFile();
|
|
||||||
if (!file) return;
|
|
||||||
|
|
||||||
if (file.type.includes("svg")) {
|
|
||||||
const svgData = await file.text();
|
|
||||||
editor.handle.pasteSvg(file.name, svgData, x, y);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (file.type.startsWith("image")) {
|
|
||||||
const imageData = await extractPixelData(file);
|
|
||||||
editor.handle.pasteImage(file.name, new Uint8Array(imageData.data), imageData.width, imageData.height, x, y);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const graphiteFileSuffix = "." + editor.handle.fileExtension();
|
|
||||||
if (file.name.endsWith(graphiteFileSuffix)) {
|
|
||||||
const content = await file.text();
|
|
||||||
const documentName = file.name.slice(0, -graphiteFileSuffix.length);
|
|
||||||
editor.handle.openDocumentFile(documentName, content);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function panCanvasX(newValue: number) {
|
function panCanvasX(newValue: number) {
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,8 @@
|
||||||
import type { DataBuffer, LayerPanelEntry, Layout } from "@graphite/messages";
|
import type { DataBuffer, LayerPanelEntry, Layout } from "@graphite/messages";
|
||||||
import type { NodeGraphState } from "@graphite/state-providers/node-graph";
|
import type { NodeGraphState } from "@graphite/state-providers/node-graph";
|
||||||
import type { TooltipState } from "@graphite/state-providers/tooltip";
|
import type { TooltipState } from "@graphite/state-providers/tooltip";
|
||||||
|
import { pasteFile } from "@graphite/utility-functions/files";
|
||||||
import { operatingSystem } from "@graphite/utility-functions/platform";
|
import { operatingSystem } from "@graphite/utility-functions/platform";
|
||||||
import { extractPixelData } from "@graphite/utility-functions/rasterization";
|
|
||||||
|
|
||||||
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
|
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
|
||||||
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
|
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
|
||||||
|
|
@ -508,31 +508,7 @@
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
Array.from(e.dataTransfer.items).forEach(async (item) => {
|
Array.from(e.dataTransfer.items).forEach(async (item) => await pasteFile(item, editor, undefined, insertParentId, insertIndex));
|
||||||
const file = item.getAsFile();
|
|
||||||
if (!file) return;
|
|
||||||
|
|
||||||
if (file.type.includes("svg")) {
|
|
||||||
const svgData = await file.text();
|
|
||||||
editor.handle.pasteSvg(file.name, svgData, undefined, undefined, insertParentId, insertIndex);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (file.type.startsWith("image")) {
|
|
||||||
const imageData = await extractPixelData(file);
|
|
||||||
editor.handle.pasteImage(file.name, new Uint8Array(imageData.data), imageData.width, imageData.height, undefined, undefined, insertParentId, insertIndex);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// When we eventually have sub-documents, this should be changed to import the document instead of opening it in a separate tab
|
|
||||||
const graphiteFileSuffix = "." + editor.handle.fileExtension();
|
|
||||||
if (file.name.endsWith(graphiteFileSuffix)) {
|
|
||||||
const content = await file.text();
|
|
||||||
const documentName = file.name.slice(0, -graphiteFileSuffix.length);
|
|
||||||
editor.handle.openDocumentFile(documentName, content);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
draggingData = undefined;
|
draggingData = undefined;
|
||||||
fakeHighlightOfNotYetSelectedLayerBeingDragged = undefined;
|
fakeHighlightOfNotYetSelectedLayerBeingDragged = undefined;
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@
|
||||||
import type { Editor } from "@graphite/editor";
|
import type { Editor } from "@graphite/editor";
|
||||||
import type { Layout } from "@graphite/messages";
|
import type { Layout } from "@graphite/messages";
|
||||||
import { patchLayout, UpdateWelcomeScreenButtonsLayout } from "@graphite/messages";
|
import { patchLayout, UpdateWelcomeScreenButtonsLayout } from "@graphite/messages";
|
||||||
|
import { pasteFile } from "@graphite/utility-functions/files";
|
||||||
import { isDesktop } from "@graphite/utility-functions/platform";
|
import { isDesktop } from "@graphite/utility-functions/platform";
|
||||||
import { extractPixelData } from "@graphite/utility-functions/rasterization";
|
|
||||||
|
|
||||||
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
|
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
|
||||||
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
|
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
|
||||||
|
|
@ -33,30 +33,7 @@
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
Array.from(e.dataTransfer.items).forEach(async (item) => {
|
Array.from(e.dataTransfer.items).forEach(async (item) => await pasteFile(item, editor));
|
||||||
const file = item.getAsFile();
|
|
||||||
if (!file) return;
|
|
||||||
|
|
||||||
if (file.type.includes("svg")) {
|
|
||||||
const svgData = await file.text();
|
|
||||||
editor.handle.pasteSvg(file.name, svgData);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (file.type.startsWith("image")) {
|
|
||||||
const imageData = await extractPixelData(file);
|
|
||||||
editor.handle.pasteImage(file.name, new Uint8Array(imageData.data), imageData.width, imageData.height);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const graphiteFileSuffix = "." + editor.handle.fileExtension();
|
|
||||||
if (file.name.endsWith(graphiteFileSuffix)) {
|
|
||||||
const content = await file.text();
|
|
||||||
const documentName = file.name.slice(0, -graphiteFileSuffix.length);
|
|
||||||
editor.handle.openDocumentFile(documentName, content);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@
|
||||||
{#if $tooltip.visible}
|
{#if $tooltip.visible}
|
||||||
<Tooltip />
|
<Tooltip />
|
||||||
{/if}
|
{/if}
|
||||||
{#if isDesktop() && new Date() > new Date("2026-01-31")}
|
{#if isDesktop() && new Date() > new Date("2026-03-15")}
|
||||||
<LayoutCol class="release-candidate-expiry">
|
<LayoutCol class="release-candidate-expiry">
|
||||||
<TextLabel>
|
<TextLabel>
|
||||||
<p>
|
<p>
|
||||||
|
|
|
||||||
|
|
@ -60,13 +60,13 @@ export function createEditor(): Editor {
|
||||||
if (!demoArtwork) return;
|
if (!demoArtwork) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const url = new URL(`/demo-artwork/${demoArtwork}.graphite`, document.location.href);
|
const url = new URL(`/demo-artwork/${demoArtwork}.${handle.fileExtension()}`, document.location.href);
|
||||||
const data = await fetch(url);
|
const data = await fetch(url);
|
||||||
if (!data.ok) throw new Error();
|
if (!data.ok) throw new Error();
|
||||||
|
|
||||||
const filename = url.pathname.split("/").pop() || "Untitled";
|
const filename = url.pathname.split("/").pop() || "Untitled";
|
||||||
const content = await data.text();
|
const content = await data.bytes();
|
||||||
handle.openDocumentFile(filename, content);
|
handle.openFile(`${filename}.${handle.fileExtension()}`, content);
|
||||||
|
|
||||||
// Remove the hash fragment from the URL
|
// Remove the hash fragment from the URL
|
||||||
history.replaceState("", "", `${window.location.pathname}${window.location.search}`);
|
history.replaceState("", "", `${window.location.pathname}${window.location.search}`);
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { type DialogState } from "@graphite/state-providers/dialog";
|
||||||
import { type DocumentState } from "@graphite/state-providers/document";
|
import { type DocumentState } from "@graphite/state-providers/document";
|
||||||
import { type FullscreenState } from "@graphite/state-providers/fullscreen";
|
import { type FullscreenState } from "@graphite/state-providers/fullscreen";
|
||||||
import { type PortfolioState } from "@graphite/state-providers/portfolio";
|
import { type PortfolioState } from "@graphite/state-providers/portfolio";
|
||||||
|
import { pasteFile } from "@graphite/utility-functions/files";
|
||||||
import { makeKeyboardModifiersBitfield, textInputCleanup, getLocalizedScanCode } from "@graphite/utility-functions/keyboard-entry";
|
import { makeKeyboardModifiersBitfield, textInputCleanup, getLocalizedScanCode } from "@graphite/utility-functions/keyboard-entry";
|
||||||
import { isDesktop, operatingSystem } from "@graphite/utility-functions/platform";
|
import { isDesktop, operatingSystem } from "@graphite/utility-functions/platform";
|
||||||
import { extractPixelData } from "@graphite/utility-functions/rasterization";
|
import { extractPixelData } from "@graphite/utility-functions/rasterization";
|
||||||
|
|
@ -312,32 +313,8 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
Array.from(dataTransfer.items).forEach(async (item) => {
|
Array.from(dataTransfer.items).forEach(async (item) => {
|
||||||
if (item.type === "text/plain") {
|
if (item.type === "text/plain") item.getAsString((text) => editor.handle.pasteText(text));
|
||||||
item.getAsString((text) => {
|
await pasteFile(item, editor);
|
||||||
editor.handle.pasteText(text);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const file = item.getAsFile();
|
|
||||||
if (!file) return;
|
|
||||||
|
|
||||||
if (file.type.includes("svg")) {
|
|
||||||
const text = await file.text();
|
|
||||||
editor.handle.pasteSvg(file.name, text);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (file.type.startsWith("image")) {
|
|
||||||
const imageData = await extractPixelData(file);
|
|
||||||
editor.handle.pasteImage(file.name, new Uint8Array(imageData.data), imageData.width, imageData.height);
|
|
||||||
}
|
|
||||||
|
|
||||||
const graphiteFileSuffix = "." + editor.handle.fileExtension();
|
|
||||||
if (file.name.endsWith(graphiteFileSuffix)) {
|
|
||||||
const content = await file.text();
|
|
||||||
const documentName = file.name.slice(0, -graphiteFileSuffix.length);
|
|
||||||
editor.handle.openDocumentFile(documentName, content);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -745,7 +745,7 @@ export class TriggerFetchAndOpenDocument extends JsMessage {
|
||||||
readonly filename!: string;
|
readonly filename!: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TriggerOpenDocument extends JsMessage {}
|
export class TriggerOpen extends JsMessage {}
|
||||||
|
|
||||||
export class TriggerImport extends JsMessage {}
|
export class TriggerImport extends JsMessage {}
|
||||||
|
|
||||||
|
|
@ -1666,14 +1666,16 @@ export const messageMakers: Record<string, MessageMaker> = {
|
||||||
DisplayDialogDismiss,
|
DisplayDialogDismiss,
|
||||||
DisplayDialogPanic,
|
DisplayDialogPanic,
|
||||||
DisplayEditableTextbox,
|
DisplayEditableTextbox,
|
||||||
DisplayEditableTextboxUpdateFontData,
|
|
||||||
DisplayEditableTextboxTransform,
|
DisplayEditableTextboxTransform,
|
||||||
|
DisplayEditableTextboxUpdateFontData,
|
||||||
DisplayRemoveEditableTextbox,
|
DisplayRemoveEditableTextbox,
|
||||||
SendUIMetadata,
|
|
||||||
SendShortcutFullscreen,
|
|
||||||
SendShortcutAltClick,
|
SendShortcutAltClick,
|
||||||
|
SendShortcutFullscreen,
|
||||||
SendShortcutShiftClick,
|
SendShortcutShiftClick,
|
||||||
|
SendUIMetadata,
|
||||||
TriggerAboutGraphiteLocalizedCommitDate,
|
TriggerAboutGraphiteLocalizedCommitDate,
|
||||||
|
TriggerClipboardRead,
|
||||||
|
TriggerClipboardWrite,
|
||||||
TriggerDisplayThirdPartyLicensesDialog,
|
TriggerDisplayThirdPartyLicensesDialog,
|
||||||
TriggerExportImage,
|
TriggerExportImage,
|
||||||
TriggerFetchAndOpenDocument,
|
TriggerFetchAndOpenDocument,
|
||||||
|
|
@ -1683,7 +1685,7 @@ export const messageMakers: Record<string, MessageMaker> = {
|
||||||
TriggerLoadFirstAutoSaveDocument,
|
TriggerLoadFirstAutoSaveDocument,
|
||||||
TriggerLoadPreferences,
|
TriggerLoadPreferences,
|
||||||
TriggerLoadRestAutoSaveDocuments,
|
TriggerLoadRestAutoSaveDocuments,
|
||||||
TriggerOpenDocument,
|
TriggerOpen,
|
||||||
TriggerOpenLaunchDocuments,
|
TriggerOpenLaunchDocuments,
|
||||||
TriggerPersistenceRemoveDocument,
|
TriggerPersistenceRemoveDocument,
|
||||||
TriggerPersistenceWriteDocument,
|
TriggerPersistenceWriteDocument,
|
||||||
|
|
@ -1691,11 +1693,9 @@ export const messageMakers: Record<string, MessageMaker> = {
|
||||||
TriggerSaveDocument,
|
TriggerSaveDocument,
|
||||||
TriggerSaveFile,
|
TriggerSaveFile,
|
||||||
TriggerSavePreferences,
|
TriggerSavePreferences,
|
||||||
TriggerTextCommit,
|
|
||||||
TriggerClipboardRead,
|
|
||||||
TriggerClipboardWrite,
|
|
||||||
TriggerSelectionRead,
|
TriggerSelectionRead,
|
||||||
TriggerSelectionWrite,
|
TriggerSelectionWrite,
|
||||||
|
TriggerTextCommit,
|
||||||
TriggerVisitLink,
|
TriggerVisitLink,
|
||||||
UpdateActiveDocument,
|
UpdateActiveDocument,
|
||||||
UpdateBox,
|
UpdateBox,
|
||||||
|
|
@ -1714,6 +1714,7 @@ export const messageMakers: Record<string, MessageMaker> = {
|
||||||
UpdateDocumentScrollbars,
|
UpdateDocumentScrollbars,
|
||||||
UpdateExportReorderIndex,
|
UpdateExportReorderIndex,
|
||||||
UpdateEyedropperSamplingState,
|
UpdateEyedropperSamplingState,
|
||||||
|
UpdateFullscreen,
|
||||||
UpdateGraphFadeArtwork,
|
UpdateGraphFadeArtwork,
|
||||||
UpdateGraphViewOverlay,
|
UpdateGraphViewOverlay,
|
||||||
UpdateImportReorderIndex,
|
UpdateImportReorderIndex,
|
||||||
|
|
@ -1724,6 +1725,7 @@ export const messageMakers: Record<string, MessageMaker> = {
|
||||||
UpdateLayersPanelControlBarRightLayout,
|
UpdateLayersPanelControlBarRightLayout,
|
||||||
UpdateLayersPanelState,
|
UpdateLayersPanelState,
|
||||||
UpdateLayerWidths,
|
UpdateLayerWidths,
|
||||||
|
UpdateMaximized,
|
||||||
UpdateMenuBarLayout,
|
UpdateMenuBarLayout,
|
||||||
UpdateMouseCursor,
|
UpdateMouseCursor,
|
||||||
UpdateNodeGraphControlBarLayout,
|
UpdateNodeGraphControlBarLayout,
|
||||||
|
|
@ -1735,22 +1737,20 @@ export const messageMakers: Record<string, MessageMaker> = {
|
||||||
UpdateNodeThumbnail,
|
UpdateNodeThumbnail,
|
||||||
UpdateOpenDocumentsList,
|
UpdateOpenDocumentsList,
|
||||||
UpdatePlatform,
|
UpdatePlatform,
|
||||||
UpdateMaximized,
|
|
||||||
UpdateFullscreen,
|
|
||||||
WindowPointerLockMove,
|
|
||||||
WindowFullscreen,
|
|
||||||
UpdatePropertiesPanelLayout,
|
UpdatePropertiesPanelLayout,
|
||||||
UpdatePropertiesPanelState,
|
UpdatePropertiesPanelState,
|
||||||
UpdateStatusBarHintsLayout,
|
UpdateStatusBarHintsLayout,
|
||||||
UpdateStatusBarInfoLayout,
|
UpdateStatusBarInfoLayout,
|
||||||
UpdateToolOptionsLayout,
|
UpdateToolOptionsLayout,
|
||||||
UpdateToolShelfLayout,
|
UpdateToolShelfLayout,
|
||||||
|
UpdateUIScale,
|
||||||
UpdateViewportHolePunch,
|
UpdateViewportHolePunch,
|
||||||
UpdateViewportPhysicalBounds,
|
UpdateViewportPhysicalBounds,
|
||||||
UpdateUIScale,
|
|
||||||
UpdateVisibleNodes,
|
UpdateVisibleNodes,
|
||||||
UpdateWelcomeScreenButtonsLayout,
|
UpdateWelcomeScreenButtonsLayout,
|
||||||
UpdateWirePathInProgress,
|
UpdateWirePathInProgress,
|
||||||
UpdateWorkingColorsLayout,
|
UpdateWorkingColorsLayout,
|
||||||
|
WindowFullscreen,
|
||||||
|
WindowPointerLockMove,
|
||||||
} as const;
|
} as const;
|
||||||
export type JsMessageType = keyof typeof messageMakers;
|
export type JsMessageType = keyof typeof messageMakers;
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import {
|
||||||
TriggerExportImage,
|
TriggerExportImage,
|
||||||
TriggerSaveFile,
|
TriggerSaveFile,
|
||||||
TriggerImport,
|
TriggerImport,
|
||||||
TriggerOpenDocument,
|
TriggerOpen,
|
||||||
UpdateActiveDocument,
|
UpdateActiveDocument,
|
||||||
UpdateOpenDocumentsList,
|
UpdateOpenDocumentsList,
|
||||||
UpdateDataPanelState,
|
UpdateDataPanelState,
|
||||||
|
|
@ -16,7 +16,7 @@ import {
|
||||||
UpdateLayersPanelState,
|
UpdateLayersPanelState,
|
||||||
} from "@graphite/messages";
|
} from "@graphite/messages";
|
||||||
import { downloadFile, downloadFileBlob, upload } from "@graphite/utility-functions/files";
|
import { downloadFile, downloadFileBlob, upload } from "@graphite/utility-functions/files";
|
||||||
import { extractPixelData, rasterizeSVG } from "@graphite/utility-functions/rasterization";
|
import { rasterizeSVG } from "@graphite/utility-functions/rasterization";
|
||||||
|
|
||||||
export function createPortfolioState(editor: Editor) {
|
export function createPortfolioState(editor: Editor) {
|
||||||
const { subscribe, update } = writable({
|
const { subscribe, update } = writable({
|
||||||
|
|
@ -45,12 +45,9 @@ export function createPortfolioState(editor: Editor) {
|
||||||
});
|
});
|
||||||
editor.subscriptions.subscribeJsMessage(TriggerFetchAndOpenDocument, async (data) => {
|
editor.subscriptions.subscribeJsMessage(TriggerFetchAndOpenDocument, async (data) => {
|
||||||
try {
|
try {
|
||||||
const { name, filename } = data;
|
const url = new URL(`demo-artwork/${data.filename}`, document.location.href);
|
||||||
const url = new URL(`demo-artwork/${filename}`, document.location.href);
|
|
||||||
const response = await fetch(url);
|
const response = await fetch(url);
|
||||||
const content = await response.text();
|
editor.handle.openFile(data.filename, await response.bytes());
|
||||||
|
|
||||||
editor.handle.openDocumentFile(name, content);
|
|
||||||
} catch {
|
} catch {
|
||||||
// Needs to be delayed until the end of the current call stack so the existing demo artwork dialog can be closed first, otherwise this dialog won't show
|
// Needs to be delayed until the end of the current call stack so the existing demo artwork dialog can be closed first, otherwise this dialog won't show
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|
@ -58,37 +55,14 @@ export function createPortfolioState(editor: Editor) {
|
||||||
}, 0);
|
}, 0);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
editor.subscriptions.subscribeJsMessage(TriggerOpenDocument, async () => {
|
editor.subscriptions.subscribeJsMessage(TriggerOpen, async () => {
|
||||||
const suffix = "." + editor.handle.fileExtension();
|
const data = await upload(`image/*,.${editor.handle.fileExtension()}`, "data");
|
||||||
const data = await upload(suffix, "text");
|
editor.handle.openFile(data.filename, data.content);
|
||||||
|
|
||||||
// Use filename as document name, removing the extension if it exists
|
|
||||||
let documentName = data.filename;
|
|
||||||
if (documentName.endsWith(suffix)) {
|
|
||||||
documentName = documentName.slice(0, -suffix.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
editor.handle.openDocumentFile(documentName, data.content);
|
|
||||||
});
|
});
|
||||||
editor.subscriptions.subscribeJsMessage(TriggerImport, async () => {
|
editor.subscriptions.subscribeJsMessage(TriggerImport, async () => {
|
||||||
const data = await upload("image/*", "both");
|
// 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");
|
||||||
if (data.type.includes("svg")) {
|
editor.handle.importFile(data.filename, data.content);
|
||||||
const svg = new TextDecoder().decode(data.content.data);
|
|
||||||
editor.handle.pasteSvg(data.filename, svg);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// In case the user accidentally uploads a Graphite file, open it instead of failing to import it
|
|
||||||
const graphiteFileSuffix = "." + editor.handle.fileExtension();
|
|
||||||
if (data.filename.endsWith(graphiteFileSuffix)) {
|
|
||||||
const documentName = data.filename.slice(0, -graphiteFileSuffix.length);
|
|
||||||
editor.handle.openDocumentFile(documentName, data.content.text);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const imageData = await extractPixelData(new Blob([new Uint8Array(data.content.data)], { type: data.type }));
|
|
||||||
editor.handle.pasteImage(data.filename, new Uint8Array(imageData.data), imageData.width, imageData.height);
|
|
||||||
});
|
});
|
||||||
editor.subscriptions.subscribeJsMessage(TriggerSaveDocument, (data) => {
|
editor.subscriptions.subscribeJsMessage(TriggerSaveDocument, (data) => {
|
||||||
downloadFile(data.name, data.content);
|
downloadFile(data.name, data.content);
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,6 @@
|
||||||
|
import { type Editor } from "@graphite/editor";
|
||||||
|
import { extractPixelData } from "@graphite/utility-functions/rasterization";
|
||||||
|
|
||||||
export function downloadFileURL(filename: string, url: string) {
|
export function downloadFileURL(filename: string, url: string) {
|
||||||
const element = document.createElement("a");
|
const element = document.createElement("a");
|
||||||
|
|
||||||
|
|
@ -60,3 +63,19 @@ export async function upload<T extends "text" | "data" | "both">(accept: string,
|
||||||
}
|
}
|
||||||
export type UploadResult<T> = { filename: string; type: string; content: UploadResultType<T> };
|
export type UploadResult<T> = { filename: string; type: string; content: UploadResultType<T> };
|
||||||
type UploadResultType<T> = T extends "text" ? string : T extends "data" ? Uint8Array : T extends "both" ? { text: string; data: Uint8Array } : never;
|
type UploadResultType<T> = T extends "text" ? string : T extends "data" ? Uint8Array : T extends "both" ? { text: string; data: Uint8Array } : never;
|
||||||
|
|
||||||
|
export async function pasteFile(item: DataTransferItem, editor: Editor, mouse?: [number, number], insertParentId?: bigint, insertIndex?: number) {
|
||||||
|
const file = item.getAsFile();
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
if (file.type.startsWith("image/svg")) {
|
||||||
|
const svg = await file.text();
|
||||||
|
editor.handle.pasteSvg(file.name, svg, mouse?.[0], mouse?.[1], insertParentId, insertIndex);
|
||||||
|
} else if (file.type.startsWith("image/")) {
|
||||||
|
const imageData = await extractPixelData(file);
|
||||||
|
editor.handle.pasteImage(file.name, new Uint8Array(imageData.data), imageData.width, imageData.height, mouse?.[0], mouse?.[1], insertParentId, insertIndex);
|
||||||
|
} else if (file.name.endsWith("." + editor.handle.fileExtension())) {
|
||||||
|
// 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.handle.openFile(file.name, await file.bytes());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ use js_sys::{Object, Reflect};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use serde_wasm_bindgen::{self, from_value};
|
use serde_wasm_bindgen::{self, from_value};
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
|
use std::path::PathBuf;
|
||||||
use std::sync::atomic::{AtomicU64, Ordering};
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use wasm_bindgen::JsCast;
|
use wasm_bindgen::JsCast;
|
||||||
|
|
@ -414,13 +415,15 @@ impl EditorHandle {
|
||||||
self.dispatch(message);
|
self.dispatch(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = openDocumentFile)]
|
#[wasm_bindgen(js_name = openFile)]
|
||||||
pub fn open_document_file(&self, document_name: String, document_serialized_content: String) {
|
pub fn open_file(&self, path: String, content: Vec<u8>) {
|
||||||
let message = PortfolioMessage::OpenDocumentFile {
|
let message = PortfolioMessage::OpenFile { path: PathBuf::from(path), content };
|
||||||
document_name: Some(document_name),
|
self.dispatch(message);
|
||||||
document_path: None,
|
}
|
||||||
document_serialized_content,
|
|
||||||
};
|
#[wasm_bindgen(js_name = importFile)]
|
||||||
|
pub fn import_file(&self, path: String, content: Vec<u8>) {
|
||||||
|
let message = PortfolioMessage::ImportFile { path: PathBuf::from(path), content };
|
||||||
self.dispatch(message);
|
self.dispatch(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue