Skip to content

Improve save document semantics and add save as action #3034

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion desktop/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@ impl WinitApp {
String::new()
});
let message = PortfolioMessage::OpenDocumentFile {
document_name: path.file_name().and_then(|s| s.to_str()).unwrap_or("unknown").to_string(),
document_name: None,
document_path: Some(path),
document_serialized_content: content,
};
let _ = event_loop_proxy.send_event(CustomEvent::DispatchMessage(message.into()));
Expand Down
1 change: 1 addition & 0 deletions editor/src/consts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ pub const COLOR_OVERLAY_BLACK_75: &str = "#000000bf";

// DOCUMENT
pub const DEFAULT_DOCUMENT_NAME: &str = "Untitled Document";
pub const FILE_EXTENSION: &str = "graphite";
pub const FILE_SAVE_SUFFIX: &str = ".graphite";
pub const MAX_UNDO_HISTORY_LEN: usize = 100; // TODO: Add this to user preferences
pub const AUTO_SAVE_TIMEOUT_SECONDS: u64 = 1;
Expand Down
3 changes: 2 additions & 1 deletion editor/src/dispatcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -501,7 +501,8 @@ mod test {
);

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,
});

Expand Down
1 change: 1 addition & 0 deletions editor/src/messages/input_mapper/input_mappings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,7 @@ pub fn input_mappings() -> Mapping {
entry!(KeyDown(KeyA); modifiers=[Accel, Shift], canonical, 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, Shift], action_dispatch=DocumentMessage::SaveDocumentAs),
entry!(KeyDown(KeyD); modifiers=[Accel], canonical, 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 }),
Expand Down
1 change: 1 addition & 0 deletions editor/src/messages/portfolio/document/document_message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ pub enum DocumentMessage {
RenderRulers,
RenderScrollbars,
SaveDocument,
SaveDocumentAs,
SavedDocument {
path: Option<PathBuf>,
},
Expand Down
60 changes: 45 additions & 15 deletions editor/src/messages/portfolio/document/document_message_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::nodes::{CollapsedLayers, SelectedNodes};
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, FILE_SAVE_SUFFIX, SCALE_EFFECT, SCROLLBAR_SPACING, VIEWPORT_ROTATE_SNAP_INTERVAL};
use crate::messages::input_mapper::utility_types::macros::action_keys;
use crate::messages::layout::utility_types::widget_prelude::*;
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
Expand Down Expand Up @@ -76,8 +76,6 @@ pub struct DocumentMessageHandler {
/// 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.
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.
/// We save this to provide a hint about which version of the editor was used to create the document.
pub commit_hash: String,
Expand All @@ -104,6 +102,12 @@ pub struct DocumentMessageHandler {
// 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
#[serde(skip)]
breadcrumb_network_path: Vec<NodeId>,
Expand All @@ -116,9 +120,6 @@ pub struct DocumentMessageHandler {
/// Stack of document network snapshots for future history states.
#[serde(skip)]
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.
#[serde(skip)]
saved_hash: Option<u64>,
Expand Down Expand Up @@ -149,7 +150,6 @@ impl Default for DocumentMessageHandler {
// ============================================
network_interface: default_document_network_interface(),
collapsed: CollapsedLayers::default(),
name: DEFAULT_DOCUMENT_NAME.to_string(),
commit_hash: GRAPHITE_GIT_COMMIT_HASH.to_string(),
document_ptz: PTZ::default(),
document_mode: DocumentMode::DesignMode,
Expand All @@ -162,11 +162,12 @@ impl Default for DocumentMessageHandler {
// =============================================
// Fields omitted from the saved document format
// =============================================
name: DEFAULT_DOCUMENT_NAME.to_string(),
path: None,
breadcrumb_network_path: Vec::new(),
selection_network_path: Vec::new(),
document_undo_history: VecDeque::new(),
document_redo_history: VecDeque::new(),
path: None,
saved_hash: None,
auto_saved_hash: None,
layer_range_selection_reference: None,
Expand Down Expand Up @@ -918,7 +919,20 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
responses.add(OverlaysMessage::Draw);
}
DocumentMessage::RenameDocument { new_name } => {
self.name = new_name;
self.name = new_name.clone();

// If the new document name does not match the current path, clear the path.
if let Some(path) = self.path.as_ref() {
let document_name_from_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 Some(new_name) != document_name_from_path {
self.path = None;
}
}

responses.add(PortfolioMessage::UpdateOpenDocumentsList);
responses.add(NodeGraphMessage::UpdateNewNodeGraph);
}
Expand Down Expand Up @@ -991,25 +1005,40 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
multiplier: scrollbar_multiplier.into(),
});
}
DocumentMessage::SaveDocument => {
DocumentMessage::SaveDocument | DocumentMessage::SaveDocumentAs => {
if let DocumentMessage::SaveDocumentAs = message {
self.path = None;
}

self.set_save_state(true);
responses.add(PortfolioMessage::AutoSaveActiveDocument);
// Update the save status of the just saved document
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 {
document_id,
name,
name: self.name.clone() + FILE_SAVE_SUFFIX,
path: self.path.clone(),
content: self.serialize_document().into_bytes(),
})
}
DocumentMessage::SavedDocument { 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 => {
let selected_nodes = self.network_interface.selected_nodes();
Expand Down Expand Up @@ -1527,6 +1556,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
Noop,
Redo,
SaveDocument,
SaveDocumentAs,
SelectAllLayers,
SetSnapping,
ToggleGridVisibility,
Expand Down
26 changes: 18 additions & 8 deletions editor/src/messages/portfolio/menu_bar/menu_bar_message_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,14 +99,24 @@ impl LayoutHolder for MenuBarMessageHandler {
..MenuBarEntry::default()
},
],
vec![MenuBarEntry {
label: "Save".into(),
icon: Some("Save".into()),
shortcut: action_keys!(DocumentMessageDiscriminant::SaveDocument),
action: MenuBarEntry::create_action(|_| DocumentMessage::SaveDocument.into()),
disabled: no_active_document,
..MenuBarEntry::default()
}],
vec![
MenuBarEntry {
label: "Save".into(),
icon: Some("Save".into()),
shortcut: action_keys!(DocumentMessageDiscriminant::SaveDocument),
action: MenuBarEntry::create_action(|_| DocumentMessage::SaveDocument.into()),
disabled: no_active_document,
..MenuBarEntry::default()
},
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![
MenuBarEntry {
label: "Import…".into(),
Expand Down
9 changes: 6 additions & 3 deletions editor/src/messages/portfolio/portfolio_message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use crate::messages::prelude::*;
use graphene_std::Color;
use graphene_std::raster::Image;
use graphene_std::text::Font;
use std::path::PathBuf;

#[impl_message(Message, Portfolio)]
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
Expand Down Expand Up @@ -68,18 +69,20 @@ pub enum PortfolioMessage {
NextDocument,
OpenDocument,
OpenDocumentFile {
document_name: String,
document_name: Option<String>,
document_path: Option<PathBuf>,
document_serialized_content: String,
},
ToggleResetNodesToDefinitionsOnOpen,
OpenDocumentFileWithId {
document_id: DocumentId,
document_name: String,
document_name: Option<String>,
document_path: Option<PathBuf>,
document_is_auto_saved: bool,
document_is_saved: bool,
document_serialized_content: String,
to_front: bool,
},
ToggleResetNodesToDefinitionsOnOpen,
PasteIntoFolder {
clipboard: Clipboard,
parent: LayerNodeIdentifier,
Expand Down
34 changes: 29 additions & 5 deletions editor/src/messages/portfolio/portfolio_message_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use super::document::utility_types::network_interface;
use super::spreadsheet::SpreadsheetMessageHandler;
use super::utility_types::{PanelType, PersistentData};
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::debug::utility_types::MessageLoggingVerbosity;
use crate::messages::dialog::simple_dialogs;
Expand Down Expand Up @@ -408,12 +408,14 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
}
PortfolioMessage::OpenDocumentFile {
document_name,
document_path,
document_serialized_content,
} => {
let document_id = DocumentId(generate_uuid());
responses.add(PortfolioMessage::OpenDocumentFileWithId {
document_id,
document_name,
document_path,
document_is_auto_saved: false,
document_is_saved: true,
document_serialized_content,
Expand All @@ -428,6 +430,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
PortfolioMessage::OpenDocumentFileWithId {
document_id,
document_name,
document_path,
document_is_auto_saved,
document_is_saved,
document_serialized_content,
Expand All @@ -439,10 +442,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
let document_serialized_content = document_migration_string_preprocessing(document_serialized_content);

// Deserialize the document
let document = DocumentMessageHandler::deserialize_document(&document_serialized_content).map(|mut document| {
document.name.clone_from(&document_name);
document
});
let document = DocumentMessageHandler::deserialize_document(&document_serialized_content);

// Display an error to the user if the document could not be opened
let mut document = match document {
Expand Down Expand Up @@ -503,6 +503,30 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
document.set_auto_save_state(document_is_auto_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
self.load_document(document, document_id, responses, to_front);
}
Expand Down
6 changes: 4 additions & 2 deletions frontend/wasm/src/editor_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -425,7 +425,8 @@ impl EditorHandle {
#[wasm_bindgen(js_name = openDocumentFile)]
pub fn open_document_file(&self, document_name: String, document_serialized_content: String) {
let message = PortfolioMessage::OpenDocumentFile {
document_name,
document_name: Some(document_name),
document_path: None,
document_serialized_content,
};
self.dispatch(message);
Expand All @@ -436,7 +437,8 @@ impl EditorHandle {
let document_id = DocumentId(document_id);
let message = PortfolioMessage::OpenDocumentFileWithId {
document_id,
document_name,
document_name: Some(document_name),
document_path: None,
document_is_auto_saved: true,
document_is_saved,
document_serialized_content,
Expand Down