Desktop: Add File > Save As… (#3034)
* Make file name and document name identical * Add save as action * Fix test errors * Add missing save as action * Desktop fix drop file open document file message * Address review comments * Replace file save suffix with file extension * Add comment specifying that the upload function takes a html input accept string * Fix remove file extension in web * Use let * Don't show save as menu entry in web * Don't add SaveDocumentAs in web * Remove file extension on all open document file calls --------- Co-authored-by: Dennis Kobert <dennis@kobert.dev>
This commit is contained in:
parent
7c30f6168b
commit
e70862b399
|
|
@ -10,7 +10,6 @@ use graph_craft::wasm_application_io::WasmApplicationIo;
|
||||||
use graphene_std::Color;
|
use graphene_std::Color;
|
||||||
use graphene_std::raster::Image;
|
use graphene_std::raster::Image;
|
||||||
use graphite_editor::application::Editor;
|
use graphite_editor::application::Editor;
|
||||||
use graphite_editor::consts::DEFAULT_DOCUMENT_NAME;
|
|
||||||
use graphite_editor::messages::prelude::*;
|
use graphite_editor::messages::prelude::*;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
@ -79,7 +78,8 @@ impl WinitApp {
|
||||||
String::new()
|
String::new()
|
||||||
});
|
});
|
||||||
let message = PortfolioMessage::OpenDocumentFile {
|
let message = PortfolioMessage::OpenDocumentFile {
|
||||||
document_name: path.file_stem().and_then(|s| s.to_str()).unwrap_or("unknown").to_string(),
|
document_name: None,
|
||||||
|
document_path: Some(path),
|
||||||
document_serialized_content: content,
|
document_serialized_content: content,
|
||||||
};
|
};
|
||||||
let _ = event_loop_proxy.send_event(CustomEvent::DispatchMessage(message.into()));
|
let _ = event_loop_proxy.send_event(CustomEvent::DispatchMessage(message.into()));
|
||||||
|
|
@ -294,7 +294,8 @@ impl ApplicationHandler<CustomEvent> for WinitApp {
|
||||||
let Some(content) = load_string(&path) else { return };
|
let Some(content) = load_string(&path) else { return };
|
||||||
|
|
||||||
let message = PortfolioMessage::OpenDocumentFile {
|
let message = PortfolioMessage::OpenDocumentFile {
|
||||||
document_name: name.unwrap_or(DEFAULT_DOCUMENT_NAME.to_string()),
|
document_name: None,
|
||||||
|
document_path: Some(path),
|
||||||
document_serialized_content: content,
|
document_serialized_content: content,
|
||||||
};
|
};
|
||||||
self.dispatch_message(message.into());
|
self.dispatch_message(message.into());
|
||||||
|
|
|
||||||
|
|
@ -150,8 +150,8 @@ pub const COLOR_OVERLAY_WHITE: &str = "#ffffff";
|
||||||
pub const COLOR_OVERLAY_BLACK_75: &str = "#000000bf";
|
pub const COLOR_OVERLAY_BLACK_75: &str = "#000000bf";
|
||||||
|
|
||||||
// DOCUMENT
|
// DOCUMENT
|
||||||
|
pub const FILE_EXTENSION: &str = "graphite";
|
||||||
pub const DEFAULT_DOCUMENT_NAME: &str = "Untitled Document";
|
pub const DEFAULT_DOCUMENT_NAME: &str = "Untitled Document";
|
||||||
pub const FILE_SAVE_SUFFIX: &str = ".graphite";
|
|
||||||
pub const MAX_UNDO_HISTORY_LEN: usize = 100; // TODO: Add this to user preferences
|
pub const MAX_UNDO_HISTORY_LEN: usize = 100; // TODO: Add this to user preferences
|
||||||
pub const AUTO_SAVE_TIMEOUT_SECONDS: u64 = 1;
|
pub const AUTO_SAVE_TIMEOUT_SECONDS: u64 = 1;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -497,7 +497,8 @@ mod test {
|
||||||
);
|
);
|
||||||
|
|
||||||
let responses = editor.editor.handle_message(PortfolioMessage::OpenDocumentFile {
|
let responses = editor.editor.handle_message(PortfolioMessage::OpenDocumentFile {
|
||||||
document_name: document_name.into(),
|
document_name: Some(document_name.to_string()),
|
||||||
|
document_path: None,
|
||||||
document_serialized_content,
|
document_serialized_content,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ impl MessageHandler<ExportDialogMessage, ExportDialogMessageContext<'_>> for Exp
|
||||||
ExportDialogMessage::ExportBounds { bounds } => self.bounds = bounds,
|
ExportDialogMessage::ExportBounds { bounds } => self.bounds = bounds,
|
||||||
|
|
||||||
ExportDialogMessage::Submit => responses.add_front(PortfolioMessage::SubmitDocumentExport {
|
ExportDialogMessage::Submit => responses.add_front(PortfolioMessage::SubmitDocumentExport {
|
||||||
file_name: portfolio.active_document().map(|document| document.name.clone()).unwrap_or_default(),
|
name: portfolio.active_document().map(|document| document.name.clone()).unwrap_or_default(),
|
||||||
file_type: self.file_type,
|
file_type: self.file_type,
|
||||||
scale_factor: self.scale_factor,
|
scale_factor: self.scale_factor,
|
||||||
bounds: self.bounds,
|
bounds: self.bounds,
|
||||||
|
|
|
||||||
|
|
@ -340,6 +340,7 @@ pub fn input_mappings() -> Mapping {
|
||||||
entry!(KeyDown(KeyA); modifiers=[Accel, Shift], canonical, action_dispatch=DocumentMessage::DeselectAllLayers),
|
entry!(KeyDown(KeyA); modifiers=[Accel, Shift], canonical, action_dispatch=DocumentMessage::DeselectAllLayers),
|
||||||
entry!(KeyDown(KeyA); modifiers=[Alt], action_dispatch=DocumentMessage::DeselectAllLayers),
|
entry!(KeyDown(KeyA); modifiers=[Alt], action_dispatch=DocumentMessage::DeselectAllLayers),
|
||||||
entry!(KeyDown(KeyS); modifiers=[Accel], action_dispatch=DocumentMessage::SaveDocument),
|
entry!(KeyDown(KeyS); modifiers=[Accel], action_dispatch=DocumentMessage::SaveDocument),
|
||||||
|
entry!(KeyDown(KeyS); modifiers=[Accel, Shift], action_dispatch=DocumentMessage::SaveDocumentAs),
|
||||||
entry!(KeyDown(KeyD); modifiers=[Accel], canonical, action_dispatch=DocumentMessage::DuplicateSelectedLayers),
|
entry!(KeyDown(KeyD); modifiers=[Accel], canonical, action_dispatch=DocumentMessage::DuplicateSelectedLayers),
|
||||||
entry!(KeyDown(KeyJ); modifiers=[Accel], action_dispatch=DocumentMessage::DuplicateSelectedLayers),
|
entry!(KeyDown(KeyJ); modifiers=[Accel], action_dispatch=DocumentMessage::DuplicateSelectedLayers),
|
||||||
entry!(KeyDown(KeyG); modifiers=[Accel], action_dispatch=DocumentMessage::GroupSelectedLayers { group_folder_type: GroupFolderType::Layer }),
|
entry!(KeyDown(KeyG); modifiers=[Accel], action_dispatch=DocumentMessage::GroupSelectedLayers { group_folder_type: GroupFolderType::Layer }),
|
||||||
|
|
|
||||||
|
|
@ -118,6 +118,7 @@ pub enum DocumentMessage {
|
||||||
RenderRulers,
|
RenderRulers,
|
||||||
RenderScrollbars,
|
RenderScrollbars,
|
||||||
SaveDocument,
|
SaveDocument,
|
||||||
|
SaveDocumentAs,
|
||||||
SavedDocument {
|
SavedDocument {
|
||||||
path: Option<PathBuf>,
|
path: Option<PathBuf>,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ use super::utility_types::misc::{GroupFolderType, SNAP_FUNCTIONS_FOR_BOUNDING_BO
|
||||||
use super::utility_types::network_interface::{self, NodeNetworkInterface, TransactionStatus};
|
use super::utility_types::network_interface::{self, NodeNetworkInterface, TransactionStatus};
|
||||||
use super::utility_types::nodes::{CollapsedLayers, SelectedNodes};
|
use super::utility_types::nodes::{CollapsedLayers, SelectedNodes};
|
||||||
use crate::application::{GRAPHITE_GIT_COMMIT_HASH, generate_uuid};
|
use crate::application::{GRAPHITE_GIT_COMMIT_HASH, generate_uuid};
|
||||||
use crate::consts::{ASYMPTOTIC_EFFECT, COLOR_OVERLAY_GRAY, DEFAULT_DOCUMENT_NAME, FILE_SAVE_SUFFIX, SCALE_EFFECT, SCROLLBAR_SPACING, VIEWPORT_ROTATE_SNAP_INTERVAL};
|
use crate::consts::{ASYMPTOTIC_EFFECT, COLOR_OVERLAY_GRAY, DEFAULT_DOCUMENT_NAME, FILE_EXTENSION, SCALE_EFFECT, SCROLLBAR_SPACING, VIEWPORT_ROTATE_SNAP_INTERVAL};
|
||||||
use crate::messages::input_mapper::utility_types::macros::action_keys;
|
use crate::messages::input_mapper::utility_types::macros::action_keys;
|
||||||
use crate::messages::layout::utility_types::widget_prelude::*;
|
use crate::messages::layout::utility_types::widget_prelude::*;
|
||||||
use crate::messages::portfolio::document::data_panel::{DataPanelMessageContext, DataPanelMessageHandler};
|
use crate::messages::portfolio::document::data_panel::{DataPanelMessageContext, DataPanelMessageHandler};
|
||||||
|
|
@ -85,8 +85,6 @@ pub struct DocumentMessageHandler {
|
||||||
/// List of the [`LayerNodeIdentifier`]s that are currently collapsed by the user in the Layers panel.
|
/// List of the [`LayerNodeIdentifier`]s that are currently collapsed by the user in the Layers panel.
|
||||||
/// Collapsed means that the expansion arrow isn't set to show the children of these layers.
|
/// Collapsed means that the expansion arrow isn't set to show the children of these layers.
|
||||||
pub collapsed: CollapsedLayers,
|
pub collapsed: CollapsedLayers,
|
||||||
/// The name of the document, which is displayed in the tab and title bar of the editor.
|
|
||||||
pub name: String,
|
|
||||||
/// The full Git commit hash of the Graphite repository that was used to build the editor.
|
/// The full Git commit hash of the Graphite repository that was used to build the editor.
|
||||||
/// We save this to provide a hint about which version of the editor was used to create the document.
|
/// We save this to provide a hint about which version of the editor was used to create the document.
|
||||||
pub commit_hash: String,
|
pub commit_hash: String,
|
||||||
|
|
@ -113,6 +111,12 @@ pub struct DocumentMessageHandler {
|
||||||
// Fields omitted from the saved document format
|
// Fields omitted from the saved document format
|
||||||
// =============================================
|
// =============================================
|
||||||
//
|
//
|
||||||
|
/// The name of the document, which is displayed in the tab and title bar of the editor.
|
||||||
|
#[serde(skip)]
|
||||||
|
pub name: String,
|
||||||
|
/// The path of the to the document file.
|
||||||
|
#[serde(skip)]
|
||||||
|
pub(crate) path: Option<PathBuf>,
|
||||||
/// Path to network currently viewed in the node graph overlay. This will eventually be stored in each panel, so that multiple panels can refer to different networks
|
/// Path to network currently viewed in the node graph overlay. This will eventually be stored in each panel, so that multiple panels can refer to different networks
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
breadcrumb_network_path: Vec<NodeId>,
|
breadcrumb_network_path: Vec<NodeId>,
|
||||||
|
|
@ -125,9 +129,6 @@ pub struct DocumentMessageHandler {
|
||||||
/// Stack of document network snapshots for future history states.
|
/// Stack of document network snapshots for future history states.
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
document_redo_history: VecDeque<NodeNetworkInterface>,
|
document_redo_history: VecDeque<NodeNetworkInterface>,
|
||||||
/// The path of the to the document file.
|
|
||||||
#[serde(skip)]
|
|
||||||
path: Option<PathBuf>,
|
|
||||||
/// Hash of the document snapshot that was most recently saved to disk by the user.
|
/// Hash of the document snapshot that was most recently saved to disk by the user.
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
saved_hash: Option<u64>,
|
saved_hash: Option<u64>,
|
||||||
|
|
@ -159,7 +160,6 @@ impl Default for DocumentMessageHandler {
|
||||||
// ============================================
|
// ============================================
|
||||||
network_interface: default_document_network_interface(),
|
network_interface: default_document_network_interface(),
|
||||||
collapsed: CollapsedLayers::default(),
|
collapsed: CollapsedLayers::default(),
|
||||||
name: DEFAULT_DOCUMENT_NAME.to_string(),
|
|
||||||
commit_hash: GRAPHITE_GIT_COMMIT_HASH.to_string(),
|
commit_hash: GRAPHITE_GIT_COMMIT_HASH.to_string(),
|
||||||
document_ptz: PTZ::default(),
|
document_ptz: PTZ::default(),
|
||||||
document_mode: DocumentMode::DesignMode,
|
document_mode: DocumentMode::DesignMode,
|
||||||
|
|
@ -172,11 +172,12 @@ impl Default for DocumentMessageHandler {
|
||||||
// =============================================
|
// =============================================
|
||||||
// Fields omitted from the saved document format
|
// Fields omitted from the saved document format
|
||||||
// =============================================
|
// =============================================
|
||||||
|
name: DEFAULT_DOCUMENT_NAME.to_string(),
|
||||||
|
path: None,
|
||||||
breadcrumb_network_path: Vec::new(),
|
breadcrumb_network_path: Vec::new(),
|
||||||
selection_network_path: Vec::new(),
|
selection_network_path: Vec::new(),
|
||||||
document_undo_history: VecDeque::new(),
|
document_undo_history: VecDeque::new(),
|
||||||
document_redo_history: VecDeque::new(),
|
document_redo_history: VecDeque::new(),
|
||||||
path: None,
|
|
||||||
saved_hash: None,
|
saved_hash: None,
|
||||||
auto_saved_hash: None,
|
auto_saved_hash: None,
|
||||||
layer_range_selection_reference: None,
|
layer_range_selection_reference: None,
|
||||||
|
|
@ -947,7 +948,11 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
||||||
responses.add(OverlaysMessage::Draw);
|
responses.add(OverlaysMessage::Draw);
|
||||||
}
|
}
|
||||||
DocumentMessage::RenameDocument { new_name } => {
|
DocumentMessage::RenameDocument { new_name } => {
|
||||||
self.name = new_name;
|
self.name = new_name.clone();
|
||||||
|
|
||||||
|
self.path = None;
|
||||||
|
self.set_save_state(false);
|
||||||
|
|
||||||
responses.add(PortfolioMessage::UpdateOpenDocumentsList);
|
responses.add(PortfolioMessage::UpdateOpenDocumentsList);
|
||||||
responses.add(NodeGraphMessage::UpdateNewNodeGraph);
|
responses.add(NodeGraphMessage::UpdateNewNodeGraph);
|
||||||
}
|
}
|
||||||
|
|
@ -1020,25 +1025,40 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
||||||
multiplier: scrollbar_multiplier.into(),
|
multiplier: scrollbar_multiplier.into(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
DocumentMessage::SaveDocument => {
|
DocumentMessage::SaveDocument | DocumentMessage::SaveDocumentAs => {
|
||||||
|
if let DocumentMessage::SaveDocumentAs = message {
|
||||||
|
self.path = None;
|
||||||
|
}
|
||||||
|
|
||||||
self.set_save_state(true);
|
self.set_save_state(true);
|
||||||
responses.add(PortfolioMessage::AutoSaveActiveDocument);
|
responses.add(PortfolioMessage::AutoSaveActiveDocument);
|
||||||
// Update the save status of the just saved document
|
// Update the save status of the just saved document
|
||||||
responses.add(PortfolioMessage::UpdateOpenDocumentsList);
|
responses.add(PortfolioMessage::UpdateOpenDocumentsList);
|
||||||
|
|
||||||
let name = match self.name.ends_with(FILE_SAVE_SUFFIX) {
|
|
||||||
true => self.name.clone(),
|
|
||||||
false => self.name.clone() + FILE_SAVE_SUFFIX,
|
|
||||||
};
|
|
||||||
responses.add(FrontendMessage::TriggerSaveDocument {
|
responses.add(FrontendMessage::TriggerSaveDocument {
|
||||||
document_id,
|
document_id,
|
||||||
name,
|
name: format!("{}.{}", self.name.clone(), FILE_EXTENSION),
|
||||||
path: self.path.clone(),
|
path: self.path.clone(),
|
||||||
content: self.serialize_document().into_bytes(),
|
content: self.serialize_document().into_bytes(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
DocumentMessage::SavedDocument { path } => {
|
DocumentMessage::SavedDocument { path } => {
|
||||||
self.path = path;
|
self.path = path;
|
||||||
|
|
||||||
|
// Update the name to match the file stem
|
||||||
|
let document_name_from_path = self.path.as_ref().and_then(|path| {
|
||||||
|
if path.extension().is_some_and(|e| e == FILE_EXTENSION) {
|
||||||
|
path.file_stem().map(|n| n.to_string_lossy().to_string())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if let Some(name) = document_name_from_path {
|
||||||
|
self.name = name;
|
||||||
|
|
||||||
|
responses.add(PortfolioMessage::UpdateOpenDocumentsList);
|
||||||
|
responses.add(NodeGraphMessage::UpdateNewNodeGraph);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
DocumentMessage::SelectParentLayer => {
|
DocumentMessage::SelectParentLayer => {
|
||||||
let selected_nodes = self.network_interface.selected_nodes();
|
let selected_nodes = self.network_interface.selected_nodes();
|
||||||
|
|
@ -1571,6 +1591,10 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
||||||
ZoomCanvasToFitAll,
|
ZoomCanvasToFitAll,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Additional actions available on desktop
|
||||||
|
#[cfg(not(target_family = "wasm"))]
|
||||||
|
common.extend(actions!(DocumentMessageDiscriminant::SaveDocumentAs));
|
||||||
|
|
||||||
// Additional actions if there are any selected layers
|
// Additional actions if there are any selected layers
|
||||||
if self.network_interface.selected_nodes().selected_layers(self.metadata()).next().is_some() {
|
if self.network_interface.selected_nodes().selected_layers(self.metadata()).next().is_some() {
|
||||||
let mut select = actions!(DocumentMessageDiscriminant;
|
let mut select = actions!(DocumentMessageDiscriminant;
|
||||||
|
|
|
||||||
|
|
@ -101,14 +101,25 @@ impl LayoutHolder for MenuBarMessageHandler {
|
||||||
..MenuBarEntry::default()
|
..MenuBarEntry::default()
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
vec![MenuBarEntry {
|
vec![
|
||||||
|
MenuBarEntry {
|
||||||
label: "Save".into(),
|
label: "Save".into(),
|
||||||
icon: Some("Save".into()),
|
icon: Some("Save".into()),
|
||||||
shortcut: action_keys!(DocumentMessageDiscriminant::SaveDocument),
|
shortcut: action_keys!(DocumentMessageDiscriminant::SaveDocument),
|
||||||
action: MenuBarEntry::create_action(|_| DocumentMessage::SaveDocument.into()),
|
action: MenuBarEntry::create_action(|_| DocumentMessage::SaveDocument.into()),
|
||||||
disabled: no_active_document,
|
disabled: no_active_document,
|
||||||
..MenuBarEntry::default()
|
..MenuBarEntry::default()
|
||||||
}],
|
},
|
||||||
|
#[cfg(not(target_family = "wasm"))]
|
||||||
|
MenuBarEntry {
|
||||||
|
label: "Save As…".into(),
|
||||||
|
icon: Some("Save".into()),
|
||||||
|
shortcut: action_keys!(DocumentMessageDiscriminant::SaveDocumentAs),
|
||||||
|
action: MenuBarEntry::create_action(|_| DocumentMessage::SaveDocumentAs.into()),
|
||||||
|
disabled: no_active_document,
|
||||||
|
..MenuBarEntry::default()
|
||||||
|
},
|
||||||
|
],
|
||||||
vec![
|
vec![
|
||||||
MenuBarEntry {
|
MenuBarEntry {
|
||||||
label: "Import…".into(),
|
label: "Import…".into(),
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ use crate::messages::prelude::*;
|
||||||
use graphene_std::Color;
|
use graphene_std::Color;
|
||||||
use graphene_std::raster::Image;
|
use graphene_std::raster::Image;
|
||||||
use graphene_std::text::Font;
|
use graphene_std::text::Font;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
#[impl_message(Message, Portfolio)]
|
#[impl_message(Message, Portfolio)]
|
||||||
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
|
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||||
|
|
@ -66,18 +67,20 @@ pub enum PortfolioMessage {
|
||||||
NextDocument,
|
NextDocument,
|
||||||
OpenDocument,
|
OpenDocument,
|
||||||
OpenDocumentFile {
|
OpenDocumentFile {
|
||||||
document_name: String,
|
document_name: Option<String>,
|
||||||
|
document_path: Option<PathBuf>,
|
||||||
document_serialized_content: String,
|
document_serialized_content: String,
|
||||||
},
|
},
|
||||||
ToggleResetNodesToDefinitionsOnOpen,
|
|
||||||
OpenDocumentFileWithId {
|
OpenDocumentFileWithId {
|
||||||
document_id: DocumentId,
|
document_id: DocumentId,
|
||||||
document_name: String,
|
document_name: Option<String>,
|
||||||
|
document_path: Option<PathBuf>,
|
||||||
document_is_auto_saved: bool,
|
document_is_auto_saved: bool,
|
||||||
document_is_saved: bool,
|
document_is_saved: bool,
|
||||||
document_serialized_content: String,
|
document_serialized_content: String,
|
||||||
to_front: bool,
|
to_front: bool,
|
||||||
},
|
},
|
||||||
|
ToggleResetNodesToDefinitionsOnOpen,
|
||||||
PasteIntoFolder {
|
PasteIntoFolder {
|
||||||
clipboard: Clipboard,
|
clipboard: Clipboard,
|
||||||
parent: LayerNodeIdentifier,
|
parent: LayerNodeIdentifier,
|
||||||
|
|
@ -115,7 +118,7 @@ pub enum PortfolioMessage {
|
||||||
document_id: DocumentId,
|
document_id: DocumentId,
|
||||||
},
|
},
|
||||||
SubmitDocumentExport {
|
SubmitDocumentExport {
|
||||||
file_name: String,
|
name: String,
|
||||||
file_type: FileType,
|
file_type: FileType,
|
||||||
scale_factor: f64,
|
scale_factor: f64,
|
||||||
bounds: ExportBounds,
|
bounds: ExportBounds,
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ use super::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||||
use super::document::utility_types::network_interface;
|
use super::document::utility_types::network_interface;
|
||||||
use super::utility_types::{PanelType, PersistentData};
|
use super::utility_types::{PanelType, PersistentData};
|
||||||
use crate::application::generate_uuid;
|
use crate::application::generate_uuid;
|
||||||
use crate::consts::{DEFAULT_DOCUMENT_NAME, DEFAULT_STROKE_WIDTH};
|
use crate::consts::{DEFAULT_DOCUMENT_NAME, DEFAULT_STROKE_WIDTH, FILE_EXTENSION};
|
||||||
use crate::messages::animation::TimingInformation;
|
use crate::messages::animation::TimingInformation;
|
||||||
use crate::messages::debug::utility_types::MessageLoggingVerbosity;
|
use crate::messages::debug::utility_types::MessageLoggingVerbosity;
|
||||||
use crate::messages::dialog::simple_dialogs;
|
use crate::messages::dialog::simple_dialogs;
|
||||||
|
|
@ -419,12 +419,14 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
||||||
}
|
}
|
||||||
PortfolioMessage::OpenDocumentFile {
|
PortfolioMessage::OpenDocumentFile {
|
||||||
document_name,
|
document_name,
|
||||||
|
document_path,
|
||||||
document_serialized_content,
|
document_serialized_content,
|
||||||
} => {
|
} => {
|
||||||
let document_id = DocumentId(generate_uuid());
|
let document_id = DocumentId(generate_uuid());
|
||||||
responses.add(PortfolioMessage::OpenDocumentFileWithId {
|
responses.add(PortfolioMessage::OpenDocumentFileWithId {
|
||||||
document_id,
|
document_id,
|
||||||
document_name,
|
document_name,
|
||||||
|
document_path,
|
||||||
document_is_auto_saved: false,
|
document_is_auto_saved: false,
|
||||||
document_is_saved: true,
|
document_is_saved: true,
|
||||||
document_serialized_content,
|
document_serialized_content,
|
||||||
|
|
@ -439,6 +441,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
||||||
PortfolioMessage::OpenDocumentFileWithId {
|
PortfolioMessage::OpenDocumentFileWithId {
|
||||||
document_id,
|
document_id,
|
||||||
document_name,
|
document_name,
|
||||||
|
document_path,
|
||||||
document_is_auto_saved,
|
document_is_auto_saved,
|
||||||
document_is_saved,
|
document_is_saved,
|
||||||
document_serialized_content,
|
document_serialized_content,
|
||||||
|
|
@ -450,10 +453,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
||||||
let document_serialized_content = document_migration_string_preprocessing(document_serialized_content);
|
let document_serialized_content = document_migration_string_preprocessing(document_serialized_content);
|
||||||
|
|
||||||
// Deserialize the document
|
// Deserialize the document
|
||||||
let document = DocumentMessageHandler::deserialize_document(&document_serialized_content).map(|mut document| {
|
let document = DocumentMessageHandler::deserialize_document(&document_serialized_content);
|
||||||
document.name.clone_from(&document_name);
|
|
||||||
document
|
|
||||||
});
|
|
||||||
|
|
||||||
// Display an error to the user if the document could not be opened
|
// Display an error to the user if the document could not be opened
|
||||||
let mut document = match document {
|
let mut document = match document {
|
||||||
|
|
@ -514,6 +514,30 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
||||||
document.set_auto_save_state(document_is_auto_saved);
|
document.set_auto_save_state(document_is_auto_saved);
|
||||||
document.set_save_state(document_is_saved);
|
document.set_save_state(document_is_saved);
|
||||||
|
|
||||||
|
let document_name_from_path = document_path.as_ref().and_then(|path| {
|
||||||
|
if path.extension().is_some_and(|e| e == FILE_EXTENSION) {
|
||||||
|
path.file_stem().map(|n| n.to_string_lossy().to_string())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
match (document_name, document_path, document_name_from_path) {
|
||||||
|
(Some(name), _, None) => {
|
||||||
|
document.name = name;
|
||||||
|
}
|
||||||
|
(_, Some(path), Some(name)) => {
|
||||||
|
document.name = name;
|
||||||
|
document.path = Some(path);
|
||||||
|
}
|
||||||
|
(_, _, Some(name)) => {
|
||||||
|
document.name = name;
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
document.name = DEFAULT_DOCUMENT_NAME.to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Load the document into the portfolio so it opens in the editor
|
// Load the document into the portfolio so it opens in the editor
|
||||||
self.load_document(document, document_id, self.layers_panel_open, responses, to_front);
|
self.load_document(document, document_id, self.layers_panel_open, responses, to_front);
|
||||||
}
|
}
|
||||||
|
|
@ -899,7 +923,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
PortfolioMessage::SubmitDocumentExport {
|
PortfolioMessage::SubmitDocumentExport {
|
||||||
file_name,
|
name,
|
||||||
file_type,
|
file_type,
|
||||||
scale_factor,
|
scale_factor,
|
||||||
bounds,
|
bounds,
|
||||||
|
|
@ -907,7 +931,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
||||||
} => {
|
} => {
|
||||||
let document = self.active_document_id.and_then(|id| self.documents.get_mut(&id)).expect("Tried to render non-existent document");
|
let document = self.active_document_id.and_then(|id| self.documents.get_mut(&id)).expect("Tried to render non-existent document");
|
||||||
let export_config = ExportConfig {
|
let export_config = ExportConfig {
|
||||||
file_name,
|
name,
|
||||||
file_type,
|
file_type,
|
||||||
scale_factor,
|
scale_factor,
|
||||||
bounds,
|
bounds,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
use crate::consts::FILE_SAVE_SUFFIX;
|
|
||||||
use crate::messages::frontend::utility_types::{ExportBounds, FileType};
|
use crate::messages::frontend::utility_types::{ExportBounds, FileType};
|
||||||
use crate::messages::prelude::*;
|
use crate::messages::prelude::*;
|
||||||
use glam::{DAffine2, DVec2, UVec2};
|
use glam::{DAffine2, DVec2, UVec2};
|
||||||
|
|
@ -229,18 +228,11 @@ impl NodeGraphExecutor {
|
||||||
};
|
};
|
||||||
|
|
||||||
let ExportConfig {
|
let ExportConfig {
|
||||||
file_type,
|
file_type, name, size, scale_factor, ..
|
||||||
file_name,
|
|
||||||
size,
|
|
||||||
scale_factor,
|
|
||||||
..
|
|
||||||
} = export_config;
|
} = export_config;
|
||||||
|
|
||||||
let file_suffix = &format!(".{file_type:?}").to_lowercase();
|
let file_suffix = &format!(".{file_type:?}").to_lowercase();
|
||||||
let name = match file_name.ends_with(FILE_SAVE_SUFFIX) {
|
let name = name + file_suffix;
|
||||||
true => file_name.replace(FILE_SAVE_SUFFIX, file_suffix),
|
|
||||||
false => file_name + file_suffix,
|
|
||||||
};
|
|
||||||
|
|
||||||
if file_type == FileType::Svg {
|
if file_type == FileType::Svg {
|
||||||
responses.add(FrontendMessage::TriggerSaveFile { name, content: svg.into_bytes() });
|
responses.add(FrontendMessage::TriggerSaveFile { name, content: svg.into_bytes() });
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,7 @@ pub struct GraphUpdate {
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Default, Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
pub struct ExportConfig {
|
pub struct ExportConfig {
|
||||||
pub file_name: String,
|
pub name: String,
|
||||||
pub file_type: FileType,
|
pub file_type: FileType,
|
||||||
pub scale_factor: f64,
|
pub scale_factor: f64,
|
||||||
pub bounds: ExportBounds,
|
pub bounds: ExportBounds,
|
||||||
|
|
|
||||||
|
|
@ -151,9 +151,11 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (file.name.endsWith(".graphite")) {
|
const graphiteFileSuffix = "." + editor.handle.fileExtension();
|
||||||
|
if (file.name.endsWith(graphiteFileSuffix)) {
|
||||||
const content = await file.text();
|
const content = await file.text();
|
||||||
editor.handle.openDocumentFile(file.name, content);
|
const documentName = file.name.slice(0, -graphiteFileSuffix.length);
|
||||||
|
editor.handle.openDocumentFile(documentName, content);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -438,9 +438,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// When we eventually have sub-documents, this should be changed to import the document instead of opening it in a separate tab
|
// When we eventually have sub-documents, this should be changed to import the document instead of opening it in a separate tab
|
||||||
if (file.name.endsWith(".graphite")) {
|
const graphiteFileSuffix = "." + editor.handle.fileExtension();
|
||||||
|
if (file.name.endsWith(graphiteFileSuffix)) {
|
||||||
const content = await file.text();
|
const content = await file.text();
|
||||||
editor.handle.openDocumentFile(file.name, content);
|
const documentName = file.name.slice(0, -graphiteFileSuffix.length);
|
||||||
|
editor.handle.openDocumentFile(documentName, content);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -83,9 +83,11 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (file.name.endsWith(".graphite")) {
|
const graphiteFileSuffix = "." + editor.handle.fileExtension();
|
||||||
|
if (file.name.endsWith(graphiteFileSuffix)) {
|
||||||
const content = await file.text();
|
const content = await file.text();
|
||||||
editor.handle.openDocumentFile(file.name, content);
|
const documentName = file.name.slice(0, -graphiteFileSuffix.length);
|
||||||
|
editor.handle.openDocumentFile(documentName, content);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -334,8 +334,11 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
|
||||||
editor.handle.pasteImage(file.name, new Uint8Array(imageData.data), imageData.width, imageData.height);
|
editor.handle.pasteImage(file.name, new Uint8Array(imageData.data), imageData.width, imageData.height);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (file.name.endsWith(".graphite")) {
|
const graphiteFileSuffix = "." + editor.handle.fileExtension();
|
||||||
editor.handle.openDocumentFile(file.name, await file.text());
|
if (file.name.endsWith(graphiteFileSuffix)) {
|
||||||
|
const content = await file.text();
|
||||||
|
const documentName = file.name.slice(0, -graphiteFileSuffix.length);
|
||||||
|
editor.handle.openDocumentFile(documentName, content);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -62,9 +62,16 @@ export function createPortfolioState(editor: Editor) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
editor.subscriptions.subscribeJsMessage(TriggerOpenDocument, async () => {
|
editor.subscriptions.subscribeJsMessage(TriggerOpenDocument, async () => {
|
||||||
const extension = editor.handle.fileSaveSuffix();
|
const suffix = "." + editor.handle.fileExtension();
|
||||||
const data = await upload(extension, "text");
|
const data = await upload(suffix, "text");
|
||||||
editor.handle.openDocumentFile(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");
|
const data = await upload("image/*", "both");
|
||||||
|
|
@ -76,8 +83,10 @@ export function createPortfolioState(editor: Editor) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// In case the user accidentally uploads a Graphite file, open it instead of failing to import it
|
// In case the user accidentally uploads a Graphite file, open it instead of failing to import it
|
||||||
if (data.filename.endsWith(".graphite")) {
|
const graphiteFileSuffix = "." + editor.handle.fileExtension();
|
||||||
editor.handle.openDocumentFile(data.filename, data.content.text);
|
if (data.filename.endsWith(graphiteFileSuffix)) {
|
||||||
|
const documentName = data.filename.slice(0, -graphiteFileSuffix.length);
|
||||||
|
editor.handle.openDocumentFile(documentName, data.content.text);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,11 +22,12 @@ export function downloadFile(filename: string, content: ArrayBuffer) {
|
||||||
downloadFileBlob(filename, blob);
|
downloadFileBlob(filename, blob);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function upload<T extends "text" | "data" | "both">(acceptedExtensions: string, textOrData: T): Promise<UploadResult<T>> {
|
// See https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input/file#accept for the `accept` string format
|
||||||
|
export async function upload<T extends "text" | "data" | "both">(accept: string, textOrData: T): Promise<UploadResult<T>> {
|
||||||
return new Promise<UploadResult<T>>((resolve, _) => {
|
return new Promise<UploadResult<T>>((resolve, _) => {
|
||||||
const element = document.createElement("input");
|
const element = document.createElement("input");
|
||||||
element.type = "file";
|
element.type = "file";
|
||||||
element.accept = acceptedExtensions;
|
element.accept = accept;
|
||||||
|
|
||||||
element.addEventListener(
|
element.addEventListener(
|
||||||
"change",
|
"change",
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
//
|
//
|
||||||
use crate::helpers::translate_key;
|
use crate::helpers::translate_key;
|
||||||
use crate::{EDITOR_HANDLE, EDITOR_HAS_CRASHED, Error, MESSAGE_BUFFER};
|
use crate::{EDITOR_HANDLE, EDITOR_HAS_CRASHED, Error, MESSAGE_BUFFER};
|
||||||
use editor::consts::FILE_SAVE_SUFFIX;
|
use editor::consts::FILE_EXTENSION;
|
||||||
use editor::messages::input_mapper::utility_types::input_keyboard::ModifierKeys;
|
use editor::messages::input_mapper::utility_types::input_keyboard::ModifierKeys;
|
||||||
use editor::messages::input_mapper::utility_types::input_mouse::{EditorMouseState, ScrollDelta, ViewportBounds};
|
use editor::messages::input_mapper::utility_types::input_mouse::{EditorMouseState, ScrollDelta, ViewportBounds};
|
||||||
use editor::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
use editor::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||||
|
|
@ -344,10 +344,10 @@ impl EditorHandle {
|
||||||
cfg!(debug_assertions)
|
cfg!(debug_assertions)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the constant `FILE_SAVE_SUFFIX`
|
/// Get the constant `FILE_EXTENSION`
|
||||||
#[wasm_bindgen(js_name = fileSaveSuffix)]
|
#[wasm_bindgen(js_name = fileExtension)]
|
||||||
pub fn file_save_suffix(&self) -> String {
|
pub fn file_extension(&self) -> String {
|
||||||
FILE_SAVE_SUFFIX.into()
|
FILE_EXTENSION.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update the value of a given UI widget, but don't commit it to the history (unless `commit_layout()` is called, which handles that)
|
/// Update the value of a given UI widget, but don't commit it to the history (unless `commit_layout()` is called, which handles that)
|
||||||
|
|
@ -421,7 +421,8 @@ impl EditorHandle {
|
||||||
#[wasm_bindgen(js_name = openDocumentFile)]
|
#[wasm_bindgen(js_name = openDocumentFile)]
|
||||||
pub fn open_document_file(&self, document_name: String, document_serialized_content: String) {
|
pub fn open_document_file(&self, document_name: String, document_serialized_content: String) {
|
||||||
let message = PortfolioMessage::OpenDocumentFile {
|
let message = PortfolioMessage::OpenDocumentFile {
|
||||||
document_name,
|
document_name: Some(document_name),
|
||||||
|
document_path: None,
|
||||||
document_serialized_content,
|
document_serialized_content,
|
||||||
};
|
};
|
||||||
self.dispatch(message);
|
self.dispatch(message);
|
||||||
|
|
@ -432,7 +433,8 @@ impl EditorHandle {
|
||||||
let document_id = DocumentId(document_id);
|
let document_id = DocumentId(document_id);
|
||||||
let message = PortfolioMessage::OpenDocumentFileWithId {
|
let message = PortfolioMessage::OpenDocumentFileWithId {
|
||||||
document_id,
|
document_id,
|
||||||
document_name,
|
document_name: Some(document_name),
|
||||||
|
document_path: None,
|
||||||
document_is_auto_saved: true,
|
document_is_auto_saved: true,
|
||||||
document_is_saved,
|
document_is_saved,
|
||||||
document_serialized_content,
|
document_serialized_content,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue